Files
mtgodot-poc/tools/server/inventory-5-pages/migrate_db.py
T
shenleiandClaude Opus 5.5 e33e41c12b 背包扩展到 5 页:服务端补丁 + DB 迁移 + 客户端 mt_inventory
- GameType.h: c_Inventory_Page_Count 2 -> 5; SQuickSlot.Position BYTE -> WORD
- quickslot 包/接口 pos 改为 WORD(CG/GC 5 字节),wire_classic 同步
- mt_inventory PORT 模块:inventorywindow.py 装备格 90 -> EQUIPMENT_SLOT_START 重映射、5 个小页签、
  自动药水高亮与 SelectItemWindow 扫描全部 5 页;40250 脚本保持不变
- tools/server/inventory-5-pages: server.patch(格子布局 +135、quickslot WORD)、migrate_db.py、README

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:37:44 +09:00

80 lines
3.2 KiB
Python

#!/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()