Compare commits

..

No commits in common. "31ff800b2afcaf5884d463430f02108ae649ed9b" and "7834b98bf002131add0fca0898c7266e249330c2" have entirely different histories.

20 changed files with 590 additions and 1456 deletions

2
.gitignore vendored
View file

@ -8,3 +8,5 @@ ghidra
# Licence-restricted vendor material, local reference only (see third-party/README.md)
third-party/_local/
# Meson build output
src/build/

View file

@ -45,6 +45,7 @@ code.
| `schema/` | SQL Server database schema/log files (`.mdf` / `.ldf`) for `RustyHearts`, `RustyHearts_Account`, `RustyHearts_Auth`, `RustyHearts_Log`, `GMRustyHearts` | **Read-only.** Never modify. Gitignored. |
| `decomp/` | Raw decompilation objects exported from Ghidra/REA (pseudocode, listings, function metadata) | Derived output. Regenerable; created on demand. |
| `.agents/skills/` | Installed agent skills, notably `reverse-engineer-anything` (REA) | Agent configuration. |
| `src/` | Linux C++ reconstructions of analysed binaries (Meson; one sub-folder per binary, e.g. `agent_release/`). Build: `cd src && meson setup build && ninja -C build` | Hand-written; cite decomp addresses; `src/build/` is gitignored. |
| `docs/` | Project documentation, e.g. `ARCHITECTURE.md` (language/toolchain, libraries, code conventions) | Hand-maintained; update as findings are confirmed. |
| `third-party/` | Upstream sources/headers matched to the shipped support DLLs (OpenSSL 1.0.0d, curl 7.21.4, BugTrap ~1.3, mingw-w64 `dbghelp.h`, TinyXML 2.5.x) | Reference only, not build inputs. Do not edit upstream files; record provenance changes in the doc below. |

View file

@ -16,8 +16,6 @@ per class (e.g. `MagiAgentNetwork`, `MGAgentManagerSession`) indicate multiple
inheritance. Statically linked C++ libraries: ProudNet, TinyXML (`TiXml*`), ATL.
## Outputs
Porting spec (Win32 → Linux substitutions, behaviour to reproduce, original bugs): `porting.md`.
| Path | Content |
|---|---|
| `full/functions/<addr>.c` | Ghidra pseudocode for all 6,423 non-thunk functions (1 failed; see `full/index.tsv`). Includes library code (ProudNet, TinyXML, CRT) — not yet labelled. |

View file

@ -1,55 +0,0 @@
# Agent_Release_x64.exe — porting spec (Win32 → Linux)
This was kept from the removed Linux reconstruction `src/agent_release/`
(removed 2026-10-15). It records the facts about the original binary that a
re-implementation needs, and the nearest Linux equivalent for each Win32 call.
Sources: `evidence.md`, `cpp/AgentProcessControl.cpp`. Names are inferred;
the behaviour is static-analysis evidence only.
## Behaviour to reproduce
The servers being managed are read from config (`option\*.xml`, mapping not
yet recovered).
| Item | Behaviour |
|---|---|
| Single instance (`0x1400100b0`) | Named mutex `Global\RustyHearts_Agent` |
| Launch (`0x1400094e0`) | `CreateProcessW(NULL, "<exe> <arg>", …, CREATE_NEW_CONSOLE, …, workDir)`, console 700×110, fill attribute 0x17. Command line is limited to 260 characters (`MAX_PATH`). Returns the pid, or 0 on failure (logs `=======Server Fail=======`) |
| After each launch attempt | `Sleep(1000)` |
| Command line (`0x140008ea0`) | `<exe for server type> <code>`. An alternative argument builder `FUN_140027540` exists, but its output is unknown |
| Watcher (`0x140008c90`) | Polls `GetExitCodeProcess` every 10 ms. On exit it sends two RMIs to AgentManager, then relaunches if the per-server revive flag (+0xd5) or the global flag (+0x171) is set |
| Kill (`0x140009780`) | Toolhelp32 snapshot, case-insensitive image-name match, terminates the **first** match only (`TerminateProcess(h,0)`) |
| Main loop (`0x1400037a0`) | 100 ms tick, 1 s FPS counter. Optionally auto-starts servers after a configured delay (+0x178). At startup it kills a stale process of type 0x18 (identity unknown) |
## Win32 → Linux substitutions
| Original (address) | Win32 | Linux replacement | Behaviour difference |
|---|---|---|---|
| Single instance (`0x1400100b0`) | `CreateMutexW("Global\\RustyHearts_Agent")` | `flock(LOCK_EX\|LOCK_NB)` on `/tmp/RustyHearts_Agent.lock` | Lock file instead of a kernel object; still released on exit or crash |
| Launch (`0x1400094e0`) | `CreateProcessW(…, CREATE_NEW_CONSOLE, …, workDir)` + `STARTUPINFOW` console geometry | `posix_spawnp` + `POSIX_SPAWN_SETSID` + `posix_spawn_file_actions_addchdir_np` (glibc ≥ 2.29) | No console window; the command line is split on whitespace (no Windows quoting rules) |
| Watcher (`0x140008c90`) | `GetExitCodeProcess` polled every 10 ms | blocking `waitpid` on a detached thread | No polling. Reaping fixes the original handle leak; passing the pid by value fixes the original stack-pointer race |
| Kill (`0x140009780`) | Toolhelp32 + `OpenProcess` + `TerminateProcess(h,0)` | scan `/proc/*/cmdline` for the argv[0] basename with `strcasecmp`, then `kill(SIGKILL)` | Same rule: first match only, case-insensitive |
| Main-loop timer | `QueryPerformanceCounter` (`TimeSourceHRWin32`) | `std::chrono::steady_clock` | — |
| Thread start | `_beginthreadex` | `std::thread` | — |
| `Sleep` | `Sleep` | `std::this_thread::sleep_for` | — |
| Shutdown | console menu (`FUN_140001dc0`, not analysed) | `SIGINT` / `SIGTERM` | Different trigger |
## Original bugs (O)
- **Process handle leak:** the watcher never closes the process handle.
- **Stack-pointer race:** `LaunchProcess` hands the watcher a pointer to a
stack `PROCESS_INFORMATION`. It is only safe because of the
`Sleep(1000)` that follows.
- **Snapshot handle leak:** `KillProcessByName` leaks the snapshot handle on
the success path.
## Unknown / not recovered
- **ProudNet RMI to AgentManager and the game servers:** proprietary
transport. Wire-incompatible with ProudNet 1.8 (see the ProudNet section of
`third-party/README.md`). The proxy vtable slots used are `+0x40` and `+0x50`.
- **XML config field mapping** for the Agent.
- **`FUN_140027540` argument builder; the `type == 1` exit follow-up
(`FUN_140009f20`); the type 0x18 process killed at startup.**
- **Locking around the server table:** not visible in the analysed code.

