PDA

View Full Version : Script variable design discussion



DarkDragon
10-20-2006, 08:47 AM
One of the elements of the ZScript language that I originally designed poorly, as has been pointed out by several of you recently, are the script variables, which have various problems currently such as malfunctioning when multiple instances of the same script are running on the same screen. In this post I'll present some thoughts and opinions on how these variables should be redesigned, and invite comments from those of you who have shown interest in the matter.

First, a brief description of how script variables are currently implemented, for those not familiar with ZScript's inner workings. ZScripts are built in two broad steps:
1) The ZScript source is compiled into ZAsm assembly. This is the source you see when you look in allegro.log after compiling a ZScript.
2) Scripts are assigned to slots, and the ZAsm is copied verbatim into those slots.
We'll see the consequences of this two-step process in a minute.
Each running script has a stack associated with it, which is invisible to ZScript coders but is relied upon heavily for things like function calls and, relevant to this discussion, local variables. At compile time each function is scanned looking for declared variables, and each of these variables is reserved a place on the stack. For instance, in the snippet

void foo() {
int a;
for(int b=0; b<3; b++)
int c;
}

the three variables a,b,c would be assigned stack slots 0,1,and 2. Since each running script has its own stack, each script has its own separate copy of a, b, and c, and there are no problems.
But now consider a script variable:


ffc script foo {
int a;
}

Since a has to be visible to other scripts, a cannot be stored on the stack, as one script's stack cannot currently be read by a different script. Thus at the moment a is assigned a global variable, say GD0. But now if two scripts are running foo, because the ZASM code is just copied verbatim into both slots, both will be using GD0 simultaneously, leading to undesirable interference.

The goal, then, is to redesign the script variables so that they
1) still allow inter-script communication, and
2) are distinct for different running instances of the same script.

Satisfying 2) is not too difficult, and can be accomplished for instance by storing script variables on the stack, like local variables are.
But now we have a problem with 1). Currently there's no way in ASM to read a different script's stack, but that's not really a problem, a way to do so could be implemented. The actual problem is that which script is running in which slot is not known until runtime, whereas the ZScripts must be compiled at script-import time. For instance, consider the pair of scripts

ffc script one {int a;}
ffc script two {int a; int b;}
and you want to interact with a script running in slot 1. You can get a pointer to that script using LoadFFC:

ffc ptr = Screen->LoadFFC(1);
But now how would you go about specifying you want to change ptr's script variables? We could allow code like

ptr->b = 4; but the problem is that there's no way to know at compile time where on ptr's stack variable b is stored, and moreover we don't even know that ptr is an instance of two. Even worse
ptr->a might point to two different stack locations depending on whether ptr is a one or a two.

Other languages resolve this ambiguity using casts, ie
((one)ptr)->a = 4; but I'm not convinced this is the optimal approach. Alternatively, maybe there's a way to encode script information so that the script variable accesses can be validated and resolved entirely at runtime.

Thoughts?

C-Dawg
10-20-2006, 10:28 AM
Is there no way to keep information regarding where in the stack each variable points? That is, no way to keep a little table for each ffc that will tell it where variable a is so that it can resolve ptr->a?

Heck, the data for the table could be the first entry in the stack, always. A string with a list of variable names and null characters that lets the game check where variables are.

FFC stack
01 - "a b c d e"
02 - a
03 - b
04 - c
05 - d
06 - e

etc

Now I don't program much anymore, and didn't do so well in assembly class, so I may have no clue what I'm talking about.

_L_
10-20-2006, 01:10 PM
Hummmm....

Umm, maybe change the stacks to associative arrays of some sort?

(This is sort of like the previous post.)

Saffith
10-20-2006, 11:40 PM
This is the best I've come up with so far...

Add a function, something like void SendData(int data, int usage). Two arguments: the number to pass in, and another number to tell the receiver what to do with the data. This would be common to all FFCs—empty, by default, and overridden if wanted—so it could simply be called using ffcPtr->SendData().

