Destructible props LIVE: revive StateInstanceSwitchRenderable (Gitea issue #3)

Playtest report: trucks play a collision sound on impact but never change
state -- no explosion swap, no destroyed model, collision volume persists.

Investigation (BT_CULT_LOG census + BT_GOTO ram harness): the SIM was never
broken.  ARENA1 map-streams 124 CulturalIcon entities, each with a damage
zone, explosion resource and removeOnDeath; the mech crunch dispatch
(ProcessCollision -> BTDispatchCollisionDamage) reaches them; ~19 walking
bumps burn the zone -> CulturalIcon::TakeDamageMessageHandler spawns the
Explosion (psfx effect 1008 observed), posts the delayed BurningState and
deletes the collision boxes.  All of that ran correctly and invisibly.

The ACTUAL bug: the 2007 WinTesla port fully stubbed
StateInstanceSwitchRenderable ('STUBBED: DPL RB 1/14/07') -- ctor never
registered on the state dial, Execute never toggled anything -- so the
intact->rubble visual swap on BurningState NEVER fired, for every cultural
icon in the game.  The destroyed truck kept its intact model: exactly the
reported 'no state change'.

Revival (D3D9-native, 1995 semantics verbatim):
- StateInstanceSwitchRenderable now controls the draw COMPONENT via the
  SetDrawObj in-place drawable swap (the mech RemakeEntity mechanism):
  visible = captured d3d_OBJECT, hidden = NULL.  Initial state in the ctor,
  AddVideoWatcher on the SimulationState dial (SetState fires video
  watchers), toggle only on a real change.
- L4VIDEO cultural case passes the draw component + object (the dead
  dpl_INSTANCE param is gone); [video] Object hides at BurningState,
  Rubble shows.
- REQUIRED: cultural objects stay OUT of static-mesh consolidation
  (RecurseStaticObject, same exclusion class as banded LODs/shadows) --
  a merged static draws forever regardless of DrawObj.
- Permanent env diag BT_CULT_LOG: creation census (zones/explosion/flags/
  pos), TakeDamage trace, death transition, [cultvis] switch actions.

Verified live (BT_GOTO=-620,-328 ram, ARENA1): 19 crunches -> DYING ->
explosion 1008 at the icon -> [cultvis] HIDE x2 (intact) + SHOW (rubble)
-> contacts cease, mech walks into the former footprint.  Boot + render
un-regressed (statics consolidate as before minus the 124 icons).

KB: context/rendering.md new section (the full chain + the consolidation
gotcha).  MP replicant path shares the same SetState watcher (cross-pod
verification pending -- noted in the issue).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-19 09:23:02 -05:00
co-authored by Claude Fable 5
parent 0bcba26213
commit 53ac927d4b
5 changed files with 177 additions and 89 deletions
+22
View File
@@ -243,6 +243,28 @@ the user plays the DEBUG build, but the 10fps was NOT the debug build (RelWithDe
split draw-vs-present + check the DEVICE flags before blaming the compiler. Diagnostics kept:
`[spike]/[loadobj]/[rstat]`. [T2]
## Cultural-icon (map prop) DESTRUCTION — revived 2026-07-19 (Gitea issue #3) [T2]
Trucks/props/buildings are **CulturalIcon/Landmark** entities (`engine/MUNGA/cultural.h`, map-
streamed via INTEREST.cpp; ARENA1 = 124 icons, each with a damage zone, `ExplosionModelFile`,
`removeOnDeath`). The SIM was always complete [T0]: ram/weapon damage → `CulturalIcon::
TakeDamageMessageHandler` → zone `damageLevel ≥ 1`**Explosion entity** (Crunch variant for
collision) + `WaitingToBurn` (+`timeDelay`) → `BurningState` + `UpdateCollisionVolumes` (boxes
DELETED on `removeOnDeath`, else sunk by `destroyedYTranslation`). The mech-side crunch dispatch
(`mech4.cpp ProcessCollision``BTDispatchCollisionDamage`, ×0.001 ram economy) reaches icons
(≈19 walking-speed bumps kill the spawn-yard truck). What was BROKEN (the "no state change"
report): the 2007 port fully stubbed **`StateInstanceSwitchRenderable`** ("STUBBED: DPL RB
1/14/07") — the intact→rubble visual swap NEVER fired. Revived D3D9-native: the switch now
controls the draw component via the **`SetDrawObj` in-place drawable swap** (the mech
RemakeEntity mechanism); `[video] Object` hides at BurningState, `Rubble` shows; watcher =
`StateIndicator::AddVideoWatcher` (SetState DOES fire video watchers). ⚠ REQUIRED gotcha:
cultural objects must stay **OUT of static-mesh consolidation** (`RecurseStaticObject` — same
exclusion class as banded LODs/shadows), else the merged mesh draws the intact model forever
regardless of DrawObj. Explosion visual = the psfx route (effect 1008 observed). Verified live
(BT_GOTO ram): HIDE×2 + SHOW(rubble), contacts cease, mech occupies the footprint. Diags:
`BT_CULT_LOG` (census/damage/death + `[cultvis]`). MP: replicants transition via
`ReadUpdateRecord` → same SetState watcher path (untested cross-pod).
## Key Relationships
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
- Renders on: [[pod-hardware]] (main 3D view).
- Prop destruction feeds: [[combat-damage]] (crunch dispatch) · uses the psfx layer.
+34
View File
@@ -73,6 +73,16 @@ void
{
Check(damage_message);
// BT_CULT_LOG (issue #3): does damage ever reach a cultural icon?
if (getenv("BT_CULT_LOG"))
{
DEBUG_STREAM << "[cult] TakeDamage entity=" << (long)GetEntityID()
<< " type=" << (int)damage_message->damageData.damageType
<< " amount=" << (float)damage_message->damageData.damageAmount
<< " zone=" << damage_message->damageZone
<< " state=" << GetSimulationState()
<< "\n" << std::flush;
}
if (damage_message->damageZone == -1 )
{
@@ -144,6 +154,15 @@ void
Explosion::Make(&exp_message);
Register_Object(new_exp);
// BT_CULT_LOG (issue #3): the death transition fired
if (getenv("BT_CULT_LOG"))
{
DEBUG_STREAM << "[cult] DYING entity=" << (long)GetEntityID()
<< " explosion res=" << explosion_ID
<< " delay=" << (float)timeDelay
<< " -> WaitingToBurn\n" << std::flush;
}
Message dummy_message(SetBurningStateMessageID, sizeof(Message));
Check(application);
Time now = Now();
@@ -335,6 +354,21 @@ CulturalIcon::CulturalIcon(
}
SetValidFlag();
dmg_res->Unlock();
// BT_CULT_LOG: per-icon creation census (issue #3 -- truck destruction)
if (getenv("BT_CULT_LOG"))
{
DEBUG_STREAM << "[cult] made icon entity=" << (long)GetEntityID()
<< " res=" << resourceID
<< " zones=" << damageZoneCount
<< " explode=" << explosionResourceID
<< " crunch=" << crunchExplosionResourceID
<< " removeOnDeath=" << (int)RemoveCollisionVolumeOnDeath()
<< " stopping=" << (int)IsStoppingCollisionVolume()
<< " pos=(" << localOrigin.linearPosition.x
<< "," << localOrigin.linearPosition.y
<< "," << localOrigin.linearPosition.z << ")"
<< "\n" << std::flush;
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+23 -9
View File
@@ -3813,6 +3813,20 @@ void DPLRenderer::RecurseStaticObject(HierarchicalDrawComponent *obj)
// (translucent + depth bias); merging them would draw them opaque.
if (dobj->GetIsShadow())
banded = true;
// Issue #3 (cultural destruction): CulturalIcon/Landmark objects are
// DESTRUCTIBLE -- their intact model must vanish (and rubble appear) at
// BurningState via the revived StateInstanceSwitchRenderable's
// SetDrawObj swap. A model merged into the consolidated static mesh
// draws forever regardless of DrawObj, so destructibles stay OUT of
// consolidation and keep drawing individually (the same mechanism as
// the banded-LOD exclusion above).
{
Entity *owning = obj->GetOwningEntity();
if (owning != NULL
&& (owning->GetClassID() == CulturalIconClassID
|| owning->GetClassID() == LandmarkClassID))
banded = true;
}
if (!banded)
{
//Only load it for a static object if it has a valid mesh-
@@ -7086,12 +7100,6 @@ void
case CulturalIconClassID:
case LandmarkClassID:
{
dpl_INSTANCE *this_instance = NULL; // BT bring-up: the per-object instance
// assignments below are DPL-stubbed
// (commented out) -> initialise so the
// /RTC1 uninitialised-variable check does
// not fault when this_instance is passed
// to StateInstanceSwitchRenderable.
d3d_OBJECT *this_object;
dpl_ISECT_MODE intersect_mode;
HierarchicalDrawComponent *component = NULL;
@@ -7204,7 +7212,10 @@ void
// this_instance = another_instance->GetInstance();
}
}
// add the new object to our renderables list
// add the new object to our renderables list. Keep the draw
// component in hand -- the revived state switch (issue #3)
// controls IT (SetDrawObj show/hide), not a dead DPL instance.
HierarchicalDrawComponent *draw_component = component;
if (component)
mRenderables.Add(component);
component = NULL;
@@ -7212,6 +7223,7 @@ void
// Hook up the object to an instance switch renderable responsive to state
//
StateIndicator* simulation_state = (StateIndicator *)entity->GetAttributePointer("SimulationState");
if (simulation_state != NULL && draw_component != NULL)
switch(resource_type)
{
case L4VideoObject::Object:
@@ -7219,7 +7231,8 @@ void
component = new StateInstanceSwitchRenderable(
entity, // Entity to attach the renderable to
StateInstanceSwitchRenderable::Watcher, // How/when to execute the renderable
this_instance, // the instance to control
draw_component, // the draw component to control
this_object, // its drawable (restored on show)
False, // true to turn on in this state, false for off
simulation_state, // State dial we use to control the on/off
CulturalIcon::BurningState); // State that we look for
@@ -7230,7 +7243,8 @@ void
component = new StateInstanceSwitchRenderable(
entity, // Entity to attach the renderable to
StateInstanceSwitchRenderable::Watcher, // How/when to execute the renderable
this_instance, // the instance to control
draw_component, // the draw component to control
this_object, // its drawable (restored on show)
True, // true to turn on in this state, false for off
simulation_state, // State dial we use to control the on/off
CulturalIcon::BurningState); // State that we look for
+71 -74
View File
@@ -4118,51 +4118,47 @@ void
// Constructor for StateInstanceSwitchRenderable
//
StateInstanceSwitchRenderable::StateInstanceSwitchRenderable(
Entity *entity, // Entity to attach the renderable to
ExecutionType execution_type, // How/when to execute the renderable
dpl_INSTANCE *this_instance, // the instance to control
Logical sense, // true to turn on in this state, false for off
StateIndicator *state_dial, // State dial we use to control the on/off
unsigned trigger_state // State that we look for
Entity *entity, // Entity to attach the renderable to
ExecutionType execution_type, // How/when to execute the renderable
HierarchicalDrawComponent *controlled_component,// the draw component to show/hide
d3d_OBJECT *controlled_object, // the drawable it shows when visible
Logical sense, // true to turn on in this state, false for off
StateIndicator *state_dial, // State dial we use to control the on/off
unsigned trigger_state // State that we look for
):
VideoRenderable(entity, execution_type)
{
//STUBBED: DPL RB 1/14/07
////
//// Check incoming data for correctness
////
//Check_Pointer(this_instance);
//Check(state_dial);
////
//// Initialize variables
////
//myInstance = this_instance;
//mySense = sense;
//myStateDial = state_dial;
//myTriggerState = trigger_state;
////
//// Put the instance in the proper state and std::flush it
////
//if(myStateDial->GetState() == myTriggerState)
//{
// if(mySense == True)
// myLastInstanceState = True;
// else
// myLastInstanceState = False;
//}
//else
//{
// if(mySense == True)
// myLastInstanceState = False;
// else
// myLastInstanceState = True;
//}
//dpl_SetInstanceVisibility ( myInstance, myLastInstanceState );
//dpl_FlushInstance ( myInstance );
////
//// Connect us to the state dial's watcher hook
////
//state_dial->AddVideoWatcher(this);
// REVIVED (issue #3): the 1995 body verbatim, retargeted from the dead DPL
// instance-visibility API onto the port's SetDrawObj in-place drawable swap.
//
// Check incoming data for correctness
//
Check_Pointer(controlled_component);
Check(state_dial);
//
// Initialize variables
//
myComponent = controlled_component;
myObject = controlled_object;
mySense = sense;
myStateDial = state_dial;
myTriggerState = trigger_state;
//
// Put the instance in the proper state
//
if(myStateDial->GetState() == myTriggerState)
{
myLastInstanceState = mySense ? True : False;
}
else
{
myLastInstanceState = mySense ? False : True;
}
myComponent->SetDrawObj(myLastInstanceState ? myObject : NULL);
//
// Connect us to the state dial's watcher hook
//
state_dial->AddVideoWatcher(this);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Destructor for StateInstanceSwitchRenderable
@@ -4178,7 +4174,7 @@ Logical
{
// Call our parent's TestInstance first
VideoRenderable::TestInstance();
Check_Pointer(myInstance);
Check_Pointer(myComponent);
Check(myStateDial);
return True;
}
@@ -4191,37 +4187,38 @@ Logical
void
StateInstanceSwitchRenderable::Execute()
{
//STUBBED: DPL RB 1/14/07
//unsigned
// new_state;
////
//// Check our variables
////
//Check(this);
////
//// See what state we should go into
////
//if(myStateDial->GetState() == myTriggerState)
//{
// if(mySense == True)
// new_state = True;
// else
// new_state = False;
//}
//else
//{
// if(mySense == True)
// new_state = False;
// else
// new_state = True;
//}
//if(new_state != myLastInstanceState)
//{
// myLastInstanceState = new_state;
// dpl_SetInstanceVisibility ( myInstance, myLastInstanceState );
// dpl_FlushInstance ( myInstance );
//}
//// Call the next lower execute method
// REVIVED (issue #3): the 1995 body verbatim on the SetDrawObj swap.
unsigned
new_state;
//
// Check our variables
//
Check(this);
//
// See what state we should go into
//
if(myStateDial->GetState() == myTriggerState)
{
new_state = mySense ? True : False;
}
else
{
new_state = mySense ? False : True;
}
if((Logical)new_state != myLastInstanceState)
{
myLastInstanceState = (Logical)new_state;
myComponent->SetDrawObj(myLastInstanceState ? myObject : NULL);
if (getenv("BT_CULT_LOG"))
{
DEBUG_STREAM << "[cultvis] switch: entity=" << (long)myEntity->GetEntityID()
<< " sense=" << (int)mySense
<< " state=" << myStateDial->GetState()
<< " -> " << (myLastInstanceState ? "SHOW" : "HIDE")
<< "\n" << std::flush;
}
}
// Call the next lower execute method
VideoRenderable::Execute();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+27 -6
View File
@@ -72,6 +72,12 @@ public:
// task #58 diagnostics: expose the parent link so probes can walk the tree.
HierarchicalDrawComponent *GetParentProbe() const { return m_parent; }
// Issue #3 (cultural destruction): the consolidation pass must identify which
// entity a static component belongs to (destructible CulturalIcon/Landmark
// objects stay OUT of the merged static mesh so their visibility can switch).
// Base components have no entity link; VideoRenderable overrides with myEntity.
virtual Entity *GetOwningEntity() const { return NULL; }
d3d_OBJECT *GetDrawObj() { return this->graphicalObject; }
// Swap this component's drawable in place. Execute() re-reads graphicalObject
// every frame, so changing it makes the segment draw a different mesh next
@@ -241,6 +247,8 @@ class VideoRenderable:
virtual bool IsStatic() { return (this->myExecutionType == ExecutionType::Static); }
virtual Entity *GetOwningEntity() const { return myEntity; } // issue #3 (cultural destruction)
protected:
Entity *myEntity; // The entity we are linked to
ExecutionType myExecutionType; // How/when to execute the renderable
@@ -1594,17 +1602,28 @@ class InstanceSwitchRenderable:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// StateInstanceSwitchRenderable
// REVIVED 2026-07-19 (issue #3, cultural destruction): the 2007 port stubbed the
// whole class ("STUBBED: DPL RB 1/14/07"), so the intact->rubble visual swap on
// CulturalIcon/Landmark death NEVER fired -- destroyed trucks/buildings kept
// their intact model. The DPL instance-visibility mechanism is dead in the
// D3D9 renderer; the port-native equivalent is the SetDrawObj() in-place
// drawable swap (the same mechanism the mech damage-model RemakeEntity swap
// uses). The switch now controls a HierarchicalDrawComponent directly:
// visible = component draws its captured d3d_OBJECT, hidden = DrawObj is NULL.
// Semantics preserved verbatim from the commented 1995 body: initial state set
// in the ctor, AddVideoWatcher on the state dial, toggle only on a real change.
class StateInstanceSwitchRenderable:
public VideoRenderable
{
public:
StateInstanceSwitchRenderable(
Entity *entity, // Entity to attach the renderable to
ExecutionType execution_type, // How/when to execute the renderable
dpl_INSTANCE *this_instance, // the instance to control
Entity *entity, // Entity to attach the renderable to
ExecutionType execution_type, // How/when to execute the renderable
HierarchicalDrawComponent *controlled_component,// the draw component to show/hide
d3d_OBJECT *controlled_object, // the drawable it shows when visible
Logical sense, // true to turn on in this state, false for off
StateIndicator *state_dial, // State dial we use to control the on/off
unsigned trigger_state); // State that we look for
unsigned trigger_state); // State that we look for
~StateInstanceSwitchRenderable();
@@ -1618,8 +1637,10 @@ class StateInstanceSwitchRenderable:
Logical
myLastInstanceState,
mySense;
dpl_INSTANCE
*myInstance;
HierarchicalDrawComponent
*myComponent; // the controlled draw component
d3d_OBJECT
*myObject; // its drawable (restored on show)
unsigned
myTriggerState;
StateIndicator