From dddf804261930b7d750157445e83a7655debe8b2 Mon Sep 17 00:00:00 2001 From: MrShadow Date: Thu, 13 Nov 2025 15:51:30 -0300 Subject: [PATCH 1/2] Implement custom companion shops --- src/Shared/Data/Database/CompanionShops.cs | 66 +++++++++++++++++++ src/Shared/Data/MeliaData.cs | 1 + .../Commands/ChatCommands.Handlers.cs | 24 +++++++ .../Scripting/CompanionShopBuilder.cs | 52 +++++++++++++++ src/ZoneServer/Scripting/Dialogues/Dialog.cs | 42 ++++++++++++ src/ZoneServer/Scripting/Shortcuts.Npcs.cs | 20 ++++++ .../zone/content/test/npcs/cities/c_klaipe.cs | 32 +++++++++ .../core/client/custom_npc_shops/004b.lua | 10 +++ .../zone/core/client/custom_npc_shops/005.lua | 17 +++++ .../core/client/custom_npc_shops/005b.lua | 47 +++++++++++++ .../core/client/custom_npc_shops/005c.lua | 11 ++++ .../core/client/custom_npc_shops/005d.lua | 31 +++++++++ .../core/client/custom_npc_shops/005e.lua | 23 +++++++ .../core/client/custom_npc_shops/005f.lua | 48 ++++++++++++++ .../core/client/custom_npc_shops/005g.lua | 34 ++++++++++ .../zone/core/client/custom_npc_shops/006.lua | 34 ++++++++++ .../core/client/custom_npc_shops/006b.lua | 31 +++++++++ .../core/client/custom_npc_shops/006c.lua | 36 ++++++++++ 18 files changed, 559 insertions(+) create mode 100644 src/Shared/Data/Database/CompanionShops.cs create mode 100644 src/ZoneServer/Scripting/CompanionShopBuilder.cs create mode 100644 system/scripts/zone/core/client/custom_npc_shops/004b.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/005.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/005b.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/005c.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/005d.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/005e.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/005f.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/005g.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/006.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/006b.lua create mode 100644 system/scripts/zone/core/client/custom_npc_shops/006c.lua diff --git a/src/Shared/Data/Database/CompanionShops.cs b/src/Shared/Data/Database/CompanionShops.cs new file mode 100644 index 000000000..6534720a5 --- /dev/null +++ b/src/Shared/Data/Database/CompanionShops.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; + +namespace Melia.Shared.Data.Database +{ + [Serializable] + public class CompanionShopData + { + public string Name { get; set; } + public Dictionary Products { get; set; } = new Dictionary(); + + public CompanionProductData GetProduct(int id) + { + this.Products.TryGetValue(id, out var product); + return product; + } + } + + [Serializable] + public class CompanionProductData + { + public string ShopName { get; set; } + public int Id { get; set; } + public string CompanionClassName { get; set; } + public int Price { get; set; } + } + + /// + /// Companion shop database, indexed by shop name. + /// + public class CompanionShopDb + { + private readonly Dictionary _entries = new Dictionary(); + + /// + /// Adds or replaces a companion shop in the database. + /// + /// + /// + public void AddOrReplace(string name, CompanionShopData data) + { + _entries[name] = data; + } + + /// + /// Tries to find a companion shop by name. + /// + /// + /// + /// + public bool TryFind(string name, out CompanionShopData data) + { + return _entries.TryGetValue(name, out data); + } + + /// + /// Returns true if a companion shop with the given name exists. + /// + /// + /// + public bool Exists(string name) + { + return _entries.ContainsKey(name); + } + } +} diff --git a/src/Shared/Data/MeliaData.cs b/src/Shared/Data/MeliaData.cs index 8d84e9070..087e11e9f 100644 --- a/src/Shared/Data/MeliaData.cs +++ b/src/Shared/Data/MeliaData.cs @@ -16,6 +16,7 @@ public class MeliaData public BuffDb BuffDb = new(); public ChatMacroDb ChatMacroDb = new(); public CollectionDb CollectionDb; + public CompanionShopDb CompanionShopDb = new(); public CooldownDb CooldownDb = new(); public CustomCommandDb CustomCommandDb = new(); public DialogDb DialogDb = new(); diff --git a/src/ZoneServer/Commands/ChatCommands.Handlers.cs b/src/ZoneServer/Commands/ChatCommands.Handlers.cs index de7a5e38c..7c10bbb30 100644 --- a/src/ZoneServer/Commands/ChatCommands.Handlers.cs +++ b/src/ZoneServer/Commands/ChatCommands.Handlers.cs @@ -47,6 +47,7 @@ public ChatCommands() // Custom Client Commands this.Add("buyshop", "", "", this.HandleBuyShop); this.Add("updatemouse", "", "", this.HandleUpdateMouse); + this.Add("pethire", "", "", this.HandlePetHire); // Normal this.Add("where", "", "Displays current location.", this.HandleWhere); @@ -2333,5 +2334,28 @@ private CommandResult HandleMedals(Character sender, Character target, string me return CommandResult.Okay; } + + // ============================================================================ + // WARNING: STUB IMPLEMENTATION - NOT ACTUALLY IMPLEMENTED! + // ============================================================================ + // TODO: This is a placeholder stub for companion/pet hiring functionality. + // The actual implementation needs to be completed in a future PR. + // This method currently does nothing except return a message to the user. + // ============================================================================ + /// + /// Stub handler for pet/companion hiring functionality. + /// + /// + /// + /// + /// + /// + /// + private CommandResult HandlePetHire(Character sender, Character target, string message, string commandName, Arguments args) + { + // STUB: Not actually implemented yet! + sender.ServerMessage("Companion hired! But not actually implemented!"); + return CommandResult.Okay; + } } } diff --git a/src/ZoneServer/Scripting/CompanionShopBuilder.cs b/src/ZoneServer/Scripting/CompanionShopBuilder.cs new file mode 100644 index 000000000..98df159f6 --- /dev/null +++ b/src/ZoneServer/Scripting/CompanionShopBuilder.cs @@ -0,0 +1,52 @@ +using System; +using Melia.Shared.Data.Database; + +namespace Melia.Zone.Scripting +{ + /// + /// A builder for creating companion shop data. + /// + public class CompanionShopBuilder + { + private readonly CompanionShopData _shopData; + private int _productClassId = 200_001; + + /// + /// Creates new instance for creating a companion shop with the given name. + /// + /// + public CompanionShopBuilder(string shopName) + { + _shopData = new CompanionShopData(); + _shopData.Name = shopName; + } + + /// + /// Returns the built companion shop data. + /// + /// + public CompanionShopData Build() + { + return _shopData; + } + + /// + /// Adds companion to the shop. + /// + /// Class name of the companion for sale. + /// The price for the companion. + public void AddCompanion(string companionClassName, int price = 1) + { + price = Math.Max(1, price); + + var productData = new CompanionProductData(); + + productData.Id = _productClassId++; + productData.CompanionClassName = companionClassName; + productData.Price = price; + productData.ShopName = _shopData.Name; + + _shopData.Products.Add(productData.Id, productData); + } + } +} diff --git a/src/ZoneServer/Scripting/Dialogues/Dialog.cs b/src/ZoneServer/Scripting/Dialogues/Dialog.cs index 8734902df..d6f2dfbed 100644 --- a/src/ZoneServer/Scripting/Dialogues/Dialog.cs +++ b/src/ZoneServer/Scripting/Dialogues/Dialog.cs @@ -622,6 +622,48 @@ public async Task OpenShop(ShopData shopData) await this.GetClientResponse(); } + + /// + /// Opens a custom companion shop with the given name. + /// + /// + public async Task OpenCustomCompanionShop(string shopName) + { + if (!ZoneServer.Instance.Data.CompanionShopDb.TryFind(shopName, out var shopData)) + throw new ArgumentException($"Companion shop '{shopName}' not found."); + + // Start receival of companion data + Send.ZC_EXEC_CLIENT_SCP(this.Player.Connection, "Melia.Comm.BeginRecv('CustomCompanionShop')"); + + // Send companion data + var sb = new StringBuilder(); + foreach (var productData in shopData.Products.Values) + { + sb.AppendFormat("{{\"{0}\",{1}}},", productData.CompanionClassName, productData.Price); + + if (sb.Length > ClientScript.ScriptMaxLength * 0.8) + { + Send.ZC_EXEC_CLIENT_SCP(this.Player.Connection, $"Melia.Comm.Recv('CustomCompanionShop', {{ {sb} }})"); + sb.Clear(); + } + } + + // Send remaining companions + if (sb.Length > 0) + { + Send.ZC_EXEC_CLIENT_SCP(this.Player.Connection, $"Melia.Comm.Recv('CustomCompanionShop', {{ {sb} }})"); + sb.Clear(); + } + + // End receival and set the companion shop + Send.ZC_EXEC_CLIENT_SCP(this.Player.Connection, "Melia.Comm.ExecData('CustomCompanionShop', M_SET_CUSTOM_COMPANION_SHOP)"); + Send.ZC_EXEC_CLIENT_SCP(this.Player.Connection, "Melia.Comm.EndRecv('CustomCompanionShop')"); + + // Open the companion shop UI + Send.ZC_ADDON_MSG(this.Player, "OPEN_DLG_COMPANIONSHOP", 0, "Normal"); + + await this.GetClientResponse(); + } } /// diff --git a/src/ZoneServer/Scripting/Shortcuts.Npcs.cs b/src/ZoneServer/Scripting/Shortcuts.Npcs.cs index f69aa128b..6522d8c5e 100644 --- a/src/ZoneServer/Scripting/Shortcuts.Npcs.cs +++ b/src/ZoneServer/Scripting/Shortcuts.Npcs.cs @@ -117,5 +117,25 @@ public static void CreateShop(string name, ShopCreationFunc creationFunc) var shopData = shopBuilder.Build(); ZoneServer.Instance.Data.ShopDb.AddOrReplace(shopData.Name, shopData); } + + /// + /// A function that initializes a companion shop. + /// + /// + public delegate void CompanionShopCreationFunc(CompanionShopBuilder shop); + + /// + /// Creates a custom companion shop. + /// + /// + /// + public static void CreateCompanionShop(string name, CompanionShopCreationFunc creationFunc) + { + var shopBuilder = new CompanionShopBuilder(name); + creationFunc(shopBuilder); + + var shopData = shopBuilder.Build(); + ZoneServer.Instance.Data.CompanionShopDb.AddOrReplace(shopData.Name, shopData); + } } } diff --git a/system/scripts/zone/content/test/npcs/cities/c_klaipe.cs b/system/scripts/zone/content/test/npcs/cities/c_klaipe.cs index a2037e11e..928601731 100644 --- a/system/scripts/zone/content/test/npcs/cities/c_klaipe.cs +++ b/system/scripts/zone/content/test/npcs/cities/c_klaipe.cs @@ -132,5 +132,37 @@ protected override void Load() await dialog.Msg("The capital may already be in ruins, but I will protect Klaipeda."); }); + + // [Companion Trader] Marina + //------------------------------------------------------------------------- + CreateMarinaCompanionShop(); + AddNpc(153005, "[Companion Trader] Marina", "Marina", "c_Klaipe", -1, -760, 90, async dialog => + { + dialog.SetTitle("Marina"); + dialog.SetPortrait("Dlg_port_kristina"); + + var response = await dialog.Select("Welcome! Looking for a companion?", + Option("Adopt Companion", "adopt"), + Option("Leave", "leave") + ); + + if (response == "adopt") + { + await dialog.OpenCustomCompanionShop("MarinaCompanions"); + } + }); + } + + /// + /// Creates Marina's companion shop + /// + private void CreateMarinaCompanionShop() + { + CreateCompanionShop("MarinaCompanions", shop => + { + shop.AddCompanion("Velhider", price: 110000); + shop.AddCompanion("hoglan_Pet", price: 453600); + shop.AddCompanion("pet_hawk", price: 453600); + }); } } diff --git a/system/scripts/zone/core/client/custom_npc_shops/004b.lua b/system/scripts/zone/core/client/custom_npc_shops/004b.lua new file mode 100644 index 000000000..b6180f9b7 --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/004b.lua @@ -0,0 +1,10 @@ +function COMPANIONSHOP_ADOPT_PAGE_LEFT(frame, ctrl, argstr, argnum) + local topFrame = frame:GetTopParentFrame() + local currentPage = tonumber(topFrame:GetUserValue('COMPANION_SHOP_PAGE')) or 1 + if argnum == 1 then + topFrame:SetUserValue('COMPANION_SHOP_PAGE', 1) + elseif currentPage > 1 then + topFrame:SetUserValue('COMPANION_SHOP_PAGE', currentPage - 1) + end + UPDATE_COMPANION_SELL_LIST(topFrame) +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/005.lua b/system/scripts/zone/core/client/custom_npc_shops/005.lua new file mode 100644 index 000000000..f8b9ce7ed --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/005.lua @@ -0,0 +1,17 @@ +-- Custom companion shop with pagination (6 per page) +M_COMPANION_SHOP = {} +M_COMPANION_SHOP_ITEMS_PER_PAGE = 6 + +function M_SET_CUSTOM_COMPANION_SHOP(companions) + M_COMPANION_SHOP = {} + for i = 1, #companions do + M_COMPANION_SHOP[i] = { + className = companions[i][1], + price = companions[i][2] + } + end +end + +if not M_ORIGINAL_UPDATE_COMPANION_SELL_LIST then + M_ORIGINAL_UPDATE_COMPANION_SELL_LIST = UPDATE_COMPANION_SELL_LIST +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/005b.lua b/system/scripts/zone/core/client/custom_npc_shops/005b.lua new file mode 100644 index 000000000..822c96b65 --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/005b.lua @@ -0,0 +1,47 @@ +function UPDATE_COMPANION_SELL_LIST(frame) + if M_COMPANION_SHOP and #M_COMPANION_SHOP > 0 then + local topBox = GET_CHILD_RECURSIVELY(frame, 'adoptTopBox') + topBox:RemoveAllChild() + + local currentPage = tonumber(frame:GetUserValue('COMPANION_SHOP_PAGE')) or 1 + local itemsPerPage = M_COMPANION_SHOP_ITEMS_PER_PAGE + local totalItems = #M_COMPANION_SHOP + local totalPages = math.ceil(totalItems / itemsPerPage) + + local startIdx = (currentPage - 1) * itemsPerPage + 1 + local endIdx = math.min(startIdx + itemsPerPage - 1, totalItems) + + local displayIdx = 1 + for i = startIdx, endIdx do + local comp = M_COMPANION_SHOP[i] + local cls = GetClass('Companion', comp.className) + if cls then + local ctrlSet = topBox:CreateControlSet("companionshop_ctrl", "CTRLSET_" .. displayIdx, ui.CENTER_HORZ, ui.TOP, 0, 0, 0, 0) + ctrlSet:SetUserValue("CLSNAME", cls.ClassName) + ctrlSet:SetUserValue('SELL_PRICE', comp.price) + ctrlSet:GetChild("price"):SetTextByKey("txt", GET_MONEY_IMG(20) .. " " .. GetCommaedText(comp.price)) + + local name = ctrlSet:GetChild("name") + local monCls = GetClass("Monster", cls.ClassName) + if monCls then + name:SetTextByKey("txt", monCls.Name) + name:SetTextByKey("JobID", cls.JobID) + CreateIcon(GET_CHILD(ctrlSet, "slot", "ui::CSlot")):SetImage(monCls.Icon) + end + ctrlSet:SetEventScript(ui.LBUTTONUP, "COMPANIONSHOP_SELECT_COMPANION") + displayIdx = displayIdx + 1 + end + end + GBOX_AUTO_ALIGN(topBox, 20, 0, 10, true, false) + + local pageText = GET_CHILD_RECURSIVELY(frame, 'adopt_pagetxt') + if pageText then + pageText:SetText('{@st41b}' .. currentPage .. ' / ' .. totalPages .. '{/}') + end + + frame:SetUserValue('COMPANION_SHOP_TOTAL_PAGES', totalPages) + M_UPDATE_COMPANION_PAGE_BUTTONS(frame) + else + M_ORIGINAL_UPDATE_COMPANION_SELL_LIST(frame) + end +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/005c.lua b/system/scripts/zone/core/client/custom_npc_shops/005c.lua new file mode 100644 index 000000000..77c521eac --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/005c.lua @@ -0,0 +1,11 @@ +function COMPANIONSHOP_ADOPT_PAGE_RIGHT(frame, ctrl, argstr, argnum) + local topFrame = frame:GetTopParentFrame() + local currentPage = tonumber(topFrame:GetUserValue('COMPANION_SHOP_PAGE')) or 1 + local totalPages = tonumber(topFrame:GetUserValue('COMPANION_SHOP_TOTAL_PAGES')) or 1 + if argnum == 1 then + topFrame:SetUserValue('COMPANION_SHOP_PAGE', totalPages) + elseif currentPage < totalPages then + topFrame:SetUserValue('COMPANION_SHOP_PAGE', currentPage + 1) + end + UPDATE_COMPANION_SELL_LIST(topFrame) +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/005d.lua b/system/scripts/zone/core/client/custom_npc_shops/005d.lua new file mode 100644 index 000000000..cdbcaa600 --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/005d.lua @@ -0,0 +1,31 @@ +if not M_ORIGINAL_COMPANIONSHOP_SELECT_COMPANION then + M_ORIGINAL_COMPANIONSHOP_SELECT_COMPANION = COMPANIONSHOP_SELECT_COMPANION +end + +function COMPANIONSHOP_SELECT_COMPANION(frame, ctrl) + local selectedCompa = ctrl:GetUserValue('CLSNAME') + + if M_COMPANION_SHOP and #M_COMPANION_SHOP > 0 then + local topFrame = frame:GetTopParentFrame() + local slot = GET_CHILD_RECURSIVELY(topFrame, 'compaSelectSlot') + local name = GET_CHILD_RECURSIVELY(topFrame, 'compaSelectText') + local buyMoneyText = GET_CHILD_RECURSIVELY(topFrame, 'buyMoneyText') + + local compaCls = GetClass('Companion', selectedCompa) + local monCls = GetClass('Monster', selectedCompa) + + if compaCls == nil or monCls == nil then + return + end + + local icon = CreateIcon(slot) + icon:SetImage(monCls.Icon) + name:SetTextByKey('name', monCls.Name) + buyMoneyText:SetTextByKey('money', ctrl:GetUserValue('SELL_PRICE')) + topFrame:SetUserValue('CLSNAME', selectedCompa) + + COMPANIONSHOP_UPDATE_REMAINMONEY(topFrame) + else + M_ORIGINAL_COMPANIONSHOP_SELECT_COMPANION(frame, ctrl) + end +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/005e.lua b/system/scripts/zone/core/client/custom_npc_shops/005e.lua new file mode 100644 index 000000000..dc6e73241 --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/005e.lua @@ -0,0 +1,23 @@ +if not M_ORIGINAL_COMPANIONSHOP_ADOPT then + M_ORIGINAL_COMPANIONSHOP_ADOPT = COMPANIONSHOP_ADOPT +end + +function COMPANIONSHOP_ADOPT(frame, ctrl) + local topFrame = frame:GetTopParentFrame() + local selectedCompa = topFrame:GetUserValue('CLSNAME') + + if M_COMPANION_SHOP and #M_COMPANION_SHOP > 0 then + local compaCls = GetClass('Companion', selectedCompa) + if compaCls == nil then + return + end + + if compaCls.JobID == 0 or 0 ~= session.GetJobGradePC(compaCls.JobID) then + TRY_CHECK_BARRACK_SLOT(frame, ctrl, true) + else + ui.MsgBox(ScpArgMsg("HaveNotJobForbyingCompaion")) + end + else + M_ORIGINAL_COMPANIONSHOP_ADOPT(frame, ctrl) + end +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/005f.lua b/system/scripts/zone/core/client/custom_npc_shops/005f.lua new file mode 100644 index 000000000..2c935cb25 --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/005f.lua @@ -0,0 +1,48 @@ +function M_TRY_COMPANION_HIRE_PART2(frm, clsName, byShop) + local cls = GetClass("Companion", clsName) + if not cls then + return + end + + local price + if M_COMPANION_SHOP and #M_COMPANION_SHOP > 0 then + for i = 1, #M_COMPANION_SHOP do + if M_COMPANION_SHOP[i].className == clsName then + price = M_COMPANION_SHOP[i].price + break + end + end + if not price then + return + end + else + if cls.SellPrice == "None" then + return + end + price = _G[cls.SellPrice](cls, GetMyPCObject()) + end + + local name = byShop and GET_CHILD_RECURSIVELY(frm, 'compaNameEdit') or frm:GetChild("input") + if not name then + return + end + local txt = name:GetText() + + if not txt then return end + if string.find(txt, ' ') then + ui.SysMsg(ClMsg("NameCannotIncludeSpace")) + return + end + if not ui.IsValidCharacterName(txt) then + ui.SysMsg(ScpArgMsg('CompanionNameIsInvalid')) + return + end + + if IsGreaterThanForBigNumber(price, GET_TOTAL_MONEY_STR()) == 1 then + ui.SysMsg(ClMsg('NotEnoughMoney')) + else + local scp = string.format("EXEC_BUY_COMPANION(\"%s\", \"%s\")", clsName, txt) + local msg = ScpArgMsg("PossibleChangeName_2{Name}", "Name", txt) + ui.MsgBox(msg.." {nl}"..ScpArgMsg("ReallyBuyCompanion?"), scp, "None") + end +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/005g.lua b/system/scripts/zone/core/client/custom_npc_shops/005g.lua new file mode 100644 index 000000000..b0361cfb9 --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/005g.lua @@ -0,0 +1,34 @@ +function TRY_COMPANION_HIRE(byShop) + local acc = session.barrack.GetMyAccount() + local bCls = GetClass("BarrackMap", acc:GetThemaName()) + if acc:GetPCCount() >= bCls.MaxCashPC + bCls.BaseSlot then + ui.SysMsg(ClMsg('CanCreateCharCuzMaxSlot')) + return + end + if session.pet.GetPetTotalCount() >= GET_MY_AVAILABLE_CHARACTER_SLOT() then + EXEC_BUY_CHARACTER_SLOT() + return + end + + local frm = byShop and ui.GetFrame('companionshop') or ui.GetFrame("companionhire") + local eggGuid = frm:GetUserValue("EGG_GUID") + if "None" ~= eggGuid then + local nfrm = ui.GetFrame("inputstring") + nfrm:SetUserValue("InputType", "PetName") + nfrm:SetUserValue("ItemIES", eggGuid) + nfrm:SetUserValue("ItemType", "Companionhire") + INPUT_STRING_BOX(ClMsg("InputCompanionName"), "EXEC_CHANGE_NAME_BY_ITEM", "", 0, 16) + frm:SetUserValue("EGG_GUID", 'None') + return + end + + local clsName = frm:GetUserValue("CLSNAME") + local exchange = frm:GetUserIValue("EXCHANGE_TIKET") + if 0 < exchange then + frm:SetUserValue("EXCHANGE_TIKET", 0) + TRY_CECK_BARRACK_SLOT_BY_COMPANION_EXCHANGE(exchange) + return + end + + M_TRY_COMPANION_HIRE_PART2(frm, clsName, byShop) +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/006.lua b/system/scripts/zone/core/client/custom_npc_shops/006.lua new file mode 100644 index 000000000..4b3f15b9e --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/006.lua @@ -0,0 +1,34 @@ +-- Create pagination UI elements for companion shop if they don't exist +function M_CREATE_COMPANION_PAGINATION_UI(frame) + local adoptBox = GET_CHILD_RECURSIVELY(frame, 'adoptBox') + if not adoptBox then return end + + -- Check if pagination elements already exist + if GET_CHILD_RECURSIVELY(frame, 'adopt_pagetxt') then return end + + -- Create page start button + local pageStart = adoptBox:CreateControl('button', 'adopt_pagestart', 0, 0, 60, 40) + pageStart = tolua.cast(pageStart, 'ui::CButton') + pageStart:SetGravity(ui.CENTER_HORZ, ui.BOTTOM) + pageStart:SetMargin(-135, 0, 0, 285) + pageStart:SetText('{img white_left_arrow 16 16}{img white_left_arrow 16 16}') + pageStart:SetClickSound('button_click_close') + pageStart:SetOverSound('button_cursor_over_2') + pageStart:SetSkinName('test_normal_button') + pageStart:SetTextAlign('center', 'center') + pageStart:SetEventScript(ui.LBUTTONUP, 'COMPANIONSHOP_ADOPT_PAGE_LEFT') + pageStart:SetEventScriptArgNumber(ui.LBUTTONUP, 1) + + -- Create page left button + local pageLeft = adoptBox:CreateControl('button', 'adopt_pageleft', 0, 0, 60, 40) + pageLeft = tolua.cast(pageLeft, 'ui::CButton') + pageLeft:SetGravity(ui.CENTER_HORZ, ui.BOTTOM) + pageLeft:SetMargin(-70, 0, 0, 285) + pageLeft:SetText('{img white_left_arrow 16 16}') + pageLeft:SetClickSound('button_click_close') + pageLeft:SetOverSound('button_cursor_over_2') + pageLeft:SetSkinName('test_normal_button') + pageLeft:SetTextAlign('center', 'center') + pageLeft:SetEventScript(ui.LBUTTONUP, 'COMPANIONSHOP_ADOPT_PAGE_LEFT') + pageLeft:SetEventScriptArgNumber(ui.LBUTTONUP, 0) +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/006b.lua b/system/scripts/zone/core/client/custom_npc_shops/006b.lua new file mode 100644 index 000000000..c4d430443 --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/006b.lua @@ -0,0 +1,31 @@ +-- Create pagination UI elements for companion shop (continued) +function M_CREATE_COMPANION_PAGINATION_UI_PART2(frame) + local adoptBox = GET_CHILD_RECURSIVELY(frame, 'adoptBox') + if not adoptBox then return end + + -- Create page right button + local pageRight = adoptBox:CreateControl('button', 'adopt_pageright', 0, 0, 60, 40) + pageRight = tolua.cast(pageRight, 'ui::CButton') + pageRight:SetGravity(ui.CENTER_HORZ, ui.BOTTOM) + pageRight:SetMargin(70, 0, 0, 285) + pageRight:SetText('{img white_right_arrow 16 16}') + pageRight:SetClickSound('button_click_close') + pageRight:SetOverSound('button_cursor_over_2') + pageRight:SetSkinName('test_normal_button') + pageRight:SetTextAlign('center', 'center') + pageRight:SetEventScript(ui.LBUTTONUP, 'COMPANIONSHOP_ADOPT_PAGE_RIGHT') + pageRight:SetEventScriptArgNumber(ui.LBUTTONUP, 0) + + -- Create page end button + local pageEnd = adoptBox:CreateControl('button', 'adopt_pageend', 0, 0, 60, 40) + pageEnd = tolua.cast(pageEnd, 'ui::CButton') + pageEnd:SetGravity(ui.CENTER_HORZ, ui.BOTTOM) + pageEnd:SetMargin(135, 0, 0, 285) + pageEnd:SetText('{img white_right_arrow 16 16}{img white_right_arrow 16 16}') + pageEnd:SetClickSound('button_click_close') + pageEnd:SetOverSound('button_cursor_over_2') + pageEnd:SetSkinName('test_normal_button') + pageEnd:SetTextAlign('center', 'center') + pageEnd:SetEventScript(ui.LBUTTONUP, 'COMPANIONSHOP_ADOPT_PAGE_RIGHT') + pageEnd:SetEventScriptArgNumber(ui.LBUTTONUP, 1) +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/006c.lua b/system/scripts/zone/core/client/custom_npc_shops/006c.lua new file mode 100644 index 000000000..c324488ff --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/006c.lua @@ -0,0 +1,36 @@ +-- Create pagination UI elements for companion shop (page text) +function M_CREATE_COMPANION_PAGINATION_UI_PART3(frame) + local adoptBox = GET_CHILD_RECURSIVELY(frame, 'adoptBox') + if not adoptBox then return end + + -- Create page text + local pageText = adoptBox:CreateControl('richtext', 'adopt_pagetxt', 0, 0, 35, 20) + pageText = tolua.cast(pageText, 'ui::CRichText') + pageText:SetGravity(ui.CENTER_HORZ, ui.BOTTOM) + pageText:SetMargin(0, 0, 0, 293) + pageText:SetText('{@st41b}1 / 1{/}') + pageText:SetTextAlign('center', 'center') +end + +-- Update button enable/disable states based on current page +function M_UPDATE_COMPANION_PAGE_BUTTONS(frame) + local currentPage = tonumber(frame:GetUserValue('COMPANION_SHOP_PAGE')) or 1 + local totalPages = tonumber(frame:GetUserValue('COMPANION_SHOP_TOTAL_PAGES')) or 1 + + local pageLeft = GET_CHILD_RECURSIVELY(frame, 'adopt_pageleft') + local pageStart = GET_CHILD_RECURSIVELY(frame, 'adopt_pagestart') + local pageRight = GET_CHILD_RECURSIVELY(frame, 'adopt_pageright') + local pageEnd = GET_CHILD_RECURSIVELY(frame, 'adopt_pageend') + + if pageLeft and pageStart then + local enableLeft = currentPage > 1 and 1 or 0 + pageLeft:SetEnable(enableLeft) + pageStart:SetEnable(enableLeft) + end + + if pageRight and pageEnd then + local enableRight = currentPage < totalPages and 1 or 0 + pageRight:SetEnable(enableRight) + pageEnd:SetEnable(enableRight) + end +end From 0f39869968a6a941334fe9732f1c53fb46c99f89 Mon Sep 17 00:00:00 2001 From: MrShadow Date: Thu, 13 Nov 2025 16:32:25 -0300 Subject: [PATCH 2/2] Companion shop fixes, Dialog resume silently fails. --- .../Commands/ChatCommands.Handlers.cs | 3 ++- src/ZoneServer/Network/PacketHandler.cs | 2 +- src/ZoneServer/Network/Send.cs | 11 ++++++++ src/ZoneServer/Scripting/Dialogues/Dialog.cs | 7 ++++- .../zone/content/test/npcs/cities/c_klaipe.cs | 6 ++--- .../zone/core/client/custom_npc_shops/004.lua | 27 +++++++++++++++++++ .../zone/core/client/custom_npc_shops/main.cs | 24 +++++++++++++++++ 7 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 system/scripts/zone/core/client/custom_npc_shops/004.lua diff --git a/src/ZoneServer/Commands/ChatCommands.Handlers.cs b/src/ZoneServer/Commands/ChatCommands.Handlers.cs index 7c10bbb30..1592d14de 100644 --- a/src/ZoneServer/Commands/ChatCommands.Handlers.cs +++ b/src/ZoneServer/Commands/ChatCommands.Handlers.cs @@ -2354,7 +2354,8 @@ private CommandResult HandleMedals(Character sender, Character target, string me private CommandResult HandlePetHire(Character sender, Character target, string message, string commandName, Arguments args) { // STUB: Not actually implemented yet! - sender.ServerMessage("Companion hired! But not actually implemented!"); + var petName = args.Count > 1 ? args.Get(1) : "Unknown"; + sender.ServerMessage($"Companion '{petName}' hired! But this feature is not actually implemented!"); return CommandResult.Okay; } } diff --git a/src/ZoneServer/Network/PacketHandler.cs b/src/ZoneServer/Network/PacketHandler.cs index 652efadf3..0b10f59bc 100644 --- a/src/ZoneServer/Network/PacketHandler.cs +++ b/src/ZoneServer/Network/PacketHandler.cs @@ -954,7 +954,7 @@ public void CZ_DIALOG_ACK(IZoneConnection conn, Packet packet) } else { - Send.ZC_DIALOG_CLOSE(conn); + Send.ZC_LEAVE_TRIGGER(conn); conn.CurrentDialog = null; } } diff --git a/src/ZoneServer/Network/Send.cs b/src/ZoneServer/Network/Send.cs index 8adb3d947..00a3b9b08 100644 --- a/src/ZoneServer/Network/Send.cs +++ b/src/ZoneServer/Network/Send.cs @@ -1694,6 +1694,17 @@ public static void ZC_DIALOG_CLOSE(IZoneConnection conn) conn.Send(packet); } + /// + /// Send ZC_LEAVE_TRIGGER after dialog close. + /// + /// + public static void ZC_LEAVE_TRIGGER(IZoneConnection conn) + { + var packet = new Packet(Op.ZC_LEAVE_TRIGGER); + + conn.Send(packet); + } + /// /// Sends ZC_DIALOG_STRINGINPUT to connection, containing a dialog /// message, and requesting putting in a string. diff --git a/src/ZoneServer/Scripting/Dialogues/Dialog.cs b/src/ZoneServer/Scripting/Dialogues/Dialog.cs index d6f2dfbed..56b0d8cb9 100644 --- a/src/ZoneServer/Scripting/Dialogues/Dialog.cs +++ b/src/ZoneServer/Scripting/Dialogues/Dialog.cs @@ -149,7 +149,12 @@ internal async void Start(DialogFunc dialogFunc) internal void Resume(string response) { if (this.State != DialogState.Waiting) - throw new InvalidOperationException($"The dialog is not paused and waiting for a response."); + { + // Return silently because client sometimes sends + // DialogAcknowledgement.Okay twice and this could crash the + // server. + return; + } _response = response; _resumeSignal.Release(); diff --git a/system/scripts/zone/content/test/npcs/cities/c_klaipe.cs b/system/scripts/zone/content/test/npcs/cities/c_klaipe.cs index 928601731..53b8266f7 100644 --- a/system/scripts/zone/content/test/npcs/cities/c_klaipe.cs +++ b/system/scripts/zone/content/test/npcs/cities/c_klaipe.cs @@ -141,15 +141,13 @@ protected override void Load() dialog.SetTitle("Marina"); dialog.SetPortrait("Dlg_port_kristina"); - var response = await dialog.Select("Welcome! Looking for a companion?", + var selectedOption = await dialog.Select("Welcome! Looking for a companion?", Option("Adopt Companion", "adopt"), Option("Leave", "leave") ); - if (response == "adopt") - { + if (selectedOption == "adopt") await dialog.OpenCustomCompanionShop("MarinaCompanions"); - } }); } diff --git a/system/scripts/zone/core/client/custom_npc_shops/004.lua b/system/scripts/zone/core/client/custom_npc_shops/004.lua new file mode 100644 index 000000000..7849d014b --- /dev/null +++ b/system/scripts/zone/core/client/custom_npc_shops/004.lua @@ -0,0 +1,27 @@ +if not M_ORIGINAL_ON_OPEN_DLG_COMPANIONSHOP then + M_ORIGINAL_ON_OPEN_DLG_COMPANIONSHOP = ON_OPEN_DLG_COMPANIONSHOP +end + +function ON_OPEN_DLG_COMPANIONSHOP(frame, msg, shopGroup) + M_ORIGINAL_ON_OPEN_DLG_COMPANIONSHOP(frame, msg, shopGroup) + frame:SetUserValue('COMPANION_SHOP_PAGE', 1) + + local tabCtrl = GET_CHILD_RECURSIVELY(frame, 'companionTab') + if tabCtrl then tabCtrl:ShowWindow(0) end + + local foodBox = GET_CHILD_RECURSIVELY(frame, 'foodBox') + if foodBox then foodBox:ShowWindow(0) end + + local adoptBox = GET_CHILD_RECURSIVELY(frame, 'adoptBox') + if adoptBox then adoptBox:ShowWindow(1) end + + -- Create pagination UI elements + M_CREATE_COMPANION_PAGINATION_UI(frame) + M_CREATE_COMPANION_PAGINATION_UI_PART2(frame) + M_CREATE_COMPANION_PAGINATION_UI_PART3(frame) + + -- Refresh the display now that pagination UI exists + if M_COMPANION_SHOP and #M_COMPANION_SHOP > 0 then + UPDATE_COMPANION_SELL_LIST(frame) + end +end diff --git a/system/scripts/zone/core/client/custom_npc_shops/main.cs b/system/scripts/zone/core/client/custom_npc_shops/main.cs index 8e50fd45c..eef2f7471 100644 --- a/system/scripts/zone/core/client/custom_npc_shops/main.cs +++ b/system/scripts/zone/core/client/custom_npc_shops/main.cs @@ -23,6 +23,18 @@ protected override void Load() this.LoadLuaScript("001.lua"); this.LoadLuaScript("002.lua"); this.LoadLuaScript("003.lua"); + this.LoadLuaScript("004.lua"); + this.LoadLuaScript("004b.lua"); + this.LoadLuaScript("005.lua"); + this.LoadLuaScript("005b.lua"); + this.LoadLuaScript("005c.lua"); + this.LoadLuaScript("005d.lua"); + this.LoadLuaScript("005e.lua"); + this.LoadLuaScript("005f.lua"); + this.LoadLuaScript("005g.lua"); + this.LoadLuaScript("006.lua"); + this.LoadLuaScript("006b.lua"); + this.LoadLuaScript("006c.lua"); } protected override void Ready(Character character) @@ -30,5 +42,17 @@ protected override void Ready(Character character) this.SendLuaScript(character, "001.lua"); this.SendLuaScript(character, "002.lua"); this.SendLuaScript(character, "003.lua"); + this.SendLuaScript(character, "004.lua"); + this.SendLuaScript(character, "004b.lua"); + this.SendLuaScript(character, "005.lua"); + this.SendLuaScript(character, "005b.lua"); + this.SendLuaScript(character, "005c.lua"); + this.SendLuaScript(character, "005d.lua"); + this.SendLuaScript(character, "005e.lua"); + this.SendLuaScript(character, "005f.lua"); + this.SendLuaScript(character, "005g.lua"); + this.SendLuaScript(character, "006.lua"); + this.SendLuaScript(character, "006b.lua"); + this.SendLuaScript(character, "006c.lua"); } }