View file

@ -13,8 +13,6 @@ Shared conclusions are in `docs/DATABASE.md` and `docs/CONFIG.md`. This ledger
holds the binary-specific evidence.
## Outputs
Porting spec (Win32 → Linux substitutions, behaviour to reproduce, original bugs): `porting.md`.
| Path | Content |
|---|---|
| `full/functions/*.c`, `full/index.tsv` | 20,334 functions. 2 failed: `0x14000e4d0` (SERVERS2DBC stub dispatcher, timeout) and `0x1400a8190` (worker command dispatcher, flow limit) |
@ -33,7 +31,7 @@ Porting spec (Win32 → Linux substitutions, behaviour to reproduce, original bu
| 3 | The worker dispatches commands 1001–1317 through a jump table to `DBReq*` methods | O (disassembly) | `0x1400a8190`, `cmd_dispatch.tsv` |
| 4 | Each `DBReq*` runs one stored procedure through `ADODB.Command` with named parameters, and maps recordset fields by name | O | `0x1400acbb0` (`up_create_character`) |
| 5 | Four connection types: 1 WEB(`db_web`), 2 AUTH(`db_account`), 3 GAME(`db_game`), 4 GAME LOG(`db_game_log`). AUTH uses the WEB timeout (bug). Timeout 0 → 500. String length ≤255 | O | `0x1400a7aa0` (REA `ev_00545a3e…`) |
| 6 | Startup opens AUTH and GAME connections, allocates 1000-slot write-queue arrays (125 iterations × 8 pointers), and logs connect failures to `Error_ADO DB Connect.Log` | O | `0x140042cf0`, `0x1400a80c0` |
| 6 | Startup opens AUTH and GAME connections, allocates 125-slot write-queue arrays, and logs connect failures to `Error_ADO DB Connect.Log` | O | `0x140042cf0`, `0x1400a80c0` |
| 7 | 210 of 262 RMIs map to a procedure. 7 of those procedures are missing from the shipped schema | O + D | `rmi_to_proc.tsv` |
| 8 | Original source layout: `DBLobbyWriteQuery<Area>.cpp` ×28 plus `DBWriteQueryServerOption.cpp` | O (`__FILE__` strings) | `dbreq_methods.tsv` |
| 9 | The option-loader layout equals MessageServer's (same offsets for every element) | O | `0x140156650`, `0x140155420` |

View file

@ -1,91 +0,0 @@
# DBCServer_Release_x64.exe — porting spec (Win32 → Linux)
This was kept from the removed Linux reconstruction `src/dbc_server/`
(removed 2026-10-15). It records the facts about the original binary that a
re-implementation needs, and the nearest Linux equivalent for each Win32 call.
Sources: `evidence.md`, `docs/DATABASE.md`, `docs/CONFIG.md`. Static analysis
only.
## Structure to reproduce
```
peer ──SERVERS2DBC RMI {rmi_id, …}──▶ stub handler (e.g. 0x140050d20: ++per-RMI counter)
│ push {size, cmd 1001–1317, args}
▼
write queue [idx < 1000] (0x140144730; else "Invalid Queue Index")
│ critical section + semaphore, 1 worker per queue
▼
worker switch (cmd - 1001) (0x1400a8190, jump table 0x1400a9dd4)
▼
CDBLobbyWriteQuery::DBReq* (e.g. 0x1400acbb0) — ADODB.Command "dbo.up_xxx"
▼
reply via DBC2SERVERS proxy (76 RMIs, 0x2af9–0x2b46)
```
- **Data tables:**
- The RMI → command → procedure mapping is in `rmi_to_proc.tsv`, and the
command dispatch in `cmd_dispatch.tsv`.
- Procedure parameters (names, SQL types, OUTPUT flags) are in
`decomp/schema/<db>/routines.tsv`.
- `decomp/scripts/gen_dbc_tables.py` joins them into `procs_used.tsv`.
- **Queue array:** a 0x1f48-byte block holding 1000 pointers. How queue slots
are created has not been decoded.
- **Connections (`0x1400a7aa0`):**
- Type 1 WEB = `db_web`, 2 AUTH = `db_account`, 3 GAME = `db_game`,
4 GAME LOG = `db_game_log`.
- A timeout of 0 becomes 500.
- A string longer than 255 characters gives `Error … String Max.!`.
- **AUTH uses the WEB timeout** (+0x134); this is an original bug.
- **Startup (`0x140042cf0`):** opens AUTH and GAME connections, then
`up_read_world_server` reads the world list.
- **Connect failure (`0x1400a80c0`):**
`DB Connect failed\n Connection String = %s` is appended to
`Error_ADO DB Connect.Log`. The original writes the password verbatim.
- **Procedure error:** `ADO_ON_ERROR : [ %s ] Proc [ %s ]` with source file
and line (catch funclets, `dbreq_methods.tsv`).
- **Single instance:** mutex `Global\RustyHearts_DBC`.
- **Startup modes:** a numeric `argv[1]` selects agent mode (`total_option.xml`
`<db><game>`); otherwise `argv[1]` is the path of the per-server XML
(`docs/CONFIG.md`).
## Win32 → Linux substitutions
| Original (address) | Win32 / library | Nearest Linux equivalent | Notes |
|---|---|---|---|
| Single instance | `CreateMutexW("Global\\RustyHearts_DBC")` | `flock(LOCK_EX\|LOCK_NB)` on `$TMPDIR/RustyHearts_DBC.lock` | Lock file instead of a kernel object |
| DB access (`0x1400a7aa0`, `0x1400a80c0`, `DBReq*`) | ADO `_Connection` / `ADODB.Command` over `SQLOLEDB`, `CoInitialize`, `_com_issue_errorex` | ODBC: unixODBC + Microsoft ODBC Driver 18, or FreeTDS `libtdsodbc` | Call as `{? = call dbo.proc(?,…)}` with positional parameters (ADO uses named ones); GUIDs in SQL Server byte order. ODBC 18 needs `TrustServerCertificate=yes`, because SQLOLEDB never validated TLS. FreeTDS needs `Port=` (not `host,port`) and `TDS_Version=7.4` |
| ADO connection string | `Provider=SQLOLEDB;Data Source;Initial Catalog;User ID;Password` | `Driver={…};Server;Database;UID;PWD` | Keys map 1:1. Treating the XML `timeout` as milliseconds (rounded to seconds) is an inference |
| Transport | ProudNet `CNetServer` over Winsock2/MSWSOCK/IOCP | BSD sockets / epoll | ProudNet internal net version is **196978**; 1.8 is 197144, so the protocols are incompatible. Interop would need a reimplementation of the 2013 protocol (`third-party/README.md`) |
| Write queue (`0x140144730`) | `CRITICAL_SECTION` + semaphore (`ReleaseSemaphore`) | `std::mutex` + `std::condition_variable` | — |
| Threads | `_beginthreadex` | `std::thread` | — |
| XML (`MagiServerOption`, `MagiCustomOption`) | TinyXML 2.5.x | pugixml with `parse_fragment` (the files have several top-level elements) | — |
| Console / stop | `wprintf`, console menu | stdout, `SIGINT`/`SIGTERM`; ignore `SIGPIPE` | — |
| `timeGetTime` | WINMM | `std::chrono::steady_clock` | — |
| Crash reporting | BugTrap, `MiniDumpWriteDump` | core dumps | — |
## Prior validation of the DB mapping (2026-10-14)
Run with the removed port against copies of the shipped `.mdf` files, on
SQL Server 2022 through FreeTDS 1.5.19:
- `up_read_world_server` returned 1 row.
- `REQ_READ_ALL_GUILD(10101)` returned the guild row.
- `REQ_UPDATE_CHARACTER_HEARTS(guid, n)` updated
`CharacterTable.Hearts` (then reverted).
- `REQ_INVEN_SLOT` failed with "Could not find stored procedure", confirming
that procedure is missing from the shipped schema.
This validates the mapping and the parameter types against the schema. It
says nothing about the original binary's runtime behaviour.
## Unknown / not recovered
- **RMI argument marshalling** (ProudNet) and the per-`DBReq` post-processing
of recordsets (e.g. `FIRST_CHARACTER` in `0x1400acbb0`).
- **52 RMIs without a procedure mapping**, mostly log, logout and auth.
- **Some mappings may be wrong:** those found two call levels deep, e.g.
`REQ_CHARACTER_READ_ALL_DATA` → `up_read_Inquiry_character`.
- **Which connection each queue/`DBReq` uses:** inferred from the database
that holds the procedure.
- **Queue-slot creation; agent-mode address/port** (pushed by the Agent over
RMI); the `guildlobby`, `channel` and `Option` elements.

File diff suppressed because it is too large Load diff

View file

@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""Join the DBCServer RMI -> procedure map with procedure parameters.
Inputs (derived, regenerable):
decomp/DBCServer_Release_x64/rmi_to_proc.tsv RMI -> cmd -> procedures
decomp/schema/<db>/routines.tsv procedure parameters
Output:
decomp/DBCServer_Release_x64/procs_used.tsv
one row per (procedure, parameter) for every procedure reachable from an RMI;
db = database that holds it (NOT_IN_SCHEMA if called but absent).
Run from the repo root: python3 decomp/scripts/gen_dbc_tables.py
"""
import csv, os
ROOT = os.getcwd()
OUT = os.path.join(ROOT, 'decomp/DBCServer_Release_x64/procs_used.tsv')
DBS = ['RustyHearts_Account', 'RustyHearts_Auth', 'RustyHearts', 'RustyHearts_Log']
def rows(path):
with open(path, newline='') as f:
return list(csv.reader(f, delimiter='\t'))
procs = {} # lower name -> (name, db, [(param, type, maxlen, out)])
for db in DBS:
for r in rows(f'{ROOT}/decomp/schema/{db}/routines.tsv')[1:]:
if len(r) < 7 or r[0] != 'SQL_STORED_PROCEDURE':
continue
p = procs.setdefault(r[1].lower(), (r[1], db, []))
if r[2] != 'NULL':
p[2].append((r[3], r[4], r[5], r[6]))
rmi = rows(f'{ROOT}/decomp/DBCServer_Release_x64/rmi_to_proc.tsv')
hdr = rmi[0]
callers = {}
for r in (dict(zip(hdr, x)) for x in rmi[1:]):
for p in filter(None, r['procs'].split(',')):
callers.setdefault(p.lower(), (p, []))[1].append(r['rmi_name'])
out = [['proc', 'db', 'rmis', 'param_no', 'param', 'sql_type', 'max_length', 'is_output']]
for key, (name, rmis) in sorted(callers.items()):
pname, db, params = procs.get(key, (name, 'NOT_IN_SCHEMA', []))
if not params:
out.append([pname, db, ','.join(rmis), '', '', '', '', ''])
for i, (n, t, m, o) in enumerate(params, 1):
out.append([pname, db, ','.join(rmis), str(i), n, t, m, o])
with open(OUT, 'w', newline='') as f:
csv.writer(f, delimiter='\t', lineterminator='\n').writerows(out)
print(f'{len(callers)} procedures -> {OUT}')

View file

@ -114,7 +114,7 @@ Further details:
`Error_ADO DB Connect.Log` (`DBC:0x1400a80c0`).
- Startup (`DBC:0x140042cf0`) opens a type-2 (AUTH) and a type-3 (GAME)
connection for the server itself. It also allocates write-queue arrays of
1000 slots (a 0x1f48-byte block: 125 loop iterations × 8 pointers).
125 slots.
**Which connection each procedure uses?** The connection object passed to each
`DBReq` method hasn't been traced yet. The **I** answer is: the database that

View file

@ -169,15 +169,19 @@ In `decomp/<X>/`:
- Mark every name as inferred.
- Record the original bugs you notice (leaks, races).
## 7. Porting spec
## 7. Port to Linux
There is no source tree in this repo. The earlier `src/` reconstructions were
removed on 2026-10-15. Instead, record the facts a re-implementation needs in
`decomp/<X>/porting.md`:
- the behaviour to reproduce, with decomp addresses;
- a substitution table: original address → Win32 API → nearest Linux
equivalent → behaviour difference;
- the original bugs, and what is unknown or unrecovered.
`src/<name>/` builds the lifted code with Meson (see `src/README.md`). Keep a
`platform.{hpp,cpp}` layer that maps each Win32 call to its nearest POSIX
equivalent.
The per-binary `README.md` must contain:
- a substitution table: original address → Win32 API → Linux replacement →
behaviour difference;
- what is stubbed or not ported, and why.
Build with `ninja -C build` and smoke-test it. A passing test proves only that
the port works, not how the original behaves.
## 8. Close
@ -195,5 +199,5 @@ the Evidence bundle, because session annotations are temporary.
`third-party/README.md`**
- [ ] Key functions decompiled; `full/` export done
- [ ] `evidence.md`, `INVENTORY.md`, `docs/ARCHITECTURE.md` updated
- [ ] `cpp/` lift and `porting.md` updated, if in scope
- [ ] Lift and Linux port updated, if in scope
- [ ] Evidence bundle exported; REA session closed

52
src/README.md Normal file
View file

@ -0,0 +1,52 @@
# src — Linux reconstructions
Buildable Linux C++ ports of analysed Rusty Hearts server binaries. Each
sub-folder ports one binary and has its own README. That README covers
provenance, the Win32 → Linux substitutions, and what is not yet ported.
| Folder | Original binary | Output |
|---|---|---|
| `agent_release/` | `Agent_Release_x64.exe` | `build/agent_release/agent_release` |
These are reconstructions from decompiled code, not original source.
## Requirements
- Linux with glibc ≥ 2.29 (for `posix_spawn_file_actions_addchdir_np`)
- A C++17 compiler (tested with GCC 16.2)
- Meson ≥ 1.0 and Ninja. Run both from this repo's `mise` environment or the
system packages.
## Build
From this directory:
```sh
meson setup build # first time only
ninja -C build
```
## Common tasks
- **Rebuild after changes:** `ninja -C build`.
- **Release build:** `meson setup build-release --buildtype=release && ninja -C build-release`.
- **Clean rebuild:** `rm -rf build && meson setup build && ninja -C build`.
## Run
```sh
./build/agent_release/agent_release --help
./build/agent_release/agent_release --auto-on 1 \
--type 1:/path/to/server:/path/to/workdir --server 1:1:1
```
Stop it with Ctrl-C or SIGTERM. See `agent_release/README.md` for every
option.
`build/` is gitignored.
## Adding another binary
1. Create `<name>/` with its sources, a `meson.build` and a `README.md`. The
README must cite decomp addresses and list substitutions and gaps.
2. Add `subdir('<name>')` to `meson.build` in this directory.

View file

@ -0,0 +1,65 @@
# agent_release — Linux port of `Agent_Release_x64.exe` (process-supervisor core)
A buildable Linux reconstruction of the parts of the Agent that have been
decompiled. It is **not** the original program: names are inferred (there is no
PDB), and only the analysed functions are ported.
Source analysis:
- `decomp/Agent_Release_x64/evidence.md`
- `decomp/Agent_Release_x64/cpp/AgentProcessControl.cpp`
## Build and run
```sh
cd src
meson setup build
ninja -C build
./build/agent_release/agent_release --auto-on 1 \
--type 1:/path/to/server:/path/to/workdir --server 1:1:1
```
`--type TYPE:EXE[:WORKDIR]` maps a server type to its executable.
`--server CODE:TYPE:REVIVE` registers one managed server. By default the
server's code is passed as its argument, so the command line is `EXE CODE`.
## Win32 → Linux substitutions
| Original (address) | Win32 | Linux replacement | Behaviour difference |
|---|---|---|---|
| Single instance (`0x1400100b0`) | `CreateMutexW("Global\\RustyHearts_Agent")` | `flock(LOCK_EX\|LOCK_NB)` on `/tmp/RustyHearts_Agent.lock` | Lock file instead of a kernel object; still released on exit/crash |
| Launch (`0x1400094e0`) | `CreateProcessW(…, CREATE_NEW_CONSOLE, …, workDir)` + `STARTUPINFOW` console geometry | `posix_spawnp` + `POSIX_SPAWN_SETSID` + `addchdir_np` | No console window; command line split on whitespace (no Windows quoting rules) |
| Watcher (`0x140008c90`) | `GetExitCodeProcess` polled every 10 ms | blocking `waitpid` on a detached `std::thread` | No polling; child reaped (fixes original handle leak); PID passed by value (fixes original stack-pointer race) |
| Kill (`0x140009780`) | Toolhelp32 snapshot + `OpenProcess` + `TerminateProcess(h,0)` | `/proc/*/cmdline` argv[0] basename, `strcasecmp`, `kill(SIGKILL)` | Same: first match only, case-insensitive; no snapshot leak |
| Main loop timer (`0x1400037a0`) | `QueryPerformanceCounter` (`TimeSourceHRWin32`) | `std::chrono::steady_clock` | — |
| Thread start | `_beginthreadex` | `std::thread` | — |
| `Sleep` | `Sleep` | `std::this_thread::sleep_for` | — |
| Shutdown flag | console menu (`FUN_140001dc0`, not analysed) | `SIGINT` / `SIGTERM` | Different trigger |
Kept from the original:
- the 1-second sleep after each launch attempt;
- the 260-character (`MAX_PATH`) command-line limit;
- the log strings.
## Not ported (unknown or proprietary)
- **ProudNet networking / RMI to AgentManager and game servers.** ProudNet is
proprietary and the protocol is undecoded. `IManagerSession` is a logging
stub at the points where the original sends RMI messages (proxy vtable
slots `+0x40` and `+0x50`).
- **XML config (`option\*.xml`, TinyXML).** Field mapping is not yet recovered,
so command-line flags stand in for it.
- **Launch-argument builder `FUN_140027540`.** Its output is unknown, so only
the "server code as argument" mode is implemented.
- **Exit follow-ups.**
- The `type == 1` exit follow-up (`FUN_140009f20`) is not ported.
- The identity of the type `0x18` process killed at startup is unknown.
`--kill-on-start` stands in for it.
- **Locking.** The original's locking around the server table is not visible
in the analysed code. This port uses a recursive mutex.
## Verified (2026-10-14, Linux)
Smoke test, using `sleep` as the managed server: auto-start after the delay,
revive on exit, second-instance refusal, case-insensitive kill by name,
clean SIGTERM shutdown. This tests the port only. It says nothing about the
original binary's runtime behaviour.

125
src/agent_release/agent.cpp Normal file
View file

@ -0,0 +1,125 @@
// agent.cpp — logic follows the Ghidra pseudocode cited per function.
#include "agent.hpp"
#include "platform.hpp"
#include <chrono>
#include <cstdio>
namespace agent {
void LoggingManagerSession::NotifyServerState(const ServerEntry& e)
{
std::printf("[RMI->AgentManager] ServerState code=%d pid=%d running=%d\n",
e.code, static_cast<int>(e.pid), e.running ? 1 : 0);
}
void LoggingManagerSession::NotifyServerStopped(const ServerEntry& e)
{
std::printf("[RMI->AgentManager] ServerStopped code=%d\n", e.code);
}
void LoggingManagerSession::SendLog(const std::string& text)
{
std::printf("[RMI->AgentManager] Log: %s\n", text.c_str());
}
Agent::Agent(Options opts, std::map<uint8_t, ServerTypeInfo> types,
std::unique_ptr<IManagerSession> session)
: opts_(std::move(opts)), types_(std::move(types)), session_(std::move(session)) {}
void Agent::AddServer(const ServerEntry& e)
{
std::lock_guard<std::recursive_mutex> g(mu_);
servers_[e.code] = e;
}
std::string Agent::BuildCommandLine(const ServerEntry& e) const
{
auto t = types_.find(e.type);
std::string cmd = (t == types_.end() ? std::string() : t->second.exePath) + " ";
if (opts_.useCodeAsArgument)
cmd += std::to_string(e.code);
// else: original appends FUN_140027540's output — contents UNKNOWN, not ported.
return cmd;
}
// 0x140008ea0
void Agent::ServerOn(int32_t code)
{
std::lock_guard<std::recursive_mutex> g(mu_);
auto it = servers_.find(code);
if (it == servers_.end()) {
std::printf("Error - NOT EXIST SERVER : %d \n", code);
return;
}
ServerEntry& e = it->second;
if (e.running) return;
e.running = true;
std::string cmd = BuildCommandLine(e);
auto t = types_.find(e.type);
std::string workDir = t == types_.end() ? std::string() : t->second.workDir;
pid_t pid = 0;
if (cmd.size() < 260 && (pid = platform::LaunchProcess(cmd, workDir)) > 0) { // MAX_PATH check kept
e.pid = pid;
e.running = true;
session_->NotifyServerState(e);
platform::WatchProcess(pid, [this](pid_t p) { OnProcessExit(p); });
} else {
e.running = false;
session_->SendLog("FAILED SERVER ON " + opts_.agentName + " ");
}
}
void Agent::AllServerOn()
{
std::lock_guard<std::recursive_mutex> g(mu_);
std::printf("All Server On. \n");
for (auto& kv : servers_) ServerOn(kv.first);
}
// 0x140008c90 (after the wait loop)
void Agent::OnProcessExit(pid_t pid)
{
std::lock_guard<std::recursive_mutex> g(mu_);
ServerEntry* e = nullptr;
for (auto& kv : servers_)
if (kv.second.pid == pid) { e = &kv.second; break; }
if (!e) { std::printf("NOT FIND PROCESS ID \n"); return; }
e->running = false;
session_->NotifyServerState(*e);
session_->NotifyServerStopped(*e);
if (running_ && (e->revive || opts_.reviveAll))
ServerOn(e->code);
// type==1 follow-up (FUN_140009f20) — purpose UNKNOWN, not ported.
}
// 0x1400037a0 MagiAgentServer::Run — network setup omitted (ProudNet).
int Agent::Run()
{
using clock = std::chrono::steady_clock;
std::printf("# Agent Startup #\n\n+ Prepare Startup\n");
std::printf("\n # Agent Running... #\n");
if (!opts_.killOnStart.empty()) platform::KillProcessByName(opts_.killOnStart);
const auto t0 = clock::now();
auto seconds = [&] { return std::chrono::duration<double>(clock::now() - t0).count(); };
bool autoOnPending = true;
double lastTick = 0;
while (running_) {
double now = seconds();
if (opts_.autoServerOnDelay > 0 && now > opts_.autoServerOnDelay && autoOnPending) {
autoOnPending = false;
std::printf("Start Auto Server On. \n");
AllServerOn();
}
if (now - lastTick >= 0.1) lastTick = now; // 10 Hz tick: network update (not ported)
platform::SleepMs(1);
}
std::printf("+ Prepare Stop\n# Agent Stoped... #\n");
return 0;
}
} // namespace agent

View file

@ -0,0 +1,82 @@
// agent.hpp — Linux port of the process-supervisor core of Agent_Release_x64.exe.
// Reconstructed from decomp/Agent_Release_x64/cpp/AgentProcessControl.cpp.
// Names are inferred (no PDB). See README.md for what is and is not ported.
#pragma once
#include <sys/types.h>
#include <atomic>
#include <cstdint>
#include <map>
#include <memory>
#include <mutex>
#include <string>
namespace agent {
// Per-server record (original: hash map at 0x140162d60, ServerEntry offsets
// +0x04 code, +0x0c type, +0xa4 pid, +0xc4 running, +0xd5 revive).
struct ServerEntry {
int32_t code = 0;
uint8_t type = 0;
pid_t pid = 0;
bool running = false;
bool revive = false;
};
// Server-type table (original: vector at 0x140162d58, type -> exe path).
struct ServerTypeInfo {
std::string exePath; // may include fixed arguments
std::string workDir; // original derives it from the exe path (FUN_14002fe90)
};
// Agent options (original: singleton at 0x140162d68).
struct Options {
std::string agentName = "AGENT"; // +0x030
bool useCodeAsArgument = true; // +0x152
bool reviveAll = false; // +0x171
double autoServerOnDelay = 0.0; // +0x178, seconds; <= 0 disables
std::string killOnStart; // exe of server type 0x18 (identity UNKNOWN)
};
// Stand-in for the ProudNet RMI proxy to AgentManager
// (AGENT2AGENTMANAGER_PROTOCOL::Proxy, vtable slots +0x40 / +0x50).
// ProudNet is proprietary and the wire protocol is not yet decoded, so this
// interface only logs. Replace with a real transport once the protocol is known.
class IManagerSession {
public:
virtual ~IManagerSession() = default;
virtual void NotifyServerState(const ServerEntry& e) = 0; // slot +0x40
virtual void NotifyServerStopped(const ServerEntry& e) = 0; // slot +0x50
virtual void SendLog(const std::string& text) = 0;
};
class LoggingManagerSession : public IManagerSession {
public:
void NotifyServerState(const ServerEntry& e) override;
void NotifyServerStopped(const ServerEntry& e) override;
void SendLog(const std::string& text) override;
};
class Agent {
public:
Agent(Options opts, std::map<uint8_t, ServerTypeInfo> types,
std::unique_ptr<IManagerSession> session);
void AddServer(const ServerEntry& e);
void ServerOn(int32_t code); // 0x140008ea0
void AllServerOn(); // 0x140009bd0 (inferred: ServerOn for every entry)
int Run(); // 0x1400037a0 MagiAgentServer::Run
void Stop() { running_ = false; }
private:
void OnProcessExit(pid_t pid); // body of watcher thread 0x140008c90
std::string BuildCommandLine(const ServerEntry& e) const;
Options opts_;
std::map<uint8_t, ServerTypeInfo> types_;
std::unique_ptr<IManagerSession> session_;
std::map<int32_t, ServerEntry> servers_;
std::recursive_mutex mu_; // original has no visible lock here (UNKNOWN)
std::atomic<bool> running_{true};
};
} // namespace agent

View file

@ -0,0 +1,70 @@
// main.cpp — entry point. The original reads option\*.xml (format not yet
// recovered); this port takes the equivalent settings on the command line.
#include "agent.hpp"
#include "platform.hpp"
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <string>
static agent::Agent* g_agent = nullptr;
static void OnSignal(int) { if (g_agent) g_agent->Stop(); }
static void Usage()
{
std::printf(
"usage: agent_release [options] --server CODE:TYPE:REVIVE --type TYPE:EXE[:WORKDIR] ...\n"
" --auto-on SEC start all servers SEC seconds after startup\n"
" --revive-all relaunch any server that exits\n"
" --no-code-arg do not pass the server code as argv[1]\n"
" --kill-on-start EXE kill the first process named EXE before running\n"
" --name NAME agent name used in log messages\n");
}
int main(int argc, char** argv)
{
agent::Options opts;
std::map<uint8_t, agent::ServerTypeInfo> types;
std::vector<agent::ServerEntry> servers;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
auto next = [&]() -> std::string {
if (i + 1 >= argc) { Usage(); std::exit(2); }
return argv[++i];
};
if (a == "--auto-on") opts.autoServerOnDelay = std::stod(next());
else if (a == "--revive-all") opts.reviveAll = true;
else if (a == "--no-code-arg") opts.useCodeAsArgument = false;
else if (a == "--kill-on-start") opts.killOnStart = next();
else if (a == "--name") opts.agentName = next();
else if (a == "--type") {
std::string v = next();
auto p1 = v.find(':');
if (p1 == std::string::npos) { Usage(); return 2; }
auto p2 = v.find(':', p1 + 1);
agent::ServerTypeInfo t;
t.exePath = v.substr(p1 + 1, p2 == std::string::npos ? std::string::npos : p2 - p1 - 1);
if (p2 != std::string::npos) t.workDir = v.substr(p2 + 1);
types[static_cast<uint8_t>(std::stoi(v.substr(0, p1)))] = t;
} else if (a == "--server") {
std::string v = next();
agent::ServerEntry e;
int code = 0, type = 0, revive = 0;
if (std::sscanf(v.c_str(), "%d:%d:%d", &code, &type, &revive) < 2) { Usage(); return 2; }
e.code = code; e.type = static_cast<uint8_t>(type); e.revive = revive != 0;
servers.push_back(e);
} else { Usage(); return a == "-h" || a == "--help" ? 0 : 2; }
}
platform::SingleInstanceLock lock("RustyHearts_Agent"); // Global\RustyHearts_Agent
if (!lock.acquired()) return 1;
agent::Agent ag(opts, types, std::make_unique<agent::LoggingManagerSession>());
for (auto& e : servers) ag.AddServer(e);
g_agent = &ag;
std::signal(SIGINT, OnSignal);
std::signal(SIGTERM, OnSignal);
return ag.Run();
}

View file

@ -0,0 +1,6 @@
threads = dependency('threads')
executable('agent_release',
['main.cpp', 'agent.cpp', 'platform.cpp'],
dependencies : threads,
install : false)

View file

@ -0,0 +1,126 @@
// platform.cpp — Linux implementations; Win32 originals cited per function.
#include "platform.hpp"
#include <dirent.h>
#include <fcntl.h>
#include <signal.h>
#include <spawn.h>
#include <strings.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
#include <thread>
#include <vector>
extern char** environ;
namespace platform {
SingleInstanceLock::SingleInstanceLock(const std::string& name)
{
// "Global\\" kernel-object namespace -> machine-wide lock file in /tmp.
std::string path = "/tmp/" + name + ".lock";
fd_ = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644);
if (fd_ < 0) {
std::printf("ERROR_ACCESS_DENIED!!! %s \n", path.c_str());
} else if (::flock(fd_, LOCK_EX | LOCK_NB) != 0) {
std::printf("ERROR_ALREADY_EXISTS!!! %s \n", path.c_str());
::close(fd_);
fd_ = -1;
}
if (fd_ < 0) SleepMs(3000); // original sleeps 3 s before failing
}
SingleInstanceLock::~SingleInstanceLock()
{
if (fd_ >= 0) ::close(fd_);
}
pid_t LaunchProcess(const std::string& commandLine, const std::string& workDir)
{
std::istringstream in(commandLine);
std::vector<std::string> args;
for (std::string a; in >> a;) args.push_back(a);
if (args.empty()) return 0;
std::vector<char*> argv;
for (auto& a : args) argv.push_back(a.data());
argv.push_back(nullptr);
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
if (!workDir.empty()) posix_spawn_file_actions_addchdir_np(&fa, workDir.c_str());
posix_spawnattr_t attr;
posix_spawnattr_init(&attr);
posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSID); // ~ CREATE_NEW_CONSOLE
pid_t pid = 0;
int rc = posix_spawnp(&pid, argv[0], &fa, &attr, argv.data(), environ);
posix_spawnattr_destroy(&attr);
posix_spawn_file_actions_destroy(&fa);
if (rc != 0) {
std::printf("=======Server Fail=======\n");
std::printf("Path\t: [%s] \n", workDir.c_str());
std::printf("File\t: [%s] (%s)\n", commandLine.c_str(), std::strerror(rc));
pid = 0;
} else {
std::printf("=======ServerOn=======\n");
}
std::printf("//////////////////////////////////////////////////////////////////////////\n");
SleepMs(1000); // kept: original sleeps 1 s after every launch attempt
return pid;
}
void WatchProcess(pid_t pid, std::function<void(pid_t)> onExit)
{
// pid passed by value: avoids the original's stack-pointer race.
std::thread([pid, onExit = std::move(onExit)] {
int status = 0;
while (::waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
onExit(pid); // waitpid reaps the child: no handle/zombie leak
}).detach();
}
static std::string ProcArgv0Basename(const char* pidDir)
{
std::ifstream f(std::string("/proc/") + pidDir + "/cmdline", std::ios::binary);
std::string argv0;
std::getline(f, argv0, '\0');
auto slash = argv0.find_last_of('/');
return slash == std::string::npos ? argv0 : argv0.substr(slash + 1);
}
bool KillProcessByName(const std::string& exeName)
{
if (exeName.empty()) return false;
DIR* d = ::opendir("/proc");
if (!d) return false;
bool killed = false;
while (dirent* e = ::readdir(d)) {
char* end = nullptr;
long pid = std::strtol(e->d_name, &end, 10);
if (*end != '\0' || pid <= 0 || pid == ::getpid()) continue;
if (::strcasecmp(ProcArgv0Basename(e->d_name).c_str(), exeName.c_str()) != 0) continue;
if (::kill(static_cast<pid_t>(pid), SIGKILL) == 0) {
std::printf("Process Kill.!!! [%s] \n", exeName.c_str());
killed = true;
break; // original stops at the first match
}
}
::closedir(d);
return killed;
}
void SleepMs(unsigned ms)
{
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
} // namespace platform

View file

@ -0,0 +1,39 @@
// platform.hpp — Linux substitutes for the Win32 calls used by
// Agent_Release_x64.exe (see decomp/Agent_Release_x64/cpp/AgentProcessControl.cpp).
#pragma once
#include <sys/types.h>
#include <functional>
#include <string>
namespace platform {
// Replaces CreateMutexW(L"Global\\RustyHearts_Agent") (0x1400100b0).
// Uses an exclusive flock() on a lock file; released automatically on exit.
class SingleInstanceLock {
public:
explicit SingleInstanceLock(const std::string& name);
~SingleInstanceLock();
bool acquired() const { return fd_ >= 0; }
private:
int fd_ = -1;
};
// Replaces CreateProcessW(..., CREATE_NEW_CONSOLE, ..., workDir) (0x1400094e0).
// commandLine is split on whitespace (the original passes one string to Windows).
// The child gets its own session (setsid) as the nearest analogue of a new console.
// Returns pid > 0, or 0 on failure (same contract as the original).
pid_t LaunchProcess(const std::string& commandLine, const std::string& workDir);
// Replaces the watcher's GetExitCodeProcess polling loop (0x140008c90) with a
// blocking waitpid(). Calls onExit(pid) on a detached thread once the child exits.
void WatchProcess(pid_t pid, std::function<void(pid_t)> onExit);
// Replaces Toolhelp32 + OpenProcess + TerminateProcess (0x140009780).
// Scans /proc, matches basename of argv[0] case-insensitively, kills the FIRST
// match with SIGKILL (TerminateProcess is unconditional), returns true if killed.
bool KillProcessByName(const std::string& exeName);
// Replaces Sleep(ms).
void SleepMs(unsigned ms);
} // namespace platform

7
src/meson.build Normal file
View file

@ -0,0 +1,7 @@
# Linux reconstructions of the Rusty Hearts server binaries.
# Each sub-folder ports one binary; see its README.md for provenance and gaps.
project('rusty-hearts-recon', 'cpp',
version : '0.1.0',
default_options : ['cpp_std=c++17', 'warning_level=3', 'buildtype=debugoptimized'])
subdir('agent_release')

31
third-party/README.md vendored
View file

@ -95,37 +95,6 @@ ProudNet is not a separate DLL; it is linked into the server EXEs.
- To re-fetch:
`git archive ProudNetLibSetup-1.8.00001 ProudNet/include ProudNet-License-En.txt`
from that repo into `_local/proudnet-1.8.00001/`.
- **`github.com/Nettention/ProudChatSDK` (checked 2026-10-15, `main` = tag
`1.1.0001`, commit `e0d9d6b8`).**
- **Contents:** bundles ProudNet **1.8.58849**: 185 headers, plus prebuilt
libraries for Windows (v110/v120/v140, dll), Android, iOS and
**x86_64 Linux** (`libProudNetServer.so/.a`, `libProudNetClient.so/.a`;
`__xstat` → old glibc).
- **Licence:** the repo's `LICENSE` (MIT) covers the chat SDK only;
`ProudNet/` keeps the same proprietary EULA (§1.2b). Nothing from it is
stored in this repo.
- **Headers:** identical to 1.8.00001 apart from the version banner and
translated doc comments in `ErrorType.h`. They add one error code and give
no new information beyond `_local/proudnet-1.8.00001`.
- **Compatibility with the 2013 ProudNet linked into DBCServer:**
| Aspect | Result | Type |
|---|---|---|
| PIDL proxy ABI | Matches: `IRmiProxy` keeps `m_signature` at +0x14, which the generated constructor checks against 1 (`MS:0x140008e20`). `RmiID` is still `uint16_t`. The `PIDL compiler is incompatible` and `CStartServerParameter::m_(fast)encryptedMessageKeyLength` strings appear in both | O |
| Internal implementation | 29 of DBCServer's 63 `Proud::` RTTI classes are absent from the 1.8.58849 Linux libraries' symbols, e.g. `CTcpLayer_C`, `CUdpSocket_S`, `CUpnp`, `ICompletionPortEventSink` | O |
| Internal net version | **2013 build: 196978 (0x30172)**, the global at `DBC:0x140344d40`. It is copied into the server object at +0x220 (`DBC:0x14018fa70`, instruction at `0x14019018d`) and into the client objects (`0x1401dd3e0`, `0x1401df000`). In the connect handshake (`DBC:0x1401a8110`, `NotifyServerConnectionRequestData`) the server reads a credential byte array, a 16-byte protocol GUID and a 4-byte int, and accepts only if the GUID **and** the int equal its own. The same value appears exactly once in `.data` of all 16 EXEs (O). **1.8.58849: 197144 (0x30218)** (`CNetConfig::InternalNetVersion`, rejected via `NotifyProtocolVersionMismatch`). The values differ by 166 | O |
| Licensing | 1.8 also contains license and activation checks (`CPNLic`, `NotifyLicenseMismatch`) | S |
- **Conclusion (I):**
- **Source-level:** usable as a header/ABI reference for PIDL-generated
stubs and proxies.
- **Wire-level:** **not** compatible with the original 2013 peers. The
internal net versions differ (196978 vs 197144), and the 2013 handshake
requires an exact match. This is O for the 2013 server side; I for the
1.8 side, which was not traced beyond the symbol name.
- **Linking:** linking `libProudNetServer.so` into `src/dbc_server`
would require accepting the EULA and a licence, and would give a
ProudNet 1.8 server that the original binaries likely cannot talk to.
Vendor attributions for XTrap (WiseLogic) and Apex are inferred from DLL
names, not corroborated by decompilation.