diff --git a/ClientLibrary/ConnectionManager.ClientToServerFunctions.cs b/ClientLibrary/ConnectionManager.ClientToServerFunctions.cs
index 7963153b5d..77a6922db9 100644
--- a/ClientLibrary/ConnectionManager.ClientToServerFunctions.cs
+++ b/ClientLibrary/ConnectionManager.ClientToServerFunctions.cs
@@ -626,6 +626,46 @@ public static void SendPlayerShopCloseOther(int handle, ushort @playerId, IntPtr
}
}
+ ///
+ /// Sends a to this connection.
+ ///
+ /// The handle of the connection.
+ /// The price in zen, or in account coins when PriceItemType is one of the account coin values. Encoded like the price of PlayerShopItemExtended.
+ /// The item (group in the highest 4 bits, number in the remaining ones) with which the price is paid, when RequiredItemAmount is greater than 0. Otherwise, 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P) and 0xFFF2 = Goblin Points define an account coin price; any other value a zen price. Encoded like the price of PlayerShopItemExtended.
+ /// The number of pieces of the item which is defined by PriceItemType; 0, if the price is not paid with items. Encoded like the price of PlayerShopItemExtended.
+ /// The item slot.
+ ///
+ /// Is sent by the client when: The player wants to set a price of an item which is inside his personal item shop, in zen or in one of the price types which are allowed by the server (see PlayerShopPriceTypesExtended).
+ /// Causes reaction on server side: The price is set for the specified item, if its price type is allowed and the amount is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse.
+ ///
+ [UnmanagedCallersOnly(EntryPoint = "SendPlayerShopSetItemPriceExtended")]
+ public static void SendPlayerShopSetItemPriceExtended(int handle, uint @moneyPrice, ushort @priceItemType, ushort @requiredItemAmount, byte @itemSlot)
+ {
+ if (!Connections.TryGetValue(handle, out var connection))
+ {
+ return;
+ }
+
+ try
+ {
+ connection.CreateAndSend(pipeWriter =>
+ {
+ var length = PlayerShopSetItemPriceExtendedRef.Length;
+ var packet = new PlayerShopSetItemPriceExtendedRef(pipeWriter.GetSpan(length)[..length]);
+ packet.MoneyPrice = @moneyPrice;
+ packet.PriceItemType = @priceItemType;
+ packet.RequiredItemAmount = @requiredItemAmount;
+ packet.ItemSlot = @itemSlot;
+
+ return length;
+ });
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ }
+ }
+
///
/// Sends a to this connection.
///
@@ -1570,14 +1610,14 @@ public static void SendCastleSiegeTaxInfoRequest(int handle)
/// Sends a to this connection.
///
/// The handle of the connection.
- /// 0=Undefined, 1=ChaosMachine, 2 = Normal, 3 = EntranceFeeLandOfTrials
- /// The tax rate.
+ /// The tax type.
+ /// The percentage rate for shop and Chaos Machine taxes, or the entrance fee amount for the hunting zone.
///
/// Is sent by the client when: The guild master wants to change the tax rate in the castle npc.
/// Causes reaction on server side: The server changes the tax rates accordingly.
///
[UnmanagedCallersOnly(EntryPoint = "SendCastleSiegeTaxChangeRequest")]
- public static void SendCastleSiegeTaxChangeRequest(int handle, byte @taxType, uint @taxRate)
+ public static void SendCastleSiegeTaxChangeRequest(int handle, CastleSiegeTaxType @taxType, uint @taxValue)
{
if (!Connections.TryGetValue(handle, out var connection))
{
@@ -1591,7 +1631,7 @@ public static void SendCastleSiegeTaxChangeRequest(int handle, byte @taxType, ui
var length = CastleSiegeTaxChangeRequestRef.Length;
var packet = new CastleSiegeTaxChangeRequestRef(pipeWriter.GetSpan(length)[..length]);
packet.TaxType = @taxType;
- packet.TaxRate = @taxRate;
+ packet.TaxValue = @taxValue;
return length;
});
@@ -1640,14 +1680,14 @@ public static void SendCastleSiegeTaxMoneyWithdraw(int handle, uint @amount)
/// Sends a to this connection.
///
/// The handle of the connection.
- /// The close state.
+ /// The is open.
/// The gate id.
///
/// Is sent by the client when: The guild member of the castle owner wants to toggle the gate switch.
/// Causes reaction on server side: The castle gate is getting opened or closed.
///
[UnmanagedCallersOnly(EntryPoint = "SendToggleCastleGateRequest")]
- public static void SendToggleCastleGateRequest(int handle, byte @closeState, ushort @gateId)
+ public static void SendToggleCastleGateRequest(int handle, byte @isOpen, ushort @gateId)
{
if (!Connections.TryGetValue(handle, out var connection))
{
@@ -1660,7 +1700,7 @@ public static void SendToggleCastleGateRequest(int handle, byte @closeState, ush
{
var length = ToggleCastleGateRequestRef.Length;
var packet = new ToggleCastleGateRequestRef(pipeWriter.GetSpan(length)[..length]);
- packet.CloseState = @closeState == 1;
+ packet.IsOpen = @isOpen == 1;
packet.GateId = @gateId;
return length;
@@ -1679,13 +1719,13 @@ public static void SendToggleCastleGateRequest(int handle, byte @closeState, ush
/// Team Number 0 to 7.
/// The position x.
/// The position y.
- /// 0 = Attack, 1 = Defend, 2 = Wait
+ /// The command.
///
/// Is sent by the client when: The guild master sent a command to his guild during the castle siege event.
/// Causes reaction on server side: The command is shown on the mini map of the guild members.
///
[UnmanagedCallersOnly(EntryPoint = "SendCastleGuildCommand")]
- public static void SendCastleGuildCommand(int handle, byte @team, byte @positionX, byte @positionY, byte @command)
+ public static void SendCastleGuildCommand(int handle, byte @team, byte @positionX, byte @positionY, CastleSiegeGuildCommandType @command)
{
if (!Connections.TryGetValue(handle, out var connection))
{
diff --git a/ClientLibrary/MUnique.Client.Library.csproj b/ClientLibrary/MUnique.Client.Library.csproj
index 59610080e8..e7fecc998e 100644
--- a/ClientLibrary/MUnique.Client.Library.csproj
+++ b/ClientLibrary/MUnique.Client.Library.csproj
@@ -15,7 +15,7 @@
nullable;CS4014;VSTHRD110;VSTHRD100
True
- 0.9.10
+ 0.9.11
diff --git a/docs/control-socket.md b/docs/control-socket.md
index 138f5c4fee..03d54635c3 100644
--- a/docs/control-socket.md
+++ b/docs/control-socket.md
@@ -110,7 +110,12 @@ The shops: `npc_shop` is the open NPC shop (`repair_shop`, `tax_rate`,
included) or `null`; `my_shop` is the player's own personal shop (`open`, the
`title` in its title field, its goods with `price`), `purchase_shop` the
personal shop the player looks into (`seller`, `title`, goods with `price`) or
-`null`. `repair_mode` says whether a click repairs instead of picking up.
+`null`. The goods of the personal shops whose price isn't in zen also carry the
+`price_item_type` of their price, as the server encodes it: the item (group in
+the highest 4 bits, number in the others) or the account coin (`0xFFF0` WCoin
+(C), `0xFFF1` WCoin (P), `0xFFF2` Goblin Points); see
+[personal-shop.md](personal-shop.md). `repair_mode` says whether a click
+repairs instead of picking up.
For the quests and their rewards: `class_name` (e.g. `Blade Knight`; `class`
is the client's number, e.g. 1 Dark Knight, 8 Blade Knight, 12 Blade Master),
diff --git a/docs/personal-shop.md b/docs/personal-shop.md
new file mode 100644
index 0000000000..e6d1ae947f
--- /dev/null
+++ b/docs/personal-shop.md
@@ -0,0 +1,49 @@
+# Personal shop prices in items and account coins
+
+A personal shop sells its items for zen. When the server allows it (the
+OpenMU plugin *Player shop currencies*), the price of each item can also be
+set in:
+
+- items which the server configured as price items, by default the Jewels of
+ Bless, Soul, Chaos, Creation and Life;
+- the account coins WCoin (C), WCoin (P) and Goblin Points, if the server
+ allows them.
+
+The server tells the client which price items and coins it allows when the
+character enters the game. Without that, the shop works with zen only, as in
+the original client.
+
+## Setting a price
+
+Right-click an item in the own shop (or drop one into it) to enter its price.
+When the server allows other prices, the price dialog names the currency of the
+price in its own line ("Currency: Jewel of Bless"), and its "Currency" button
+opens a menu of the allowed currencies:
+
+- the common ones first: zen, the account coins, and the jewels (Bless, Soul,
+ Chaos, Life, Creation, Guardian, Harmony);
+- the other items the server allows are listed in a second menu, behind an
+ "Other items" button, in the order the server sent them.
+
+A long menu takes two columns. Choosing a currency, or closing the menu, returns
+to the price dialog and keeps the typed amount. Zen prices below the item's value
+still ask for a confirmation; the other currencies don't.
+
+Which items can be prices is the server's choice: OpenMU offers the five jewels
+Bless, Soul, Chaos, Creation and Life by default, and its administrators can add
+any other item.
+
+The server checks every price: a price item or coin that isn't allowed, or an
+amount above the server's maximum, is refused with a message.
+
+## Buying
+
+The tooltip of an item in a shop shows its price with the currency, e.g.
+`Selling Price: 12 Jewel of Bless`. The "short of zen" warning is only shown
+for zen prices.
+
+Item prices are paid with the price items of the inventory: a single item
+counts as one piece, and the jewel bundles of the jewel mix as their number of
+pieces. The server doesn't split bundles and gives no change, so the buyer
+needs items which match the price exactly. When the items or coins are not
+enough, the server says which amount is missing.
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7b8c8dc0cc..c702f0aa9a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -586,7 +586,7 @@ file(MAKE_DIRECTORY "${MU_NUGET_CACHE_DIR}")
# same package the .NET ClientLibrary consumes (see MUnique.Client.Library.csproj).
# Sourcing from NuGet means a single versioned dependency instead of vendoring
# the entire OpenMU repository as a submodule.
-set(OPENMU_PACKETS_VERSION "0.9.10" CACHE STRING
+set(OPENMU_PACKETS_VERSION "0.9.11" CACHE STRING
"Version of MUnique.OpenMU.Network.Packets to source packet XML from")
set(OPENMU_PACKETS_XML
"${MU_NUGET_CACHE_DIR}/munique.openmu.network.packets/${OPENMU_PACKETS_VERSION}/contentFiles/any/net10.0/ServerToClient/ServerToClientPackets.xml")
diff --git a/src/Localization/Game.de.resx b/src/Localization/Game.de.resx
index afbb764cf7..ba32c8e86e 100644
--- a/src/Localization/Game.de.resx
+++ b/src/Localization/Game.de.resx
@@ -13114,4 +13114,19 @@
Bonus: %ls
+
+ W Coin(C)
+
+
+ Goblin-Punkte
+
+
+ Währung
+
+
+ Währung: %ls
+
+
+ Andere Gegenstände
+
diff --git a/src/Localization/Game.en.resx b/src/Localization/Game.en.resx
index aaf6f313b5..e0582d5a53 100644
--- a/src/Localization/Game.en.resx
+++ b/src/Localization/Game.en.resx
@@ -13177,4 +13177,19 @@
Bonus: %ls
+
+ W Coin(C)
+
+
+ Goblin Points
+
+
+ Currency
+
+
+ Currency: %ls
+
+
+ Other items
+
diff --git a/src/source/App/Control/ControlState.cpp b/src/source/App/Control/ControlState.cpp
index 2e433f013c..9af7ece173 100644
--- a/src/source/App/Control/ControlState.cpp
+++ b/src/source/App/Control/ControlState.cpp
@@ -250,8 +250,12 @@ json PersonalShopItems(mu::ui::window::CInventoryCtrl* grid, int priceTable)
}
const int slot = grid->GetIndexByItem(item);
json described = DescribeItem(*item, slot);
- int price = 0;
- described["price"] = GetPersonalItemPrice(slot, price, priceTable) ? json(price) : json(nullptr);
+ GameLogic::PersonalShop::ItemPrice price;
+ const bool hasPrice = GetPersonalItemPrice(slot, price, priceTable);
+ described["price"] = hasPrice ? json(price.Amount) : json(nullptr);
+ described["price_item_type"] = hasPrice && price.Kind != GameLogic::PersonalShop::PriceKind::Zen
+ ? json(price.PriceItemType)
+ : json(nullptr);
items.push_back(std::move(described));
}
return items;
diff --git a/src/source/Dotnet/PacketBindings_ClientToServer.h b/src/source/Dotnet/PacketBindings_ClientToServer.h
index 773349bde4..f5f976140e 100644
--- a/src/source/Dotnet/PacketBindings_ClientToServer.h
+++ b/src/source/Dotnet/PacketBindings_ClientToServer.h
@@ -52,6 +52,8 @@ typedef void(CORECLR_DELEGATE_CALLTYPE* SendPlayerShopItemBuyRequest)(int32_t, u
typedef void(CORECLR_DELEGATE_CALLTYPE* SendPlayerShopCloseOther)(int32_t, uint16_t , const char16_t* );
+typedef void(CORECLR_DELEGATE_CALLTYPE* SendPlayerShopSetItemPriceExtended)(int32_t, uint32_t , uint16_t , uint16_t , BYTE );
+
typedef void(CORECLR_DELEGATE_CALLTYPE* SendPickupItemRequest)(int32_t, uint16_t );
typedef void(CORECLR_DELEGATE_CALLTYPE* SendPickupItemRequest075)(int32_t, uint16_t );
@@ -104,13 +106,13 @@ typedef void(CORECLR_DELEGATE_CALLTYPE* SendCastleSiegeDefenseUpgradeRequest)(in
typedef void(CORECLR_DELEGATE_CALLTYPE* SendCastleSiegeTaxInfoRequest)(int32_t);
-typedef void(CORECLR_DELEGATE_CALLTYPE* SendCastleSiegeTaxChangeRequest)(int32_t, BYTE , uint32_t );
+typedef void(CORECLR_DELEGATE_CALLTYPE* SendCastleSiegeTaxChangeRequest)(int32_t, CastleSiegeTaxType , uint32_t );
typedef void(CORECLR_DELEGATE_CALLTYPE* SendCastleSiegeTaxMoneyWithdraw)(int32_t, uint32_t );
typedef void(CORECLR_DELEGATE_CALLTYPE* SendToggleCastleGateRequest)(int32_t, BYTE , uint16_t );
-typedef void(CORECLR_DELEGATE_CALLTYPE* SendCastleGuildCommand)(int32_t, BYTE , BYTE , BYTE , BYTE );
+typedef void(CORECLR_DELEGATE_CALLTYPE* SendCastleGuildCommand)(int32_t, BYTE , BYTE , BYTE , CastleSiegeGuildCommandType );
typedef void(CORECLR_DELEGATE_CALLTYPE* SendCastleSiegeHuntingZoneEntranceSetting)(int32_t, BYTE );
diff --git a/src/source/Dotnet/PacketFunctions_ClientToServer.cpp b/src/source/Dotnet/PacketFunctions_ClientToServer.cpp
index 9e5777a14b..7b060cad97 100644
--- a/src/source/Dotnet/PacketFunctions_ClientToServer.cpp
+++ b/src/source/Dotnet/PacketFunctions_ClientToServer.cpp
@@ -194,6 +194,17 @@ void PacketFunctions_ClientToServer::SendPlayerShopCloseOther(uint16_t playerId,
dotnet_SendPlayerShopCloseOther(this->GetHandle(), playerId, playerName);
}
+void PacketFunctions_ClientToServer::SendPlayerShopSetItemPriceExtended(uint32_t moneyPrice, uint16_t priceItemType, uint16_t requiredItemAmount, BYTE itemSlot)
+{
+ static const auto dotnet_SendPlayerShopSetItemPriceExtended = LoadManagedSymbol<::SendPlayerShopSetItemPriceExtended>("SendPlayerShopSetItemPriceExtended");
+ if (!dotnet_SendPlayerShopSetItemPriceExtended)
+ {
+ return;
+ }
+
+ dotnet_SendPlayerShopSetItemPriceExtended(this->GetHandle(), moneyPrice, priceItemType, requiredItemAmount, itemSlot);
+}
+
void PacketFunctions_ClientToServer::SendPickupItemRequest(uint16_t itemId)
{
static const auto dotnet_SendPickupItemRequest = LoadManagedSymbol<::SendPickupItemRequest>("SendPickupItemRequest");
@@ -480,7 +491,7 @@ void PacketFunctions_ClientToServer::SendCastleSiegeTaxInfoRequest()
dotnet_SendCastleSiegeTaxInfoRequest(this->GetHandle());
}
-void PacketFunctions_ClientToServer::SendCastleSiegeTaxChangeRequest(BYTE taxType, uint32_t taxRate)
+void PacketFunctions_ClientToServer::SendCastleSiegeTaxChangeRequest(CastleSiegeTaxType taxType, uint32_t taxValue)
{
static const auto dotnet_SendCastleSiegeTaxChangeRequest = LoadManagedSymbol<::SendCastleSiegeTaxChangeRequest>("SendCastleSiegeTaxChangeRequest");
if (!dotnet_SendCastleSiegeTaxChangeRequest)
@@ -488,7 +499,7 @@ void PacketFunctions_ClientToServer::SendCastleSiegeTaxChangeRequest(BYTE taxTyp
return;
}
- dotnet_SendCastleSiegeTaxChangeRequest(this->GetHandle(), taxType, taxRate);
+ dotnet_SendCastleSiegeTaxChangeRequest(this->GetHandle(), taxType, taxValue);
}
void PacketFunctions_ClientToServer::SendCastleSiegeTaxMoneyWithdraw(uint32_t amount)
@@ -502,7 +513,7 @@ void PacketFunctions_ClientToServer::SendCastleSiegeTaxMoneyWithdraw(uint32_t am
dotnet_SendCastleSiegeTaxMoneyWithdraw(this->GetHandle(), amount);
}
-void PacketFunctions_ClientToServer::SendToggleCastleGateRequest(BYTE closeState, uint16_t gateId)
+void PacketFunctions_ClientToServer::SendToggleCastleGateRequest(BYTE isOpen, uint16_t gateId)
{
static const auto dotnet_SendToggleCastleGateRequest = LoadManagedSymbol<::SendToggleCastleGateRequest>("SendToggleCastleGateRequest");
if (!dotnet_SendToggleCastleGateRequest)
@@ -510,10 +521,10 @@ void PacketFunctions_ClientToServer::SendToggleCastleGateRequest(BYTE closeState
return;
}
- dotnet_SendToggleCastleGateRequest(this->GetHandle(), closeState, gateId);
+ dotnet_SendToggleCastleGateRequest(this->GetHandle(), isOpen, gateId);
}
-void PacketFunctions_ClientToServer::SendCastleGuildCommand(BYTE team, BYTE positionX, BYTE positionY, BYTE command)
+void PacketFunctions_ClientToServer::SendCastleGuildCommand(BYTE team, BYTE positionX, BYTE positionY, CastleSiegeGuildCommandType command)
{
static const auto dotnet_SendCastleGuildCommand = LoadManagedSymbol<::SendCastleGuildCommand>("SendCastleGuildCommand");
if (!dotnet_SendCastleGuildCommand)
diff --git a/src/source/Dotnet/PacketFunctions_ClientToServer.h b/src/source/Dotnet/PacketFunctions_ClientToServer.h
index 98dae06bb5..fa78c881f9 100644
--- a/src/source/Dotnet/PacketFunctions_ClientToServer.h
+++ b/src/source/Dotnet/PacketFunctions_ClientToServer.h
@@ -218,6 +218,19 @@ class PacketFunctions_ClientToServer : public PacketFunctions_ClientToServer_Cus
///
void SendPlayerShopCloseOther(uint16_t playerId, const char16_t* playerName);
+ ///
+ /// Sends a PlayerShopSetItemPriceExtended to this connection.
+ ///
+ /// The price in zen, or in account coins when PriceItemType is one of the account coin values. Encoded like the price of PlayerShopItemExtended.
+ /// The item (group in the highest 4 bits, number in the remaining ones) with which the price is paid, when RequiredItemAmount is greater than 0. Otherwise, 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P) and 0xFFF2 = Goblin Points define an account coin price; any other value a zen price. Encoded like the price of PlayerShopItemExtended.
+ /// The number of pieces of the item which is defined by PriceItemType; 0, if the price is not paid with items. Encoded like the price of PlayerShopItemExtended.
+ /// The item slot.
+ ///
+ /// Is sent by the client when: The player wants to set a price of an item which is inside his personal item shop, in zen or in one of the price types which are allowed by the server (see PlayerShopPriceTypesExtended).
+ /// Causes reaction on server side: The price is set for the specified item, if its price type is allowed and the amount is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse.
+ ///
+ void SendPlayerShopSetItemPriceExtended(uint32_t moneyPrice, uint16_t priceItemType, uint16_t requiredItemAmount, BYTE itemSlot);
+
///
/// Sends a PickupItemRequest to this connection.
///
@@ -514,13 +527,13 @@ class PacketFunctions_ClientToServer : public PacketFunctions_ClientToServer_Cus
///
/// Sends a CastleSiegeTaxChangeRequest to this connection.
///
- /// 0=Undefined, 1=ChaosMachine, 2 = Normal, 3 = EntranceFeeLandOfTrials
- /// The tax rate.
+ /// The tax type.
+ /// The percentage rate for shop and Chaos Machine taxes, or the entrance fee amount for the hunting zone.
///
/// Is sent by the client when: The guild master wants to change the tax rate in the castle npc.
/// Causes reaction on server side: The server changes the tax rates accordingly.
///
- void SendCastleSiegeTaxChangeRequest(BYTE taxType, uint32_t taxRate);
+ void SendCastleSiegeTaxChangeRequest(CastleSiegeTaxType taxType, uint32_t taxValue);
///
/// Sends a CastleSiegeTaxMoneyWithdraw to this connection.
@@ -535,13 +548,13 @@ class PacketFunctions_ClientToServer : public PacketFunctions_ClientToServer_Cus
///
/// Sends a ToggleCastleGateRequest to this connection.
///
- /// The close state.
+ /// The is open.
/// The gate id.
///
/// Is sent by the client when: The guild member of the castle owner wants to toggle the gate switch.
/// Causes reaction on server side: The castle gate is getting opened or closed.
///
- void SendToggleCastleGateRequest(BYTE closeState, uint16_t gateId);
+ void SendToggleCastleGateRequest(BYTE isOpen, uint16_t gateId);
///
/// Sends a CastleGuildCommand to this connection.
@@ -549,12 +562,12 @@ class PacketFunctions_ClientToServer : public PacketFunctions_ClientToServer_Cus
/// Team Number 0 to 7.
/// The position x.
/// The position y.
- /// 0 = Attack, 1 = Defend, 2 = Wait
+ /// The command.
///
/// Is sent by the client when: The guild master sent a command to his guild during the castle siege event.
/// Causes reaction on server side: The command is shown on the mini map of the guild members.
///
- void SendCastleGuildCommand(BYTE team, BYTE positionX, BYTE positionY, BYTE command);
+ void SendCastleGuildCommand(BYTE team, BYTE positionX, BYTE positionY, CastleSiegeGuildCommandType command);
///
/// Sends a CastleSiegeHuntingZoneEntranceSetting to this connection.
diff --git a/src/source/Dotnet/PacketFunctions_ClientToServer_Enums.h b/src/source/Dotnet/PacketFunctions_ClientToServer_Enums.h
index 09fc84fba4..26eb05bc76 100644
--- a/src/source/Dotnet/PacketFunctions_ClientToServer_Enums.h
+++ b/src/source/Dotnet/PacketFunctions_ClientToServer_Enums.h
@@ -67,6 +67,21 @@ Join = 1, ///< The join type.
Leave = 2, ///< The leave type.
};
+enum class CastleSiegeTaxType : uint32_t
+{
+Undefined = 0, ///< No tax type is selected.
+ChaosMachine = 1, ///< The Chaos Machine tax rate.
+Store = 2, ///< The NPC store tax rate.
+HuntingZoneEntranceFee = 3, ///< The hunting-zone entrance fee.
+};
+
+enum class CastleSiegeGuildCommandType : uint32_t
+{
+Attack = 0, ///< Orders the team to attack.
+Defend = 1, ///< Orders the team to defend.
+Wait = 2, ///< Orders the team to wait.
+};
+
enum class FruitUsage : uint32_t
{
AddPoints = 0, ///< Adds 1~3 stat points to the character.
diff --git a/src/source/Engine/Object/ZzzInventory.cpp b/src/source/Engine/Object/ZzzInventory.cpp
index 876c91b88f..3cd519b4b2 100644
--- a/src/source/Engine/Object/ZzzInventory.cpp
+++ b/src/source/Engine/Object/ZzzInventory.cpp
@@ -1921,12 +1921,26 @@ static void RenderItemInfo(const UI::Scaling::Transform& screen, int sx, int sy,
&& !GameLogic::Items::IsPersonalShopBan(ip))
{
{
- int price = 0;
+ GameLogic::PersonalShop::ItemPrice itemPrice;
int indexInv = g_pMyShopInventory->GetInventoryCtrl()->GetIndexByItem(ip);
wchar_t Text[100];
- if (GetPersonalItemPrice(indexInv, price, g_IsPurchaseShop))
+ const bool hasPrice = GetPersonalItemPrice(indexInv, itemPrice, g_IsPurchaseShop);
+ if (hasPrice && itemPrice.Kind != GameLogic::PersonalShop::PriceKind::Zen)
{
+ // The server tells the player when the jewels or coins are not enough.
+ mu_swprintf(Text, L"%d %ls", itemPrice.Amount, GameLogic::PersonalShop::GetCurrencyName(itemPrice));
+ mu_swprintf(TextList[TextNum], I18N::Game::SellingPriceS, Text);
+ TextListColor[TextNum] = TEXT_COLOR_YELLOW;
+ TextBold[TextNum] = true;
+ TextNum++;
+ mu_swprintf(TextList[TextNum], L"\n");
+ TextNum++;
+ SkipNum++;
+ }
+ else if (hasPrice)
+ {
+ const int price = itemPrice.Amount;
ConvertGold(price, Text);
mu_swprintf(TextList[TextNum], I18N::Game::SellingPriceS, Text);
diff --git a/src/source/GameLogic/Events/SenatusInfo.cpp b/src/source/GameLogic/Events/SenatusInfo.cpp
index d30056d64e..736c4e2888 100644
--- a/src/source/GameLogic/Events/SenatusInfo.cpp
+++ b/src/source/GameLogic/Events/SenatusInfo.cpp
@@ -272,8 +272,8 @@ void CSenatusInfo::DoStatueUpgradeRecoverAction()
void CSenatusInfo::DoApplyTaxAction()
{
- SocketClient->ToGameServer()->SendCastleSiegeTaxChangeRequest(1, m_iChaosTaxRate);
- SocketClient->ToGameServer()->SendCastleSiegeTaxChangeRequest(2, m_iNormalTaxRate);
+ SocketClient->ToGameServer()->SendCastleSiegeTaxChangeRequest(CastleSiegeTaxType::ChaosMachine, m_iChaosTaxRate);
+ SocketClient->ToGameServer()->SendCastleSiegeTaxChangeRequest(CastleSiegeTaxType::Store, m_iNormalTaxRate);
}
void CSenatusInfo::DoWithdrawAction(DWORD dwMoney)
diff --git a/src/source/GameLogic/Items/PersonalShopCurrency.cpp b/src/source/GameLogic/Items/PersonalShopCurrency.cpp
new file mode 100644
index 0000000000..37dfcdf51a
--- /dev/null
+++ b/src/source/GameLogic/Items/PersonalShopCurrency.cpp
@@ -0,0 +1,106 @@
+#include "stdafx.h"
+#include "GameLogic/Items/PersonalShopCurrency.h"
+
+#include
+#include
+#include
+
+namespace
+{
+using GameLogic::PersonalShop::IsAccountCoin;
+using GameLogic::PersonalShop::ItemPrice;
+using GameLogic::PersonalShop::PriceKind;
+
+std::vector AllowedPriceTypes;
+
+// The jewels which are commonly used as prices, in the order of their usual value: Bless, Soul,
+// Chaos, Life, Creation, Guardian and Harmony (group in the highest 4 bits, number in the others).
+constexpr std::array CommonPriceItemTypes = {0xE00D, 0xE00E, 0xC00F, 0xE010, 0xE016, 0xE01F, 0xE02A};
+
+// The position of a price item type in the menu: account coins, then common jewels, then the rest.
+size_t MenuRank(uint16_t priceItemType)
+{
+ if (IsAccountCoin(priceItemType))
+ {
+ return priceItemType - GameLogic::PersonalShop::FirstAccountCoinPriceItemType;
+ }
+
+ const auto common = std::find(CommonPriceItemTypes.begin(), CommonPriceItemTypes.end(), priceItemType);
+ if (common != CommonPriceItemTypes.end())
+ {
+ return GameLogic::PersonalShop::AccountCoinCount + (common - CommonPriceItemTypes.begin());
+ }
+
+ return GameLogic::PersonalShop::AccountCoinCount + CommonPriceItemTypes.size();
+}
+
+ItemPrice ToPrice(uint16_t priceItemType, int amount)
+{
+ const PriceKind kind = IsAccountCoin(priceItemType) ? PriceKind::AccountCoin : PriceKind::Item;
+ return {amount, kind, priceItemType};
+}
+} // namespace
+
+namespace GameLogic::PersonalShop
+{
+ItemPrice DecodeExtendedPrice(uint32_t moneyPrice, uint16_t priceItemType, uint16_t requiredItemAmount)
+{
+ if (requiredItemAmount > 0)
+ {
+ return {requiredItemAmount, PriceKind::Item, priceItemType};
+ }
+
+ if (IsAccountCoin(priceItemType))
+ {
+ return {static_cast(moneyPrice), PriceKind::AccountCoin, priceItemType};
+ }
+
+ return {static_cast(moneyPrice), PriceKind::Zen, 0};
+}
+
+EncodedPrice EncodeExtendedPrice(const ItemPrice& price)
+{
+ switch (price.Kind)
+ {
+ case PriceKind::Item:
+ return {0, price.PriceItemType, static_cast(price.Amount)};
+ case PriceKind::AccountCoin:
+ return {static_cast(price.Amount), price.PriceItemType, 0};
+ case PriceKind::Zen:
+ default:
+ return {static_cast(price.Amount), 0, 0};
+ }
+}
+
+void SetAllowedPriceTypes(std::span priceItemTypes)
+{
+ AllowedPriceTypes.assign(priceItemTypes.begin(), priceItemTypes.end());
+}
+
+bool AreOtherPricesThanZenAllowed()
+{
+ return !AllowedPriceTypes.empty();
+}
+
+bool IsCommonPriceType(const ItemPrice& price)
+{
+ return price.Kind != PriceKind::Item ||
+ MenuRank(price.PriceItemType) < AccountCoinCount + CommonPriceItemTypes.size();
+}
+
+std::vector GetAllowedPriceTypes()
+{
+ std::vector priceItemTypes = AllowedPriceTypes;
+ // Stable, so the items which aren't common keep the order of the server.
+ std::stable_sort(priceItemTypes.begin(), priceItemTypes.end(),
+ [](uint16_t a, uint16_t b) { return MenuRank(a) < MenuRank(b); });
+
+ std::vector result{ItemPrice{}};
+ for (const uint16_t priceItemType : priceItemTypes)
+ {
+ result.push_back(ToPrice(priceItemType, 0));
+ }
+
+ return result;
+}
+} // namespace GameLogic::PersonalShop
diff --git a/src/source/GameLogic/Items/PersonalShopCurrency.h b/src/source/GameLogic/Items/PersonalShopCurrency.h
new file mode 100644
index 0000000000..170e9f3d4f
--- /dev/null
+++ b/src/source/GameLogic/Items/PersonalShopCurrency.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include
+#include
+#include
+
+// The prices of items in personal shops. Zen always works; prices in items
+// (e.g. jewels) and account coins only when the server allows them and told
+// the client which ones with the price types message.
+namespace GameLogic::PersonalShop
+{
+/// Bits of the item in the price item type: the group in the highest 4
+/// bits, the number in the remaining 12, as the server encodes it.
+inline constexpr int PriceItemTypeGroupShift = 12;
+inline constexpr uint16_t PriceItemTypeNumberMask = (1u << PriceItemTypeGroupShift) - 1;
+
+/// The server marks prices in account coins with these price item types,
+/// in the order WCoin (C), WCoin (P), Goblin Points. The amount is in the
+/// money field.
+inline constexpr uint16_t FirstAccountCoinPriceItemType = 0xFFF0;
+inline constexpr uint16_t AccountCoinCount = 3;
+
+constexpr bool IsAccountCoin(uint16_t priceItemType)
+{
+ return priceItemType >= FirstAccountCoinPriceItemType &&
+ priceItemType < FirstAccountCoinPriceItemType + AccountCoinCount;
+}
+
+enum class PriceKind : uint8_t
+{
+ Zen,
+ Item,
+ AccountCoin,
+};
+
+/// The price of an item in a personal shop.
+struct ItemPrice
+{
+ /// Zen, pieces of the price item or account coins, depending on the kind.
+ int Amount = 0;
+ PriceKind Kind = PriceKind::Zen;
+ /// The price type as the server names it: the item for item prices
+ /// (group in the highest 4 bits, number in the others), the reserved
+ /// value of the coin for coin prices, 0 for zen.
+ uint16_t PriceItemType = 0;
+};
+
+/// The price fields of the extended protocol, which the shop item list
+/// and the set price request share.
+struct EncodedPrice
+{
+ uint32_t MoneyPrice = 0;
+ uint16_t PriceItemType = 0;
+ uint16_t RequiredItemAmount = 0;
+};
+
+ItemPrice DecodeExtendedPrice(uint32_t moneyPrice, uint16_t priceItemType, uint16_t requiredItemAmount);
+EncodedPrice EncodeExtendedPrice(const ItemPrice& price);
+
+/// Stores the price types which the server allows in addition to zen.
+void SetAllowedPriceTypes(std::span priceItemTypes);
+
+/// True when the server allows other prices than zen, so the price can be
+/// set with the extended packet.
+bool AreOtherPricesThanZenAllowed();
+
+/// True for the currencies which are commonly used as prices: zen, the
+/// account coins and the jewels.
+bool IsCommonPriceType(const ItemPrice& price);
+
+/// The price types the player can choose from, without amount: the common
+/// ones first (zen, the account coins, the jewels), then the other allowed
+/// items in the order the server sent them.
+std::vector GetAllowedPriceTypes();
+
+/// The name of the currency of the price, e.g. "Jewel of Bless".
+/// Defined in PersonalShopCurrencyName.cpp, as it needs the item and
+/// text tables of the game.
+const wchar_t* GetCurrencyName(const ItemPrice& price);
+} // namespace GameLogic::PersonalShop
diff --git a/src/source/GameLogic/Items/PersonalShopCurrencyName.cpp b/src/source/GameLogic/Items/PersonalShopCurrencyName.cpp
new file mode 100644
index 0000000000..20e9f3885e
--- /dev/null
+++ b/src/source/GameLogic/Items/PersonalShopCurrencyName.cpp
@@ -0,0 +1,46 @@
+#include "stdafx.h"
+#include "GameLogic/Items/PersonalShopCurrency.h"
+
+#include
+
+#include "Engine/Object/ZzzInfomation.h"
+#include "I18N/All.h"
+
+// Kept apart from the protocol logic in PersonalShopCurrency.cpp, so that the
+// tests of that logic don't need the item and text tables of the game.
+namespace
+{
+int ToClientItemType(uint16_t priceItemType)
+{
+ const int group = priceItemType >> GameLogic::PersonalShop::PriceItemTypeGroupShift;
+ const int number = priceItemType & GameLogic::PersonalShop::PriceItemTypeNumberMask;
+ return group * MAX_ITEM_INDEX + number;
+}
+} // namespace
+
+namespace GameLogic::PersonalShop
+{
+const wchar_t* GetCurrencyName(const ItemPrice& price)
+{
+ switch (price.Kind)
+ {
+ case PriceKind::Item:
+ {
+ const int itemType = ToClientItemType(price.PriceItemType);
+ return itemType < MAX_ITEM ? ItemAttribute[itemType].Name : I18N::Game::Zen;
+ }
+ case PriceKind::AccountCoin:
+ {
+ static const std::array CoinNames = {
+ I18N::Game::WCoinC,
+ I18N::Game::WCoinP,
+ I18N::Game::GoblinPoints,
+ };
+ return CoinNames[price.PriceItemType - FirstAccountCoinPriceItemType];
+ }
+ case PriceKind::Zen:
+ default:
+ return I18N::Game::Zen;
+ }
+}
+} // namespace GameLogic::PersonalShop
diff --git a/src/source/GameLogic/Items/PersonalShopTitleImp.h b/src/source/GameLogic/Items/PersonalShopTitleImp.h
index f4f806bf25..38bdbb05cd 100644
--- a/src/source/GameLogic/Items/PersonalShopTitleImp.h
+++ b/src/source/GameLogic/Items/PersonalShopTitleImp.h
@@ -7,6 +7,7 @@
#include "Render/Models/ZzzBMD.h"
#include "Engine/Object/ZzzObject.h"
#include "Engine/Object/ZzzCharacter.h"
+#include "GameLogic/Items/PersonalShopCurrency.h"
inline POINT MakePos(long x, long y)
{
@@ -220,7 +221,7 @@ inline void DrawPersonalShopTitleImp()
//. CPersonalItemPriceTable
class CPersonalItemPriceTable
{
- std::map m_mapTable;
+ std::map m_mapTable;
static CPersonalItemPriceTable* ms_pSeller;
static CPersonalItemPriceTable* ms_pBuyer;
@@ -239,14 +240,9 @@ class CPersonalItemPriceTable
RemoveAllItemPrice();
}
- void AddItemPrice(int index, int price)
+ void AddItemPrice(int index, const GameLogic::PersonalShop::ItemPrice& price)
{
- auto mi = m_mapTable.find(index);
- if (mi != m_mapTable.end())
- {
- m_mapTable.erase(mi);
- }
- m_mapTable.insert(std::map::value_type(index, price));
+ m_mapTable[index] = price;
}
void RemoveItemPrice(int index)
{
@@ -261,7 +257,7 @@ class CPersonalItemPriceTable
m_mapTable.clear();
}
- bool GetItemPrice(int index, int& price)
+ bool GetItemPrice(int index, GameLogic::PersonalShop::ItemPrice& price)
{
auto mi = m_mapTable.find(index);
if (mi != m_mapTable.end())
@@ -323,7 +319,7 @@ inline void ReleasePersonalItemTable()
CPersonalItemPriceTable::ReleasePersonalItemTable();
}
-inline void AddPersonalItemPrice(int index, int price, int type)
+inline void AddPersonalItemPrice(int index, const GameLogic::PersonalShop::ItemPrice& price, int type)
{
CPersonalItemPriceTable* pPersonalItemTable = CPersonalItemPriceTable::GetObjPtr(type);
if (pPersonalItemTable)
@@ -332,6 +328,12 @@ inline void AddPersonalItemPrice(int index, int price, int type)
}
}
+/// Adds a price in zen.
+inline void AddPersonalItemPrice(int index, int price, int type)
+{
+ AddPersonalItemPrice(index, GameLogic::PersonalShop::ItemPrice{price}, type);
+}
+
inline void RemovePersonalItemPrice(int index, int type)
{
CPersonalItemPriceTable* pPersonalItemTable = CPersonalItemPriceTable::GetObjPtr(type);
@@ -350,7 +352,7 @@ inline void RemoveAllPerosnalItemPrice(int type)
}
}
-inline bool GetPersonalItemPrice(int index, int& price, int type)
+inline bool GetPersonalItemPrice(int index, GameLogic::PersonalShop::ItemPrice& price, int type)
{
CPersonalItemPriceTable* pPersonalItemTable = CPersonalItemPriceTable::GetObjPtr(type);
if (!pPersonalItemTable)
@@ -359,6 +361,17 @@ inline bool GetPersonalItemPrice(int index, int& price, int type)
return pPersonalItemTable->GetItemPrice(index, price);
}
+/// Gets the amount of the price, whatever its currency is.
+inline bool GetPersonalItemPrice(int index, int& price, int type)
+{
+ GameLogic::PersonalShop::ItemPrice itemPrice;
+ if (!GetPersonalItemPrice(index, itemPrice, type))
+ return false;
+
+ price = itemPrice.Amount;
+ return true;
+}
+
inline bool CheckPriceIntegrity(const wchar_t* szZen, int size)
{
if (size > 255)
diff --git a/src/source/GameLogic/Pets/GIPetManager.cpp b/src/source/GameLogic/Pets/GIPetManager.cpp
index 36e63e2488..c513ab1344 100644
--- a/src/source/GameLogic/Pets/GIPetManager.cpp
+++ b/src/source/GameLogic/Pets/GIPetManager.cpp
@@ -651,12 +651,22 @@ static std::uint8_t g_tabBar = 0;
}
else if ((iInvenType == mu::ui::window::TOOLTIP_TYPE_MY_SHOP) || (iInvenType == mu::ui::window::TOOLTIP_TYPE_PURCHASE_SHOP))
{
- int price = 0;
+ GameLogic::PersonalShop::ItemPrice itemPrice;
const int indexInv = g_pMyShopInventory->GetInventoryCtrl()->GetIndexByItem(pItem);
wchar_t textBuffer[kTooltipBufferCapacity] {};
- if (GetPersonalItemPrice(indexInv, price, g_IsPurchaseShop))
+ const bool hasPrice = GetPersonalItemPrice(indexInv, itemPrice, g_IsPurchaseShop);
+ if (hasPrice && itemPrice.Kind != GameLogic::PersonalShop::PriceKind::Zen)
{
+ // The server tells the player when the jewels or coins are not enough.
+ std::swprintf(textBuffer, kTooltipBufferCapacity, L"%d %ls", itemPrice.Amount,
+ GameLogic::PersonalShop::GetCurrencyName(itemPrice));
+ appendLine(TEXT_COLOR_YELLOW, true, false, priceFormat.c_str(), textBuffer);
+ appendEmptyLine();
+ }
+ else if (hasPrice)
+ {
+ const int price = itemPrice.Amount;
ConvertGold(price, textBuffer);
int priceColor = TEXT_COLOR_WHITE;
diff --git a/src/source/Network/Server/WSclient.cpp b/src/source/Network/Server/WSclient.cpp
index 0675296a2c..2113882e7a 100644
--- a/src/source/Network/Server/WSclient.cpp
+++ b/src/source/Network/Server/WSclient.cpp
@@ -692,6 +692,9 @@ void ReceiveCharacterListExtended(const BYTE* ReceiveBuffer)
{
InitGuildWar();
+ // The server sends the allowed price types again when a character enters the game.
+ GameLogic::PersonalShop::SetAllowedPriceTypes({});
+
ClearCharacters();
SelectedCharacter = -1;
SelectedHero = -1;
@@ -6218,7 +6221,7 @@ BOOL ReceiveEquipmentItemExtended(std::span ReceiveBuffer)
if (iSourceIndex >= MAX_MY_INVENTORY_EX_INDEX)
{
- int price = 0;
+ GameLogic::PersonalShop::ItemPrice price;
if (GetPersonalItemPrice(iSourceIndex, price, g_IsPurchaseShop))
{
RemovePersonalItemPrice(iSourceIndex, g_IsPurchaseShop);
@@ -9333,6 +9336,36 @@ void ReceiveDestroyPersonalShop(const BYTE* ReceiveBuffer)
}
}
+namespace
+{
+GameLogic::PersonalShop::ItemPrice DecodeShopItemPrice(const GETPSHOPITEM_DATAINFO& shopItem)
+{
+ return GameLogic::PersonalShop::DecodeExtendedPrice(static_cast(shopItem.MoneyPrice),
+ shopItem.PriceItemType, shopItem.RequiredItemAmount);
+}
+} // namespace
+
+void ReceivePersonalShopPriceTypes(std::span ReceiveBuffer)
+{
+ auto Header = safe_cast(ReceiveBuffer);
+ if (Header == nullptr)
+ {
+ assert(false);
+ return;
+ }
+
+ const auto priceTypesData = ReceiveBuffer.subspan(sizeof(PSHOP_PRICETYPES_HEADERINFO));
+ const size_t count = std::min(Header->Count, priceTypesData.size() / sizeof(WORD));
+ std::vector priceItemTypes(count);
+ for (size_t i = 0; i < count; ++i)
+ {
+ priceItemTypes[i] =
+ static_cast(priceTypesData[i * sizeof(WORD)] | (priceTypesData[i * sizeof(WORD) + 1] << 8));
+ }
+
+ GameLogic::PersonalShop::SetAllowedPriceTypes(priceItemTypes);
+}
+
void ReceivePersonalShopItemList(std::span ReceiveBuffer)
{
auto Header = safe_cast(ReceiveBuffer);
@@ -9366,11 +9399,11 @@ void ReceivePersonalShopItemList(std::span ReceiveBuffer)
int length = CalcItemLength(itemData);
itemData = itemData.subspan(0, length);
- // todo: use item prices as well when the UI is ready
- if (pShopItem->MoneyPrice > 0)
+ const auto price = DecodeShopItemPrice(*pShopItem);
+ if (price.Amount > 0)
{
UI::Shop::AddBrowsedShopItem({ pShopItem->ItemSlot, itemData });
- AddPersonalItemPrice(pShopItem->ItemSlot, pShopItem->MoneyPrice, PSHOPWNDTYPE_PURCHASE);
+ AddPersonalItemPrice(pShopItem->ItemSlot, price, PSHOPWNDTYPE_PURCHASE);
}
else
{
@@ -9443,7 +9476,7 @@ void ReceiveRefreshItemList(std::span ReceiveBuffer)
itemData = itemData.subspan(0, length);
items.push_back({ pShopItem->ItemSlot, itemData });
- AddPersonalItemPrice(pShopItem->ItemSlot, pShopItem->MoneyPrice, PSHOPWNDTYPE_SALE);
+ AddPersonalItemPrice(pShopItem->ItemSlot, DecodeShopItemPrice(*pShopItem), PSHOPWNDTYPE_SALE);
Offset += length;
}
@@ -9474,7 +9507,7 @@ void ReceiveRefreshItemList(std::span ReceiveBuffer)
itemData = itemData.subspan(0, length);
items.push_back({ pShopItem->ItemSlot, itemData });
- AddPersonalItemPrice(pShopItem->ItemSlot, pShopItem->MoneyPrice, PSHOPWNDTYPE_PURCHASE);
+ AddPersonalItemPrice(pShopItem->ItemSlot, DecodeShopItemPrice(*pShopItem), PSHOPWNDTYPE_PURCHASE);
Offset += length;
}
@@ -9545,6 +9578,9 @@ void ReceivePurchaseItem(std::span ReceiveBuffer)
UI::Chat::PostSystem(I18N::Game::InventoryIsFull, mu::ui::window::TYPE_ERROR_MESSAGE);
}
break;
+ case PURCHASEITEM_RESULTINFO::LackOfPriceCurrency:
+ // The server tells which amount of jewels or coins is missing in a message.
+ break;
case PURCHASEITEM_RESULTINFO::ItemBlock:
default:
g_ErrorReport.Write(L"@ [Fault] ReceivePurchaseItem (result : %d)\n", Header->Result);
@@ -14081,6 +14117,9 @@ static void ProcessPacket(const BYTE* ReceiveBuffer, int32_t Size)
case 0x08:
NotifySoldItem(ReceiveBuffer);
break;
+ case 0x0A:
+ ReceivePersonalShopPriceTypes(received_span);
+ break;
case 0x10:
ReceiveShopTitleChange(ReceiveBuffer);
break;
diff --git a/src/source/Network/Server/WSclient.h b/src/source/Network/Server/WSclient.h
index 1aa0db6980..095498a214 100644
--- a/src/source/Network/Server/WSclient.h
+++ b/src/source/Network/Server/WSclient.h
@@ -1729,11 +1729,20 @@ typedef struct tagGETPSHOPITEMLIST_HEADER {
#pragma pack(push, 1) // just to get the actual length by sizeof...
typedef struct tagGETPSHOPITEM_DATA {
INT MoneyPrice;
- WORD PriceItemType; // not yet used
- WORD RequiredItemAmount; // not yet used
+ WORD PriceItemType;
+ WORD RequiredItemAmount;
BYTE ItemSlot;
BYTE Item[PACKET_ITEM_LENGTH_EXTENDED_MIN];
} GETPSHOPITEM_DATAINFO, * LPGETPSHOPITEM_DATAINFO;
+
+// The price types which the server allows for prices in personal shops, in
+// addition to zen (C1 3F 0A). The price item types (WORD) follow it.
+typedef struct tagPSHOP_PRICETYPES_HEADER
+{
+ PBMSG_HEADER Header;
+ BYTE bySubcode;
+ BYTE Count;
+} PSHOP_PRICETYPES_HEADERINFO, *LPPSHOP_PRICETYPES_HEADERINFO;
#pragma pack(pop)
typedef struct tagCLOSEPSHOP_RESULT {
@@ -1756,6 +1765,7 @@ typedef struct tagPURCHASEITEM_RESULT {
LackOfMoney = 7,
MoneyOverflowOrNotEnoughSpace = 8,
ItemBlock = 9,
+ LackOfPriceCurrency = 10,
};
PBMSG_HEADER Header;
diff --git a/src/source/UI/Combat/SiegeWarCommander.cpp b/src/source/UI/Combat/SiegeWarCommander.cpp
index e03ed8de96..89985a1a62 100644
--- a/src/source/UI/Combat/SiegeWarCommander.cpp
+++ b/src/source/UI/Combat/SiegeWarCommander.cpp
@@ -82,7 +82,7 @@ bool mu::ui::window::CSiegeWarCommander::OnUpdateMouseEvent()
SelectCmd.byLifeTime = 100;
SocketClient->ToGameServer()->SendCastleGuildCommand(SelectCmd.byTeam, SelectCmd.byX, SelectCmd.byY,
- SelectCmd.byCmd);
+ static_cast(SelectCmd.byCmd));
m_iCurSelectBtnCommand = -1;
diff --git a/src/source/UI/Inventory/MyShopInventory.cpp b/src/source/UI/Inventory/MyShopInventory.cpp
index 15e8ff9fdf..0b62f26fe6 100644
--- a/src/source/UI/Inventory/MyShopInventory.cpp
+++ b/src/source/UI/Inventory/MyShopInventory.cpp
@@ -6,7 +6,9 @@
#include "UI/RmlBridge/RmlPointer.h"
#include "UI/RmlBridge/RmlWindowClose.h"
#include "UI/Dialogs/GenericConfirmDialog.h"
+#include "UI/Dialogs/GenericMenuDialog.h"
#include "UI/Core/WindowCommon.h" // g_IsPurchaseShop
+#include "GameLogic/Items/PersonalShopCurrency.h"
#include "GameLogic/Items/PersonalShopTitleImp.h"
#include "I18N/All.h"
#include "GameLogic/Items/ShopRestrictions.h"
@@ -18,174 +20,212 @@
#include "UI/RmlBridge/RmlNativeTextSize.h"
#include "Core/Utilities/StringUtils.h"
#include
+#include
+#include
const int iMAX_SHOPTITLE_MULTI = 26;
using namespace SEASON3B;
using namespace mu::ui::window;
-void mu::ui::window::ShowPersonalShopItemValueDialog()
+namespace
{
- // Numeric Mode::Text price entry. See MyShopInventory.h's own declaration comment for why
- // this is a free function shared across all 4 call sites instead of duplicated per site.
- GenericDialogConfig cfg;
- cfg.showCancel = true;
- cfg.lines = { { I18N::Game::EnterSellingPrice, false } };
- cfg.input = GenericDialogConfig::InputField{};
- cfg.input->mode = GenericDialogConfig::InputField::Mode::Text;
- cfg.input->maxLength = 8;
- cfg.input->numericOnly = true;
+// Sends the price of an item of the own personal shop. Zen prices use the original packet, so
+// they work with every server.
+void SendItemPrice(int itemSlot, const GameLogic::PersonalShop::ItemPrice& price)
+{
+ if (price.Kind == GameLogic::PersonalShop::PriceKind::Zen)
+ {
+ SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(itemSlot, price.Amount);
+ return;
+ }
+
+ const auto encoded = GameLogic::PersonalShop::EncodeExtendedPrice(price);
+ SocketClient->ToGameServer()->SendPlayerShopSetItemPriceExtended(encoded.MoneyPrice, encoded.PriceItemType,
+ encoded.RequiredItemAmount, itemSlot);
+}
- cfg.onPrimary = []
+// Sends the price of the item which is moved into or within the own shop, or whose price is
+// changed, and remembers it for the tooltip. sourceIndex is the shop slot of the item whose price
+// is changed when no item is picked; a negative one changes nothing.
+void ApplyPersonalShopItemPrice(const GameLogic::PersonalShop::ItemPrice& price, int sourceIndex)
+{
+ if (g_pMyShopInventory->IsEnablePersonalShop() == true)
{
- const std::wstring strTextW = g_pGenericConfirmDialog->GetInputText();
- if (strTextW.empty())
- {
- g_pGenericConfirmDialog->KeepOpen();
- return;
- }
- const int iInputZen = _wtoi(strTextW.c_str());
- if (iInputZen == 0)
- {
- g_pGenericConfirmDialog->KeepOpen();
- return;
- }
- const wchar_t* strText = strTextW.c_str();
+ SocketClient->ToGameServer()->SendPlayerShopClose();
+ }
- CPickedItem* pPickedItem = CInventoryCtrl::GetPickedItem();
- ITEM* pItem = NULL;
- if (pPickedItem)
- {
- pItem = pPickedItem->GetItem();
- }
- else
+ CPickedItem* pPickedItem = CInventoryCtrl::GetPickedItem();
+ if (pPickedItem == nullptr)
+ {
+ if (sourceIndex >= 0)
{
- int iSourceIndex = g_pMyShopInventory->GetSourceIndex();
- pItem = g_pMyShopInventory->FindItem(iSourceIndex);
+ SendItemPrice(sourceIndex, price);
+ AddPersonalItemPrice(sourceIndex, price, g_IsPurchaseShop);
}
- bool bResult = false;
- if (pItem)
+ return;
+ }
+
+ ITEM* pItemObj = pPickedItem->GetItem();
+ const int iSourceIndex = pPickedItem->GetSourceLinealPos();
+ const int iTargetIndex = g_pMyShopInventory->GetTargetIndex();
+ const auto* pOwnerInventory = pPickedItem->GetOwnerInventory();
+ const bool isFromInventory = pOwnerInventory == g_pMyInventory->GetInventoryCtrl() || pOwnerInventory == nullptr;
+ const bool isFromShop = pOwnerInventory == g_pMyShopInventory->GetInventoryCtrl();
+ if (isFromInventory || isFromShop)
+ {
+ SendItemPrice(iSourceIndex, price);
+ const STORAGE_TYPE sourceStorage = isFromShop ? STORAGE_TYPE::MYSHOP : STORAGE_TYPE::INVENTORY;
+ SendRequestEquipmentItem(sourceStorage, iSourceIndex, pItemObj, STORAGE_TYPE::MYSHOP, iTargetIndex);
+ }
+
+ AddPersonalItemPrice(iTargetIndex, price, g_IsPurchaseShop);
+}
+
+ITEM* FindPricedItem()
+{
+ CPickedItem* pPickedItem = CInventoryCtrl::GetPickedItem();
+ return pPickedItem ? pPickedItem->GetItem() : g_pMyShopInventory->FindItem(g_pMyShopInventory->GetSourceIndex());
+}
+
+// A zen price below the value of the item has to be confirmed by the player.
+void ShowZenPriceConfirmation(const ITEM& item, const wchar_t* strPrice,
+ const GameLogic::PersonalShop::ItemPrice& price)
+{
+ // Plain item3D + OkCancel confirm, same shape as NPCShop.cpp's own IsHighValueItem() sell
+ // confirm. The price is just captured by value here instead of needing a GenericDialogConfig
+ // field of its own.
+ wchar_t strText[MAX_TEXT_LENGTH] = {};
+ mu_swprintf(strText, I18N::Game::SellingPriceSZen, strPrice);
+
+ GenericDialogConfig cfg;
+ cfg.showCancel = true;
+ cfg.item3D = item;
+ cfg.lines = {
+ {strText, true},
+ {I18N::Game::DoYouWantToSellItemAtThisPrice, false},
+ };
+ cfg.onPrimary = [price]
+ {
+ int sourceIndex = -1;
+ if (ITEM* pItem = g_pMyShopInventory->FindItem(g_pMyShopInventory->GetSourceIndex()))
{
- DWORD dwItemValue = ItemValue(pItem, 2);
- if (iInputZen < (int)dwItemValue)
- bResult = true;
+ sourceIndex = g_pMyShopInventory->GetItemInventoryIndex(pItem);
}
- if (bResult == true)
- {
- // Plain item3D + OkCancel confirm, same shape as NPCShop.cpp's own IsHighValueItem()
- // sell confirm. iInputZen is just captured by value here instead of needing a
- // GenericDialogConfig field of its own.
- wchar_t strText2[MAX_TEXT_LENGTH] = { 0, };
- mu_swprintf(strText2, I18N::Game::SellingPriceSZen, strText);
-
- GenericDialogConfig cfg;
- cfg.showCancel = true;
- cfg.item3D = *pItem;
- cfg.lines = {
- { strText2, true },
- { I18N::Game::DoYouWantToSellItemAtThisPrice, false },
- };
- cfg.onPrimary = [iInputZen]
- {
- if (g_pMyShopInventory->IsEnablePersonalShop() == true)
- {
- SocketClient->ToGameServer()->SendPlayerShopClose();
- }
+ ApplyPersonalShopItemPrice(price, sourceIndex);
+ };
+ cfg.onCancel = [] { CInventoryCtrl::BackupPickedItem(); };
+ // Chained from inside the price dialog's own onPrimary -- proven-safe pattern, see
+ // CGenericConfirmDialog::Resolve()'s own comment on why this doesn't stomp the dialog that's
+ // still resolving.
+ g_pGenericConfirmDialog->Show(std::move(cfg));
+}
- CPickedItem* pPickedItem2 = CInventoryCtrl::GetPickedItem();
+void ShowItemPriceDialog(const GameLogic::PersonalShop::ItemPrice& priceType, const std::wstring& initialText);
- int iSourceIndex = -1, iTargetIndex = -1;
+// A menu of currencies to choose the one of the price from. Choosing one, or closing the menu,
+// shows the price dialog again with the typed amount. A long list takes two columns, so the
+// menu stays on the screen.
+void ShowPriceTypeMenu(const std::wstring& title, const std::vector& priceTypes,
+ std::optional> otherItems,
+ const GameLogic::PersonalShop::ItemPrice& current, const std::wstring& typedText)
+{
+ constexpr size_t MaximumSingleColumnButtons = 6;
- if (pPickedItem2)
- {
- ITEM* pItemObj = pPickedItem2->GetItem();
- iSourceIndex = pPickedItem2->GetSourceLinealPos();
- iTargetIndex = g_pMyShopInventory->GetTargetIndex();
+ GenericMenuConfig cfg;
+ cfg.title = title;
+ for (const auto& priceType : priceTypes)
+ {
+ GenericMenuConfig::MenuButton button;
+ button.label = GameLogic::PersonalShop::GetCurrencyName(priceType);
+ button.onClick = [priceType, typedText] { ShowItemPriceDialog(priceType, typedText); };
+ cfg.buttons.push_back(std::move(button));
+ }
- if (pPickedItem2->GetOwnerInventory() == g_pMyInventory->GetInventoryCtrl())
- {
- SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(iSourceIndex, iInputZen);
- SendRequestEquipmentItem(STORAGE_TYPE::INVENTORY, iSourceIndex, pItemObj, STORAGE_TYPE::MYSHOP, iTargetIndex);
- }
- else if (pPickedItem2->GetOwnerInventory() == nullptr)
- {
- SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(iSourceIndex, iInputZen);
- SendRequestEquipmentItem(STORAGE_TYPE::INVENTORY, iSourceIndex, pItemObj, STORAGE_TYPE::MYSHOP, iTargetIndex);
- }
- else if (pPickedItem2->GetOwnerInventory() == g_pMyShopInventory->GetInventoryCtrl())
- {
- SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(iSourceIndex, iInputZen);
- SendRequestEquipmentItem(STORAGE_TYPE::MYSHOP, iSourceIndex, pItemObj, STORAGE_TYPE::MYSHOP, iTargetIndex);
- }
+ if (otherItems)
+ {
+ GenericMenuConfig::MenuButton button;
+ button.label = I18N::Game::OtherItems;
+ button.onClick = [items = *otherItems, current, typedText]
+ { ShowPriceTypeMenu(I18N::Game::OtherItems, items, std::nullopt, current, typedText); };
+ cfg.buttons.push_back(std::move(button));
+ }
- AddPersonalItemPrice(iTargetIndex, iInputZen, g_IsPurchaseShop);
- }
- else
- {
- ITEM* pItem2 = g_pMyShopInventory->FindItem(g_pMyShopInventory->GetSourceIndex());
- if (pItem2)
- {
- iSourceIndex = g_pMyShopInventory->GetItemInventoryIndex(pItem2);
- if (iSourceIndex >= 0)
- {
- SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(iSourceIndex, iInputZen);
- AddPersonalItemPrice(iSourceIndex, iInputZen, g_IsPurchaseShop);
- }
- }
- }
- };
- cfg.onCancel = []
- {
- CInventoryCtrl::BackupPickedItem();
- };
- // Chained from inside ShowPersonalShopItemValueDialog()'s own onPrimary -- proven-safe
- // pattern, see CGenericConfirmDialog::Resolve()'s own comment on why this doesn't stomp
- // the dialog that's still resolving.
- g_pGenericConfirmDialog->Show(std::move(cfg));
- }
- else
- {
- if (g_pMyShopInventory->IsEnablePersonalShop() == true)
- {
- SocketClient->ToGameServer()->SendPlayerShopClose();
- }
+ if (cfg.buttons.size() > MaximumSingleColumnButtons)
+ {
+ cfg.columns = 2;
+ }
- CPickedItem* pPickedItem2 = CInventoryCtrl::GetPickedItem();
+ GenericMenuConfig::MenuButton cancel;
+ cancel.label = I18N::Game::Cancel;
+ cancel.compact = true;
+ cancel.dismiss = true;
+ cancel.onClick = [current, typedText] { ShowItemPriceDialog(current, typedText); };
+ cfg.buttons.push_back(std::move(cancel));
+ cfg.onCancel = [current, typedText] { ShowItemPriceDialog(current, typedText); };
- int iSourceIndex = -1, iTargetIndex = -1;
+ g_pGenericMenuDialog->Show(std::move(cfg));
+}
- if (pPickedItem2)
- {
- ITEM* pItemObj = pPickedItem2->GetItem();
- iSourceIndex = pPickedItem2->GetSourceLinealPos();
- iTargetIndex = g_pMyShopInventory->GetTargetIndex();
+// The currencies the server allows: the common ones (zen, the account coins, the jewels), and
+// the other items in a second menu behind an "Other items" button.
+void ShowPriceTypeMenu(const GameLogic::PersonalShop::ItemPrice& current, const std::wstring& typedText)
+{
+ std::vector common;
+ std::vector others;
+ for (const auto& priceType : GameLogic::PersonalShop::GetAllowedPriceTypes())
+ {
+ (GameLogic::PersonalShop::IsCommonPriceType(priceType) ? common : others).push_back(priceType);
+ }
- if (pPickedItem2->GetOwnerInventory() == g_pMyInventory->GetInventoryCtrl())
- {
- SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(iSourceIndex, iInputZen);
- SendRequestEquipmentItem(STORAGE_TYPE::INVENTORY, iSourceIndex, pItemObj, STORAGE_TYPE::MYSHOP, iTargetIndex);
- }
- else if (pPickedItem2->GetOwnerInventory() == nullptr)
- {
- SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(iSourceIndex, iInputZen);
- SendRequestEquipmentItem(STORAGE_TYPE::INVENTORY, iSourceIndex, pItemObj, STORAGE_TYPE::MYSHOP, iTargetIndex);
- }
- else if (pPickedItem2->GetOwnerInventory() == g_pMyShopInventory->GetInventoryCtrl())
- {
- SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(iSourceIndex, iInputZen);
- SendRequestEquipmentItem(STORAGE_TYPE::MYSHOP, iSourceIndex, pItemObj, STORAGE_TYPE::MYSHOP, iTargetIndex);
- }
+ ShowPriceTypeMenu(I18N::Game::Currency, common, others.empty() ? std::nullopt : std::optional(std::move(others)),
+ current, typedText);
+}
- AddPersonalItemPrice(iTargetIndex, iInputZen, g_IsPurchaseShop);
- }
- else
- {
- iSourceIndex = g_pMyShopInventory->GetSourceIndex();
- SocketClient->ToGameServer()->SendPlayerShopSetItemPrice(iSourceIndex, iInputZen);
- AddPersonalItemPrice(iSourceIndex, iInputZen, g_IsPurchaseShop);
- }
+// The price dialog with the given currency. When the server allows other prices than zen, it
+// names the currency in its own line, and its secondary button opens the menu of the currencies.
+void ShowItemPriceDialog(const GameLogic::PersonalShop::ItemPrice& priceType, const std::wstring& initialText)
+{
+ GenericDialogConfig cfg;
+ cfg.showCancel = true;
+ cfg.lines = {{I18N::Game::EnterSellingPrice, false}};
+ cfg.input = GenericDialogConfig::InputField{};
+ cfg.input->mode = GenericDialogConfig::InputField::Mode::Text;
+ cfg.input->maxLength = 8;
+ cfg.input->numericOnly = true;
+ cfg.input->initialText = initialText;
+
+ if (GameLogic::PersonalShop::AreOtherPricesThanZenAllowed())
+ {
+ wchar_t currencyLine[MAX_TEXT_LENGTH] = {};
+ mu_swprintf(currencyLine, I18N::Game::CurrencyLs, GameLogic::PersonalShop::GetCurrencyName(priceType));
+ cfg.lines.push_back({currencyLine, true, RGBA(255, 210, 60, 255)});
+ cfg.secondaryLabel = I18N::Game::Currency;
+ cfg.onSecondary = [priceType] { ShowPriceTypeMenu(priceType, g_pGenericConfirmDialog->GetInputText()); };
+ }
+
+ cfg.onPrimary = [priceType]
+ {
+ const std::wstring strTextW = g_pGenericConfirmDialog->GetInputText();
+ GameLogic::PersonalShop::ItemPrice price = priceType;
+ price.Amount = strTextW.empty() ? 0 : _wtoi(strTextW.c_str());
+ if (price.Amount == 0)
+ {
+ g_pGenericConfirmDialog->KeepOpen();
+ return;
+ }
+
+ ITEM* pItem = FindPricedItem();
+ if (pItem != nullptr && price.Kind == GameLogic::PersonalShop::PriceKind::Zen &&
+ price.Amount < static_cast(ItemValue(pItem, 2)))
+ {
+ ShowZenPriceConfirmation(*pItem, strTextW.c_str(), price);
+ }
+ else
+ {
+ ApplyPersonalShopItemPrice(price, g_pMyShopInventory->GetSourceIndex());
}
g_pMyShopInventory->SetInputValueTextBox(false);
@@ -198,6 +238,14 @@ void mu::ui::window::ShowPersonalShopItemValueDialog()
g_pGenericConfirmDialog->Show(std::move(cfg));
}
+} // namespace
+
+void mu::ui::window::ShowPersonalShopItemValueDialog()
+{
+ // Numeric Mode::Text price entry. See MyShopInventory.h's own declaration comment for why
+ // this is a free function shared across all 4 call sites instead of duplicated per site.
+ ShowItemPriceDialog({}, {});
+}
mu::ui::window::CMyShopInventory::CMyShopInventory() : m_SourceIndex(-1), m_TargetIndex(-1), m_EnablePersonalShop(false)
{
diff --git a/src/source/UI/NPCs/UIGateKeeper.cpp b/src/source/UI/NPCs/UIGateKeeper.cpp
index b1023292d9..df4f31f9db 100644
--- a/src/source/UI/NPCs/UIGateKeeper.cpp
+++ b/src/source/UI/NPCs/UIGateKeeper.cpp
@@ -27,7 +27,8 @@ void CUIGateKeeper::SendPublicSetting()
void CUIGateKeeper::SendEnteranceFee()
{
- SocketClient->ToGameServer()->SendCastleSiegeTaxChangeRequest(3, m_iViewEntranceFee);
+ SocketClient->ToGameServer()->SendCastleSiegeTaxChangeRequest(CastleSiegeTaxType::HuntingZoneEntranceFee,
+ m_iViewEntranceFee);
}
void CUIGateKeeper::EnteranceFeeUp()
diff --git a/tests/gamelogic/CMakeLists.txt b/tests/gamelogic/CMakeLists.txt
index 79e4f6c739..ae26bd3c01 100644
--- a/tests/gamelogic/CMakeLists.txt
+++ b/tests/gamelogic/CMakeLists.txt
@@ -5,3 +5,11 @@ mu_add_test(
LINK_LIBS
MuClient
)
+
+mu_add_test(
+ NAME test_personal_shop_currency
+ SOURCES
+ test_personal_shop_currency.cpp
+ LINK_LIBS
+ MuClient
+)
diff --git a/tests/gamelogic/test_personal_shop_currency.cpp b/tests/gamelogic/test_personal_shop_currency.cpp
new file mode 100644
index 0000000000..a5277cc37c
--- /dev/null
+++ b/tests/gamelogic/test_personal_shop_currency.cpp
@@ -0,0 +1,109 @@
+// Tests of the prices in personal shops: how the extended protocol encodes them, and which currencies the price
+// input offers after the server sent its allowed price types.
+
+#include "stdafx.h"
+
+#include "doctest.h"
+
+#include "GameLogic/Items/PersonalShopCurrency.h"
+
+#include
+#include
+
+using GameLogic::PersonalShop::DecodeExtendedPrice;
+using GameLogic::PersonalShop::EncodeExtendedPrice;
+using GameLogic::PersonalShop::GetAllowedPriceTypes;
+using GameLogic::PersonalShop::IsCommonPriceType;
+using GameLogic::PersonalShop::ItemPrice;
+using GameLogic::PersonalShop::PriceKind;
+using GameLogic::PersonalShop::SetAllowedPriceTypes;
+
+namespace
+{
+// Jewel of Bless: group 14, number 13; Jewel of Soul: group 14, number 14; Jewel of Chaos: group 12,
+// number 15; Loch's Feather: group 13, number 14; Gemstone: group 14, number 41.
+constexpr uint16_t JewelOfBless = 0xE00D;
+constexpr uint16_t JewelOfSoul = 0xE00E;
+constexpr uint16_t JewelOfChaos = 0xC00F;
+constexpr uint16_t LochsFeather = 0xD00E;
+constexpr uint16_t Gemstone = 0xE029;
+constexpr uint16_t WCoinC = 0xFFF0;
+constexpr uint16_t GoblinPoints = 0xFFF2;
+} // namespace
+
+TEST_CASE("a price without item type and amount is in zen")
+{
+ const auto price = DecodeExtendedPrice(1000, 0, 0);
+ CHECK(price.Amount == 1000);
+ CHECK(price.Kind == PriceKind::Zen);
+}
+
+TEST_CASE("a price in items has the item as type and the pieces as amount")
+{
+ const auto price = DecodeExtendedPrice(0, JewelOfBless, 12);
+ CHECK(price.Amount == 12);
+ CHECK(price.Kind == PriceKind::Item);
+ CHECK(price.PriceItemType == JewelOfBless);
+}
+
+TEST_CASE("a price in account coins has a reserved item type and the coins as money")
+{
+ const auto price = DecodeExtendedPrice(500, GoblinPoints, 0);
+ CHECK(price.Amount == 500);
+ CHECK(price.Kind == PriceKind::AccountCoin);
+ CHECK(price.PriceItemType == GoblinPoints);
+}
+
+TEST_CASE("a price is encoded like it's decoded")
+{
+ const std::array prices = { {
+ { 1000, PriceKind::Zen, 0 },
+ { 12, PriceKind::Item, JewelOfBless },
+ { 500, PriceKind::AccountCoin, GoblinPoints },
+ } };
+
+ for (const auto& price : prices)
+ {
+ const auto encoded = EncodeExtendedPrice(price);
+ const auto decoded = DecodeExtendedPrice(encoded.MoneyPrice, encoded.PriceItemType, encoded.RequiredItemAmount);
+ CHECK(decoded.Amount == price.Amount);
+ CHECK(decoded.Kind == price.Kind);
+ CHECK(decoded.PriceItemType == price.PriceItemType);
+ }
+}
+
+TEST_CASE("the price input offers only zen until the server allows other prices")
+{
+ SetAllowedPriceTypes({});
+ CHECK_FALSE(GameLogic::PersonalShop::AreOtherPricesThanZenAllowed());
+ const auto choices = GetAllowedPriceTypes();
+ REQUIRE(choices.size() == 1);
+ CHECK(choices[0].Kind == PriceKind::Zen);
+}
+
+TEST_CASE("the price input offers zen, the coins and the jewels before the other items")
+{
+ // As a server could send them: items of its configuration first, then the coins.
+ const std::array allowed = { Gemstone, JewelOfChaos, LochsFeather, JewelOfBless, GoblinPoints,
+ JewelOfSoul, WCoinC };
+ SetAllowedPriceTypes(allowed);
+ CHECK(GameLogic::PersonalShop::AreOtherPricesThanZenAllowed());
+
+ const auto choices = GetAllowedPriceTypes();
+ const std::array expected = { 0, WCoinC, GoblinPoints, JewelOfBless, JewelOfSoul, JewelOfChaos,
+ Gemstone, LochsFeather };
+ REQUIRE(choices.size() == expected.size());
+ for (size_t i = 0; i < expected.size(); ++i)
+ {
+ CHECK(choices[i].PriceItemType == expected[i]);
+ CHECK(choices[i].Amount == 0);
+ }
+
+ CHECK(choices[0].Kind == PriceKind::Zen);
+ CHECK(choices[1].Kind == PriceKind::AccountCoin);
+ CHECK(choices[3].Kind == PriceKind::Item);
+ CHECK(IsCommonPriceType(choices[5]));
+ CHECK_FALSE(IsCommonPriceType(choices[6]));
+ CHECK_FALSE(IsCommonPriceType(choices[7]));
+ SetAllowedPriceTypes({});
+}