Files
SpartanPort/tests/test_ledge_hook.cpp
Felipe Prado 24589edc5a Feat(Engine): high FPS safety nets for QTEs and the Cronos pull (#1144)
EN-US

- Added "High Frame Rate Physics Fix" (auto_safe_fps, on by default): any QTE prompt kind caps the engine at 60, held 5 s after the last one, never saved
- Kept chests and other hold-a-button prompts at the player's rate
- Added the Cronos blade pull cap: R1 near the one pull that throws Kratos out of the map caps the engine at 60 for 4 s
- Added src/player_hook.h, which finds Kratos for that cap
- Added the "High FPS Wall Seam Fix" patch, on by default, so Kratos holds the Realm of Hades wall
- Fixed imported settings: shader notes stay off and new default patches are ticked once
- Marked frame generation as experimental in the launcher (#1149)
- Changed the trophy notification defaults and added a built-in unlock sound
- Added tools/find_rate_readers.py and the high FPS docs

---

PT-BR

- Adicionado "Correção de física em FPS alto" (auto_safe_fps, ligado por padrão): qualquer tipo de prompt de QTE trava o motor em 60, mantido 5 s depois do último, nunca salvo
- Mantidos os baús e outros prompts de segurar botão na taxa do jogador
- Adicionada a trava da puxada no Cronos: R1 perto da única puxada que joga o Kratos para fora do mapa trava o motor em 60 por 4 s
- Adicionado src/player_hook.h, que acha o Kratos para essa trava
- Adicionado o patch "High FPS Wall Seam Fix", ligado por padrão, para o Kratos segurar a parede do Reino de Hades
- Corrigidas as configurações importadas: as notas de shader ficam desligadas e os novos patches padrão são marcados uma vez
- Marcada a geração de quadros como experimental no launcher (#1149)
- Alterados os padrões das notificações de troféu e adicionado um som de desbloqueio embutido
- Adicionados tools/find_rate_readers.py e a documentação de FPS alto
2026-10-31 22:08:26 -03:00

133 lines
6.7 KiB
C++

// The wall seam fix: the reuse rule, its stub and the patched site's signature.
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iterator>
#include <vector>
#include <windows.h>
#include "../src/ledge_hook.h"
// Enters a stub the way the patched call site does: rbp is the game frame, rbx the object
// and rsp 16-byte aligned, then returns the eax the stub resumes with.
extern "C" __attribute__((sysv_abi)) uint32_t ledge_invoke(void *code, uintptr_t frame, uintptr_t object);
asm(".globl ledge_invoke\nledge_invoke:\n"
"push %rbp\npush %rbx\nmov %rsi, %rbp\nmov %rdx, %rbx\ncall *%rdi\npop %rbx\npop %rbp\nret\n");
static uint32_t ray_result, ray_calls;
alignas(16) static uint32_t ray_global[24];
extern "C" __attribute__((sysv_abi)) uint32_t fake_ledge_ray() {
++ray_calls;
ray_global[0] = ray_result ? 0x40000000 + ray_calls : 0xdead;
return ray_result;
}
static void ledge_seam_fix(unsigned char *code) {
alignas(16) unsigned char stack[0x400]{};
const uintptr_t frame = (uintptr_t)(stack + 0x380);
uint32_t object[0x200 / 4]{};
object[0x1c0 / 4] = 0x10;
float *position = (float *)(frame - 0x230);
position[0] = 302.729f; position[1] = -51.604f; position[2] = 180.685f;
SpartanLedgeCache cache{};
cache.result = (uintptr_t)ray_global;
// A hit is kept unchanged and cached.
ray_global[5] = 0x1234;
assert(spartanport_ledge_ray(&cache, frame, (uintptr_t)object, 3) == 3);
assert(cache.hits == 1 && cache.hit == 3 && cache.cache[5] == 0x1234 && cache.object == (uintptr_t)object);
// A miss inside the seam (0.006 further) returns the cached hit.
position[2] = 180.691f;
ray_global[5] = 0;
assert(spartanport_ledge_ray(&cache, frame, (uintptr_t)object, 0) == 3);
assert(cache.reused == 1 && cache.misses == 1 && ray_global[5] == 0x1234);
// Still measured from the real hit, not from the reused one.
position[2] = 180.685f + SPARTAN_LEDGE_REUSE + 0.001f;
ray_global[5] = 0;
assert(spartanport_ledge_ray(&cache, frame, (uintptr_t)object, 0) == 0);
assert(cache.reused == 1 && cache.misses == 2 && ray_global[5] == 0);
// Another object never takes this one's hit.
uint32_t other[0x200 / 4]{};
other[0x1c0 / 4] = 0x10;
position[2] = 180.686f;
assert(spartanport_ledge_ray(&cache, frame, (uintptr_t)other, 0) == 0 && cache.reused == 1);
// Outside the wall traversal the result passes through and nothing is cached.
other[0x1c0 / 4] = 1;
assert(spartanport_ledge_ray(&cache, frame, (uintptr_t)other, 5) == 5 && cache.hits == 1);
assert(spartanport_ledge_ray(&cache, frame, (uintptr_t)other, 0) == 0 && cache.misses == 3);
// The stub calls the game's ray, then the helper, and resumes after the original call.
cache = {};
cache.result = (uintptr_t)ray_global;
std::memset(code, 0xcc, 4096);
code[1024] = 0xc3;
const unsigned length = spartanport_ledge_stub(code, (uintptr_t)&cache, (uintptr_t)spartanport_ledge_ray,
(uintptr_t)fake_ledge_ray, (uintptr_t)(code + 1024));
assert(length && length <= SPARTAN_LEDGE_STUB_SIZE);
assert(!spartanport_ledge_stub(code, (uintptr_t)&cache, (uintptr_t)spartanport_ledge_ray, 0, (uintptr_t)(code + 1024)));
FlushInstructionCache(GetCurrentProcess(), code, 4096);
position[2] = 180.685f;
ray_result = 1;
assert(ledge_invoke(code, frame, (uintptr_t)object) == 1 && ray_calls == 1 && cache.hits == 1);
const uint32_t kept = ray_global[0];
ray_result = 0;
position[2] = 180.69f;
assert(ledge_invoke(code, frame, (uintptr_t)object) == 1 && ray_calls == 2 && cache.reused == 1);
assert(ray_global[0] == kept);
position[2] = 181.0f;
assert(ledge_invoke(code, frame, (uintptr_t)object) == 0 && ray_global[0] == 0xdead);
// The site exists only when the launcher patch swaps 31 c9 for 33 c9 (same xor ecx, ecx).
int pattern[SPARTAN_SIGNATURE_MAX];
const unsigned size = spartanport_signature_parse(spartanport_ledge_site.pattern, pattern);
assert(size);
std::vector<unsigned char> image(256);
for (unsigned i = 0; i < size; ++i) image[32 + i] = pattern[i] < 0 ? 0 : (unsigned char)pattern[i];
assert(spartanport_signature_find(image.data(), image.size(), &spartanport_ledge_site) == 32);
image[32 + 5] = 0x31;
assert(spartanport_signature_find(image.data(), image.size(), &spartanport_ledge_site) == UINT64_MAX);
}
static uint64_t read64(const std::vector<unsigned char>& data, size_t at) {
uint64_t value;
assert(at + sizeof(value) <= data.size());
std::memcpy(&value, data.data() + at, sizeof(value));
return value;
}
// The maintainer's eboot (the launcher's out/eboot.elf): no site unpatched, one with the patch's 33 c9.
static void check_reference(const char *path) {
std::ifstream file(path, std::ios::binary);
assert(file);
std::vector<unsigned char> elf{std::istreambuf_iterator<char>(file), {}};
assert(elf.size() >= 64 && !std::memcmp(elf.data(), "\x7f" "ELF", 4));
uint16_t stride, count;
std::memcpy(&stride, elf.data() + 0x36, 2);
std::memcpy(&count, elf.data() + 0x38, 2);
std::vector<unsigned char> image;
for (unsigned i = 0; i < count; ++i) {
const size_t at = static_cast<size_t>(read64(elf, 0x20)) + i * stride;
uint32_t type;
std::memcpy(&type, elf.data() + at, 4);
if (type != 1) continue;
const size_t offset = read64(elf, at + 8), address = read64(elf, at + 16), size = read64(elf, at + 32);
assert(offset <= elf.size() && size <= elf.size() - offset && address + size <= 512 * 1024 * 1024);
if (image.size() < address + size) image.resize(address + size);
std::memcpy(image.data() + address, elf.data() + offset, size);
}
assert(spartanport_signature_find(image.data(), image.size(), &spartanport_ledge_site) == UINT64_MAX);
const SpartanSignature original = {"e8 ?? ?? ?? ?? 31 c9 85 c0 0f 84 ?? ?? ?? ?? 48 8d 05 ?? ?? ?? ?? c5 f8 28 00", 5};
const uint64_t swap = spartanport_signature_find(image.data(), image.size(), &original);
assert(swap != UINT64_MAX);
image[swap] = 0x33;
const uint64_t at = spartanport_signature_find(image.data(), image.size(), &spartanport_ledge_site);
assert(at == swap - 5);
std::printf("Wall seam fix: signature at image+0x%llx\n", static_cast<unsigned long long>(at));
}
int main(int argc, char **argv) {
auto *code = static_cast<unsigned char *>(VirtualAlloc(nullptr, 4096, MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE));
assert(code);
ledge_seam_fix(code);
if (argc > 1) check_reference(argv[1]);
std::puts("Wall seam fix: hit, near miss, far miss, other object, stub and signature OK");
return 0;
}