Author Topic: [FF7] AI Template Project - Welder  (Read 18165 times)

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #25 on: 2010-05-22 19:03:56 »
Thank you, NFITC1. I think we now have enough data to cover most functions. A provisional set of plugins can now be bandied about.

NEW PLUGINS

**********************************
* ALTERNATIVE HEADER WITH VARIABLE IQ *
**********************************

This is an alternative to the standard header. It uses LOCALVAR 0200 to determine intelligence.

The purpose of this is for enemies to get smarter and more efficient under certain circumstances. I'm sure we've all played games where one leader unit grants new abilities or better stats to his peers - or where a group will become more aggressive and vengeful on the death of certain allies. This header allows you to implement such behaviour.

Code: [Select]
12 2070
02 20a0
82
90
60 20
61 index of the 'dumb' attack

81
60 FF
34
00 0200 <- reads one byte from LOCALVAR 0200
43
70 ZZZZ <- address of the footer

To use this, you will need to set a default intelligence level in at least one of the active Pre-battle scripts for each formation. This should be a value from 0 to FF (one byte). This value over 255 will be the 'IQ' - the proportion of attacks that are 'smart'.

Establish this with the following code

Code: [Select]
10 0200
60 FF <- this is the default intel level, from 0h to FFh - 80h is about 'half' intelligence
90

To alter IQ on certain events, you will need to write counter scripts, or counter-death scripts. These will take the form of the above code for Pre-Battle scripts, but contain the 'new' intelligence level.

So, to make things clear, let's imagine a soldier and his two dogs

*Dog IQ = 2/3rds by default. 'Normal' attack is just the usual, but the 'smart' attack will be 'fang' (more potent), used against the weakest opp
*Soldier's counter-death script sets IQ to zero
*Dog now acts like a wild animal without a trainer

****************************
* THE TWO-STATUS CHECK PLUGIN *
****************************

This is very close to the code NFITC1 contributed. It looks for enemies with either two specified status conditions, or looks for enemies n one condition *or* another. By 'condition', I mean being either in or *out* of a status.

For instance, you can check for
* Opponents in both SLEEP and MINI
* Opponents both ASLEEP and NOT MINI'D
* Opponents either ASLEEP or MINI'D
* Opponents either ASLEEP or NOT MINI'D

This is useful when wanting to take heed of things like Resist or Reflect. It also allows you to compound effects, or limit difficulty by stopping monsters over-loading the party with nasty statuses. Want to cast debarrier on opponents with either protect or shell? Want to cast despell on enemies with regen but not slow? Fancy being able to haste allies in berserk but not confusion? This is the script to use.

Code: [Select]
<<OPTIONAL MP CHECKING SECTION START>>
02 2060
02 4140
80
61 INDEXOFATTACK
86
42
70 ADDRESS OF NEXTPLUGIN
<<OPTIONAL MP CHECKING SECTION END>>
12 0400
02 20A0
00 40XX
80
60 01
40
02 20A0
00 40YY
80
60 01
40
35
82
90
02 0400
70 ADDRESS OF NEXTPLUGIN
02 2070
02 0400
90
91
61 FFFF
90

Let's go over that again to explain how it works, and what you should change to make the plugin suit your purposes:

--optional MP checking section--
02 2060
02 4140
80
61 INDEXOFATTACK
86 <- checks if user has MP for the above attack
42
70 ADDRESS OF NEXT PLUGIN
--end of optional MP checking section--
12 0400
02 20A0 <- active opponents (2080 for allies)
00 40XX <- the first status to check, for instance, 4006 for confusion
80
60 01 <- are we looking for opponents who have the above status? If so, this should be 1. If we're looking for opponents who *don't*, then 0
40 <- create list of opponents with/without the status in question
02 20A0
00 40YY <- second status to check
80
60 01 <- again, 01 if we want opponents WITH the status, 00 if we want units WITHOUT the status.
40 <- we now have two lists: one of opps. with status X (in)active, and one of opps. with status Y (in)active. We're now going to compare them.
35 <- bitwise AND: creates a list of opps with both status X and status Y active. We use the opcode 36 instead if we're wanting an OR.
82 <- a random member of the resultant list (optional)
90 <- stores in 0400
02 0400 <- loads list from 0400
70 ADDRESS OF NEXT PLUGIN <- goes to next plugin if this list is empty
12 2070 <- tar address
02 0400 <- list data
90 <- uses list as target
91 <- POPS off default attack / prior set attacks
61 FFFF <- pushes data of new attack

***************************
* The Kamikaze / Darkside script   *
***************************

This script has a monster look at its own HP, and use a certain attack if its HP is above or below a certain fraction. This is useful for darkside-like attacks/ souleater moves and blowup attacks. You don't want to use darkside (sacrifice a little HP for a powerful attack) when in low HP; nor do you want to explode when well-healed. So that's where this plugin comes in.

Code: [Select]
02 2060
02 4160
80
02 2060
02 4160
80
60 FF
33
60 X
32
42/43
70 ADDRESS OF NEXT PLUGIN
12 2070
02 20a0
82
90
91
61 INDEX OF ATTACK

Explained:

02 2060
02 4160
80 <-own HP
02 2060
02 4160
80
60 FF
33 <- own HP over 255
60 X <- this is our multiplier. This over 255 makes the fraction we're going to compare our HP with. 80h makes for half HP.
32
42/43 <- if HP below this fraction (42)/if HP above this fraction (43) ...then leave a TRUE on the stack
70 ADDRESS OF NEXT PLUGIN <- if there's a FALSE on the stack, go to next plugin
12 2070
02 20a0 <- so, random opponent
82
90
91
61 INDEX OF ATTACK

************************************************
* Use Attack A on LOCALVAR 0300 when LOCALVAR 0400 == 1 *
************************************************

This is a small script whose purpose may seem obscure at first. Basically, this allows for decisions to be set in counter scripts and the like. For instance, an ally might have a counter-death script that sets LVAR 0400 to 1 and puts the identity of its killer in LVAR0300. This script means that the downed monster's friends avenge its death.

Alternatively, a powerful fire attack might cause creatures to catch fire. A monster will, as a counter, flash, "The woods caught fire!" - from that point, attack A gets used, a fire-like attack called "Fire's outta control!". Think the Soulcage battle from FFIX.

You can replace the LocalVar 0300 with 20a0 (opponent list general) if you like. Here's the code:

Code: [Select]
00 0400
70 ADDRESS OF NEXT PLUGIN
12 2070
02 0300
(81)
90
91
61 INDEX OF ATTACK

So

00 0400
70 ADDRESS OF NEXT PLUGIN <- jumps if 0400 is FALSE
12 2070
02 0300 <- replace with 20a0 if you just want to target opponents as per usual
(81) <- optional: choose a random target of the above
90
91
61 INDEX OF ATTACK

***************************************
* ATTACK A on opp/ally with greatest/ least STAT *
***************************************

This is an attack that you may have seen a couple of times in vanilla FF7. It allows you to target the enemy with, say, the least HP or the greatest Magic stat; likewise you can target an ally with the highest Dex (taking advantage of haste, anyone?) or the lowest Defence (Barrier, for instance).

As always, this can be preceded with the 'MP checking section' seen elsewhere.

Code: [Select]
12 2070
02 20a0*
02 4100**
80
85***
82
90
91
61 INDEX OF ATTACK

With notes:

* Use 20a0 for opponents, 2080 for allies
** - This is the stat that you're interested in. Values include
HP - 4160   
MP  -  4140
MaxHP - 4180   
MaxMP  - 4150
STR  - 4068   
MAG  - 4070
DEF - 4100   
MDEF  - 4110
DEX - 40a0   
LUCK  - 40a8
LEVEL  - 4048   
EVADE  - 4078
MagEVADE - 4268

*** - 85 looks for the target with the smallest value of the stat we're interested in; 84 looks for the target with the largest.

I think that with these, we more or less have the basic plugins for just about every function. Now the issue is to refine what we have (if you want), mark up the typical values for 'address of next plugin' in each plugin, and maybe add a few specialist plugins for specific circumstances / consider writing counter and death scripts.
« Last Edit: 2010-07-10 22:23:43 by Bosola »

nikfrozty

  • No life
  • *
  • Posts: 1232
  • Karma: 0
  • Cloud kicks Sephiroth's Butt Anytime
    • View Profile
Welder: A little AI Template Project
« Reply #26 on: 2010-05-23 12:40:45 »
Sorry for butting in your topic but really curious in how you mod the PSX version. What do you actually mod in the PSX version?? The files inside the cd?? And after modding it can you still play it on the PlayStation or only in the emulator??
« Last Edit: 2010-05-23 13:51:25 by nikfrozty »

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #27 on: 2010-05-23 13:04:39 »
Quote
Insert Quote
Sorry for butting in your topic but really curios in how you mod the PSX version. What do you actually mod in the PSX version?? The files inside the cd?? And after modding it can you still play it on the PlayStation or only in the emulator??

Use CD Mage or similar software to extract files from ISO. Use CD Mage or similar software to reinsert without trying to reauthor ISO. Done.

Will play on original equipment if modified to run 'backup' CDs (a 'chipped' playstation).

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #28 on: 2010-06-06 17:12:02 »
Just had a thought - is my 'find enemies withOUT status Y' plugin broken? Think about it:

Code: [Select]
MAIN: IS OPP IN STATUS --?

13 1000
02 20a0 <2080 for allies>
00 <status flag address in memory - 4001 for Critical>
80
90
<this has just created a list of enemies and their critical statuses>
03 1000
60 01
45 <makes game look for enemies WITHOUT the status>
70 <address in AI of next plugin / footer>
12 2070
03 1000
82
90
91
61 <index of attack>

Looks fine, but the target is wrong. This will still find opponents who HAVE the status as a target, won't it? Would using the target code

12 2070
03 1000
37
82
90
91
61 <attack index...>

work?

Edit: Tried it, makes enemy hit everyone EXCEPT those in localvar. Back to the drawing board.

--

Use this plugin, then, for RandomOpp/AllyWith(Out)X

12    0300
02    20A0
00    40XX
80
60    01
40
82
90
02    0300
70    next
12    2070
02    0300
90
« Last Edit: 2010-06-06 20:56:37 by Bosola »

NFITC1

  • No life
  • *
  • Posts: 2845
  • Karma: 71
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #29 on: 2010-06-07 16:42:00 »
Looks like your logic is a bit backwards:

Just had a thought - is my 'find enemies withOUT status Y' plugin broken? Think about it:

Code: [Select]
MAIN: IS OPP IN STATUS --?

13 1000
02 20a0 <2080 for allies>
00 <status flag address in memory - 4001 for Critical>
80
90
<this has just created a list of enemies and their critical statuses>
03 1000
60 01
45 <makes game look for enemies WITHOUT the status>
70 <address in AI of next plugin / footer>
12 2070
03 1000
82
90
91
61 <index of attack>

Looks fine, but the target is wrong. This will still find opponents who HAVE the status as a target, won't it? Would using the target code

When you're using the 45, that is only going to check if there IS a target in critical condition. Look at the logic:

Code: [Select]
LocalVar:1000 <- AllOpponentMask.Status:NearDeath
If ( (LocalVar:1000 < 1) )
{
TargetMask <- RandomBit(LocalVar:1000)
POP(Previously loaded attack index)
...

Oops. This is only going to work if NO target has NearDeath status and you want to know if there ARE targets with this status. NOTing it seems like it would work. Indeed the If statement triggers since it's less than 0 (I guess variables are signed), but then the mask is backwards and indicating the ones that are not NearDeath.

Try this instead:

Code: [Select]
12  1000  <- don't make this a DWord, Target can only be a Word
02  20A0
00  4001  <- whatever status
80
60  00    <- does not have, make '1' to retrieve those that have the status
40
82        <- leave out to target all
90
02  1000
70  <end> <- if no such target   (If LocalVar:1000 <> 0)
12  2070
02  1000
90

Code: [Select]
LocalVar:1000 <- RandomBit( (AllOpponentMask.Status:NearDeath == 0) )
If (LocalVar:1000)
{
TargetMask <- LocalVar:1000
...

When in doubt with regards to targets with status, ask Prof. Eligor. He knows lots about it. ;)

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #30 on: 2010-06-07 20:21:13 »
Bit of a tangent, but I had another, similar logic issue with an original version of the header, through a misunderstanding of opcode documentation:

Quote
Popped:  top two values on stack
Pushed:  non-zero value if first pop is less than second pop.

I assumed the 'first pop' was pop1, the second pop2, imagining the stack was:

<top>
pop1
pop2
<bottom>

Actually, 'first pop' refers to pop2, 'second' to pop1.

The difference is important. Imagine

PUSH 0300
PUSH 0001
45 LESSTHAN

I assumed this would ask if 1 < 0300. It doesn't. It does the opposite.

Anyway.

Code: [Select]
12  1000  <- don't make this a DWord, Target can only be a Word
02  20A0
00  4001  <- whatever status
80
60  00    <- does not have, make '1' to retrieve those that have the status
40
82        <- leave out to target all
90
02  1000
70  <end> <- if no such target   (If LocalVar:1000 <> 0)
12  2070
02  1000
90

Resembles my revision:

Code: [Select]
02 2060
02 2140
80
61 AAAA
86
42
70 FFFF
12 1000
02 20A0
00 EEEE
80
90
03 1000
60 01
42
70 FFFF
12 2070
03 1000
82
90
91
61 AAAA

Why do I take the extra step? No idea. I can cut it.

Another thing I'd like to know is: exactly WHAT is on the stack when I push target data? What does this data look like? How is it structured?

I'm going to guess that, at the start of a battle, a 'nomenclature' is formed. This assigns a unit to each bit in a word, so there can be up to sixteen targets. Target data gives the bits of all targets relevant to a particular search. So,

If we have our bits referring to

Cloud / Tifa / JenovaSYNTH-Body / Cait Sith / JenovaSYNTH-LeftArm / JenovaSYNTH-RightArm

a request for 'all allies' (from Jenova's point of view) pushes

0 - 0 - 1 - 0 - 1 - 1

a request for 'all opps'

1 - 1 - 0 - 1 - 0 - 0

all active

1 - 1 - 1 - 1 - 1 - 1

etc.

Am I right?

Also, I believe Sir Eligor is well educated in 'elimination' too - targeting each member of the party, never hitting one more than others with his laser attack. I may have to ask him for a demonstration of this unusual skill.

My question is, though - why are the enemies so inconsistently written? Some are wonderfully complex and interesting bouts that last 30 seconds due to some local balance issue, others are little more than RAN MOD2 - if zero, use X, if one, use Y, where X and Y are only marginally different to one another anyhow. I wonder who wrote them - and how.
« Last Edit: 2010-06-07 20:41:26 by Bosola »

NFITC1

  • No life
  • *
  • Posts: 2845
  • Karma: 71
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #31 on: 2010-06-08 13:54:35 »
Bit of a tangent, but I had another, similar logic issue with an original version of the header, through a misunderstanding of opcode documentation:

Quote
Popped:  top two values on stack
Pushed:  non-zero value if first pop is less than second pop.

I assumed the 'first pop' was pop1, the second pop2, imagining the stack was:

<top>
pop1
pop2
<bottom>

Actually, 'first pop' refers to pop2, 'second' to pop1.

The difference is important. Imagine

PUSH 0300
PUSH 0001
45 LESSTHAN

I assumed this would ask if 1 < 0300. It doesn't. It does the opposite.

I may have written it wrong in the documentation. I don't really know how it compares, but I'd assume it's top of the stack < next value in stack. If that's not the way it's working then it might be mislabeled and really be greater than or equal to or I got the pops out of order.

Anyway.

Code: [Select]
12  1000  <- don't make this a DWord, Target can only be a Word
02  20A0
00  4001  <- whatever status
80
60  00    <- does not have, make '1' to retrieve those that have the status
40
82        <- leave out to target all
90
02  1000
70  <end> <- if no such target   (If LocalVar:1000 <> 0)
12  2070
02  1000
90

Resembles my revision:

Code: [Select]
02 2060
02 2140
80
61 AAAA
86
42
70 FFFF
12 1000
02 20A0
00 EEEE
80
90
03 1000
60 01
42
70 FFFF
12 2070
03 1000
82
90
91
61 AAAA

Why do I take the extra step? No idea. I can cut it.

It looks good, but you don't need LocalVar:1000 to be a DWord. Just a Word is fine. Since it won't save space either way it doesn't matter.

Another thing I'd like to know is: exactly WHAT is on the stack when I push target data? What does this data look like? How is it structured?

I'm going to guess that, at the start of a battle, a 'nomenclature' is formed. This assigns a unit to each bit in a word, so there can be up to sixteen targets. Target data gives the bits of all targets relevant to a particular search. So,

If we have our bits referring to

Cloud / Tifa / JenovaSYNTH-Body / Cait Sith / JenovaSYNTH-LeftArm / JenovaSYNTH-RightArm

a request for 'all allies' (from Jenova's point of view) pushes

0 - 0 - 1 - 0 - 1 - 1

a request for 'all opps'

1 - 1 - 0 - 1 - 0 - 0

all active

1 - 1 - 1 - 1 - 1 - 1

etc.

Am I right?

Essentially yes. Looking through several of the enemy's AI (Eligor's and Emerald Weapon's in particular) you notice that they just push flagged bits into variables and treat those as pointers to actors (this is how I'm going to refer to "battle participants" from now on, that was a clunky way of saying it in the first place). When getting a mask of actors, there's just one Word associated with the formation that indicates if there's a participant or not. Bits 0-2 refer to player characters while bits 4-9 refer to enemies (which furthers my theory that there used to be four characters allowed in a battle. If you look at the formation data, the first enemy listed is bit 3, the second is bit 4, etc.

Code: [Select]
Bit     Actor
0       Player Character Position 1
1       Player Character Position 2
2       Player Character Position 3
3       Not Used
4       Enemy in slot 1
5       Enemy in slot 2
6       Enemy in slot 3
7       Enemy in slot 4
8       Enemy in slot 5
9       Enemy in slot 6
A       Not Used
B       Not Used
C       Not Used
D       Not Used
E       Not Used
F       Not Used

Also, I believe Sir Eligor is well educated in 'elimination' too - targeting each member of the party, never hitting one more than others with his laser attack. I may have to ask him for a demonstration of this unusual skill.

Looks to me that Eligor has a tendency to hit the middle Player Character more than the wingmates. I believe at this point in the game, unless you messed with the formation, Cloud would be in the center.

My question is, though - why are the enemies so inconsistently written? Some are wonderfully complex and interesting bouts that last 30 seconds due to some local balance issue, others are little more than RAN MOD2 - if zero, use X, if one, use Y, where X and Y are only marginally different to one another anyhow. I wonder who wrote them - and how.

I think it's obvious that you would want bosses and rare encounters to have more challenging and complex behaviors. Certain special battles like Grangalan and Midgar Zolom would need some complexity to them too. Other than that most enemies are only vicious based on their stats. It's better to make many simple enemies than to make fewer "intelligent" enemies. Gives the game more variety and the player rarely knows the difference in how complex the code is. I was shocked to see how complicated Eligor was. He didn't seem to me to be a very complicated enemy. In fact, his code could be dramatically reduced in size and still retain all its functionality. I don't know who wrote it, but I'd kind of like to slap him (or her or them) in the face(s) for being so complex about it.

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #32 on: 2010-06-08 18:45:39 »
Quote
I think it's obvious that you would want bosses and rare encounters to have more challenging and complex behaviors. Certain special battles like Grangalan and Midgar Zolom would need some complexity to them too. Other than that most enemies are only vicious based on their stats. It's better to make many simple enemies than to make fewer "intelligent" enemies. Gives the game more variety and the player rarely knows the difference in how complex the code is. I was shocked to see how complicated Eligor was. He didn't seem to me to be a very complicated enemy. In fact, his code could be dramatically reduced in size and still retain all its functionality. I don't know who wrote it, but I'd kind of like to slap him (or her or them) in the face(s) for being so complex about it.

I disagree there, actually. One of my aims in modifying enemies for Rebirth has been to create a smaller number of longer, more interesting and varied fights. As is, you end up with lots of different enemies that play almost exactly the same. Compare with something like X, which cuts numbers and gives enemy differences far more impact. Actually, that seems to underline X full stop: pruning statuses, making those remaining more problematic; cutting elements, but putting them at the front of combat. In VIII, by contrast, there's a whole plethora of statuses that are each hardly ever invoked, and many not worth taking note of. Even IX has this problem (honestly, who actually cared about Cold and Trouble?).

Akari

  • Freak
  • *
  • Posts: 761
  • Karma: 26
    • View Profile
Re: Welder: A little AI Template Project
« Reply #33 on: 2010-06-11 11:55:19 »
Bit of a tangent, but I had another, similar logic issue with an original version of the header, through a misunderstanding of opcode documentation:

Quote
Popped:  top two values on stack
Pushed:  non-zero value if first pop is less than second pop.

I assumed the 'first pop' was pop1, the second pop2, imagining the stack was:

<top>
pop1
pop2
<bottom>

Actually, 'first pop' refers to pop2, 'second' to pop1.

The difference is important. Imagine

PUSH 0300
PUSH 0001
45 LESSTHAN

I assumed this would ask if 1 < 0300. It doesn't. It does the opposite.

I may have written it wrong in the documentation. I don't really know how it compares, but I'd assume it's top of the stack < next value in stack. If that's not the way it's working then it might be mislabeled and really be greater than or equal to or I got the pops out of order.

Everything is right. 0x4X command work as follows:
1) Pop value from stack and store it to slot1
2) Pop value from stack and store it to slot0
3) Call function that does comparsion depending on X in 0x4X
Code: [Select]
switch ()
{
    case 0: return (slot0_value == slot1_value) ? 1 : 0;
    case 1: return (slot0_value != slot1_value) ? 1 : 0;
    case 2: return (slot0_value >= slot1_value) ? 1 : 0;
    case 3: return (slot0_value <= slot1_value) ? 1 : 0;
    case 4: return (slot0_value >  slot1_value) ? 1 : 0;
    case 5: return (slot0_value <  slot1_value) ? 1 : 0;
}

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #34 on: 2010-06-12 20:59:37 »
Thanks for the input, Akari.

