Target info
This byte is read by two independent paths, and a bit inert in one can still matter in the other. getTargetMaskFromInfo / computeTargetMaskDeadAndSpread decode it when the engine auto-resolves a target mask (AI attacks, auto-summons, item randomization); sub_4AB190 and the cursor engine sub_4AA1D0 decode it when a player aims a command by hand. 0x04 is the case that bites: no reader at all on the auto-resolve side, the cursor’s side switch on the player side.
| ID | Description |
|---|---|
| 0x0000 | None |
| 0x0001 | Dead — targets KO’d units (adds the revive bit 0x4000 to the target mask). In the player battle menu this only takes effect when the entry also has Attack flag 0x80 set, which is what opens the cursor to KO’d units in the first place |
| 0x0002 | Multi-target spread — adds mask bit 0x2000 (TARGET_MASK32_TARGET_SEVERAL; IDA TARGET_INFO_SEVERAL_HIT_SPREAD) |
| 0x0004 | Side switch — lets the player move the target cursor to the other side. Suppressed by 0x0008; see below. Skipped by the AI target-mask decoders, which is why it looks unused from that path |
| 0x0008 | Single Side — locks the cursor to one side; overrides 0x0004 |
| 0x0010 | Scope pair with 0x0020 — see below. Alone: one unit of the chosen side |
| 0x0020 | Scope pair with 0x0010 — see below. Alone: everyone on BOTH sides |
| 0x0040 | Enemy — selects the side opposing the actor, and it alone decides which side is targeted; 0x04/0x08 only govern whether the player may leave it. See below |
| 0x0080 | Skip target selection — the command takes no target at all; the engine picks one and commits immediately. See below |
0x0004 is the cursor’s side switch, not an unused bit
0x0004 has no reader in getMagicTargetMask / computeTargetMaskDeadAndSpread, which is why it was previously recorded as unused — but those only cover the AI / auto-resolve path. The player target cursor reads it, twice.
sub_4AB190 (0x4AB190), which sets the cursor up, turns it into the set of target name windows to open:
if ( ((a2 >> 2) & 3) == 1 ) // a2 = TargetInfo: 0x04 set AND 0x08 clear
v10 = 3; // both windows
else
v10 = 2 - ((a2 & 0x40) != 0); // one side only, picked by 0x40 Enemy
*(_BYTE *)(v9 + 11) = v10;
v9 + 11 is consumed in sub_4AB4F0 (0x4AB4F0), where bit 1 opens the party window (fixed width 92, height from ctx+22 = party row count) and bit 0 opens the enemy window (width from ctx+16, the value sub_4AA920 computes from the monster-slot name widths). The 0x40 branch above confirms the assignment: Enemy set gives 1 = the enemy window, clear gives 2 = the party window.
The cursor engine sub_4AA1D0 (0x4AA1D0) then gates the actual side swap on the same expression, at 0x4AA644:
and al, 0Ch
cmp al, 4 ; TargetInfo & 0x0C == 0x04
jnz ... ; otherwise no switch is possible
Inside that branch, a direction press swaps the current-side mask a1[7] between 7 (party) and 78h (monsters), saving and restoring a remembered cursor slot per side in a1[16] / a1[17] and playing cursor sound 1. That is the player pressing left/right to cross sides.
Bits 2-3 are one 2-bit field, not two flags
0x04 and 0x08 are never read independently — every reader tests the pair TargetInfo & 0x0C, and only the value 0x04 does anything:
& 0x0C | Meaning | Vanilla entries |
|---|---|---|
0x00 | Locked to one side (the side 0x40 names) | 56 |
0x04 | Both sides; the player can switch | 100 |
0x08 | Locked to one side — indistinguishable from 0x00 in every reader found | 53 |
0x0C | Would behave as locked; never used | 0 |
So “Single Side” (0x08) has no effect of its own: its only observable role is to veto 0x04, and since vanilla never sets both, 0x00 and 0x08 are two spellings of the same behaviour. Setting 0x08 alone changes nothing; clearing 0x04 is what locks the cursor.
Reader census — complete. Every reader of this byte in the binary: getMagicTargetMask (0x4838C0), getTargetMaskFromInfo (0x483880), computeTargetMaskDeadAndSpread (0x483860), sub_483D20, sub_483D60, Battle_PickRandomActionConfusedBerserk (0x483940), sub_4AB190, sub_4AA1D0, plus linkedStockFieldCharData / setMenuFlagMagicOnCharaData / updateBattleItemData, which only copy it. queuePlayerBattleCommand, BattleAction_ExecuteCommand and MonsterAI (two sites) load the raw byte and hand it straight to computeTargetMaskDeadAndSpread or getTargetMaskFromInfo without testing anything themselves. Only sub_4AB190 and sub_4AA1D0 touch bits 2-3, and both test the pair. Nothing anywhere tests 0x08 on its own.
The vanilla data agrees exactly. The bit is set on 100 of 209 entries that carry a TargetInfo byte, and only ever on the three tables a player aims by hand:
| Section | With side switch |
|---|---|
| Battle commands | 25 / 39 |
| Magic | 50 / 57 |
| Battle items | 25 / 33 |
| Junctionable GFs | 0 / 16 |
| Non-junctionable GF attacks | 0 / 16 |
| Renzokuken finishers | 0 / 4 |
| Blue magic (+ params) | 0 / 21 |
| Duel (+ params) | 0 / 18 |
| Rinoa limit part 2 | 0 / 5 |
Every auto-targeted or side-locked table has it clear on every single entry.
Bits 4-5 are a scope pair, and 0x20 is not “one side”
Like bits 2-3, these are read as one 2-bit value, never as separate flags. Every decoder switches on target_info & 0x30 (getTargetMaskFromInfo, getMagicTargetMask, sub_483D20, sub_483D60, Battle_PickRandomActionConfusedBerserk) and the cursor code on (target_info >> 4) & 3 (sub_4AB190, sub_4AA1D0):
& 0x30 | Resolves to | Vanilla entries |
|---|---|---|
0x00 | Every unit of the side 0x40 names — getTargetMaskAllChara / getTargetMaskAllEnemy | 74 |
0x10 | One unit of that side (random on the auto-resolve path) | 135 |
0x20 | getMaskEveryone() — but broken in practice, see below | 0 |
0x30 | Matches no branch; the raw 0x30 is returned | 0 |
The name “Everyone on one side” was wrong. getMaskEveryone returns all eight slots — the three party slots and the five monster slots:
TargetMask getMaskEveryone()
{
return TARGET_MASK_SLOT_0_CHARA|TARGET_MASK_SLOT_1_CHARA|TARGET_MASK_SLOT_2_CHARA
| TARGET_MASK_SLOT_3_MONSTER|...|TARGET_MASK_SLOT_7_MONSTER
| TARGET_MASK_TARGET_SEVERAL;
}
So 0x20 means both sides at once, and it is the one value that ignores 0x40 entirely. Note also that “whole side” is 0x00, not 0x20 — clearing both bits is what targets a full side. Nothing in vanilla ships with 0x20 or 0x30.
Scope 0x20 works for enemy attacks, not for player commands
Set scope 0x20 on something the player uses and the cursor highlights every unit, then the damage lands on the monsters only. The slot bits are discarded before damage is applied.
getMaskEveryone returns all slot bits plus TARGET_MASK_TARGET_SEVERAL (0x8000), and the player cursor builds the same shape (sub_4AA1D0: BYTE1(v4) |= 0x80 then v48 = v4 | v47). At damage time processMultiHitAttackExecution (0x48E830) branches on that flag:
if ( special & TARGET_MASK_SPECIAL_BYTE_TARGET_SEVERAL ) // 0x8000 was set
validated = expandTargetMaskToValidSide(current_hit_mask, revive); // slot bits DISCARDED
else
validated = getClosestTargetMaskValid(current_hit_mask, revive); // exact bits kept
and expandTargetMaskToValidSide (0x48EE50) re-derives a single side:
| slot bits | result |
|---|---|
== 0xFF | every valid slot — both sides |
<= 0x07 | the party (slots 0-2) |
| anything else | the monsters (slots 3-6) |
Bit 7 is a phantom slot. There are only 7 battle slots, not 8: getMaskTargetTargetValid (0x485FB0) and getMaskTargetTargetValidAndAlive (0x485F60) both loop i = 1248; i > -208; i -= 208 over BATTLE_SLOT_DATA (stride 0xD0), giving indices 6..0 — a 7-bit mask whose maximum value is 0x7F. Slots 0-2 are the party (the i < 624 test) and 3-6 the monsters, so four monsters maximum. TARGET_MASK_SLOT_7_MONSTER (0x80) is declared in the enum but no live-slot builder ever sets it, and the cursor code agrees — sub_4AA190 validates with & 0x7F, sub_4AA920 uses & 7 and & 0x78.
That splits the two paths:
- Enemy / auto-resolved actions work.
getMaskEveryonereturns the hardcoded constant0x00FF | 0x8000— phantom bit 7 included — so it matches== 0xFFexactly and every valid slot is hit. - Player commands cannot.
sub_4AA1D0builds the mask from the live slot set, which can never reach0xFF, so it always falls into the last branch. The menu highlights from the unexpanded mask, which is why the selection looks correct while the damage is one-sided.
So there is no data-only way to give a player action a both-sides hit — not even on a maximally full battlefield, because the deciding bit is unreachable. It needs a patch at 0x48EE50 (test “has bits on both sides” rather than == 0xFF). Scope 0x00 is immune throughout: its mask is ANDed with the current side, so it always lands in one of the first two branches correctly.
0x0040 — which side, and relative to whom
In the player menu the answer is direct. sub_4AB190 sets the cursor’s starting side from this bit alone:
*(_BYTE *)(v9 + 14) = (a2 & 0x40) ? 120 : 7; // 0x78 = monster slots, 7 = party slots
So 0x40 set = enemies, clear = allies. 0x08 “Single Side” does not name a side — it only means “one side only”, and which side is 0x40’s job.
On the auto-resolve path the bit is relative to whoever is acting, which is easy to trip over. The two decoders read it with opposite polarity:
| Decoder | Called by | 0x40 set resolves to |
|---|---|---|
getMagicTargetMask (0x4838C0) | MonsterAI only | getTargetMaskAllChara — the party |
getTargetMaskFromInfo (0x483880) | queuePlayerBattleCommand, others | getTargetMaskAllEnemy — the monsters |
The helpers themselves are absolute — getTargetMaskAllChara returns party slots 0-2, getTargetMaskAllEnemy returns monster slots 3-7 (bit 7 being the phantom slot; only 3-6 ever exist, see scope 0x20) — so the mirroring is deliberate: the kernel bit means “the opposing side”, and each decoder bakes in whose turn it is. The same target_info byte therefore points at different concrete slots depending on whether a character or a monster is acting.
MonsterAI_DispatchSection also calls getTargetMaskFromInfo (0x487C96, 0x487CE1), which looks at first like player polarity in a monster function — but those two calls read K_NONJ_GF_ATTACK_NAME_OFFSET.targetInfo, the non-junctionable GF attack table, right beside IS_ODIN_GILGA_PHOENIX_SUMMONED_THIS_FIGHT. They dispatch the player-side auto-summons (Odin, Gilgamesh, Phoenix, Angelo, Boko, MiniMog), so player polarity is correct there despite the function’s name. The data confirms it: under player polarity the four entries without 0x40 come out as party-targeting, and they are exactly Moogle Dance, Angelo Recover, Angelo Reverse and Angelo Search — the four that help the party — while every attacking summon (Zantetsuken, Rebirth Flame, the Choco* series, Excalibur, Masamune, Angelo Rush, MoombaMoomba) has 0x40 and comes out monster-targeting. Read with monster polarity the list would be exactly inverted.
0x0080 — “this command has no target”
BattleMenu_ExecuteSelectedCommand (0x4BC770) tests the byte as a signed char:
if ( (menuFlags & 0x20) != 0 ) // direct-targeting command
{
if ( targetInfo >= 0 ) // bit 0x80 CLEAR
BattleMenu_OpenTargetSelection(...); // normal interactive cursor
else // bit 0x80 SET
{
... // pick a slot: first enemy if 0x40 Enemy, else self
BattleMenu_PendingSelections[n] = { command, mask, ... }; // commit straight away
}
}
So the command skips target selection entirely, resolves a target itself and pushes the selection into the pending queue. Two more sites enforce the same thing with the same sign test, suppressing the cursor UI: sub_4A9DF0 (if (*(char *)(ctx + 4) >= 0 && ...) — otherwise no cursor finger is drawn) and sub_4AA1D0 case 0 (if (*((char *)a1 + 4) < 0) a1[21] = 0 — the window-open animation counter is zeroed instead of set to 4096).
Vanilla sets it on five battle commands, but it is only reached by two of them:
| # | Command | target_info | menu bits | Reached? |
|---|---|---|---|---|
| 2 | Magic | 0xD4 | 0x80 | No — takes the sub-list branch |
| 3 | GF | 0xD4 | 0x80 | No — sub-list |
| 4 | Item | 0xD4 | 0x80 | No — sub-list |
| 8 | nomsg | 0xD4 | 0xA0 | Yes |
| 10 | Stock | 0xD4 | 0xA0 | Yes |
Only commands with menu bit 0x20 (direct targeting) ever reach the test. Magic, GF and Item open a spell/GF/item list first, and from there the chosen entry’s own target_info drives the cursor — their command-level copy of 0x80 is dormant data. Stock is the case that shows the intent: stocking a drawn spell onto yourself has no target to pick, so the cursor is skipped.
Attack Type
| ID | Hex | Description |
|---|---|---|
| 1 | 0x00 | None |
| 2 | 0x01 | Physical Attack |
| 3 | 0x02 | Magic Attack |
| 4 | 0x03 | Curative Magic |
| 5 | 0x04 | Curative Item |
| 6 | 0x05 | Revive |
| 7 | 0x06 | Revive At Full HP |
| 8 | 0x07 | % Physical Damage |
| 9 | 0x08 | % Magic Damage |
| 10 | 0x09 | Renzokuken Finisher |
| 11 | 0x0A | Squall Gunblade Attack |
| 12 | 0x0B | GF |
| 13 | 0x0C | Scan |
| 14 | 0x0D | LV Down |
| 15 | 0x0E | Summon item (non-junctionable GF) — see below |
| 16 | 0x0F | GF (Ignore Target SPR) |
| 17 | 0x10 | LV Up |
| 18 | 0x11 | Card |
| 19 | 0x12 | Kamikaze |
| 20 | 0x13 | Devour |
| 21 | 0x14 | % GF Damage |
| 22 | 0x15 | Curative Magic (% of target’s Max HP) — see below |
| 23 | 0x16 | Magic Attack (Ignore Target SPR) |
| 24 | 0x17 | Angelo Search |
| 25 | 0x18 | Moogle Dance |
| 26 | 0x19 | White Wind (Quistis) |
| 27 | 0x1A | LV? Attack |
| 28 | 0x1B | Fixed Damage |
| 29 | 0x1C | Target Current HP - 1 |
| 30 | 0x1D | Fixed Magic Damage Based on GF Level |
| 31 | 0x1E | No handler — does nothing, see below |
| 32 | 0x1F | No handler — does nothing, see below |
| 33 | 0x20 | Give Percentage HP |
| 34 | 0x21 | Magic damage from step count — see below |
| 35 | 0x22 | Everyone’s Grudge |
| 36 | 0x23 | 1 HP Damage |
| 37 | 0x24 | Physical Attack (Ignore Target VIT) |
The four that used to be “Unknown”
Everything below comes from Damage_DispatchByAttackType (0x4922B0), the switch every attack type goes through, and from its jump table at 0x4922E0 — whose own IDA annotation reads “default case, cases 14, 30, 31”, i.e. 0x0E, 0x1E and 0x1F have no case in it.
0x15 — Curative Magic (% of target’s Max HP). Calls Damage_ComputeCurativeMagic (0x493280) with mode 8, which computes heal = Spell power × target MaxHP / 16. It is the magic-path twin of 0x20 (Give Percentage HP / Angelo Recover), which runs the identical formula through Damage_ComputeCurativeItemSpecial mode 15. The difference is the path, not the number:
0x15 (magic path) | 0x20 (item/special path) | |
|---|---|---|
| Reflect | bounces | not checked |
| Shell | halves the heal | not checked |
| Hit roll | none | rolls against the entry’s Hit% |
Med Data (charaAbilities & 2) | no effect | doubles the heal |
| Petrified target | heal forced to 0 | not checked |
| Zombie target | heal flips to damage | heal flips to damage |
0x21 — Magic damage from step count. Calls Damage_ComputeMagicAndGF (0x491AD0) with GF_MAGIC_DAMAGE_TYPE_STEP_COUNTER, giving damage = Attack power × (STEP_COUNTER / 1000) — the party’s total step counter, divided by
- The result then goes through the normal magic post-processing (Shell, Defend, the elemental multiplier, drain, status application).
0x1E and 0x1F — inert. Neither has a case in the dispatcher, so both fall to the default and return 0 damage, and no other code reads the attack-type byte with these values.
None of 0x15, 0x1E, 0x1F or 0x21 is used by any entry in the vanilla kernel.bin.
0x0E — Summon item
0x0E never reaches the damage dispatcher. It is tested once, in computeCommandAction (0x48D26F), and only for the Item command:
if ((command == COMMAND_ITEM || command == COMMAND_LINK_TO_ITEM)
&& K_ITEM[id].attackType == 0x0E)
When it matches, the command is switched to COMMAND_CHOCOBO and the action is taken from the Non-junctionable GF attacks table, indexed by the item’s own Special action ID. The item itself therefore has no damage behaviour of its own — its Attack type, power and so on are never consulted.
Two items are special-cased inside that branch: Gysahl Greens reads Chocobo World state (Boko’s attack becomes BokoAttack + 2, otherwise the “is not here” / “nothing happened” messages), and Phoenix Pinion also sets the Phoenix-called-once flag (SG_ODIN_ANGEL_GILGA_FLAG |= SPECIAL_BYTE_FLAG_PHOENIX_CALLED_ONCE).
Vanilla users — the only three entries in the kernel with Attack type 0x0E:
| Battle item | Non-junctionable GF attack it summons |
|---|---|
| 30 Gysahl Greens | ChocoFire / ChocoFlare / ChocoMeteor / ChocoBocle (by Chocobo World level) |
| 31 Phoenix Pinion | Rebirth Flame |
| 32 Friendship | MoombaMoomba |
Attack flag
| ID | Meaning |
|---|---|
| 0x03 | Damage-type pair (bits 0-1) — see below |
| 0x04 | Unused — no reader anywhere (also set on nothing in vanilla) |
| 0x08 | Break damage limit (dmg cap 9999 → 60000) |
| 0x10 | Reflectable — the spell can be bounced by Reflect status (handleSpellReflection) |
| 0x20 | Read only for battle items — CLEAR greys the row out and makes it unselectable (updateBattleItemData, test byte, 20h); see below. On magic / GF / Blue Magic / limits this bit is inert, even though it is set on virtually every player ability (an authoring convention: set on all player abilities, clear on all 384 enemy attacks) |
| 0x40 | Unused — no reader anywhere; an authoring marker for “restores HP or cures an ailment”. See the enumeration below |
| 0x80 | “May target KO’d units” (ATTACK_FLAG_REVIVE) — a menu targeting bit, not a damage-time one; see below. Set on exactly Life and Full-life in the magic table, and on all 32 real battle items |
0x80 is menu targeting, not “revive on hit”
The name is misleading and the battle-item table makes it obvious: 32 of the 33 battle items have 0x80 set, Potion included — only the blank entry 0 lacks it. If the bit meant “this action revives”, every Potion would be a Phoenix Down. In the magic table it is set on exactly two of 57 entries (Life, Full-life).
What actually revives is the separate Attack type byte — Damage_ComputeReviveHP (0x491940) is reached through Damage_DispatchByAttackType on attack types 5 (Revive) and 6 (Revive at full HP), and the only flag it tests is 0x10 (Reflect). This matters because on the Item command path Battle_applyDamage copies the item’s whole flag byte verbatim into the global ATTACK_FLAG (0x490C40), so a Potion does carry 0x80 at damage time and nothing treats it as revival.
Instead, three parallel “menu status” builders fold 0x80 into bit 0 of a per-entry status byte, one per list:
| Builder | Table | Effect of 0x80 |
|---|---|---|
setMenuFlagMagicOnCharaData (0x4954B0) | Magic | magicStatus = MAGIC_MENU_STATUS_REVIVE (1) |
linkedStockFieldCharData (0x48CAE0) | Magic (field/stock) | unk_8 = 1 |
updateBattleItemData (0x48C670) | Battle items | selectable = 1 |
That status byte rides along in the 5-byte per-row record the shared battle list window reads (BattleMenu_ListWindow_Update, 0x4FDD90), and reaches target selection in sub_4AB190 (0x4AB190), which is where it finally does something:
*(_BYTE *)(v9 + 10) = a4 & 1; // a4 = the menu status byte
if ( (a4 & 1) != 0 )
v19 = MASK_MONSTER_CHARA_ENABLE; // low word
else
v19 = HIWORD(MASK_MONSTER_CHARA_ENABLE); // high word
MASK_MONSTER_CHARA_ENABLE (0x1D750BC) is two 16-bit valid-target masks packed in one dword. The low word includes KO’d units, the high word is living-only — provable from the very next block, which computes the default cursor slot for a revive spell as the targets present in the low mask but absent from the high one:
if ( *(_BYTE *)(v9 + 10) && (a2 & 1) != 0 ) // a2 = TargetInfo, bit 0 = Dead
v12 = MASK_MONSTER_CHARA_ENABLE & ~BYTE2(MASK_MONSTER_CHARA_ENABLE) & 7;
So 0x80 is the precondition for TargetInfo 0x01 (Dead) to have any effect: the flag opens the cursor to KO’d units, and TargetInfo 0x01 then parks the cursor on one by default. This is why you cannot point Cure at a KO’d ally but can point a Potion at one — the Item cursor is always allowed to, so vanilla sets 0x80 on every item.
0x20 on battle items — the greyed row
The same updateBattleItemData folds 0x20 into bit 1 of that status byte, inverted:
if ( (K_ITEM[id].attackFlagsAndSelectability & 0x20) == 0 )
selectable |= 2u;
Bit 1 is read in two places, both in the shared list window: the item row draw callback (0x4C8680) swaps the text palette from 7 to 0, and the OK handler (state 22 of BattleMenu_ListWindow_Update) refuses the row with the buzzer alongside the “quantity <= 0” check. So clearing 0x20 greys the item out and makes it unselectable. Vanilla sets 0x20 on all 32 real items, so only the blank entry 0 is ever greyed. The magic status byte uses bits 0 and 4 instead of 0 and 1, which is why this bit is inert everywhere but the item table.
0x40 — verified unread, but not random
0x40 varies meaningfully between entries, which makes “unused” look wrong. It isn’t — the variation is an authoring convention that no code consumes.
The enumeration. The byte reaches the engine by two routes, and both were walked exhaustively rather than pattern-matched:
Route 1 — the runtime global ATTACK_FLAG (0x1D28E0E). It has 26 references. 16 are stores (mov [ATTACK_FLAG], cl / mov byte ptr [ATTACK_FLAG], imm, one per command type in Battle_applyDamage). The 10 loads test only three masks:
| Mask | Sites |
|---|---|
0x03 damage-type pair | 0x49466E, 0x4934F1, 0x491CCF, 0x4922B0, 0x4925D2 |
0x08 break damage limit | 0x491124 — and cl, 8 / and ecx, 0C351h / add ecx, 270Fh, i.e. cap 9999 → 60000 |
0x10 reflectable | 0x491952, 0x491AE6, 0x493120, 0x493291 |
Route 2 — code reading a kernel table’s own attackFlags field directly, bypassing the global. This is the route that makes 0x20 and 0x80 live, so it cannot be skipped:
| Reader | Table | Mask |
|---|---|---|
setMenuFlagMagicOnCharaData (0x4954B0) | Magic | 0x80 |
linkedStockFieldCharData (0x48CAE0) | Magic | 0x80 |
updateBattleItemData (0x48C670) | Battle items | 0x80, 0x20 |
ResetAndParseBattleAndFieldCharacter (0x4957AC) | Battle command abilities | 0x80 |
MonsterAI (0x489F8A) | Enemy attacks | 0x80 (adds target-mask bit 0x4000) |
computeCoverTargetSelection (0x48EBDA) | Enemy attacks | 0x03 (Cover needs a pure physical hit) |
Every other field reference is a copy into the global. So the complete set of masks the engine ever tests on this byte is 0x03, 0x08, 0x10, 0x20, 0x80 — 0x04 and 0x40 appear in no test anywhere.
What the bit tracks instead. It is set on 8 of 57 magics and 17 of 33 battle items:
- Magic: Cure, Cura, Curaga, Life, Full-life, Esuna, Dispel, Float
- Items: the five Potions, Mega-Potion, Phoenix Down, Mega Phoenix, Elixir, Megalixir, Antidote, Soft, Eye Drops, Echo Screen, Holy Water, Remedy, Remedy+
Cross-tabulated against the Attack Type field it is almost exactly “the attack type is curative”: every Curative Magic, Curative Item, Revive and Revive At Full HP entry has it, and every non-curative type lacks it, with four exceptions — X-Potion, Elixir and Megalixir (type Give Percentage HP, still heals) and Float, the one genuine oddity, a buff marked as curative. Note it is not “beneficial”: Regen, Protect, Shell, Reflect, Aura, Haste, Double, Triple, Hero and Holy War all lack it.
So it duplicates information the Attack type byte already carries, which is presumably why nothing reads it.
Caveat: route 2 relies on those accesses being typed as the struct field in the IDB. Code reaching the byte through an untyped pointer would not appear in either enumeration.
Damage-type pair (bits 0-1)
The low two bits form a damage-type pair, stored per hit as the target’s last_attacker_attack_flag (ATTACK_FLAG & 3). It is not a free bitfield — it’s one of four mutually-exclusive values. The kernel attackFlags byte holds the “resting” value; the runtime overrides some (Gunblade/Renzokuken force 3; on a Gunblade hit the value becomes the source’s own type).
| Pair | Name | Set by |
|---|---|---|
| 0 | Physical | Attack, most physical commands |
| 1 | Magical | Magic, GF, most offensive spells |
| 2 | Item/Medicine | Battle items |
| 3 | (special) | Forced by Renzokuken finishers / Gunblade |
What pair = 0 (Physical) uniquely enables — pure-physical hits are the only ones that: trigger the target’s Counter ability, wake a Sleeping/Confused target, remove Back Attack status, and mark the kill result on a killing blow. Magical, Item and special hits do none of these. The monster-AI condition LAST ACTION DAMAGE TYPE also compares this pair against Physical/Magical, so an attack’s type decides which AI branches fire.
What pair = 1 (Magical) does — the target’s Shell status halves the damage or heal (ATTACK_FLAG_MAGICAL_SHELLED). This is unconditional in computeCurativeMagic, so curative magic is always Shell-halved — a penalty spells cannot avoid.
What pair = 2 (Item/Medicine) does — it is the gate for the Med Data doubling in Damage_ComputeCurativeItemSpecial:
if ((ATTACK_FLAG & 3) == 2 && attacker is a player character && attacker knows Med Data)
heal *= 2;
Curative-item heals use 50 × power (Potion, power 4 → 200 HP; doubled to 400 with Med Data). But the implications of the pair value matter just as much as the doubling:
- Fun fact — items dodge Shell. Because a curative item is pair 2 (not 1/Magical), a target’s Shell status does not halve its healing (
Damage_ComputeCurativeItemSpecialhas no Shell check). The Shell penalty that hits a Cure spell (pair 1, halved unconditionally incomputeCurativeMagic) simply never applies to a Potion. - Fun fact — Med Data never provokes a counter. Med Data doubling only exists on the Item path (pair 2), which is non-Physical, so a doubled heal can never trigger the target’s Counter — you get the boost with none of the physical-hit side effects.
- Fun fact — Phoenix Down ignores this flag entirely. Its Med Data boost (maxHP/8 → maxHP/4 revive) lives in
GetReviveHPand checkscommand == Itemdirectly, notATTACK_FLAG & 3. So the flag governs curative-item HP restore, while revival amount is gated separately by the command type.
Hit count
Most ability tables carry a Hit count byte (magic 0x0D, junctionable GF 0x0C, enemy attacks 0x09 bits 0-6, Renzokuken finishers 0x0C, battle items 0x16, non-junctionable GF 0x0A, command abilities 0x07, temp-char limits 0x0C, blue magic 0x0B, Shot 0x0C, Duel 0x0C, Rinoa part 2 0x0A). It is not animation-specific and it is not limited to Meteor.
computeCommandAction copies the byte into CURRENT_ATTACK_HIT_COUNT, and processMultiHitAttackExecution (0x48E830) — its only reader — uses it for exactly two things.
1. One target mask per hit. The function fills TARGET_MASK_FOR_EACH_HIT[0 … n-1], one entry per hit. What goes in each entry depends on the spread bit 0x2000 of the resolved target mask, which comes from Target info bit 0x02 (“Multi-target spread”):
- spread bit set → each hit re-rolls its own random target (
getRandomTargetCharaMask/getRandomTargetMonsterMask) - spread bit clear → every hit reuses the same mask and lands on the same target
This is the part that looks Meteor-specific but isn’t: the random re-targeting comes from Target info, not from Hit count.
2. One full damage pass per hit. The second loop calls Battle_applyDamage + computeTargetData once per hit, per slot in that hit’s mask — so the hit roll, crit roll, elemental multiplier, Shell/Defend reduction and status roll are all re-evaluated for every hit, independently. calculateHitDistributionPerSlot then tallies how many hits each slot receives into BATTLE_SLOT_DATA[].hit_count_to_receive, which is what drives the stacked damage numbers.
A Hit count of 0 skips the loop entirely: the ability resolves with no damage, no status and no damage number.
Every vanilla entry with more than one hit:
| Entry | Table | Hits | Spread bit 0x02 |
|---|---|---|---|
| Lion Heart | Renzokuken finishers #3 | 17 | no — all 17 on the same target |
| Terra Break | Enemy attacks #311 | 16 | — |
| Meteor | Magic #16 | 10 | yes — 10 independently random targets |
| Meteor Stone | Battle items #28 | 10 | yes |
| Wishing Star | Rinoa limits part 2 #3 | 8 | no |
| Blood Pain | Temp-char limits #3 (Seifer) | 6 | no |
Table of contents
- Header
- Battle commands
- Magic
- Junctionable GFs
- Enemy attacks
- Weapons
- Renzokuken finishers
- Characters
- Battle items
- Non battle item name and description offset
- Non junctionable GF attacks
- Command abilities in battle
- Junction abilities
- Command abilities GF
- Stat percentage increasing abilities
- Characters abilities
- Party abilities
- GF abilities
- Menu abilities
- Temporary characters limit breaks
- Blue magic
- Blue magic parameters
- Shot (irvine limit breaks)
- Duel parameters
- Duel (zell limit break)
- Rinoa commands
- Rinoa combine limit break
- Slot array
- Slot sets
- Devour
- Misc
- Misc text pointers