Site hosted by Angelfire.com: Build your free website today!
MobProg Variables for ROM

This document assumes you're using the excellent MobProgs snippet by Markku Nylander. What we're attempting to do is to add support for variables within the MobProgs themselves. These variables could be used for just about any situation where a MobProg might need to store a number (eg for a counter).

Table of Contents

Goal

My goal for now is to create a simple variable system that will handle local variables for the mob, however it could easily be expanded to deal with any of the following:

My original design was to have 26 variables for each MobProg, referenced by a letter of the alphabet. The problem with this approach is that it only allows for 26 variables, and also single letters of the alphabet are not very descriptive. After running this design by the ROM mailing list, Erwin Andreasen suggested the system I describe here.

The System

Variables consist of the '$' character, followed by a name of any length (constrained only by memory). Variables are stored in a linked list in the mob's data structure. The variable must first be initialized before it can be used, this is done in one of the following ways:

Basic initialization with no other functionality:
$variablename

Initialize and assign a value to variablename, expression can be a number, maths, or another variable: $variablename = expression

Initialize and assign a value to variablename, type can be any of the values from base MobProgs such as level: $variablename = type

Once initialized variables can be used anywhere in the MobProg. The variable is simply replaced with it's numerical value and then the code is evaluated as normal. If any maths operations are required they must be completely surrounded with brackets or the maths parser won't pick them up.

Any MobProg requiring a variable followed by text with no space between must show this with a '#' character. For example, '$mself' will look for a variable named 'mself', but '$m#self' will evaluate '$m' and place 'self' immediately after (the '#' character disappears).

The Code

A number of new functions are needed, I recommend putting them in a new file (In my case mob_vars.c). In this file we need the following:

Searching a linked list is a topic covered here.

Expanding Variables

Expanding a string with variables such that the variables are replaced with their correct values can be achieved like this:

void expand_vars( CHAR_DATA *ch, char *line )
{
    MPROG_VAR *var;
    char *point, buf[MAX_STRING_LENGTH], copybuf[MAX_STRING_LENGTH];
    const char *str;
    int i;
    bool do_math = FALSE;

point = line; strcpy( copybuf, line ); str = copybuf; while( ( str ) && ( *str != '0' ) ) { if ( *str != '$' ) { /* Check for braces, if found run maths parser */ if ( *str == '(' ) do_math = TRUE; *point++ = *str++; continue; } /* Found a variable */ ++str; i = 0; /* Get the name */ while( ( str ) && ( isalnum( *str ) ) ) buf[i++] = *str++; buf[i] = '0'; /* Check for the name */ for( var = ch->pIndexData->mpvars; var; var = var->next ) if ( !str_cmp( var->name, buf ) ) break; /* Safer than dynamic initialization */ if ( !var ) { /* You should be able to write a bugf function yourself */ bugf( "Expand_Vars: Uninitialized var '%s'.", buf ); return; } /* Convert value to string format */ sprintf( buf, "%d", var->value ); /* Place value into original string */ for( i = 0; i < strlen( buf ); i++ ) *point++ = buf[i]; } *point = '0'; /* Run maths parser only if required */ if ( do_math ) parse_math( ch, line ); }

You should notice the similarity to expand_args in base MobProgs.

Evaluating If Statements

Next we need a function to evaluate 'if' statements:

int var_eval( CHAR_DATA *ch, char *line, char *control )
{
    char buf[MAX_STRING_LENGTH];
    int op, lval, rval;

/* Find the lval directly as expand_vars has already been called */ lval = atoi( control ); line = one_argument( line, buf ); /* Find the desired operation */ if ( ( op = keyword_lookup( fn_evals, buf ) ) < 0 ) { bugf( "Var_Eval: Bad operation '%s'.", buf ); return 0; }

/* Get the rval directly */ line = one_argument( line, buf ); if ( is_number( buf ) ) rval = atoi( buf ); else { bugf( "Var_Eval: Bad number '%s'.", buf ); return 0; }

/* Evaluate the lval and rval */ return ( num_eval( lval, op, rval ) ); }

Following it so far? Good.

Interpreting Variable Commands

The function to interpret variable commands is quite long, so I'll cut out the repetitive code:

