Guard: grenade impact particles out of the colour table
Mörsergranate's explosion crashed twice reading the colour of a particle with a huge life (sacred.exe+0x361b54). Particles with life >= 256 or the unset fourth row are switched off before drawing; logged cases were all row 3 with garbage sizes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
91e7f844cc
commit
42b99526a4
2 files changed
+59
No files matched your search
@@ -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
|
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
|
book), in that line's colour, from the same `ImpactRadiusAt(level)` as the
|
||||||
hit check and the marker.
|
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.
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
#include <intrin.h>
|
#include <intrin.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
#include "re/log.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);
|
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<uint8_t**>(self + kParticlesBegin);
|
||||||
|
uint8_t* end = *reinterpret_cast<uint8_t**>(self + kParticlesEnd);
|
||||||
|
for (uint8_t* particle = begin; particle && particle < end; particle += kParticleSize) {
|
||||||
|
float& life = *reinterpret_cast<float*>(particle + kParticleLife);
|
||||||
|
const float size = *reinterpret_cast<const float*>(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<int>((particle - begin) / kParticleSize), static_cast<int>((end - begin) / kParticleSize),
|
||||||
|
life, row, size, self[kGrenadeImpactPhase]);
|
||||||
|
}
|
||||||
|
life = 0.0f;
|
||||||
|
}
|
||||||
|
GrenadeImpact_Render_original(self, edx, device, transform);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace game
|
} // namespace game
|
||||||
Reference in new issue
Block a user