bugfix: Revert death module consolidation regressions - #172
Conversation
…inal DestroyDie module
|
Is there a technical explanation for why these bugs occur? Maybe there is a code bug in the new module or are the misbehavior warranted? |
The objects affected by this change did not originally have a The Aurora Bomb is set up in a funky way where the damage is only applied when the bomb impacts the ground rather than the target, so deleting it upon impacting the target caused it to never apply its initial impact damage. |
|
So if I understand right This is quite a bit confusing to keep track of. To me it sounds like |
The original intent was to combine |
|
Should we revert all of them to restore exact original behavior? To me it sounds like something else might be overlooked if we just revert part of it. |
I effectively reverted the original change and then recombined everything using the new rule where a I counted the number of objects with either a This effectively means that no *The singular 1,222 → 1,221 object discrepancy is caused by What else do you think could be overlooked? |
|
I lost the track here and do not understand. So the original issue was that the differences between the various death effect modules was not well understood and it is unclear to me if they are now 100% understood. The safest strategy would be to revert all death module consolidations to the original state, which will restore 100% original behavior. How confident are we that the current change with a partly reverted consolidations restores 100% original functionality? Is the confidence 100%, if yes, why, and if not, why not play it safe and revert its entirety? |
…ie module using RequiredStatus
|
I asked Claude Opus: Q: Breakdown similarities and differences of modules CreateObjectDie, DestroyDie, FireWeaponWhenDeadBehavior, FXListDie in relation to InstantDeathBehavior. In particular show under what circumstances the former 4 modules can be consolidated into a InstantDeathBehavior module. A: Die Module Consolidation:
|
| Module | Base classes | Module data |
|---|---|---|
DestroyDie (DestroyDie.h:39) |
DieModule |
inherits DieModuleData (no own fields) |
CreateObjectDie (CreateObjectDie.h:59) |
DieModule |
CreateObjectDieModuleData |
FXListDie (FXListDie.h:76) |
DieModule + UpgradeMux |
FXListDieModuleData |
FireWeaponWhenDeadBehavior (FireWeaponWhenDeadBehavior.h:72) |
BehaviorModule + UpgradeMux + DieModuleInterface |
FireWeaponWhenDeadBehaviorModuleData (holds its own DieMuxData) |
InstantDeathBehavior (InstantDeathBehavior.h:61) |
DieModule |
InstantDeathBehaviorModuleData |
All five are driven from the same place — Object.cpp:4697-4702 walks the behavior list in
INI declaration order and calls onDie on every module exposing getDie().
All five gate on the identical DieMuxData field set (DeathTypes, VeterancyLevels,
ExemptStatus, RequiredStatus) via DieModule.cpp:66. FireWeaponWhenDeadBehavior gets
them by embedding DieMuxData directly, since it is not a DieModule.
None of them persist per-instance state except the two UpgradeMux ones, which xfer their
upgrade flag.
So InstantDeathBehavior really is the union of the other four's core actions, in a fixed
order — FX -> OCL -> weapon -> destroy (InstantDeathBehavior.cpp:122-196).
2. Where the semantics diverge
a) All-of vs one-of
FX, OCL and Weapon on InstantDeathBehavior are vectors, and exactly one entry per
category is picked at random each death. Two FXListDie modules both play their FX; one
InstantDeathBehavior with two FX entries plays one of them. Collapsing N modules into N
list entries is a behavior change unless N = 1.
b) FX invocation
InstantDeathBehavior always calls FXList::doFXObj(fxl, obj, nullptr). That equals
FXListDie with OrientToObject = Yes (the default) and a null damage dealer.
OrientToObject = No is not representable — it routes to doFXPos(pos) with a null transform
matrix and a different shroud test (FXList.cpp:806-833). FX nuggets that consume the
secondary position also lose the killer.
c) OCL invocation
CreateObjectDie resolves damageInfo->in.m_sourceID and passes the damage dealer as the OCL
secondary (CreateObjectDie.cpp:96-98); InstantDeathBehavior passes nullptr.
FireWeaponNugget, AttackNugget and DeliverPayloadNugget all bail out with
DEBUG_CRASH("You must have a primary and secondary source for this effect") on a null
secondary. GenericObjectCreationNugget (ObjectCreation / Debris) and
ApplyRandomForceNugget ignore it and are safe.
d) CreateObjectDie extras
TransferPreviousHealth and TransferSelection (Zero Hour only — Generals has neither)
operate on the object returned by the OCL: subdual + health transfer, attacker retargeting,
and re-selection. InstantDeathBehavior discards the return value entirely, so neither is
expressible.
e) Under-construction guard
FireWeaponWhenDeadBehavior refuses to fire while OBJECT_STATUS_UNDER_CONSTRUCTION is set,
so cancelling a scaffold does not detonate it. InstantDeathBehavior only got that guard
behind #if !RETAIL_COMPATIBLE_CRC (InstantDeathBehavior.cpp:162-166) — in a
retail-compatible build the consolidated version will fire on a cancelled scaffold.
f) Upgrade gating
FXListDie and FireWeaponWhenDeadBehavior are UpgradeMux: StartsActive, TriggeredBy,
ConflictsWith, RequiresAllTriggers, plus a conflicting-mask test against both the
object's and the controlling player's completed upgrade masks. InstantDeathBehavior has none
of this. Note the differing defaults: FXListDie starts active (m_initiallyActive = TRUE,
the "847 cases" hack), FireWeaponWhenDeadBehavior starts inactive.
g) Destruction is unconditional
Only DestroyDie and InstantDeathBehavior call TheGameLogic->destroyObject. Folding an
FXListDie / CreateObjectDie / FireWeaponWhenDeadBehavior into an InstantDeathBehavior
adds a destroy the object previously did not have at that point.
h) AI dead-state handshake
InstantDeathBehavior uniquely calls markAsDead() — which also sets
setEffectivelyDead(TRUE) and wakes the AI — and skips if isAiInDeadState(). Under
RETAIL_COMPATIBLE_CRC that check runs first, so on an object with an AIUpdate whose AI is
already dead the whole module (FX, OCL, weapon) is skipped; in modern builds the check moved to
the end so only the destroy is guarded. None of the other four touch AI state.
i) RNG stream
In a RETAIL_COMPATIBLE_CRC build GameLogicRandomValue(0, 0) still draws from the logic seed
(delta == 1); in modern builds the lo >= hi early-out returns without drawing
(RandomValue.cpp:268-290). So even a single-entry consolidation perturbs the logic RNG stream
in retail-compat builds — a replay/CRC hazard, not just cosmetic.
j) Ordering
Four modules occupy four slots in the behavior list; the consolidated module occupies one.
destroyObject runs the onDestroy modules immediately, sets OBJECT_STATUS_DESTROYED and
clears the AI locomotor/path before deferring the actual delete, so later die modules in the
list still run but observe a destroyed object.
3. Consolidation checklist
CreateObjectDie + DestroyDie + FireWeaponWhenDeadBehavior + FXListDie -> one
InstantDeathBehavior is behavior-preserving only when all of these hold:
- Identical die filter across every module being merged — same
DeathTypes,
VeterancyLevels,ExemptStatus,RequiredStatus. The merged module has exactly one
filter. - At most one module of each kind, so there is no all-of -> one-of regression.
- A
DestroyDieis among them with that same filter, and noSlowDeathBehavior/
KeepObjectDie-style handler needs the object to surviveonDie. If there is no
DestroyDie, do not consolidate — you would be adding destruction. FXListDie:OrientToObject = Yes,StartsActive = Yes, noTriggeredBy/
ConflictsWith, and itsFXListdoes not rely on the secondary (source) object.CreateObjectDie:TransferPreviousHealth = No,TransferSelection = No, and the OCL
contains onlyObjectCreation/Debris/ApplyRandomForcenuggets — noFireWeapon,
AttackorDeliverPayloadnuggets, which need the secondary.FireWeaponWhenDeadBehavior:StartsActive = Yeswith noTriggeredBy/
ConflictsWith. Since the default is inactive, an upgrade-gated one can never be folded in.- AI: either the object has no
AIUpdate, or the addedmarkAsDead()/ dead-state gate is
acceptable — and there is no otherInstantDeathBehaviororSlowDeathBehavioron the
object contending for that flag. - Ordering: the merged modules are not interleaved with other die modules that observe
object state between them, and none of the survivors care aboutOBJECT_STATUS_DESTROYED
being set earlier than before. - Build: not
RETAIL_COMPATIBLE_CRC, if bit-exact retail replay/CRC parity matters —
points (e), (h) and (i) all diverge there.
Safe common cases
- The plain
FXListDie(singleDeathFX, defaults) +DestroyDiepair on a simple unit. CreateObjectDie(plain debris/object OCL) +FXListDie+DestroyDie.
Both collapse to a single InstantDeathBehavior with one entry per list.
Cases that cannot be consolidated
- The sneak-attack tunnel (
TransferPreviousHealth/TransferSelection). - Anything whose death OCL fires a weapon or delivers a payload at the killer.
- Upgrade-gated death explosions.
- Anything with
OrientToObject = No.
The reverse direction
An InstantDeathBehavior with multi-entry lists cannot be decomposed back into the four
modules at all, because the random one-of selection has no equivalent there.
The original issue is that I mistakenly combined any combination of
These two objects behave the same: Object Example_A1
Behavior = DestroyDie
Behavior = FXListDie
Behavior = CreateObjectDie
Behavior = FireWeaponWhenDeadBehavior
EndObject Example_A2
Behavior = InstantDeathBehavior
EndThese two objects do not behave the same: Object Example_B1
Behavior = FXListDie
Behavior = CreateObjectDie
Behavior = FireWeaponWhenDeadBehavior
EndObject Example_B2
Behavior = InstantDeathBehavior
EndThe consolidated The second commit address the fact that I missed applying the
I am confident this will restore original behaviour. However, I would agree that reverting the entire consolidation change for now on the |
Ok. Please review the LLM generated info if there are any additional caveats for us to consider, and then make your decision. |
This change is a follow-up to #118 and reverts death module consolidation for objects that did not originally have a
DestroyDiemodule defined, as well as severalInstantDeathBehaviormodules that were not gated byRequiredStatus. As a result, the following notable behaviours are restored:Most of the other affected objects are cinematic and never die in the first place or have unused data. Note that the
RebuildHoleBehaviormodule destroys its object in itsonDiemethod, making any death modules superfluous, and theBridgeBehaviormodule similarly handles its own death behaviour.This also means that several objects originally missing a
DestroyDiemodule that were indirectly fixed by #118 (e.g. crushing an empty Troop Crawler would leave an indestructible hulk) are now broken again, though these can be fixed separately.