The Dink Network

magicman's rant about scripting techniques

April 30th 2014, 01:40 PM
custom_magicman.gif
magicman
Peasant They/Them Netherlands duck
Mmmm, pizza. 
I've meant to write something like this for a long time, as you may have read in Dinkerview #8. It is about some neat DinkC techniques that may help you to organize your code. Yeah, it's a stretch, I know. And it turns out that, even if you give it your best, the engine will still bite you. Everything in here applies to 1.08, which is the engine that I know best. I don't know about HD, freedink, Aural+, or anything else.

First things first: most D-Mods won't have need for many of the techniques presented here. There are many ways to do something in DinkC, and the best way is whichever works for you. On the other hand, I've tried to make the material as accessible as possible for people with at least some D-Modding experience. A result of this is that it is quite a long read.

I hope at least some of this will be interesting to some of you.

One core principle of programming (if not *the* core principle) is that of abstraction. If there's a bunch of code that has a clearly defined function and appears often, you give it a name, and from then on you only use that name.
The greatest benefit of this is that once you've convinced yourself that the piece of code called "spawn_bananas" does indeed spawn bananas, you don't have to re-read that code whenever the name "spawn_bananas" is used. You read that name and think "Oh, right, that's the code that spawns bananas".
A side benefit is that otherwise irrelevant code doesn't clutter up the main thing you're trying to accomplish. If a boss dies in a spectacular way, with randomly positioned explosions somewhere around his corpse, do you really want to see the explosion code in there? Personally, I'd prefer seeing something like explosions() and be done with it, ready to deal with the item drops, possible death cutscene, setting of story variables, etc. Of course, opinions on this vary, and everyone has a style of their own.

There are different ways of doing that in DinkC:

Introducing: goto

goto is the simplest, but also most restricted, way of naming a piece of code in order to re-use it. You indicate the start of the piece of code with label:, and then you use goto label; to jump there. If you have a shop where you can buy a sword for 400 gold, bombs for 25, and potions for 50, you don't want to write the same say_stop("`4You don't have enough gold.",&shopkeep); for every item. Instead, you put that line after a label nogold:, and you say goto nogold; just after you checked if &gold is large enough.

It is also DinkC's only real looping construct. If the label occurs before the goto itself, you can repeat a piece of code many times. This occasionally leads to bugs, most of them involving wait() somehow.

Sadly, goto has two major drawbacks. The first is that it is a one-way-only jump. The script promptly forgets where it came from, so if you want to do something like the following contrived example:

