11 KiB
Database — schema and access path
This document covers:
- how the servers reach SQL Server;
- which database each request lands in;
- the schema of the five databases.
Sources:
DBCServer_Release_x64.exe, decompiled (decomp/DBCServer_Release_x64/);MessageServer_Release_x64.exe, the client side;- a catalog dump of
schema/*.mdf(decomp/schema/).
Notation as in SHARED_FRAMEWORK.md: O observed in code, D observed in
the schema dump, S strings only, I inference, ? unknown. DBC:0x…
refers to decomp/DBCServer_Release_x64/full/functions/0x….c.
Everything here is static. No server was run against the database.
1. Databases (D)
| Catalog | Config element (DBC) | DBC conn. type | Tables | Routines | Role |
|---|---|---|---|---|---|
RustyHearts |
db_game |
3 GAME | 63 | 273 | Characters, items, guilds, mail, pets, quests, base data tables (Base_*) |
RustyHearts_Auth |
db_account |
2 AUTH | 14 | 51 | Game-side auth (AuthTable, keyed by WindyCode), server topology (ServerOption, ServerFile, WorldServer, ChannelOption), events, inquiries, GM accounts |
RustyHearts_Log |
db_game_log |
4 GAME LOG | 5 static | 110 | Logs. Most log tables are created per day by Local_Create*Table procedures, e.g. Chat_Log_<y>_<m>_<d>, so they are not in the static catalog |
RustyHearts_Account |
db_web (I) |
1 WEB | 4 | 11 | Web/launcher accounts (AccountTable, CashTable, BillingLog, VerificationCode) |
GMRustyHearts |
— | — | 8 | 1 | GM tool database (GMInfoTable, GMAudit, AdminRoles). No server binary analysed so far references it (?: GMServer / tools) |
Facts about the dump:
- No foreign keys are declared in any database (D). Relationships are by
convention:
AuthID/auth_id(uniqueidentifier),character_id,bcust_id=WindyCode(account name),item_uid,guild_id. - The
.mdffiles are in SQL Server 2022 on-disk format (version 957). SQL Server 2019 refuses to attach them. This contradicts the "up to 2019" note inAGENTS.mdfor these files, not necessarily for the servers (I: the dump was last attached on a 2022 instance). - Several procedures carry Korean comments (D), which points to the
original developer's code (I). Others reference
WindyCode(the original publisher's account ID) and the linked serversRustyHeartsLinkAuth/RustyHearts_LogDB, whichup_init_auth_link/up_init_log_linkcreate. - Some procedures are empty bodies: 7 in RustyHearts, 2 in Auth and 6 in
Log (
decomp/schema/*/empty_procs.txt). An example isup_insert_log_connection. Calls to these succeed but do nothing (I: they were stubbed out in this deployment).
Full per-database dumps are in decomp/schema/<db>/: tables.tsv (name, row
count, column count), columns.tsv, indexes.tsv, routines.tsv
(parameters) and procs/*.sql. decomp/schema/proc_tables.tsv lists the
tables each procedure reads (R) or writes (W) and its cross-DB references.
That list is derived from the SQL text, so it is I for dynamic SQL. How to
regenerate everything: decomp/schema/README.md.
Core tables (D)
| Table | Key | Notes |
|---|---|---|
RustyHearts.CharacterTable |
character_id |
53 cols: AuthID, bcust_id, server, Name, Class, Job, Level, Experience, gold, GuildID, LobbyID, … Written by 57 procedures. CharacterTable_DELETE holds soft-deleted characters |
RustyHearts.N_InventoryItem / N_EquipItem / N_MailItem / tbl_Account_Storage / tbl_Guild_Storage_Item / tbl_Pet_Inventory |
item_uid |
Share one item-row layout (41–44 cols: code, page_index, slot_index, durability, enhance_level, 3 options, sockets, …). "Moving" an item means moving its row between these tables |
RustyHearts.KeyMapTable |
AuthID |
Per-account key bindings (100 cols) |
RustyHearts.QuickSlotExTable[02/03] |
character_id |
Quick slots, 65 cols |
RustyHearts.GuildTable / GuildMemberTable |
guild_id / index |
|
RustyHearts.MailTable |
ID |
|
RustyHearts.PetTable, RearingTable |
pet_id, rearing_id |
|
RustyHearts.Base_* |
various | Static game data (items 22.5k rows, quests, skills, exp tables) |
RustyHearts_Auth.AuthTable |
WindyCode |
AuthID, online, ServerIP, ServerType, HostID, DBCIndex, cash/mileage, MAC addresses, second_pass |
RustyHearts_Auth.ServerOption |
Code |
Server topology. See CONFIG.md §5 |
RustyHearts_Auth.ServerFile |
type |
Server kind → EXE file name |
RustyHearts_Account.AccountTable |
AccountID |
Web login (AccountPwd, Token) |
2. Access path
client servers (Game/Guild/Msg/…) DBCServer
MGDBCMsgSession pool ──SERVERS2DBC RMI 0x2ee1–0x2fe6──▶ CDBCServer stub handler
│ push {size, cmd 1001–1317, args}
▼ (1 of N write queues, idx<1000)
CDBLobbyWriteCmdQueue worker
│ switch(cmd) DBC:0x1400a8190
▼
CDBLobbyWriteQuery::DBReqXxx
│ ADODB.Command "dbo.up_xxx"
▼
SQL Server (one of 4 connections)
◀──DBC2SERVERS RMI 0x2af9–0x2b46 (76 replies)───────────┘
| Step | Type | Evidence |
|---|---|---|
| Only the two DBC servers hold ADO connections. Other servers send RMIs | O for MessageServer; I for the others (they show Servers2DbcProxy RTTI) |
SHARED_FRAMEWORK.md §1, §5 |
SERVERS2DBC is byte-for-byte the same 262-entry name/ID table in MessageServer and DBCServer |
O (diffed) | decomp/shared/rmi_from_MessageServer.tsv, DBC:0x140041b90 (ID→name) |
Stub handler, e.g. DBC:0x140050d20: increments a per-RMI counter, then calls a CDBCServer method that packs a command {size, cmd_id, …} and pushes it to write queue [idx], signalling a semaphore (DBC:0x140144730) |
O | |
Worker dispatch DBC:0x1400a8190: switch (cmd - 1001) over 317 cases through a jump table at 0x1400a9dd4. Ghidra could not decompile it; decoded from the raw jump table |
O (disassembly) | decomp/DBCServer_Release_x64/cmd_dispatch.tsv |
Each DBReq* method runs a single stored procedure through ADODB.Command, with named parameters (bcust_id, auth_id, …). On COM error it logs ADO_ON_ERROR : [..] Proc [..] with source file and line |
O | DBC:0x1400acbb0 (up_create_character), catch funclets |
Original source files: 28 DBLobbyWriteQuery<Area>.cpp (Character 176 call sites, Guild 72, Item 43, User 30, Pet 28, …) and DBWriteQueryServerOption.cpp |
O (embedded __FILE__ strings) |
decomp/DBCServer_Release_x64/dbreq_methods.tsv |
Connections (O, DBC:0x1400a7aa0, REA ev_00545a3e…)
CDBConnection::Init(type) copies a connection string from the global option
object into the connection:
| type | Option field | Config element | Console label | Timeout field |
|---|---|---|---|---|
| 1 | +0xb8 | db_web |
WEB |
+0x134 |
| 2 | +0x90 | db_account |
AUTH |
+0x134 (the db_web timeout. The db_account timeout at +0x130 is never used: an original bug, O) |
| 3 | +0xe0 | db_game |
GAME |
+0x138 |
| 4 | +0x108 | db_game_log |
GAME LOG |
+0x13c |
Further details:
- Connection strings are limited to 255 characters (
Error … String Max.!). - A timeout of 0 becomes 500.
- On failure it logs
DB Connect failed\n Connection String = %stoError_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).
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
contains the procedure (column proc_db below). That holds for 210 of 210
mapped RMIs, with 7 exceptions that aren't in the schema dump (§3).
3. RMI → stored procedure map
decomp/DBCServer_Release_x64/rmi_to_proc.tsv has one row per SERVERS2DBC
RMI. Its columns are: rmi_id, rmi_name, stub_slot, stub_handler,
cmd_id, dbreq_fn, procs, dbreq_method, source_cpp and proc_db.
| Outcome | RMIs |
|---|---|
Resolved to a procedure in RustyHearts |
166 |
… in RustyHearts_Auth |
21 |
… in RustyHearts_Log |
16 |
| Procedure not present in the schema dump | 7 |
| Not resolved | 52 |
The 7 procedures missing from the dump are up_item_move_pet_inventory_to_inventory,
up_read_item_inventory_slot, up_read_extern_cash,
up_insert_log_learnguildskill, up_insert_log_cash_info,
up_insert_log_auth_login and up_skill_delete_all. In this deployment, those
RMIs would fail with an ADO error (I).
Of the 52 unresolved RMIs:
- Most are
REQ_LOG_*,*_LOG,REQ_SAVE_ITEM_*,REQ_MAIL_SEND,REQ_LOGOUTandREQ_AUTH. - 53 RMIs have no command ID in the stub. They are handled directly or forwarded, probably to DBCServer_LOG for logs (I).
- Others reach a
DBReqfunction that calls the procedure deeper than two call levels.
How the mapping was built:
- Stub vtable slot k ↔ RMI ID
0x2ee1 + (k-7). This is the PIDL convention, verified on MessageServer (slots 7–14 = 0x3a99–0x3aa0) and by name agreement, e.g.REQ_READ_ALL_GUILD→up_read_all_guild. - Stub handler → command ID (constant in the packing call).
- Command ID →
DBReqfunction (jump table). DBReqfunction →"dbo.up_…"literal.- Procedure → database (schema dump).
Steps 1–4 are O, but slot ordering is I; it was checked by name
agreement rather than decoded from the failed stub dispatcher DBC:0x14000e4d0.
DBCServer embeds 297 dbo.* names. 273 match a procedure in the dump
case-sensitively; 10 more differ only in case (*Inquiry* vs *inquiry*,
harmless on a case-insensitive collation). The remaining 14 are absent from the
dump. Seven of them are reachable through mapped RMIs (listed above).
4. Response protocol (O)
DBC2SERVERS_PROTOCOL::Proxy in DBCServer has 76 RMIs, IDs 0x2af9–0x2b46.
decomp/DBCServer_Release_x64/rmi_tables.tsv lists them. This recovers the
table that failed to decompile in MessageServer (Dbc2ServersStub, 85 slots =
7 base + 78).
5. Open questions
- Trace which connection object each
DBReqreceives (per-queue connections vs. the two startup connections). Explaindb_log/logelements, which are parsed but have no matching connection type. - Resolve the 52 unmapped RMIs. Decode
DBC:0x14000e4d0from its jump table, as was done for the worker dispatcher. - Analyse
DBCServer_LOG_Release_x64.exe, the expected owner of theLOGRMIs and of RustyHearts_Log. - Map
DBC2SERVERSreplies to their requests. - Find who uses
GMRustyHeartsandRustyHearts_Account(web/launcher, outside these binaries? ?).