Author Topic: Enemy AI (Scripters only!)  (Read 6904 times)

Terence Fergusson

  • *
  • Posts: 262
    • View Profile
Enemy AI (Scripters only!)
« on: 2004-07-26 15:13:06 »
Well, this has been my task for the last few days.

I've managed to work out enough about AI from the disassembled EXE and the actual AI data in the Scene.bin to get a handle on most parts of the AI and write a quick dumper to get hold of translated AI scripts.  I'm not going to post it since it's kinda shoddy and you really wouldn't learn much from it.

What I *will* talk about is the way AI is handled.  First, let's talk about the language type.  The AI Engine is a *stack-based* language, while still allowing opcodes and arguments.  While arguments are possible though, 90% of the commands expect variables on the stack and will use them, not the arguments.

Now, the opcodes themselves.  They're divided into categories:
  0x: Load Values from Address
  1x: Load Address
  3x: Operators
  4x: Comparison
  5x: Logical Operators
  6x: Load Constant
  7x: Jumps
  8x: Math Logic
  9x: Commands
  Ax: Dummy

These are then split down further.  Before we go over the opcodes, it's necessary to go over the stack...

============================================
  THE STACK
============================================
The Stack stores variables, and different opcodes will use these in different ways.  There are three types of variables:
  0x: 1-, 2- or 4-byte variables which take up a single DWord.
  1x: 2-byte variables specifying an address that will later be used.  Takes up a single DWord.
  2x: Multi-Word variables, taking up anywhere from 1 to 10 DWords.  These are used to contain the value of a single stat for *ALL* objects on the field at once.  For example, it might hold the current HP of all objects, so that we can decide who has the highest.

The 'x' part of the variable dictates how many bytes the variable was, and can be anywhere from 1 to 3 (byte, word or dword).  If it's equal to 0, then that means it was a 'bit'.

When a variable is pushed onto the stack, the values go first, followed by the variable *type* as explained above.

Now, let's look over the opcodes, which will help us explain these better.


============================================
  0x: LOAD VALUES
============================================

  0x: Argument of 2 bytes, "PushAddr wr:addr"
        () -- (xx)

This opcode is designed to look up an address and then push its contents onto the stack.  The address must first be translated though:

   0x0000 - 0x1FFF: Section start = 009CB9C+ID*0x80
   0x2000 - 0x3FFF: Section start = 009C750
   0x4000 - 0xFFFF: Section start = 009C78C+ID*0x68
(You may recognise that last section as the Battle Player Block)

The address itself must also be translated.  The address after the first nibble (0-4, generally) can be expressed like so in binary:
                                  xxxx xxxx x:yyy
The 'x' part is the actual byte address.  The 'y' part is reserved for individual bits.  In other words, we must divide the address by 8 to get the real offset to add to the start of the section.

The low nibble of the command dictates the size of the value to return: 1-3 returns a number either byte, word or dword, while a value of 0 will utilise that 'y' part of the address to return an individual bit.

In addition to all this, any call to the Battle Player Block (0x4000+) will grab the specific value for *all* Objects in the battle.  This automatically makes the variable returned into a '2x'-type Var.

Once the values have been returned, they are pushed onto the stack as a '0x'-Var if the Address was between 0000 and 3FFF, and as a '2x'-Var if the Address was 4000 or above.


============================================
  1x: LOAD ADDRESS
============================================

  1x: Argument of 2 bytes, "PushWord wr:addr"
        () -- (xx)

Very simple: this pushes on a '1x'-type Var onto the stack using the next two bytes.  This is used to store the location of an address that will be used down the line for, say, storing a value.


============================================
  3x: OPERATORS
============================================

These series of opcodes take variables off the stack, apply an operator to their values, and then place a new combined variable back on the stack.

  30: No arguments, "Add"
 (xx yy) -- (zz)
    'z = x + y'

  31: No arguments, "Sub"
 (xx yy) -- (zz)
    'z = x - y'

  32: No arguments, "Mul"
 (xx yy) -- (zz)
    'z = x * y'

  33: No arguments, "Div"
 (xx yy) -- (zz)
    'z = x / y'

  34: No arguments, "Mod"
 (xx yy) -- (zz)
    'z = x MOD y'

  35: No arguments, "And"
 (xx yy) -- (zz)
    'z = x AND y'

  36: No arguments, "Orr"
 (xx yy) -- (zz)
    'z = x OR y'

  37: No arguments, "Not"
    (xx) -- (zz)
    'z = NOT x'

The Var 'z' is a '2x'-Var if either of the components are a '1x' or a '2x'-Var.  In that case, the Masks of both Vars are ANDed together.  In *all* cases, the highest variable size is used to determine the new variable's size.