void talk( void )
{
  freeze(1);
  freeze(&current_sprite);
  say_stop("What should I do?", 1);
  say_stop("`3Fetch me 20 bear butts!", &current_sprite);
  say_stop("`3Bears can be found in the forest north of here.", &current_sprite);
  say_stop("Wait, what? I didn't hear you correctly.", 1);
  say_stop("`%*sigh*", &current_sprite);
  say_stop("`3Fetch me 20 bear butts!", &current_sprite);
  say_stop("`3Bears can be found in the forest north of here.", &current_sprite);
  say_stop("Gotcha!", 1);
  unfreeze(&current_sprite);
  unfreeze(1);
  return;


you can't just put the lines for &current_sprite after a label, and have two gotos to it. Okay, I admit, that was a lie... sort-of. It is certainly possible to put a label resume: after the first goto, and have a variable that tracks how often those lines have been said. If it's the first time, goto resume;, if it's the second time goto gotcha;. Or something like that:

void talk( void )
{
  freeze(1);
  freeze(&current_sprite);
  int &var = 0;
  say_stop("What should I do?", 1);
  goto mission;
resume:
  say_stop("Wait, what? I didn't hear you correctly.", 1);
  say_stop("`%*sigh*", &current_sprite);
  goto mission;
gotcha:
  say_stop("Gotcha!", 1);
  unfreeze(&current_sprite);
  unfreeze(1);
  return;

mission:
  say_stop("`3Fetch me 20 bear butts!", &current_sprite);
  say_stop("`3Bears can be found in the forest north of here.", &current_sprite);
  if (&var == 0)
  {
    &var = 1;
    goto resume;
  }
  goto gotcha;
}


In my opinion, this makes for confusing code. You can't easily see the control flow of the code: when it reaches the first goto and it has done whatever the piece indicated by that label does, there is no guarantee at all that you'll ever get back to resume at all. You'll have to read the mission code again, or have remembered where it jumps under which conditions.

The second drawback of goto is that the code it refers to must be in the same file. In the original game, when you kill a weak enemy, there's a chance of it dropping a small heart. However, the code that actually makes the heart drop isn't in every enemy script. Something similar happens for golden hearts or potions inside boxes, chests, or barrels. Instead, the code for a container dropping a golden heart is just in one place, make.c, and the code for enemy item drops are in emake.c. The enemy and container scripts are somehow able to call code inside those scripts.

Introducing: external()

While goto is the go-to (har, har, har) tool for loops and those situations where we don't care to get back to where we came from, external() will jump back. Where goto used its own system to name code (the labels), external() uses procedures like those that are used by the engine. However, instead of predefined names like "void talk( void )" or "void hit( void )", you get to name them yourself. Here's the example with the bear butts again:

// hunter.c
void talk( void )
{
  freeze(1);
  freeze(&current_sprite);
  say_stop("What should I do?", 1);
  external("mission","butts");
  say_stop("Wait, what? I didn't hear you correctly.", 1);
  say_stop("`%*sigh*", &current_sprite);
  external("mission", "butts");
  say_stop("Gotcha!", 1);
  unfreeze(&current_sprite);
  unfreeze(1);
  return;
}

// mission.c
void butts( void )
{
  say_stop("`3Fetch me 20 bear butts!", &current_sprite);
  say_stop("`3Bears can be found in the forest north of here.", &current_sprite);
}


This code is more modular. You can think of external("mission","butts"); as "This is the part where Dink is being told about bear butts", and not worry about any of the details. Although there is still some oddness in it. We'll cover it later, but it has to do with the butts procedure apparently knowing about &current_sprite.

Putting the code in a different script is okay if many other scripts need it, but if it's just the one piece of code you want to give a name and only use it in this script, do we really need to create a separate file for it? Well, no. We can use external() with the current filename, but that seems a bit of a roundabout way of doing it. Turns out DinkC provides us with a solution once again, and we arrive at the third technique, which is actually just external() in disguise.

Introducing: my_fancy_procedure()

Turns out you can actually write new procedures in the same file, and then call them as if it was a normal DinkC function! Now you can have the named code in the same place as the code that calls it, just as with goto, and you don't need any extra commands to make sure the control flows back to right after the jump, just as with external():

// hunter.c
void talk( void )
{
  freeze(1);
  freeze(&current_sprite);
  say_stop("What should I do?", 1);
  butts();
  say_stop("Wait, what? I didn't hear you correctly.", 1);
  say_stop("`%*sigh*", &current_sprite);
  butts();
  say_stop("Gotcha!", 1);
  unfreeze(&current_sprite);
  unfreeze(1);
  return;
}

// Still in hunter.c
void butts( void )
{
  say_stop("`3Fetch me 20 bear butts!", &current_sprite);
  say_stop("`3Bears can be found in the forest north of here.", &current_sprite);
}  


In fact, and this is important: using myproc() is exactly the same as using external() with the current filename. I will likely repeat this later on, but both techniques result in exactly the same behaviour for every test I've done.

Of the versions of the "bear butt"-code, this last one is the style I'd recommend when dealing with repetitive code inside a single script. In this case, the example was a bit contrived, and the plain version without any fancy goto or external() would probably be my favourite. If you find yourself using the same code across multiple scripts, consider using external().

Fancy functions

There is one other major difference between goto and external()/myproc(): As of Dink 1.08, you can actually define your own functions that take arguments and return values with them. Before then, you could still mimick those by setting some global variables before calling external(), and also setting a global variable before using return. Sadly, calling procedures in this way is still distinguishable from using built-in functions, as you have to use the &return variable. Time for a demonstration by implementing one of my favourite missing functions in DinkC: the power operator.

// math.c
void pow( void )
{
  int &res = 1;

  // Look at this!
  // The arguments passed to external() after the procedure name are stored in &arg1 to &arg8.
  // When used as myproc() you can use up to &arg9 for Dink Engine reasons.
  int &count = &arg2;

loop:
  if (&count <= 0)
  {
    // Look at this!
    // This gives the value of &res back to wherever we came from.
    return(&res);
  }
  &count -= 1;
  &res *= &arg1;
  goto loop;
}


Now you can set &var to itself cubed by doing something like:

  // Look at this!
  // The parameters here that are passed to void pow( void ) are the value in &var and 3.
  // The result, from the line return(&res); will be in the &return variable.
  external("math","pow",&var,3);
  &var = &return;


Neat, huh? Sure, ideally you'd want something like &var = external("math","pow",&var,3);, but for some reason that's not how it works.

HEY MAGICMAN THAT CODE WILL CRASH THERE IS NO WAIT IN YOUR LOOP. We'll get to that in another rant. But first I have another point to get back to. Remember back when butts() knew about &current_sprite? Let's revisit and talk about the next issue: Which variables are in scope, and where. And, most importantly, why.

Variable scope

Variable scope is all about what variables can be used at which point in the script. The easiest to reason about are those defined with make_global_int(), these are always accessible after being initialized. After global variables are local variables. These are only accessible in the current script instance after they've been initialized with int &var;. Thirdly, there are pseudo-global variables. There are only a few of them, and they obey special rules.

Sounds easy, right? Just a matter of finding out if it's a global or pseudo-global for which the rules say they're usable, and otherwise check if it has been initialized in the current script instance.

He keeps saying script instance. Why doesn't he just say script... Well, let me demonstrate with a script attached to a sprite in the editor:

void main( void )
{
  int &hits = 0;
}

void talk( void )
{
  if (&hits > 1)
  {
// Comment out the next two lines for the goto-version
    crybaby();
    return;
// Uncomment the next line for the goto-version
//    goto crybaby;
  }
  say("`4Hello there.", &current_sprite);
}

void hit( void )
{
  &hits += 1;
// Comment out the next two lines for the goto-version
  crybaby();
  return;
// Uncomment the next line for the goto-version
//  goto crybaby;
}

void crybaby( void )
{
// Uncomment the next line for the goto-version
// crybaby:
  freeze(1);
  freeze(&current_sprite);
  say_stop("`4Go away go away go away! Waaaahhhhh!!", &current_sprite);
  say_stop("Don't be such a crybaby... I've only hit you &hits times.", 1);
  unfreeze(&current_sprite);
  unfreeze(1);
}


It's two scripts in one. The version as presented uses the custom procedure void crybaby( void ), and when you uncomment the crybaby: label and the goto-line, and comment out the call to crybaby(); and the return; statement, you get a version that uses goto. The interesting bit of this script is the variable &hits: It gets initialized in void main( void ), incremented in void hit( void ), tested against 0 in void talk( void ), and used in a say_stop() call in void crybaby( void ). One version works, one doesn't.

The goto-version works correctly. The crybaby() version will have the correct value for &hits in both void hit( void ) and void talk( void ), but it has no clue at all in void crybaby( void ). The key is in the difference between script and script instance.

As far as the game is concerned, a script itself is just the file. Whenever a script is run, in whatever way, a script instance is created. This script instance runs separately from other script instances, even when they're instances of the same script. For example, when you have three pillbugs on screen, all three of them have script en-pill, but they each run a separate script instance. Local variables defined in the pillbug script will not be shared with the other pillbugs.

So the trick is to check when new script instances are created. There is a simple way to distinguish them: use the variable &current_script, which is a number that identifies the current script instance. Like &current_sprite, you can't change its value:

// I suggest placing this in a key-# script.
void main( void )
{
  int &var = 1234;
  say_stop("Current script inside main is: &current_script . Var is &var", 1);
  proc();
  goto proc;
}

void proc( void )
{
proc:
  say_stop("Current script inside proc is: &current_script . Var is &var", 1);
}


When you run the script, you'll see Dink say three lines. In the first line, he says a script number, along with 1234. In the second line, he says a different script number, along with _var (pretend that 'v' is underlined). In the third line, he says the same script number as in the first line, along with 1234. Apparently a custom procedure call creates a new script instance.

On the other hand, didn't the crybaby example successfully use &current_sprite? This is true. However, &current_sprite is one of those special pseudoglobals for which there are completely different rules. &current_sprite inside a script instance belonging to a custom procedure call or external call takes over the value of &current_sprite of the script instance that did the call.

Next to &current_script, there are three other ways to get at a script number. The most straightforward method is is_script_attached(). In the main game, this is used in the fireball script, in order to make "secret trees" burn down and reveal a staircase. If you use spawn(), the script number of the spawned script is returned. Also, if you use sp_script() to attach a script to a sprite, the script number of the new script instance is returned.

Four ways of obtaining a script number. Other than determining if two pieces of code share the same variable scope, which is mostly useful for debugging, what else can we do with them?

Introducing: run_script_by_number()

run_script_by_number() is an interesting beast. The main game's fireball script uses the script number returned from is_script_attached() in run_script_by_number() in order to get the secret trees to burn down. Think of it as a remote-controlled goto. run_script_by_number(&script_number,"proc") will poke the script instance identified by &script_number, and tell it to start running void proc( void ) immediately. Let's have an example:

// person1.c
// Place this one in the editor
void main( void )
{
  int &person2 = create_sprite(200,200,0,231,1);
  // Look at this!
  // The return value of sp_script() is a script number.
  int &script2 = sp_script(&person2,"person2");
}

void talk( void )
{
  // Look at this!
  // We can use that script number in run_script_by_number()
  say_stop("`4Hey you, talk about something else for a bit!",&current_sprite);
  say_stop("`5Okay!",&person2);
  run_script_by_number(&script2,"toggle");
}

// person2.c
void main( void )
{
  int &topic = 0;
}

void talk( void )
{
  if (&topic == 0)
  {
    say("`5I really like ducks!",&current_sprite);
  }
  else
  {
    say("`5I really like pigs!",&current_sprite);
  }
}

void toggle( void )
{
  if (&topic == 0)
  {
    &topic = 1;
  }
  else
  {
    &topic = 0;
  }
}


If you enter the screen with these NPCs on it, and talk to person2, you hear something about ducks. Until you talk to person1. Then, person2 will talk about pigs. The run_script_by_number() inside person1's void talk( void ) runs the void toggle( void ) code in person2's script. Since it's actually the same script instance of person2.c, it's the same variable &topic.

Apart from allowing you to run a different bit of code, run_script_by_number() is completely different from external(). For one, you can't pass any extra arguments to it. You will have to set up some globals if you want that. Secondly, it happens concurrently. There is no way for person1 to know if person2 is still busy running void toggle( void ), unless you set up something yourself. The invoked procedure can run for a long time, have wait()s in it, and it and the caller will run at the same time.

(Okay, not really at the same time, as only one script really runs at once, but at the same time-ish enough. Read Someone's FIFO and variables in DinkC if you want to know more about how scripts are scheduled.)

For that reason, you also can't return a value from a piece of code that's invoked by run_script_by_number(). The caller may be long past the point where it was called!

Thirdly, the invoked script instance stops what it's doing, and will instead run the procedure. This may or may not be what you want, so be careful. In particular, run_script_by_number(&current_script,"proc"); is almost like a goto, except that it jumps to a procedure name, and not to a label.

// disabler.c
void main( void )
{
  int ⌖
  int &script;
}

void talk( void )
{
  &target = get_sprite_with_this_brain(16,0);
  &script = is_script_attached(&target);
  run_script_by_number(&script,"disable");
}

// npc.c
void main( void )
{
  sp_brain(&current_sprite,16);
  say_stop("`@In about 30 seconds, I'll shout!",&current_sprite);
  wait(30000);
  say_stop("`@SHOUT! SHOUT! LET IT ALL OUT!",&current_sprite);
}

void disable( void )
{
  say("`@Well, I guess not.",&current_sprite);
}


If you talk to the disabler while the NPC is still wait()ing, the NPC's script instance will immediately jump to void disable( void ), never to return again. Again, if you don't set up something yourself, there is no way to know if a script instance is currently running some code (be it wait() or say_stop() or whatever), or if it is idling between procedures. This is actually nothing new:

// npc.c
void main( void )
{
  say_stop("`@In about 30 seconds, I'll shout!",&current_sprite);
  wait(30000);
  say_stop("`@SHOUT! SHOUT! LET IT ALL OUT!",&current_sprite);
}

void talk( void )
{
  say_stop("Don't.",1);
  say_stop("`@Okay!",&current_sprite);
}


Talking to the sprite will also abort the wait(). When you talk to a sprite, the engine will do the equivalent of run_script_by_number(&script,"talk");.

Summary

That took a while, have a summary:

goto jumps to the indicated label, no questions asked.
Use this if you need a loop, or if many code paths need to join together. For example, the "not enough gold" and "no inventory space" messages in shops.

external() and myproc() runs some code in a different file (or in the same file, but a different procedure), and then jumps back. It accepts arguments, and can return values. The code in the callee does not know about local variables in the caller.
Use this if otherwise you would copy-paste some code, and change only a few numbers. Those numbers can be the arguments.

run_script_by_number() works with an already existing script instance, and makes it jump to a specific procedure, goto-style. You can not pass arguments or return values.
Use this if you want to influence other scripts without stopping the currently running script.

I hope you enjoyed this. If there's interest, I'll write up a second rant to dissect my Persistent Loot file, which makes use of several techniques listed here.

April 30th 2014, 02:26 PM
wizardg.gif
leprochaun
Peasant He/Him Japan bloop
Responsible for making things not look like ass 
Shifting myself over to start using custom procedures and externals more has so far been a very slow process. I don't think I've used run_script_by_number once either. I didn't realize you could return(&var); although I guess I should have. I just always thought of it as being a "close this script instance" command. Now I know better. Run_script_by_number is pretty cool. I doubt I'll end up having to use it though.

This was a very interesting read. Make another.
April 30th 2014, 02:38 PM
custom_coco.gif
Cocomonkey
Bard He/Him United States
Please Cindy, say the whole name each time. 
Uh oh. You didn't look at my "Malachi the Jerk" code and become so angry you had to set everybody straight, did you?

It's frustrating that calling a custom procedure in a script creates a new instance that knows nothing about the old instance. I ran into several strange problems with this.

I still don't really understand the point of run_script_by_number(), even though I'm sure I've run into something that required me to use it at least once.
April 30th 2014, 04:08 PM
custom_magicman.gif
magicman
Peasant They/Them Netherlands duck
Mmmm, pizza. 
Hah, no. I've had most of that rant on my PC since a week after I released Persistent Loot. I added the run_script_by_number() stuff today, the rest is all from way back when.

The point of run_script_by_number() is extending the engine. Ish. You know how it's built-in that void hit( void ) is run when a sprite is hit, and how void talk( void ) is run when you talk to it? run_script_by_number() will make a script run void burn( void ) when its sprite is being hit by a fireball, or you can make void bomb( void ) run when a bomb explodes nearby. This all through nothing but DinkC scripting.
April 30th 2014, 06:03 PM
pq_knight.gif
ExDeathEvn
Peasant He/Him New Zealand rumble
"Skinny Legend" 
This sort of information should greatly improve some Dmodding work! I new maybe a fraction of what was written here could be done and had started implementing it myself, but this just takes the cake.

STICKEY IT!
April 30th 2014, 06:04 PM
dragon.gif
Quiztis
Peasant He/Him Sweden bloop
Life? What's that? Can I download it?! 
UPDATE THIS MESS YOU CALL DINKC SETH!
May 1st 2014, 09:36 AM
slimeg.gif
metatarasal
Bard He/Him Netherlands
I object 
Nice rant about some of these techniques. I knew most of these things (except the entire run_script_by_number() thing) but I keep forgetting them when actually using DinkC. For the most part the things you do in Dink tend to be simple enough that it isn't worth bothering with the complexities of custom functions. Custom functions clearly are an afterthought and that's a shame...

From another perspective, in my dinkerview which is nearly three years ago already, I stated that I was contemplating a part two for my introduction to DMOD making tutorial. Seeing this almost makes me want to start writing, if not for the fact that my DinkC is extremely rusty...
May 1st 2014, 09:49 PM
peasantmb.gif
yeoldetoast
Peasant They/Them Australia
LOOK UPON MY DEFORMED FACE! 
This is the sort of thing I like to see of this board. Thanks for this magicman.
May 5th 2014, 12:26 PM
custom_magicman.gif
magicman
Peasant They/Them Netherlands duck
Mmmm, pizza. 
There seems to be some interest in more of these things, so I've started writing a rant about how and why I wrote Persistent Loot the way it is. I'll probably finish it later this week. Until then, have a mini-rant about a cool technique that you can use with "Fancy functions":

Introducing: Optional parameters

Let's start with a bit of code:

// stuff.c
void remove( void )
{
  int &ednum = sp_editor_num(&current_sprite);
  if (&ednum > 0)
  {
    editor_type(&ednum,1);
  }
  sp_nodraw(&current_sprite,1);
  sp_nohit(&current_sprite,1);
  sp_active(&current_sprite,0);
}


Now, a call to external("stuff","remove"); will remove &current_sprite from the game. Now, in a lot of cases, this is what you want. In some cases, I'd love to remove a different sprite from the game. So. What are my options?

Change &current_sprite to &arg1 in void remove( void ), then add &current_sprite as an argument everywhere I call it using external()? That seems like a good option, though I don't look forward to doing a big search-and-replace.

Not use external() at all for those few cases? It would work, but that kind of beats the purpose of separating out code.

In my opinion, the best solution to this problem is to consider what happens when you don't pass a parameter, but use &arg1 anyway: its value will be 0. Since 0 is never a valid sprite number, we can check for that, and deal with it accordingly. The code is now:

// stuff.c
void remove( void )
{
  // Set new variable (with descriptive name!) to default value.
  int &sprite = &current_sprite;
  if (&arg1 != 0)
  {
    // Oy, we actually have a value. Better set the variable to that.
    &sprite = &arg1;
  }

  int &ednum = sp_editor_num(&sprite);
  if (&ednum > 0)
  {
    editor_type(&ednum,1);
  }
  sp_nodraw(&sprite,1);
  sp_nohit(&sprite,1);
  sp_active(&sprite,0);
}


Alternatively, the first bit can set &sprite to &arg1, and only set it to &current_sprite if &arg1 turns out to be 0.

When designing procedures with default arguments, I recommend that you order the arguments by how often you expect to use a non-default value. For example, if you have a procedure with 3 arguments, of which the first is required, and the second and third have a default, you can pass in the first two, and the third will use the default value.

The main caveat: Occasionally, you may want to pass in 0! Sadly, external("stuff","remove"); and external("stuff","remove",0) do exactly the same thing, and are indistinguishable.

Also beware when entering variables, the following code may have dire consequences:

void talk( void )
{
  int &sprite = get_sprite_with_this_brain(9, &current_sprite);
  external("stuff","remove",&sprite);
}


If there are no brain 9 sprites on the screen, get_sprite_with_this_brain will return 0. This means that &current_sprite will be removed from the game!

Summary

If you don't pass an argument to a call to external(), its corresponding &arg1 to &arg9 variable will be 0. You can check for this, and do something sensible as default.

Beware when you want to pass 0, and for cases where you can accidentally pass 0.
May 9th 2014, 06:16 PM
custom_magicman.gif
magicman
Peasant They/Them Netherlands duck
Mmmm, pizza. 
In the previous post, I said that if there was enough interest, I'd write up a dissection of how Persistent Loot works. Instead of a dissection, I decided to do a write-up of the how and why I wrote the things I wrote. It's not meant as a tutorial on how to use it, but rather as a demonstration of the techniques of the previous rant.

Also, the scripts written in this particular rant are not the actual scripts of Persistent Loot, though they'll look similar. In particular, there are a lot of extra checks in Persistent Loot. I think these extra checks detract from this rant's primary goal, which is showing how to use the techniques introduced in the previous rant using a running example.

First, let's talk about the context. The problem we're trying to solve is this:

I want to be able to open a chest containing some gold, or a potion, or whatever, then walk off the screen, and then walk back, and the stuff should still be there.


We want the engine to remember things across screen changes. There are only so many ways in which that can be done. The simplest is using a global. This is all right for a single chest, but it'd mean you'd need a different global for every single container on the map, and all of them will need a slightly different script, reading those different globals.

When using supervars, you can store 31 containers in one global. You could probably put the container number in the editor as the chest's strength (it's not as if it uses that), and then one script will suffice. However, this means you need to remember, or write down somewhere, what chest numbers are in use. And make really sure that you don't use the same chest number twice, or things will be bad. And if you have more than 31 lootable containers, you need a second global. All this is really a lot of hassle, and while the supervar-juggling can be stowed away in an external() script, there is a more elegant way.

Enter Paul's scripting trick. I think that by now that trick is not as esotheric as it used to be. Basically, every sprite that has been placed in the editor, as long as the editor_type() of its sp_editor_num() is 0, can store one number from 0 to 255 in the editor_frame(), and one number from 0 to 65535 in the editor_seq(). This is stored in the save file, and remembered across screenchanges as well, so it can effectively be used as a global. The main limitation is that you need an editor sprite number on a specific screen, but chests can store if they've been opened, but the loot hasn't been picked up.

Let's write up the basic script skeleton. This is not actual DinkC, but more a description of what it will look like:

void main( void )
{
  int &opened = lookup if chest is open and loot is not picked up;
  if (&opened == 1)
  {
    draw open chest;
    create loot;
  }
}

void hit( void )
{
  play open chest animation;
  remember that chest has been opened;
  create loot;
}


There's a catch, though. Picking up the loot and then going away should not make the loot appear again when re-entering the screen. This means that the loot should somehow be aware of the chest it came out of. Even worse, it should set the editor_type(), editor_seq(), and editor_frame() of the chest to the appropriate values. Essentially, this means that the loot script should know about the type of chest it came out of. Having a different loot script for all three chests, for the two different crates, and the barrel? That's crazytalk. Having a chain of if statements based on the sp_pseq() of the container? That is also kind of silly. If you decide that there should be bombable rocks with golden hearts or potions in them, then you can add yet another if to decide what to do with the rock.

There is really only one place where you can sensibly put code that knows about how to deal with a specific type of container: the container script itself. As part of the create loot step, we somehow tell the loot what container it came out of. Then, after doing whatever picking up the loot does, we tell the container that it's no longer needed.

How to do this? If you guessed run_script_by_number(), you were right. One last thing, how exactly do we tell the loot about what it came out of? I'll use sp_custom() for that. You can use that to store just about any amount of information on a sprite. However, the values aren't stored in the savegame, or remembered across screen changes.

// Search for this to easily jump back to this code:
// __INITIAL_VERSION__
// container.c
void main( void )
{
  int &ednum = sp_editor_num(&current_sprite);
  int &opened = 0;
  if (&ednum > 0)
  {
    &opened = editor_frame(&ednum,-1);
    if (&opened == 1)
    {
      // Fill in appropriate numbers here.
      sp_pseq(&current_sprite, open chest sequence);
      sp_pframe(&current_sprite, open chest frame);
      goto createLoot;
    }
  }
}

void hit( void )
{
  if (&opened > 0) {
    return;
  }
  // Fill in appropriate number here
  sp_seq(&current_sprite, open chest animation);
  if (&ednum > 0)
  {
    editor_frame(&ednum, 1);
    &opened = 1;
  }
  goto createLoot;
}

void createLoot( void )
{
createLoot:
  // Fill in appropriate stuff here
  int &loot = create_sprite(blah, blah, blah, blah, blah);
  sp_script(&loot, blah);
  sp_custom("myContainer",&loot,&current_sprite);
}

void takeLoot( void )
{
  if (&ednum > 0)
  {
    // Fill in appropriate numbers here
    editor_type(&ednum, probably 1 2 3 4 or 5);
    editor_seq(&ednum, open chest sequence);
    editor_frame(&ednum, open chest frame);
  }
}

// loot.c
void main( void )
{
  // Any number of things, such as setting animation,
  // hardness. nohit, etc.
  sp_touch_damage(&current_sprite, -1);
}

void touch( void )
{
  sp_touch_damage(&current_sprite, 0);
  // Code here for what happens when you pick it up
  // Such as &gold += 500; sounds, everything.
  int &container = sp_custom("myContainer",&current_sprite,-1);
  int &contScript = is_script_attached(&container);
  run_script_by_number(&contscript,"takeLoot");
}


You can stop here if you wish. I personally was kind of annoyed at how most of the container script is boilerplate. If I were to add a new container script, I'd probably copy-paste most of it, and change only some numbers, and the loot creation. This seems like a waste... Time for a new skeleton:

void main( void )
{
  I am a container with these numbers(opening animation, open sequence, open frame)
}

void hit( void )
{
  open me if I'm not open already, you have my numbers
}

void createLoot( void )
{
createLoot:
  // Fill in appropriate stuff here
  int &loot = create_sprite(blah, blah, blah, blah, blah);
  sp_script(&loot, blah);
  sp_custom("myContainer",&loot,&current_sprite);
}


The primary goal here is to remove code that's common to all containers, so we don't want the bits in void main( void ) and void hit( void ) to be more than a couple of lines. The code in void takeLoot( void ) is common to all containers (once we know the numbers), so we'd like to be able to leave that out as well. However, something resembling all that code should still run. In my previous rant I argue that this is exactly when to use external().

Let's focus first on void main( void ). This is where we somehow tell the engine what the container-specific numbers are. I have identified four: the sequence to play when the chest opens, the sequence and frame of the opened chest graphic, and the editor_type() to use when the loot has finally been taken. Since script instances created with external() get killed automatically when they reach the end of a procedure, or a return statement, those numbers won't be in scope when we finally hit the chest. We've got to store them somewhere else. And where better to store those than in some sp_custom() properties on the container itself.

After this, there's just one more problem. The previous code had a goto createLoot. The code that corresponds to has to be specific per container, so it'll be in container.c. However, we can't goto from code in the external() script to there. run_script_by_number() to the rescue!

The first bit of code is here:

// library.c
void initConainer( void )
{
  // Store the arguments as sp_custom properties on the sprite.
  sp_custom("openanim",&current_sprite,&arg1);
  sp_custom("openedseq",&current_sprite,&arg2);
  sp_custom("openedfr",&current_sprite,&arg3);
  sp_custom("edtype",&current_sprite,&arg4);

  int &ednum = sp_editor_num(&current_sprite);
  int &opened = 0;
  if (&ednum > 0)
  {
    &opened = editor_frame(&ednum,-1);
    if (&opened == 1)
    {
      sp_pseq(&current_sprite, &arg3);
      sp_pframe(&current_sprite, &arg4);

      int &script = is_script_attached(&current_sprite);
      if (&script > 0)
      {
        run_script_by_number(&script,"createLoot");
      }
    }
  }
}

// container.c
void main( void )
{
  // Fill in appropriate numbers here
  external("library","initContainer",openanim,openedseq,openedfr,edtype);
}


As you can see, most of the old code has been copy-pasted into library.c ("library" is general programming terminology for external and re-usable bits of code). It makes heavy use of the fact that scripts called through external() still know about &current_sprite of the caller.

If everything's clear so far, let's proceed with void hit( void ). If not, make sure to compare the script marked "__INITIAL_VERSION__" (for your Ctrl-F convenience) with the version above. Also familiarize yourself with external(). For that, you can read my previous rant, or the "Advanced Procedures" chapter of dinkc.chm. (which mentions global procedures, but as far as I know, those are broken). Questions in the replies are also welcome.

Let's talk about void hit( void ). This will also call a procedure from library.c. We can start with just copy-pasting the code into library.c. The first thing it does is checking if the container has been opened already. Previously, I used a local for that, and relied on the fact that this local will be in scope in the entire script instance. This would be a problem, if it weren't for the fact that it was merely a copy of the editor_frame() of the container sprite. We can easily get at that, since we know about &current_sprite.

Next, we need to find the appropriate sequence to use for the animation. We stored that in an sp_custom() in void main( void ), so that's not a problem either. Finally, the goto createLoot can be worked around in the same way as we did in void initContainer( void ). Actually, let's put the createLoot code in there as well. Instead of a label, we need a procedure name. I sneakily took care of that in the __INITIAL_VERSION__ already.

This is what's new in the next version:

// library.c
void openContainer( void )
{
  int &ednum = sp_editor_num(&current_sprite);
  int &opened = editor_frame(&ednum,-1);

  if (&opened > 0) {
    return;
  }

  int &openanim = sp_custom("openanim",&current_sprite,-1);
  sp_seq(&current_sprite, &openanim);

  if (&ednum > 0)
  {
    editor_frame(&ednum, 1);
  }

  int &script = is_script_attached(&current_sprite);
  if (&script > 0)
  {
    run_script_by_number(&script,"createLoot");
  }
}

// container.c
void hit( void )
{
  external("library","openContainer");
}

void createLoot( void )
{
createLoot:
  // Fill in appropriate stuff here
  int &loot = create_sprite(blah, blah, blah, blah, blah);
  sp_script(&loot, blah);
  sp_custom("myContainer",&loot,&current_sprite);
}


Again, make sure you understand what's going on. The next part deals with taking the loot. Most of the code in loot.c can remain the same. The main thing is that, since I want to remove void takeLoot( void ) from container.c and put it in library.c, we can no longer use run_script_by_number(). In this case, it'll be replaced by a call to external(). But since that call originates from the loot, its &current_sprite is actually the loot's sprite number. Not to worry, because the loot knows about its container through an sp_custom() property.

The final bits are here:

// library.c
void takeLoot( void )
{
  int &container = sp_custom("myContainer",&current_sprite,-1);
  int &ednum = sp_editor_num(&container);
  if (&ednum > 0)
  {
    int &edtype = sp_custom("edtype",&container,-1);
    int &openedseq = sp_custom("openedseq",&container,-1);
    int &openedfr = sp_custom("openedfr",&container,-1);

    editor_type(&ednum, &edtype);
    editor_seq(&ednum, &openedseq);
    editor_frame(&ednum, &openedfr);
  }
}

// loot.c
void main( void )
{
  // Any number of things, such as setting animation,
  // hardness. nohit, etc.
  sp_touch_damage(&current_sprite, -1);
}

void touch( void )
{
  sp_touch_damage(&current_sprite, 0);
  // Code here for what happens when you pick it up
  // Such as &gold += 500; sounds, everything.
  external("library","takeLoot");
}


And there you have it, the final version of a persistent loot system. That there is actually the full loot.c script. It's one line longer than it used to be, as we've added the call to external(). However, the container script is much shorter:

void main( void )
{
  // Fill in appropriate numbers here
  external("library","initContainer",openanim,openedseq,openedfr,edtype);
}

void hit( void )
{
  external("library","openContainer");
}

void createLoot( void )
{
createLoot:
  // Fill in appropriate stuff here
  int &loot = create_sprite(blah, blah, blah, blah, blah);
  sp_script(&loot, blah);
  sp_custom("myContainer",&loot,&current_sprite);
}


Differences with Persistent Loot

If you got through all that, you can go and compare the scripts in the story directory of Persistent Loot with the ones I just wrote. There'll be many similarities. Be aware that Persistent Loot calls containers sources.

Compare the newly written container.c to ch1-gh2.c in Persistent Loot. Here we see that Persistent Loot uses an external() script to create loot. make2.c is based on the original game's make.c, and takes care of setting the sp_custom() (actually, it delegates that to loot.c which also sets some other values). And an example loot.c is gheart2.c. The Persistent Loot-equivalent of library.c is actually loot.c.

You'll also see lots of code in Persistent Loot that wasn't in this rant. Most of this is because I've left out a part of the problem: The chest is actually opened by activating another sprite on the map, and not open when hit by Dink. This means we can't make use of the fact that &current_sprite refers to the container. Almost all procedures in Persistent Loot allow you to provide a sprite number to use as container or loot instead.

Secondly, the code written here has a few subtle bugs. While they don't manifest when dealing with chests, barrels and crates will be slightly... off. The difference is in the different editor_type()s typically used for these things. Chests, barrels, and crates are hard when unopened, and it is possible for sprites to walk behind them. However, barrels and crates are no longer hard when hit, and are drawn to the background. Persistent Loot's void source_init( void ) (which corresponds to our void initContainer( void )) has a series of if-checks to determine how to draw an opened container while it's not looted.

Thirdly, Persistent Loot allows many parameters to be left out, and there is quite some code to deal with detecting that and using hopefully sensible defaults. You can read my mini-rant about optional parameters if you want. They're used all over the place in Persistent Loot.

Next to those, there are some other subtle differences. Read through ch1-gh2.c, gheart2.c, and loot.c, and compare them to the scripts written in this post.

Final words

And that concludes this post about how and why I wrote Persistent Loot the way I did. I hope that's clear now, and I hope this gives some insight in how to use external() and run_script_by_number() in practice.

I'm planning to write another rant, this time about debugging DinkC scripts, but that may take a while as I have no clue what to even put in it. On the other hand, I'll happily take requests.