I have some idea as to how this could be implemented without excessive difficulty (although I'm probably presuming a bit much).
As far as I can tell (based on the ASM generated by the compiler), most of what FFCs do only uses d2-d6. d0 seems to be used only for special arguments like COMBODD. Even then, it just pops the number off the stack shortly beforehand. What I'm getting at is that when it's used at all, it's loaded, used immediately, and then forgotten about, so there doesn't appear to be any potential problem with changing the register's value from outside. As for d1 and d7, I haven't been able to get it to use them at all, aside from passing in arguments to run().
Considering all that, ffcPtr->SendData(x, y) could simply set the target FFC's d0 and d1 to the two arguments, and set d7 to 1 to indicate that there's data to be read. Then the Waitframe() function could be changed to check d7's value after waiting; if it's nonzero, it's just another function call, except with arguments passed in via d0 and d1. That way, the sender doesn't have to know anything about the variables in the receiver (or even what script it was running, but of course that's something you should know when you're writing the code, anyway).
There is the obvious drawback that you'd have to wait a frame between multiple calls to SendData(), but I think that's acceptable. Actually, I don't think there's any way of avoiding that at all without either making the process much more complicated (something involving pairs of consecutive global variables and counters, perhaps) or letting FFCs see each other's stacks.
Perhaps my words here emanate from my posterior, but it seems to me to be a workable way of going about it.

Assuming this can be done, the biggest problem I see is that SendData() could be written such that it completely changes the flow of the script, never letting it get back to where it was before the function was called. Now, if game can handle it, I don't think that should be problem. If not, well, I guess it'd just be something that had to be used with some care. Perhaps loops and function calls (except maybe built-in mathematical functions) from within SendData() could be prohibited, so that it could only be used to set variables.


Edit:
Come to think of it, that still wouldn't allow you to read another script's variables...


((one)ptr)->a = 4;
Y'know, that may be the best way to go about it after all. I wonder, though, if writing it ptr->one.a wouldn't be a bit easier..

Saffith
12-07-2006, 03:23 AM
Slightly different idea...
I still think using a standard function is probably the best way to go, but implemented a bit differently. Suppose the SendData() function just has its arguments in the stack, like any other function. There'd be a standard location reserved for them, though. The top eight spots in the stack, maybe. A variable number, ideally, so only five slots are reserved if it's defined as taking five arguments.
The FFC sending data could just set those variables directly, then. It would have to be able to see the other FFC's stack, but if the variables have a standard location, it would know exactly where in the stack to put the data without having to worry about which name corresponds to which slot. As before, it could simply change D7 to indicate the function had been called - last I heard, the compiler doesn't use it for anything else.
There's still the limitation that it can only be called once per FFC per frame, but I don't see that being much of a problem.

Does something like that sound possible?

DarkDragon
12-07-2006, 07:45 AM
I think I have an idea of what you're suggesting, but could you provide a mock example script demonstrating sending and receiving in this way?

Saffith
12-07-2006, 01:12 PM
Sure. Just something simple and random:

ffc script foo
{
int state;

void run()
{
// Do something depending on state
}

void SendData(int x, int y, int z)
{
if(z==0)
state=x;
else
{
while(this->X!=x && this->Y!=y)
{
this->X=(this->X+x)/2;
this->Y=(this->Y+y)/2;
Waitframe();
}
}
}
}

ffc script bar
{
void run()
{
ffc fooPtr=Screen->LoadFFC(1);

// Change foo's state to 2.
fooPtr->SendData(2, 0, 0);
Waitframe();

// To hell with that, make it move to the corner.
fooPtr->SendData(240, 176, 1);
Waitframe()

// Tell it to move elsewhere instead. Not sure how this'd work internally
// in terms of the return address; might just have to avoid using it like this.
fooPtr->SendData(128, 80, 1);
}
}Obviously, there's plenty of potential for screwing things up, but it's not like there aren't already a million ways to crash the game with scripts.