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
+8
View File
@@ -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,
+167
View File
@@ -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
} }
+27
View File
@@ -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
+42 -6
View File
@@ -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;
}
+12 -5
View File
@@ -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).
// //
+31
View File
@@ -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
+42
View File
@@ -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