The Dink Network

Reply to magicman's rant about scripting techniques

If you don't have an account, just leave the password field blank.
Username:
Password:
Subject:
Antispam: Enter Dink Smallwood's last name (surname) below.
Formatting: :) :( ;( :P ;) :D >( : :s :O evil cat blood
Bold font Italic font hyperlink Code tags
Message:
 
 
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.