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:
co-authored by
Claude Fable 5
parent
b31c6c4527
commit
412053d5af
@@ -4361,6 +4361,12 @@ void
|
|||||||
fogFar = searchLightFogFar;
|
fogFar = searchLightFogFar;
|
||||||
if(fogUpdating)
|
if(fogUpdating)
|
||||||
{
|
{
|
||||||
|
// BT (searchlight, 2026-08-05): the stubbed dpl_SetViewFog below also
|
||||||
|
// carried the PLANES; the per-frame FOGSTART/FOGEND application reads
|
||||||
|
// currentFogNear/Far, so the swap must land there too (the color line
|
||||||
|
// alone left the fog DISTANCES stuck at load values).
|
||||||
|
currentFogNear = fogNear;
|
||||||
|
currentFogFar = fogFar;
|
||||||
mDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen), (int)(255 * fogBlue)));
|
mDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen), (int)(255 * fogBlue)));
|
||||||
// dpl_SetViewFog(
|
// dpl_SetViewFog(
|
||||||
// dplMainView,
|
// dplMainView,
|
||||||
@@ -4381,6 +4387,8 @@ void
|
|||||||
fogFar = noSearchLightFogFar;
|
fogFar = noSearchLightFogFar;
|
||||||
if(fogUpdating)
|
if(fogUpdating)
|
||||||
{
|
{
|
||||||
|
currentFogNear = fogNear; // see the On case
|
||||||
|
currentFogFar = fogFar;
|
||||||
mDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen), (int)(255 * fogBlue)));
|
mDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen), (int)(255 * fogBlue)));
|
||||||
// dpl_SetViewFog(
|
// dpl_SetViewFog(
|
||||||
// dplMainView,
|
// dplMainView,
|
||||||
|
|||||||
@@ -170,6 +170,99 @@ void
|
|||||||
// localToWorld in its ctor -- no explicit AddDynamicRenderable here
|
// localToWorld in its ctor -- no explicit AddDynamicRenderable here
|
||||||
// (unlike the 1996 VideoComponent path).
|
// (unlike the 1996 VideoComponent path).
|
||||||
(void)mech_root;
|
(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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +585,22 @@ HierarchicalDrawComponent*
|
|||||||
mCamera = mEyeCockpit;
|
mCamera = mEyeCockpit;
|
||||||
dbg_eye = 1;
|
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;
|
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).
|
// Sim-side bridge (per-mech, every frame, from Mech::PerformAndWatch).
|
||||||
//
|
//
|
||||||
@@ -1557,6 +1723,7 @@ void BTArmourDamageTick(Entity *mech)
|
|||||||
if (renderer == NULL)
|
if (renderer == NULL)
|
||||||
return;
|
return;
|
||||||
renderer->TickArmourDamage(mech);
|
renderer->TickArmourDamage(mech);
|
||||||
|
renderer->TickSearchlight(mech); // the searchlight watchers ride the same beat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -721,6 +721,24 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
|||||||
struct DmgBind { d3d_OBJECT *obj; int op; int zone; };
|
struct DmgBind { d3d_OBJECT *obj; int op; int zone; };
|
||||||
std::vector<DmgBind> dmgBinds;
|
std::vector<DmgBind> dmgBinds;
|
||||||
std::map<int, int> segGState; // slot -> last applied graphic state
|
std::map<int, int> segGState; // slot -> last applied graphic state
|
||||||
|
// SEARCHLIGHT (2026-08-05, MakeMechRenderables case 0xbd8 @004cef28):
|
||||||
|
// external view = a spot.bgf cone child on the mount joint, shown/
|
||||||
|
// hidden from the subsystem's "LightOn" attribute (the @0045612c
|
||||||
|
// watcher, transcribed into TickSearchlight); cockpit view = the fog
|
||||||
|
// swap (@00456778/@00456814 -> SetFogStyle searchLightOn/Off).
|
||||||
|
struct SearchLight
|
||||||
|
{
|
||||||
|
DPLStaticChildRenderable *cone; // external only; NULL on cockpit builds
|
||||||
|
d3d_OBJECT *coneObj; // spot.bgf (held while hidden)
|
||||||
|
int *lightOn; // the subsystem's LightOn attribute
|
||||||
|
int shown; // cone visibility last applied
|
||||||
|
int mountSeg; // resource segmentIndex (subsys +0x1DC)
|
||||||
|
};
|
||||||
|
SearchLight searchLight[2]; // the binary keeps 2 slots
|
||||||
|
int searchLightCount;
|
||||||
|
int searchIsCockpit; // built with view_type 1
|
||||||
|
int searchFogCache[2]; // seeded INVERTED (@00456778
|
||||||
|
// [9]/[10]) -> first tick syncs
|
||||||
char paintSerno; // the %serno% this mech was BUILT with
|
char paintSerno; // the %serno% this mech was BUILT with
|
||||||
// (0 = none) -- ApplyViewSkeleton re-parses
|
// (0 = none) -- ApplyViewSkeleton re-parses
|
||||||
// segment BGFs, so the paint substitutions
|
// segment BGFs, so the paint substitutions
|
||||||
@@ -740,6 +758,15 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
|||||||
int
|
int
|
||||||
TickWreck(Entity *victim, float dt);
|
TickWreck(Entity *victim, float dt);
|
||||||
|
|
||||||
|
//
|
||||||
|
// SEARCHLIGHT per-frame drive (2026-08-05): the transcription of the two
|
||||||
|
// 1995 watcher renderables -- cone visibility from "LightOn" (@0045612c
|
||||||
|
// Execute @004561d8) and the cockpit fog swap (@00456778 Execute
|
||||||
|
// @00456814). Rides the same sim bridge as TickArmourDamage.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
TickSearchlight(Entity *mech);
|
||||||
|
|
||||||
//
|
//
|
||||||
// ARMOUR DARKENING (issue #87). BindArmourDamage resolves the mech's
|
// ARMOUR DARKENING (issue #87). BindArmourDamage resolves the mech's
|
||||||
// .DZM zone->material lists (DamageZone::GetMaterialList, already parsed
|
// .DZM zone->material lists (DamageZone::GetMaterialList, already parsed
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Searchlight::SharedData
|
|||||||
//
|
//
|
||||||
// Chains to the PowerWatcher ctor (FUN_004b18a4) with &DAT_00511198, installs
|
// Chains to the PowerWatcher ctor (FUN_004b18a4) with &DAT_00511198, installs
|
||||||
// the Searchlight vtable (PTR_FUN_005114d4), builds the 2-level state
|
// the Searchlight vtable (PTR_FUN_005114d4), builds the 2-level state
|
||||||
// AlarmIndicator (@0x1E4) and seeds commandedOn from the segment field
|
// AlarmIndicator (@0x1E4) and seeds mountSegment from the segment field
|
||||||
// (+0x28). When this is a live (non-copy) segment with the has-performance
|
// (+0x28). When this is a live (non-copy) segment with the has-performance
|
||||||
// flag set it registers SearchlightSimulation (this[7..9] <- PTR_FUN_00511200);
|
// flag set it registers SearchlightSimulation (this[7..9] <- PTR_FUN_00511200);
|
||||||
// otherwise it marks the instance "no per-frame performance" (flag bit 2).
|
// otherwise it marks the instance "no per-frame performance" (flag bit 2).
|
||||||
@@ -103,7 +103,7 @@ Searchlight::Searchlight(
|
|||||||
lightState = 0; // @0x1D8
|
lightState = 0; // @0x1D8
|
||||||
requestedOn = 0; // @0x1E0
|
requestedOn = 0; // @0x1E0
|
||||||
stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E4, 2)
|
stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E4, 2)
|
||||||
commandedOn = subsystem_resource->segmentIndex; // @0x1DC <- inherited resource +0x28
|
mountSegment = subsystem_resource->segmentIndex; // @0x1DC <- inherited resource +0x28 (the spot-cone mount joint)
|
||||||
|
|
||||||
// GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER
|
// GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER
|
||||||
// simulationFlags (param_2+0x28, raw part_013.c:6012), NOT the old shadow
|
// simulationFlags (param_2+0x28, raw part_013.c:6012), NOT the old shadow
|
||||||
@@ -311,8 +311,8 @@ void
|
|||||||
if (!announced)
|
if (!announced)
|
||||||
{
|
{
|
||||||
announced = 1;
|
announced = 1;
|
||||||
DEBUG_STREAM << "[light] SearchlightSimulation RUNNING (commandedOn "
|
DEBUG_STREAM << "[light] SearchlightSimulation RUNNING (mountSegment "
|
||||||
<< commandedOn << ", requestedOn " << requestedOn
|
<< mountSegment << ", requestedOn " << requestedOn
|
||||||
<< ", lightState " << lightState << ")" << std::endl;
|
<< ", lightState " << lightState << ")" << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,7 +323,7 @@ void
|
|||||||
|
|
||||||
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
||||||
DEBUG_STREAM << "[light] reported lightState " << previous << " -> " << lightState
|
DEBUG_STREAM << "[light] reported lightState " << previous << " -> " << lightState
|
||||||
<< " (commandedOn " << commandedOn << ", requestedOn " << requestedOn << ")"
|
<< " (mountSegment " << mountSegment << ", requestedOn " << requestedOn << ")"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ int
|
|||||||
struct SearchlightLayoutCheck
|
struct SearchlightLayoutCheck
|
||||||
{
|
{
|
||||||
static_assert(offsetof(Searchlight, lightState) == 0x1D8, "Searchlight lightState @0x1D8 (PowerWatcher ends 0x1D8)");
|
static_assert(offsetof(Searchlight, lightState) == 0x1D8, "Searchlight lightState @0x1D8 (PowerWatcher ends 0x1D8)");
|
||||||
static_assert(offsetof(Searchlight, commandedOn) == 0x1DC, "Searchlight commandedOn @0x1DC (res+0x28)");
|
static_assert(offsetof(Searchlight, mountSegment) == 0x1DC, "Searchlight mountSegment @0x1DC (res+0x28 segmentIndex)");
|
||||||
static_assert(offsetof(Searchlight, requestedOn) == 0x1E0, "Searchlight requestedOn @0x1E0");
|
static_assert(offsetof(Searchlight, requestedOn) == 0x1E0, "Searchlight requestedOn @0x1E0");
|
||||||
static_assert(offsetof(Searchlight, stateAlarm) == 0x1E4, "Searchlight stateAlarm @0x1E4 (proves base ends 0x1D8)");
|
static_assert(offsetof(Searchlight, stateAlarm) == 0x1E4, "Searchlight stateAlarm @0x1E4 (proves base ends 0x1D8)");
|
||||||
static_assert(sizeof(Searchlight) <= 0x238, "sizeof(Searchlight) must fit the factory Memory::Allocate(0x238)");
|
static_assert(sizeof(Searchlight) <= 0x238, "sizeof(Searchlight) must fit the factory Memory::Allocate(0x238)");
|
||||||
@@ -423,3 +423,39 @@ Subsystem *CreateSearchlightSubsystem(Mech *owner, int id, void *seg)
|
|||||||
return (Subsystem *) new (Memory::Allocate(0x238))
|
return (Subsystem *) new (Memory::Allocate(0x238))
|
||||||
Searchlight(owner, id, (Searchlight::SubsystemResource *)seg, Searchlight::DefaultData);
|
Searchlight(owner, id, (Searchlight::SubsystemResource *)seg, Searchlight::DefaultData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===========================================================================//
|
||||||
|
// Video-layer probe (2026-08-05, the spot-cone/fog reconstruction). The 1995
|
||||||
|
// MakeMechRenderables walk (@004cef28) hits `case 0xbd8` per roster subsystem
|
||||||
|
// and pulls the "LightOn" attribute + the mount joint; our port's tree builder
|
||||||
|
// does not walk the roster, so btl4vid calls this bridge instead (complete-
|
||||||
|
// Searchlight TU, per the databinding rule). Returns the number of
|
||||||
|
// searchlights found (0..2 reported -- the binary's cockpit branch keeps TWO
|
||||||
|
// attr slots, @004cef28 local_118/local_11c; more than 2 exist on no chassis).
|
||||||
|
//===========================================================================//
|
||||||
|
int BTMechSearchlightProbe(
|
||||||
|
void *mech_v,
|
||||||
|
int **light_on_out, // [2] "LightOn" attribute pointers (int*)
|
||||||
|
int *mount_seg_out) // [2] mount segment slots (resource segmentIndex)
|
||||||
|
{
|
||||||
|
Mech *mech = (Mech *)mech_v;
|
||||||
|
if (mech == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int found = 0;
|
||||||
|
for (int i = 0; i < mech->GetSubsystemCount() && found < 2; ++i)
|
||||||
|
{
|
||||||
|
Subsystem *sub = mech->GetSubsystem(i);
|
||||||
|
if (sub == 0
|
||||||
|
|| (int)sub->GetClassID() != (int)RegisteredClass::SearchlightClassID) // 0xBD8
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Searchlight *light = (Searchlight *)sub;
|
||||||
|
light_on_out[found] = light->LightOnPointer(); // == GetAttributePointer("LightOn")
|
||||||
|
mount_seg_out[found] = light->MountSegmentIndex(); // @0x1DC (res+0x28 segmentIndex)
|
||||||
|
++found;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,13 +67,13 @@
|
|||||||
//
|
//
|
||||||
// Empty extension of the PowerWatcher resource (CreateStreamedSubsystem
|
// Empty extension of the PowerWatcher resource (CreateStreamedSubsystem
|
||||||
// @004b85a8 reads no light-specific fields; it only stamps classID/model
|
// @004b85a8 reads no light-specific fields; it only stamps classID/model
|
||||||
// size). The ctor seeds commandedOn from the inherited segment field (+0x28).
|
// size). The ctor seeds mountSegment from the inherited segment field (+0x28).
|
||||||
//
|
//
|
||||||
struct Searchlight__SubsystemResource:
|
struct Searchlight__SubsystemResource:
|
||||||
public PowerWatcher::SubsystemResource
|
public PowerWatcher::SubsystemResource
|
||||||
{
|
{
|
||||||
// RESOURCE_AUDIT.md: Searchlight adds NO resource field. The ctor seeds
|
// RESOURCE_AUDIT.md: Searchlight adds NO resource field. The ctor seeds
|
||||||
// commandedOn from the INHERITED segmentIndex (resource +0x28); the old
|
// mountSegment from the INHERITED segmentIndex (resource +0x28); the old
|
||||||
// `int segmentDefault;` was appended after the base (compiled at ~0xF4, OOB)
|
// `int segmentDefault;` was appended after the base (compiled at ~0xF4, OOB)
|
||||||
// and read garbage. Empty extension = correct 0xF4 model size.
|
// and read garbage. Empty extension = correct 0xF4 model size.
|
||||||
};
|
};
|
||||||
@@ -121,13 +121,20 @@
|
|||||||
//
|
//
|
||||||
protected:
|
protected:
|
||||||
int lightState; // @0x1D8 reported on/off (gated by power + heat each frame); "LightState"
|
int lightState; // @0x1D8 reported on/off (gated by power + heat each frame); "LightState"
|
||||||
int commandedOn; // @0x1DC ctor-seeded from the subsystem resource +0x28 (@004b84dc).
|
int mountSegment; // @0x1DC ctor-seeded from the subsystem resource +0x28 (@004b84dc).
|
||||||
// NOT the toggle target and NOT read by @004b841c; it measured
|
// IDENTIFIED 2026-08-05 (was "commandedOn -- role still
|
||||||
// 21 live, so it is not a boolean -- role still unidentified.
|
// unidentified"): resource+0x28 IS segmentIndex, and the live
|
||||||
|
// value 21 is the searchlight's mount SEGMENT slot -- the joint
|
||||||
|
// the spot-cone renderable hangs from (MakeMechRenderables
|
||||||
|
// @004cef28 case 0xbd8). Not a boolean, not the toggle target.
|
||||||
int requestedOn; // @0x1E0 init 0; THE on/off input consumed by SearchlightSimulation
|
int requestedOn; // @0x1E0 init 0; THE on/off input consumed by SearchlightSimulation
|
||||||
// (@004b841c reads +0x1E0) and toggled by ToggleLamp @004b838c
|
// (@004b841c reads +0x1E0) and toggled by ToggleLamp @004b838c
|
||||||
BtAlarm stateAlarm; // @0x1E4 2-level light indicator (size 0x54)
|
BtAlarm stateAlarm; // @0x1E4 2-level light indicator (size 0x54)
|
||||||
|
|
||||||
|
public:
|
||||||
|
int MountSegmentIndex() const { return mountSegment; } // @0x1DC (see above)
|
||||||
|
int *LightOnPointer() { return &lightState; } // the "LightOn" attribute target
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// BT segment / base-state compatibility shims (CROSS-FAMILY).
|
// BT segment / base-state compatibility shims (CROSS-FAMILY).
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# SEARCHLIGHT bench 1 (cockpit fog swap): solo night run; BT_BTNTEST presses
|
||||||
|
# the searchlight button (0x14 = F5's binding) at poll 600, releases 700.
|
||||||
|
# PASS (log): "[spot] searchlight pass: N light(s) ... (cockpit: fog watcher)";
|
||||||
|
# first tick "[spot] cockpit fog -> searchLightOff" (the inverted-seed sync ->
|
||||||
|
# authentic DARK night start on nosearchlightfog=5..400); after the press
|
||||||
|
# "[light] ... requested ON", "[fogstyle] SetFogStyle(2)", "[spot] cockpit fog
|
||||||
|
# -> searchLightOn". BT_SHOT frames: post-toggle frames measurably brighter
|
||||||
|
# (fog near 5 -> 40 on arena_night-family pages).
|
||||||
|
set -e
|
||||||
|
cd /c/git/bt411/content
|
||||||
|
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||||
|
taskkill //F //IM btl4.exe > /dev/null 2>&1 || true; sleep 2
|
||||||
|
rm -f sfog_*.png sfog.log
|
||||||
|
sed "s/^map=.*/map=grass/; s/^time=.*/time=night/; 0,/^vehicle=.*/s//vehicle=madcat/" MP.EGG > SG2.EGG
|
||||||
|
|
||||||
|
(
|
||||||
|
export BT_START_INSIDE=1 BT_SHOT_EVERY=60 BT_SHOT_PREFIX=sfog
|
||||||
|
export BT_BTNTEST=0x14,600,700
|
||||||
|
export BT_FIRE_LOG=1 BT_FOG_LOG=1 BT_DEV_GAUGES=1
|
||||||
|
bt_launch "sfog.log" SG2.EGG 0x03
|
||||||
|
)
|
||||||
|
sleep 75
|
||||||
|
taskkill //F //IM btl4.exe > /dev/null 2>&1 || true; sleep 3
|
||||||
|
|
||||||
|
echo "=== searchlight pass + fog receipts:"
|
||||||
|
grep -aE "\[spot\]|\[fogstyle\]|\[light\]|btntest" sfog.log | head -16
|
||||||
|
echo "=== fog page resolve:"
|
||||||
|
grep -a "\[fog\] resolved" sfog.log | head -2
|
||||||
|
echo "=== frames:"
|
||||||
|
ls sfog_*.png 2>/dev/null | head -20
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SEARCHLIGHT bench 2 (external spot cone + replication): night 2-node.
|
||||||
|
# A walks to 40u of B and stands (facing B). B presses the searchlight button
|
||||||
|
# (BT_BTNTEST 0x14) at poll 300. B's lightState replicates (subsystem record
|
||||||
|
# 0x14) -> A's replicant Searchlight LightOn flips -> A's TickSearchlight shows
|
||||||
|
# the spot.bgf cone on B's mech.
|
||||||
|
# PASS: A log "[spot] searchlight pass: ... (external: spot cone)" for B's mech
|
||||||
|
# + "[spot] cone SHOWN (seg N)" after B's press; A BT_SHOT frames brighten near
|
||||||
|
# B post-toggle. B log: "[light] ... requested ON" + lightState 0 -> 1.
|
||||||
|
set -x
|
||||||
|
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||||
|
cd /c/git/bt411/content || exit 1
|
||||||
|
bt_assert_player_env
|
||||||
|
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
rm -f cone_a.log cone_b.log cone_relay.log coneA_*.png
|
||||||
|
bt_expert_egg MP.EGG SG3.EGG
|
||||||
|
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=night/; s/^vehicle=.*/vehicle=madcat/" SG3.EGG
|
||||||
|
|
||||||
|
(
|
||||||
|
export BT_BTNTEST=0x14,900,960
|
||||||
|
export BT_FIRE_LOG=1 BT_FOG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1
|
||||||
|
bt_launch cone_b.log SG3.EGG 0x0C -net 1601
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
(
|
||||||
|
export BT_GOTO=enemy BT_GOTO_STOP=40 BT_KEY_NOFOCUS=1
|
||||||
|
export BT_SHOT_EVERY=60 BT_SHOT_PREFIX=coneA
|
||||||
|
export BT_FIRE_LOG=1 BT_FOG_LOG=1 BT_MP_LOG=1
|
||||||
|
bt_launch cone_a.log SG3.EGG 0x03 -net 1501
|
||||||
|
)
|
||||||
|
sleep 5
|
||||||
|
python ../tools/btconsole.py SG3.EGG 127.0.0.1:1501 127.0.0.1:1601 > cone_relay.log 2>&1 &
|
||||||
|
RELAY=$!
|
||||||
|
sleep 90
|
||||||
|
kill $RELAY 2>/dev/null
|
||||||
|
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||||
|
sleep 3
|
||||||
|
echo "=== B (lamp owner):"
|
||||||
|
grep -aE "\[light\]|btntest|\[spot\]" cone_b.log | head -8
|
||||||
|
echo "=== A (observer -- cone events):"
|
||||||
|
grep -aE "\[spot\]" cone_a.log | head -8
|
||||||
Reference in New Issue
Block a user