[FF7] AI Template Project - Welder

  • Thread starter Thread starter Bosola
  • Start date Start date
Status
Not open for further replies.
From what I can understand, this sounds good.

Not entirely on topic, but I was thinking... Materia is readily available to anyone who goes to a town. It would be cool to have enemies, maybe just humanoid enemies, have some materia combinations. Some even Final Attack+Revive or something. You could have plugins for materia combinations, most just using the ones from the town they are near, and have it randomly assign those to the enemies at the start of the battle. The same enemy can have different attacks. Each materia plugin could have a code, like All+Cure will be assigned the restorative code, or Fire would be given the magic attack code.
 
This plugin heals allies when they're in low health:

Code: [Select]
Code:
02 206002 41408061 <healing spell index>864270 <address of next plugin - should be current + 31h, but do check>12 100002 208003 41608002 208003 41808060 Y3360 X32439002 100060 14270 <address of next plugin - should be current + Fh, but do check>12 207002 100082909161 <healing spell index>
Replace 'healing spell index' with the attack index of Cure, Cure2, etc, and put in the address of the next plugin where told to (if no other plugins come afterwards, you'd put the address of the footer, obviously). X/Y is the fraction of HP a creature must be at or below to be considered for healing. If several creatures meet this requirement, a random one will be selected. This plugin checks for MP, and will not try and cast a spell beyond its MP pool.

I have tested this, and it seems to work. I would appreciate any feedback from other testers, though.

Specifically, I tried it on a Nerosuferoth, turning heatwing into a curative 'spell'. The code is just the header and plugin:

Code: [Select]
Code:
12 207002 20A0829060 2061 01C5 <- Default attack, which I named 'Standard'8160 043460 02 <- 'IQ' is 3/44270 0054 <- Goes to footer***** THE FOOTER HAS ENDED ********** NOW THE PLUGIN STARTS *****02 206002 21408061 01C6 <- The index of the healing attack (which I named 'Healwing'), checks MP requirements864270 0054 <- Goes to next plugin, or in this case, the footer (because I'm not using any other plugins)13 1000 <- I'm using this to hold lists of creatures with HP below X/Y02 208003 41608002 208003 41808060 033360 02 <- In this case, X/Y = 2/3rds, so a creature with 66% or less HP will qualify32439003 100060 01 <- Checks to see if any such creatures actually meet these requirements4270 0054 <- Goes to next plugin, or in this case, the footer (because I'm not using any other plugins)12 207003 100082909161 01C6 <- We've just popped the default attack, and we're now pushing the 'healwing' index***** THE PLUGIN ENDS HERE *****< The next plugin would start here - if the above was not the last. >< In this case, there are no other plugins to include, so we proceed directly to the footer >***** THE FOOTER: *****92 <- This is the footer, or opcode 92. In this case, it's at address 54h.
You could have plugins for materia combinations, most just using the ones from the town they are near, and have it randomly assign those to the enemies at the start of the battle. The same enemy can have different attacks. Each materia plugin could have a code, like All+Cure will be assigned the restorative code, or Fire would be given the magic attack code.
This would be sort of doable, yes. You'd need to put random values into certain variables at the battle start, and then create plugins that only 'activate' when those variables say so. The AI would end up quite bulky this way, however.

EDIT: Addendum

You should be able to modify this plugin to target only particular creatures. This is useful when one species 'looks after' another, or when a creature's friend absorbs an elemental attack (and can thus be healed with non-curative spell). You would do it like

Code: [Select]
Code:
12 100002 208002 41208060 <species index - see indeces listed in formation>409002 206002 41408061 <heal/ elemental attack index>864270 <next plugin>12 100002 100003 41608002 208003 41808060 Y3360 X32439002 100060 14270 <next plugin>12 207002 100082909161 <heal/ elemental attack index>
All this does is make a list of allies with ID X and store it in localvar1000. It will then consult with these to check for HP / target critical allies. As a rule, if using both plugins, you should insert this one second to give it higher priority.

Of course, you could always achieve this by creating a list of creatures of species X in the pre-battle script, but that might complicate things too much for the target audience (people who want to at most cut and paste, and maybe tweak the odd variable).

EDIT 2: Another Plugin, this time using attack X on random opp with status Y

If correctly written (and I've leave others to say so or not), this will be an important plugin - useful for not only buffing and debuffing, but things like "Take out opps. in critical", etc.

Here's the essential template

Code: [Select]
Code:
OPTIONAL : CHECK MP02 206002 41408061 <index of spell / attack>864270 <address of next plugin - MP fail>MAIN: IS OPP IN STATUS --?13 100002 20a0 <2080 for allies>00 <status flag address in memory - 4001 for Critical>8090<this has just created a list of enemies and their critical statuses>03 100060 0142 <make 45 to look for enemies WITHOUT the status>70 <address in AI of next plugin / footer>12 207003 100082909161 <index of attack>
So, the monster can attack a low-HP party member to KO them.

This could have several other implementations. Frinstance, a simple debuff plugin:

Code: [Select]
Code:
02 206002 41408061 [index of Silence]864270 <address of next plugin - MP fail>13 100002 20a000 400a <stop>8090<this has just created a list of enemies and their stop statuses>03 100060 0145 <not stop>70 <address in AI of next plugin / footer>12 207003 100082909161 <index of silence>
The only issue with this is that it would keep using debuff spells on party members immune to them, unless we were using the default attack (chance depends on IQ) / higher-priority events came up. Most battles don't last all that long, though, so it wouldn't be that noticeable. You could get around it by adding a 'if Mod of Random and X > Y, go and do something else' piece to the start of the plugin.

More interesting are implementations like

Code: [Select]
Code:
02 206002 41408061 <index of deathblow like attack>864270 <address of next plugin - MP fail>13 100002 20a000 400a <stop>8090<this has just created a list of enemies and their stop statuses>03 100060 014270 <address in AI of next plugin / footer>12 207003 100082909161 <index of deathblow-like attack, that obviously cannot miss an enemy in stop>
With the two variants on this plugin, you've now got a monster who casts stop and then exploits your lack of evasion to rain down a powerful but inaccurate attack.
Likewise with casting only magic on sleeping party members.

MULTIEDIT: BOARD SOFTWARE IS BORKED, WILL NOT LET ME DELETE CODE TAGS FROM POAST. RECTIFY PLZ.
 
Last edited:
MULTIEDIT: BOARD SOFTWARE IS BORKED, WILL NOT LET ME DELETE CODE TAGS FROM POAST. RECTIFY PLZ.
You've got two [cod e] tags in your first code block. Get rid of those and you can get rid of those ending terminators.
 
Aha, never saw those. Guess I should re-edit that to

BOARD SOFTWARE IS NOT BORKED, MY GLASSES ARE
Anyways, here's an idea of the plugins I think need to be sorted out:

- header / footer (done)
- heal /hurt ally / opp with less/more than X/Y HP (done)
- cast X on ally / opp with status X (done)
- cast X on ally/opp with status X AND Y
- cast X on ally/opp with status X OR Y
- cast X on ally/opp with status X AND NOT Y
- cast X on self when opps have status X [eg reflect]
- cast X when in status X
- cast X ally / opp with greatest / least STAT (Magic, Dexterity, Strength, Vitality, HP, MP - whatever)
- cast X on random when a LOCALVAR-a is at 1
- cast X on LOCALVAR-a when LOCALVAR-b is at 1
- random chance of other attack
- escape when all allies inc. self dead OR critical
- cast X when below/above X/Y HP [kamikaze, darkside, etc]

I think with those the vast majority of operations will be covered. Can you think up any more?
 
Last edited:
- cast X on ally/opp with status X AND Y
- cast X on ally/opp with status X OR Y
- cast X on ally/opp with status X AND NOT Y
- cast X on self when opps have status X [eg reflect]
- cast X when in status X
- cast X ally / opp with greatest / least STAT (Magic, Dexterity, Strength, Vitality, HP, MP - whatever)
- cast X on random when a LOCALVAR-a is at 1
- cast X on LOCALVAR-a when LOCALVAR-b is at 1
- random chance of other attack
- escape when all allies inc. self dead OR critical
- cast X when below/above X/Y HP [kamikaze, darkside, etc]
Very easy. Some enemies do these kinds of things.

- cast X on ally/opp with status X AND/OR/AND NOT Y
Code: [Select]
Code:
Most of the other cases you listed are just as easy. Check out Eligor's Main for some ideas.12  207002  20A0  <- enemies, change to 20B0 for allies00  40XX  <- change XX to status18060  01    <- or 00 for NOT having status4002  20A000  40XX  <- status28060  014035        <- this is status1 AND status2, 36 is (status1 OR status2), 37 before the 35 is (status1 AND NOT status2)82        <- random target that fits the above restrictions (optional)90        <- store target60  2061  YYYY  <- attack92Translates:TargetMask <- RandomBit( (AllOpponentMask.Status:status1 == 1)  AND  (AllOpponentMask.Status:status2 == 1) )
 
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]
Code:
12 207002 20a0829060 2061 index of the 'dumb' attack8160 FF3400 0200 <- reads one byte from LOCALVAR 02004370 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]
Code:
10 020060 FF <- this is the default intel level, from 0h to FFh - 80h is about 'half' intelligence90
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]
Code:
<<OPTIONAL MP CHECKING SECTION START>>02 206002 41408061 INDEXOFATTACK864270 ADDRESS OF NEXTPLUGIN<<OPTIONAL MP CHECKING SECTION END>>12 040002 20A000 40XX8060 014002 20A000 40YY8060 014035829002 040070 ADDRESS OF NEXTPLUGIN02 207002 0400909161 FFFF90
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]
Code:
02 206002 41608002 206002 41608060 FF3360 X3242/4370 ADDRESS OF NEXT PLUGIN12 207002 20a082909161 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]
Code:
00 040070 ADDRESS OF NEXT PLUGIN12 207002 0300(81)909161 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]
Code:
12 207002 20a0*02 4100**8085***82909161 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 edited:
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 edited:
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).
 
