wreck smoke dies WITH the wreck; the audio/effect clock is calibrated to the byte-proven 28
USER REPORT: "when you destroy a mech, it continues to smoke for a very long
time after the mech disappears... up to 30secs to a minute."
MEASURED (new [pfx] timed census with per-def particle attribution): the
death plume (psfx 1 DDTHSMK) was re-armed on a 10s cadence as a WORLD effect
(ownerTag=0) -- so neither the burial transition nor the respawn cleanup
(StopAllEntityEffects) could touch it. The last-armed window kept emitting
over the empty spot for up to 10s after the hulk vanished, plus 6+-2s authored
particle lives, and back-to-back windows stretched the visible tail to the
reported range. The re-arm itself is a marked PORT ADDITION [T3]; the binary
fired DDTHSMK once -- its 10s window + particle lives ended right around the
~17s hulk sink. The authored design: the smoke dies WITH the burial.
FIX: the plume is spawned ATTACHED (BTStartPfxAttached, tagged to the dying
entity -- it also rides the sinking hulk now), and the burial transition calls
BTStopEntityPfx alongside the respawn cleanup that already did. Verified over
a 175s run containing FOUR wreck events (the enemy kill + three player bay-
fire deaths): every plume's emission ends at its wreck's burial or respawn,
particles fade within ~8-11s, no orphan emitters remain.
THE COUPLING (user relay of Oracle: "prolonged smoke and explosion INCLUDING
SOUND... same lever?") -- CONFIRMED as a family: the binary's audio layer
times in FRAME COUNTS converted by the engine frame-rate global:
AudioTime::Seconds_To_Frames == fmul [0x52140c]=28.0; fadd 0.5; round
(@0x42c611, @0x42dd86 -- the 0x42xxxx cluster of the ~90 global consumers)
Our engine's DefaultRendererRate said 30: every audio duration, sequence
delay and compression window ran ~7% fast. Calibrated to 28 [T1]. NOT yet
explained by this: a truly SUSTAINED/looping explosion sound (7% is not
"prolonged") -- the effect stop-path trace stays open on #114/#51.
Tooling: BTPfxParticle carries defIndex; [pfx] census (BT_PFX_LOG, 2s) prints
emitters + particles PER EFFECT SLOT with a timestamp; wrecksmoke.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2df1369168
commit
6dbeb3d266
@@ -560,6 +560,15 @@ and passed straight into the ApplicationManager ctor by the DOS main
|
||||
whenever the decomp shows an unexplained multiply/divide by `_DAT_0052140c`,
|
||||
it is per-second <-> per-frame conversion at the pod's 28 Hz.
|
||||
|
||||
**The AUDIO/EFFECT FRAME CLOCK is this same rate.** `AudioTime::
|
||||
Seconds_To_Frames` in the image is `fmul [0x52140c]; fadd 0.5; round` (e.g.
|
||||
@0x42c611, @0x42dd86 — the 0x42xxxx audio-layer cluster of the ~90 consumers),
|
||||
so every authored audio duration/delay counts frames at 28. WinTesla's
|
||||
`DefaultRendererRate` said **30** — all audio timing ran ~7% fast until
|
||||
corrected (2026-08-02). Oracle's coupled report ("prolonged smoke and
|
||||
explosion including sound") was the lead: effect visuals and audio share this
|
||||
clock family.
|
||||
|
||||
**So the pod's nominal simulation cadence was 28 fps, not the i860 board's
|
||||
30 Hz — and it was UNLOCKED.** Corroborated testimony (2026-08-02): Oracle —
|
||||
the pod rate was "flipbook" territory, heavy fights "a slide show"; Lynx
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
//############################ Renderer #################################
|
||||
//#############################################################################
|
||||
|
||||
const RendererRate DefaultRendererRate = 30.0f;
|
||||
// 2026-08-02 (#96 clock trace): the DOS binary's audio/effect frame clock runs
|
||||
// at the ENGINE FRAME RATE GLOBAL DAT_0052140c = 28.0f (0x401ace), not 30 --
|
||||
// AudioTime::Seconds_To_Frames in the image is `fmul [0x52140c]; fadd 0.5`
|
||||
// (e.g. @0x42c611), and every authored frame-count duration assumes 28.
|
||||
// 30 here ran ALL audio timing ~7% fast. [T1]
|
||||
const RendererRate DefaultRendererRate = 28.0f;
|
||||
const RendererComplexity MaxRendererComplexity = 3.0f;
|
||||
const RendererComplexity MinRendererComplexity = 0.0f;
|
||||
const RendererFrameBudget DefaultRendererFrameBudget = 0.14f; //0.12f; // 0.16f;
|
||||
|
||||
@@ -745,6 +745,7 @@ struct BTPfxEmitter
|
||||
struct BTPfxParticle
|
||||
{
|
||||
D3DXVECTOR3 pos, vel, accel;
|
||||
int defIndex; // census attribution (#114 / wreck-smoke tail)
|
||||
float age, life;
|
||||
float rad, exp, dexp;
|
||||
float colorWarp, alphaWarp; // def-level warps, carried per particle
|
||||
@@ -875,6 +876,39 @@ void BTStartPfxFrame(int effect_number, float x, float y, float z,
|
||||
gBTPfxEmitters.push_back(e);
|
||||
}
|
||||
|
||||
// #114/wreck-smoke census (BT_PFX_LOG): every emitter start + a 2s population
|
||||
// line, so "what is still smoking 40s after the kill" is a log fact.
|
||||
static void BTPfxCensus()
|
||||
{
|
||||
if (!getenv("BT_PFX_LOG")) return;
|
||||
static unsigned long s_at = 0;
|
||||
unsigned long now = GetTickCount();
|
||||
if (now - s_at < 2000) return;
|
||||
s_at = now;
|
||||
int perDef[BT_PFX_SLOTS]; memset(perDef, 0, sizeof(perDef));
|
||||
int emitters = 0;
|
||||
for (size_t i = 0; i < gBTPfxEmitters.size(); ++i)
|
||||
if (gBTPfxEmitters[i].active)
|
||||
{
|
||||
++emitters;
|
||||
int di = (int)(gBTPfxEmitters[i].def - gBTPfxDefs);
|
||||
if (di >= 0 && di < BT_PFX_SLOTS) ++perDef[di];
|
||||
}
|
||||
int partDef[BT_PFX_SLOTS]; memset(partDef, 0, sizeof(partDef));
|
||||
for (size_t i = 0; i < gBTPfxParticles.size(); ++i)
|
||||
{
|
||||
int di = gBTPfxParticles[i].defIndex;
|
||||
if (di >= 0 && di < BT_PFX_SLOTS) ++partDef[di];
|
||||
}
|
||||
DEBUG_STREAM << "[pfx] t=" << (now / 1000) << "s census: emitters=" << emitters
|
||||
<< " particles=" << (int)gBTPfxParticles.size();
|
||||
for (int d = 0; d < BT_PFX_SLOTS; ++d)
|
||||
if (perDef[d] > 0) DEBUG_STREAM << " {E" << d << ":" << perDef[d] << "}";
|
||||
for (int d = 0; d < BT_PFX_SLOTS; ++d)
|
||||
if (partDef[d] > 0) DEBUG_STREAM << " {p" << d << ":" << partDef[d] << "}";
|
||||
DEBUG_STREAM << std::endl;
|
||||
}
|
||||
|
||||
void BTStartPfx(int effect_number, float x, float y, float z)
|
||||
{
|
||||
BTStartPfxFrame(effect_number, x, y, z, 0, 0, 0);
|
||||
@@ -964,6 +998,7 @@ static void BTPfxSpawn(const BTPfxEmitter &e)
|
||||
// laser bursts during missile volleys)
|
||||
return;
|
||||
BTPfxParticle p;
|
||||
p.defIndex = (int)(e.def - gBTPfxDefs); // census attribution
|
||||
// Sample in the effect's LOCAL frame, then orient into the world through
|
||||
// the emitter's basis (the victim's localToWorld rows). The position
|
||||
// jitter pv is an isotropic scatter about the base offset -> symmetric
|
||||
@@ -1181,6 +1216,7 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
||||
}
|
||||
}
|
||||
}
|
||||
BTPfxCensus();
|
||||
for (size_t ei = 0; ei < gBTPfxEmitters.size(); ++ei)
|
||||
{
|
||||
BTPfxEmitter &e = gBTPfxEmitters[ei];
|
||||
|
||||
@@ -2204,6 +2204,18 @@ void
|
||||
// the empty spot).
|
||||
if (!wreck_visible && collisionVolumeCount != 0)
|
||||
{
|
||||
// #114-family (user report: smoke continues 30-60s after the mech
|
||||
// disappears): the re-armed plume was a WORLD effect (ownerTag=0),
|
||||
// so the respawn/burial cleanups could not kill it -- the last 10s
|
||||
// emission window played out over an empty spot, plus 6+-2s
|
||||
// particle lives. The plume is entity-tagged now (below), so end
|
||||
// the emitters WITH the burial; spawned particles fade in <=8s,
|
||||
// which is the authored envelope (DDTHSMK once: window 10s,
|
||||
// life 6+-2 -- the binary's smoke died WITH the sink).
|
||||
{
|
||||
extern void BTStopEntityPfx(void *owner);
|
||||
BTStopEntityPfx((void *)this);
|
||||
}
|
||||
// Zeroing the count stops MoveCollisionVolume from re-placing the
|
||||
// box -- but the mech-vs-mech gather (Mover::GetCurrentCollisions)
|
||||
// tests mover->collisionVolume DIRECTLY, never the count, so the
|
||||
@@ -2225,11 +2237,14 @@ void
|
||||
if (wreck_visible && wreckSmokeTimer <= 0.0f)
|
||||
{
|
||||
wreckSmokeTimer = 10.0f; // DDTHSMK's release window
|
||||
extern void BTStartPfx(int effect_number, float x, float y, float z);
|
||||
BTStartPfx(1,
|
||||
// ATTACHED (not world): tagged to this entity so the burial /
|
||||
// respawn cleanups can end it, and it rides the sinking hulk.
|
||||
extern void BTStartPfxAttached(int effect_number, void *owner,
|
||||
int seg_index, float x, float y, float z, const float *rows9);
|
||||
BTStartPfxAttached(1, (void *)this, 0,
|
||||
(float)localOrigin.linearPosition.x,
|
||||
(float)(localOrigin.linearPosition.y + kMuzzleHeight),
|
||||
(float)localOrigin.linearPosition.z);
|
||||
(float)localOrigin.linearPosition.z, 0);
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[death] wreck smoke plume re-armed (psfx 1)\n" << std::flush;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wreck-smoke tail: kill the enemy, then WATCH 90s after the kill.
|
||||
# Reads: [pfx] census every 2s (emitters by psfx slot + particle count),
|
||||
# [death] wreck re-arm lines, BT_SHOT frames for the visual tail.
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > WS.EGG
|
||||
rm -f wrecksmoke.log smoke_*.png
|
||||
export BT_SPAWN_ENEMY=1 BT_GOTO=enemy BT_GOTO_STOP=60
|
||||
export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=3
|
||||
export BT_PFX_LOG=1 BT_DEATH_LOG=1 BT_DMG_LOG=1
|
||||
export BT_SHOT_EVERY=60 BT_SHOT_PREFIX=smoke
|
||||
bt_launch wrecksmoke.log WS.EGG 0x03
|
||||
sleep 190
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
Reference in New Issue
Block a user