Files

213 lines
12 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Host check for mods: Eden's patch code as this build derives it (.ips and .pchtxt patches, which
// the pinned source could not apply: headless/CMakeLists.txt), what the launcher lists from a
// game's mods folder (mods.h), and the names it switches off (settings_store.h).
#include "mods.h"
#include "settings_store.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <vector>
#include "core/file_sys/ips_layer.h"
#include "core/file_sys/vfs/vfs_vector.h"
namespace {
namespace fs = std::filesystem;
void require(bool condition, const char* what) {
if (!condition) {
std::fprintf(stderr, "Mods FAIL: %s\n", what);
std::exit(1);
}
}
void write_text(const fs::path& path, const std::string& text) {
fs::create_directories(path.parent_path());
std::ofstream(path, std::ios::binary) << text;
}
FileSys::VirtualFile file(std::vector<u8> bytes, const char* name) {
return std::make_shared<FileSys::VectorVfsFile>(std::move(bytes), name);
}
// A stand-in for a game's code: 0x800 bytes that count up.
std::vector<u8> code() {
std::vector<u8> bytes(0x800);
for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast<u8>(i * 7 + 3);
return bytes;
}
void append(std::vector<u8>& out, std::initializer_list<int> bytes) {
for (const int byte : bytes) out.push_back(static_cast<u8>(byte));
}
void check_ips() {
const std::vector<u8> original = code();
// IPS: a record of three bytes at 0x10, then a run of eight 0xAA at 0x100.
std::vector<u8> ips{'P', 'A', 'T', 'C', 'H'};
append(ips, {0x00, 0x00, 0x10, 0x00, 0x03, 0x11, 0x22, 0x33});
append(ips, {0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0xAA});
append(ips, {'E', 'O', 'F'});
auto patched = FileSys::PatchIPS(file(original, "main"), file(ips, "patch.ips"));
require(patched != nullptr, "an IPS patch applies");
auto bytes = patched->ReadAllBytes();
require(bytes.size() == original.size() && bytes[0x10] == 0x11 && bytes[0x11] == 0x22 && bytes[0x12] == 0x33,
"IPS record bytes");
for (std::size_t i = 0x100; i < 0x108; ++i) require(bytes[i] == 0xAA, "IPS run bytes");
require(bytes[0x0F] == original[0x0F] && bytes[0x13] == original[0x13] && bytes[0x108] == original[0x108],
"IPS leaves the rest alone");
// IPS32: offsets of four bytes, end marker "EEOF".
std::vector<u8> ips32{'I', 'P', 'S', '3', '2'};
append(ips32, {0x00, 0x00, 0x07, 0x00, 0x00, 0x02, 0xC0, 0xDE});
append(ips32, {'E', 'E', 'O', 'F'});
patched = FileSys::PatchIPS(file(original, "main"), file(ips32, "patch.ips"));
require(patched != nullptr, "an IPS32 patch applies");
bytes = patched->ReadAllBytes();
require(bytes[0x700] == 0xC0 && bytes[0x701] == 0xDE && bytes[0x702] == original[0x702], "IPS32 record bytes");
// Not a patch, a patch without its end, and a patch past the end of the code: nothing applies.
require(FileSys::PatchIPS(file(original, "main"), file(original, "not.ips")) == nullptr, "a file that is no patch");
std::vector<u8> cut(ips.begin(), ips.end() - 3);
require(FileSys::PatchIPS(file(original, "main"), file(cut, "cut.ips")) == nullptr, "a patch without its end");
std::vector<u8> beyond{'P', 'A', 'T', 'C', 'H'};
append(beyond, {0x00, 0x09, 0x00, 0x00, 0x01, 0x55, 'E', 'O', 'F'});
require(FileSys::PatchIPS(file(original, "main"), file(beyond, "beyond.ips")) == nullptr,
"a patch past the end of the code");
}
void check_pchtxt() {
// Offsets count from the start of the NSO with the shift a patch file may name; a patch's
// values are the bytes as written, a quoted value is text.
std::string text =
"@nsobid-0123456789ABCDEF0123456789abcdef01234567\n"
"# 60 FPS\n"
"@flag offset_shift 0x100\n"
"@enabled\n"
"00000010 DEADBEEF\n"
"00000020 \"Hi\\n!\"\n"
"00000040 01 // one byte\n"
"@disabled\n"
"00000030 FFFFFFFF\n"
"@enabled\n"
// What patch files carry besides records: their authors' names and links. None of it is a
// record or a flag (read as such, it wrote over the start of the code and the build ID).
"@somebody\r\n"
"@nobody-here\r\n"
"@blender\r\n"
"https://example.invalid/somebody\r\n"
"Special thanks to the team!\r\n"
"Contributors (thank you)\r\n"
"Somebody#1234, Another One#5678\r\n";
// A value longer than the pinned code's 248-byte record.
std::string wide = "00000200 ";
for (int i = 0; i < 300; ++i) wide += "5A";
text += wide + "\n@stop\n00000050 EE\n";
const FileSys::IPSwitchCompiler compiler{
file(std::vector<u8>(text.begin(), text.end()), "60fps.pchtxt")};
const auto id = compiler.GetBuildID();
static constexpr u8 kId[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67,
0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67};
for (std::size_t i = 0; i < id.size(); ++i) require(id[i] == (i < sizeof(kId) ? kId[i] : 0), "the build ID");
const std::vector<u8> original = code();
const auto patched = compiler.Apply(file(original, "main"));
require(patched != nullptr, "a .pchtxt patch applies");
const auto bytes = patched->ReadAllBytes();
require(bytes.size() == original.size(), "the code keeps its size");
require(bytes[0x110] == 0xDE && bytes[0x111] == 0xAD && bytes[0x112] == 0xBE && bytes[0x113] == 0xEF,
"hex values, in the order written");
require(bytes[0x120] == 'H' && bytes[0x121] == 'i' && bytes[0x122] == '\n' && bytes[0x123] == '!' &&
bytes[0x124] == original[0x124], "text values");
require(bytes[0x140] == 0x01 && bytes[0x141] == original[0x141], "a value before a comment");
require(bytes[0x130] == original[0x130], "a disabled patch is skipped");
for (std::size_t i = 0x100; i < 0x110; ++i) require(bytes[i] == original[i], "credits are not records");
for (std::size_t i = 0x300; i < 0x300 + 300; ++i) require(bytes[i] == 0x5A, "a long value");
require(bytes[0x300 + 300] == original[0x300 + 300], "a long value ends where it should");
require(bytes[0x150] == original[0x150], "nothing after @stop");
}
void check_listing(const fs::path& base) {
using namespace Eden::Mods;
const std::uint64_t game = 0x0100ABCD00001000ULL, other = 0x0100FFFF00002000ULL;
const std::string root = (base / "mods").string();
require(List(root, game).empty() && TitleFolder(root, game).empty(), "no mods folder");
require(TitleFolderToCreate(root, game) == root + "/0100ABCD00001000", "the folder to make");
// The title's folder in lower case, as a file manager may have it.
const fs::path title = base / "mods" / "0100abcd00001000";
write_text(title / "60 FPS" / "exefs" / "0123456789ABCDEF.pchtxt", "@enabled\n");
write_text(title / "Texture pack" / "romfs" / "model" / "hero.bin", "x");
write_text(title / "Everything" / "ExeFS" / "main", "x");
write_text(title / "Everything" / "romfs_ext" / "a.stub", "");
write_text(title / "Everything" / "cheats" / "0123456789ABCDEF.txt", "[cheat]\n");
write_text(title / "cheat_infinite.txt", "[cheat]\n");
write_text(title / "notes.txt", "not a mod");
fs::create_directories(title / "Empty" / "exefs");
write_text(title / "Readme only" / "readme.txt", "nothing Eden uses");
write_text(title / "Wrong exefs" / "exefs" / "notes.txt", "nothing Eden uses");
require(TitleFolder(root, game) == title.string(), "the title's folder in either case");
const auto mods = List(root, game);
require(mods.size() == 4, "four mods");
require(mods[0].name == "60 FPS" && mods[0].kinds == kCode, "a patch");
require(mods[1].name == "Everything" && mods[1].kinds == (kCode | kFiles | kCheats), "all three kinds");
require(mods[2].name == "Texture pack" && mods[2].kinds == kFiles, "replacement files");
require(mods[3].name == "cheat_infinite.txt" && mods[3].kinds == kCheats, "a loose cheat file");
require(List(root, other).empty() && List(root, 0).empty(), "another game has none");
require(Summary(mods, {}) == "60 FPS, Everything, Texture pack, cheat_infinite.txt", "all in use");
require(Summary(mods, {"Everything", "Gone"}) == "60 FPS, Texture pack, cheat_infinite.txt (1 switched off)",
"one switched off");
require(Summary({}, {}) == "none", "no mods");
}
void check_settings(const fs::path& base) {
const std::string settings = (base / "prosperoeden.json").string();
const std::uint64_t game = 0x0100ABCD00001000ULL, other = 0x0100FFFF00002000ULL;
require(Eden::LoadDisabledMods(game, settings).empty(), "nothing switched off at first");
require(Eden::SaveGameSettings(game, {1, 4, 1}, settings), "game settings");
require(Eden::SaveModEnabled(game, "60 FPS", false, settings) &&
Eden::SaveModEnabled(game, "Texture pack", false, settings) &&
Eden::SaveModEnabled(game, "60 FPS", false, settings), "switching off");
auto off = Eden::LoadDisabledMods(game, settings);
require(off == std::vector<std::string>({"Texture pack", "60 FPS"}), "the names switched off, once each");
require(Eden::LoadDisabledMods(other, settings).empty(), "per game");
require(Eden::SaveModEnabled(game, "Texture pack", true, settings), "switching on");
off = Eden::LoadDisabledMods(game, settings);
require(off == std::vector<std::string>({"60 FPS"}), "one left");
const auto kept = Eden::LoadGameSettings(game, settings);
require(kept.renderer == 1 && kept.resolution == 4 && kept.upscaling_filter == 1, "the other settings stay");
require(Eden::SaveModEnabled(game, "60 FPS", true, settings) && Eden::LoadDisabledMods(game, settings).empty(),
"all on again");
require(!Eden::SaveModEnabled(0, "x", false, settings) && !Eden::SaveModEnabled(game, "", false, settings),
"nothing to save");
// The Library's Mods switch: on unless turned off, per game, and the mods' own switches and
// the game's other settings stay as they are behind it.
require(Eden::LoadModsEnabled(game, settings) && Eden::LoadModsEnabled(other, settings) &&
Eden::LoadModsEnabled(0, settings), "the switch is on at first");
require(Eden::SaveModEnabled(game, "60 FPS", false, settings) && Eden::SaveModsEnabled(game, false, settings),
"the switch turned off");
require(!Eden::LoadModsEnabled(game, settings) && Eden::LoadModsEnabled(other, settings), "off for that game only");
require(Eden::LoadDisabledMods(game, settings) == std::vector<std::string>({"60 FPS"}), "a mod's own switch stays");
require(Eden::LoadGameSettings(game, settings).resolution == 4, "the other settings stay with the switch off");
require(Eden::SaveModsEnabled(game, true, settings) && Eden::LoadModsEnabled(game, settings), "the switch back on");
std::ifstream written(settings);
const std::string text{std::istreambuf_iterator<char>(written), {}};
require(text.find("\"mods\"") == std::string::npos && text.find("\"mods_off\"") != std::string::npos,
"on is not written; the mod switched off still is");
require(!Eden::SaveModsEnabled(0, false, settings), "no game, nothing to save");
}
} // namespace
int main() {
char pattern[] = "/tmp/mods-check-XXXXXX";
require(mkdtemp(pattern) != nullptr, "temporary folder");
const fs::path base = pattern;
check_ips();
check_pchtxt();
check_listing(base);
check_settings(base);
fs::remove_all(base);
std::puts("Mods: IPS and IPS32 patches, .pchtxt hex, text and long values, the folder listing, "
"switching off per game, the game's Mods switch PASS");
}