Hitbox System (KHit linked lists)¶
How SC6 actually decides whether a hit landed. Strikes, kicks, hurtbox classification, and
pushbox physics all run through three intrusive linked lists of KHitBase* nodes on every
chara, walked once per tick by LuxBattle_TickHitResolutionAndBodyCollision @ 0x14033CCA0.
There are no UE4 physics sweeps and no GameTraceChannel — just analytical
capsule-vs-capsule tests on deterministic per-tick data.
This is a different system from the visual weapon trail. For the sword-swoosh / particle effects, see Trace System (weapon-trail VFX). For what happens after a hit is classified (yarare dispatch, per-id reaction Tick, knockdown camera, throw-react), see Reaction System.
At a glance¶
| What | Address / offset | Role |
|---|---|---|
| Tick driver | LuxBattle_TickHitResolutionAndBodyCollision @ 0x14033CCA0 |
Master per-tick hit resolution. |
| Body / pushbox list head | chara+0x44478 |
Chara-to-chara physical pushing only — not part of hit resolution. |
| Attack list head | chara+0x44498 |
Entries that DEAL damage or initiate a grab. |
| Hurtbox list head | chara+0x444B8 |
Entries that RECEIVE damage / reactions. |
| Opponent active attack cell copy | chara+0x44048 |
Copy of opponent's current FLuxMoveBankCell*, copied each tick. |
| Own active attack cell | chara+0x44058 |
Own current FLuxMoveBankCell* for the active move-slot. |
| Frame-window phase | chara+0x1980 |
0=None, 1=Startup, 2=Active, 3=Recovery; written by LuxMoveVM_ClassifyHitboxFrameState. |
| In-master-window flag | chara+0x16EA |
Boolean active-frame gate: phase is active and the sub-window inhibitors are clear. |
| Aggregation array | chara+0x44078 (u64[22]) |
PerHurtboxBitmask — one slot per kind tag. |
| Reaction output | chara+0x1C74 (i32[22]) |
PerHurtboxReactionState — LuxHitReactionState enum. |
| Node storage | Sphere 0x80; Area/FixArea 0xA0 |
Mixed-size intrusive nodes; +0x20 stores the relative next-node delta. |
| Subclass tag | node+0x16 |
0=Sphere, 1=Area, 2=FixArea. |
| Geometry gate | node+0x14 |
Attack list: (hotMask >> KindTag) & 1 per tick. Hurtbox list: written on demand by MoveVM opcode 0x13AC. See per-frame hot-mask. |
| Damage / reaction gate | node+0x14 && (node+0x08 & activeCell->u64SlotMask) && attacker+0x16EA |
Geometry, category mask, and active-frame phase must all pass before a reaction can fire. |
Source: Ghidra reverse-engineering of the SC6 Steam build, cross-validated by HorseMod's
KHitWalker.hppanddllmain.cppplate comments.
Three list heads on every chara¶
| Offset | Head | Role | Iterated by |
|---|---|---|---|
+0x44478 |
BodyListHead |
Body / pushbox — chara-to-chara physical pushing only. Not part of hit resolution. | LuxBattle_SolvePhysBodyCollision @ 0x14030CCF0 |
+0x44498 |
AttackListHead |
Entries that DEAL damage or initiate a grab. | LuxBattleChara_UpdateAllKHitWorldCenters @ 0x14030D6A0 (attacker side) |
+0x444B8 |
HurtboxListHead |
Entries that RECEIVE damage / reactions. | UpdateAllKHitWorldCenters (defender side) |
List counts live at the matching head - 0x8 offsets (+0x44470, +0x44490, +0x444B0).
Adjacent classifier state on the chara¶
| Offset | Type | Name |
|---|---|---|
+0x44048 |
FLuxMoveBankCell* |
OpponentActiveAttackCellCopy — copy of opponent's +0x44058. The pointee's first u64 is the live attacker-slot/category mask the classifier reads. |
+0x44050 |
short(*)[3] |
Mirror of opponent's +0x44060 (downstream classifier reads only). |
+0x44058 |
FLuxMoveBankCell* |
OwnActiveAttackCell — own current move-slot cell. Its u64SlotMask stays fixed while that cell is selected; chara+0x1980/+0x16EA supplies the active-frame timing. |
+0x44060 |
short(*)[3] |
NonAttackMoveDescrPtr — (DamageMultiplier, PassthroughTag, DurationTicks) for non-damaging supers / SC finishers / stance / GI / parry transitions. Set by LuxMoveVM_TransitionToMove. |
+0x44068 |
FLuxMoveLane* |
ActiveLaneStateCursorPtr — points at the running lane block (one of the three at +0x444F0/+0x44958/+0x44DC0). Was LuxMoveLaneState*. |
+0x44070 |
u8[6] |
LastHitSourceCellLo48 — opaque 48-bit packed hit-id snapshot. |
+0x44078 |
u64[22] |
PerHurtboxBitmask — defender-side aggregation; bits of every attack-list node currently overlapping kind-tag i. |
+0x44494 |
i32 |
ClassifierHurtboxBound — loop count for the per-kind walk (= attack list's max kind-tag + 1; reused as hurtbox iter bound). |
+0x444B4 |
i32 |
HurtboxMaxSlot — hurtbox list's own max kind-tag + 1. Not read by hit pipeline. |
+0x1C74 |
i32[22] |
PerHurtboxReactionState — classifier output. See reaction-state values. |
Naming note: older notes and HorseMod aliases call +0x44048 the
"current active attack cell." In this build it is specifically the
opponent copy read by the defender-side classifier. +0x44058 is the
chara's own current attack cell. Both are pointers to 0x70-byte move-bank
cells, not KHitBase nodes.
KHit node layout (variable 0x80 / 0xA0 storage)¶
Lux_KHitChk_DeserializeLinkedList @ 0x14030C940 emits mixed-size nodes into one
scratch pool: Sphere records advance the cursor by 0x80, while Area and FixArea
records advance by 0xA0. The list link at +0x18 is authoritative; +0x20
stores the relative byte delta to the next node (0x80 after a Sphere,
0xA0 after an Area/FixArea, zero on the tail).
Common header (every subclass):
| Offset | Type | Name | Notes |
|---|---|---|---|
+0x00 |
void* |
vtable |
one of four subclass vtables (see addresses below) |
+0x08 |
u64 |
PerAttackerBit / PerHurtboxBit |
1ULL << (KindTag & 0x3F) — single-bit value derived from +0x17 |
+0x10 |
u32 |
Node_Flags10 |
authored, write-only — no runtime reader. Don't classify or gate on it. |
+0x14 |
u16 |
ActiveThisFrame |
per-frame geometry gate. See hot-mask. |
+0x16 |
u8 |
StreamTypeTag |
0=Sphere, 1=Area, 2=FixArea |
+0x17 |
u8 |
KindTag |
KHit kind/category in [0, ~22). Not a skeletal bone id. |
+0x18 |
KHitBase* |
Next |
intrusive list link; null-terminates |
+0x20 |
i32 |
NextDeltaBytes |
Relative offset from this node to the next emitted node; 0x80 after Sphere, 0xA0 after Area/FixArea, zero on the tail. |
Subclass vtables:
| Symbol | Address |
|---|---|
KHitBase_vftable |
0x143E87838 |
KHitSphere_vftable |
0x143E877F0 |
KHitArea_vftable |
0x143E877A8 |
KHitFixArea_vftable |
0x143E87760 |
Subclass extension fields:
KHitSphere (StreamTypeTag = 0):
+0x30 FVector BoneLocalCenter (mirrored at +0x40)
+0x50 FVector WorldCenterCurrent (this frame)
+0x60 FVector WorldCenterPrevious (last frame; for sweep tests)
+0x70 float Radius (may be scaled by anim cell)
+0x74 float RadiusAuthored
+0x78 float ContactImpulseScale
+0x7C uint32 BoneIndexUe4 (post-Remap)
KHitArea (StreamTypeTag = 1) — SWEPT CAPSULE, double-buffered for CCD:
+0x30 FVector BoneLocalP1
+0x40 FVector BoneLocalP2
+0x50..+0x6F WorldSpaceBufA (P1, P2)
+0x70..+0x8F WorldSpaceBufB (P1, P2)
g_LuxKHitArea_DoubleBufferToggle selects cur vs prev each tick.
Overlap test does 4-way segment/segment CCD across both halves.
+0x90 uint32 BoneIndexUe4_P1
+0x94 uint32 BoneIndexUe4_P2
KHitFixArea (StreamTypeTag = 2) — STATIC OBB from THREE reference points:
+0x30 FVector BoneLocalPoint1 (P1, w=1.0 at +0x3C)
+0x40 FVector BoneLocalPoint2 (P2, w=1.0 at +0x4C)
+0x50 FVector BoneLocalPoint3 (P3, w=1.0 at +0x5C)
+0x60 FVector WorldPoint1
+0x70 FVector WorldPoint2
+0x80 FVector WorldPoint3
+0x90 uint32 BoneIndexUe4
Only KHitSphere carries a contact-impulse scale (+0x78), used by the body / pushbox
solver. KHitArea and KHitFixArea do not have a verified contact-impulse field.
To recover an OBB from KHitFixArea's three world-space points:
// Gram-Schmidt
X = normalize(WP2 - WP1); // primary axis
sideRaw = WP3 - WP1;
Y = normalize(sideRaw - dot(sideRaw, X) * X);
Z = cross(X, Y);
lenX = |WP2 - WP1|;
lenY = lenZ = |sideRaw - dot(sideRaw, X) * X|; // square cross-section
Cheaper alternative for visualisation: draw two lines WP1→WP2 (spine) + WP1→WP3 (side).
Per-frame hot-mask¶
+0x14 ActiveThisFrame is written by two separate writers, one per list. They use
different mechanisms — do not assume the attack-side hot-mask logic applies to hurtboxes.
Attack list (chara+0x44498)¶
Written every tick by LuxBattle_TickHitResolutionAndBodyCollision:
hotMask = 0x3FFFD // FLOOR — slots {0, 2..17}
| (animCellMask ? *animCellMask : 0)
| (ownActiveCell ? *ownActiveCell : 0);
for (KHitBase* n in AttackList) // ATTACK ONLY
n->ActiveThisFrame = (hotMask >> n->KindTag) & 1;
The 0x3FFFD floor (0b11_1111_1111_1111_1101) forces slots 0, 2..17 on every frame —
structural / passive kinds (foot-anchored / body-attached volumes). Slot 1 is excluded:
it's the move-driven active-attack kind, the only one that genuinely toggles per move.
This is a geometry / overlap gate only. It says the attack node is eligible
for the overlap pass in LuxBattleChara_UpdateAllKHitWorldCenters; it does not
mean the move is inside its authored active frames. Use chara+0x1980 == 2
or the stricter byte chara+0x16EA != 0 for that timing distinction.
Hurtbox list (chara+0x444B8)¶
Written on demand by the move-VM bytecode via opcode 0x13AC
(LuxMoveVM_SetHurtboxSlotsActiveMask @ 0x140308D70). The leaf uses an
inverted disable mask:
for slot in 0..22:
if slot < *(int*)(chara + 0x444B4): // HurtboxMaxSlot
for (KHitBase* n in HurtboxList):
if (n->KindTag == slot)
n->ActiveThisFrame = (~disableMask) & 1;
disableMask >>= 1;
So a mask bit of 1 disables matching hurtbox nodes (+0x14 = 0), and
a bit of 0 enables them (+0x14 = 1). The dispatcher can pack the mask into
one or two 15-bit command words, but this leaf only consumes the low 23 slots.
There is no per-tick floor on the hurtbox side. The Sphere, Area, and
FixArea deserializers initialize +0x14 = 1; later move-script opcodes can
disable or re-enable selected slots for invulnerability, armor, parry, or
extension-hurtbox windows. A hurtbox with +0x14 = 0 this frame is skipped by
the overlap pass and cannot contribute to PerHurtboxBitmask until a later VM
opcode enables its slot again.
High-slot extension/meta hurtboxes can also look "present but inert" for a
separate reason: the overlap pass may write PerHurtboxBitmask[slot], but
LuxBattle_ResolveAttackVsHurtboxMask22 only reads slots below
chara+0x44494 (clamped to the 22-slot reaction array). For overlays, keep
overlap_active (+0x14 != 0) separate from classifier_addressable
(KindTag < min(chara+0x44494, 22)).
The body list (chara+0x44478) is not iterated by the attack hot-mask writer
or by the hurtbox opcode above. A sister body/pushbox opcode exists for the
intro/body stream, but body nodes are a pushbox-physics concern, not part of
damage resolution.
Damage requires geometry, mask, and phase¶
+0x14 is a geometry-live / overlap-test gate, not a damage-live gate. The defender's
overlap loop in LuxBattleChara_UpdateAllKHitWorldCenters @ 0x14030D6A0 skips any
hurtbox with +0x14 == 0, so no PerHurtboxBitmask bits get OR'd and no reaction can fire.
For a hit to actually fire damage, all of these must pass:
node->ActiveThisFrame != 0(geometry/overlap pass)- The overlap pass must OR the attack node's
node+0x08bit into the defender'sPerHurtboxBitmask[defenderKindTag]. - The defender-side resolver must be allowed to run (global battle gate, defender not incapacitated, defender not in no-react state).
- The attacker must be inside its active frame window:
attacker+0x16EA != 0(equivalent toattacker+0x1980 == 2with the sub-window inhibitors clear). - The active attack cell mask must select the attack category:
node->PerAttackerBit & activeCell->u64SlotMaskis non-zero. From the defender's perspective,activeCellis the pointer copied todefender+0x44048.
After those KHit gates, LuxMoveVM_EvaluateMoveTransition still decides
whether the overlap becomes hit, block, tech, whiff, guard-break, or another
transition type based on stance and cell+0x32 wAttackFlags.
Practical consequence for hitbox overlays¶
If a mod displays both lists with a single +0x14-based filter, the filter is
only showing overlap eligibility:
- On the attack side,
+0x14includes the0x3FFFDfloor and can stay true through startup/recovery. AddcatMask & activeCell->u64SlotMaskandchara+0x16EA/chara+0x1980 == 2for "can actually fire damage now." - On the hurtbox side,
+0x14 == 0means the overlap pass skips that slot this frame, usually because move bytecode disabled it via opcode0x13AC. - A hurtbox with
+0x14 == 1can still be classifier-invisible when itsKindTagis outsidemin(chara+0x44494, 22).
Attack cell (FLuxMoveBankCell)¶
The pointer at chara+0x44058 (and its per-tick opponent copy at chara+0x44048)
is not a KHitBase. It points at a 112-byte FLuxMoveBankCell row in the
character's move bank — see the full layout in structures.md.
The cell carries the move's damage, hitstun, and hit-property values. Each cell holds:
u64SlotMask(+0x00) — which authored hitbox slots are live while this cell is current.wAttackFlags(+0x32) — block-direction + modifier flags (ELuxBattleAttackFlags); see Attack flags below.nMasterWindowStart/nMasterWindowEnd(+0x36 / +0x38) — frame range during which the cell is "active for damage" within the move's animation.nBaseDamage(+0x3A) andnStunRecoil(+0x3C) — the damage/stun applied on hit.nBlockstunFrames/nHitstunStanding{Normal,Air}/nHitstunCrouch{Normal,Air}/nReactionId{Standing,Air}/nThrowEscapeId(+0x44 .. +0x54) — per-stance reaction frame counts and post-hit move ids.wHitboxGroupBitfield(+0x5E) — selects one of 64 hit-sub-window timing entries; see Per-cell sub-window timing.
Attack flags¶
cell+0x32 wAttackFlags is a 16-bit flag word. Engine-verified bits
(Ghidra enum ELuxBattleAttackFlags, read by LuxMoveVM_EvaluateMoveTransition
@ 0x14033E140 and LuxBattle_ResolveAttackVsHurtboxMask22 @ 0x14033C100):
| Bit | Name | Meaning |
|---|---|---|
0x001 |
HighBlockable |
blockable while standing |
0x002 |
LowBlockable |
blockable while crouching |
0x004 |
BlockBypass_GuardBreak |
routes to the counter-hit-special result vs a guard-broken defender |
0x008 |
LowAttack |
low block-level bit |
0x010 |
MidAttack |
mid block-level bit |
0x040 |
CrouchOnly |
attacker authored as crouching |
0x080 |
HighAttack |
high block-level bit (crouch ducks under) |
0x100 |
Special_FlagX100 |
special framing rule (largely opaque) |
0x200 |
Unblockable_GIImmune |
defender's Guard-Impact roll is forced to fail. Alone (no block bits) → genuinely unblockable; combined with block bits → a Break Attack (blockable, but forces the stagger-block reaction). |
These flags are not a reliable move-class tag
The block-level bits drive block / hit resolution but are a poor proxy
for the move's displayed High / Mid / Low class — most cells set
HighBlockable regardless of the move's actual height, and one cell
cannot represent a multi-hit move's class sequence. For a move's class
use DA_MoveListTable.AttributeTag — see
Character Data: Move-list metadata.
Cell lifetime: current cell per move-slot¶
LuxMoveVM_TransitionToMove @ 0x1402FE350 resolves the initial cell from the
move-bank slot table (bank + bank[+0x10] + cellBone * 0x70) and writes both:
chara+0x44040 PrimaryAttackCellPtr— the move-start cell, frozen for the move and later used by damage/push branches as a "window still open?" source.chara+0x44058 OwnActiveAttackCell— the current attack cell that the hot-mask writer and classifier read.
LuxMoveVM_SetActiveMoveSlot @ 0x140300C70 can re-resolve +0x44058 through the
same 6-entry variant table at slot+0x3C + AnimVariantIndex*2, then mirrors
the cell's +0x5A/+0x5E/+0x60 tags to chara state and calls
LuxMoveVM_ClassifyHitboxFrameState. Treat +0x44058 as the current
move-slot / sub-slot cell, not as a per-render-frame pulse and not as a
guaranteed whole-move constant.
Practical consequence: every KHit shape selected by one current cell uses that
cell's single nBaseDamage, wAttackFlags, and reaction metadata. Damage can
change only when the VM selects a different cell or transitions to another
move; it does not vary per individual KHit shape inside one cell.
Cell mask bit-pattern decoding (u64SlotMask)¶
LuxBattle_TickHitResolutionAndBodyCollision and LuxMoveVM_TransitionToMove
do double-duty on the cell's u64SlotMask:
- As a bitmap of which KHit shape slots are live (used by the per-frame
hot-mask gate at
KHitBase.+0x14and by the classifier atLuxBattle_ResolveAttackVsHurtboxMask22). - As a bit-pattern encoding of the move's hit-class (high / mid / low /
throw / wire / super) — decoded by
TransitionToMoveinto two outputs.
The two outputs of the decode are written to the chara at move-start:
chara+0x1354 dwMoveType (5 values):
| Value | Meaning | Detect rule |
|---|---|---|
| 1 | Strike | (mask & 0x7FF0003F800000) != 0 |
| 3 | Grab | (mask & 0x33F0C0) != 0 AND not strike |
| 4 | Wire | cell+0x34 & 0x10 AND addr-flag in cell+0x0C |
| 6 | Super | None of the above set |
| 7 | SC attack | When chara's MoveSubclassAlt == 7 |
chara+0x1358 dwAnimKind (high/mid/low classification):
| Value | Meaning | Detect rule |
|---|---|---|
| 0 | Neutral / no-tag | default |
| 1 | Standard hit | default-strike |
| 2 | High-attack | bit pattern 0x1800000 (bits 23-24) |
| 3 | Mid-attack | bit pattern 0x6000000 (bits 25-26) |
| 4 | Low-attack | bit pattern 0x1000008000000 (bits 27, 48) |
| 5 | Special-mid | bit pattern 0x2000010000000 (bits 28, 49) |
| 6 | Special-low | bit pattern 0x4000020000000 (bits 29, 50) |
| 9 | Grab kind A | bit pattern 0x8100000000000 (bits 40, 51) |
| 10 | Grab kind B | bit pattern 0x10200000000000 (bits 41, 52) |
| 0xB | Grab kind C | bit pattern 0x20400000000000 OR 0x40800000000000 |
So the KindTag values in [22..30] form one "low-bits" hit-class group,
mirrored at [48..55] for the second hit-class group. Together the two
ranges encode 4-8 strike-class bits with distinct semantics, plus the
throw bits at 31/55 for grab signalling.
Override chain via chara+0x250 MoveSubclassAlt: the default decode
above is overridden for these subclass values:
4, 5, 0x1E, 0x23, 0x2F: forcedwAnimKind = 0(neutral).0xB, 0x16: setbWasFlagSet, forcedwAnimKind = 0.0x29(41): tests bits0x1C00000000000..0x40000000000000plus0x100..0x800000000000to computedwAnimKindwith a complex priority chain.0x36(54): tests bits0x800000 / 0x10000000000000to toggle between 0 and 1.0x37(55): ifmask & 0x2000010000000setsdwAnimKind = 6.0x3A(58): tests0x800000 / 0x10000000000000→ 0/1.0x3B(59): tests0x800000 / 0x100000000000 / 0x10000000000000 / 0x4000000000000with priority — outputs 3 or 0.
These overrides reinterpret the same slot-mask bits per move class. They are
not per-character: chara+0x250 is the move's MoveSubclassAlt (set
per-move from the move definition), not a character ID. The character ID is at
chara+0x23C CharaKindByte.
Non-attack ALT-path¶
When the resolved cell address has bit 0x1000 set in its high u16
(shortAddr & 0x1000), the move is a non-attack definition: it has no
damage-cell payload but still needs a per-move passthrough record for
stance/GI/parry transitions. TransitionToMove takes the ALT branch:
- Leaves
chara+0x44058 = NULL(no active attack cell). - Leaves
chara+0x44040 = NULL(no primary attack cell either). - Writes
chara+0x44060 = bankBase + bank[+0x14] + (subIdx & ~0x1000) * 6pointing at the 6-byte short[3] non-attack descriptor:
[0] i16 DamageMultiplier — multiplied into final damage scalar
[1] i16 PassthroughTag — copied to defender chara+0x210C
[2] i16 Duration60ths — /60.0f → attacker+0x414 damage bucket
- Mirrors
short[1]intochara+0x210A(PassthroughTag mirror).
The non-attack descriptor is consumed downstream by:
LuxBattle_ApplyDamageFromPendingHit @ 0x1402FF620— reads all 3 shorts on damage application.LuxMoveVM_EvaluateMoveTransition @ 0x14033E140— branches on PassthroughTag for special hit-reaction selection.
Use cases: non-damaging supers, SC finishers, stance entries, GI/parry reactions, throw-whiff recovery, scripted-damage move kickers.
Per-part attack properties (e.g. tip-of-axe vs body-of-axe)¶
This is what the cell model actually supports:
- Spatial selection only. Each
KHitBaseshape on the AttackList has a distinctKindTag(+0x17), and therefore a distinctPerAttackerBit(1ULL << KindTag). The cell'su64SlotMaskdecides which of those shapes can register a hit. A move with both a tip-shape and a body-shape can have its cell light only the tip bit — body contact then silently fails to register — without any change to damage values. - Sub-window timing. The cell can author up to 4 named sub-windows (one per
bank) within its master window via
wHitboxGroupBitfield, but BaseDamage is still single-valued. - Damage variation across cells / moves, not within one cell. A character
action whose damage genuinely differs depending on what hit (tip vs hilt vs
body) must switch to another attack cell or chain to another move. The move-VM
can route through
LuxMoveVM_SetActiveMoveSlot @ 0x140300C70or throughLuxMoveVM_EvaluateIfOpcode @ 0x1403732F0predicates. Known IF subkeys that read chaining-relevant state: 0x1389— compares an immediate againstlane+0x460 AnimVariantIndex.- The hit-window state at
chara+0x1980(master-window phase 1/2/3) and the per-bank group ids atchara+0x20BC..+0x20EEare also exposed as predicates. Different cells / sub-moves carry their ownnBaseDamage.
The visual weapon trail (FLuxCapsule) does NOT modulate damage based on hilt-vs-tip contact distance. Damage is purely cell-driven. See Trace System.
Per-cell sub-window timing¶
LuxMoveVM_ClassifyHitboxFrameState @ 0x140300620 runs every tick to classify
where the current animation frame sits within the active cell's hit window:
chara+0x1980 (state):
1 = before MasterWindow (curFrame < cell.nMasterWindowStart)
2 = inside MasterWindow (cell.nMasterWindowStart .. nMasterWindowEnd)
3 = past MasterWindow
It then partitions cell.wHitboxGroupBitfield & 0x7FF into 4 banks of 16
groups (0..15, 16..31, 32..47, 48..63) and looks each up in a per-bank
sub-window timing table at:
| Bank | Table base | Stride | Entry shape |
|---|---|---|---|
| 0 | DAT_1448554E8 + 0x338 |
8 | (int16 startOffset, int16 endOffset, ...) |
| 1 | DAT_1448554E8 + 0x3B8 |
8 | same |
| 2 | DAT_1448554E8 + 0x438 |
8 | same |
| 3 | DAT_1448554E8 + 0x4B8 |
8 | same |
Each entry's offsets are relative to nMasterWindowStart, so the effective
sub-window is [MasterWindowStart + entry.start, MasterWindowStart + entry.end].
The function writes 28 short slots at chara+0x20BC..+0x20EE that classify
the current frame against each sub-window; these are read by
LuxBattleChara_GetImpactCategory @ 0x14033CB60 and as VM IF-opcode predicates.
This system controls when, within the move's animation, different hit-groups
are considered live. It does not provide a per-window damage value — the cell
still has only one nBaseDamage.
Runtime cell mutation¶
The only field on an already-selected live cell known to mutate in place is
wRuntimePropagateField at +0x6A. LuxMoveVM_PropagateFieldToHitboxGroup
@ 0x140303590 writes it across the 8 cells of a hitbox-group entry (stride
0x40, 8 cell pointers at +0x50/+0x58/+0x60/+0x68/+0x70/+0x78/+0x80/+0x88).
Its semantics are not yet pinned down — an open question for further RE.
Cell-readers¶
| Function | RVA | Cell fields read |
|---|---|---|
LuxBattleChara_ProcessHit |
0x342780 |
nBaseDamage nStunRecoil wExtraStateFlags wAttackFlags |
LuxBattle_ApplyDamageFromPendingHit |
0x2FF620 |
nBaseDamage (counter-hit followup), null-check on +0x44040 PrimaryAttackCellPtr |
LuxMoveVM_EvaluateMoveTransition |
0x33E140 |
wAttackFlags wInputCond |
LuxBattle_ComputeHitReactionParams |
0x343B90 |
wAttackFlags, the full +0x44 .. +0x54 block |
LuxBattle_ResolveAttackVsHurtboxMask22 |
0x33C100 |
u64SlotMask (read as attackerMask), wAttackFlags (block-tier test) |
LuxMoveVM_SetActiveMoveSlot |
0x300C70 |
Re-resolves chara+0x44058, reads u64SlotMask for attack +0x14, mirrors +0x5A/+0x5E/+0x60, then re-runs ClassifyHitboxFrameState |
LuxMoveVM_ClassifyHitboxFrameState |
0x300620 |
nMasterWindowStart nMasterWindowEnd wHitboxGroupBitfield |
LuxMoveVM_PropagateFieldToHitboxGroup |
0x303590 |
writes wRuntimePropagateField |
Strike vs throw partition¶
LuxBattle_ResolveAttackVsHurtboxMask22 @ 0x14033C100 partitions the 64-slot mask space:
| Mask | Bits | Role |
|---|---|---|
0x80000080000000 |
31, 55 | Throw / grab |
0xFF7FFFFF7FFFFFFF |
every other | Strike |
A throw pre-scan runs before the per-hurtbox strike loop: if any throw bit is set in the active-move mask, grab-transition logic fires first. The pre-scan stamps a single u16:
Normal strikes leave defender+0x212E == 0xFFFF. The yarareId is what
syncs the victim's getting-thrown ("yarare" / やられ) animation to the
initiator's grab animation.
Throw connection: the size / height-bucket gate¶
There are two separate failure points for throws:
- KHit / classifier connect: geometry overlap, throw bits 31/55,
active-frame phase, and
LuxMoveVM_EvaluateMoveTransitionmust pass. This path has no per-character height test; ifdefender+0x212Enever gets stamped, inspect the attack cell'swAttackFlags/ stance match first. - Yarare dispatch: even after the classifier stamps
defender+0x212E,LuxMoveVM_TickPickAndDispatchReaction @ 0x1402DEF50can drop the reaction beforeLuxBattle_DispatchYarareReactioncommits animation/damage. The height-bucket gate below lives in this downstream dispatch layer.
LuxMoveVM_GetCharaEffectiveHeight @ 0x140309470¶
Computes a per-chara "effective height" in game units. Default (grounded-stance) branch:
iVar = (int)float(chara+0x44968) // head bone world Y
- (int)(short)g_LuxBattle_CharaKindStatureTable[bKindByte * 0xF0] // PER-CHARAKIND OFFSET
- (int)float(chara+0x44960); // foot bone world Y
return clamp_lo0(iVar);
Special-case branches (combo-active, knockback, override, etc.) return
fixed buckets (0, 1, 9999, or chara+0x1364 floor-contact). The
key per-character calibration is the table at
g_LuxBattle_CharaKindStatureTable @ 0x14470D200, stride 0xF0,
indexed by chara+0x23C bCharaKindByte. Identical world-Y bones produce
different effective heights for tall vs. short charas — this is the
per-character "size" attribute.
The dispatch-allow gate (root cause of "small grabs tall whiff")¶
At the END of TickPickAndDispatchReaction:
iVar7 = GetCharaEffectiveHeight(self); // self = DEFENDER (the chara whose VM is ticking)
iVar8 = GetCharaEffectiveHeight(self->OppChara); // opp = attacker
// Pre-pick shortcut at ~0x1402DFB58:
if (((2 < iVar8) || (iVar8 == 0)) && (4 < iVar7)) goto SKIP_WEIGHT_PICK;
// (...weight-pick selects postEffectYarareId...)
// Final dispatch-allow bitset at ~0x1402DFBC0:
uint allowDispatch = (iVar7 < 5); // 0 when defender is TALL
if (postId in {0x1F, 0x21, 0x20}) allowDispatch = 1;
if (postId in {0x2C, 0x2D}) allowDispatch = 1; // launch / air-carry
if (postId in {0x2E..0x31, 0x3B}) allowDispatch = 1; // ringout / back-breaker
if (intensity > 1 && postId in {0x28..0x2B}) allowDispatch = 1;
if (postId in {0xE..0x10}) allowDispatch = 1; // wall stagger
if ((MoveStateId == 3 && +0x1982 != 0) || allowDispatch != 0)
LuxBattle_DispatchYarareReaction(...); // <-- throw actually commits here
Throw-catch yarareIds are NOT in the unconditional allow-set. They
dispatch only when iVar7 < 5. When the DEFENDER is the tallest
character in the cast, iVar7 >= 5 and the dispatch is silently
dropped: the geometry overlap registers and the throw classifier stamps
the yarareId at defender+0x212E, but no damage, animation, or reaction
ever fires. This is the gate behind the "smallest grabs tallest, boxes
contact, but throw whiffs" empirical bug.
The iVar8 term (attacker height) is consulted only by the pre-pick
shortcut, which gates the random weight-pick for non-throw
reactions — not the throw dispatch itself.
| Field | Type | Role |
|---|---|---|
g_LuxBattle_CharaKindStatureTable @ 0x14470D200 |
int16[stride 0xF0] |
per-charaKind stature offset (the "size" attribute) |
chara+0x23C |
byte bCharaKindByte |
indexes the stature table |
chara+0x44960 |
float |
foot-bone world Y |
chara+0x44968 |
float |
head-bone world Y |
chara+0x44978 |
float |
projected reach scalar |
chara+0x19DC |
u16 |
reach override (when non-zero, replaces head-foot subtraction) |
chara+0x1364 |
float |
floor-contact height (used during combo-active branch) |
chara+0x212C |
u16 |
attacker's outgoing throw yarareId |
chara+0x212E |
u16 |
defender's incoming throw yarareId (0xFFFF when no throw stamped) |
KindTag inventory (partial)¶
| Tag | Role |
|---|---|
| 0 | passive structural (in floor) |
| 1 | move-driven active attack (NOT in floor) |
| 2..5 | passive hurtbox tiers (in floor) |
| 6, 7 | foot-anchored hit volumes (trigger ground-clamp pass) |
| 8..17 | other always-on structural volumes (in floor) |
| 18..21 | move-specific extensions |
| 22 | VFX-trigger marker (LuxMoveVM_TransitionToMove special case) |
| 23 | terrain-contact-blend marker |
| 31 | throw / grab |
| 55 | throw / grab |
The 22-wide PerHurtboxBitmask array mirrors this — one slot per kind tag.
Block direction & ducking-while-guarding¶
How the engine decides whether a hit is BLOCKED, HITS, or DUCKS UNDER, based on the defender's stance against the attack's level.
The two-byte stance system¶
The defender carries TWO crouch-state bytes inside the per-chara
motion-flag bank (chara+0x16D0..+0x170F, 64 single-byte flags):
| Byte | Flag idx | Name | Role |
|---|---|---|---|
chara+0x16D2 |
0x02 | bGuardCrouchStateBase |
AUTHORITATIVE crouch state. Set/cleared only when a stance-changing MOVE is committed (via LuxMoveVM_ApplyLaneMotionInputMasks @ 0x1402FD7F0, which reads the move-bank slot's qwInputSetMask / qwInputClearMask at +0x20/+0x28). 0 = standing, 1 = crouching. |
chara+0x16FC |
0x2C | bAltGuardCrouchState |
PER-FRAME re-derived stance. Reset to 0 every tick by TickCharaMainSimulation @ 0x14034DA70, then re-computed by LuxBattleChara_UpdateGuardStanceFlags @ 0x140309370 when a guard predicate needs the freshest answer. Reads raw inputs from chara+0x2150 / +0x2164 / +0x3494. |
chara+0x1701 |
0x31 | bAltGuardLocked |
When set, the alt is authoritative for hit-resolution. Cleared each tick + set by UpdateGuardStanceFlags. |
chara+0x16FD |
0x2D | bIsGuardingFlag |
Set this frame when the chara is actively in a guard pose with valid guard input. |
Resolver: LuxBattleChara_CheckGuardConditionForHitbox @ 0x1403056E0¶
Returns the chara's effective crouch state for matching against an incoming attack:
byte CheckGuardCondition(chara, cell)
{
if (cell != NULL && (chara+0x3494 & 0x08) == 0) {
// (bit 3 of +0x3494 = guard-disabled — fall through to alt)
if (chara+0x16DC != 0 && (cell.AttackFlags & 0x3) != 0)
return chara+0x16D2; // BLOCKSTUN + block-tagged attack: lock to base
if (chara+0x16DB != 0 && (cell.AttackFlags & 0x80) != 0)
return chara+0x16D2; // HITSTUN + high-tagged attack: lock to base
}
return (chara+0x1701 != 0) ? chara+0x16FC // alt (per-frame)
: chara+0x16D2; // base (move-VM)
}
The two LOCK conditions enforce the rule that you cannot change stance mid-blockstun or mid-hitstun. The "alt" mechanism applies only during neutral, between attacks, or in non-blocking gates.
Per-frame stance update: LuxBattleChara_UpdateGuardStanceFlags @ 0x140309370¶
Reads inputs and stance constraints, writes chara+0x16FC and
chara+0x16FD. Algorithm:
if (chara+0x1701 != 0) return; // already locked
inputAllowed = (chara+0x132C <= 1) || (chara+0x3494 & 0x04);
newStance = chara+0x16D2; // start from base
if (chara+0x198C == 0x16) {
// Perfect-guard window — lock stance to base.
} else if (chara+0x16DC == 0 && inputAllowed && (chara+0x2150 >> 3) & 1) {
// Not in blockstun, stance-change allowed, guard input held.
mask = chara+0x2164 & 0x4004; // bits 2 & 14 of input mask
if ((chara+0x3494 & 1) && mask == 0) newStance = 0; // STAND input
if ((chara+0x3494 & 2) && mask != 0) newStance = 1; // SIT input
}
chara+0x16FC = newStance;
// Verify input matches new stance and commit IS-GUARDING latch:
if ((chara+0x3494 & 3) != 0) {
if (newStance == 0 && (chara+0x3494 & 1) == 0) return;
if (newStance == 1 && (chara+0x3494 & 2) == 0) return;
}
if (((chara+0x2150 >> 3) & 1) && inputAllowed) chara+0x16FD = 1;
chara+0x1701 = 1; // lock the alt for this tick
chara+0x16D1 = 0;
Field decode:
| Field | Bits / value | Meaning |
|---|---|---|
chara+0x3494 |
bit 0 (0x01) | "STAND-input allowed" flag from move bank |
chara+0x3494 |
bit 1 (0x02) | "SIT-input allowed" flag from move bank |
chara+0x3494 |
bit 2 (0x04) | "stance-change allowed" gate |
chara+0x3494 |
bit 3 (0x08) | "guard-disabled" — guard ignored entirely |
chara+0x3494 |
bit 5 (0x20) | guard-impact-related |
chara+0x2150 |
bit 3 | "guard input active" master gate |
chara+0x2164 |
bit 2 (0x0004) + bit 14 (0x4000) | DOWN-direction held (the SIT input) |
chara+0x132C |
int | nDefenseModeAtLastHit — sample of chara+0x1334 taken at the moment of the last hit. See DefenseMode below. > 1 blocks free stance change unless +0x3494 bit 2 overrides. |
chara+0x198C |
== 0x16 | perfect-guard window — stance locked to base |
chara+0x3494 is set by LuxBattleChara_ApplyHitReactionMove from the
move bank slot's +0x5C field — so it's authored per-move.
DefenseMode (chara+0x1334)¶
chara+0x1334 is a per-tick "DefenseMode" classifier output, recomputed
every simulation tick by LuxBattleChara_EvaluateDefenseMode @
0x14034EA60 and stashed straight into chara+0x1334 from
TickCharaMainSimulation. Values:
| Value | Meaning |
|---|---|
| 1 | normal defense (no special condition) |
| 2 | opp not attacking |
| 3 | self recovering, opp attacking, move type ≠ 1 |
| 4 | self backing away (+0x16D5 held), opp attacking |
| 6 | counter window available (bank slot has counter-hit followup, no guard break) |
| 8 | hit recovery (+0x1718/+0x1719 set, in mid-recovery move state 6..8) |
| 10 | counter super-armor / specific stance ID |
| 11 | full counter (+0x170B set) |
| 13 | special mode (+0x171A set) |
chara+0x132C is a SAMPLE of this value, taken in
LuxBattleChara_ProcessHit @ 0x140342780 when a hit lands —
"the DefenseMode the chara was in when last hit". TickCharaMainSimulation
resets it back to 1 each tick whenever the chara is not in
hitstun, or is in blockstun (+0x16DB == 0 || +0x16DC != 0), so it
retains a non-trivial value only during pure hitstun.
The +0x132C > 1 test in UpdateGuardStanceFlags therefore means "the
chara was in a special defensive posture (counter window / super armor /
backing / hit recovery / etc.) when last hit, and is still in the
hit-recovery move now". When that holds, free stance-change via raw
input is denied unless the move explicitly opts in via
+0x3494 bit 2. Effect on play:
- Normal blockstun release →
+0x132C == 1(the per-tick reset fires during blockstun), so ducking/un-ducking mid-block is unrestricted. This is what enables the duck-under-on-blockstun-release trick. - Hit-recovery from a special-state hit →
+0x132Cretains the fancy value, so the chara cannot free-flip stance during the recovery unless the recovery move is authored to allow it.
Hit-resolution use¶
Both LuxBattle_ResolveAttackVsHurtboxMask22 @ 0x14033C100 and
LuxMoveVM_EvaluateMoveTransition @ 0x14033E140 call the resolver above
and use the returned byte as the "stance" axis of the (stance, attack
level) match table:
| Defender stance | Attack flag (cell+0x32) | Outcome |
|---|---|---|
| 0 (standing) | bit 7 (0x80) HighAttack | Hit normally; not blocked unless guarding HIGH |
| 0 (standing) | bit 3 (0x08) LowAttack | Cannot block low while standing — HITS |
| 0 (standing) | bit 4 (0x10) MidAttack | Blocks if guarding (any stance) |
| 1 (crouching) | bit 7 (0x80) HighAttack | DUCKS UNDER — hurtbox geometry already excluded the hit upstream; if it reached this gate, returns "no transition" |
| 1 (crouching) | bit 3 (0x08) LowAttack | Blocks if guarding |
| 1 (crouching) | bit 4 (0x10) MidAttack | Blocks if guarding |
The "duck under high" works on two levels:
1. Geometry: the chara's hurtbox shapes shrink when crouched, so the
high attack's KHit shape simply does not overlap. No bit gets OR'd
into PerHurtboxBitmask, and the classifier never sees the attack.
2. Resolver fallback: even if geometry passes (e.g. a tracking
high), EvaluateMoveTransition returns 0 (no transition) when the
stance + attack-level combination has no legal block / hit outcome.
Throw exception¶
The throw pre-scan in the classifier uses the resolved stance byte:
if ((attackerMask & 0x0080000080000000) != 0 // throw bits set
&& (isBlockingActive == 0 || blockDirMatches != 0)) {
// throw connects
}
A stand-block of a high throw escapes it
(isBlockingActive=1 && blockDirMatches=0). A crouch-block
(blockDirMatches=1) does not — high throws connect against
crouching defenders.
Why ducking mid-blockstun-release works¶
The classic SC tech of "duck under a high while in blockstun":
- Defender is in blockstun from the previous mid attack.
chara+0x16DC = 1,chara+0x16D2 = 0(standing block). - Resolver locks to base byte (
+0x16D2 = 0) for any cell with bit 0 or 1 set inAttackFlags— so within blockstun, the stance can't change. - Blockstun expires (
+0x16DC = 0), but the chara is still mid-recovery. - Player presses Down + Guard.
UpdateGuardStanceFlagssees: +0x16DC == 0✓+0x132Cnot blocking- guard input held (
+0x2150 bit 3) - sit input held (
+0x2164 bit 2) → writeschara+0x16FC = 1and+0x1701 = 1(alt locked). - The next-attack hit-resolution reads
+0x16FC = 1(crouched), ducks under any high attack that lands during this 1-frame window. - Two ticks later the player commits to a crouching guard MOVE, the
move's
qwInputSetMaskwrites+0x16D2 = 1(base now crouched), and the alt mechanism is no longer needed.
The window is exactly one tick, between blockstun ending and the move-VM committing a stance-change move. During that window the alt is the only thing keeping the chara in the "crouching" stance — which is why a high attack that would have hit during blockstun gets dodged on the release frame.
Reaction-state values¶
PerHurtboxReactionState[i] (i32[22] at chara+0x1C74) — the LuxHitReactionState enum:
| Value | Meaning |
|---|---|
| 0 | None |
| 1 | Hit |
| 2 | BlockedLow |
| 3 | BlockedHigh |
| 4 | MH_Loser |
| 6 | Tech |
| 8 | MH_Winner |
| 9 | AirHit |
| 0xA | MH_Trade |
| 0xB | WallSplat |
| 0xC | Stagger |
Deserialisation path¶
Move bytecode hit volumes are deserialised on move-start by
LuxBattle_InitCharaSlotForMove_FirstRound @ 0x1402D4070, which calls
Lux_KHitChk_DeserializeLinkedList three times, once per stream type, into the three
list heads:
| Stream | Init helpers | List head | Max-slot |
|---|---|---|---|
| BODY | KHitChk_InitSphereFromStream @ 0x14030E0D0 (sphere) / KHitChk_InitAreaFromStream @ 0x14030E3A0 (area) / inlined FixArea branch |
chara+0x44478 |
chara+0x44484 |
| HURTBOX | (same three helpers) | chara+0x444B8 |
chara+0x444B4 |
| ATTACK | (same three helpers) | chara+0x44498 |
chara+0x44494 |
All three init paths set KHitBase.+0x14 = 1 when a node is emitted. Attack
nodes are then refreshed by TickHitResolutionAndBodyCollision / SetActiveMoveSlot;
hurtbox nodes are toggled by opcode 0x13AC; body nodes use the body/pushbox path.
Per-frame world-center updates run in LuxBattleChara_UpdateAllKHitWorldCenters @
0x14030D6A0, which dispatches to the subclass updater:
KHitSphere_UpdateWorldCenter @ 0x14030E1A0—World = M * BoneLocalusing the chara's skeletal-mesh pose matrix.KHitArea_UpdateWorldCenters @ 0x14030E480— same per endpoint, plus toggles the double-buffer.
Reading hit volumes from a mod¶
Do not rebuild KHit collision geometry through
ALuxBattleChara_GetBoneTransformForPose. That helper returns a render-space
pose-bank matrix and can disagree with the live battle-pose matrix array that
LuxBattleChara_UpdateAllKHitWorldCenters uses for collision. It also returns
identity if called with the static battle slot instead of the UObject-shaped
chara context.
Read the native world buffers that the engine has already written into each node, then convert battle-world coordinates to UE render coordinates once:
Use these subclass buffers:
| Subclass | Native fields to read | Render note |
|---|---|---|
KHitSphere |
+0x50 current world center, +0x60 previous world center, +0x70 live radius |
Convert center with the axis remap above; radius is +0x70 * 100.0f. |
KHitArea |
Current and previous endpoint buffers at +0x50..+0x6F and +0x70..+0x8F |
g_LuxKHitArea_DoubleBufferToggle @ 0x14470DEC4 selects which buffer is current this tick. Draw both halves when visualising CCD sweep. |
KHitFixArea |
+0x60, +0x70, +0x80 world reference points |
Convert the three points; derive/draw the OBB axes from P1->P2 and P1->P3. |
ConvertBattlePoseMatrixToUERenderMatrix @ 0x1404555A0 explains the same
render-space convention: it applies g_LuxMoveVM_FrameDivisor60 @ 0x143E8A4C8
(100.0f) to translation. g_LuxCmToUEScale @ 0x143E8A418 (10.0f) belongs
to the visual Trace/FLuxCapsule pipeline; do not use it for KHit drawing.
Frame-accurate attack visibility¶
The mask in **(chara+0x44058) is set per current move-slot and can stay
constant across that slot's startup, active, and recovery frames. For an overlay
predicate that matches the engine's "can this attack fire a reaction now?" use:
primaryMask = cell ? cell->u64SlotMask : 0;
active = attackNode->ActiveThisFrame != 0 // node+0x14, geometry gate
&& (attackNode->PerAttackerBit & primaryMask) // node+0x08 vs cell+0x00
&& *(uint8_t*)(chara + 0x16EA) != 0; // active-frame window
For the phase display, chara+0x1980 is:
chara+0x16EA is the stricter boolean form of phase == 2: it is cleared when
the sub-window inhibitors at +0x16EB or +0x16FE are set.
TickHitResolutionAndBodyCollision also looks up a sub-frame cell while building
the attack-list +0x14 hot mask:
moveSubId = *(uint16_t*)(chara + 0x44dc2); // current sub-frame id
bankBase = *(void** )(chara + 0x455c0); // MoveVM bank base
subBank = (moveSubId >> 12) & 0xF; // 0..15 sub-bank index
subOff = *(uint16_t*)(bankBase + (subBank + 7)*4);
subCnt = *(uint16_t*)(bankBase + 0x1e + subBank*4);
frameIdx = moveSubId & 0x7FF;
if (frameIdx < subCnt) {
sfRec = bankBase + (subOff + frameIdx)*0x48 + 0x30;
cellBone = *(int16_t*)(sfRec + 0x3c);
cell = (uint64_t*)(bankBase
+ *(uint32_t*)(bankBase + 0x10)
+ cellBone * 0x70);
subFrameMask = *cell; // OR'd into the attack-list +0x14 hot mask
}
That sub-frame mask is a hot-mask layer, not a complete active-damage predicate;
many SC6 moves keep this layer empty and rely on the current +0x44058 cell plus
the +0x1980/+0x16EA frame-window classifier.
Lane 2 alt-classify can add a second attacker mask for Soul Charge, GI/parry,
and stance-tech paths. LuxBattle_ResolveAttackVsHurtboxMask22 opens that pass
when attacker+0x1725 != 0, +0x1726 != 0, +0x1727 == 0, and +0x1728 == 0,
then resolves the Lane 2 cell from attacker+0x44DC2, +0x45220, and
+0x455C0. A strict overlay should OR that Lane 2 mask into primaryMask while
the alt gate is open.
What's still unfound¶
- Projectile attacks (Cervantes's pistol, etc.). Whether they have entries in the attack list with a ranged-indicator kind tag, or use a parallel path, is open.
- Per-character KindTag dictionaries. Tags ≥18 and per-style overrides are not catalogued.
- Cross-reference with the trace system. The visual
FLuxCapsulesystem and the KHitKHitAreasubclass both encode bone-pair endpoints. Whether any move authoring tool emits both representations from a single source is not confirmed. See Trace System for the visual side.
Key binary addresses (RVA, image base 0x140000000)¶
Tick / classify / iterate¶
| Symbol | RVA | Description |
|---|---|---|
LuxBattle_TickHitResolutionAndBodyCollision |
0x33CCA0 |
Master hit-resolution tick. Plate comment in Ghidra documents the full hot-mask logic. |
LuxBattle_ResolveAttackVsHurtboxMask22 |
0x33C100 |
Classifier; reads OpponentActiveAttackCellCopy and walks PerHurtboxBitmask. |
LuxMoveVM_ClassifyHitboxFrameState |
0x300620 |
Writes chara+0x1980 phase and +0x16EA active-frame byte from cell+0x36/+0x38. |
LuxBattle_SolvePhysBodyCollision |
0x30CCF0 |
Body / pushbox physics. Reads BodyList. |
LuxBattleChara_UpdateAllKHitWorldCenters |
0x30D6A0 |
Per-tick world-center refresh; runs the OR-aggregation that fills PerHurtboxBitmask. |
Move-start deserialisation¶
| Symbol | RVA | Description |
|---|---|---|
LuxBattle_InitCharaSlotForMove_FirstRound |
0x2D4070 |
Top-level move-start init — calls the deserialiser for body / hurtbox / attack streams. |
Lux_KHitChk_DeserializeLinkedList |
(multiple sites) | Move-start deserialiser; called 3× from InitCharaSlotForMove_FirstRound. |
KHitChk_InitSphereFromStream |
0x30E0D0 |
Sphere-node deserialiser. |
KHitChk_InitAreaFromStream |
0x30E3A0 |
Area / FixArea deserialiser. |
Per-tick world-center updates¶
| Symbol | RVA | Description |
|---|---|---|
KHitSphere_UpdateWorldCenter |
0x30E1A0 |
Per-tick sphere-node world refresh. |
KHitArea_UpdateWorldCenters |
0x30E480 |
Per-tick area-node world refresh (double-buffer toggle). |
g_LuxKHitArea_DoubleBufferToggle |
0x14470DEC4 |
Selects current vs previous Area endpoint buffer. |
Hit / damage / state transitions¶
| Symbol | RVA | Description |
|---|---|---|
LuxBattleChara_ProcessHit |
0x342780 |
Defender-side post-hit handler; mirrors anim-frame into chara+0x1360. |
LuxBattle_ApplyDamageFromPendingHit |
(see Ghidra) | Damage application; checks PrimaryAttackCellPtr +0x44040 for "damage window expired". |
LuxMoveVM_TransitionToMove |
0x2FE350 |
Move-transition writer; sets per-lane state and +0x44060 non-attack descriptor. |
LuxMoveVM_SetActiveMoveSlot |
0x300C70 |
Re-resolves chara+0x44058, refreshes attack +0x14, mirrors cell tags, and re-runs ClassifyHitboxFrameState. |
LuxMoveVM_SetHurtboxSlotsActiveMask |
0x308D70 |
Hurtbox-list +0x14 writer. Called by VM opcode 0x13AC; inverted disable mask (1 disables, 0 enables). |
LuxMoveVM_AdvanceLaneFrameStep |
0x2FFEB0 |
Per-tick lane frame-step advance. |
LuxMoveVM_CommitMoveEnd |
0x2FCFB0 |
Move-end finaliser. |
LuxBattleChara_SetStartPosition |
0x301E60 |
Canonical chara-teleport call. Writes +0xA0 / +0xC0 / +0x2090 triples. |
LuxBattle_PositionCharasSymmetrically |
0x302670 |
Round-start pose; writes the side-flag at +0x23C. |
LuxEffectCamera_EvaluateAndTriggerSlowMotion |
0x31D8F0 |
Slow-motion gate; reads from active lane cursor. |
Throw connection / yarare dispatch¶
| Symbol | RVA / Address | Description |
|---|---|---|
LuxMoveVM_TickPickAndDispatchReaction |
0x2DEF50 |
Per-tick reaction picker. Contains the height-bucket gate that drops small-vs-tall throws (the dispatch-allow bitset at ~0x1402DFBC0). |
LuxMoveVM_GetCharaEffectiveHeight |
0x309470 |
Computes per-chara effective height. Default branch: head.Y - statureTable[bKindByte] - foot.Y. |
LuxBattle_DispatchYarareReaction |
(see Ghidra) | Actual reaction commit — gated by TickPickAndDispatchReaction's allow set. |
LuxBattle_CheckYarareReactionGate |
0x362E70 |
Per-yarare-table-entry gate test (consults EvaluateAttackRange for case 0x72). |
LuxMoveVM_EvaluateAttackRange |
0x35F670 |
Per-cell reach gate consulting cell+0x62..+0x66 range bounds and cell+0x38 reach ceiling. |
g_LuxBattle_CharaKindStatureTable |
0x14470D200 |
Per-charaKind stature offset table (stride 0xF0, indexed by chara+0x23C). The "size attribute" that distinguishes tall and short charas. |
Throw-catch yarareIds NOT in the unconditional allow set ({0x1F, 0x21,
0x20, 0x2C-0x2D, 0x2E-0x31, 0x3B, 0x28-0x2B@intensity>1, 0xE-0x10})
require the defender's effective-height bucket to be < 5 for the
reaction to dispatch. When the defender is the tallest chara in the
cast, iVar7 >= 5 and the throw is silently dropped.
Bone / matrix helpers¶
| Symbol | RVA | Description |
|---|---|---|
ALuxBattleChara_GetBoneTransformForPose |
0x462760 |
Render-pose helper. Returns 4×4 affine, NOT FTransform; do not use it to rebuild KHit collision buffers. |
ConvertBattlePoseMatrixToUERenderMatrix |
0x4555A0 |
Applies the KHit-compatible battle-to-UE render axis/translation convention (*100 translation). |
LuxSkeletalBoneIndex_Remap |
0x898140 |
8-bit internal idx → UE skeleton bone idx. Returns 0xFFFFFFFF on failure. |
g_LuxMoveVM_FrameDivisor60 |
0x143E8A4C8 |
100.0f; render translation scale used by ConvertBattlePoseMatrixToUERenderMatrix and HorseMod's KHit buffer conversion. |
g_LuxCmToUEScale |
0x143E8A418 |
10.0f; visual Trace/FLuxCapsule scale, not the KHit buffer scale. |
Subclass vtables¶
| Symbol | Address |
|---|---|
KHitBase_vftable |
0x143E87838 |
KHitSphere_vftable |
0x143E877F0 |
KHitArea_vftable |
0x143E877A8 |
KHitFixArea_vftable |
0x143E87760 |