- 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>
80 lines
3.2 KiB
Python
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()
|