void var_interpret( CHAR_DATA *ch, char *line )
{
    MPROG_VAR *var;
    CHAR_DATA *victim;
    char buf[MAX_STRING_LENGTH];

/* Get variable name */ line = one_argument( line, buf ); /* Check for an existing variable */ var = get_var( ch, buf+1 ); /* Initialize a new variable */ if ( !var ) { var = new_var(); var->name = str_dup( buf+1 ); var->next = ch->pIndexData->mpvars; ch->pIndexData->mpvars = var; }

/* Expand any other variables on this line */ expand_vars( ch, line ); line = one_argument( line, buf ); /* If it's not an assignment then stop */ if ( buf[0] != '=' ) { return; } line = one_argument( line, buf ); /* Is it an expression or a type */ if ( is_number( buf ) ) var->value = atoi( buf ); else { if ( !str_cmp( buf, "people" ) ) var->value = count_people_room( ch, 0 ); else if ( !str_cmp( buf, "players" ) ) var->value = count_people_room( ch, 1 ); else if ( !str_cmp( buf, "mobs" ) ) var->value = count_people_room( ch, 2 ); /* Similar to above cases cut for length */ else if ( !str_cmp( buf, "vnum" ) ) { line = one_argument( line, buf ); if ( ( ( victim = get_char_room( ch, buf ) ) != NULL ) && ( IS_NPC( victim ) ) ) var->value = victim->pIndexData->vnum; else var->value = ch->pIndexData->vnum; } else if ( !str_cmp( buf, "hpcnt" ) ) { line = one_argument( line, buf ); if ( ( victim = get_char_room( ch, buf ) ) != NULL ) var->value = ( victim->hit * 100 ) / ( UMAX(1, victim->max_hit) ); else var->value = ( ch->hit * 100 ) / ( UMAX(1, ch->max_hit) ); } else if ( !str_cmp( buf, "room" ) ) { line = one_argument( line, buf ); if ( ( victim = get_char_room( ch, buf ) ) != NULL ) var->value = victim->in_room->vnum; else if ( ( victim = get_char_world( ch, buf ) ) != NULL ) var->value = victim->in_room->vnum; else var->value = ch->in_room->vnum; } /* Similar to above cases cut for length */ else { bugf( "Var_Interpret: Bad type '%s'.", buf ); return; } } }

Parsing Mathematical Expressions

  Separating The Expression

Now all we have left is the maths parser function. Parsing mathematical expressions is a complicated topic which I'll touch on in a minute, but first we'll need a wrapper function to separate the expression from the rest of the string:

void parse_math( CHAR_DATA *ch, char *line )
{
    const char *str;
    char *point, buf[MAX_STRING_LENGTH], copybuf[MAX_STRING_LENGTH];
    int retval, i, braces;

strcpy( copybuf, line ); str = copybuf; point = line; while( ( str ) && ( *str != '0' ) ) { /* Find the start of the expression */ if ( *str != '(' ) { *point++ = *str++; continue; } i = 0; braces = 0; /* Search for the bracket matching the first one */ do { buf[i++] = *str++; if ( buf[i-1] == '(' ) ++braces; else if ( buf[i-1] == ')' ) --braces; } while( ( str ) && ( braces > 0 ) ); buf[i++] = '='; buf[i] = '0'; /* Evaluate expression */ retval = eval( buf ); /* Convert value to string format */ sprintf( buf, "%d", retval ); /* Add value to original string */ for( i = 0; i < strlen( buf ); i++ ) *point++ = buf[i]; } *point = '0'; }

  Reverse Polish Notation

This is where you might need to do some research of your own. I'll describe how this technique works, but my code is way too hackish to put here. Also, I'm assuming you know about precedence in C - or you have a good C book with that sort of info in. This explanation is partly from "Illustrating C" by Donald Alcock, and partly from personal experience.

So just what is reverse Polish notation then? It's a way of making the evaluation of standard algebraic expressions easier by reordering in order of precedence. The hard part is getting your algebraic expression into reverse Polish notation in the first place.

Conversion Process

This can be achieved using two stacks, an operand stack (called A from here on in), and an operator stack (called B from here on in). Work through the expression from left to right placing all operands on A and all operators on B. Whenever a right bracket is encountered, search back through B and find the matching left bracket, then put the contents of the brackets onto A.

Do not stack one operator on top of another operator unless the operator below has lower precedence or is a left bracket. If you get in this situation (with a low precedence operator going on top of a high precedence operator) then you must remove the higher precedence operator from stack B and place it on stack A. If the top operator on stack B still has a higher precedence than the low precedence operator waiting to be placed, then the process must be repeated until the low precedence operator can be put onto an operator of lower precedence, or stack B is empty (in which case the low precedence operator can be put on stack B).

When there's nothing left we take the top item off stack A and place it on top of stack B. Continue this until stack A is empty. Stack B now contains the new expression, from the top down.

Try working through this example, writing down what goes in each stack:

Take the expression A * ( B / C ) + D - ( E * F ) - G

