Searchlight reconstruction -- the pod's night kit, both halves

The subsystem (sim/toggle/attribute/replication) was already complete; this
lands the missing VISUALS, decoded from MakeMechRenderables @004cef28 case
0xbd8 (raw pseudocode part_014):

- COCKPIT: the 1995 searchlight is a FOG SWAP -- the @00456778/@00456814
  watcher switches DPLRenderer::SetFogStyle between the authored fog= (lit)
  and nosearchlightfog= (dark) sets per map/time page in BTDPL.INI.  The
  engine kept the whole system under its real names; completed the stubbed
  plane application (currentFogNear/Far) and transcribed the watcher (with
  its inverted-cache seed) into TickSearchlight.  CONSEQUENCE: night now
  STARTS on the authentic dark set (near-plane 5u on arena pages) -- our
  builds had rendered the searchlight-ON fog permanently.
- EXTERNAL: spot.bgf beam cone hung on the searchlight SITE joint, shown/
  hidden from the replicated LightOn attribute (@0045612c watcher).  Site
  segments now build geometry-less DCS children (posed + parentable, as the
  1995 graph did) -- previously they were skipped entirely.
- searchlight.hpp: commandedOn @0x1DC identified as mountSegment (resource
  segmentIndex; the cone's mount joint).

Benches: searchfog.sh (solo cockpit: first-tick dark sync, F5/0x14 press ->
SetFogStyle(2), red-fog probe end-to-end), spotcone.sh (2-node: B's button ->
lightState replication -> A logs "[spot] cone SHOWN (seg 20)").  Cone look
(size/aim on the mount) pending an eyeball pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-05 19:44:23 -05:00
co-authored by Claude Fable 5
parent b31c6c4527
commit 412053d5af
7 changed files with 329 additions and 11 deletions
+167
View File
@@ -170,6 +170,99 @@ void
// localToWorld in its ctor -- no explicit AddDynamicRenderable here
// (unlike the 1996 VideoComponent path).
(void)mech_root;
//
// SEARCHLIGHT pass (2026-08-05) -- the roster walk's `case 0xbd8`
// (@004cef28, raw pseudocode part_014). The binary, per searchlight
// subsystem: resolve the "LightOn" attribute (error print
// BTL4VID.CPP:0x941 when absent); COCKPIT build (view 1) -> stash up
// to TWO attrs for the fog-swap watcher (@00456778); EXTERNAL build
// -> load "spot.bgf" (warn if missing), hang it as a child of the
// mount joint and bind visibility to the attribute (@0045612c). Our
// tree builder does not walk the roster, so the probe bridge
// (searchlight.cpp) supplies {attr, mount segment} and the two
// watcher Execute bodies live in TickSearchlight.
//
{
extern int BTMechSearchlightProbe(void *, int **, int *);
int *attrs[2];
int segs[2];
int count = BTMechSearchlightProbe((void *)entity, attrs, segs);
std::map<Entity*, MechRenderTree>::iterator ti =
mMechRenderTrees.find(entity);
if (count > 0 && ti != mMechRenderTrees.end())
{
MechRenderTree &tree = ti->second;
tree.searchLightCount = 0;
tree.searchIsCockpit = (view_type == 1);
for (int sl = 0; sl < count; ++sl)
{
MechRenderTree::SearchLight &light =
tree.searchLight[tree.searchLightCount];
light.cone = NULL;
light.coneObj = NULL;
light.lightOn = attrs[sl];
light.shown = 0;
light.mountSeg = segs[sl];
// the binary's inverted seed (@00456778: [9]/[10] =
// (*attr == 0)) -- guarantees the FIRST tick "changes"
// and normalizes the fog to the real lamp state, which
// is how a night mission starts on nosearchlightfog.
tree.searchFogCache[tree.searchLightCount] =
(*attrs[sl] == 0);
if (!tree.searchIsCockpit)
{
// NB: the renderer-level LoadObject wrapper (@00498448) is
// still a btstubs no-op; load through the d3d route the
// tree builder itself uses (extension REQUIRED there).
d3d_OBJECT *spot = d3d_OBJECT::LoadObject(
GetDevice(), (char *)"spot.bgf"); // @0051d6c0
if (spot == NULL)
{
DEBUG_STREAM << "[spot] Couldn't locate spot.bgf "
"for mech\n" << std::flush; // @0051d6c9
}
else
{
std::map<int, HierarchicalDrawComponent*>::iterator si =
tree.segRenderable.find(segs[sl]);
HierarchicalDrawComponent *parent =
(si != tree.segRenderable.end()) ? si->second : NULL;
if (parent == NULL)
{
// slot-space mismatch would land here -- keep the
// receipt loud (see the bench).
DEBUG_STREAM << "[spot] mount segment " << segs[sl]
<< " has no joint renderable -- cone skipped; slots:";
for (std::map<int, HierarchicalDrawComponent*>::iterator di =
tree.segRenderable.begin();
di != tree.segRenderable.end(); ++di)
DEBUG_STREAM << " " << di->first;
DEBUG_STREAM << "\n" << std::flush;
}
else
{
LinearMatrix identity(True);
dpl_ISECT_MODE gSpotIsectMode; // stub type (binary: mode 1)
light.cone = new DPLStaticChildRenderable(
entity, false /* main zone */, spot,
gSpotIsectMode, 0 /* mask 0: never pickable */,
identity, parent);
light.cone->SetDrawObj(NULL); // hidden until LightOn
light.coneObj = spot;
}
}
}
++tree.searchLightCount;
}
DEBUG_STREAM << "[spot] searchlight pass: " << count
<< " light(s), view=" << (int)view_type
<< " mountSeg=" << segs[0]
<< (tree.searchIsCockpit ? " (cockpit: fog watcher)"
: " (external: spot cone)")
<< "\n" << std::flush;
}
}
break;
}
@@ -492,6 +585,22 @@ HierarchicalDrawComponent*
mCamera = mEyeCockpit;
dbg_eye = 1;
}
//
// SEARCHLIGHT (2026-08-05): sites are ATTACHMENT joints, and the
// 1995 render graph gave every one a DCS node -- that node is what
// the subsystem-visual walk parents to (the spot cone hangs on the
// searchlight site, @004cef28 case 0xbd8). Build the geometry-less
// child so site slots are posed + parentable; nothing draws for the
// site itself.
//
{
dpl_ISECT_MODE site_isect;
HierarchicalDrawComponent *site_child = new DPLStaticChildRenderable(
entity, inDeathZone, NULL /* no geometry */,
site_isect, 0, offset_matrix, parent_DCS);
dcs_array[segment->GetIndex()] = site_child;
render_tree.segRenderable[segment->GetIndex()] = site_child;
}
continue;
}
@@ -1545,6 +1654,63 @@ void
}
//
//#############################################################################
// TickSearchlight -- the two 1995 searchlight watchers, transcribed
//#############################################################################
//
// External cone (@0045612c, Execute @004561d8): poll the subsystem's LightOn
// attribute; on change, set the spot instance visible iff the value matches
// (match value 1) and flush. Our equivalent of SetInstanceOn/Flush is the
// SetDrawObj object swap (the wreck-reveal mechanism).
//
// Cockpit fog (@00456778, Execute @00456814): poll BOTH stashed attrs; when
// either changed, either-on -> SetFogStyle(searchLightOnFogStyle) else
// SetFogStyle(searchLightOffFogStyle). The ctor's INVERTED caches make the
// first poll always fire, normalizing a night start onto nosearchlightfog --
// the authentic dark (fog= is the LIT set; see BTDPL.INI per-page authoring).
//
void
BTL4VideoRenderer::TickSearchlight(Entity *mech)
{
std::map<Entity*, MechRenderTree>::iterator ti = mMechRenderTrees.find(mech);
if (ti == mMechRenderTrees.end() || ti->second.searchLightCount <= 0)
return;
MechRenderTree &tree = ti->second;
int any_on = 0, changed = 0;
for (int sl = 0; sl < tree.searchLightCount; ++sl)
{
MechRenderTree::SearchLight &light = tree.searchLight[sl];
if (light.lightOn == NULL)
continue;
const int cur = (*light.lightOn != 0);
if (light.cone != NULL && cur != light.shown) // @004561d8
{
light.cone->SetDrawObj(cur ? light.coneObj : NULL);
light.shown = cur;
if (getenv("BT_FIRE_LOG") || getenv("BT_FOG_LOG"))
DEBUG_STREAM << "[spot] cone " << (cur ? "SHOWN" : "HIDDEN")
<< " (seg " << light.mountSeg << ")\n" << std::flush;
}
if (cur != tree.searchFogCache[sl]) // @00456814
changed = 1;
tree.searchFogCache[sl] = cur;
any_on |= cur;
}
if (tree.searchIsCockpit && changed)
{
SetFogStyle(any_on ? searchLightOnFogStyle : searchLightOffFogStyle);
if (getenv("BT_FIRE_LOG") || getenv("BT_FOG_LOG"))
DEBUG_STREAM << "[spot] cockpit fog -> "
<< (any_on ? "searchLightOn" : "searchLightOff") << "\n" << std::flush;
}
}
//
// Sim-side bridge (per-mech, every frame, from Mech::PerformAndWatch).
//
@@ -1557,6 +1723,7 @@ void BTArmourDamageTick(Entity *mech)
if (renderer == NULL)
return;
renderer->TickArmourDamage(mech);
renderer->TickSearchlight(mech); // the searchlight watchers ride the same beat
}