From e33e41c12bf8b41cc9c63c340f79e9a0923257dd Mon Sep 17 00:00:00 2001 From: shenlei Date: Tue, 29 Sep 2026 22:37:44 +0900 Subject: [PATCH] =?UTF-8?q?=E8=83=8C=E5=8C=85=E6=89=A9=E5=B1=95=E5=88=B0?= =?UTF-8?q?=205=20=E9=A1=B5=EF=BC=9A=E6=9C=8D=E5=8A=A1=E7=AB=AF=E8=A1=A5?= =?UTF-8?q?=E4=B8=81=20+=20DB=20=E8=BF=81=E7=A7=BB=20+=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=20mt=5Finventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/PORT-PLAN.md | 6 +- .../ScriptLib/InventoryPagesScript.inc | 225 +++++++++++ src/platform/ScriptLib/PythonBoot.cpp | 2 + src/port/UserInterface/AbstractPlayer.h | 2 +- src/port/UserInterface/GameType.h | 4 +- src/port/UserInterface/PythonNetworkStream.h | 2 +- .../PythonNetworkStreamPhaseGameItem.cpp | 2 +- src/port/UserInterface/PythonPlayer.cpp | 6 +- src/port/UserInterface/PythonPlayer.h | 2 +- tests/port/classic/wire_classic.h | 8 +- tools/server/inventory-5-pages/README.md | 39 ++ tools/server/inventory-5-pages/migrate_db.py | 79 ++++ tools/server/inventory-5-pages/server.patch | 362 ++++++++++++++++++ 13 files changed, 724 insertions(+), 15 deletions(-) create mode 100644 src/platform/ScriptLib/InventoryPagesScript.inc create mode 100644 tools/server/inventory-5-pages/README.md create mode 100644 tools/server/inventory-5-pages/migrate_db.py create mode 100644 tools/server/inventory-5-pages/server.patch diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index b0d5eaf8..ffdcff4b 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -42,6 +42,7 @@ metin2-client 是 40250 Windows 客户端的跨平台版本。**首要目标是 | 2026-09-28 | Android 原生客户端优先,iOS 延后 | 目标用户在手机上 | | 2026-09-28 | 代码页转换用内置 WindowsBestFit 表(`mt_codepage`),不再用 iconv | Android API 24 没有可用的 iconv;CP949/GBK 结果与 Windows 一致 | | 2026-09-29 | 移动端专有功能(触控、自动狩猎、账号注册、帧率设置)以 PORT 模块形式注入,不改 40250 脚本 | 40250 脚本字节保持不变,账本 `RUN_AS_IS` 仍成立 | +| 2026-09-29 | **背包扩到 5 页**(225 格,40250 为 2 页 90 格):服务端补丁 + DB 迁移在 `tools/server/inventory-5-pages/`,客户端 `c_Inventory_Page_Count = 5`、`TQuickSlot.Position` 改 WORD、`mt_inventory` 模块补 5 个页签和写死 2 页的脚本;装备/龙魂/腰带格整体后移 135,快捷栏封包长度变化,旧客户端不再兼容 | 用户要求的玩法扩展;203 已部署并实测 | ## 3. 三个层和代码位置 @@ -112,10 +113,11 @@ SDL3 宿主主循环(src/host/live_client.cpp run_live_client) 的原写法改成 `uintptr_t`,在 port-map 的 `note` 里记录适配不变式。 - 单位保持 40250 的(`TPixelPosition` 用 cm,时间是 `ELTimer_GetMSec` 的毫秒,角度用度),只在适配层换算。 -PORT 扩展(40250 没有对应物,只为移动端/平台而加;不能改 40250 脚本和玩法语义): +PORT 扩展(40250 没有对应物,只为移动端/平台而加;不能改 40250 脚本;玩法语义只在用户明确要求时扩展,如 5 页背包): - 由 `PythonBoot::RunMainScript` 注入的 Python 模块:`mt_autohunt`(自动狩猎,`AutoHuntScript.inc`)、`mt_register` - (客户端内账号注册)、`mt_framerate`(系统选项里的"画面帧率"行)。它们包装 40250 窗口类的方法,不修改脚本文件。 + (客户端内账号注册)、`mt_framerate`(系统选项里的"画面帧率"行)、`mt_inventory`(5 页背包:改写 `InventoryWindow.py` 的装备格起点和页签, + 泛化翻页、自动药水高亮和选宝石窗口的格子扫描)。它们包装 40250 窗口类的方法,不修改脚本文件。 - 触控控制(`src/host/touch_controller.h`):摇杆、攻击/技能按钮,转成 40250 的输入接口。 - 帧率与功耗(`platform/EterBase/FrameRateMode.h` + `src/host/frame_policy.cpp` 的 `FrameRatePolicy`):30 / 60(默认)/ 显示器最高三档, 无输入一段时间降到 30 fps(自动狩猎时除外),Android 上配合 `setFrameRate` 和 ADPF。 diff --git a/src/platform/ScriptLib/InventoryPagesScript.inc b/src/platform/ScriptLib/InventoryPagesScript.inc new file mode 100644 index 00000000..208f02c2 --- /dev/null +++ b/src/platform/ScriptLib/InventoryPagesScript.inc @@ -0,0 +1,225 @@ +// The 5-page bag (PORT; 40250 has 2 pages of 45 cells), run by PythonBoot::RunMainScript as module +// mt_inventory. GameType.h's c_Inventory_Page_Count = 5 matches the 203 server patch +// (tools/server/inventory-5-pages), which moves every cell after the bag from 90 to 225. The 40250 scripts +// stay unchanged; this module patches what they hard-code for 2 pages: +// - InventoryWindow.py: EQUIPMENT_START_INDEX = 90 is remapped to player.EQUIPMENT_SLOT_START, and the two +// large page tabs become five small ones (the equipment tabs' image) across the 5x32 grid; +// - uiInventory.InventoryWindow: five tab events, SetInventoryPage's `inventoryTab[1-page]`, and the +// auto-potion highlight in RefreshBagSlotWindow, which only subtracts one page; +// - uiselectitem.SelectItemWindow.RefreshSlot: scans INVENTORY_PAGE_SIZE*2 cells. +static const char c_szInventoryPagesScript[] = R"PY(# -*- coding: utf-8 -*- +import constInfo +import player +import ui +import uiInventory +import uiselectitem +import item + +OLD_EQUIPMENT_START = 90 +OLD_EQUIPMENT_END = OLD_EQUIPMENT_START + 32 +PAGE_COUNT = player.INVENTORY_PAGE_COUNT +PAGE_TEXTS = ("I", "II", "III", "IV", "V") +SMALL_TAB = "d:/ymir work/ui/game/windows/tab_button_small_%02d.sub" +TAB_X = 8 +TAB_W = 32 + + +def _Children(node): + return node.get("children", ()) + + +def _Find(node, name): + if node.get("name") == name: + return node + for child in _Children(node): + found = _Find(child, name) + if found: + return found + return None + + +def _PageTab(page, x, y, tooltip): + tab = { + "name" : "Inventory_Tab_%02d" % (page + 1), + "type" : "radio_button", + "x" : x, + "y" : y, + "default_image" : SMALL_TAB % 1, + "over_image" : SMALL_TAB % 2, + "down_image" : SMALL_TAB % 3, + "children" : ( + { + "name" : "Inventory_Tab_%02d_Print" % (page + 1), + "type" : "text", + "x" : 0, + "y" : 0, + "all_align" : "center", + "text" : PAGE_TEXTS[page], + }, + ), + } + if tooltip: + tab["tooltip_text"] = tooltip + return tab + + +def _PatchInventoryWindow(window): + delta = player.EQUIPMENT_SLOT_START - OLD_EQUIPMENT_START + equip = _Find(window, "EquipmentSlot") + if equip and delta: + slots = [] + for slot in equip["slot"]: + if OLD_EQUIPMENT_START <= slot["index"] < OLD_EQUIPMENT_END: + slot = dict(slot) + slot["index"] += delta + slots.append(slot) + equip["slot"] = tuple(slots) + + board = _Find(window, "board") + first = _Find(window, "Inventory_Tab_01") + if not board or not first: + return + tooltips = [first.get("tooltip_text")] + second = _Find(window, "Inventory_Tab_02") + tooltips.append(second.get("tooltip_text") if second else None) + children = [] + for child in board["children"]: + name = child.get("name", "") + if name.startswith("Inventory_Tab_"): + if name == "Inventory_Tab_01": + for page in xrange(PAGE_COUNT): + tip = tooltips[page] if page < len(tooltips) else None + children.append(_PageTab(page, TAB_X + TAB_W * page, first["y"], tip)) + continue + children.append(child) + board["children"] = tuple(children) + + +def _InstallScriptPatch(): + execfile = ui.execfile + + def ExecFile(fileName, dict): + execfile(fileName, dict) + if fileName.replace("\\", "/").split("/")[-1].lower() == "inventorywindow.py" and "window" in dict: + _PatchInventoryWindow(dict["window"]) + + ui.execfile = ExecFile + + +def _InstallInventoryWindow(): + cls = uiInventory.InventoryWindow + load = cls._InventoryWindow__LoadWindow + refresh = cls.RefreshBagSlotWindow + + def LoadWindow(self): + loaded = self.isLoaded + load(self) + if loaded: + return + try: + self.inventoryTab = [self.GetChild("Inventory_Tab_%02d" % (page + 1)) for page in xrange(PAGE_COUNT)] + for page in xrange(PAGE_COUNT): + self.inventoryTab[page].SetEvent(lambda arg=page: self.SetInventoryPage(arg)) + self.inventoryTab[0].Down() + except: + import dbg + dbg.TraceError("mt_inventory: cannot bind the page tabs") + + def SetInventoryPage(self, page): + self.inventoryPageIndex = page + for i in xrange(len(self.inventoryTab)): + if i != page: + self.inventoryTab[i].SetUp() + self.RefreshBagSlotWindow() + + def RefreshBagSlotWindow(self): + refresh(self) + page = getattr(self, "inventoryPageIndex", 0) + if page < 2: + return + # 40250 turns a cell into a slot by subtracting one page, so pages 3-5 highlight nothing. + for i in xrange(player.INVENTORY_PAGE_SIZE): + slotNumber = page * player.INVENTORY_PAGE_SIZE + i + if not player.GetItemCount(slotNumber): + continue + if not constInfo.IS_AUTO_POTION(player.GetItemIndex(slotNumber)): + continue + if player.GetItemMetinSocket(slotNumber, 0): + self.wndItem.ActivateSlot(i) + else: + self.wndItem.DeactivateSlot(i) + self.wndItem.RefreshSlot() + + cls._InventoryWindow__LoadWindow = LoadWindow + cls.SetInventoryPage = SetInventoryPage + cls.RefreshBagSlotWindow = RefreshBagSlotWindow + + +def _InstallSelectItem(): + # SelectItemWindow.RefreshSlot with the cell loop over every page instead of INVENTORY_PAGE_SIZE*2. + def RefreshSlot(self): + slotPos = 0 + self.inventorySlotPosDict = {} + + getItemVNum=player.GetItemIndex + getItemCount=player.GetItemCount + setItemVNum=self.itemSlot.SetItemSlot + + for i in xrange(player.INVENTORY_PAGE_SIZE*PAGE_COUNT): + slotNumber = i + + itemVNum = getItemVNum(slotNumber) + if 0 == itemVNum: + continue + + if not item.IsMetin(itemVNum): + continue + + itemGrade = player.GetItemGrade(slotNumber) + if itemGrade > 2: + continue + + self.inventorySlotPosDict[slotPos] = i + + slotPos += 1 + + if slotPos > 54: + break + + itemCount = len(self.inventorySlotPosDict) + if itemCount < 15: + self.SetTableSize(3) + + else: + lineCount = 3 + lineCount += (itemCount - 15) / 5 + if itemCount % 5: + lineCount += 1 + self.SetTableSize(lineCount) + + for selectWndPos, inventoryPos in self.inventorySlotPosDict.items(): + itemVNum = getItemVNum(inventoryPos) + itemCount = getItemCount(inventoryPos) + + if itemCount <= 1: + itemCount = 0 + + setItemVNum(selectWndPos, itemVNum, itemCount) + + self.itemSlot.RefreshSlot() + + uiselectitem.SelectItemWindow.RefreshSlot = RefreshSlot + + +def __Install(): + if PAGE_COUNT <= 2: + return + for install in (_InstallScriptPatch, _InstallInventoryWindow, _InstallSelectItem): + try: + install() + except: + import dbg + dbg.TraceError("mt_inventory: %s failed" % install.__name__) + +__Install() +)PY"; diff --git a/src/platform/ScriptLib/PythonBoot.cpp b/src/platform/ScriptLib/PythonBoot.cpp index 3f469d9b..7aad295d 100644 --- a/src/platform/ScriptLib/PythonBoot.cpp +++ b/src/platform/ScriptLib/PythonBoot.cpp @@ -877,6 +877,7 @@ namespace PythonBoot #include "AutoHuntScript.inc" #include "RegisterScript.inc" #include "FrameRateOptionScript.inc" +#include "InventoryPagesScript.inc" static void install_script_module(const char* c_szModule, const char* c_szScript) { @@ -933,6 +934,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error) install_script_module("mt_autohunt", c_szAutoHuntScript); install_script_module("mt_register", c_szRegisterScript); install_script_module("mt_framerate", c_szFrameRateOptionScript); + install_script_module("mt_inventory", c_szInventoryPagesScript); } return true; } diff --git a/src/port/UserInterface/AbstractPlayer.h b/src/port/UserInterface/AbstractPlayer.h index bbfa6e9e..fc2198db 100644 --- a/src/port/UserInterface/AbstractPlayer.h +++ b/src/port/UserInterface/AbstractPlayer.h @@ -41,7 +41,7 @@ class IAbstractPlayer : public TAbstractSingleton virtual bool IsEquipItemInSlot(TItemPos itemPos) = 0; - virtual void AddQuickSlot(int QuickslotIndex, char IconType, char IconPosition) = 0; + virtual void AddQuickSlot(int QuickslotIndex, char IconType, WORD IconPosition) = 0; virtual void DeleteQuickSlot(int QuickslotIndex) = 0; virtual void MoveQuickSlot(int Source, int Target) = 0; diff --git a/src/port/UserInterface/GameType.h b/src/port/UserInterface/GameType.h index e2f17e12..7c8fa39f 100644 --- a/src/port/UserInterface/GameType.h +++ b/src/port/UserInterface/GameType.h @@ -38,7 +38,7 @@ const DWORD c_FileName_Max_Length = 128; const DWORD c_Short_Name_Max_Length = 32; const DWORD c_Inventory_Page_Size = 5*9; // x*y -const DWORD c_Inventory_Page_Count = 2; +const DWORD c_Inventory_Page_Count = 5; // PORT: 2 in 40250; the 203 server patch widens the bag to 5 pages (tools/server/inventory-5-pages) const DWORD c_ItemSlot_Count = c_Inventory_Page_Size * c_Inventory_Page_Count; const DWORD c_Equipment_Count = 12; @@ -306,7 +306,7 @@ enum typedef struct SQuickSlot { BYTE Type; - BYTE Position; + WORD Position; // PORT: BYTE in 40250; cells past 255 once the bag has 5 pages } TQuickSlot; typedef struct TPlayerItemAttribute diff --git a/src/port/UserInterface/PythonNetworkStream.h b/src/port/UserInterface/PythonNetworkStream.h index f87e26f6..70e1f701 100644 --- a/src/port/UserInterface/PythonNetworkStream.h +++ b/src/port/UserInterface/PythonNetworkStream.h @@ -155,7 +155,7 @@ class CPythonNetworkStream : public CNetworkStream, public CSingleton= QUICKSLOT_MAX_NUM) return; diff --git a/src/port/UserInterface/PythonPlayer.h b/src/port/UserInterface/PythonPlayer.h index 8e4c91f3..bd92388c 100644 --- a/src/port/UserInterface/PythonPlayer.h +++ b/src/port/UserInterface/PythonPlayer.h @@ -317,7 +317,7 @@ class CPythonPlayer : public CSingleton, public IAbstractPlayer // Quickslot int GetQuickPage(); void SetQuickPage(int nPageIndex); - void AddQuickSlot(int QuickslotIndex, char IconType, char IconPosition); + void AddQuickSlot(int QuickslotIndex, char IconType, WORD IconPosition); void DeleteQuickSlot(int QuickslotIndex); void MoveQuickSlot(int Source, int Target); diff --git a/tests/port/classic/wire_classic.h b/tests/port/classic/wire_classic.h index 4d0d5d29..33aecb36 100644 --- a/tests/port/classic/wire_classic.h +++ b/tests/port/classic/wire_classic.h @@ -728,10 +728,10 @@ struct CGItemUseToItem { uint8_t header; ItemPos pos; ItemPos target_pos; }; static_assert(sizeof(CGItemUseToItem) == 7); struct CGGiveItem { uint8_t header; uint32_t target_vid; ItemPos pos; uint8_t count; }; static_assert(sizeof(CGGiveItem) == 9); -struct CGQuickSlotAdd { uint8_t header; uint8_t pos; uint8_t type; uint8_t position; }; +struct CGQuickSlotAdd { uint8_t header; uint8_t pos; uint8_t type; uint16_t position; }; // position WORD: 5-page bag struct CGQuickSlotDel { uint8_t header; uint8_t pos; }; struct CGQuickSlotSwap { uint8_t header; uint8_t pos; uint8_t pos_to; }; -static_assert(sizeof(CGQuickSlotAdd) == 4 && sizeof(CGQuickSlotDel) == 2 && +static_assert(sizeof(CGQuickSlotAdd) == 5 && sizeof(CGQuickSlotDel) == 2 && sizeof(CGQuickSlotSwap) == 3); // --- P8 social / exchange / storage intents ------------------------------- @@ -1049,11 +1049,11 @@ static_assert(sizeof(PlayerSkill3) == 6); struct GCSkillLevel { uint8_t header; PlayerSkill3 skills[SKILL_MAX_NUM]; }; // header 76 static_assert(sizeof(GCSkillLevel) == 1 + 255 * 6); // 1531 -struct QuickSlot2 { uint8_t type; uint8_t position; }; // TQuickSlot / TQuickslot +struct QuickSlot2 { uint8_t type; uint16_t position; }; // TQuickSlot / TQuickslot (position WORD: 5-page bag) struct GCQuickSlotAdd { uint8_t header; uint8_t pos; QuickSlot2 slot; }; // header 28 struct GCQuickSlotDel { uint8_t header; uint8_t pos; }; // header 29 struct GCQuickSlotSwap { uint8_t header; uint8_t pos; uint8_t pos_to; }; // header 30 -static_assert(sizeof(GCQuickSlotAdd) == 4 && sizeof(GCQuickSlotDel) == 2 && +static_assert(sizeof(GCQuickSlotAdd) == 5 && sizeof(GCQuickSlotDel) == 2 && sizeof(GCQuickSlotSwap) == 3); // --- GC affects --- diff --git a/tools/server/inventory-5-pages/README.md b/tools/server/inventory-5-pages/README.md new file mode 100644 index 00000000..feeef6d9 --- /dev/null +++ b/tools/server/inventory-5-pages/README.md @@ -0,0 +1,39 @@ +# 背包 5 页(服务端) + +40250 背包是 2 页 × 45 格(`INVENTORY_MAX_NUM = 90`)。本补丁改为 5 页 225 格,客户端对应改动见 +`src/port/UserInterface/GameType.h`(`c_Inventory_Page_Count`)和 `src/platform/ScriptLib/InventoryPagesScript.inc`。 + +## 格子布局 + +| 区段 | 40250 | 5 页 | +| --- | --- | --- | +| 背包 | 0..89 | 0..224 | +| 穿戴(32) | 90..121 | 225..256 | +| 龙魂装备(12) | 122..133 | 257..268 | +| 龙魂预留(18) | 134..151 | 269..286 | +| 腰带背包(16) | 152..167 | 287..302 | + +背包之后的格子整体 +135,超过 255,所以 `TQuickslot.pos` 从 BYTE 改成 WORD(快捷栏封包变成 5 字节, +`player.quickslot` 从 72 字节变成 108 字节)。**旧客户端不兼容**,必须和新客户端一起上线。 + +## 文件 + +- `server.patch`:对 40250 `src/server` 的改动(`common/length.h`、`common/tables.h`、`game/src` 里把格子号存成 BYTE 的地方、 + `exchange.cpp` 的每页空间检查)。在 `/usr/metin2/src` 下 `patch -p1 < server.patch`。源码是 EUC-KR + CRLF,补丁按字节保存。 +- `migrate_db.py`:DB 迁移,停服后运行一次(`python3.9 migrate_db.py [--dry-run] [mysql 参数]`,默认 `-uroot player`)。 + 腰带物品(`window='INVENTORY'` 且 `pos` 152..167)+135;`player`/`player_deleted` 的 `quickslot` 转成 WORD 布局, + 指向腰带格的物品快捷栏同样 +135。结果记入 `mt_migration` 表,重复运行不会再改。穿戴/龙魂装备按 EQUIPMENT 窗口的相对位置存,不受影响。 + +## 构建与部署(203,FreeBSD 13.2 i386) + +Makefile 默认把二进制移到 `share/bin`(线上正在用),构建时要改输出目录: + +```sh +cd /usr/metin2/src/server/game/src && gmake -j2 GAMEDIR=/root/inv5-out/new +cd ../../db/src && gmake -j2 DBDIR=/root/inv5-out/new +``` + +部署顺序:`close.sh` → 备份 `player` 库 → `migrate_db.py` → 覆盖 `share/bin/{game,db}` → `echo 1 | sh start.sh`。 +auth 用的也是 `game` 二进制;`TPlayerTable` 含快捷栏数组,所以 game 和 db 必须一起换。 + +2026-09-29 已在 203 部署。备份在 `/root/backup-inv5/`(源码 tgz、原 game/db、迁移前的 player 库)。回滚:停服,恢复这三样。 diff --git a/tools/server/inventory-5-pages/migrate_db.py b/tools/server/inventory-5-pages/migrate_db.py new file mode 100644 index 00000000..c20264a3 --- /dev/null +++ b/tools/server/inventory-5-pages/migrate_db.py @@ -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() diff --git a/tools/server/inventory-5-pages/server.patch b/tools/server/inventory-5-pages/server.patch new file mode 100644 index 00000000..29c80a13 --- /dev/null +++ b/tools/server/inventory-5-pages/server.patch @@ -0,0 +1,362 @@ +diff --git a/server/common/length.h b/server/common/length.h +index fba03e3..f4cafc3 100755 +--- a/server/common/length.h ++++ b/server/common/length.h +@@ -12,7 +12,9 @@ enum EMisc + ACCOUNT_STATUS_MAX_LEN = 8, + CHARACTER_NAME_MAX_LEN = 24, + SHOP_SIGN_MAX_LEN = 32, +- INVENTORY_MAX_NUM = 90, ++ INVENTORY_PAGE_SIZE = 45, // 5 x 9 ++ INVENTORY_PAGE_COUNT = 5, // MT: 40250 ships 2 pages (90 cells) ++ INVENTORY_MAX_NUM = INVENTORY_PAGE_SIZE * INVENTORY_PAGE_COUNT, + ABILITY_MAX_NUM = 50, + EMPIRE_MAX_NUM = 4, + BANWORD_MAX_LEN = 24, +@@ -87,15 +89,15 @@ enum EMisc + **** ÇöÀç±îÁö ÇҴç µÈ ¾ÆÀÌÅÛ ¿µ¿ª Á¤¸® (DB»ó Item Position) **** + +------------------------------------------------------+ 0 + | ij¸¯ÅÍ ±⺻ Àκ¥Å丮 (45ĭ * 2ÆäÀÌÁö) 90ĭ | +- +------------------------------------------------------+ 90 = INVENTORY_MAX_NUM(90) ++ +------------------------------------------------------+ 225 = INVENTORY_MAX_NUM(225) MT: 5 pages; 40250 had 90 and every cell below moved by +135 + | ij¸¯ÅÍ Àåºñ â (Âø¿ëÁßÀÎ ¾ÆÀÌÅÛ) 32ĭ | +- +------------------------------------------------------+ 122 = INVENTORY_MAX_NUM(90) + WEAR_MAX_NUM(32) ++ +------------------------------------------------------+ 257 = INVENTORY_MAX_NUM(225) + WEAR_MAX_NUM(32) + | ¿ëȥ¼® Àåºñ â (Âø¿ëÁßÀÎ ¿ëȥ¼®) 12ĭ | +- +------------------------------------------------------+ 134 = 122 + DS_SLOT_MAX(6) * DRAGON_SOUL_DECK_MAX_NUM(2) ++ +------------------------------------------------------+ 269 = 257 + DS_SLOT_MAX(6) * DRAGON_SOUL_DECK_MAX_NUM(2) + | ¿ëȥ¼® Àåºñ â ¿¹¾à (¾ÆÁ÷ ¹̻ç¿ë) 18ĭ | +- +------------------------------------------------------+ 152 = 134 + DS_SLOT_MAX(6) * DRAGON_SOUL_DECK_RESERVED_MAX_NUM(3) ++ +------------------------------------------------------+ 287 = 269 + DS_SLOT_MAX(6) * DRAGON_SOUL_DECK_RESERVED_MAX_NUM(3) + | º§Ʈ Àκ¥Å丮 (º§Ʈ Âø¿ë½ÿ¡¸¸ º§Ʈ ·¹º§¿¡ µû¶ó Ȱ¼º)| +- +------------------------------------------------------+ 168 = 152 + BELT_INVENTORY_SLOT_COUNT(16) = INVENTORY_AND_EQUIP_CELL_MAX ++ +------------------------------------------------------+ 303 = 287 + BELT_INVENTORY_SLOT_COUNT(16) = INVENTORY_AND_EQUIP_CELL_MAX + | ¹̻ç¿ë | + +------------------------------------------------------+ ?? + */ +diff --git a/server/common/tables.h b/server/common/tables.h +index 6f7adac..8767f3d 100755 +--- a/server/common/tables.h ++++ b/server/common/tables.h +@@ -403,7 +403,7 @@ typedef struct SPlayerItem + typedef struct SQuickslot + { + BYTE type; +- BYTE pos; ++ WORD pos; // MT: BYTE in 40250; belt cells are >= 256 with 5 inventory pages + } TQuickslot; + + typedef struct SPlayerSkill +diff --git a/server/game/src/char.h b/server/game/src/char.h +index 6eca64e..a6cba74 100755 +--- a/server/game/src/char.h ++++ b/server/game/src/char.h +@@ -395,7 +395,7 @@ typedef struct character_point_instant + WORD parts[PART_MAX_NUM]; + + LPITEM pItems[INVENTORY_AND_EQUIP_SLOT_MAX]; +- BYTE bItemGrid[INVENTORY_AND_EQUIP_SLOT_MAX]; ++ WORD bItemGrid[INVENTORY_AND_EQUIP_SLOT_MAX]; // MT: BYTE in 40250; holds cell + 1, and cells reach 302 + + // ¿ëȥ¼® Àκ¥Å丮. + LPITEM pDSItems[DRAGON_SOUL_INVENTORY_MAX_NUM]; +@@ -877,12 +877,12 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider + + // Quickslot °ü·Ã + public: +- void SyncQuickslot(BYTE bType, BYTE bOldPos, BYTE bNewPos); ++ void SyncQuickslot(BYTE bType, WORD bOldPos, WORD bNewPos); + bool GetQuickslot(BYTE pos, TQuickslot ** ppSlot); + bool SetQuickslot(BYTE pos, TQuickslot & rSlot); + bool DelQuickslot(BYTE pos); + bool SwapQuickslot(BYTE a, BYTE b); +- void ChainQuickslotItem(LPITEM pItem, BYTE bType, BYTE bOldPos); ++ void ChainQuickslotItem(LPITEM pItem, BYTE bType, WORD bOldPos); + + protected: + TQuickslot m_quickslot[QUICKSLOT_MAX_NUM]; +@@ -1120,7 +1120,7 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider + // Âø¿ëÁßÀÎ itemÀ» ¹þÀ» ¼ö Àִ Áö ȮÀÎÇϰí, ºҰ¡´É Çϴٸé ij¸¯ÅͿ¡°Ô ÀÌÀ¯¸¦ ¾˷ÁÁִ ÇԼö + bool CanUnequipNow(const LPITEM item, const TItemPos& srcCell = NPOS, const TItemPos& destCell = NPOS); + +- bool SwapItem(BYTE bCell, BYTE bDestCell); ++ bool SwapItem(WORD bCell, WORD bDestCell); + LPITEM AutoGiveItem(DWORD dwItemVnum, BYTE bCount=1, int iRarePct = -1, bool bMsg = true); + void AutoGiveItem(LPITEM item, bool longOwnerShip = false); + +diff --git a/server/game/src/char_item.cpp b/server/game/src/char_item.cpp +index 0c2403f..a77696c 100755 +--- a/server/game/src/char_item.cpp ++++ b/server/game/src/char_item.cpp +@@ -523,7 +523,7 @@ bool CHARACTER::IsEmptyItemGrid(TItemPos Cell, BYTE bSize, int iExceptionCell) c + { + case INVENTORY: + { +- BYTE bCell = Cell.cell; ++ WORD bCell = Cell.cell; + + // bItemCellÀº 0ÀÌ falseÀÓÀ» ³ªŸ³»±â À§ÇØ + 1 Çؼ­ ó¸®ÇѴÙ. + // µû¶ó¼­ iExceptionCell¿¡ 1À» ´õÇØ ºñ±³ÇѴÙ. +@@ -562,16 +562,16 @@ bool CHARACTER::IsEmptyItemGrid(TItemPos Cell, BYTE bSize, int iExceptionCell) c + return true; + + int j = 1; +- BYTE bPage = bCell / (INVENTORY_MAX_NUM / 2); ++ int bPage = bCell / INVENTORY_PAGE_SIZE; + + do + { +- BYTE p = bCell + (5 * j); ++ int p = bCell + (5 * j); + + if (p >= INVENTORY_MAX_NUM) + return false; + +- if (p / (INVENTORY_MAX_NUM / 2) != bPage) ++ if (p / INVENTORY_PAGE_SIZE != bPage) + return false; + + if (m_pointsInstant.bItemGrid[p]) +@@ -592,16 +592,16 @@ bool CHARACTER::IsEmptyItemGrid(TItemPos Cell, BYTE bSize, int iExceptionCell) c + else + { + int j = 1; +- BYTE bPage = bCell / (INVENTORY_MAX_NUM / 2); ++ int bPage = bCell / INVENTORY_PAGE_SIZE; + + do + { +- BYTE p = bCell + (5 * j); ++ int p = bCell + (5 * j); + + if (p >= INVENTORY_MAX_NUM) + return false; + +- if (p / (INVENTORY_MAX_NUM / 2) != bPage) ++ if (p / INVENTORY_PAGE_SIZE != bPage) + return false; + + if (m_pointsInstant.bItemGrid[p]) +@@ -924,7 +924,7 @@ bool CHARACTER::DoRefine(LPITEM item, bool bMoneyOnly) + ITEM_MANAGER::CopyAllAttrTo(item, pkNewItem); + LogManager::instance().ItemLog(this, pkNewItem, "REFINE SUCCESS", pkNewItem->GetName()); + +- BYTE bCell = item->GetCell(); ++ WORD bCell = item->GetCell(); + + // DETAIL_REFINE_LOG + NotifyRefineSuccess(this, item, IsRefineThroughGuild() ? "GUILD" : "POWER"); +@@ -1197,7 +1197,7 @@ bool CHARACTER::DoRefineWithScroll(LPITEM item) + ITEM_MANAGER::CopyAllAttrTo(item, pkNewItem); + LogManager::instance().ItemLog(this, pkNewItem, "REFINE SUCCESS", pkNewItem->GetName()); + +- BYTE bCell = item->GetCell(); ++ WORD bCell = item->GetCell(); + + NotifyRefineSuccess(this, item, szRefineType); + DBManager::instance().SendMoneyLog(MONEY_LOG_REFINE, item->GetVnum(), -prt->cost); +@@ -1226,7 +1226,7 @@ bool CHARACTER::DoRefineWithScroll(LPITEM item) + ITEM_MANAGER::CopyAllAttrTo(item, pkNewItem); + LogManager::instance().ItemLog(this, pkNewItem, "REFINE FAIL", pkNewItem->GetName()); + +- BYTE bCell = item->GetCell(); ++ WORD bCell = item->GetCell(); + + DBManager::instance().SendMoneyLog(MONEY_LOG_REFINE, item->GetVnum(), -prt->cost); + NotifyRefineFail(this, item, szRefineType, -1); +@@ -5982,7 +5982,7 @@ bool CHARACTER::PickupItem(DWORD dwVID) + return false; + } + +-bool CHARACTER::SwapItem(BYTE bCell, BYTE bDestCell) ++bool CHARACTER::SwapItem(WORD bCell, WORD bDestCell) + { + if (!CanHandleItem()) + return false; +@@ -6034,7 +6034,7 @@ bool CHARACTER::SwapItem(BYTE bCell, BYTE bDestCell) + if (TItemPos(EQUIPMENT, item2->GetCell()).IsEquipPosition()) + { + BYTE bEquipCell = item2->GetCell() - INVENTORY_MAX_NUM; +- BYTE bInvenCell = item1->GetCell(); ++ WORD bInvenCell = item1->GetCell(); + + // Âø¿ëÁßÀÎ ¾ÆÀÌÅÛÀ» ¹þÀ» ¼ö Àְí, Âø¿ë ¿¹Á¤ ¾ÆÀÌÅÛÀÌ Âø¿ë °¡´ÉÇÑ »óÅ¿©¾߸¸ ÁøÇà + if (false == CanUnequipNow(item2) || false == CanEquipNow(item1)) +@@ -6052,8 +6052,8 @@ bool CHARACTER::SwapItem(BYTE bCell, BYTE bDestCell) + } + else + { +- BYTE bCell1 = item1->GetCell(); +- BYTE bCell2 = item2->GetCell(); ++ WORD bCell1 = item1->GetCell(); ++ WORD bCell2 = item2->GetCell(); + + item1->RemoveFromCharacter(); + item2->RemoveFromCharacter(); +@@ -6182,7 +6182,7 @@ bool CHARACTER::EquipItem(LPITEM item, int iCandidateCell) + } + else + { +- BYTE bOldCell = item->GetCell(); ++ WORD bOldCell = item->GetCell(); + + if (item->EquipTo(this, iWearCell)) + { +diff --git a/server/game/src/char_quickslot.cpp b/server/game/src/char_quickslot.cpp +index 884cd3b..4894fbb 100755 +--- a/server/game/src/char_quickslot.cpp ++++ b/server/game/src/char_quickslot.cpp +@@ -9,7 +9,7 @@ + ///////////////////////////////////////////////////////////////////////////// + // QUICKSLOT HANDLING + ///////////////////////////////////////////////////////////////////////////// +-void CHARACTER::SyncQuickslot(BYTE bType, BYTE bOldPos, BYTE bNewPos) // bNewPos == 255 ¸é DELETE ++void CHARACTER::SyncQuickslot(BYTE bType, WORD bOldPos, WORD bNewPos) // bNewPos == 255 ¸é DELETE + { + if (bOldPos == bNewPos) + return; +@@ -135,7 +135,7 @@ bool CHARACTER::SwapQuickslot(BYTE a, BYTE b) + return true; + } + +-void CHARACTER::ChainQuickslotItem(LPITEM pItem, BYTE bType, BYTE bOldPos) ++void CHARACTER::ChainQuickslotItem(LPITEM pItem, BYTE bType, WORD bOldPos) + { + if (pItem->IsDragonSoul()) + return; +diff --git a/server/game/src/exchange.cpp b/server/game/src/exchange.cpp +index a59452d..c3229f0 100755 +--- a/server/game/src/exchange.cpp ++++ b/server/game/src/exchange.cpp +@@ -299,30 +299,27 @@ bool CExchange::Check(int * piItemCount) + + bool CExchange::CheckSpace() + { +- static CGrid s_grid1(5, INVENTORY_MAX_NUM/5 / 2); // inven page 1 +- static CGrid s_grid2(5, INVENTORY_MAX_NUM/5 / 2); // inven page 2 ++ // MT: one grid per inventory page (40250 had s_grid1/s_grid2 for its 2 pages) ++ static CGrid * s_grids[INVENTORY_PAGE_COUNT]; + +- s_grid1.Clear(); +- s_grid2.Clear(); ++ for (int page = 0; page < INVENTORY_PAGE_COUNT; ++page) ++ { ++ if (!s_grids[page]) ++ s_grids[page] = new CGrid(5, INVENTORY_PAGE_SIZE / 5); ++ s_grids[page]->Clear(); ++ } + + LPCHARACTER victim = GetCompany()->GetOwner(); + LPITEM item; + + int i; + +- for (i = 0; i < INVENTORY_MAX_NUM / 2; ++i) +- { +- if (!(item = victim->GetInventoryItem(i))) +- continue; +- +- s_grid1.Put(i, 1, item->GetSize()); +- } +- for (i = INVENTORY_MAX_NUM / 2; i < INVENTORY_MAX_NUM; ++i) ++ for (i = 0; i < INVENTORY_MAX_NUM; ++i) + { + if (!(item = victim->GetInventoryItem(i))) + continue; + +- s_grid2.Put(i - INVENTORY_MAX_NUM / 2, 1, item->GetSize()); ++ s_grids[i / INVENTORY_PAGE_SIZE]->Put(i % INVENTORY_PAGE_SIZE, 1, item->GetSize()); + } + + // ¾Æ... ¹º°¡ °³º´½Å °°Áö¸¸... ¿ëȥ¼® Àκ¥À» ³ë¸Ö Àκ¥ º¸°í µû¶ó ¸¸µç ³» À߸øÀ̴Ù ¤ФÐ +@@ -387,25 +384,21 @@ bool CExchange::CheckSpace() + } + else + { +- int iPos = s_grid1.FindBlank(1, item->GetSize()); ++ bool bPut = false; + +- if (iPos >= 0) ++ for (int page = 0; page < INVENTORY_PAGE_COUNT && !bPut; ++page) + { +- s_grid1.Put(iPos, 1, item->GetSize()); +- } +- else +- { +- iPos = s_grid2.FindBlank(1, item->GetSize()); ++ int iPos = s_grids[page]->FindBlank(1, item->GetSize()); + + if (iPos >= 0) + { +- s_grid2.Put(iPos, 1, item->GetSize()); +- } +- else +- { +- return false; ++ s_grids[page]->Put(iPos, 1, item->GetSize()); ++ bPut = true; + } + } ++ ++ if (!bPut) ++ return false; + } + } + +diff --git a/server/game/src/fishing.cpp b/server/game/src/fishing.cpp +index b240401..20b2a21 100755 +--- a/server/game/src/fishing.cpp ++++ b/server/game/src/fishing.cpp +@@ -877,7 +877,7 @@ int RealRefineRod(LPCHARACTER ch, LPITEM item) + + if (pkNewItem) + { +- BYTE bCell = rod->GetCell(); ++ WORD bCell = rod->GetCell(); + // ³¬½ôë °³·® ¼º°ø + ITEM_MANAGER::instance().RemoveItem(rod, "REMOVE (REFINE FISH_ROD)"); + pkNewItem->AddToCharacter(ch, TItemPos (INVENTORY, bCell)); +@@ -895,7 +895,7 @@ int RealRefineRod(LPCHARACTER ch, LPITEM item) + LPITEM pkNewItem = ITEM_MANAGER::instance().CreateItem(rod->GetValue(4), 1); + if (pkNewItem) + { +- BYTE bCell = rod->GetCell(); ++ WORD bCell = rod->GetCell(); + // ³¬½ôë °³·®¿¡ ¼º°ø + ITEM_MANAGER::instance().RemoveItem(rod, "REMOVE (REFINE FISH_ROD)"); + pkNewItem->AddToCharacter(ch, TItemPos(INVENTORY, bCell)); +diff --git a/server/game/src/mining.cpp b/server/game/src/mining.cpp +index f1ea8a6..b6dd7b8 100755 +--- a/server/game/src/mining.cpp ++++ b/server/game/src/mining.cpp +@@ -253,7 +253,7 @@ namespace mining + LPITEM pkNewPick = ITEM_MANAGER::instance().CreateItem(rkOldPick.GetRefinedVnum(), 1); + if (pkNewPick) + { +- BYTE bCell = rkOldPick.GetCell(); ++ WORD bCell = rkOldPick.GetCell(); + rkItemMgr.RemoveItem(item, "REMOVE (REFINE PICK)"); + pkNewPick->AddToCharacter(ch, TItemPos(INVENTORY, bCell)); + LogManager::instance().ItemLog(ch, pkNewPick, "REFINE PICK SUCCESS", pkNewPick->GetName()); +@@ -270,7 +270,7 @@ namespace mining + + if (pkNewPick) + { +- BYTE bCell = rkOldPick.GetCell(); ++ WORD bCell = rkOldPick.GetCell(); + rkItemMgr.RemoveItem(item, "REMOVE (REFINE PICK)"); + pkNewPick->AddToCharacter(ch, TItemPos(INVENTORY, bCell)); + rkLogMgr.ItemLog(ch, pkNewPick, "REFINE PICK FAIL", pkNewPick->GetName()); +diff --git a/server/game/src/questlua_item.cpp b/server/game/src/questlua_item.cpp +index befc676..23200e8 100755 +--- a/server/game/src/questlua_item.cpp ++++ b/server/game/src/questlua_item.cpp +@@ -436,7 +436,7 @@ namespace quest + ITEM_MANAGER::CopyAllAttrTo(pItem, pkNewItem); + LogManager::instance().ItemLog(pChar, pkNewItem, "COPY SUCCESS", pkNewItem->GetName()); + +- BYTE bCell = pItem->GetCell(); ++ WORD bCell = pItem->GetCell(); + + ITEM_MANAGER::instance().RemoveItem(pItem, "REMOVE (COPY SUCCESS)"); +