DikuMUD with ROM 2.4
ROM 2.4 stores player data in binary flat files prone to corruption during crashes and difficult to query for analytics. This guide implements a MySQL/PostgreSQL backend for player persistence while maintaining compatibility with legacy rent and crash-recovery systems. You will modify the core save_char_obj and load_char_obj functions, build a migration utility for existing pfiles, and implement transaction-safe storage for equipment and affects.

Audit pfile dependencies across the codebase
Identify every location that reads or writes to PLAYER_DIR. Search for fread_char, fwrite_char, and direct fopen calls in save_char_obj, load_char_obj, and objsave.c. Document the struct char_data fields that persist to disk, paying special attention to the linked lists for affects, skills, and aliases that require special handling during SQL serialization.
⚠ Common Pitfalls
- •Missing obscure references in nanny() for new character creation
- •Forgetting that clan data and quest data may be stored in separate files
- •Assuming all equipment is handled by objsave.c—some ROM variants save worn items in pfiles
Design SQL schema mapping ROM structs to tables
Create a normalized schema: table `players` for core stats (hp, mana, move, exp, gold), table `player_skills` for skill percentages (indexed by player_id and sn), table `player_affects` for spell affects with expiration timestamps, and table `player_equipment` for rented items stored as JSON blobs or normalized rows. Match ROM's MAX_SKILL and MAX_GROUP constants to foreign key constraints.
CREATE TABLE players (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(32) UNIQUE NOT NULL,
level INT UNSIGNED,
exp BIGINT UNSIGNED,
hp INT, mana INT, move INT,
max_hp INT, max_mana INT, max_move INT,
gold INT, bank_gold INT,
played INT UNSIGNED,
last_logon TIMESTAMP,
INDEX (name)
);
CREATE TABLE player_skills (
player_id INT,
skill_num INT,
percent INT UNSIGNED,
PRIMARY KEY (player_id, skill_num),
FOREIGN KEY (player_id) REFERENCES players(id)
);Implement database connection layer
Add db.c and db.h to your src/ directory. Initialize the connection in boot_db() and close it in proper_exit(). Use prepared statements for all queries to prevent SQL injection from player names containing apostrophes. Implement connection pooling or persistent connections to avoid overhead during frequent autosaves.
/* db.h */
#ifndef _DB_H
#define _DB_H
#include <mysql/mysql.h>
extern MYSQL *mud_db;
void db_connect(void);
void db_disconnect(void);
MYSQL_STMT *db_prepare(const char *sql);
#endif
/* db.c */
MYSQL *mud_db = NULL;
void db_connect(void) {
mud_db = mysql_init(NULL);
if (!mysql_real_connect(mud_db, DB_HOST, DB_USER, DB_PASS, DB_NAME, 0, NULL, 0)) {
log_string("Database connection failed");
exit(1);
}
}Build pfile-to-SQL migration utility
Create a standalone migrator in migrate.c that reads existing binary pfiles from PLAYER_DIR using the legacy fread_char logic, then inserts into SQL tables using your new db layer. Run this against a copy of your player directory first. Log failed conversions separately for manual inspection—common failures include corrupt pfiles from 1990s-era endianness differences or truncated files from ancient crashes.
void migrate_player(const char *name) {
char fname[128];
struct char_data *ch;
FILE *fp;
sprintf(fname, "%s/%s", PLAYER_DIR, capitalize(name));
if ((fp = fopen(fname, "r")) == NULL) return;
ch = alloc_char();
if (fread_char(ch, fp) == NULL) {
logf("Corrupt pfile: %s", name);
fclose(fp);
return;
}
sql_insert_player(ch);
migrate_skills(ch);
migrate_affects(ch);
free_char(ch);
fclose(fp);
}Replace save_char_obj with SQL transactions
Modify save_char_obj to call sql_save_char() which uses REPLACE INTO or UPDATE queries wrapped in a transaction. For worn equipment, either serialize obj_to_store_list output to a TEXT column or maintain a separate player_equipment table. Ensure the function still respects IS_NPC checks and handles link-dead players correctly by checking ch->desc before attempting saves.
void save_char_obj(struct char_data *ch) {
if (IS_NPC(ch) || ch->level < 2) return;
mysql_query(mud_db, "START TRANSACTION");
if (sql_save_char(ch) < 0) {
mysql_query(mud_db, "ROLLBACK");
log_string("SQL save failed");
return;
}
sql_save_skills(ch);
sql_save_affects(ch);
sql_save_equipment(ch);
mysql_query(mud_db, "COMMIT");
}⚠ Common Pitfalls
- •Forgetting to handle ch->pcdata->pwd (password) hashing compatibility
- •Losing player data if transaction fails mid-save—always verify COMMIT success
- •Not handling case where ch->name contains SQL wildcards
Update rent and crash-save routines
The objsave.c rent code expects to write inventory to files in the rent directory. Redirect this to SQL BLOB columns or keep files for objects but store metadata in SQL. For crash-recovery, ensure that SQL writes are synchronous (innodb_flush_log_at_trx_commit=1) or implement a WAL log in your MUD to replay transactions if the database connection drops during a crash save.
⚠ Common Pitfalls
- •Rent code calculating item cost based on file existence—update pricing logic
- •Crash recovery assuming pfile timestamps match object file timestamps—use SQL NOW()
- •Carry-over limits not being enforced if SQL insert succeeds but object storage fails
Implement dual-write verification mode
Before fully committing to SQL, run both systems in parallel for one week: write to binary pfiles as usual, but also write to SQL. Compare the SQL data against the binary files during off-peak hours using a verification script. Check for discrepancies in gold amounts, skill percentages, and affect durations. Fix encoding issues (Latin-1 vs UTF-8) in player descriptions before they corrupt your database.
⚠ Common Pitfalls
- •Character encoding mismatches causing truncation in player descriptions
- •Time zone mismatches between ROM's time() calls and SQL timestamps
- •Floating-point precision loss on double values like practiced percentages
Create rollback procedures and backups
Maintain a script that can export SQL tables back to the legacy binary pfile format. Keep the old PLAYER_DIR mounting point active as a symlink to a backup location. If SQL performance degrades under player load or corruption occurs, you must be able to revert to binary mode within 15 minutes without data loss. Document the exact MySQL/PostgreSQL version requirements for your schema.
⚠ Common Pitfalls
- •Assuming rollback is unnecessary—always prepare for query performance disasters
- •Not backing up SQL grants and user permissions separately from data
- •Forgetting that binary pfiles store some data (like aliases) that SQL might normalize differently
Deploy with monitoring and maintenance windows
Schedule downtime during low-traffic periods (typically early morning server time). Run the migrator on live data one final time, then switch the codebase to SQL mode by flipping a #define SQL_BACKEND flag. Monitor slow_query logs and implement EXPLAIN analysis on the player_skills table which will see heavy read load during login. Set up a daily mysqldump or pg_dump that runs before your automated backup window.
⚠ Common Pitfalls
- •Locking tables during backup causing player login delays
- •Not indexing player_names causing O(n) lookup times during who lists
- •Running migrations during peak hours causing connection pool exhaustion
What you built
After completing migration, monitor slow query logs for the first week as player counts scale. Keep the legacy pfile backup system active for one month as a rollback safety net. Once stable, remove binary I/O code to reduce attack surface. Document your specific SQL schema version in a CHANGELOG for future codebase merges.