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:
co-authored by
Claude Fable 5
parent
0bcba26213
commit
53ac927d4b
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user