From 412053d5af79d72882ddaed859909f5af81b4ea4 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 5 Aug 2026 19:44:23 -0500 Subject: [PATCH] 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 --- engine/MUNGA_L4/L4VIDEO.cpp | 8 ++ game/reconstructed/btl4vid.cpp | 167 +++++++++++++++++++++++++++++ game/reconstructed/btl4vid.hpp | 27 +++++ game/reconstructed/searchlight.cpp | 48 +++++++-- game/reconstructed/searchlight.hpp | 17 ++- scratchpad/night12/searchfog.sh | 31 ++++++ scratchpad/night12/spotcone.sh | 42 ++++++++ 7 files changed, 329 insertions(+), 11 deletions(-) create mode 100644 scratchpad/night12/searchfog.sh create mode 100644 scratchpad/night12/spotcone.sh diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 8461311..b02d778 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -4361,6 +4361,12 @@ void fogFar = searchLightFogFar; 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))); // dpl_SetViewFog( // dplMainView, @@ -4381,6 +4387,8 @@ void fogFar = noSearchLightFogFar; 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))); // dpl_SetViewFog( // dplMainView, diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index f23e4fb..78fcf62 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -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::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::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::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::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 } diff --git a/game/reconstructed/btl4vid.hpp b/game/reconstructed/btl4vid.hpp index 243fcdb..e1f0dd3 100644 --- a/game/reconstructed/btl4vid.hpp +++ b/game/reconstructed/btl4vid.hpp @@ -721,6 +721,24 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device); struct DmgBind { d3d_OBJECT *obj; int op; int zone; }; std::vector dmgBinds; std::map 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 // (0 = none) -- ApplyViewSkeleton re-parses // segment BGFs, so the paint substitutions @@ -740,6 +758,15 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device); int 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 // .DZM zone->material lists (DamageZone::GetMaterialList, already parsed diff --git a/game/reconstructed/searchlight.cpp b/game/reconstructed/searchlight.cpp index 17ebd07..c43d724 100644 --- a/game/reconstructed/searchlight.cpp +++ b/game/reconstructed/searchlight.cpp @@ -82,7 +82,7 @@ Searchlight::SharedData // // Chains to the PowerWatcher ctor (FUN_004b18a4) with &DAT_00511198, installs // 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 // flag set it registers SearchlightSimulation (this[7..9] <- PTR_FUN_00511200); // otherwise it marks the instance "no per-frame performance" (flag bit 2). @@ -103,7 +103,7 @@ Searchlight::Searchlight( lightState = 0; // @0x1D8 requestedOn = 0; // @0x1E0 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 // simulationFlags (param_2+0x28, raw part_013.c:6012), NOT the old shadow @@ -311,8 +311,8 @@ void if (!announced) { announced = 1; - DEBUG_STREAM << "[light] SearchlightSimulation RUNNING (commandedOn " - << commandedOn << ", requestedOn " << requestedOn + DEBUG_STREAM << "[light] SearchlightSimulation RUNNING (mountSegment " + << mountSegment << ", requestedOn " << requestedOn << ", lightState " << lightState << ")" << std::endl; } } @@ -323,7 +323,7 @@ void if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG")) DEBUG_STREAM << "[light] reported lightState " << previous << " -> " << lightState - << " (commandedOn " << commandedOn << ", requestedOn " << requestedOn << ")" + << " (mountSegment " << mountSegment << ", requestedOn " << requestedOn << ")" << std::endl; } @@ -405,7 +405,7 @@ int struct SearchlightLayoutCheck { 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, stateAlarm) == 0x1E4, "Searchlight stateAlarm @0x1E4 (proves base ends 0x1D8)"); 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)) 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; +} diff --git a/game/reconstructed/searchlight.hpp b/game/reconstructed/searchlight.hpp index e87e77e..93b8a43 100644 --- a/game/reconstructed/searchlight.hpp +++ b/game/reconstructed/searchlight.hpp @@ -67,13 +67,13 @@ // // Empty extension of the PowerWatcher resource (CreateStreamedSubsystem // @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: public PowerWatcher::SubsystemResource { // 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) // and read garbage. Empty extension = correct 0xF4 model size. }; @@ -121,13 +121,20 @@ // protected: int lightState; // @0x1D8 reported on/off (gated by power + heat each frame); "LightState" - int commandedOn; // @0x1DC ctor-seeded from the subsystem resource +0x28 (@004b84dc). - // NOT the toggle target and NOT read by @004b841c; it measured - // 21 live, so it is not a boolean -- role still unidentified. + int mountSegment; // @0x1DC ctor-seeded from the subsystem resource +0x28 (@004b84dc). + // IDENTIFIED 2026-08-05 (was "commandedOn -- role still + // 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 // (@004b841c reads +0x1E0) and toggled by ToggleLamp @004b838c 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). // diff --git a/scratchpad/night12/searchfog.sh b/scratchpad/night12/searchfog.sh new file mode 100644 index 0000000..187159c --- /dev/null +++ b/scratchpad/night12/searchfog.sh @@ -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 diff --git a/scratchpad/night12/spotcone.sh b/scratchpad/night12/spotcone.sh new file mode 100644 index 0000000..242bd6e --- /dev/null +++ b/scratchpad/night12/spotcone.sh @@ -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