Just had a thought - is my 'find enemies withOUT status Y' plugin broken? Think about it:

Code: [Select]
Code:
MAIN: IS OPP IN STATUS --?13 100002 20a0 <2080 for allies>00 <status flag address in memory - 4001 for Critical>8090<this has just created a list of enemies and their critical statuses>03 100060 0145 <makes game look for enemies WITHOUT the status>70 <address in AI of next plugin / footer>12 207003 100082909161 <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 edited:
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]
Code:
MAIN: IS OPP IN STATUS --?13 100002 20a0 <2080 for allies>00 <status flag address in memory - 4001 for Critical>8090<this has just created a list of enemies and their critical statuses>03 100060 0145 <makes game look for enemies WITHOUT the status>70 <address in AI of next plugin / footer>12 207003 100082909161 <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]
Code:
LocalVar:1000 <- AllOpponentMask.Status:NearDeathIf ( (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]
Code:
12  1000  <- don't make this a DWord, Target can only be a Word02  20A000  4001  <- whatever status8060  00    <- does not have, make '1' to retrieve those that have the status4082        <- leave out to target all9002  100070  <end> <- if no such target   (If LocalVar:1000 <> 0)12  207002  100090
Code: [Select]
Code:
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. ;)
 
Bit of a tangent, but I had another, similar logic issue with an original version of the header, through a misunderstanding of opcode documentation:

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]
Code:
12  1000  <- don't make this a DWord, Target can only be a Word02  20A000  4001  <- whatever status8060  00    <- does not have, make '1' to retrieve those that have the status4082        <- leave out to target all9002  100070  <end> <- if no such target   (If LocalVar:1000 <> 0)12  207002  100090
Resembles my revision:

