Exploiting 'x_relay's for scriptless timers...

This forum is for the Lua scriptable clone of DM/CSB called Dungeon Strikes Back by Sophia. Use DSB to build your own highly customised games.

Moderator: Sophia

Forum rules
Please read the Forum rules and policies before posting.
Post Reply
Remy
Craftsman
Posts: 111
Joined: Wed Sep 05, 2007 5:24 pm
Contact:

Exploiting 'x_relay's for scriptless timers...

Post by Remy »

While making the editor, I've been 'testing' it's usability by trying to recreate the original DM dungeon. While I'm not hugely worried about accuracy (there's already a DSB DM dungeon available, why make another?), I've been trying to at least get the mechanics working.
From what I've read, the 'x_relay' arch was created so that porting FTL's dungeons through RTSB could be simplified. Because of that, and it's supposed 'limited use in from-scratch DSB dungeons', I've tried to avoid using it as much as possible. But there are a couple of cases when I've found it useful.

Most triggers* in DSB work in a specific way - when they are 'operated', they fire a message at a target (both the message and the target(s) are specified by exvars). How a trigger gets operated is dictated by various exvars (the 'opby's, but 'air' and 'air_only' fall into this group as well), though most deal with something the player does.
'x_relay's are different in that they are operated by messages**, which means they can do some nifty things that triggers normally can't. I've found them useful in two situations in particular.

The first is in sending multiple messages of different types*** (such as different message values or different delays). For example, if you want the player to push a button to activate a teleporter, then want the teleporter to deactivate after a short delay, you can use an 'x_relay'.
When the button is operated, it fires an M_ACTIVATE message to both the x_relay and the teleporter. The 'x_relay' has it's exvars set to fire a M_DEACTIVATE with a delay of 10 at the teleporter. The result is that when the button is pressed, the teleporter activates and the relay is operated, firing it's own message (M_DEACTIVATE) with a 10 tick delay, shutting the teleporter back down.

The second case is if you want to start a message loop; because an 'x_relay' is operated by messages, it can operate itself, setting up a loop. This is useful if you want a continuous operation - such as a door that goes up and down over and over, and the player has to race beneath it.
To set this up, we'd create a door and an 'x_relay', as well as a floor trigger for our "starter". The floor trigger fires an M_ACTIVATE message at the x_relay to initiate the loop. The x_relay is set to fire a M_TOGGLE message at both itself and the door, with a delay of 6. So, when the floor trigger message arrives, it fires those messages - after 6 ticks, the door toggles, and the x_relay is operated again, firing two new messages.

* - Triggers include lots of things in DSB; almost every wall decoration I tested is, in fact, a trigger in disguise. That isn't true with floor decorations, but the invisible floor trigger can be used with anything on top of it.

** - The one message that doesn't work with 'x_relay's is M_DEACTIVATE. This shuts down the x_relay without it operating. Of course, if it's in a messaging loop, it'll recieve an M_TOGGLE message later and reactivate.

*** - This can be duplicated with two triggers being operated at the same time, such as two floor triggers on the same tile or two buttons overlapping on a wall. I don't like this method, because it's harder to see what's going on by just looking at it in the editor (you'd have to select the tile to see it has two buttons or triggers), but it's useful if you need to fire a M_DEACTIVATE message first, since that would normally shut an 'x_relay' down. If you do it this way, be sure to silence one of the triggers (if they make a noise), since two will cause a wierd "echo".


I should note that I'm unsure how stable the message looping is; that is, I don't know what would happen if you had lots of these going on, and the message queue gets over-crowded. I know there are certain archtypes that use looping (like monster generators), but I'm unclear of the actual mechanics of how DSB decides to pay attention to them (do generators work on levels the player is not located?).
Obviously, the best way to accomplish this sort of thing is with scripting, but as I was trying to avoid that, I thought I'd point these out. (I did say the editor could recreate the original DM dungeon without scripting, after all, and I know some people are going to try to do just that sort of thing).
User avatar
Sophia
Concise and Honest
Posts: 4240
Joined: Thu Sep 12, 2002 9:50 pm
Location: Nowhere in particular
Contact:

Re: Exploiting 'x_relay's for scriptless timers...

Post by Sophia »

I'm going to be totally blunt: I don't like "x_relay." I feel it should not be used in new dungeons. (That is, dungeons created specifically for DSB, rather than converted from DM/CSB/RTC/whatever) The x_ in front of its name is there because it's a hack, and sticking an x_ in front of the name seemed a good way to set it apart.
RemyR wrote:From what I've read, the 'x_relay' arch was created so that porting FTL's dungeons through RTSB could be simplified. Because of that, and it's supposed 'limited use in from-scratch DSB dungeons', I've tried to avoid using it as much as possible. But there are a couple of cases when I've found it useful.
You're pretty much correct. It was actually created to emulate RTC's WALLITEM_RELAY. RTC, like original DM, has no scripting ability, so to create any sort of complicated effects, complex chains of triggers, relays, etc., are needed. Both of the uses of "x_relay" you've described below are used quite commonly in RTC dungeons for creating the very effects you described.

My intention in DSB was to get away from the "apples and teleporters" school of thought, though. There's nothing inherently wrong with it-- it's just that the dungeon turns into a twisted mess of esoteric hacks very, very quickly.

The fact remains, though-- right now, there are still some good uses for "x_relay." I think the solution would be to come up with a more "DSB-spirted" way of doing it, though, rather than using lots of "x_relay" insts.
RemyR wrote:The first is in sending multiple messages of different types (such as different message values or different delays). For example, if you want the player to push a button to activate a teleporter, then want the teleporter to deactivate after a short delay, you can use an 'x_relay'.
It seems like changing the base code to support a message type and a delay that are also stored as tables just like the targets would be a simpler, more direct approach, and eliminate the need for an "x_relay" here.

So something like:

Code: Select all

