The Dink Network

Reply to Re: 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:
 
 
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.