Working from left to right: 1) Put 'A' on stack A. 2) Put '*' on stack B. 3) Put '(' on stack B. 4) Put 'B' on stack A. 5) Put '/' on stack B. 6) Put 'C' on stack A. 7) Found ')', find matching '(' in stack B. 8) Between brackets on stack B is '/' so place '/' on stack A. 9) '+' cannot go on stack B because '*' has higher precedence. 10) '*' placed on stack A. 11) Nothing left in stack B, so '+' put on. 12) Put 'D' on stack A. 13) '-' cannot go on stack B because '+' has same precedence. 14) '+' placed on stack A. 15) Nothing left in stack B, so '-' put on. 16) Put '(' on stack B. 17) Put 'E' on stack A. 18) Put '*' on stack B. 19) Put 'F' on stack A. 20) Found ')', find matching '(' in stack B. 21) Between brackets on stack B is '*' so place '*' on stack A. 22) '-' cannot go on stack B because '-' has same precedence. 23) '-' placed on stack A. 24) Nothing left in stack B, so '-' put on. 25) Put 'G' on stack A. 26) Nothing left to put on so start emptying stack A.

The new expression is ABC/*D+EF*-G-.

Evaluation

Evaluating the expression is surprisingly easy. Simply work from left to right taking each item in turn. Whenever an operator is encountered, apply it to the previous two terms, reducing them to one:

Take the expression ABC/*D+EF*-G- where:
        A = 1   B = 3   C = 8   D = 2   E = 1   F = 9   G = 4

This gives us: A B C / * D + E F * - G - 1 3 8 / * 2 + 1 9 * - 4 -

First operator is '/' acting on 3 and 8, so do 3 / 8 (.375) and store it: 1 .375 * 2 + 1 9 * - 4 -

Next operator is '*' acting on 1 and .375, so do 1 * .375 (.375): .375 2 + 1 9 * - 4 -

Next operator is '+' acting on .375 and 2, so do .375 + 2 (2.375): 2.375 1 9 * - 4 -

Next operator is '*' acting on 1 and 9, so do 1 * 9 (9): 2.375 9 - 4 -

Next operator is '-' acting on 2.375 and 9, so do 2.375 - 9 (-6.625): -6.625 4 -

Next operator is '-' acting on -6.625 and 4, so do -6.625 - 4 (-10.625): -10.625

Solution is -10.625

Okay.. so it's not the best example in the world, but at least it works ;).

The hardest part of all is to put that into code, unfortunately you're going to have to do that by yourself. The only advice left to give is to make a precedence table with all the operators in, and give '(' a very low precedence. This means you won't have to worry about having special cases in the check for lower precedence before putting an operator on the stack.

I believe there's an advanced version of this available here that can handle sin and cos, but I may be wrong. In any case, if you want maths support you'll need a function that takes a string argument and returns the result (int eval( char *str ) in my case).

Finishing Touches

To finish things off we'll need some code to integrate with the base MobProg implementation.

Lets start by adding provision for the mob to use variables in it's ordinary commands, such as talking. This is done in mob_cmds.c by simply adding a call to expand_vars immediately before the command function is called.

We'll also be needing a call to expand_vars in mob_prog.c in the function cmd_eval, just before the switch statement. The function expand_arg needs to be altered so it won't try to interpret the new variables. I suggest adding the following just before the switch statement:

if ( ( str+1 ) && ( isalnum( str[1] ) ) )
{
    *point++ = '$';
    continue;
}

And we also need provision for the '#' special character, I suggest this code immediately following the '++str' after the switch statement:

if ( ( str ) && ( *str == '#' ) )
    ++str;

The program_flow function needs some special consideration as we need to add the calls to var_eval somewhere here. You should find three blocks of code containing calls to the cmd_eval function. Place an expand_vars call immediately before the call to one_argument in each of these code blocks. This will expand the variables and allow cmd_eval to evaluate them correctly.

In each of the blocks of code, you'll want to place a call to var_eval somewhere. I placed the following code just after the call to cmd_eval, outside the if statement:

else if ( is_number( control ) )
{
cond[level] = var_eval( mob, line, control );
}

You might want an expand_vars function call immediately before the final call to interpret, but that's up to you. There's only one thing left and that's the call to var_interpret. I recommend a check for the control character '$' just after the call to mob_interpret, but outside that block of code:

else if ( control[0] == '$' )
{
    var_interpret( mob, data );
}

That's just about it. Congratulations if you made it this far!

Mail me at dasran@claramail.com

"MERC/Derivative Programmers" webring [Next] [Index] [Prev] [Joining]