target = { targ_1, targ 2}
msg = { M_ACTIVATE, M_DEACTIVATE }
delay = { 0, 12 }
would send a M_ACTIVATE to targ_1 immediately, and send a M_DEACTIVATE to targ_2 after 12 ticks.
(This doesn't work in DSB 0.20, but it will in DSB 0.21 unless we find something significantly wrong with this idea)
RemyR wrote:The second case is if you want to start a message loop; because an 'x_relay' is operated by messages, it can operate itself, setting up a loop. This is useful if you want a continuous operation - such as a door that goes up and down over and over, and the player has to race beneath it.
This one is a bit trickier. Objects in DSB can handle messages however the designer chooses, of course, so the door itself could be made to re-activate itself as needed, but, that's not the most generic solution, and it also requires custom code-- not that that's a problem, as most DSB dungeons will, but for something this simple, it seems like there should be an easy, elegant way. A recurring exvar comes to mind, but, of course, there'd have to be a way to stop that loop, too... that's also a problem with "x_relay," mind you.

I may have to give this one more thought!
RemyR wrote:I should note that I'm unsure how stable the message looping is; that is, I don't know what would happen if you had lots of these going on, and the message queue gets over-crowded. I know there are certain archtypes that use looping (like monster generators), but I'm unclear of the actual mechanics of how DSB decides to pay attention to them (do generators work on levels the player is not located?).
It's perfectly stable. DSB dynamically resizes the message queue as needed, so its length is limited only by available memory.
To answer the second question, monster generators will work whenever activated. Original DM did a lot of suppression of timers and such for levels the party wasn't on, in order to save the already overtasked CPU; this concern doesn't apply to DSB so timers generated by a dsb_msg will run no matter where in the dungeon their associated instance is.
User avatar
Joramun
Mon Master
Posts: 925
Joined: Thu May 25, 2006 7:05 pm
Location: The Universe

Post by Joramun »

I think x_relay and x_counter are ugly, because they involve using a dungeon instance to control the behavior of other instances, instead of using a script.

But their good point is that they provide a point and click way of designing mechanism, that gets well with the design of an editor.

The good point of x_relay, is that to kill a message loop, you just need to destroy the relay, and it can save some time writing control scripts.
The good point of x_counter, is that it provides an all-made counter, and a lot of riddles actually need counting something.

As for implementing a "recurring" message, I have a proposal :

In combination with the above multiple message design, the addition of :

M_TRIGGER : force execution of got_triggered for target instance
M_UNTRIGGER : force execution of got_untriggered for target instance
Add a mechanical exvar :
is_mute (or whatever), if this exvar is set to true, the instance won't send any further message (disables the execution of "got_triggered" and "got_untriggered")
M_MUTE sets is_mute to true
M_UNMUTE sets is mute to nil

It allows to control a loop, and to interrupt it without destroying the instance.
There might be some subtleties to take care of
( e.g. a constant_weight pad, receiving a M_MUTE message would be forever activated with the current design...)
But that's the downside (or is it ;) of lua : if people want to do bad things with the engine, they can.
What Is Your Quest ?
User avatar
Parallax
DMwiki contributor
Posts: 424
Joined: Mon Aug 28, 2006 7:56 pm
Location: Back in New Jersey

Post by Parallax »

I have been wondering about a general porblem that fits well in the x_relay discussion, so I'll submit it here.

Let's take Sophia' example of the exvars as tables.
Let's also assume that both targets are the same, and what we want to do is to activate the target for a limited but significant time dt.
If the activation mechanism is triggered once, at time t1, then triggered again before its first expiration, at time t2<t1+dt, what happens?

What I might want for it to happen might be that the timer is reset so that the target remains activated all the way to t2+dt. In practice, however, that does not work. The timeline is as follows:
t1: activation, the target is activated. A deactivation message is scheduled for t1+dt.
t2: activation again, nothing happens since the target is already active. A deactivation message is sent for t2+dt.
t1+dt: deactivation message reaches the target. Target deactivated.
t2+dt: second deactivation has no effect.

In other words, everything happens as if the second activation never took place. My question is, how would you modify this system so that the second activation 'cancels' the first deactivation? I think I had this discussion once with Sophia in FC, but the answer at the time involved unique flags for messages and I have no idea I would implement it.
User avatar
Sophia
Concise and Honest
Posts: 4240
Joined: Thu Sep 12, 2002 9:50 pm
Location: Nowhere in particular
Contact:

Post by Sophia »

Joramund wrote:The good point of x_relay, is that to kill a message loop, you just need to destroy the relay, and it can save some time writing control scripts.
Well, that works once. ;)
Destroying the relay isn't the best solution if you want to be able to turn the loop on and off at will.
Joramund wrote:The good point of x_counter, is that it provides an all-made counter, and a lot of riddles actually need counting something.
True, but also remember that there is a count exvar provided for most simpler applications of this.
Joramund wrote:M_TRIGGER : force execution of got_triggered for target instance
M_UNTRIGGER : force execution of got_untriggered for target instance
Not bad, though, practically speaking, I wonder how well it'd work with opbys and such... it's definitely something to think about, though.
Parallax wrote:What I might want for it to happen might be that the timer is reset so that the target remains activated all the way to t2+dt. In practice, however, that does not work. The timeline is as follows:
t1: activation, the target is activated. A deactivation message is scheduled for t1+dt.
t2: activation again, nothing happens since the target is already active. A deactivation message is sent for t2+dt.
t1+dt: deactivation message reaches the target. Target deactivated.
t2+dt: second deactivation has no effect.
I'd implement some kind of activation counting system:

t1: activation, the target is activated. A deactivation message is scheduled for t1+dt. The activation count is increased to 1.
t2: activation again, nothing happens, but the count is increased to 2.
t1+dt: deactivation message reaches the target. The count is reduced to 1.
t2+dt: second deactivation reduces the count back to 0, and the target is deactivated.

For many objects, this behavior is not desirable, as they should switch on and off on the first message they get regardless of any superfluous messages, so I'm not going to change the base code for everything-- it might be a worthwhile option if it can be folded in without making the system (very much) uglier, though.
User avatar
Parallax
DMwiki contributor
Posts: 424
Joined: Mon Aug 28, 2006 7:56 pm
Location: Back in New Jersey

Post by Parallax »

Oooh, a count exvar. Yep, that would work, and it's simple. Thanks!
Remy
Craftsman
Posts: 111
Joined: Wed Sep 05, 2007 5:24 pm
Contact:

Post by Remy »

It seems like changing the base code to support a message type and a delay that are also stored as tables just like the targets would be a simpler, more direct approach, and eliminate the need for an "x_relay" here.
I actually tried to write a script to do this (sort of), but I got stuck on a particular question. if this

Code: Select all

target = {targ_1, targ_2}
msg = M_TOGGLE
delay = 0
Is now going to be a shorthand way of writing:

Code: Select all

target = {targ_1, targ_2}
msg = {M_TOGGLE, M_TOGGLE}
delay = {0, 0}
I have to wonder what this:

Code: Select all

target = {targ_1, targ_2}
msg = M_TOGGLE
delay = {0, 12}
should do. Because either it should send two messages, or four messages. The way I have my script setup, this will fire two messages (an M_TOGGLE at targ_1 with a 0 delay and an M_TOGGLE at targ_2 with a 12 delay). And with this:

Code: Select all

target = targ_1
msg - M_TOGGLE
delay = {0, 12}
*EDIT*
Now that I think about it, my script for got_triggered never ignores a tabled 'delay' . What breaks it is if there are tables in both 'target' and 'delay' and their number of elements aren't the same. This:

Code: Select all

target = {targ_1, targ_2, targ_3}
msg = M_TOGGLE
delay = {0, 12}
will break it. But then, what should this be doing, anyway?
True, but also remember that there is a count exvar provided for most simpler applications of this.
I'm planning on removing the 'x_counter' from the editor's Archtype Library, for the same reason I don't use a 'translate_dungeon' call at the end of generated code: it leads to un-DSB ways of doing things (both can, of course, be added by the designer).
Not bad, though, practically speaking, I wonder how well it'd work with opbys and such... it's definitely something to think about, though.
That worries me, too, because you'd need a new variable to tell the instance it can only be triggered by it's 'opby's, it can only be triggered by 'M_TRIGGER's, or both.
Here's why: let's say you have two pads (Pad_1, and Pad_2) and a door (Door). Pad_1 gets stepped on - it triggers, firing an M_UNMUTE at itself (I'll explain why in a moment) first, an M_TOGGLE at Door, and an M_TRIGGER at itself with a delay (we'll say 10). 10 ticks later, it's supposed to receive M_TRIGGER and repeat those messages.
But what happens if the player steps on Pad_1 again? Another loop starts. Let's say they do it 5 ticks into the first loop. Now, Door is recieving M_TOGGLEs every 5 ticks. If they step on it again (and again), the Door is going to go haywire (I once did this with an 'x_relay' loop).
So, we need a way to disable Pad_1 from being triggered by it's opbys, without killing it with disable_self (which eliminates it's target exvar, rendering it useless). We also have to not totally disable it (with dsb_disable), because it still needs to react to M_TRIGGER. So we 'switch' it to M_TRIGGER_ONLY_MODE mode when it gets triggered. That's another message that must be included with got_triggered.
Now, the player makes it by Door, and steps on Pad_2. Pad_2 fires an M_MUTE at Pad_1 to stop it looping, and also fires a message to put it in OPBY_ONLY_MODE, because it'll still have M_TRIGGERs in the Message Queue. This also lets the Player go back and step on Pad_1, starting the loop again. That's why it needs to M_UNMUTE itself, by the way.

I scripted (though, it remains untested) a very simple msg_generator archtype, that basically functions like a monster_generator except that it calls dsb_msg instead of dsb_spawn. Well, actually it calls got_triggered, since that includes all the nifty code for multiple targets and such, but it's basically the same idea. The nice part about it is that I could have expanded it to include an 'mcounter' - so that you could specify the number of times it loops, and it can be deactivated with a simple M_DEACTIVATE message. It doesn't have the 'x_relay' problem of not being able to use this message, since it doesn't ever fire it's exvar.msg at itself.
Unfortunately, it doesn't encapsulate the ability of being triggered by messages in triggers, but it also doesn't require what I found ugly about 'x_relay' in the first place - the hackish need to send messages to itself (the M_TRIGGER method still needs this - as well as a few other messages that need to passed around).
User avatar
Sophia
Concise and Honest
Posts: 4240
Joined: Thu Sep 12, 2002 9:50 pm
Location: Nowhere in particular
Contact:

Post by Sophia »

RemyR wrote:I have to wonder what this:

Code: Select all

target = {targ_1, targ_2} 
msg = M_TOGGLE 
delay = {0, 12}

should do.
As I have it implemented now, it will send two messages:
M_TOGGLE to targ_1 after no delay, and M_TOGGLE to targ_2 after a delay of 12.
RemyR wrote:What breaks it is if there are tables in both 'target' and 'delay' and their number of elements aren't the same.
I don't think there's really any problem with not handling this intelligently because there's not really an intelligent way to handle it.

My implementation expects that if target is a table, then delay and msg will also be tables of the same length, or a single argument, which is taken as shorthand for a table of equal size of all the same thing. (For example, in this case, { M_TOGGLE, M_TOGGLE })

I think that's reasonable enough. :)
RemyR wrote:let's say you have two pads (Pad_1, and Pad_2) and a door (Door). Pad_1 gets stepped on - it triggers, firing an M_UNMUTE at itself (I'll explain why in a moment) first, an M_TOGGLE at Door, and an M_TRIGGER at itself with a delay (we'll say 10). 10 ticks later, it's supposed to receive M_TRIGGER and repeat those messages.
But what happens if the player steps on Pad_1 again? Another loop starts.
Actually, it's even stickier than that. Your version assumes that M_TRIGGER has a sane handling of the tc exvar, and I'm not even sure how that should work. If M_TRIGGER raises the trigger count, the situation can get very sticky very fast for the designer. If it doesn't, then-- well, what then? Does every M_TRIGGER assume an immediate untrigger afterwards? Or does it just trigger and not affect the trigger count, which makes things a bit weird if the party is standing on the trigger! How should this affect constant weight?

It starts to make "x_relay" seem sane, really! ;)
RemyR wrote:the hackish need to send messages to itself
I wouldn't file this one away as inherently bad.
Doors send themselves messages to continue to go up or down. Clouds send themselves messages to disappear or reduce in size. There are uses for it. In this case, though, you're probably right-- there has to be a better way. Your "msg_generator" seems like it might be something a bit more 'DSBish' but still offering a similar functionality.
User avatar
Joramun
Mon Master
Posts: 925
Joined: Thu May 25, 2006 7:05 pm
Location: The Universe

Post by Joramun »

I like this idea of a msg generator.

With enough parameters, it would provide an "encapsulated" control script, very good for an editor.
Even with the ability to script and copy&paste in Lua, I think a few simple thing should be doable in the editor,
and stuff like the counters in DM or CSB should have a reasonable way of being reproduced.

msg_generator should definetely start sending messages when activated, and stop at once when deactivated. It should not be able to enter into multiple loops like x_relay can do... unless the designer want it of course.