============================================
  4x: COMPARISONS
============================================

Very similar to the last category, except that these are meant to compare two values.

  40: No arguments, "=="
 (xx yy) -- (zz)
    Returns 'x == y'

  41: No arguments, "!="
 (xx yy) -- (zz)
    Returns 'x != y'

  42: No arguments, ">="
 (xx yy) -- (zz)
    Returns 'x >= y'

  43: No arguments, "<="
 (xx yy) -- (zz)
    Returns 'x <= y'

  44: No arguments, ">"
 (xx yy) -- (zz)
    Returns 'x > y'

  45: No arguments, "<"
 (xx yy) -- (zz)
    Returns 'x < y'

The new Var 'z' will be an '00'-type Var if either component is a '1x' or a '2x' Var.  It will only have a single value: 1 or 0.

If both were '0x'-Vars though, the result will be an '02'-Var which contains the Mask of all the objects in the variables that passed the comparison (yes, I'm aware that objects are identified with '2x'-Vars, but this is precisely what it appears to do... it doesn't matter much in the end though, as you'll see....)


============================================
  5x: LOGICAL OPERATORS
============================================

Strictly speaking, these are really Non-Zero operators.  Instead of applying the operator to each bit in a value in turn, all they care about is if the value in the variable is zero or non-zero.

  50: No arguments, "LogAND"
 (xx yy) -- (zz)
    Returns 1 if both 'x' and 'y' are non-zero, 0 if either is zero.

  51: No arguments, "LogOR"
 (xx yy) -- (zz)
    Returns 1 if either 'x' or 'y' is non-zero, 0 if they're both zero.

  52: No arguments, "LogNOT"
    (xx) -- (zz)
    Returns 0 if 'x' is non-zero, 1 if 'x' is zero.

In all cases, the new Var 'z' will be an '00'-type Var.



============================================
  6x: CONSTANTS
============================================

These take arguments, depending on the opcode:

  60: Argument of 1 byte, "Push1Con by:constant"
        () -- (xx)

  61: Argument of 2 byte, "Push2Con wr:constant"
        () -- (xx)

  62: Argument of 3 bytes, "Push3Con 3by:constant"
        () -- (xx)

In each case, the new variable pushed onto the stack will either be a '01', '02' or '03'-type variable, with a single value equal to the constant defined by the arguments.


============================================
  7x: JUMPS
============================================

Now we start hitting the more difficult categories.  I'll go over each individual opcode one by one.

  70: Argument of 2 bytes, "!IfGoto wr:addr"
      (xx) -- ()
If the Var 'x' is zero, then jump to the specified location in the AI script.  A non-zero value will cause no jump to occur.


  71: Argument of 2 bytes, "!EqGoto wr:addr"
   (xx yy) -- (xx)
Although 'x' is checked, it is not popped from the stack.
The vars 'x' and 'y' are compared; if they are different, then we jump to the specified location in the AI script.  If either 'x' or 'y' were '2x'-type Vars, then we only look at the value of the *1st* Object defined by the Mask.


  72: Argument of 2 bytes, "Goto wr:addr"
        () -- ()
An unconditional goto to the selected address.


  73: No arguments, "END"
        () -- ()
Ends the AI script section.


74 and 75 are valid opcodes, but they are dummied out; both will pop a variable from the stack, however, and 75 will store the value of that Var as a byte into [008F4E3D].


============================================
  8x: MATH LOGIC
============================================

These opcodes are designed to aid battle logic.

  80: No arguments, "MaskSet"
   (xx yy) -- (zz)
Will update the Mask of Var 'y' by ANDing it with the value of 'x'.  The updated Var is then pushed back onto the stack.
Absolutely *NOTHING* happens (not even popping variables off) if 'y' is a '1x'-type Var.


  81: No arguments, "Random"
        () -- (xx)
Creates a random number between 0 and 65535.  This will be pushed onto the stack as the value of a new '02'-Var.


  82: No arguments, "PickRndBit"
      (xx) -- (zz)
'z' is a '02'-type Var, which takes the value of 'x' and picks one of the bits in the 0xFFFF region at random.  For example, a value of 0x0718 could result in a value of 0x0100, 0x0200, 0x0400, 0x0010 or 0x0008.  The formula used is "Rnd(0..255) Mod (Number of Bits Set)", and counting from the Least Significant Bit first.  This *does* mean that the lesser bits tend to have a slightly higher chance of being picked.


  83: No arguments, "CountBits"
      (xx) -- (zz)
The function of the opcode depends on the type of Var 'x' is.  For '0x' and '1x'-type Vars, 'z' is a '01' Var containing a single integer which is a count of how many bits in the 0x3FF region 'x' had turned on in its value.
If 'x' is a '2x'-type Var however, then 'z' is a '0x'-type Var containing the DWord in 'x' indicated by the first bit set in 'x''s Mask.


  84: No arguments, "GetHighMask"
      (xx) -- (zz)
A nice little function which expects a '2x'-type Var.  It will go through the values of 'x' and pick only the *equal highest* values indicated by 'x''s Mask.  The result is a new Mask that indicates which objects had the highest of that value.  This new Mask is the value of an '02'-Var which is pushed onto the stack as 'z'.


  85: No arguments, "GetLowMask"
      (xx) -- (zz)
Identical to the previous function, except that only the *equal lowest* values count.


  86: No arguments, "GetMPCost"
      (xx) -- (zz)
Will get the MP Cost of the ability refenced in Var 'x'.  The value of 'x' is referenced to the following areas:

  If Value >= 0xFFFF, return 0
  If Value <= 0x00FF, Addr = 00DAAC78 + Value*0x1C
  If Value >= 0x0100, match Value with wr[0099AAF4+(ID=[0..31])*2]
     First ID it matches, Addr = 0099A774 + ID*0x1C
  If no Addr is found, return 0
  Otherwise, return wr[Addr + 4]

The returned value is, thus, the cost of the defined ability.  Note that 0x00 to 0xFF are standard magic and always loaded, while 0x100+ are the unique abilities loaded through scene.bin.  The MP Cost is stored in an '02'-Var pushed onto the stack.


  87: No arguments, "ConvToBit"
      (xx) -- (zz)
This simply takes the value of 'x' (first DWord yadayada if it's a '2x'-type Var) and convert it to a bit.  For example, 0 converts to 0x000001, 2 converts to 0x000004, 7 converts to 0x000080.  The returned bit is used as the value of an '02'-type Var which is pushed onto the stack.


============================================
  9x: COMMANDS
============================================

Finally, where everything comes together.  But I'm not sure of the use of some of these... heh....

  90: No arguments, "SetAddr"
   (xx yy) -- ()
(zz xx yy) -- ()
This strange opcode has two formats, depending on what it's passed.  'x' is expected to be a '1x'-type Var, containing an address value.  If the Address is 0000-3FFF, then the first format is used, using the Object ID of the monster running the AI script.
If the Address is 4000 and above though, then the second format is used, where 'z' is an Address that points to the location of a Mask that should be used to dictate which Battle Objects will be updated by the command.
In either case, the Value 'y' will be stored in the Address 'x'.


  92: No arguments, "RunCmd"
   (xx yy) -- ()
One of the most important commands.  'y' contains the ID of the move being used, while 'x' contains... well, it's usually '0x20' for standard attacks, but sometimes it's '0x24' for... animations?  Unsure.  Maybe I'll find out more later.

  93: Variable arguments, "LoadString var:string"
       ??? -- ???
Loads a string into memory.  Possible displays it immediately too; unsure.  FF is the terminator, and EA-F1 means a 2-byte char follows.  Standard FF7 string rules.

91 is a dummy opcode that merely pops the topmost variable from the stack and does nothing with it.  However, it's actually used by a fair number of scripts, so perhaps a translation as "Pop" will do; maybe it's just there for cleanliness in keeping the stack down... though I'd suspect that FF7 resets the stack after every AI run.

94-96 are opcodes similar to 92, taking the top two variables from the stack and calling something with their values; however, I do not know what they do as of yet.  They have no arguments though.

And finally, A0 and A1 both appear to be dummy commands.  A1 pops the top two Vars from the Stack, doing nothing with them.  A0, however, is a confusing mess that looks like it *should* be doing something, but then does nothing with the data (that I can see).  In fact, let me cover it properly:

  A0: Variable arguments, "Unknown by:size var:data"
      (??) -- ()
The first byte after the opcode is the 'size', and is the number of variables that will be popped from the stack, eventually.  The data afterwards is... unknown, but is terminated when a '00' is found.  The 'size' is stored at EBP-0x168, a pointer to the start of the 'data' in the AI Script is stored at EBP-0x1B0, and the value of each Var popped from the stack are stored as DWords sequentially from EBP-0x1AC.  So, the maximum 'size' has to be 17 (0x11).
Only Ultimate Weapon and Safer*Sephiroth use this command, so it's kinda low priority at the moment.  The *data* passed as arguments, when looking at them, look similar to 'printf' statements... perhaps Debug strings?  Yes... makes sense.  It's stuff like saying how much Safer's stats have increased, or Ultimate Weapons' escapes and HP and the like.  Fun.  Ah well.

============================================

That's pretty much the entire AI script in a nutshell.  There's still lots to do; I need to nail down where some of these variables are stored, especially since FF7 is something being very annoying and using virtual addresses, making them hard to nail down.  Will get there in the end though...

Enjoy.

Micky

  • *
  • Posts: 300
    • View Profile
Enemy AI (Scripters only!)
« Reply #1 on: 2004-07-26 17:46:22 »
This is great!
This makes the second scripting language used by the game that is decoded. Do you have any idea if attack, magic and item effects are hard-coded into the kernel or scripted?

Terence Fergusson

  • *
  • Posts: 262
    • View Profile
Enemy AI (Scripters only!)
« Reply #2 on: 2004-07-27 15:41:18 »
Damage, statuses and special effects are all in the executable.  Animations *may* be scripted, but that's not my department.

Anyhow, thought I'd cover one of the unknown Opcodes today.

95: No arguments, "AccessGlobalVar"
(xx yy) -- ()

This is a nice and fun opcode.  'y' contains the variable we're interested in.  'x' contains either a 0 or a 1, with a 0 meaning Read and a 1 meaning Write.  When AccessGlobalVar is called, we take the address indicated by Var 'y' and either: (1) store it in Address '2010' [0099C752 translated], if it's a Read operation, or (2) store the contents of Address '2010' in that address, if it's a Write operation.

The GlobalVar address is almost identical, basically, to the Field Global Variables: 000-0FF refers to Bank 1, 100-1FF refers to Bank 2, 200-2FF refers to Bank 3, etc.

There are a number of enemies that do access Global Variables.  A nice example is Zombie Dragon; it checks to see if it's ever used Pandora's Box in the game before.  If it has, it doesn't use it again (unless the variable is reset).  The variable in question is Bit 1 of Address 05E (Bank 1, Offset 5E)

===

Now, I'll go over the Battle Addresses again just to identify them, since that's also useful:

0x0000 - 0x1FFF: Temporary variables UNIQUE to this instance of the enemy.  No other enemy in the battle may access them.
0x2000 - 0x3FFF: Temporary variables and important constants UNIQUE to this battle.  Often contains masks that include what ability an enemy is currently countering, which party members are alive (so we can use it as a mask), and the aforementioned Global Var gateway (0x2010).
0x4000+: Standard Battle Player block.  Accessed by anything and everything in battle.  I don't have to explain much about this.

Anyhow, til next time.

Sephiroth2000

  • Guest
Enemy AI (Scripters only!)
« Reply #3 on: 2004-07-31 00:37:32 »
So do you think this is the same as FF8 and so on? I mean once a game company has come up with an engine for things (such as AI) they will re-use them as many times as possible before it gets old. Once you look at it, these systems aren't really that difficult to make. Its just very time consuming trying to figure out what to do with it, or other plot-wise things.
Very interesting stuff...

Cyberman

  • *
  • Posts: 1572
    • View Profile
Enemy AI (Scripters only!)
« Reply #4 on: 2004-07-31 03:36:57 »
Quote from: Sephiroth2000
So do you think this is the same as FF8 and so on? I mean once a game company has come up with an engine for things (such as AI) they will re-use them as many times as possible before it gets old. Once you look at it, these systems aren't really that difficult to make. Its just very time consuming trying to figure out what to do with it, or other plot-wise things.
Very interesting stuff...


Yes quite interesting, however I can assure you that FF8 script system is going to be different.  This is because there is no materia instead they substituted the GForce.  Attacking criters carry a number of things that can be drawn from them, GF's inclusive.  Too me this means that there can only be a few things in common.  The fact they use similiar control code, is likely but the actual scripting commands must vary signicantly.  Then you have the cards that are used for making objects and visa versa.  I think they had to redo the entire engine.  Now FF9 and FF9 I think are very much closer cousins the models are similar in structure but not identical. Also the field data is similiar along with the background information.  FF7 may have been used as a 'base' and they recoded the engine to be more extensible for FF8.  In FF9 they switched from GF's to summons like in FF7.  They added training for things to gain skills and had cards (the cards though are kind of useless I think Ff9 was cliped to get it out the door on time, it's just too short and linear in my opinion.)  Before you can travel a lot it's practically the end of the game darn it. You also don't travel much in the alternate world.
FF9's use of gems is much more like FF7's materia however it is cool that you can learn a skill and dump the gem then.

In FF9 some 'hacking' has revealed that each character has a set of skills they can learn ( I think it's a total of 80 for each).  The common ones are last (IE ones that all characters can learn) and this makes it easier to figure those out (by inspecting save files mostly :D ).

The scripting languages can only be so much in terms of similarity because the base game systems are fairly differnt.  I suppose that's what I'm getting at :D

Cyb - I finally quit babbling!