diff --git a/docs/dwarf-arts.md b/docs/dwarf-arts.md index 2e00766..10f8741 100644 --- a/docs/dwarf-arts.md +++ b/docs/dwarf-arts.md @@ -76,3 +76,14 @@ a game::Art tooltip filter adds "Explosionsradius: N" after the last value line of each level section (current level, and the next level's in the skill book), in that line's colour, from the same `ImpactRadiusAt(level)` as the hit check and the marker. + +## Grenade impact crash (2026-10-26) + +Crashes at sacred.exe+0x361b54 (2026-10-26 00:12 and 22:39) in the particle +drawer `0x7613e0`, called by `cParticleSystem_grenadeimpact::Render` (vf04 +0x7b66b0): colour = table +0x20d0 at row (+0x3d) * 256 + life (+0x2c), and a +particle had a life of over 500000. Guard (`mods/guards.cpp`): live particles +with life >= 256 or row >= 3 are switched off before drawing. Logged in the +next session: 5 particles, all row 3, life 255, garbage sizes (e.g. 190555, +4.6e-40): the explosion emits into a fourth row whose emitter parameters it +never sets (vf10 0x7b6980 fills three rows at +0x30d0). No crash since. diff --git a/src/mods/guards.cpp b/src/mods/guards.cpp index 93f341e..0535e4e 100644 --- a/src/mods/guards.cpp +++ b/src/mods/guards.cpp @@ -3,6 +3,9 @@ #include +#include +#include + #include "re/hook.h" #include "re/log.h" @@ -31,4 +34,49 @@ void __fastcall BurningBone_Render(void* self, void* edx, void* device, Matrix4 BurningBone_Render_original(self, edx, device, transform); } +// cParticleSystem_grenadeimpact::Render (vf04 0x7b66b0, vtable 0x896cdc; +// (device, matrix by value), ret 0x44): Mörsergranate's explosion. Crashed +// twice (2026-10-26 00:12 and 22:39, sacred.exe+0x361b54 in the particle +// drawer 0x7613e0): a particle's colour is read from the table at +// +0x20d0 at row (+0x3d) * 256 + life (+0x2c), and the particle had a life of +// over 500000 (it counts down from about 255, at 250..400 per second, by +// row: cParticleSystem update 0x763ea0). Particles are 0x40 bytes in the +// vector +0x68..+0x6c; alive while life (+0x2c) and size (+0x28) are above 0. +// How a particle gets there is not known yet: such particles are switched +// off (life 0, the game's dead state) and the first ones logged. +constexpr size_t kParticlesBegin = 0x68; +constexpr size_t kParticlesEnd = 0x6c; +constexpr size_t kParticleSize = 0x40; +constexpr size_t kParticleSizeValue = 0x28; // float +constexpr size_t kParticleLife = 0x2c; // float, colour column 0..255 +constexpr size_t kParticleRow = 0x3d; // u8, colour row +constexpr float kColourColumns = 256.0f; +constexpr uint8_t kGrenadeImpactRows = 3; // emitter parameter rows at +0x30d0 (0x20 bytes each) +constexpr size_t kGrenadeImpactPhase = 0x20a9; // u8: 0 burning, 1 waiting, 2 fading (vf08 0x7b64c0) + +void __fastcall GrenadeImpact_Render(uint8_t* self, void* edx, void* device, Matrix4 transform); +RE_HOOK(0x007b66b0, GrenadeImpact_Render); +void __fastcall GrenadeImpact_Render(uint8_t* self, void* edx, void* device, Matrix4 transform) { + static int logged = 0; + uint8_t* begin = *reinterpret_cast(self + kParticlesBegin); + uint8_t* end = *reinterpret_cast(self + kParticlesEnd); + for (uint8_t* particle = begin; particle && particle < end; particle += kParticleSize) { + float& life = *reinterpret_cast(particle + kParticleLife); + const float size = *reinterpret_cast(particle + kParticleSizeValue); + const uint8_t row = particle[kParticleRow]; + if (!(life > 0.0f) || !(size > 0.0f) || (life < kColourColumns && row < kGrenadeImpactRows)) { + continue; + } + if (logged < 5) { + ++logged; + re::Log("guards: grenade impact particle %d of %d out of range (life %g, row %u, size %g, phase %u), " + "switched off", + static_cast((particle - begin) / kParticleSize), static_cast((end - begin) / kParticleSize), + life, row, size, self[kGrenadeImpactPhase]); + } + life = 0.0f; + } + GrenadeImpact_Render_original(self, edx, device, transform); +} + } // namespace game