Merge pull request 'feature/ivtr-operation' (#449) from feature/ivtr-operation into develop

Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/449
This commit is contained in:
hungdk
2026-05-19 06:49:57 +00:00
9 changed files with 1152 additions and 298 deletions
@@ -59,6 +59,12 @@ namespace BrewMonster.Scripts
public const int IVTRSIZE_BOOTHBPACK_MAX = 20; // Max booth pack for buying
public const int IVTRSIZE_CLIENTCARDPACK = 32; // Client pack for general card collection
// Main bag UI (C++ DlgInventory.h: CECDLGSHOP_PACKMAX / PACKLINEMAX)
public const int IVTRSIZE_PACK_UI_PAGE = 32;
public const int IVTRSIZE_PACK_UI_LINE = 8;
/// <summary>Upper bound for client pack slots (server ivtr_size is byte).</summary>
public const int IVTRSIZE_PACK_MAX = 255;
public const int NUM_NPCIVTR = 8; // NPC inventory number
@@ -25,6 +25,10 @@ namespace BrewMonster.Scripts.Managers
[Header("Pack Buttons (assign in Inspector)")]
[Tooltip("Main slot grid: shows IVTRTYPE_PACK (0) on Item tab and IVTRTYPE_TASKPACK (2) on Task tab.")]
[SerializeField] private List<Button> inventoryPackButtons = new List<Button>();
[Tooltip("Parent of pack slot buttons (auto: first slot's parent). Cloned when server expands bag.")]
[SerializeField] private RectTransform inventorySlotContainer;
[Tooltip("Template for new slots (auto: first pack button).")]
[SerializeField] private Button inventorySlotTemplate;
[SerializeField] private List<Button> equipmentPackButtons = new List<Button>(); // byPackage: 1
[SerializeField] private List<Button> fashionPackButtons = new List<Button>(); // byPackage: 3
@@ -53,6 +57,9 @@ namespace BrewMonster.Scripts.Managers
[Header("Stack combine — merge into another stack (C++ inventory drag-merge, assign in Inspector)")]
[SerializeField] private Button combineStackButton;
[Header("Sort / arrange pack (C++ DlgInventory arrange)")]
[SerializeField] private Button sortInventoryButton;
private int _splitAmount = 1;
private int _splitMaxAmount = 1;
@@ -123,6 +130,7 @@ namespace BrewMonster.Scripts.Managers
}
private InventoryBagTab _bagTab = InventoryBagTab.Item;
private bool _inventorySlotTemplateResolved;
private void Awake()
{
@@ -131,6 +139,7 @@ namespace BrewMonster.Scripts.Managers
WireBagTabButtons();
WireSplitUI();
WireCombineUI();
WireSortInventoryUI();
//if (currentDragImage == null)
//{
@@ -239,6 +248,60 @@ namespace BrewMonster.Scripts.Managers
}
}
private void WireSortInventoryUI()
{
ResolveSortInventoryButton();
if (sortInventoryButton != null)
{
sortInventoryButton.onClick.RemoveAllListeners();
sortInventoryButton.onClick.AddListener(OnSortInventoryClicked);
}
}
private void ResolveSortInventoryButton()
{
if (sortInventoryButton != null)
return;
var buttons = GetComponentsInChildren<Button>(true);
for (int i = 0; i < buttons.Length; i++)
{
var btn = buttons[i];
if (btn == null)
continue;
string n = btn.name.ToLowerInvariant();
if (n.Contains("arrange") || n.Contains("sort") || n == "btn_arrange")
{
sortInventoryButton = btn;
break;
}
}
}
/// <summary>Arrange main inventory (IVTRTYPE_PACK). C++ CDlgInventory::OnCommand_arrange.</summary>
public void OnSortInventoryClicked()
{
if (_bagTab != InventoryBagTab.Item)
{
Debug.LogWarning("[InventoryUI] Sort pack: switch to Item tab first");
return;
}
var host = CECGameRun.Instance?.GetHostPlayer();
if (host == null)
return;
int cool = host.GetCoolTime((int)CoolTimeIndex.GP_CT_MULTI_EXCHANGE_ITEM, out _);
if (cool > 0)
{
EC_Game.GetGameRun()?.AddFixedMessage((int)FixedMsg.FIXMSG_CMD_INCOOLTIME);
return;
}
host.SortPack(InventoryConst.IVTRTYPE_PACK);
RefreshAll();
}
private void ShowSplitPanel(bool show)
{
if (splitPanelRoot != null)
@@ -378,7 +441,7 @@ namespace BrewMonster.Scripts.Managers
for (int slot = 0; slot < buttons.Count; slot++)
{
var button = buttons[slot];
if (button == null)
if (button == null || !button.gameObject.activeInHierarchy)
continue;
// Get item at this slot
@@ -405,6 +468,9 @@ namespace BrewMonster.Scripts.Managers
{
lastRefreshTime = Time.time;
int requiredPackSlots = GetRequiredMainPackSlotCount();
EnsureInventoryPackSlotButtons(requiredPackSlots);
var invItems = model.GetInventoryData(PKG_INVENTORY);
var eqpItems = model.GetInventoryData(PKG_EQUIPMENT);
var fshItems = model.GetInventoryData(PKG_FASHION);
@@ -423,6 +489,82 @@ namespace BrewMonster.Scripts.Managers
UpdateCharacterInfo();
}
/// <summary>
/// Match server pack size for the active bag tab (C++ CDlgInventory uses GetPack/GetTaskPack size).
/// </summary>
private int GetRequiredMainPackSlotCount()
{
var host = CECGameRun.Instance?.GetHostPlayer();
if (host == null)
return inventoryPackButtons != null ? inventoryPackButtons.Count : 0;
byte pack = _bagTab == InventoryBagTab.Item ? PKG_INVENTORY : PKG_TASK;
var inv = host.GetInventory(pack);
return inv != null ? Math.Max(0, inv.GetSize()) : 0;
}
/// <summary>
/// Grow/shrink visible slot buttons to match <see cref="EC_Inventory.GetSize"/> (PW: CHANGE_IVTR_SIZE, OWN_IVTR_DATA).
/// </summary>
private void EnsureInventoryPackSlotButtons(int requiredCount)
{
if (requiredCount < 0)
requiredCount = 0;
if (requiredCount > InventoryConst.IVTRSIZE_PACK_MAX)
requiredCount = InventoryConst.IVTRSIZE_PACK_MAX;
ResolveInventorySlotTemplate();
if (inventorySlotTemplate == null || inventorySlotContainer == null)
return;
if (inventoryPackButtons == null)
inventoryPackButtons = new List<Button>();
int prevCount = inventoryPackButtons.Count;
while (inventoryPackButtons.Count < requiredCount)
{
var clone = Instantiate(inventorySlotTemplate, inventorySlotContainer);
clone.gameObject.SetActive(true);
clone.name = $"item ({inventoryPackButtons.Count})";
inventoryPackButtons.Add(clone);
}
if (requiredCount > prevCount && inventorySlotContainer != null)
LayoutRebuilder.ForceRebuildLayoutImmediate(inventorySlotContainer);
for (int i = 0; i < inventoryPackButtons.Count; i++)
{
var btn = inventoryPackButtons[i];
if (btn == null)
continue;
bool show = i < requiredCount;
if (btn.gameObject.activeSelf != show)
btn.gameObject.SetActive(show);
}
}
private void ResolveInventorySlotTemplate()
{
if (_inventorySlotTemplateResolved)
return;
_inventorySlotTemplateResolved = true;
if (inventorySlotTemplate == null && inventoryPackButtons != null)
{
for (int i = 0; i < inventoryPackButtons.Count; i++)
{
if (inventoryPackButtons[i] != null)
{
inventorySlotTemplate = inventoryPackButtons[i];
break;
}
}
}
if (inventorySlotContainer == null && inventorySlotTemplate != null)
inventorySlotContainer = inventorySlotTemplate.transform.parent as RectTransform;
}
/// <summary>
/// Update all configured money text components with the current amount.
/// Call this when GET_OWN_MONEY arrives.
@@ -1079,7 +1221,7 @@ namespace BrewMonster.Scripts.Managers
for (int slot = 0; slot < buttons.Count; slot++)
{
var button = buttons[slot];
if (button == null)
if (button == null || !button.gameObject.activeInHierarchy)
{
continue;
}
@@ -439,6 +439,25 @@ namespace CSNetwork.C2SCommand
return SerializeCommand(CommandID.MOVE_IVTR_ITEM, cmd);
}
/// <summary>C++ c2s_SendCmdMultiExchangeItem — pack arrange / sort (variable-length operation list).</summary>
public static Octets CreateMultiExchangeItem(byte location, int pairCount, int[] indexPairs)
{
if (pairCount < 1 || indexPairs == null || indexPairs.Length < pairCount * 2)
return null;
var octets = new Octets();
WriteBasicValue(octets, (ushort)CommandID.MULTI_EXCHANGE_ITEM);
WriteBasicValue(octets, location);
WriteBasicValue(octets, (byte)pairCount);
for (int i = 0; i < pairCount; i++)
{
WriteBasicValue(octets, (byte)indexPairs[i * 2]);
WriteBasicValue(octets, (byte)indexPairs[i * 2 + 1]);
}
return octets;
}
public static Octets CreatePickupItem(int idItem, int tid)
{
var cmd = new CMD_Pickup
@@ -471,6 +471,16 @@ namespace CSNetwork
SendProtocol(gamedatasendRequest);
}
public void RequestMultiExchangeItem(byte location, int pairCount, int[] indexPairs)
{
var data = C2SCommandFactory.CreateMultiExchangeItem(location, pairCount, indexPairs);
if (data == null)
return;
var gamedatasendRequest = new gamedatasend();
gamedatasendRequest.Data = data;
SendProtocol(gamedatasendRequest);
}
public void RequestPickupItem(int idItem, int tid)
{
gamedatasend gamedatasendRequest = new gamedatasend();
@@ -497,6 +497,12 @@ namespace BrewMonster.Network
{
Instance._gameSession.RequestMoveIvtrItem(src, dest, count);
}
public static void RequestMultiExchangeItem(byte location, int pairCount, int[] indexPairs)
{
Instance._gameSession.RequestMultiExchangeItem(location, pairCount, indexPairs);
}
public static void LoadConfigData()
{
Instance._gameSession.LoadConfigData();
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -137,12 +137,15 @@ namespace BrewMonster
}
case CommandID.CHANGE_IVTR_SIZE:
{
// C++: resize pack (normal inventory)
// C++ EC_HostMsg.cpp: m_pPack->Resize + FIXMSG_NEW_INVENTORY_SIZE
if (data != null && data.Length >= 4)
{
int newSize = BitConverter.ToInt32(data, 0);
if (m_pPack != null)
m_pPack.Resize(newSize);
EC_Game.GetGameRun()?.AddFixedMessage((int)FixedMsg.FIXMSG_NEW_INVENTORY_SIZE, newSize);
var ui = GameObject.FindFirstObjectByType<EC_InventoryUI>();
ui?.RefreshAll();
}
+208
View File
@@ -0,0 +1,208 @@
using BrewMonster.Network;
using BrewMonster.Scripts;
using CSNetwork.GPDataType;
using System.Collections.Generic;
using static BrewMonster.Scripts.EC_Inventory;
namespace BrewMonster
{
public partial class CECHostPlayer
{
/// <summary>
/// C++ CECHostPlayer::SortPack — reorder pack via MULTI_EXCHANGE_ITEM (DlgInventory OnCommand_arrange).
/// </summary>
public void SortPack(int iPack)
{
EC_Inventory pInventory = GetPack(iPack);
if (pInventory == null)
return;
int nIvtrSize = pInventory.GetSize();
if (nIvtrSize <= 0)
return;
if (pInventory.GetEmptySlotNum() == nIvtrSize)
return;
for (int i = 0; i < nIvtrSize; i++)
{
var pItem = pInventory.GetItem(i, false);
if (pItem != null && pItem.IsFrozen())
return;
}
for (int i = 0; i < nIvtrSize; i++)
{
var pItem = pInventory.GetItem(i, false);
pItem?.Freeze(true);
}
try
{
var vecItem = new List<int>(nIvtrSize);
for (int i = 0; i < nIvtrSize; i++)
vecItem.Add(i);
vecItem.Sort((a, b) => ComparePackSortIndices(pInventory, a, b));
var vecExchange = new List<int>();
int pos = 0;
while (pos < nIvtrSize)
{
int j = vecItem[pos];
if (j == pos)
{
pos++;
continue;
}
int k = vecItem[j];
if (pInventory.GetItem(j, false) != null || pInventory.GetItem(k, false) != null)
{
vecExchange.Add(pos);
vecExchange.Add(j);
}
int tmp = vecItem[pos];
vecItem[pos] = vecItem[j];
vecItem[j] = tmp;
}
if (vecExchange.Count > 0)
{
int pairCount = vecExchange.Count / 2;
for (int i = 0, j = vecExchange.Count - 1; i < j; i++, j--)
{
int t = vecExchange[i];
vecExchange[i] = vecExchange[j];
vecExchange[j] = t;
}
UnityGameSession.RequestMultiExchangeItem((byte)iPack, pairCount, vecExchange.ToArray());
}
else
{
var pGameRun = EC_Game.GetGameRun();
pGameRun?.AddChatMessage("Không cần sắp xếp kho đồ.", (int)ChatChannel.GP_CHAT_SYSTEM);
}
}
finally
{
for (int i = 0; i < nIvtrSize; i++)
{
var pItem = pInventory.GetItem(i, false);
pItem?.Freeze(false);
}
}
}
private static int ComparePackSortIndices(EC_Inventory pInventory, int index1, int index2)
{
if (DefaultPackSortLess(pInventory, index1, index2))
return -1;
if (DefaultPackSortLess(pInventory, index2, index1))
return 1;
return 0;
}
/// <summary>Returns true when slot <paramref name="index1"/> should appear before <paramref name="index2"/>.</summary>
private static bool DefaultPackSortLess(EC_Inventory pInventory, int index1, int index2)
{
if (pInventory == null)
return false;
EC_IvtrItem pItem1 = pInventory.GetItem(index1, false);
EC_IvtrItem pItem2 = pInventory.GetItem(index2, false);
if (pItem1 == null)
return false;
if (pItem2 == null)
return true;
int cid1 = pItem1.GetClassID();
int tid1 = pItem1.GetTemplateID();
int cid2 = pItem2.GetClassID();
int tid2 = pItem2.GetTemplateID();
if (cid1 != cid2)
{
int cidOrder1 = GetPackSortClassOrder(cid1);
int cidOrder2 = GetPackSortClassOrder(cid2);
if (cidOrder1 != cidOrder2)
return cidOrder1 > cidOrder2;
return cid1 < cid2;
}
if (cid1 == (int)EC_IvtrItem.InventoryClassId.ICID_WEAPON)
{
if (pItem1 is CECIvtrWeapon w1 && pItem2 is CECIvtrWeapon w2)
{
var e1 = w1.GetDBEssence();
var e2 = w2.GetDBEssence();
if (e1.level != e2.level)
return e1.level > e2.level;
}
}
else if (cid1 == (int)EC_IvtrItem.InventoryClassId.ICID_ARMOR)
{
if (pItem1 is EC_IvtrArmor a1 && pItem2 is EC_IvtrArmor a2)
{
var e1 = a1.GetDBEssence();
var e2 = a2.GetDBEssence();
if (e1.level != e2.level)
return e1.level > e2.level;
}
}
else if (cid1 == (int)EC_IvtrItem.InventoryClassId.ICID_GENERALCARD)
{
if (pItem1 is EC_IvtrGeneralCard c1 && pItem2 is EC_IvtrGeneralCard c2)
{
int t1 = c1.GetEssence().type;
int t2 = c2.GetEssence().type;
if (t1 != t2)
return t1 < t2;
}
}
return tid1 < tid2;
}
private static int GetPackSortClassOrder(int cid)
{
int[] s_CIDs =
{
(int)EC_IvtrItem.InventoryClassId.ICID_WEAPON,
(int)EC_IvtrItem.InventoryClassId.ICID_ARROW,
(int)EC_IvtrItem.InventoryClassId.ICID_TOSSMAT,
(int)EC_IvtrItem.InventoryClassId.ICID_ARMOR,
(int)EC_IvtrItem.InventoryClassId.ICID_DECORATION,
(int)EC_IvtrItem.InventoryClassId.ICID_BIBLE,
(int)EC_IvtrItem.InventoryClassId.ICID_FLYSWORD,
(int)EC_IvtrItem.InventoryClassId.ICID_WING,
(int)EC_IvtrItem.InventoryClassId.ICID_GOBLIN,
(int)EC_IvtrItem.InventoryClassId.ICID_GOBLIN_EQUIP,
(int)EC_IvtrItem.InventoryClassId.ICID_FASHION,
(int)EC_IvtrItem.InventoryClassId.ICID_AUTOHP,
(int)EC_IvtrItem.InventoryClassId.ICID_AUTOMP,
(int)EC_IvtrItem.InventoryClassId.ICID_MEDICINE,
(int)EC_IvtrItem.InventoryClassId.ICID_SKILLMATTER,
(int)EC_IvtrItem.InventoryClassId.ICID_TARGETITEM,
(int)EC_IvtrItem.InventoryClassId.ICID_STONE,
(int)EC_IvtrItem.InventoryClassId.ICID_PETEGG,
(int)EC_IvtrItem.InventoryClassId.ICID_REFINETICKET,
(int)EC_IvtrItem.InventoryClassId.ICID_DYETICKET,
(int)EC_IvtrItem.InventoryClassId.ICID_GOBLIN_EXPPILL,
(int)EC_IvtrItem.InventoryClassId.ICID_GENERALCARD,
(int)EC_IvtrItem.InventoryClassId.ICID_GENERALCARD_DICE,
};
for (int i = 0; i < s_CIDs.Length; i++)
{
if (cid == s_CIDs[i])
return s_CIDs.Length - i;
}
return 0;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 22d7c6cd009100d44956b805ed093795