mirror of
https://github.com/aimrebirth/tools.git
synced 2026-04-14 17:33:25 +00:00
729 lines
25 KiB
C++
729 lines
25 KiB
C++
#pragma once
|
|
|
|
#include <db2.h>
|
|
#include <mmo2.h>
|
|
|
|
#include <boost/container_hash/hash.hpp>
|
|
#include <primitives/command.h>
|
|
#include <primitives/filesystem.h>
|
|
#include <primitives/sw/main.h>
|
|
|
|
#include <format>
|
|
#include <fstream>
|
|
#include <set>
|
|
#include <source_location>
|
|
#include <print>
|
|
|
|
constexpr auto aim_exe = "aim.exe"sv;
|
|
|
|
using byte_array = std::vector<uint8_t>;
|
|
|
|
auto operator""_bin(const char *ptr, uint64_t len) {
|
|
byte_array ret;
|
|
auto lines = split_lines(ptr);
|
|
for (auto &&line : lines) {
|
|
auto d = line.substr(0, line.find(';'));
|
|
auto bytes = split_string(d, " \r\n");
|
|
for (auto &&v : bytes) {
|
|
if (v.size() != 2) {
|
|
throw std::runtime_error{"bad input string"};
|
|
}
|
|
auto hex2int1 = [](auto c) {
|
|
if (isdigit(c)) {
|
|
return c - '0';
|
|
} else if (isupper(c)) {
|
|
return c - 'A' + 10;
|
|
} else {
|
|
return c - 'a' + 10;
|
|
}
|
|
};
|
|
auto hex2int = [&](auto c) {
|
|
auto v = hex2int1(c);
|
|
if (v < 0 || v > 15) {
|
|
throw std::runtime_error{"bad input char"};
|
|
}
|
|
return v;
|
|
};
|
|
auto d1 = hex2int(v[0]);
|
|
auto d2 = hex2int(v[1]);
|
|
ret.push_back((d1 << 4) | d2);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void log(auto &&format, auto &&arg, auto &&...args) {
|
|
std::println("{}", std::vformat(format, std::make_format_args(arg, args...)));
|
|
}
|
|
void log(auto &&str) {
|
|
std::println("{}", str);
|
|
}
|
|
|
|
struct aim_exe_v1_06_constants {
|
|
enum : uint32_t {
|
|
trampoline_base_real = 0x00025100,
|
|
trampoline_target_real = 0x001207f0,
|
|
code_base = 0x00401000,
|
|
//data_base = 0x00540000,
|
|
//free_data_base_virtual = 0x006929C0,
|
|
free_data_base_virtual = 0x00692FF0,
|
|
our_code_start_virtual = 0x005207F0, // place to put out dll load
|
|
};
|
|
};
|
|
|
|
struct mod_maker {
|
|
enum class file_type {
|
|
unknown,
|
|
|
|
mmp,
|
|
mmo,
|
|
mmm,
|
|
model,
|
|
tm,
|
|
script,
|
|
sound,
|
|
};
|
|
struct {
|
|
mod_maker &m;
|
|
db2 db_;
|
|
db2 quest_;
|
|
|
|
db2 &db() {
|
|
add_files("db");
|
|
return db_;
|
|
}
|
|
db2 &quest() {
|
|
add_files("quest");
|
|
return quest_;
|
|
}
|
|
private:
|
|
void add_files(const path &fn) {
|
|
auto base = path{"data"} / fn;
|
|
m.files_to_distribute.insert(path{base} += ".ind");
|
|
m.files_to_distribute.insert(path{base} += ".dat");
|
|
m.files_to_distribute.insert(path{base} += ".tab");
|
|
}
|
|
} db{*this};
|
|
|
|
std::string name;
|
|
std::string version;
|
|
path game_dir;
|
|
std::set<path> files_to_pak;
|
|
std::set<path> files_to_distribute;
|
|
std::set<path> code_files_to_distribute;
|
|
std::source_location loc;
|
|
|
|
mod_maker(std::source_location loc = std::source_location::current()) : loc{loc} {
|
|
init(fs::current_path());
|
|
}
|
|
mod_maker(const std::string &name, std::source_location loc = std::source_location::current()) : name{name}, loc{loc} {
|
|
init(fs::current_path());
|
|
}
|
|
mod_maker(const std::string &name, const path &dir, std::source_location loc = std::source_location::current()) : name{name}, loc{loc} {
|
|
init(dir);
|
|
}
|
|
|
|
void replace(const path &fn, const std::string &from, const std::string &to) {
|
|
auto ft = check_file_type(fn);
|
|
switch (ft) {
|
|
case file_type::script: {
|
|
auto p = find_real_filename(fn);
|
|
auto txt = make_script_txt_fn(p);
|
|
if (!fs::exists(txt)) {
|
|
run_p4_tool("script2txt", p);
|
|
}
|
|
auto dst_txt = get_mod_dir() / txt.filename();
|
|
fs::copy_file(txt, dst_txt, fs::copy_options::overwrite_existing);
|
|
txt = dst_txt;
|
|
replace_in_file_raw(txt, from, to);
|
|
run_p4_tool("txt2script", txt);
|
|
files_to_pak.insert(get_mod_dir() / txt.stem());
|
|
break;
|
|
}
|
|
default:
|
|
SW_UNIMPLEMENTED;
|
|
}
|
|
}
|
|
void apply() {
|
|
db.db_.close();
|
|
db.quest_.close();
|
|
|
|
std::vector<std::string> files;
|
|
for (auto &&p : files_to_pak) {
|
|
if (p.filename() == aim_exe) {
|
|
continue;
|
|
}
|
|
files.push_back(p.string());
|
|
}
|
|
auto fn = get_mod_dir() / get_full_mod_name() += ".pak"s;
|
|
run_p4_tool("paker", fn, files);
|
|
fs::copy_file(fn, get_data_dir() / fn.filename(), fs::copy_options::overwrite_existing);
|
|
files_to_distribute.insert(path{"data"} / fn.filename());
|
|
// make patch notes
|
|
auto patchnotes_fn = path{game_dir / get_full_mod_name()} += ".README.txt";
|
|
files_to_distribute.insert(patchnotes_fn.filename());
|
|
std::ofstream ofile{patchnotes_fn};
|
|
#ifndef NDEBUG
|
|
ofile << "Developer Mode!!!\nOnly for testing purposes!\nDO NOT USE FOR ACTUAL PLAYING !!!\n\n";
|
|
#endif
|
|
ofile << name;
|
|
if (!version.empty()) {
|
|
ofile << " (version: " << version << ")";
|
|
}
|
|
ofile << "\n\n";
|
|
ofile << std::format("Release Date\n{:%d.%m.%Y %X}\n\n", std::chrono::system_clock::now());
|
|
for (auto &&line : read_lines(loc.file_name())) {
|
|
auto f = [&](auto &&a) {
|
|
auto pos = line.find(a);
|
|
if (pos != -1) {
|
|
auto s = line.substr(pos + a.size());
|
|
if (!s.empty() && s[0] == ' ') {
|
|
s = s.substr(1);
|
|
}
|
|
boost::trim_right(s);
|
|
if (!s.empty() && (s[0] >= 'a' && s[0] <= 'z' || s[0] >= '0' && s[0] <= '9')) {
|
|
s = "* " + s;
|
|
}
|
|
ofile << s << "\n";
|
|
}
|
|
};
|
|
auto anchor = "patch note:"sv;
|
|
auto anchor_dev = "patch note dev:"sv;
|
|
f(anchor);
|
|
#ifndef NDEBUG
|
|
f(anchor_dev);
|
|
#endif
|
|
}
|
|
ofile.close();
|
|
|
|
// we do not check for presence of 7z command here
|
|
if (has_in_path("7z")) {
|
|
auto ar = get_full_mod_name() + ".zip";
|
|
if (fs::exists(ar)) {
|
|
fs::remove(ar);
|
|
}
|
|
|
|
primitives::Command c;
|
|
c.working_directory = game_dir;
|
|
c.push_back("7z");
|
|
c.push_back("a");
|
|
c.push_back(ar); // we use zip as more common
|
|
for (auto &&f : files_to_distribute) {
|
|
c.push_back(f);
|
|
}
|
|
for (auto &&f : code_files_to_distribute) {
|
|
c.push_back(f);
|
|
}
|
|
run_command(c);
|
|
|
|
auto rename = [&](auto &&from, auto &&to) {
|
|
primitives::Command c;
|
|
c.working_directory = game_dir;
|
|
c.push_back("7z");
|
|
c.push_back("rn");
|
|
c.push_back(ar);
|
|
c.push_back(from);
|
|
c.push_back(to);
|
|
run_command(c);
|
|
};
|
|
for (auto &&f : code_files_to_distribute) {
|
|
if (f.filename() == path{loc.file_name()}.filename()) {
|
|
rename(f.filename(), path{"data"} / "mods" / get_full_mod_name() / get_full_mod_name() += ".cpp");
|
|
} else {
|
|
rename(f.filename(), path{"data"} / "mods" / get_full_mod_name() / f.filename());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
void patch(path fn, uint32_t offset, T val) {
|
|
fn = find_real_filename(fn);
|
|
files_to_pak.insert(fn);
|
|
patch_raw(fn, offset, val);
|
|
}
|
|
// this one checks for old value as well, so incorrect positions (files) won't be patched
|
|
template <typename T>
|
|
bool patch(path fn, uint32_t offset, T oldval, T val) {
|
|
fn = find_real_filename(fn);
|
|
files_to_pak.insert(fn);
|
|
return patch_raw(fn, offset, oldval, val);
|
|
}
|
|
void insert(path fn, uint32_t offset, const byte_array &data) {
|
|
files_to_pak.insert(find_real_filename(fn));
|
|
if (is_already_inserted(fn, data)) {
|
|
return;
|
|
}
|
|
|
|
log("inserting into {} offset 0x{:08X} {} bytes", fn.string(), offset, data.size());
|
|
|
|
auto rfn = find_real_filename(fn);
|
|
fs::resize_file(rfn, fs::file_size(rfn) + data.size());
|
|
primitives::templates2::mmap_file<uint8_t> f{rfn, primitives::templates2::mmap_file<uint8_t>::rw{}};
|
|
memmove(f.p + offset + data.size(), f.p + offset, f.sz - offset);
|
|
::memcpy(f.p + offset, data.data(), data.size());
|
|
f.close();
|
|
|
|
write_file(get_hash_fn(fn, data), ""s);
|
|
}
|
|
void add_map_good(path mmo_fn, const std::string &building_name, const std::string &after_good_name, const byte_array &data) {
|
|
files_to_pak.insert(find_real_filename(mmo_fn));
|
|
if (is_already_inserted(mmo_fn, data)) {
|
|
return;
|
|
}
|
|
|
|
auto fn = find_real_filename(mmo_fn);
|
|
mmo_storage2 m;
|
|
m.load(fn);
|
|
|
|
auto it = m.map_building_goods.find(building_name);
|
|
if (it == m.map_building_goods.end()) {
|
|
throw std::runtime_error{"no such building: "s + building_name};
|
|
}
|
|
|
|
uint32_t insertion_offset = it->second.offset + sizeof(uint32_t);
|
|
if (!after_good_name.empty()) {
|
|
auto it2 = it->second.building_goods.find(after_good_name);
|
|
if (it2 == it->second.building_goods.end()) {
|
|
throw std::runtime_error{"no such building good: "s + after_good_name};
|
|
}
|
|
insertion_offset = it2->second;
|
|
}
|
|
|
|
{
|
|
primitives::templates2::mmap_file<uint8_t> f{fn, primitives::templates2::mmap_file<uint8_t>::rw{}};
|
|
if (::memcmp(f.p + insertion_offset, data.data(), data.size()) == 0) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
insert(mmo_fn, insertion_offset, data);
|
|
|
|
primitives::templates2::mmap_file<uint8_t> f{fn, primitives::templates2::mmap_file<uint8_t>::rw{}};
|
|
// increase section size
|
|
*(uint32_t *)(f.p + m.sections.map_goods.offset) += data.size();
|
|
// increase number of goods
|
|
++*(uint32_t *)(f.p + it->second.offset);
|
|
}
|
|
|
|
// all you need is to provide injection address (virtual) with size
|
|
// handle the call instruction in 'dispatcher' symbol (naked) of your dll
|
|
constexpr static inline auto call_command_length = 5;
|
|
void make_injection(uint32_t virtual_address, uint32_t size = call_command_length) {
|
|
uint32_t len = size;
|
|
if (len < call_command_length) {
|
|
throw std::runtime_error{"jumppad must be 5 bytes atleast"};
|
|
}
|
|
primitives::templates2::mmap_file<uint8_t> f{find_real_filename(aim_exe),
|
|
primitives::templates2::mmap_file<uint8_t>::rw{}};
|
|
auto ptr = f.p + virtual_address - aim_exe_v1_06_constants::our_code_start_virtual + aim_exe_v1_06_constants::trampoline_target_real;
|
|
memcpy(ptr, make_insn_with_address("e8"_bin, aim_exe_v1_06_constants::free_data_base_virtual -
|
|
(virtual_address + call_command_length)));
|
|
memcpy(ptr, make_nops(len - call_command_length));
|
|
log("making injection on the virtual address 0x{:0X} (real address 0x{:0X}), size {}", virtual_address, ptr - f.p,
|
|
size);
|
|
}
|
|
|
|
#define ENABLE_DISABLE_FUNC(name, enable, disable) \
|
|
void enable_##name() { name(enable); } \
|
|
void disable_##name() { name(disable); }
|
|
ENABLE_DISABLE_FUNC(free_camera, 1, 0)
|
|
ENABLE_DISABLE_FUNC(win_key, 0x00, 0x10)
|
|
#undef ENABLE_DISABLE_FUNC
|
|
|
|
void add_code_file_for_archive(const path &fn) {
|
|
code_files_to_distribute.insert(path{loc.file_name()}.parent_path() / fn);
|
|
}
|
|
|
|
private:
|
|
path get_hash_fn(path fn, const byte_array &data) const {
|
|
return get_mod_dir() / std::format("{:0X}.hash", get_insert_hash(fn, data));
|
|
}
|
|
size_t get_insert_hash(path fn, const byte_array &data) const {
|
|
auto s = fn.wstring();
|
|
boost::to_lower(s);
|
|
fn = s;
|
|
|
|
size_t hash{};
|
|
boost::hash_combine(hash, fn);
|
|
boost::hash_combine(hash, data);
|
|
return hash;
|
|
}
|
|
bool is_already_inserted(path fn, const byte_array &data) const {
|
|
return fs::exists(get_hash_fn(fn,data));
|
|
}
|
|
void init(const path &dir) {
|
|
read_name();
|
|
detect_game_dir(dir);
|
|
fs::create_directories(get_mod_dir());
|
|
code_files_to_distribute.insert(loc.file_name());
|
|
db.db_.open(get_data_dir() / "db", primitives::templates2::mmap_file<uint8_t>::rw{});
|
|
db.quest_.open(get_data_dir() / "quest", primitives::templates2::mmap_file<uint8_t>::rw{});
|
|
detect_tools();
|
|
prepare_injections();
|
|
#ifndef NDEBUG
|
|
enable_win_key();
|
|
#endif
|
|
}
|
|
void read_name() {
|
|
if (name.empty()) {
|
|
name = path{loc.file_name()}.stem().string();
|
|
}
|
|
// use regex?
|
|
auto p = name.find('-');
|
|
if (p != -1) {
|
|
version = name.substr(p + 1);
|
|
name = name.substr(0, p);
|
|
}
|
|
}
|
|
decltype(name) get_full_mod_name() const {
|
|
auto s = name;
|
|
if (!version.empty()) {
|
|
s += "-" + version;
|
|
}
|
|
return s;
|
|
}
|
|
static void memcpy(auto &ptr, const byte_array &data) {
|
|
::memcpy(ptr, data.data(), data.size());
|
|
ptr += data.size();
|
|
}
|
|
static auto memmem(auto ptr, auto sz, const byte_array &bytes) {
|
|
sz -= bytes.size();
|
|
for (int i = 0; i < sz; ++i) {
|
|
if (memcmp(ptr + i, bytes.data(), bytes.size()) == 0) {
|
|
return ptr + i;
|
|
}
|
|
}
|
|
throw std::runtime_error{"not found"};
|
|
}
|
|
static auto memreplace(auto base, auto sz, const byte_array &from, const byte_array &to) {
|
|
if (from.size() != to.size()) {
|
|
throw std::runtime_error{"size mismatch"};
|
|
}
|
|
auto ptr = memmem(base, sz, from);
|
|
byte_array old;
|
|
old.resize(from.size());
|
|
::memcpy(old.data(), ptr, old.size());
|
|
::memcpy(ptr, to.data(), to.size());
|
|
return std::tuple{ptr, old};
|
|
}
|
|
static auto make_insn_with_address(auto &&insn, uint32_t addr) {
|
|
byte_array arr(insn.size() + sizeof(addr));
|
|
::memcpy(arr.data(), insn.data(), insn.size());
|
|
*(uint32_t *)(&arr[insn.size()]) = addr;
|
|
return arr;
|
|
}
|
|
static byte_array make_nops(uint32_t len) {
|
|
byte_array arr(len, 0x90);
|
|
return arr;
|
|
}
|
|
void make_injected_dll() {
|
|
log("making injected dll");
|
|
|
|
path fn = loc.file_name();
|
|
//fs::copy_file(fn, get_mod_dir() / fn.filename(), fs::copy_options::overwrite_existing);
|
|
std::string contents;
|
|
contents += "void build(Solution &s) {\n";
|
|
contents += "auto &t = s.addSharedLibrary(\"" + name + "\"";
|
|
if (!version.empty()) {
|
|
contents += ", \"" + version + "\"";
|
|
}
|
|
contents += ");\n";
|
|
contents += "t += cpp23;\n";
|
|
contents += "t += \"" + boost::replace_all_copy(fn.string(), "\\", "/") + "\";\n";
|
|
contents += "t += \"INJECTED_DLL\"_def;\n";
|
|
contents += "}\n";
|
|
write_file(get_mod_dir() / "sw.cpp", contents);
|
|
|
|
primitives::Command c;
|
|
c.working_directory = get_mod_dir();
|
|
c.push_back("sw");
|
|
c.push_back("build");
|
|
c.push_back("-platform");
|
|
c.push_back("x86");
|
|
c.push_back("-config");
|
|
c.push_back("r");
|
|
c.push_back("-config-name");
|
|
c.push_back("r");
|
|
run_command(c);
|
|
|
|
auto dllname = get_mod_dir() / ".sw" / "out" / "r" / get_sw_dll_name();
|
|
fs::copy_file(dllname, game_dir / get_dll_name(), fs::copy_options::overwrite_existing);
|
|
files_to_distribute.insert(get_dll_name());
|
|
}
|
|
decltype(name) get_sw_dll_name() const {
|
|
if (!version.empty()) {
|
|
return get_dll_name();
|
|
}
|
|
return name + "-0.0.1.dll";
|
|
}
|
|
decltype(name) get_dll_name() const {
|
|
return get_full_mod_name() + ".dll";
|
|
}
|
|
uint32_t virtual_to_real(uint32_t v) {
|
|
return v - aim_exe_v1_06_constants::code_base + 0x1000;
|
|
}
|
|
void patch(uint8_t *p, uint32_t off, const byte_array &from, const byte_array &to) {
|
|
if (from.size() != to.size()) {
|
|
throw std::runtime_error{"size mismatch"};
|
|
}
|
|
if (memcmp(p + off, to.data(), to.size()) == 0) {
|
|
return; // ok, already patched
|
|
}
|
|
::memcpy(p + off, to.data(), to.size());
|
|
}
|
|
void prepare_injections() {
|
|
create_backup_exe_file();
|
|
#ifdef NDEBUG
|
|
make_injected_dll();
|
|
#endif
|
|
files_to_distribute.insert(aim_exe);
|
|
primitives::templates2::mmap_file<uint8_t> f{find_real_filename(aim_exe), primitives::templates2::mmap_file<uint8_t>::rw{}};
|
|
uint32_t our_data = aim_exe_v1_06_constants::our_code_start_virtual;
|
|
|
|
auto ptr = f.p + aim_exe_v1_06_constants::trampoline_target_real;
|
|
|
|
auto strcpy = [&](const std::string &s) {
|
|
::strcpy((char *)ptr, s.c_str());
|
|
ptr += s.size() + 1;
|
|
our_data += s.size() + 1;
|
|
};
|
|
|
|
auto push_dll_name = make_insn_with_address("68"_bin, our_data); // push
|
|
#ifdef NDEBUG
|
|
strcpy(get_sw_dll_name());
|
|
#else
|
|
strcpy("h:\\Games\\AIM\\1\\.sw\\out\\d\\aim_fixes-0.0.1.dll"s);
|
|
#endif
|
|
const auto jumppad = "68 30 B8 51 00"_bin; // push offset SEH_425100
|
|
uint32_t jump_offset = ptr - f.p - aim_exe_v1_06_constants::trampoline_base_real - jumppad.size() * 2;
|
|
patch(f.p, virtual_to_real(0x00425105), jumppad, make_insn_with_address("e9"_bin, jump_offset));
|
|
memcpy(ptr, jumppad); // put our removed insn
|
|
memcpy(ptr, R"(
|
|
60 ; pusha
|
|
)"_bin);
|
|
memcpy(ptr, push_dll_name);
|
|
memcpy(ptr, R"(
|
|
8B 3D D8 10 52 00 ; mov edi, ds:LoadLibraryA
|
|
FF D7 ; call edi
|
|
61 ; popa
|
|
)"_bin);
|
|
memcpy(ptr, make_insn_with_address("e9"_bin, -(ptr - f.p - aim_exe_v1_06_constants::trampoline_base_real - jumppad.size())));
|
|
}
|
|
path find_real_filename(path fn) const {
|
|
auto s = fn.wstring();
|
|
boost::to_lower(s);
|
|
fn = s;
|
|
if (fs::exists(fn)) {
|
|
return fn;
|
|
}
|
|
if (fn == aim_exe) {
|
|
return game_dir / fn;
|
|
}
|
|
|
|
auto ft = check_file_type(fn);
|
|
switch (ft) {
|
|
case file_type::script: {
|
|
auto p = get_data_dir() / "scripts.pak";
|
|
unpak(p);
|
|
p = make_unpak_dir(p);
|
|
if (!fs::exists(p / fn)) {
|
|
p = p / "Script" / "bin" / fn.filename();
|
|
} else {
|
|
p /= fn;
|
|
}
|
|
if (!fs::exists(p)) {
|
|
throw SW_RUNTIME_ERROR("Cannot find file in archives: "s + fn.string());
|
|
}
|
|
return p;
|
|
}
|
|
case file_type::mmo: {
|
|
auto p = get_data_dir() / "maps2.pak";
|
|
if (fs::exists(p) && (false
|
|
// it contains only these
|
|
|| fn == "location3.mmo"
|
|
|| fn == "location4.mmo"
|
|
|| fn == "location5.mmo"
|
|
)) {
|
|
unpak(p, fn);
|
|
p = make_unpak_dir(p);
|
|
if (!fs::exists(p / fn)) {
|
|
p = p / fn.filename();
|
|
} else {
|
|
p /= fn;
|
|
}
|
|
if (fs::exists(p)) {
|
|
auto dst = get_mod_dir() / p.filename();
|
|
if (!fs::exists(dst)) {
|
|
fs::copy_file(p, dst);
|
|
}
|
|
return dst;
|
|
}
|
|
}
|
|
p = get_data_dir() / "maps.pak";
|
|
unpak(p
|
|
// takes too long to extract specific files
|
|
// maybe speedup the unpaker
|
|
//, fn
|
|
);
|
|
p = make_unpak_dir(p);
|
|
if (!fs::exists(p / fn)) {
|
|
p = p / fn.filename();
|
|
} else {
|
|
p /= fn;
|
|
}
|
|
if (!fs::exists(p)) {
|
|
throw SW_RUNTIME_ERROR("Cannot find file in archives: "s + fn.string());
|
|
}
|
|
auto dst = get_mod_dir() / p.filename();
|
|
if (!fs::exists(dst)) {
|
|
fs::copy_file(p, dst);
|
|
}
|
|
return dst;
|
|
}
|
|
default:
|
|
SW_UNIMPLEMENTED;
|
|
}
|
|
}
|
|
// from https://github.com/Solant/aim-patches
|
|
void free_camera(uint8_t val) {
|
|
create_backup_exe_file();
|
|
patch(aim_exe, 0x1F805, val);
|
|
}
|
|
void win_key(uint8_t val) {
|
|
create_backup_exe_file();
|
|
patch(aim_exe, 0x4A40D, val);
|
|
}
|
|
void create_backup_exe_file() {
|
|
auto fn = find_real_filename(aim_exe);
|
|
auto backup = path{fn} += ".orig";
|
|
if (!fs::exists(backup)) {
|
|
fs::copy_file(fn, backup);
|
|
}
|
|
}
|
|
template <typename T>
|
|
void patch_raw(path fn, uint32_t offset, T val) const {
|
|
primitives::templates2::mmap_file<uint8_t> f{fn, primitives::templates2::mmap_file<uint8_t>::rw{}};
|
|
auto &old = *(T *)(f.p + offset);
|
|
log("patching {} offset 0x{:08X} to {} (old value: {})", fn.string(), offset, val, old);
|
|
old = val;
|
|
}
|
|
template <typename T>
|
|
bool patch_raw(path fn, uint32_t offset, T expected, T val) const {
|
|
primitives::templates2::mmap_file<uint8_t> f{fn, primitives::templates2::mmap_file<uint8_t>::rw{}};
|
|
auto &old = *(T *)(f.p + offset);
|
|
log("patching {} offset 0x{:08X} from {} to {}", fn.string(), offset, expected, val);
|
|
if (old == expected) {
|
|
log("success");
|
|
old = val;
|
|
return true;
|
|
} else if (old == val) {
|
|
log("success, already patched");
|
|
return true;
|
|
} else {
|
|
log("old value {} != expected {}", old, expected);
|
|
return false;
|
|
}
|
|
}
|
|
path get_mod_dir() const {
|
|
return get_data_dir() / "mods" / get_full_mod_name();
|
|
}
|
|
path get_data_dir() const {
|
|
return game_dir / "data";
|
|
}
|
|
static void replace_in_file_raw(const path &fn, const std::string &from, const std::string &to) {
|
|
log("replacing in file {} from '{}' to '{}'", fn.string(), from, to);
|
|
auto f = read_file(fn);
|
|
boost::replace_all(f, from, to);
|
|
boost::replace_all(f, "\r", "");
|
|
write_file_if_different(fn, f);
|
|
}
|
|
static path make_unpak_dir(path p) {
|
|
p += ".dir";
|
|
return p;
|
|
}
|
|
static path make_script_txt_fn(path p) {
|
|
p += ".txt";
|
|
return p;
|
|
}
|
|
void unpak(const path &p, const path &fn = {}) const {
|
|
auto udir = make_unpak_dir(p);
|
|
if (fs::exists(udir) && (fn.empty() || fs::exists(udir / fn))) {
|
|
return;
|
|
}
|
|
if (fn.empty()) {
|
|
run_p4_tool("unpaker", p);
|
|
} else {
|
|
run_p4_tool("unpaker", p, fn);
|
|
}
|
|
}
|
|
void run_p4_tool(const std::string &tool, auto && ... args) const {
|
|
run_sw("pub.lzwdgc.Polygon4.Tools."s + tool + "-master", args...);
|
|
}
|
|
void run_sw(auto &&...args) const {
|
|
primitives::Command c;
|
|
c.working_directory = get_mod_dir();
|
|
fs::create_directories(c.working_directory);
|
|
c.push_back("sw");
|
|
c.push_back("run");
|
|
(c.push_back(args),...);
|
|
run_command(c);
|
|
}
|
|
static void run_command(auto &c) {
|
|
c.out.inherit = true;
|
|
c.err.inherit = true;
|
|
log(c.print());
|
|
c.execute();
|
|
}
|
|
void detect_game_dir(const path &dir) {
|
|
const auto aim1_exe = aim_exe;
|
|
if (fs::exists(dir / aim1_exe)) {
|
|
game_dir = dir;
|
|
} else if (fs::exists(dir.parent_path() / aim1_exe)) {
|
|
game_dir = dir.parent_path();
|
|
} else {
|
|
throw SW_RUNTIME_ERROR("Cannot detect aim1 game dir.");
|
|
}
|
|
game_dir = fs::absolute(game_dir).lexically_normal();
|
|
}
|
|
void detect_tools() {
|
|
//check_in_path("git");
|
|
// also --self-upgrade?
|
|
check_in_path("sw");
|
|
}
|
|
void check_in_path(const path &program) const {
|
|
if (!has_in_path(program)) {
|
|
throw SW_RUNTIME_ERROR("Cannot find "s + program.string() + " in PATH.");
|
|
}
|
|
}
|
|
static bool has_in_path(const path &program) {
|
|
return !primitives::resolve_executable(program).empty();
|
|
}
|
|
static file_type detect_file_type(const path &fn) {
|
|
auto ext = fn.extension().string();
|
|
boost::to_lower(ext);
|
|
if (ext.empty()) {
|
|
return file_type::model;
|
|
} else if (ext == ".mmp") {
|
|
return file_type::mmp;
|
|
} else if (ext == ".mmo") {
|
|
return file_type::mmo;
|
|
} else if (ext == ".mmm") {
|
|
return file_type::mmm;
|
|
} else if (ext == ".scr" || ext == ".qst") {
|
|
return file_type::script;
|
|
} else if (ext == ".tm") {
|
|
return file_type::tm;
|
|
} else if (ext == ".ogg") {
|
|
return file_type::sound;
|
|
}
|
|
return file_type::unknown;
|
|
}
|
|
file_type check_file_type(const path &fn) const {
|
|
auto t = detect_file_type(fn);
|
|
if (t == file_type::unknown) {
|
|
throw SW_RUNTIME_ERROR("Unknown file type: "s + fn.string());
|
|
}
|
|
return t;
|
|
}
|
|
};
|