Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/Shared/Data/Database/CompanionShops.cs
Original file line number Diff line number Diff line change
@@ -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<int, CompanionProductData> Products { get; set; } = new Dictionary<int, CompanionProductData>();

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; }
}

/// <summary>
/// Companion shop database, indexed by shop name.
/// </summary>
public class CompanionShopDb
{
private readonly Dictionary<string, CompanionShopData> _entries = new Dictionary<string, CompanionShopData>();

/// <summary>
/// Adds or replaces a companion shop in the database.
/// </summary>
/// <param name="name"></param>
/// <param name="data"></param>
public void AddOrReplace(string name, CompanionShopData data)
{
_entries[name] = data;
}

/// <summary>
/// Tries to find a companion shop by name.
/// </summary>
/// <param name="name"></param>
/// <param name="data"></param>
/// <returns></returns>
public bool TryFind(string name, out CompanionShopData data)
{
return _entries.TryGetValue(name, out data);
}

/// <summary>
/// Returns true if a companion shop with the given name exists.
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public bool Exists(string name)
{
return _entries.ContainsKey(name);
}
}
}
1 change: 1 addition & 0 deletions src/Shared/Data/MeliaData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
25 changes: 25 additions & 0 deletions src/ZoneServer/Commands/ChatCommands.Handlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -2333,5 +2334,29 @@ 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.
// ============================================================================
/// <summary>
/// Stub handler for pet/companion hiring functionality.
/// </summary>
/// <param name="sender"></param>
/// <param name="target"></param>
/// <param name="message"></param>
/// <param name="commandName"></param>
/// <param name="args"></param>
/// <returns></returns>
private CommandResult HandlePetHire(Character sender, Character target, string message, string commandName, Arguments args)
{
// STUB: Not actually implemented yet!
var petName = args.Count > 1 ? args.Get(1) : "Unknown";
sender.ServerMessage($"Companion '{petName}' hired! But this feature is not actually implemented!");
return CommandResult.Okay;
}
}
}
2 changes: 1 addition & 1 deletion src/ZoneServer/Network/PacketHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/ZoneServer/Network/Send.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,17 @@ public static void ZC_DIALOG_CLOSE(IZoneConnection conn)
conn.Send(packet);
}

/// <summary>
/// Send ZC_LEAVE_TRIGGER after dialog close.
/// </summary>
/// <param name="conn"></param>
public static void ZC_LEAVE_TRIGGER(IZoneConnection conn)
{
var packet = new Packet(Op.ZC_LEAVE_TRIGGER);

conn.Send(packet);
}

/// <summary>
/// Sends ZC_DIALOG_STRINGINPUT to connection, containing a dialog
/// message, and requesting putting in a string.
Expand Down
52 changes: 52 additions & 0 deletions src/ZoneServer/Scripting/CompanionShopBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using Melia.Shared.Data.Database;

namespace Melia.Zone.Scripting
{
/// <summary>
/// A builder for creating companion shop data.
/// </summary>
public class CompanionShopBuilder
{
private readonly CompanionShopData _shopData;
private int _productClassId = 200_001;

/// <summary>
/// Creates new instance for creating a companion shop with the given name.
/// </summary>
/// <param name="shopName"></param>
public CompanionShopBuilder(string shopName)
{
_shopData = new CompanionShopData();
_shopData.Name = shopName;
}

/// <summary>
/// Returns the built companion shop data.
/// </summary>
/// <returns></returns>
public CompanionShopData Build()
{
return _shopData;
}

/// <summary>
/// Adds companion to the shop.
/// </summary>
/// <param name="companionClassName">Class name of the companion for sale.</param>
/// <param name="price">The price for the companion.</param>
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);
}
}
}
49 changes: 48 additions & 1 deletion src/ZoneServer/Scripting/Dialogues/Dialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -622,6 +627,48 @@ public async Task OpenShop(ShopData shopData)

await this.GetClientResponse();
}

/// <summary>
/// Opens a custom companion shop with the given name.
/// </summary>
/// <param name="shopName"></param>
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();
}
}

/// <summary>
Expand Down
20 changes: 20 additions & 0 deletions src/ZoneServer/Scripting/Shortcuts.Npcs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,25 @@ public static void CreateShop(string name, ShopCreationFunc creationFunc)
var shopData = shopBuilder.Build();
ZoneServer.Instance.Data.ShopDb.AddOrReplace(shopData.Name, shopData);
}

/// <summary>
/// A function that initializes a companion shop.
/// </summary>
/// <param name="shop"></param>
public delegate void CompanionShopCreationFunc(CompanionShopBuilder shop);

/// <summary>
/// Creates a custom companion shop.
/// </summary>
/// <param name="name"></param>
/// <param name="creationFunc"></param>
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);
}
}
}
30 changes: 30 additions & 0 deletions system/scripts/zone/content/test/npcs/cities/c_klaipe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,35 @@ 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 selectedOption = await dialog.Select("Welcome! Looking for a companion?",
Option("Adopt Companion", "adopt"),
Option("Leave", "leave")
);

if (selectedOption == "adopt")
await dialog.OpenCustomCompanionShop("MarinaCompanions");
});
}

/// <summary>
/// Creates Marina's companion shop
/// </summary>
private void CreateMarinaCompanionShop()
{
CreateCompanionShop("MarinaCompanions", shop =>
{
shop.AddCompanion("Velhider", price: 110000);
shop.AddCompanion("hoglan_Pet", price: 453600);
shop.AddCompanion("pet_hawk", price: 453600);
});
}
}
27 changes: 27 additions & 0 deletions system/scripts/zone/core/client/custom_npc_shops/004.lua
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions system/scripts/zone/core/client/custom_npc_shops/004b.lua
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions system/scripts/zone/core/client/custom_npc_shops/005.lua
Original file line number Diff line number Diff line change
@@ -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
Loading