背包扩展到 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:
shenlei
2026-09-29 22:37:44 +09:00
co-authored by Claude Opus 5.5
parent 22d534c5eb
commit e33e41c12b
13 changed files with 724 additions and 15 deletions
@@ -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";
+2
View File
@@ -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;
}
+1 -1
View File
@@ -41,7 +41,7 @@ class IAbstractPlayer : public TAbstractSingleton<IAbstractPlayer>
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;
+2 -2
View File
@@ -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
+1 -1
View File
@@ -155,7 +155,7 @@ class CPythonNetworkStream : public CNetworkStream, public CSingleton<CPythonNet
bool SendItemMovePacket(TItemPos pos, TItemPos change_pos, BYTE num);
bool SendItemPickUpPacket(DWORD vid);
bool SendQuickSlotAddPacket(BYTE wpos, BYTE type, BYTE pos);
bool SendQuickSlotAddPacket(BYTE wpos, BYTE type, WORD pos);
bool SendQuickSlotDelPacket(BYTE wpos);
bool SendQuickSlotMovePacket(BYTE wpos, BYTE change_pos);
@@ -705,7 +705,7 @@ bool CPythonNetworkStream::SendItemPickUpPacket(DWORD vid)
}
bool CPythonNetworkStream::SendQuickSlotAddPacket(BYTE wpos, BYTE type, BYTE pos)
bool CPythonNetworkStream::SendQuickSlotAddPacket(BYTE wpos, BYTE type, WORD pos)
{
if (!__CanActMainInstance())
return true;
+3 -3
View File
@@ -781,7 +781,7 @@ void CPythonPlayer::RequestAddLocalQuickSlot(DWORD dwLocalSlotIndex, DWORD dwWnd
DWORD dwGlobalSlotIndex=LocalQuickSlotIndexToGlobalQuickSlotIndex(dwLocalSlotIndex);
CPythonNetworkStream& rkNetStream=CPythonNetworkStream::Instance();
rkNetStream.SendQuickSlotAddPacket((BYTE)dwGlobalSlotIndex, (BYTE)dwWndType, (BYTE)dwWndItemPos);
rkNetStream.SendQuickSlotAddPacket((BYTE)dwGlobalSlotIndex, (BYTE)dwWndType, (WORD)dwWndItemPos);
}
void CPythonPlayer::RequestAddToEmptyLocalQuickSlot(DWORD dwWndType, DWORD dwWndItemPos)
@@ -794,7 +794,7 @@ void CPythonPlayer::RequestAddToEmptyLocalQuickSlot(DWORD dwWndType, DWORD dwWnd
{
DWORD dwGlobalQuickSlotIndex=LocalQuickSlotIndexToGlobalQuickSlotIndex(i);
CPythonNetworkStream& rkNetStream=CPythonNetworkStream::Instance();
rkNetStream.SendQuickSlotAddPacket((BYTE)dwGlobalQuickSlotIndex, (BYTE)dwWndType, (BYTE)dwWndItemPos);
rkNetStream.SendQuickSlotAddPacket((BYTE)dwGlobalQuickSlotIndex, (BYTE)dwWndType, (WORD)dwWndItemPos);
return;
}
}
@@ -845,7 +845,7 @@ void CPythonPlayer::RequestUseLocalQuickSlot(DWORD dwLocalSlotIndex)
}
}
void CPythonPlayer::AddQuickSlot(int QuickSlotIndex, char IconType, char IconPosition)
void CPythonPlayer::AddQuickSlot(int QuickSlotIndex, char IconType, WORD IconPosition)
{
if (QuickSlotIndex < 0 || QuickSlotIndex >= QUICKSLOT_MAX_NUM)
return;
+1 -1
View File
@@ -317,7 +317,7 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, 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);