This confirms my suspicions, then. Namely, my belief that opcode descriptions in PC's help are open to misreading. Consider the following case.

Quote

12   2070
02   20A0
82
90
60   20
61   0112 'Bite'
60   80
60   FF
42     'Greater than'
70   0021
12   2070
02   20A0
82
90
91
61   0113 'Tentacle'
92

The help file says that 42 returns a 1 if "the first value popped is greater than or equal to the second value popped". What is the 'first value'?

Convention suggests that the term 'first pop' refers to the FF, the 'second pop' to the 80. This would suggest that in this case, 42 would always yield a '1', prompting the game to pop the attack data and replace it with the index for 'Tentacle'. In battle, the enemy (actually a guard dog) always bites.

In practice, the help file uses 'first pop' to refer to the 'earliest', as it were. This creates ambiguity.

I think, as a rule, it's *always* better to refer to 'top' - 'bottom' / 'upper' - 'lower' when talking about stacks. Still, no harm done.


Edit:

I just don't know any more. I can't deal with this asm. It's impossible to follow, labarynthine and ambiguous. Why is my creature not acting in the way I anticipate? What keeps going wrong?
« Last Edit: 2010-06-12 21:29:55 by Bosola »

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #35 on: 2010-06-12 21:59:46 »
Double-post - some more investigation has suggested something else, which may be the issue.

