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>
83 lines
3.8 KiB
C++
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
|