remove src recomp

This commit is contained in:
Benjamin Palko 2026-10-15 09:08:48 -04:00
parent af36074fed
commit 31ff800b2a
19 changed files with 1398 additions and 644 deletions

View file

@ -16,6 +16,8 @@ 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

@ -0,0 +1,55 @@
# 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,6 +13,8 @@ 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) |

View file

@ -0,0 +1,91 @@
# 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

@ -26,7 +26,7 @@ hypotheses to confirm or revise during analysis.
| `ManagerServer_Release_USA_x64.exe` | 6666752 | 6510.5 | c239477294c199f8df4ef70138cc1583c87838dd5e8cebed2ff0fae2bc6c5be2 | management/admin | pending |
| `AuctionServer_Release_USA_x64.exe` | 6477824 | 6326.0 | ebca9cc047aeca125ac0621e3652339096c74862746157adc6c72057422f6dcf | auction house | pending |
| `MatchServer_Release_USA_x64.exe` | 6134784 | 5991.0 | 341dd4a38c9e1abb590f60325ef52bf24b60a48a70e36e2f1651bbf00591fcf2 | matchmaking | pending |
| `DBCServer_Release_x64.exe` | 3542016 | 3459.0 | d9e4fccdf3482e50247686d4452c0b13c75a53efc4bd7320668eece8d1ce828c | DB access server (observed: SERVERS2DBC RMI → write queues → ADO stored procs; 4 connection types; Linux port `src/dbc_server/`) | open |
| `DBCServer_Release_x64.exe` | 3542016 | 3459.0 | d9e4fccdf3482e50247686d4452c0b13c75a53efc4bd7320668eece8d1ce828c | DB access server (observed: SERVERS2DBC RMI → write queues → ADO stored procs; 4 connection types) | open |
| `DBCServer_LOG_Release_x64.exe` | 3394560 | 3315.0 | 2636d51679abb7703a6386a398f2afb985704aa0ce00d53e75d0ff3b093f5836 | logging DB variant | pending |
| `GameGatewayServer_Release_x64.exe` | 3330560 | 3252.5 | 4cbec55dc3dfe5a0eeba3170722e6f1d33e55d6a0d007774266fe9db96f25d1a | gateway/proxy | pending |
| `GMServer_Release_x64.exe` | 2004480 | 1957.5 | 8fba9dbb8809419d6e2edaee31d9ad60501341d97dca0fc82a1c1e7cc32199d1 | game-master tools | pending |

View file

@ -1,81 +1,49 @@
#!/usr/bin/env python3
"""Generate the DBCServer port's static tables from decomp evidence.
"""Join the DBCServer RMI -> procedure map with procedure parameters.
Inputs (all derived, regenerable):
Inputs (derived, regenerable):
decomp/DBCServer_Release_x64/rmi_to_proc.tsv RMI -> cmd -> procedures
decomp/schema/<db>/routines.tsv procedure parameters
Output:
src/dbc_server/generated/tables.inc
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, 'src/dbc_server/generated/tables.inc')
# Database -> DBC connection type (DBC:0x1400a7aa0). The DB for a procedure is
# where it lives in the schema dump (inference, see docs/DATABASE.md).
DBTYPE = {'RustyHearts_Account': 'Web', 'RustyHearts_Auth': 'Auth',
'RustyHearts': 'Game', 'RustyHearts_Log': 'GameLog'}
SQLTYPE = {'int': 'Int', 'smallint': 'SmallInt', 'tinyint': 'TinyInt', 'bigint': 'BigInt',
'bit': 'Bit', 'float': 'Float', 'uniqueidentifier': 'Guid', 'nvarchar': 'NVarChar',
'nchar': 'NVarChar', 'varchar': 'VarChar', 'char': 'VarChar', 'datetime': 'DateTime'}
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 DBTYPE:
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], SQLTYPE.get(r[4], 'VarChar'), int(r[5]), r[6] == '1'))
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]; rmi = [dict(zip(hdr, r)) for r in rmi[1:]]
used = []
for r in rmi:
hdr = rmi[0]
callers = {}
for r in (dict(zip(hdr, x)) for x in rmi[1:]):
for p in filter(None, r['procs'].split(',')):
if p.lower() not in procs:
# Called by DBCServer but absent from the shipped schema: keep it so
# the port fails like the original would (ADO error). DB guessed from name.
procs[p.lower()] = (p, 'RustyHearts_Log' if '_log_' in p else 'RustyHearts', None)
if p.lower() not in used:
used.append(p.lower())
callers.setdefault(p.lower(), (p, []))[1].append(r['rmi_name'])
def cstr(s):
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
out = ['// GENERATED by decomp/scripts/gen_dbc_tables.py - do not edit.',
'// Sources: decomp/DBCServer_Release_x64/rmi_to_proc.tsv, decomp/schema/*/routines.tsv', '']
for i, key in enumerate(used):
name, db, ps = procs[key]
ps = ps or []
out.append(f'static const ProcParam kParams{i}[] = {{' +
''.join(f'{{{cstr(n)}, SqlType::{t}, {m}, {str(o).lower()}}},' for n, t, m, o in ps) +
('{nullptr, SqlType::Int, 0, false}' if not ps else '') + '};')
out.append('')
out.append('static const ProcDef kProcs[] = {')
for i, key in enumerate(used):
name, db, ps = procs[key]
known = ps is not None
out.append(f' {{{cstr(name)}, DbType::{DBTYPE[db]}, kParams{i}, {len(ps or [])}, {str(known).lower()}}},')
out.append('};')
out.append('')
out.append('// rmi_id, name, cmd_id (0 = not queued / unknown), proc indexes (-1 = none)')
out.append('static const RmiDef kRmis[] = {')
for r in rmi:
idx = [used.index(p.lower()) for p in filter(None, r['procs'].split(',')) if p.lower() in used]
idx = (idx + [-1, -1, -1])[:3]
missing = [p for p in filter(None, r['procs'].split(',')) if procs[p.lower()][2] is None]
note = f' // missing in schema: {",".join(missing)}' if missing else ''
out.append(f' {{{r["rmi_id"]}, {cstr(r["rmi_name"])}, {r["cmd_id"] or 0}, '
f'{{{idx[0]}, {idx[1]}, {idx[2]}}}}},{note}')
out.append('};')
os.makedirs(os.path.dirname(OUT), exist_ok=True)
open(OUT, 'w').write('\n'.join(out) + '\n')
print(f'{len(used)} procedures, {len(rmi)} RMIs -> {OUT}')
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}')