背包扩展到 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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
22d534c5eb
commit
e33e41c12b
@@ -0,0 +1,79 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user