Essentially, I've being trying to get my 'variable header' working - that's the header that uses data set by 'Leaders' to determine intelligence.

The issue seems to be that 'Leaders' aren't changing data in LocalVars as part of their Counter:Death scripts.

I had a guard hound use the following AI script:

00   0000
70   0024 (points to second 93 opcode)
93   "There is a ONE on stack"
72   0041 (jumps to 73 opcode)
93   "There was a ZERO on stack"
73

I then had an MP use the following INIT (pre-battle) script:

10   0000
60   00
90
73

And the following Counter-Death script:

12   0000
60   01
90
73

The hound, however, keeps shouting out "There was a ZERO on stack". This hints that though logic may be an issue, the real problem has to do with this Counter script.

I may have to re-write the variable header to actively look out for allies of a certain 'index', and then alter a variable 'IQ' internally. I wish someone had mentioned this issue beforehand.

Terence Fergusson

  • Insane poster
  • *
  • Posts: 262
  • Karma: 1
    • View Profile
Re: Welder: A little AI Template Project
« Reply #36 on: 2010-06-13 23:25:01 »
The issue seems to be that 'Leaders' aren't changing data in LocalVars as part of their Counter:Death scripts.

No, the real problem is that LocalVars are *specific to the object that uses them*.  You can't change Object 1's LocalVar:0000, and expect to read the changed value out of Object 2's LocalVar:0000.  They're two completely different variables.

Look at Warning Board's script for examples of how to use the Battle Player Block to use the specially set aside variables in the Battle Player Block to change the behavior of other enemies ([41A0], [41C0], [41E0] and [4200] at least are set aside for this).

Also look at Manhole's script on how to use *Battle* Variables for storing information ([2180]/[2190]/[21A0] at least are set aside for this), which any enemy in battle can use.


Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #37 on: 2010-06-14 17:28:47 »
This is very new information to me. And, I think, to other board members - more than once, I've heard people warn each other not to let different creatures use the same LocalVar numbers. I suppose the assumption was that 'Local' referred to a battle-only namespace, as opposed to global vars.