Code: [Select]
Code:
02 206002 21408061 AAAA864270 FFFF12 100002 20A000 EEEE809003 100060 014270 FFFF12 207003 100082909161 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 edited:
Bit of a tangent, but I had another, similar logic issue with an original version of the header, through a misunderstanding of opcode documentation:

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]
Code:
12  1000  <- don't make this a DWord, Target can only be a Word02  20A000  4001  <- whatever status8060  00    <- does not have, make '1' to retrieve those that have the status4082        <- leave out to target all9002  100070  <end> <- if no such target   (If LocalVar:1000 <> 0)12  207002  100090
Resembles my revision:

Code: [Select]
Code:
02 206002 21408061 AAAA864270 FFFF12 100002 20A000 EEEE809003 100060 014270 FFFF12 207003 100082909161 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]
Code:
Bit     Actor0       Player Character Position 11       Player Character Position 22       Player Character Position 33       Not Used4       Enemy in slot 15       Enemy in slot 26       Enemy in slot 37       Enemy in slot 48       Enemy in slot 59       Enemy in slot 6A       Not UsedB       Not UsedC       Not UsedD       Not UsedE       Not UsedF       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.
 
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?).
 
Bit of a tangent, but I had another, similar logic issue with an original version of the header, through a misunderstanding of opcode documentation:

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]
Code:
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;}
 
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.

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 edited:
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.
 
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.
 
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.
 
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]
Code:
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 edited:
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]
Code:
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?
 
Status
Not open for further replies.
Back
Top