The particle library moves particles by their velocity times a tick's time at each tick; between ticks, the adapter (ModelInstance::render's call to ParticleSystem::render, redirected after checking it) draws them moved on by their velocity times the time since, then moves each back by its own velocity: ParticleSystem::render sorts them by distance, reordering them in memory, so restoring saved positions in order scrambled them. Like ParticleSystem::render, it only reads particles when there's a group (the library throws without one, which crashed the client). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
869eb19a1f
commit
9d0faecf4d
3 files changed
+103
-3
No files matched your search
+98
-2
@@ -31,6 +31,24 @@ std::atomic<int> stepTicks = 1;
|
||||
std::atomic<bool> stepHooked = false;
|
||||
std::atomic<bool> poseHooked = false;
|
||||
std::atomic<bool> cameraHooked = false;
|
||||
std::atomic<bool> particlesHooked = false;
|
||||
|
||||
// IMVU's ParticleSystem::render(RenderManager*, const Transform&, const Transform&): a method with 3 arguments.
|
||||
using RenderParticles = void(__thiscall*)(void* self, void* renderManager, const void* a, const void* b);
|
||||
RenderParticles originalRenderParticles = nullptr;
|
||||
// The particle library's (ParticleLib.dll: PAPI, the Particle System API) PContextParticleGroup_t::GetParticlePointer:
|
||||
// the current group's particles, as floats: returns their count, and sets the first's address, the stride between
|
||||
// them, and the offsets of their attributes (in floats): position, previous position, size, velocity, previous
|
||||
// velocity, color, alpha, age, up, rotational velocity, previous up, mass, data.
|
||||
using GetParticlePointer = unsigned(__thiscall*)(void* group, float*& particles, unsigned& stride, unsigned& position,
|
||||
unsigned& positionB, unsigned& size, unsigned& velocity,
|
||||
unsigned& velocityB, unsigned& color, unsigned& alpha, unsigned& age,
|
||||
unsigned& up, unsigned& rotationalVelocity, unsigned& upB,
|
||||
unsigned& mass, unsigned& data);
|
||||
GetParticlePointer getParticlePointer = nullptr;
|
||||
// PContextParticleGroup_t::GetGroupCount: how many particle groups there are (GetParticlePointer throws without one).
|
||||
using GetGroupCount = unsigned(__thiscall*)(void* group);
|
||||
GetGroupCount getGroupCount = nullptr;
|
||||
|
||||
// zero.dll's constants: a tick, in milliseconds (33), and an animation frame, in seconds (1/30).
|
||||
const unsigned* millisecondsPerFrame = nullptr;
|
||||
@@ -224,6 +242,54 @@ void* __fastcall getCameraTransform(void* camera, void* /*edx*/, void* result)
|
||||
return transform;
|
||||
}
|
||||
|
||||
// Particles (SceneWindow.dll's ParticleSystem::step, at each tick) move by their velocity times the tick's time
|
||||
// (PContextActionList_t::TimeStep, millisecondsPerFrame / 1000 seconds a tick, then Move). Between ticks, they're drawn
|
||||
// moved on by their velocity times the time since the tick, then moved back the same way. (ParticleSystem::render
|
||||
// sorts the particles by distance before drawing them, reordering them in memory: each is moved back by its own
|
||||
// velocity, wherever it now is.) A ParticleSystem's particle group is at +0x28 (what ParticleSystem::render reads
|
||||
// them from).
|
||||
constexpr std::size_t particleGroup = 0x28;
|
||||
|
||||
void __fastcall renderParticles(void* system, void* /*edx*/, void* renderManager, const void* a, const void* b)
|
||||
{
|
||||
const float fraction = tick_fraction();
|
||||
void* group = static_cast<std::byte*>(system) + particleGroup;
|
||||
float* particles = nullptr;
|
||||
unsigned stride = 0, position = 0, positionB = 0, size = 0, velocity = 0, velocityB = 0, color = 0, alpha = 0,
|
||||
age = 0, up = 0, rotationalVelocity = 0, upB = 0, mass = 0, data = 0;
|
||||
// As ParticleSystem::render does: no particles without a group. The library reports errors by throwing; then the
|
||||
// particles are drawn as they are.
|
||||
const auto particle_pointer = [&] {
|
||||
try {
|
||||
if (getGroupCount(group) != 0) {
|
||||
return getParticlePointer(group, particles, stride, position, positionB, size, velocity, velocityB,
|
||||
color, alpha, age, up, rotationalVelocity, upB, mass, data);
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
return 0u;
|
||||
};
|
||||
const unsigned count = fraction >= 0.01f ? particle_pointer() : 0;
|
||||
if (count == 0 || particles == nullptr || stride == 0) {
|
||||
originalRenderParticles(system, renderManager, a, b);
|
||||
return;
|
||||
}
|
||||
const float seconds = fraction * static_cast<float>(*millisecondsPerFrame) / 1000.0f;
|
||||
const auto move = [&](float by) {
|
||||
for (unsigned i = 0; i < count; ++i) {
|
||||
float* particle = particles + std::size_t{i} * stride;
|
||||
for (unsigned axis = 0; axis < 3; ++axis) {
|
||||
particle[position + axis] += particle[velocity + axis] * by;
|
||||
}
|
||||
}
|
||||
};
|
||||
move(seconds);
|
||||
originalRenderParticles(system, renderManager, a, b);
|
||||
if (particle_pointer() == count && particles != nullptr) { // the same particles, maybe reordered
|
||||
move(-seconds);
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces the vtable entry at slot, if it holds expected; returns whether it did.
|
||||
bool replace(void** slot, void* expected, void* replacement)
|
||||
{
|
||||
@@ -300,6 +366,34 @@ void install_pose(HMODULE sceneWindow)
|
||||
poseHooked = true;
|
||||
}
|
||||
|
||||
void install_particles(HMODULE sceneWindow)
|
||||
{
|
||||
HMODULE particleLib = GetModuleHandleW(L"ParticleLib.dll");
|
||||
getParticlePointer = particleLib == nullptr
|
||||
? nullptr
|
||||
: reinterpret_cast<GetParticlePointer>(GetProcAddress(
|
||||
particleLib, "?GetParticlePointer@PContextParticleGroup_t@PAPI@@QAEIAAPAMAAI1111111111111@Z"));
|
||||
getGroupCount = particleLib == nullptr ? nullptr
|
||||
: reinterpret_cast<GetGroupCount>(GetProcAddress(
|
||||
particleLib, "?GetGroupCount@PContextParticleGroup_t@PAPI@@QAEIXZ"));
|
||||
auto* modelRender = reinterpret_cast<std::byte*>(GetProcAddress(
|
||||
sceneWindow, "?render@ModelInstance@@QAEXPAVRenderManager@@ABVCullingFrustum@@ABVTransform@@2ABUFogState@@"
|
||||
"ABULightState@@_NAAVSceneStats@@ABVRenderSettings@@@Z"));
|
||||
auto* render = GetProcAddress(sceneWindow, "?render@ParticleSystem@@QAEXPAVRenderManager@@ABVTransform@@1@Z");
|
||||
if (getParticlePointer == nullptr || getGroupCount == nullptr || modelRender == nullptr || render == nullptr
|
||||
|| millisecondsPerFrame == nullptr) {
|
||||
log::info("scene hooks: ParticleLib's GetParticlePointer, ModelInstance::render or ParticleSystem::render not found");
|
||||
return;
|
||||
}
|
||||
// ModelInstance::render draws its particle systems with a call at +0x114.
|
||||
originalRenderParticles = reinterpret_cast<RenderParticles>(render);
|
||||
if (!redirect_call(modelRender + 0x114, reinterpret_cast<void*>(render), reinterpret_cast<void*>(&renderParticles))) {
|
||||
log::info("scene hooks: ModelInstance::render doesn't call ParticleSystem::render where expected");
|
||||
return;
|
||||
}
|
||||
particlesHooked = true;
|
||||
}
|
||||
|
||||
void install_camera(HMODULE sceneWindow)
|
||||
{
|
||||
auto* vtable = reinterpret_cast<void**>(GetProcAddress(sceneWindow, "??_7CameraController@@6B@"));
|
||||
@@ -346,10 +440,12 @@ void install()
|
||||
install_step(sceneWindow);
|
||||
if (stepHooked) { // drawing between ticks needs the ticks
|
||||
install_pose(sceneWindow);
|
||||
install_particles(sceneWindow);
|
||||
install_camera(sceneWindow);
|
||||
}
|
||||
log::info("scene hooks: ticks {}, posing between ticks {}, camera between ticks {}",
|
||||
stepHooked ? "recorded" : "not recorded", poseHooked ? "on" : "off", cameraHooked ? "on" : "off");
|
||||
log::info("scene hooks: ticks {}, posing between ticks {}, particles between ticks {}, camera between ticks {}",
|
||||
stepHooked ? "recorded" : "not recorded", poseHooked ? "on" : "off", particlesHooked ? "on" : "off",
|
||||
cameraHooked ? "on" : "off");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
// - ModelInstance::render's call to updateCalSkeleton, which poses the model before it's drawn: between ticks, the
|
||||
// model is posed at the time between them (its animations' times moved on by the fraction of a frame since the
|
||||
// tick; Cal3D poses between keyframes), then its animations are put back as they were.
|
||||
// - ModelInstance::render's call to ParticleSystem::render: between ticks, particles drawn moved on by their velocity
|
||||
// times the time since the tick (as the particle library moves them at each tick), then put back.
|
||||
// - CameraController::getCameraTransform (its exported vtable): between ticks, the camera where its easing toward its
|
||||
// target has brought it since the tick.
|
||||
|
||||
|
||||
Reference in new issue
Block a user