In order to be useful, such a generator need a lot of control variables :
- [ target, msg, delay ] to define one "run"
- [ count, interval, disable_self ... ]
- running (to track if the generator is running and shouldn't start another thread), run_count (to keep track of the number of runs, in case one wants to restart the generator with the same parameters later in the game)
etc.

EDIT : also, I don't exactly know how things are timed in dsb...
- what happens if I call a function in dungeon.lua just after dsb_party_place() and this function calls dsb_msg_chain or something like that, to make timed events ?
- what is the cleanest way to make a series of "absolutely-timed" events in a dungeon ?
What Is Your Quest ?
Remy
Craftsman
Posts: 111
Joined: Wed Sep 05, 2007 5:24 pm
Contact:

Post by Remy »

Well, currently the 'msg_generator' is broken - I think this is actually another issue (and I'll start another thread in a moment), but to get through a few of your suggestions...
In order to be useful, such a generator need a lot of control variables :
- [ target, msg, delay ] to define one "run"
- [ count, interval, disable_self ... ]
- running...
Well, because I just have the msg_generator call got_triggered, it uses target, msg, delay, count, and disable_self automatically. I'm not sure what 'interval' is, but if you mean the time between got_triggered calls, it uses the 'regen' exvar. Which means that 'regen' and 'delay' will stack, which might not be what you want. Techncally, it uses the GF_INACTIVE flag to determine whether it can operate, so that multiple messages don't start multiple loops (I'll bring this up in the other thread).
what happens if I call a function in dungeon.lua just after dsb_party_place() and this function calls dsb_msg_chain or something like that, to make timed events ?
You can do a dsb_party_place whenever you feel like, as long as the 'place' you put the party exists - that is, the level has been created. The editor used to place the party right after it generated the level they were starting on, then spawned all the instances (and made 'dsb_msg_chain' calls) and exvars,
then added champions to the party, and it worked just fine. You could, in fact, use multiple 'dsb_party_place's to move the party around during creation, but I really couldn't fathom why (this might be useful to do later - if, for example, you had an 'Event Screen' where the player could choose his starting place in the dungeon - but that's rather pointless during the compiling of the dungeon).
- what is the cleanest way to make a series of "absolutely-timed" events in a dungeon ?
That depends on how you define "clean". :)
I'd do it with a global variable and override 'sys_tick', but I'm pretty cavalier about overriding base functions to get things to work. I also don't know how well that would work, since I just realized I don't know if sys_tick starts "ticking" when you first enter the dungeon, or when DSB first loads the dungeon, which might be bad. (I think the proper way would be with an archtype that has a message handler for, say M_NEXTTICK - or, even better, your own Message Values - and have a trigger at the dungeon's starting square that sends out a series of M_NEXTTICKs (or, your own message values) to it, with the proper delays, but I don't know if those get saved with a savegame).

And, back up to Sophia:
I wouldn't file this one away as inherently bad.
I should rephrase - I don't like the fact that the designer has to make instances send themselves messages to get a particular function to work correctly. That, to me, says you're better off designing something to handle that particular function. Doors, clouds, and even my msg_generator send themselves messages - but it's not something you have to "add" to get functionality, it's encapsulated within the archtypes themselves.
User avatar
Sophia
Concise and Honest
Posts: 4240
Joined: Thu Sep 12, 2002 9:50 pm
Location: Nowhere in particular
Contact:

Post by Sophia »

RemyR wrote:
- what is the cleanest way to make a series of "absolutely-timed" events in a dungeon ?
That depends on how you define "clean". :)
As it happens, it also depends on how you define "absolute."
By default, DSB ticks along at 5 ticks per second, but, just like DM, it also generates an extra tick whenever the party takes a step. So, if the party is dashing along at a pretty good speed, there may be more than 5 ticks generated in a second.
RemyR wrote:but I don't know if those get saved with a savegame
They do. The only things in the message queue that don't get retained in a savegame are calls to dsb_delay_func, mostly because I don't have any clue how to save a reference to an arbitrary Lua function.
User avatar
Joramun
Mon Master
Posts: 925
Joined: Thu May 25, 2006 7:05 pm
Location: The Universe

Post by Joramun »

So, if I want to make an event happen, for exemple after rougly 10 minutes of game time,

I just have to make a dsb_msg(10*60*7,id,msg,data,[sender]) ?
There is no problem with saving a function this way :
Any function can be attached to a message handler :D

BTW, I don't think having a "savegame-leak" (dsb_delay_func) function is a good idea.
What Is Your Quest ?
User avatar
Sophia
Concise and Honest
Posts: 4240
Joined: Thu Sep 12, 2002 9:50 pm
Location: Nowhere in particular
Contact:

Post by Sophia »

Joramund wrote:So, if I want to make an event happen, for exemple after rougly 10 minutes of game time,

I just have to make a dsb_msg(10*60*7,id,msg,data,[sender]) ?
That might be close if the party is dashing around a lot.
10*60*5 would be a better formula if they're just standing still. :)
Joramund wrote:BTW, I don't think having a "savegame-leak" (dsb_delay_func) function is a good idea.
Sometimes it's nice to have. In particular, dsb_delay_func timers continue to run even while the game is locked (it is thus impossible to save, eliminating the problem), so they're very useful for "cut scenes." For example, the fusing Chaos animation is handled by a series of dsb_delay_funcs.
Post Reply