I'll play with the battlevars right away. I think this is the 643,543rd time I've found your apparently limitless knowledge of VII useful, so thanks... again.

Oh, I'll have to properly confirm it, but testing so far suggests that the stack is cleared between script calls. For instance,

INIT SCRIPT:
60 20 : PUSH byte 20
60 22 : PUSH byte 22
73 : end

MAIN:
92 : attack
73 : end

...does not force Unknown3 to use Bolt2. Or anything, for that matter. Pretty sure you or Akari wanted confirmation on it.

Akari

  • Freak
  • *
  • Posts: 761
  • Karma: 26
    • View Profile
Re: Welder: A little AI Template Project
« Reply #38 on: 2010-06-14 19:41:37 »
This is very new information to me. And, I think, to other board members - more than once, I've heard people warn each other not to let different creatures use the same LocalVar numbers. I suppose the assumption was that 'Local' referred to a battle-only namespace, as opposed to global vars.

I'll play with the battlevars right away. I think this is the 643,543rd time I've found your apparently limitless knowledge of VII useful, so thanks... again.

Oh, I'll have to properly confirm it, but testing so far suggests that the stack is cleared between script calls. For instance,

INIT SCRIPT:
60 20 : PUSH byte 20
60 22 : PUSH byte 22
73 : end

MAIN:
92 : attack
73 : end

...does not force Unknown3 to use Bolt2. Or anything, for that matter. Pretty sure you or Akari wanted confirmation on it.

Stack is reseted before script call. If you want to store value you need to do this in some variable.
Why do you even think about stack. It's now that hard to create some abstraction to be closer to original code.

Example output from my dumper:

Code: [Select]
script 1:
0x0000 [0x0004 + 0x00] = b(0x00);
0x0006 [0x0008 + 0x00] = b(0x00);
0x000c [0x0000 + 0x00] = b(0x00);
0x0012 JumpIfNotAllFriendsInFrontRow(0x0029);
0x001f [0x0008 + 0x00] = b(b[0x0008 + 0x00] & 0x01);
0x0029 JumpIfNotAllFriendsInBackRow(0x0040);
0x0036 [0x0008 + 0x00] = b(b[0x0008 + 0x00] & 0x02);
0x0040 JumpIfNotAllOpponentsInFrontRow(0x0057);
0x004d [0x0004 + 0x00] = b(b[0x0004 + 0x00] & 0x01);
0x0057 JumpIfNotAllOpponentsInBackRow(0x006e);
0x0064 [0x0004 + 0x00] = b(b[0x0004 + 0x00] & 0x02);
0x006e JumpIfSelfNotInFrontRow(0x0084);
0x007b [0x0000 + 0x00] = b(0x00);
0x0081 0x72 JumpTo(0x008a)
0x0084 [0x0000 + 0x00] = b(0x01);
0x008a JumpIfRandomNot(1/0x03)(0x00c7);
0x0092 JumpIf((b[0x0004 + 0x00] & 0x02) != 0x02)(0x00b0);
0x009e SetRandomOpponentInBackRowToAttack();
0x00ad 0x72 JumpTo(0x00bd)
0x00b0 SetRandomOpponentWithLowestCurrentHP();
0x00bd [0x000c + 0x00] = h(0x0113);
0x00c4 0x72 JumpTo(0x00f9)
0x00c7 JumpIf((b[0x0004 + 0x00] & 0x01) != 0x01)(0x00e5);
0x00d3 SetRandomOpponentInFrontRowToAttack();
0x00e2 0x72 JumpTo(0x00f2)
0x00e5 SetRandomOpponentWithLowestCurrentHP();
0x00f2 [0x000c + 0x00] = h(0x0112);
0x00f9 RunCommand(0x20, h[0x000c + 0x00]);
0x00ff 0x73 FinishScript()
« Last Edit: 2010-06-14 19:50:07 by Akari »

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #39 on: 2010-06-14 20:23:29 »
Stack is reseted before script call. If you want to store value you need to do this in some variable.

Yeah, but there was some uncertainty over whether or not the stack got wiped. I just wanted to confirm the assumption that it gets cleared.

Why do you even think about stack. It's now that hard to create some abstraction to be closer to original code.

Example output from my dumper:

Code: [Select]
script 1:
0x0000 [0x0004 + 0x00] = b(0x00);
0x0006 [0x0008 + 0x00] = b(0x00);
0x000c [0x0000 + 0x00] = b(0x00);
0x0012 JumpIfNotAllFriendsInFrontRow(0x0029);
0x001f [0x0008 + 0x00] = b(b[0x0008 + 0x00] & 0x01);
0x0029 JumpIfNotAllFriendsInBackRow(0x0040);
0x0036 [0x0008 + 0x00] = b(b[0x0008 + 0x00] & 0x02);
0x0040 JumpIfNotAllOpponentsInFrontRow(0x0057);
0x004d [0x0004 + 0x00] = b(b[0x0004 + 0x00] & 0x01);
0x0057 JumpIfNotAllOpponentsInBackRow(0x006e);
0x0064 [0x0004 + 0x00] = b(b[0x0004 + 0x00] & 0x02);
0x006e JumpIfSelfNotInFrontRow(0x0084);
0x007b [0x0000 + 0x00] = b(0x00);
0x0081 0x72 JumpTo(0x008a)
0x0084 [0x0000 + 0x00] = b(0x01);
0x008a JumpIfRandomNot(1/0x03)(0x00c7);
0x0092 JumpIf((b[0x0004 + 0x00] & 0x02) != 0x02)(0x00b0);
0x009e SetRandomOpponentInBackRowToAttack();
0x00ad 0x72 JumpTo(0x00bd)
0x00b0 SetRandomOpponentWithLowestCurrentHP();
0x00bd [0x000c + 0x00] = h(0x0113);
0x00c4 0x72 JumpTo(0x00f9)
0x00c7 JumpIf((b[0x0004 + 0x00] & 0x01) != 0x01)(0x00e5);
0x00d3 SetRandomOpponentInFrontRowToAttack();
0x00e2 0x72 JumpTo(0x00f2)
0x00e5 SetRandomOpponentWithLowestCurrentHP();
0x00f2 [0x000c + 0x00] = h(0x0112);
0x00f9 RunCommand(0x20, h[0x000c + 0x00]);
0x00ff 0x73 FinishScript()

