#!/usr/bin/env python3 """Migrate the 40250 player DB to the 5-page bag (server.patch). Run once, with db/game stopped. python3 migrate_db.py [--dry-run] [mysql args...] (default mysql args: -uroot player) - Belt-inventory items are stored as window INVENTORY at absolute cells 152..167; they move to 287..302 (every cell after the bag moves by +135). Worn and dragon-soul-equipped items are stored relative to their EQUIPMENT window and do not move. - player.quickslot / player_deleted.quickslot hold TQuickslot[36] raw: {BYTE type; BYTE pos} (72 bytes) becomes {BYTE type; WORD pos} (108 bytes). Item quick slots (type 1) on a belt cell get +135 too. A row in table mt_migration records the run; a second run changes nothing. """ import subprocess import sys NAME = "inventory-5-pages" QUICKSLOT_MAX_NUM = 36 QUICKSLOT_TYPE_ITEM = 1 OLD_BELT = range(152, 168) SHIFT = 135 def mysql(args, sql): out = subprocess.run(["mysql", "-N", "-B"] + args, input=sql, capture_output=True, text=True) if out.returncode: sys.exit("mysql failed: " + out.stderr.strip()) return [line.split("\t") for line in out.stdout.splitlines()] def convert(blob): assert len(blob) == QUICKSLOT_MAX_NUM * 2, len(blob) out = bytearray() for i in range(QUICKSLOT_MAX_NUM): kind, pos = blob[2 * i], blob[2 * i + 1] if kind == QUICKSLOT_TYPE_ITEM and pos in OLD_BELT: pos += SHIFT out += bytes([kind, pos & 0xFF, pos >> 8]) return bytes(out) def main(): argv = sys.argv[1:] dry = "--dry-run" in argv args = [a for a in argv if a != "--dry-run"] or ["-uroot", "player"] mysql(args, "CREATE TABLE IF NOT EXISTS mt_migration (name VARCHAR(64) PRIMARY KEY, done DATETIME)") if mysql(args, "SELECT name FROM mt_migration WHERE name='%s'" % NAME): print("%s: already applied" % NAME) return sql = ["UPDATE item SET pos = pos + %d WHERE window = 'INVENTORY' AND pos BETWEEN %d AND %d;" % (SHIFT, OLD_BELT[0], OLD_BELT[-1])] belt = mysql(args, "SELECT COUNT(*) FROM item WHERE window='INVENTORY' AND pos BETWEEN %d AND %d" % (OLD_BELT[0], OLD_BELT[-1]))[0][0] bad = mysql(args, "SELECT COUNT(*) FROM item WHERE window='INVENTORY' AND pos BETWEEN 90 AND 151")[0][0] if bad != "0": sys.exit("%s INVENTORY items at cells 90..151, which 40250 does not use; stop" % bad) rows = 0 for table in ("player", "player_deleted"): for pid, blob in mysql(args, "SELECT id, HEX(quickslot) FROM %s" % table): data = bytes.fromhex(blob) if not data: continue if len(data) != QUICKSLOT_MAX_NUM * 2: sys.exit("%s %s: quickslot is %d bytes, expected %d" % (table, pid, len(data), QUICKSLOT_MAX_NUM * 2)) sql.append("UPDATE %s SET quickslot = X'%s' WHERE id = %s;" % (table, convert(data).hex().upper(), pid)) rows += 1 sql.append("INSERT INTO mt_migration VALUES ('%s', NOW());" % NAME) print("%s: %s belt item(s), %d quickslot row(s)" % (NAME, belt, rows)) if dry: print("\n".join(sql)) return mysql(args, "\n".join(sql)) print("%s: applied" % NAME) if __name__ == "__main__": main()