Files
sacred_k/src/mods/guards.cpp
T
khallaandClaude Opus 5.5 42b99526a4 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>
2026-10-26 23:39:20 +01:00

83 lines
3.8 KiB
C++

// Guards against crashes in the game's own code (each with what is known of
// its cause).
#include <intrin.h>
#include <cstddef>
#include <cstdint>
#include "re/hook.h"
#include "re/log.h"
namespace game {
// cParticleSystem_burningBone::Render (vf04 0x774e10, vtable 0x895b84):
// crashed reading the device it was given, which was null (2026-10-16, Esc
// pressed while walking; Windows reported sacred.exe+0x374e39). Who renders
// it without a device is not known yet (sacredK_crash.txt will tell). With
// no device there is nothing to draw.
// (device, a 4x4 matrix by value; ret 0x44).
struct Matrix4 {
float m[16];
};
void __fastcall BurningBone_Render(void* self, void* edx, void* device, Matrix4 transform);
RE_HOOK(0x00774e10, BurningBone_Render);
void __fastcall BurningBone_Render(void* self, void* edx, void* device, Matrix4 transform) {
if (!device) {
static bool logged = false;
if (!logged) {
logged = true;
re::Log("guards: burning bone particles rendered without a device, skipped (%p)", _ReturnAddress());
}
return;
}
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