...This is a good opportunity to ask more about the AI code and the battle engine. You seem to be suggesting that the AI code is not just some assembly-like scripting language, but was compiled from some other source. What sort of source? If compiled, could some of the 'oddities' of certain scripts (see Ultimate Weapon's HP code) be down to compiler issues? And what *actually* happens to the AI code every frame? Does it just push the battle engine around as a scripting language, or does it get transformed into some intermediate form, like bytecode?

Akari

  • Freak
  • *
  • Posts: 761
  • Karma: 26
    • View Profile
Re: Welder: A little AI Template Project
« Reply #40 on: 2010-06-15 07:32:41 »
Why do you even think about stack. It's now that hard to create some abstraction to be closer to original code.

Example output from my dumper:

Code: [Select]
script 1:
0x0000 [0x0004 + 0x00] = b(0x00);
0x0006 [0x0008 + 0x00] = b(0x00);
0x000c [0x0000 + 0x00] = b(0x00);
0x0012 JumpIfNotAllFriendsInFrontRow(0x0029);
0x001f [0x0008 + 0x00] = b(b[0x0008 + 0x00] & 0x01);
0x0029 JumpIfNotAllFriendsInBackRow(0x0040);
0x0036 [0x0008 + 0x00] = b(b[0x0008 + 0x00] & 0x02);
0x0040 JumpIfNotAllOpponentsInFrontRow(0x0057);
0x004d [0x0004 + 0x00] = b(b[0x0004 + 0x00] & 0x01);
0x0057 JumpIfNotAllOpponentsInBackRow(0x006e);
0x0064 [0x0004 + 0x00] = b(b[0x0004 + 0x00] & 0x02);
0x006e JumpIfSelfNotInFrontRow(0x0084);
0x007b [0x0000 + 0x00] = b(0x00);
0x0081 0x72 JumpTo(0x008a)
0x0084 [0x0000 + 0x00] = b(0x01);
0x008a JumpIfRandomNot(1/0x03)(0x00c7);
0x0092 JumpIf((b[0x0004 + 0x00] & 0x02) != 0x02)(0x00b0);
0x009e SetRandomOpponentInBackRowToAttack();
0x00ad 0x72 JumpTo(0x00bd)
0x00b0 SetRandomOpponentWithLowestCurrentHP();
0x00bd [0x000c + 0x00] = h(0x0113);
0x00c4 0x72 JumpTo(0x00f9)
0x00c7 JumpIf((b[0x0004 + 0x00] & 0x01) != 0x01)(0x00e5);
0x00d3 SetRandomOpponentInFrontRowToAttack();
0x00e2 0x72 JumpTo(0x00f2)
0x00e5 SetRandomOpponentWithLowestCurrentHP();
0x00f2 [0x000c + 0x00] = h(0x0112);
0x00f9 RunCommand(0x20, h[0x000c + 0x00]);
0x00ff 0x73 FinishScript()

...This is a good opportunity to ask more about the AI code and the battle engine. You seem to be suggesting that the AI code is not just some assembly-like scripting language, but was compiled from some other source. What sort of source? If compiled, could some of the 'oddities' of certain scripts (see Ultimate Weapon's HP code) be down to compiler issues? And what *actually* happens to the AI code every frame? Does it just push the battle engine around as a scripting language, or does it get transformed into some intermediate form, like bytecode?

Of course it was normal script that was later compiled to bytecode. Using this instructions you can implement almost all known constructions in languages. For example Lua language compile


Code: [Select]
y = 5
print(y)

into

Code: [Select]
LOADK           0 1     ; 5
SETGLOBAL       0 0     ; y
GETGLOBAL       0 2     ; print
GETGLOBAL       1 0     ; y
CALL            0 2 1
RETURN          0 1 0

Let's write decompiler already =)

And what stranges in Ultima Weapon's script you talking about?

Ai code already bytecode that executed when it's called (when timer filled or when some conditions met).
« Last Edit: 2010-06-15 07:36:15 by Akari »

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #41 on: 2010-06-15 14:53:21 »
Of course it was normal script that was later compiled to bytecode.

So it's bytecode, huh? Do portions of the AI scripting code have 1:1 relations to the binary they eventually yield? Is it bytecode in the sense of 'generalized, partially abstracted ASM'? That surprises me, actually. Especially considering how complex things like calling for targets would be on the machine level.

Quote
And what stranges in Ultima Weapon's script you talking about?

Ai code already bytecode that executed when it's called (when timer filled or when some conditions met).

I don't have access to the code right now, but IIRC, Ultima Weapon uses a very verbose method of storing its HP in a GlobalVar.

Terence Fergusson

  • Insane poster
  • *
  • Posts: 262
  • Karma: 1
    • View Profile
Re: Welder: A little AI Template Project
« Reply #42 on: 2010-06-16 01:40:53 »
This is very new information to me. And, I think, to other board members - more than once, I've heard people warn each other not to let different creatures use the same LocalVar numbers. I suppose the assumption was that 'Local' referred to a battle-only namespace, as opposed to global vars.

Well, my very first post on the subject of AI scripting all those years ago did show where each set of variables were mapped to: such that 0000-1FFF and 4000+ were mapped to blocks that were unique to each object, but any call to 4000 would retrieve *all* blocks rather than just the object's own block.

Which brings up another thing I've noticed in this thread which is ill-advised: don't use LocalVar 0400 or higher.  Only 128 bytes are given to each enemy, so anything higher than that will start overwriting other data.

So it's bytecode, huh? Do portions of the AI scripting code have 1:1 relations to the binary they eventually yield? Is it bytecode in the sense of 'generalized, partially abstracted ASM'? That surprises me, actually. Especially considering how complex things like calling for targets would be on the machine level.

Just because something is bytecode doesn't mean it's machine code.  It's just a form of code that can be quickly read by an interpreter (in that the operands are all single bytes).  Game designers don't program enemies, rooms or other scripts in bytecode: they use some higher level custom language (usually designed specifically for the game), which they then compile down into an easier format for the game to read.  The resulting code doesn't take up very much memory and is relatively fast to run compared to translating human readable code in real-time (which would be both slower and take up more space).

I don't have access to the code right now, but IIRC, Ultima Weapon uses a very verbose method of storing its HP in a GlobalVar.

It's only verbose because AccessGlobalVar can only read and write Bytes.  So storing what requires at least 3 bytes is going to require a bit of maths.  There's obviously going to be some compiler artifacts around in the code, which is why a lot of the enemy scripts can be rewritten to be more efficient in bytecode, but some of it is just working around the limitations of the system.
« Last Edit: 2010-06-16 02:07:47 by Terence Fergusson »

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #43 on: 2010-06-16 09:42:35 »
Ah, so it's a bytecode in the strict sense of 'a code with one-byte opcodes'. I was thinking in terms of the 'bytecode' people refer to when talking about Java etc. It is, technically speaking, the same principle - something between human-readable code and actual machine action that can easily be dealt with by an interpreter.

Quote
Which brings up another thing I've noticed in this thread which is ill-advised: don't use LocalVar 0400 or higher.  Only 128 bytes are given to each enemy, so anything higher than that will start overwriting other data.

Asking a bit much, but does anyone actually have a complete memory map? I'm wondering what we get after 0400.

I think at some point I'd like to get into this further, perhaps by using disassemblers. How did you get into reverse engineering yourself?

Akari

  • Freak
  • *
  • Posts: 761
  • Karma: 26
    • View Profile
Re: Welder: A little AI Template Project
« Reply #44 on: 2010-06-16 12:38:36 »
Asking a bit much, but does anyone actually have a complete memory map? I'm wondering what we get after 0400.

http://q-gears.svn.sourceforge.net/viewvc/q-gears/trunk/reversing/ffvii/ffvii_battle/ffvii_address.txt?revision=505&view=markup

What are you looking is at

800f87f0 temp variable unit data structure. (size 0x80). (address 0x0000).
800f83a4 battle global structure. (address 0x2000).
800f83e0 unit data structure. (size 0x68). (address 0x4000).

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #45 on: 2010-06-16 18:22:25 »
Interesting stuff. What's the actual range of values that can be written from within the AI script? Could AI code, for instance, clear the battle queue (which seems to come shortly after the 2000 addresses)?

Akari

  • Freak
  • *
  • Posts: 761
  • Karma: 26
    • View Profile
Re: Welder: A little AI Template Project
« Reply #46 on: 2010-06-16 18:32:48 »
Interesting stuff. What's the actual range of values that can be written from within the AI script? Could AI code, for instance, clear the battle queue (which seems to come shortly after the 2000 addresses)?

Even if it can - doesn't mean you should.

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #47 on: 2010-06-16 18:56:04 »
Of course! I'm just curious how values 0-FFFF are assigned in the memory map.

Bosola

  • Fire hazard!
  • No life
  • *
  • Posts: 1753
  • Karma: 15
    • View Profile
    • My YouTube Channel
Re: Welder: A little AI Template Project
« Reply #48 on: 2010-07-07 21:10:43 »
To make a little clearer: if you want variable 'IQ' levels, you need to write the custom IQ in AI setups / death counters to address 21A0.

It looks to me as though the following addresses can be used for BattleVars, though I may need to test further:

21a0
21a8
21b0
21b8
21c0-c8

When I'm a little more inclined, I'll probably clean all the material up and link to it from the initial post.