mirror of
https://github.com/mitch030504/AstroQuest-Frame.git
synced 2026-10-23 19:01:24 +02:00
305 lines
14 KiB
C++
305 lines
14 KiB
C++
// Checks how the emulator tells the builds of ASTRO BOT Rescue Mission apart, and that it only
|
|
// ever writes into one it has recognised, all of what it means to write or nothing.
|
|
//
|
|
// clang-cl /std:c++latest /EHsc -fuse-ld=lld /I shadps4-arm64-main/src
|
|
// tools/tests/known_title_builds_test.cpp /Fe:build/tests/known_title_builds_test.exe
|
|
// build/tests/known_title_builds_test.exe [plain ELF of an eboot.bin ...]
|
|
//
|
|
// Without arguments it works on images made up here, which hold what a build has at the places
|
|
// the emulator looks at and nothing else. Given executables (made plain with
|
|
// `node tools/ps4elf.mjs unwrap <eboot.bin> <out.elf>`), it also says which build each is.
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <span>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/known_title_builds.h"
|
|
|
|
using namespace Core::KnownTitle::Builds;
|
|
|
|
static int failures = 0;
|
|
|
|
static void Check(bool ok, const std::string& what) {
|
|
std::printf("%s %s\n", ok ? "ok " : "FAIL", what.c_str());
|
|
failures += ok ? 0 : 1;
|
|
}
|
|
|
|
template <typename T>
|
|
static void Put(std::vector<u8>& image, u64 at, T value) {
|
|
std::memcpy(image.data() + at, &value, sizeof(T));
|
|
}
|
|
|
|
/// An image that holds what `build` has, as loaded, in every place the emulator looks at.
|
|
static std::vector<u8> MadeUp(const Build& build) {
|
|
std::vector<u8> image(0x3200000);
|
|
std::memcpy(image.data() + build.set_recentre, SetRecentreCode.data(), SetRecentreCode.size());
|
|
Put(image, build.frame_rate, ConsoleFrameRate);
|
|
Put(image, build.frame_seconds, ConsoleFrameSeconds);
|
|
Put(image, build.frame_microseconds, ConsoleFrameMicroseconds);
|
|
for (u32 level = 0; level < ConsoleSizes.size(); ++level) {
|
|
Put(image, build.size_widths + 4 * level, ConsoleSizes[level][0]);
|
|
Put(image, build.size_heights + 4 * level, ConsoleSizes[level][1]);
|
|
}
|
|
for (const Change& change : SizeChanges(build, Sizes{})) {
|
|
std::memcpy(image.data() + change.at, &change.was, change.bytes);
|
|
}
|
|
for (const Change& change : PhysicsStepChanges(build)) {
|
|
std::memcpy(image.data() + change.at, &change.was, change.bytes);
|
|
}
|
|
for (const Change& change : SoccerTimingChanges(build)) {
|
|
std::memcpy(image.data() + change.at, &change.was, change.bytes);
|
|
}
|
|
return image;
|
|
}
|
|
|
|
static void CheckSoccerTiming(const Build& build, std::vector<u8> image,
|
|
const std::string& name) {
|
|
const auto changes = SoccerTimingChanges(build);
|
|
if (build.soccer_nominal_seconds == 0) {
|
|
Check(changes.empty(), name + ": no guessed soccer patch for an unverified build");
|
|
return;
|
|
}
|
|
const auto original = image;
|
|
Check(Apply(image, changes) == nullptr, name + ": guarded soccer timing patch applied");
|
|
bool correct_targets = true;
|
|
for (const u64 at : build.soccer_budget_reads) {
|
|
u32 opcode = 0;
|
|
s32 relative = 0;
|
|
std::memcpy(&opcode, image.data() + at, sizeof(opcode));
|
|
std::memcpy(&relative, image.data() + at + 4, sizeof(relative));
|
|
correct_targets &= opcode == 0x0510fac5 && at + 8 + relative == build.soccer_nominal_seconds;
|
|
}
|
|
Check(correct_targets, name + ": both budget reads load the immutable console frame unit");
|
|
auto restored = image;
|
|
for (const Change& change : changes) {
|
|
std::memcpy(restored.data() + change.at, &change.was, change.bytes);
|
|
}
|
|
Check(restored == original, name + ": elapsed-time integration and assertion code unchanged");
|
|
float unit = 0;
|
|
std::memcpy(&unit, image.data() + build.soccer_nominal_seconds, sizeof(unit));
|
|
for (const double fps : {60., 72., 80., 90., 120., 144.}) {
|
|
const double budget = 30 * unit;
|
|
Check(budget >= 0.49 && budget < 0.6,
|
|
name + ": valid clip fits and oversized clip still fails at " + std::to_string(fps));
|
|
if (fps > 60) {
|
|
Check(30 / fps < 0.49, name + ": former variable-frame budget reproduces mismatch");
|
|
}
|
|
}
|
|
for (const Change& change : changes) {
|
|
auto odd = original;
|
|
odd[change.at] ^= 1;
|
|
const auto before = odd;
|
|
Check(Apply(odd, changes) != nullptr && odd == before,
|
|
name + ": unexpected bytes refuse the entire soccer patch");
|
|
}
|
|
auto truncated = std::span<u8>{image}.first(build.soccer_budget_reads.back() + 4);
|
|
Check(Apply(truncated, changes) != nullptr, name + ": truncated soccer code refused");
|
|
}
|
|
|
|
/// Where the call in a run of code leads: the first E8 of `code`, which starts at `at`.
|
|
static u64 CallTarget(std::span<const u8> code, u64 at) {
|
|
for (size_t i = 0; i + 5 <= code.size(); ++i) {
|
|
if (code[i] == 0xe8) {
|
|
s32 distance = 0;
|
|
std::memcpy(&distance, code.data() + i + 1, sizeof(distance));
|
|
return at + i + 5 + distance;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// The physics step of a build, changed: written once and whole, into one run of bytes that
|
|
/// still calls (or jumped to) the library's step it called before.
|
|
static void CheckPhysicsStep(const Build& build, std::vector<u8> image, const std::string& name) {
|
|
const auto changes = PhysicsStepChanges(build);
|
|
Check(changes.size() >= 3, name + ": the physics step is changed in " +
|
|
std::to_string(changes.size()) + " places");
|
|
bool in_a_row = true;
|
|
u64 bytes = 0;
|
|
for (const Change& change : changes) {
|
|
in_a_row &= change.at == changes[0].at + bytes;
|
|
bytes += change.bytes;
|
|
}
|
|
Check(in_a_row, name + ": they follow one another");
|
|
const u64 from = changes[0].at;
|
|
const std::vector<u8> before{image.begin() + from, image.begin() + from + bytes};
|
|
// The step the library takes was called at the end, or jumped to (E9) where the function
|
|
// had nothing left to do after it.
|
|
u64 led_to = CallTarget(before, from);
|
|
for (size_t i = 0; led_to == 0 && i + 5 <= before.size(); ++i) {
|
|
if (before[i] == 0xe9) {
|
|
s32 distance = 0;
|
|
std::memcpy(&distance, before.data() + i + 1, sizeof(distance));
|
|
led_to = from + i + 5 + distance;
|
|
}
|
|
}
|
|
Check(Apply(image, changes) == nullptr, name + ": the physics step is written");
|
|
const std::span<const u8> after{image.data() + from, bytes};
|
|
Check(led_to != 0 && CallTarget(after, from) == led_to,
|
|
name + ": it calls the library's step it went to before");
|
|
// The time step is written down after the call, no longer before it.
|
|
static constexpr std::array<u8, 6> Store{0x89, 0x87, 0x60, 0x27, 0x00, 0x00};
|
|
const auto call = std::find(after.begin(), after.end(), u8{0xe8});
|
|
const auto store = std::search(after.begin(), after.end(), Store.begin(), Store.end());
|
|
Check(store != after.end() && call < store, name + ": the time step is kept after the step");
|
|
Check(Apply(image, changes) != nullptr, name + ": not written a second time");
|
|
|
|
std::vector<u8> odd{image};
|
|
std::memcpy(odd.data() + from, before.data(), bytes);
|
|
odd[changes.back().at] ^= 1;
|
|
const std::vector<u8> untouched = odd;
|
|
Check(Apply(odd, changes) == &changes.back() && odd == untouched,
|
|
name + ": nothing of it written when the last place is not as expected");
|
|
}
|
|
|
|
/// Twice the console's sizes, as the emulator works them out for a width of 2880.
|
|
static Sizes Doubled() {
|
|
Sizes sizes;
|
|
for (s32 level = FirstHeadsetLevel; level <= LastHeadsetLevel; ++level) {
|
|
sizes.sizes[level] = {ConsoleSizes[level][0] * 2, ConsoleSizes[level][1] * 2};
|
|
}
|
|
sizes.target_pool = 880u << 20;
|
|
sizes.small_pool = 44u << 20;
|
|
sizes.graphics_heap = 2186ull << 20;
|
|
return sizes;
|
|
}
|
|
|
|
/// The image an executable makes in memory: its loaded parts at their addresses.
|
|
static std::vector<u8> Load(const char* path) {
|
|
std::ifstream file{path, std::ios::binary};
|
|
std::vector<u8> elf{std::istreambuf_iterator<char>{file}, std::istreambuf_iterator<char>{}};
|
|
if (elf.size() < 0x40 || std::memcmp(elf.data(), "\x7f" "ELF", 4) != 0) {
|
|
return {};
|
|
}
|
|
const auto read = [&](u64 at, auto& value) {
|
|
if (at + sizeof(value) > elf.size()) {
|
|
return false;
|
|
}
|
|
std::memcpy(&value, elf.data() + at, sizeof(value));
|
|
return true;
|
|
};
|
|
u64 headers = 0;
|
|
u16 header_size = 0;
|
|
u16 header_count = 0;
|
|
read(0x20, headers);
|
|
read(0x36, header_size);
|
|
read(0x38, header_count);
|
|
std::vector<u8> image;
|
|
for (u32 i = 0; i < header_count; ++i) {
|
|
const u64 at = headers + u64{i} * header_size;
|
|
u32 type = 0;
|
|
u64 offset = 0, address = 0, in_file = 0, in_memory = 0;
|
|
if (!read(at, type) || !read(at + 8, offset) || !read(at + 16, address) ||
|
|
!read(at + 32, in_file) || !read(at + 40, in_memory)) {
|
|
return {};
|
|
}
|
|
// PT_LOAD and PT_SCE_RELRO.
|
|
if ((type != 1 && type != 0x61000010) || offset + in_file > elf.size()) {
|
|
continue;
|
|
}
|
|
if (image.size() < address + in_memory) {
|
|
image.resize(address + in_memory);
|
|
}
|
|
std::memcpy(image.data() + address, elf.data() + offset, in_file);
|
|
}
|
|
return image;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
for (const Build& build : Known) {
|
|
const std::string name = build.name;
|
|
std::vector<u8> image = MadeUp(build);
|
|
Check(Recognise(image) == &build, name + ": recognised");
|
|
Check(Recognise(std::span<const u8>{}) == nullptr, name + ": not an empty image");
|
|
Check(Recognise(std::span{image}.first(build.set_recentre + 4)) == nullptr,
|
|
name + ": not an image cut short");
|
|
|
|
// One byte off anywhere the emulator looks, and it is no build that is known.
|
|
std::vector<u64> places{build.set_recentre + 3, build.frame_rate, build.frame_seconds,
|
|
build.frame_microseconds, build.size_widths + 4 * 6,
|
|
build.size_heights + 4 * 2};
|
|
for (const Change& change : SizeChanges(build, Sizes{})) {
|
|
places.push_back(change.at);
|
|
}
|
|
bool all_refused = true;
|
|
for (const u64 place : places) {
|
|
image[place] ^= 0x40;
|
|
all_refused &= Recognise(image) == nullptr;
|
|
image[place] ^= 0x40;
|
|
}
|
|
Check(all_refused, name + ": refused with any of " + std::to_string(places.size()) +
|
|
" places changed");
|
|
Check(Recognise(image) == &build, name + ": recognised again once they are restored");
|
|
|
|
// Larger sizes: all of them written, the television's left alone.
|
|
const Sizes doubled = Doubled();
|
|
const auto changes = SizeChanges(build, doubled);
|
|
Check(changes.size() == 30, name + ": 30 places set the sizes");
|
|
std::vector<u8> larger = image;
|
|
Check(Apply(larger, changes) == nullptr, name + ": larger sizes written");
|
|
bool written = true;
|
|
for (const Change& change : changes) {
|
|
u64 found = 0;
|
|
std::memcpy(&found, larger.data() + change.at, change.bytes);
|
|
written &= found == change.now;
|
|
}
|
|
Check(written, name + ": every place holds the larger size");
|
|
u32 television = 0;
|
|
std::memcpy(&television, larger.data() + build.size_widths + 4 * 2, sizeof(u32));
|
|
Check(television == 1920, name + ": the television's sizes are left alone");
|
|
Check(Apply(larger, changes) != nullptr, name + ": not written a second time");
|
|
Check(Recognise(larger) == nullptr, name + ": an image with larger sizes is not asked "
|
|
"what it is again");
|
|
|
|
// One place that is not as expected, the last one: nothing at all is written.
|
|
std::vector<u8> odd = image;
|
|
odd[changes.back().at] ^= 1;
|
|
const std::vector<u8> before = odd;
|
|
Check(Apply(odd, changes) == &changes.back() && odd == before,
|
|
name + ": nothing written when the last place is not as expected");
|
|
// A place beyond the image.
|
|
const std::vector<Change> beyond{{image.size() - 2, 0, 1, 4}};
|
|
Check(Apply(image, beyond) == &beyond[0], name + ": nothing written beyond the image");
|
|
|
|
CheckPhysicsStep(build, image, name);
|
|
CheckSoccerTiming(build, image, name);
|
|
// What the game is told apart by does not depend on it.
|
|
std::vector<u8> stepped = image;
|
|
Check(Apply(stepped, PhysicsStepChanges(build)) == nullptr &&
|
|
Recognise(stepped) == &build,
|
|
name + ": still recognised with its physics step changed");
|
|
}
|
|
|
|
// An image that would be both builds at once is neither.
|
|
std::vector<u8> both = MadeUp(Known[0]);
|
|
const std::vector<u8> other = MadeUp(Known[1]);
|
|
for (size_t i = 0; i < both.size(); ++i) {
|
|
both[i] |= other[i];
|
|
}
|
|
Check(Is(Known[0], both) && Is(Known[1], both) && Recognise(both) == nullptr,
|
|
"an image that is both builds at once is neither");
|
|
|
|
for (int i = 1; i < argc; ++i) {
|
|
const std::vector<u8> image = Load(argv[i]);
|
|
const Build* build = image.empty() ? nullptr : Recognise(image);
|
|
Check(build != nullptr, std::string{"recognised supplied executable: "} + argv[i]);
|
|
std::printf("%s: %s\n", argv[i],
|
|
image.empty() ? "not a plain ELF"
|
|
: build != nullptr ? build->name : "no build that is known");
|
|
if (build != nullptr) {
|
|
std::vector<u8> larger = image;
|
|
Check(Apply(larger, SizeChanges(*build, Doubled())) == nullptr,
|
|
std::string{"larger sizes written into "} + argv[i]);
|
|
CheckPhysicsStep(*build, image, argv[i]);
|
|
CheckSoccerTiming(*build, image, argv[i]);
|
|
}
|
|
}
|
|
|
|
std::printf("failed: %d\n", failures);
|
|
return failures == 0 ? 0 : 1;
|
|
}
|