Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
466 changes: 466 additions & 0 deletions .github/workflows/_build.yml

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: CI

on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read
actions: write

jobs:
build:
uses: ./.github/workflows/_build.yml
with:
publish_mode: artifact
artifact_name: reeot-${{ github.sha }}
secrets: inherit
17 changes: 17 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write
actions: write

jobs:
build:
uses: ./.github/workflows/_build.yml
with:
publish_mode: release
secrets: inherit
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ vcpkg_installed/
# test output & cache
Testing/
.cache/
/ref_installer
SDKWIKI.md
game_iso/Spider-Man - Edge of Time (USA, Europe).iso
game_iso/Vimm's Lair.txt
game_tu/TU_10LC25I_0000008000000.0000000000082
game_dlc/564E216B8B02133E5F0FFB3D2E3CD412CBA2D0FF41
15 changes: 15 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[submodule "res"]
path = res
url = https://github.com/goliath-tilers/reeot-resources.git
[submodule "thirdparty/nativefiledialog-extended"]
path = thirdparty/nativefiledialog-extended
url = https://github.com/btzy/nativefiledialog-extended.git
[submodule "thirdparty/ddspp"]
path = thirdparty/ddspp
url = https://github.com/redorav/ddspp.git
[submodule "thirdparty/libmspack"]
path = thirdparty/libmspack
url = https://github.com/kyz/libmspack
[submodule "thirdparty/tiny-AES-c"]
path = thirdparty/tiny-AES-c
url = https://github.com/kokke/tiny-AES-c.git
44 changes: 42 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,46 @@
# SDK boilerplate lives in generated/rexglue.cmake.

cmake_minimum_required(VERSION 3.25)
project(reeot LANGUAGES CXX)
if(APPLE)
project(reeot LANGUAGES C CXX OBJC)
else()
project(reeot LANGUAGES C CXX)
endif()

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(generated/rexglue.cmake)

set(NFD_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(NFD_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(thirdparty/nativefiledialog-extended)
add_library(ddspp INTERFACE)
target_include_directories(ddspp INTERFACE thirdparty/ddspp)

add_library(xenon_patcher STATIC
thirdparty/xenon_utils/xex_patcher.cpp
thirdparty/xenon_utils/memory_mapped_file.cpp
thirdparty/libmspack/libmspack/mspack/lzxd.c
thirdparty/tiny-AES-c/aes.c
)
target_include_directories(xenon_patcher PUBLIC
thirdparty/xenon_utils
thirdparty/libmspack/libmspack/mspack
thirdparty/tiny-AES-c
thirdparty/TinySHA1
)

# Sources
set(REEOT_SOURCES
src/main.cpp
src/goliath_engine/common/cvars.cpp
src/goliath_engine/common/misc.cpp # Eventually merge this into cvars.cpp
src/goliath_engine/installer/installer_dialog.cpp
src/goliath_engine/installer/install_manifest.cpp
src/goliath_engine/installer/install_paths.cpp
src/goliath_engine/installer/installer.cpp
src/goliath_engine/installer/source_vfs.cpp
)

if(WIN32)
Expand All @@ -21,4 +51,14 @@ else()
add_executable(reeot ${REEOT_SOURCES})
endif()

rexglue_setup_target(reeot)
rexglue_setup_target(reeot GPU_PLUGINS xenos)
target_link_libraries(reeot PRIVATE nfd::nfd ddspp xenon_patcher)

if(APPLE AND TARGET reeot_GameLogic)
add_custom_command(TARGET reeot_GameLogic POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
"$<TARGET_FILE_NAME:reeot_GameLogic>"
"$<TARGET_FILE_DIR:reeot_GameLogic>/reeot_GameLogic"
COMMENT "Creating macOS dlopen alias for reeot_GameLogic"
)
endif()
9 changes: 6 additions & 3 deletions reeot_default_xex.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
# Run codegen with the manifest: rexglue codegen reeot_manifest.toml

project_name = "reeot"
file_path = "assets/default.xex"
out_directory_path = "generated/default_xex"
file_path = "assets/Default.xex"
patch_file_path = "assets/Update/Default.xexp"
patched_file_path = "assets/Default_Patched.xex"
out_directory_path = "generated/default"

[rexcrt]
# Heap
Expand Down Expand Up @@ -1823,6 +1825,7 @@ strcpy_s = 0x8233E498
0x82342C18 = { name = "eot_Thread_GetTLS_OrDefault" }
0x822471C8 = { name = "eot_Thread_SetAffinity" }
0x8224B8E0 = { name = "eot_Thread_SleepMs" }
0x82247460 = { name = "eot_Sleep" }
0x8233BEA8 = { name = "eot_threadstartex" }
0x82247388 = { name = "eot_WaitForSingleObject" }
0x8224B688 = { name = "eot_WaitForSingleObjectEx" }
Expand Down Expand Up @@ -2741,4 +2744,4 @@ strcpy_s = 0x8233E498
0x8229c6b0 = { name = "XGUntileVolumeTextureLevel" }

0x8238D9A8 = { size = 0x4D8 }
0x8238DE80 = { size = 0x86C }
0x8238DE80 = { size = 0x86C }
5 changes: 5 additions & 0 deletions reeot_gamelogic.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

project_name = "reeot"
file_path = "assets/Data/GameLogic.dll"
patch_file_path = "assets/Update/Data/GameLogic.dllp"
patched_file_path = "assets/Data/GameLogic_Patched.dll"
out_directory_path = "generated/gamelogic"
is_dll = true

# [rexcrt]
# Heap
Expand All @@ -32,6 +35,8 @@ out_directory_path = "generated/gamelogic"
0x882F3338 = { size = 0x10}
0x88260CC8 = {}
0x88120340 = {}
0x882F3388 = {}
0x8821FB30 = {}

# IDA Generated
0x880A0040 = { name = "eot_GameLogic_0001", size = 0x6EF0 }
Expand Down
6 changes: 5 additions & 1 deletion reeot_manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ game_root = "assets"

[entrypoint]
file_path = "assets/Default.xex"
patch_file_path = "assets/Update/Default.xexp"
patched_file_path = "assets/Default_Patched.xex"
out_directory_path = "generated/default"
includes = ["reeot_default_xex.toml"]

[[modules]]
guest_path = "data/gamelogic.dll"
guest_path = "Data/GameLogic.dll"
file_path = "assets/Data/GameLogic.dll"
patch_file_path = "assets/Update/Data/GameLogic.dllp"
patched_file_path = "assets/Data/GameLogic_Patched.dll"
out_directory_path = "generated/gamelogic"
includes = ["reeot_gamelogic.toml"]
1 change: 1 addition & 0 deletions res
Submodule res added at 041c4a
158 changes: 158 additions & 0 deletions src/goliath_engine/common/crash_handler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* @file goliath_engine/common/crash_handler.cpp
* @brief See header.
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
* @edit Modifications Copyright (c) 2026 Rien Gupta <rgupta9@scu.edu>
* All rights reserved.
*/
#include "goliath_engine/common/crash_handler.h"

#include <atomic>
#include <cstdint>

#include <rex/exception_handler.h>
#include <rex/logging.h>
#include <rex/runtime.h>
#include <rex/version.h>

#include "goliath_engine/common/logging.h"

// Linker-provided base of THIS module (reblue.exe). Its ADDRESS is the post-ASLR
// load base; using it avoids including Windows.h (project rule) while letting us
// reduce a faulting host PC to an RVA that the .map/.pdb resolves offline.
extern "C" const unsigned char __ImageBase;

// Exported by kernel32/ntdll. Declared locally to avoid pulling in Windows.h; x64
// has a single calling convention so __stdcall is a no-op there.
extern "C" __declspec(dllimport) unsigned short __stdcall RtlCaptureStackBackTrace(
unsigned long frames_to_skip, unsigned long frames_to_capture, void** back_trace,
unsigned long* back_trace_hash);

namespace eot {
namespace {

// Guest virtual address space is the low 4 GB of the host mapping.
constexpr uint64_t kGuestAddressSpaceSize = 0x100000000ull;

// Generous upper bound on the reblue.exe image span (it embeds all recompiled guest
// code, so it is large). Used only to decide whether a host address reduces to an
// in-module RVA; oversizing it never misclassifies an in-module address.
constexpr uint64_t kHostImageSpan = 0x20000000ull; // 512 MiB

uint64_t HostModuleBase() { return reinterpret_cast<uint64_t>(&__ImageBase); }

const char* ExceptionCodeName(rex::arch::Exception::Code code) {
using Code = rex::arch::Exception::Code;
switch (code) {
case Code::kAccessViolation: return "ACCESS_VIOLATION";
case Code::kIllegalInstruction: return "ILLEGAL_INSTRUCTION";
default: return "UNKNOWN";
}
}

const char* AvOperationName(rex::arch::Exception::AccessViolationOperation op) {
using Op = rex::arch::Exception::AccessViolationOperation;
switch (op) {
case Op::kRead: return "read";
case Op::kWrite: return "write";
default: return "unknown";
}
}

// Walk the faulting thread's stack (the VEH runs inline on it) and log each return
// address with its reblue.exe-relative RVA. The faulting PC is already logged
// separately; this gives the call chain that led there. The top frames are the
// crash handler itself - scan for the first reblue.exe+RVA below the OS dispatch
// frames to find the faulting caller.
void LogBacktrace(uint64_t base) {
void* frames[32] = {};
const unsigned short n = RtlCaptureStackBackTrace(0, 32, frames, nullptr);
if (!n) return;
EOT_CRITICAL("backtrace ({} frames; top frames are the crash handler):", n);
for (unsigned short i = 0; i < n; ++i) {
const uint64_t a = reinterpret_cast<uint64_t>(frames[i]);
if (a >= base && a - base < kHostImageSpan) {
EOT_CRITICAL(" [{:>2}] {:#018x} (reblue.exe+{:#010x})", i, a, a - base);
} else {
EOT_CRITICAL(" [{:>2}] {:#018x}", i, a);
}
}
}

// Read the register file straight off the public HostThreadContext members -
// the SDK's GetRegisterName/GetStringFromValue helpers are header-declared but
// not exported by the runtime lib, so format the values here instead.
void LogRegisters(const rex::arch::HostThreadContext* ctx) {
if (!ctx) return;
#if REX_ARCH_AMD64
static const char* const kNames[16] = {
"rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi",
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"};
EOT_CRITICAL(" rip = {:#018x} eflags = {:#010x}", ctx->rip, ctx->eflags);
for (int i = 0; i < 16; ++i) {
EOT_CRITICAL(" {:>3} = {:#018x}", kNames[i], ctx->int_registers[i]);
}
#elif REX_ARCH_ARM64
EOT_CRITICAL(" pc = {:#018x} sp = {:#018x} pstate = {:#010x}", ctx->pc,
ctx->sp, ctx->pstate);
for (int i = 0; i < 31; ++i) {
EOT_CRITICAL(" x{:<2} = {:#018x}", i, ctx->x[i]);
}
#endif
}

// Chained after the SDK MMIO handler; only sees faults the runtime declined.
// Returns false: never recover, let the OS produce its default termination.
bool CrashHandler(rex::arch::Exception* ex, void* /*data*/) {
// A fault inside the handler must not loop; bail straight to the OS.
static std::atomic_flag s_in_handler = ATOMIC_FLAG_INIT;
if (s_in_handler.test_and_set(std::memory_order_acq_rel)) return false;

const uint64_t host_base = HostModuleBase();

EOT_CRITICAL("================ reblue host crash ================");
EOT_CRITICAL("build: {}", REXGLUE_BUILD_TITLE);
EOT_CRITICAL("module base: reblue.exe @ {:#018x}", host_base);
EOT_CRITICAL("exception: {} @ host pc {:#018x}",
ExceptionCodeName(ex->code()), ex->pc());
if (ex->pc() >= host_base && ex->pc() - host_base < kHostImageSpan) {
EOT_CRITICAL("faulting RVA: reblue.exe+{:#010x}", ex->pc() - host_base);
}

if (ex->code() == rex::arch::Exception::Code::kAccessViolation) {
const uint64_t fa = ex->fault_address();
EOT_CRITICAL("fault address: {:#018x} ({})", fa,
AvOperationName(ex->access_violation_operation()));

// Map a guest-range fault back to its guest VA - the actionable triage line
// for a fault inside recompiled code.
auto* rt = rex::Runtime::instance();
uint8_t* membase = rt ? rt->virtual_membase() : nullptr;
const uint64_t base = reinterpret_cast<uint64_t>(membase);
if (membase && fa >= base && fa < base + kGuestAddressSpaceSize) {
EOT_CRITICAL("guest fault VA: {:#010x}", static_cast<uint32_t>(fa - base));
}
}

EOT_CRITICAL("registers:");
LogRegisters(ex->thread_context());
LogBacktrace(host_base);
EOT_CRITICAL("===================================================");

// Drain sinks before the OS tears us down; a crash bypasses OnShutdown.
rex::FlushLogging();
return false;
}

} // namespace

void InstallCrashHandler() {
rex::arch::ExceptionHandler::Install(&CrashHandler, nullptr);
EOT_INFO("[crash] last-chance handler installed");
}

} // namespace eot
24 changes: 24 additions & 0 deletions src/goliath_engine/common/crash_handler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @file goliath_engine/common/crash_handler.h
* @brief Last-chance host crash logger: dumps the faulting PC, fault address,
* guest VA, and register state to the eot log before the process dies,
* so crashes in the wild are triageable.
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
* @edit Modifications Copyright (c) 2026 Rien Gupta <rgupta9@scu.edu>
* All rights reserved.
*/
#pragma once

namespace eot {

// Install the last-chance host exception handler. Call once, after the Runtime
// and logging are up: it chains *after* the SDK's guest-MMIO handler, so it only
// fires on faults the runtime did not resolve (i.e. real crashes), logs them via
// EOT_CRITICAL, flushes the log, and returns control to the OS for termination.
void InstallCrashHandler();

} // namespace eot
Loading
Loading