From c549b517e5cb11ea6fb492d623cdfdf07351b668 Mon Sep 17 00:00:00 2001 From: Sven Nicolai Date: Sun, 11 Oct 2026 11:04:32 +0000 Subject: [PATCH 1/2] feat(packets): add extended player shop currency packets Adds the protocol for player shop prices in other currencies than zen (see #1186), without changing any of the original packet layouts: - C3 3F 09 PlayerShopSetItemPriceExtended: sets a price with a currency. - C1 3F 0A PlayerShopCurrenciesExtended: tells the client which currencies are enabled, as a bit mask. - ShopCurrencyType common enum (Zen, five jewels, three account coins). - PlayerShopBuyResultExtended gets LackOfPriceCurrency. - Documents how PlayerShopItemExtended carries jewel and coin prices. Bumps the MUnique.OpenMU.Network.Packets package to 0.9.11, so the open source client can consume the new definitions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7XufeAZ22z7ocXAXUqUsE --- ...6-PlayerShopBuyResultExtended_by-server.md | 3 +- ...-PlayerShopCurrenciesExtended_by-server.md | 19 ++++ ...05-PlayerShopItemListExtended_by-server.md | 6 +- ...layerShopSetItemPriceExtended_by-client.md | 37 +++++++ docs/Packets/ClientToServer.md | 1 + docs/Packets/ServerToClient.md | 1 + .../ClientToServer/ClientToServerPackets.cs | 104 ++++++++++++++++++ .../ClientToServer/ClientToServerPackets.xml | 30 +++++ .../ClientToServerPacketsRef.cs | 104 ++++++++++++++++++ .../ClientToServer/ConnectionExtensions.cs | 32 ++++++ src/Network/Packets/CommonEnums.cs | 51 +++++++++ src/Network/Packets/CommonEnums.xml | 51 +++++++++ .../MUnique.OpenMU.Network.Packets.csproj | 4 +- .../ServerToClient/ConnectionExtensions.cs | 28 +++++ .../ServerToClient/ServerToClientPackets.cs | 97 +++++++++++++++- .../ServerToClient/ServerToClientPackets.xml | 27 ++++- .../ServerToClientPacketsRef.cs | 92 +++++++++++++++- .../ClientToServerPacketTests.cs | 26 +++++ .../ServerToClientPacketTests.cs | 18 +++ 19 files changed, 718 insertions(+), 13 deletions(-) create mode 100644 docs/Packets/C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md create mode 100644 docs/Packets/C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md diff --git a/docs/Packets/C1-3F-06-PlayerShopBuyResultExtended_by-server.md b/docs/Packets/C1-3F-06-PlayerShopBuyResultExtended_by-server.md index 51b1e1ed9d..8543eda9d7 100644 --- a/docs/Packets/C1-3F-06-PlayerShopBuyResultExtended_by-server.md +++ b/docs/Packets/C1-3F-06-PlayerShopBuyResultExtended_by-server.md @@ -36,4 +36,5 @@ The kind of result. | 6 | NameMismatchOrPriceMissing | The requested player with the specified id has a different name or price is missing. | | 7 | LackOfMoney | The player has not enough money to buy the item from the seller. | | 8 | MoneyOverflowOrNotEnoughSpace | The selling player cannot sell the item, because the sale would overflow his money amount in the inventory. Another possibility is that the inventory of the buyer cannot take the item. | -| 9 | ItemBlock | The requested player has item block active. | \ No newline at end of file +| 9 | ItemBlock | The requested player has item block active. | +| 10 | LackOfPriceCurrency | The player has not enough of the currency (jewels or account coins) in which the price of the item is defined. | \ No newline at end of file diff --git a/docs/Packets/C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md b/docs/Packets/C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md new file mode 100644 index 0000000000..85167bbeb3 --- /dev/null +++ b/docs/Packets/C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md @@ -0,0 +1,19 @@ +# C1 3F 0A - PlayerShopCurrenciesExtended (by server) + +## Is sent when + +After entering the game world, and before the player shop of the own player or of another player is shown. + +## Causes the following actions on the client side + +The client offers only the enabled currencies when the player sets an item price in his player shop. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 8 | Packet header - length of the packet | +| 2 | 1 | Byte | 0x3F | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x0A | Packet header - sub packet type identifier | +| 4 | 4 | IntegerLittleEndian | | CurrencyMask; The bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. | \ No newline at end of file diff --git a/docs/Packets/C2-3F-05-PlayerShopItemListExtended_by-server.md b/docs/Packets/C2-3F-05-PlayerShopItemListExtended_by-server.md index 07a3e36801..0941492a83 100644 --- a/docs/Packets/C2-3F-05-PlayerShopItemListExtended_by-server.md +++ b/docs/Packets/C2-3F-05-PlayerShopItemListExtended_by-server.md @@ -30,9 +30,9 @@ Data of an item in a player shop, which allows for dynamic item sizes and trades | Index | Length | Data Type | Value | Description | |-------|--------|-----------|-------|-------------| -| 0 | 4 | IntegerLittleEndian | | MoneyPrice | -| 4 | 2 | ShortLittleEndian | | PriceItemType; Contains the item group in the highest 4 bits, and the item number in the remaining ones. | -| 6 | 2 | ShortLittleEndian | | RequiredItemAmount | +| 0 | 4 | IntegerLittleEndian | | MoneyPrice; The price in zen, or in account coins when PriceItemType is one of the account coin values (see there). It's 0 when the price is defined in items. | +| 4 | 2 | ShortLittleEndian | | PriceItemType; When RequiredItemAmount is greater than 0, it contains the item group in the highest 4 bits, and the item number in the remaining ones, of the item (e.g. a jewel) which is required to buy the item. When RequiredItemAmount is 0, the following reserved values define an account coin as currency, with the amount in MoneyPrice: 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P), 0xFFF2 = Goblin Points. Any other value means that MoneyPrice is the price in zen. | +| 6 | 2 | ShortLittleEndian | | RequiredItemAmount; The number of pieces of the item which is defined by PriceItemType, which is required to buy the item. 0, if the price is not defined in items. | | 8 | 1 | Byte | | ItemSlot | | 9 | | Binary | | ItemData | diff --git a/docs/Packets/C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md b/docs/Packets/C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md new file mode 100644 index 0000000000..bb9de38dfb --- /dev/null +++ b/docs/Packets/C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md @@ -0,0 +1,37 @@ +# C3 3F 09 - PlayerShopSetItemPriceExtended (by client) + +## Is sent when + +The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). + +## Causes the following actions on the server side + +The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC3 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 10 | Packet header - length of the packet | +| 2 | 1 | Byte | 0x3F | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x09 | Packet header - sub packet type identifier | +| 4 | 1 | Byte | | ItemSlot | +| 5 | 1 | ShopCurrencyType | | Currency | +| 6 | 4 | IntegerLittleEndian | | Price; The price amount in the specified currency. For jewels, it's the number of jewel pieces. | + +### ShopCurrencyType Enum + +Defines the currency in which the price of an item in a player shop is defined. The value is also the bit index in the currency mask of the PlayerShopCurrenciesExtended message. + +| Value | Name | Description | +|-------|------|-------------| +| 0 | Zen | Zen, the money in the inventory. | +| 1 | JewelOfBless | Jewel of Bless (item group 14, number 13). | +| 2 | JewelOfSoul | Jewel of Soul (item group 14, number 14). | +| 3 | JewelOfChaos | Jewel of Chaos (item group 12, number 15). | +| 4 | JewelOfCreation | Jewel of Creation (item group 14, number 22). | +| 5 | JewelOfLife | Jewel of Life (item group 14, number 16). | +| 6 | WCoinC | WCoin (C), an account coin. | +| 7 | WCoinP | WCoin (P), an account coin. | +| 8 | GoblinPoints | Goblin Points, an account coin. | \ No newline at end of file diff --git a/docs/Packets/ClientToServer.md b/docs/Packets/ClientToServer.md index 741eaf2412..ecb090c8c2 100644 --- a/docs/Packets/ClientToServer.md +++ b/docs/Packets/ClientToServer.md @@ -43,6 +43,7 @@ * [C3 3F 05 - PlayerShopItemListRequest (by client)](C3-3F-05-PlayerShopItemListRequest_by-client.md) * [C3 3F 06 - PlayerShopItemBuyRequest (by client)](C3-3F-06-PlayerShopItemBuyRequest_by-client.md) * [C3 3F 07 - PlayerShopCloseOther (by client)](C3-3F-07-PlayerShopCloseOther_by-client.md) + * [C3 3F 09 - PlayerShopSetItemPriceExtended (by client)](C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md) * [C1 40 - PartyInviteRequest (by client)](C1-40-PartyInviteRequest_by-client.md) * [C1 41 - PartyInviteResponse (by client)](C1-41-PartyInviteResponse_by-client.md) * [C1 42 - PartyListRequest (by client)](C1-42-PartyListRequest_by-client.md) diff --git a/docs/Packets/ServerToClient.md b/docs/Packets/ServerToClient.md index bb41254ab6..eb236dcb32 100644 --- a/docs/Packets/ServerToClient.md +++ b/docs/Packets/ServerToClient.md @@ -81,6 +81,7 @@ * [C1 3F 06 - PlayerShopBuyResult (by server)](C1-3F-06-PlayerShopBuyResult_by-server.md) * [C1 3F 06 - PlayerShopBuyResultExtended (by server)](C1-3F-06-PlayerShopBuyResultExtended_by-server.md) * [C1 3F 08 - PlayerShopItemSoldToPlayer (by server)](C1-3F-08-PlayerShopItemSoldToPlayer_by-server.md) + * [C1 3F 0A - PlayerShopCurrenciesExtended (by server)](C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md) * [C1 3F 12 - ClosePlayerShopDialog (by server)](C1-3F-12-ClosePlayerShopDialog_by-server.md) * [C1 3F 2 - PlayerShopOpenSuccessful (by server)](C1-3F-2-PlayerShopOpenSuccessful_by-server.md) * [C1 3F 3 - PlayerShopClosed (by server)](C1-3F-3-PlayerShopClosed_by-server.md) diff --git a/src/Network/Packets/ClientToServer/ClientToServerPackets.cs b/src/Network/Packets/ClientToServer/ClientToServerPackets.cs index e1e09e34a3..21d7b1e3e3 100644 --- a/src/Network/Packets/ClientToServer/ClientToServerPackets.cs +++ b/src/Network/Packets/ClientToServer/ClientToServerPackets.cs @@ -1544,6 +1544,110 @@ public string PlayerName } +/// +/// Is sent by the client when: The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). +/// Causes reaction on server side: The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. +/// +public readonly struct PlayerShopSetItemPriceExtended +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public PlayerShopSetItemPriceExtended(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private PlayerShopSetItemPriceExtended(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC3; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0x3F; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x09; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 10; + + /// + /// Gets the header of this packet. + /// + public C3HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the item slot. + /// + public byte ItemSlot + { + get => this._data.Span[4]; + set => this._data.Span[4] = value; + } + + /// + /// Gets or sets the currency. + /// + public ShopCurrencyType Currency + { + get => (ShopCurrencyType)this._data.Span[5]; + set => this._data.Span[5] = (byte)value; + } + + /// + /// Gets or sets the price amount in the specified currency. For jewels, it's the number of jewel pieces. + /// + public uint Price + { + get => ReadUInt32LittleEndian(this._data.Span[6..]); + set => WriteUInt32LittleEndian(this._data.Span[6..], value); + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator PlayerShopSetItemPriceExtended(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(PlayerShopSetItemPriceExtended packet) => packet._data; +} + + /// /// Is sent by the client when: A player requests to pick up an item which is laying on the ground in the near of the players character. /// Causes reaction on server side: If the player is allowed to pick the item up, and is the first player which tried that, it tries to add the item to the inventory. The server sends a response about the result of the request. diff --git a/src/Network/Packets/ClientToServer/ClientToServerPackets.xml b/src/Network/Packets/ClientToServer/ClientToServerPackets.xml index 33e6a50ee7..5387b8d452 100644 --- a/src/Network/Packets/ClientToServer/ClientToServerPackets.xml +++ b/src/Network/Packets/ClientToServer/ClientToServerPackets.xml @@ -397,6 +397,36 @@ + + C3HeaderWithSubCode + 3F + 09 + PlayerShopSetItemPriceExtended + Player Shop - Set Item Price (with currency) + 10 + ClientToServer + The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). + The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. + + + 4 + Byte + ItemSlot + + + 5 + Enum + ShopCurrencyType + Currency + + + 6 + IntegerLittleEndian + Price + The price amount in the specified currency. For jewels, it's the number of jewel pieces. + + + C3Header 22 diff --git a/src/Network/Packets/ClientToServer/ClientToServerPacketsRef.cs b/src/Network/Packets/ClientToServer/ClientToServerPacketsRef.cs index 8bb115ffab..89032c724f 100644 --- a/src/Network/Packets/ClientToServer/ClientToServerPacketsRef.cs +++ b/src/Network/Packets/ClientToServer/ClientToServerPacketsRef.cs @@ -1544,6 +1544,110 @@ public string PlayerName } +/// +/// Is sent by the client when: The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). +/// Causes reaction on server side: The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. +/// +public readonly ref struct PlayerShopSetItemPriceExtendedRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public PlayerShopSetItemPriceExtendedRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private PlayerShopSetItemPriceExtendedRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC3; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0x3F; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x09; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 10; + + /// + /// Gets the header of this packet. + /// + public C3HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the item slot. + /// + public byte ItemSlot + { + get => this._data[4]; + set => this._data[4] = value; + } + + /// + /// Gets or sets the currency. + /// + public ShopCurrencyType Currency + { + get => (ShopCurrencyType)this._data[5]; + set => this._data[5] = (byte)value; + } + + /// + /// Gets or sets the price amount in the specified currency. For jewels, it's the number of jewel pieces. + /// + public uint Price + { + get => ReadUInt32LittleEndian(this._data[6..]); + set => WriteUInt32LittleEndian(this._data[6..], value); + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator PlayerShopSetItemPriceExtendedRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(PlayerShopSetItemPriceExtendedRef packet) => packet._data; +} + + /// /// Is sent by the client when: A player requests to pick up an item which is laying on the ground in the near of the players character. /// Causes reaction on server side: If the player is allowed to pick the item up, and is the first player which tried that, it tries to add the item to the inventory. The server sends a response about the result of the request. diff --git a/src/Network/Packets/ClientToServer/ConnectionExtensions.cs b/src/Network/Packets/ClientToServer/ConnectionExtensions.cs index ce289c8f9f..4e2a6b6362 100644 --- a/src/Network/Packets/ClientToServer/ConnectionExtensions.cs +++ b/src/Network/Packets/ClientToServer/ConnectionExtensions.cs @@ -512,6 +512,38 @@ int WritePacket() await connection.SendAsync(WritePacket).ConfigureAwait(false); } + /// + /// Sends a to this connection. + /// + /// The connection. + /// The item slot. + /// The currency. + /// The price amount in the specified currency. For jewels, it's the number of jewel pieces. + /// + /// Is sent by the client when: The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). + /// Causes reaction on server side: The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. + /// + public static async ValueTask SendPlayerShopSetItemPriceExtendedAsync(this IConnection? connection, byte @itemSlot, ShopCurrencyType @currency, uint @price) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = PlayerShopSetItemPriceExtendedRef.Length; + var packet = new PlayerShopSetItemPriceExtendedRef(connection.Output.GetSpan(length)[..length]); + packet.ItemSlot = @itemSlot; + packet.Currency = @currency; + packet.Price = @price; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + /// /// Sends a to this connection. /// diff --git a/src/Network/Packets/CommonEnums.cs b/src/Network/Packets/CommonEnums.cs index 84d0cc4f01..528d089f4f 100644 --- a/src/Network/Packets/CommonEnums.cs +++ b/src/Network/Packets/CommonEnums.cs @@ -350,3 +350,54 @@ public enum MiniGameType Doppelganger = 6, } + /// + /// Defines the currency in which the price of an item in a player shop is defined. The value is also the bit index in the currency mask of the PlayerShopCurrenciesExtended message. + /// + public enum ShopCurrencyType + { + /// + /// Zen, the money in the inventory. + /// + Zen = 0, + + /// + /// Jewel of Bless (item group 14, number 13). + /// + JewelOfBless = 1, + + /// + /// Jewel of Soul (item group 14, number 14). + /// + JewelOfSoul = 2, + + /// + /// Jewel of Chaos (item group 12, number 15). + /// + JewelOfChaos = 3, + + /// + /// Jewel of Creation (item group 14, number 22). + /// + JewelOfCreation = 4, + + /// + /// Jewel of Life (item group 14, number 16). + /// + JewelOfLife = 5, + + /// + /// WCoin (C), an account coin. + /// + WCoinC = 6, + + /// + /// WCoin (P), an account coin. + /// + WCoinP = 7, + + /// + /// Goblin Points, an account coin. + /// + GoblinPoints = 8, + } + diff --git a/src/Network/Packets/CommonEnums.xml b/src/Network/Packets/CommonEnums.xml index 131bf8bbba..859714b268 100644 --- a/src/Network/Packets/CommonEnums.xml +++ b/src/Network/Packets/CommonEnums.xml @@ -341,5 +341,56 @@ + + ShopCurrencyType + Defines the currency in which the price of an item in a player shop is defined. The value is also the bit index in the currency mask of the PlayerShopCurrenciesExtended message. + + + Zen + Zen, the money in the inventory. + 0 + + + JewelOfBless + Jewel of Bless (item group 14, number 13). + 1 + + + JewelOfSoul + Jewel of Soul (item group 14, number 14). + 2 + + + JewelOfChaos + Jewel of Chaos (item group 12, number 15). + 3 + + + JewelOfCreation + Jewel of Creation (item group 14, number 22). + 4 + + + JewelOfLife + Jewel of Life (item group 14, number 16). + 5 + + + WCoinC + WCoin (C), an account coin. + 6 + + + WCoinP + WCoin (P), an account coin. + 7 + + + GoblinPoints + Goblin Points, an account coin. + 8 + + + \ No newline at end of file diff --git a/src/Network/Packets/MUnique.OpenMU.Network.Packets.csproj b/src/Network/Packets/MUnique.OpenMU.Network.Packets.csproj index ec0f00af74..add339226a 100644 --- a/src/Network/Packets/MUnique.OpenMU.Network.Packets.csproj +++ b/src/Network/Packets/MUnique.OpenMU.Network.Packets.csproj @@ -16,8 +16,8 @@ MUnique OpenMU MUOnline Network Packets MUnique.OpenMU.Network.Packets This package contains message structs for the MMORPG "MU Online", which are primarily focused on the english version of Season 6 Episode 3. - 0.9.10 - 0.9.10 + 0.9.11 + 0.9.11 diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs index 89fb20a477..07f1f421e9 100644 --- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs +++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs @@ -2494,6 +2494,34 @@ int WritePacket() await connection.SendAsync(WritePacket).ConfigureAwait(false); } + /// + /// Sends a to this connection. + /// + /// The connection. + /// The bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. + /// + /// Is sent by the server when: After entering the game world, and before the player shop of the own player or of another player is shown. + /// Causes reaction on client side: The client offers only the enabled currencies when the player sets an item price in his player shop. + /// + public static async ValueTask SendPlayerShopCurrenciesExtendedAsync(this IConnection? connection, uint @currencyMask) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = PlayerShopCurrenciesExtendedRef.Length; + var packet = new PlayerShopCurrenciesExtendedRef(connection.Output.GetSpan(length)[..length]); + packet.CurrencyMask = @currencyMask; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + /// /// Sends a to this connection. /// diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs index f9286bd864..684d3c87f7 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs @@ -123,7 +123,7 @@ public PlayerShopItemExtended(Memory data) } /// - /// Gets or sets the money price. + /// Gets or sets the price in zen, or in account coins when PriceItemType is one of the account coin values (see there). It's 0 when the price is defined in items. /// public uint MoneyPrice { @@ -132,7 +132,7 @@ public uint MoneyPrice } /// - /// Gets or sets contains the item group in the highest 4 bits, and the item number in the remaining ones. + /// Gets or sets when RequiredItemAmount is greater than 0, it contains the item group in the highest 4 bits, and the item number in the remaining ones, of the item (e.g. a jewel) which is required to buy the item. When RequiredItemAmount is 0, the following reserved values define an account coin as currency, with the amount in MoneyPrice: 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P), 0xFFF2 = Goblin Points. Any other value means that MoneyPrice is the price in zen. /// public ushort PriceItemType { @@ -141,7 +141,7 @@ public ushort PriceItemType } /// - /// Gets or sets the required item amount. + /// Gets or sets the number of pieces of the item which is defined by PriceItemType, which is required to buy the item. 0, if the price is not defined in items. /// public ushort RequiredItemAmount { @@ -12001,6 +12001,11 @@ public enum ResultKind /// The requested player has item block active. /// ItemBlock = 9, + + /// + /// The player has not enough of the currency (jewels or account coins) in which the price of the item is defined. + /// + LackOfPriceCurrency = 10, } private readonly Memory _data; @@ -12267,6 +12272,92 @@ public byte ItemCount } +/// +/// Is sent by the server when: After entering the game world, and before the player shop of the own player or of another player is shown. +/// Causes reaction on client side: The client offers only the enabled currencies when the player sets an item price in his player shop. +/// +public readonly struct PlayerShopCurrenciesExtended +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public PlayerShopCurrenciesExtended(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private PlayerShopCurrenciesExtended(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0x3F; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x0A; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 8; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. + /// + public uint CurrencyMask + { + get => ReadUInt32LittleEndian(this._data.Span[4..]); + set => WriteUInt32LittleEndian(this._data.Span[4..], value); + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator PlayerShopCurrenciesExtended(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(PlayerShopCurrenciesExtended packet) => packet._data; +} + + /// /// Is sent by the server when: After the player gets into scope of a player with an opened shop. /// Causes reaction on client side: The player shop title is shown at the specified players. diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml index 8378cc7cba..3fabb011a8 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml @@ -49,17 +49,19 @@ 0 IntegerLittleEndian MoneyPrice + The price in zen, or in account coins when PriceItemType is one of the account coin values (see there). It's 0 when the price is defined in items. 4 ShortLittleEndian PriceItemType - Contains the item group in the highest 4 bits, and the item number in the remaining ones. + When RequiredItemAmount is greater than 0, it contains the item group in the highest 4 bits, and the item number in the remaining ones, of the item (e.g. a jewel) which is required to buy the item. When RequiredItemAmount is 0, the following reserved values define an account coin as currency, with the amount in MoneyPrice: 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P), 0xFFF2 = Goblin Points. Any other value means that MoneyPrice is the price in zen. 6 ShortLittleEndian RequiredItemAmount + The number of pieces of the item which is defined by PriceItemType, which is required to buy the item. 0, if the price is not defined in items. 8 @@ -4481,6 +4483,11 @@ The requested player has item block active. 9 + + LackOfPriceCurrency + The player has not enough of the currency (jewels or account coins) in which the price of the item is defined. + 10 + @@ -4555,6 +4562,24 @@ + + C1HeaderWithSubCode + 3F + 0A + PlayerShopCurrenciesExtended + 8 + ServerToClient + After entering the game world, and before the player shop of the own player or of another player is shown. + The client offers only the enabled currencies when the player sets an item price in his player shop. + + + 4 + IntegerLittleEndian + CurrencyMask + The bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. + + + C2HeaderWithSubCode 3F diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs index c1af132e94..c92246bc90 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs @@ -123,7 +123,7 @@ public PlayerShopItemExtendedRef(Span data) } /// - /// Gets or sets the money price. + /// Gets or sets the price in zen, or in account coins when PriceItemType is one of the account coin values (see there). It's 0 when the price is defined in items. /// public uint MoneyPrice { @@ -132,7 +132,7 @@ public uint MoneyPrice } /// - /// Gets or sets contains the item group in the highest 4 bits, and the item number in the remaining ones. + /// Gets or sets when RequiredItemAmount is greater than 0, it contains the item group in the highest 4 bits, and the item number in the remaining ones, of the item (e.g. a jewel) which is required to buy the item. When RequiredItemAmount is 0, the following reserved values define an account coin as currency, with the amount in MoneyPrice: 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P), 0xFFF2 = Goblin Points. Any other value means that MoneyPrice is the price in zen. /// public ushort PriceItemType { @@ -141,7 +141,7 @@ public ushort PriceItemType } /// - /// Gets or sets the required item amount. + /// Gets or sets the number of pieces of the item which is defined by PriceItemType, which is required to buy the item. 0, if the price is not defined in items. /// public ushort RequiredItemAmount { @@ -11488,6 +11488,92 @@ public byte ItemCount } +/// +/// Is sent by the server when: After entering the game world, and before the player shop of the own player or of another player is shown. +/// Causes reaction on client side: The client offers only the enabled currencies when the player sets an item price in his player shop. +/// +public readonly ref struct PlayerShopCurrenciesExtendedRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public PlayerShopCurrenciesExtendedRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private PlayerShopCurrenciesExtendedRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0x3F; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x0A; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 8; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. + /// + public uint CurrencyMask + { + get => ReadUInt32LittleEndian(this._data[4..]); + set => WriteUInt32LittleEndian(this._data[4..], value); + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator PlayerShopCurrenciesExtendedRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(PlayerShopCurrenciesExtendedRef packet) => packet._data; +} + + /// /// Is sent by the server when: After the player gets into scope of a player with an opened shop. /// Causes reaction on client side: The player shop title is shown at the specified players. diff --git a/tests/MUnique.OpenMU.Network.Packets.Tests/ClientToServerPacketTests.cs b/tests/MUnique.OpenMU.Network.Packets.Tests/ClientToServerPacketTests.cs index 96b5119c73..cc2227915e 100644 --- a/tests/MUnique.OpenMU.Network.Packets.Tests/ClientToServerPacketTests.cs +++ b/tests/MUnique.OpenMU.Network.Packets.Tests/ClientToServerPacketTests.cs @@ -376,6 +376,32 @@ public void PlayerShopCloseOther_PacketSizeValidation() "Field 'PlayerName' exceeds packet boundary"); } + /// + /// Tests the packet size calculation for PlayerShopSetItemPriceExtended. + /// + [Test] + public void PlayerShopSetItemPriceExtended_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 10; + var actualLength = PlayerShopSetItemPriceExtendedRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'ItemSlot' boundary + Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'ItemSlot' exceeds packet boundary"); + + // Validate field 'Currency' boundary + Assert.That(5 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Currency' exceeds packet boundary"); + + // Validate field 'Price' boundary + Assert.That(6 + 4, Is.LessThanOrEqualTo(expectedLength), + "Field 'Price' exceeds packet boundary"); + } + /// /// Tests the packet size calculation for PickupItemRequest. /// diff --git a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs index 045072327e..69567bb761 100644 --- a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs +++ b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs @@ -2424,6 +2424,24 @@ public void PlayerShopItemListExtended_PacketSizeValidation() "Field 'ItemCount' has invalid negative index"); } + /// + /// Tests the packet size calculation for PlayerShopCurrenciesExtended. + /// + [Test] + public void PlayerShopCurrenciesExtended_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 8; + var actualLength = PlayerShopCurrenciesExtendedRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'CurrencyMask' boundary + Assert.That(4 + 4, Is.LessThanOrEqualTo(expectedLength), + "Field 'CurrencyMask' exceeds packet boundary"); + } + /// /// Tests the packet size calculation for PlayerShops. /// From 9fffd88a9c06b73472ae0abf6112e2319a0f65f3 Mon Sep 17 00:00:00 2001 From: Sven Nicolai Date: Sun, 11 Oct 2026 11:54:16 +0000 Subject: [PATCH 2/2] refactor(packets): encode set prices like the shop item list The extended set-price packet carried a fixed currency enum, while the item list describes a price by MoneyPrice, PriceItemType and RequiredItemAmount. The enum limited prices to a hard-coded set of jewels and coins, and any other price item would have needed a protocol change. - C3 3F 09 PlayerShopSetItemPriceExtended now has the same three price fields as PlayerShopItemExtended (including the reserved account coin values), followed by the item slot. - C1 3F 0A is now PlayerShopPriceTypesExtended: the list of allowed PriceItemType values, instead of a bit mask over the enum. - The ShopCurrencyType enum is removed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7XufeAZ22z7ocXAXUqUsE --- ...-PlayerShopCurrenciesExtended_by-server.md | 19 ----- ...-PlayerShopPriceTypesExtended_by-server.md | 30 +++++++ ...layerShopSetItemPriceExtended_by-client.md | 29 ++----- docs/Packets/ServerToClient.md | 2 +- .../ClientToServer/ClientToServerPackets.cs | 39 +++++---- .../ClientToServer/ClientToServerPackets.xml | 34 ++++---- .../ClientToServerPacketsRef.cs | 39 +++++---- .../ClientToServer/ConnectionExtensions.cs | 16 ++-- src/Network/Packets/CommonEnums.cs | 51 ------------ src/Network/Packets/CommonEnums.xml | 51 ------------ .../ServerToClient/ConnectionExtensions.cs | 28 ------- .../ServerToClient/ServerToClientPackets.cs | 81 ++++++++++++++----- .../ServerToClient/ServerToClientPackets.xml | 34 ++++++-- .../ServerToClientPacketsRef.cs | 81 ++++++++++++++----- .../ClientToServerPacketTests.cs | 24 +++--- .../ServerToClientPacketTests.cs | 18 ++--- 16 files changed, 283 insertions(+), 293 deletions(-) delete mode 100644 docs/Packets/C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md create mode 100644 docs/Packets/C1-3F-0A-PlayerShopPriceTypesExtended_by-server.md diff --git a/docs/Packets/C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md b/docs/Packets/C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md deleted file mode 100644 index 85167bbeb3..0000000000 --- a/docs/Packets/C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md +++ /dev/null @@ -1,19 +0,0 @@ -# C1 3F 0A - PlayerShopCurrenciesExtended (by server) - -## Is sent when - -After entering the game world, and before the player shop of the own player or of another player is shown. - -## Causes the following actions on the client side - -The client offers only the enabled currencies when the player sets an item price in his player shop. - -## Structure - -| Index | Length | Data Type | Value | Description | -|-------|--------|-----------|-------|-------------| -| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | -| 1 | 1 | Byte | 8 | Packet header - length of the packet | -| 2 | 1 | Byte | 0x3F | Packet header - packet type identifier | -| 3 | 1 | Byte | 0x0A | Packet header - sub packet type identifier | -| 4 | 4 | IntegerLittleEndian | | CurrencyMask; The bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. | \ No newline at end of file diff --git a/docs/Packets/C1-3F-0A-PlayerShopPriceTypesExtended_by-server.md b/docs/Packets/C1-3F-0A-PlayerShopPriceTypesExtended_by-server.md new file mode 100644 index 0000000000..87d23475c7 --- /dev/null +++ b/docs/Packets/C1-3F-0A-PlayerShopPriceTypesExtended_by-server.md @@ -0,0 +1,30 @@ +# C1 3F 0A - PlayerShopPriceTypesExtended (by server) + +## Is sent when + +After entering the game world, and before the player shop of another player is shown, when the server allows prices in player shops which are not paid in zen. + +## Causes the following actions on the client side + +The client offers the allowed price types, in addition to zen, when the player sets an item price in his player shop. Without this message, only zen prices are possible. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | | Packet header - length of the packet | +| 2 | 1 | Byte | 0x3F | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x0A | Packet header - sub packet type identifier | +| 4 | 1 | Byte | | Count | +| 5 | PlayerShopPriceType.Length * Count | Array of PlayerShopPriceType | | PriceTypes | + +### PlayerShopPriceType Structure + +A price type which is allowed for prices in player shops, in addition to zen. + +Length: 2 Bytes + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 2 | ShortLittleEndian | | PriceItemType; The value of PriceItemType of a price of this type, like in PlayerShopItemExtended: the item (group in the highest 4 bits, number in the remaining ones) with which the price is paid, or 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P), 0xFFF2 = Goblin Points. | \ No newline at end of file diff --git a/docs/Packets/C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md b/docs/Packets/C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md index bb9de38dfb..59e95bb275 100644 --- a/docs/Packets/C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md +++ b/docs/Packets/C3-3F-09-PlayerShopSetItemPriceExtended_by-client.md @@ -2,36 +2,21 @@ ## Is sent when -The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). +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 the following actions on the server side -The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. +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. ## Structure | Index | Length | Data Type | Value | Description | |-------|--------|-----------|-------|-------------| | 0 | 1 | Byte | 0xC3 | [Packet type](PacketTypes.md) | -| 1 | 1 | Byte | 10 | Packet header - length of the packet | +| 1 | 1 | Byte | 13 | Packet header - length of the packet | | 2 | 1 | Byte | 0x3F | Packet header - packet type identifier | | 3 | 1 | Byte | 0x09 | Packet header - sub packet type identifier | -| 4 | 1 | Byte | | ItemSlot | -| 5 | 1 | ShopCurrencyType | | Currency | -| 6 | 4 | IntegerLittleEndian | | Price; The price amount in the specified currency. For jewels, it's the number of jewel pieces. | - -### ShopCurrencyType Enum - -Defines the currency in which the price of an item in a player shop is defined. The value is also the bit index in the currency mask of the PlayerShopCurrenciesExtended message. - -| Value | Name | Description | -|-------|------|-------------| -| 0 | Zen | Zen, the money in the inventory. | -| 1 | JewelOfBless | Jewel of Bless (item group 14, number 13). | -| 2 | JewelOfSoul | Jewel of Soul (item group 14, number 14). | -| 3 | JewelOfChaos | Jewel of Chaos (item group 12, number 15). | -| 4 | JewelOfCreation | Jewel of Creation (item group 14, number 22). | -| 5 | JewelOfLife | Jewel of Life (item group 14, number 16). | -| 6 | WCoinC | WCoin (C), an account coin. | -| 7 | WCoinP | WCoin (P), an account coin. | -| 8 | GoblinPoints | Goblin Points, an account coin. | \ No newline at end of file +| 4 | 4 | IntegerLittleEndian | | MoneyPrice; The price in zen, or in account coins when PriceItemType is one of the account coin values. Encoded like the price of PlayerShopItemExtended. | +| 8 | 2 | ShortLittleEndian | | PriceItemType; 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. | +| 10 | 2 | ShortLittleEndian | | RequiredItemAmount; 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. | +| 12 | 1 | Byte | | ItemSlot | \ No newline at end of file diff --git a/docs/Packets/ServerToClient.md b/docs/Packets/ServerToClient.md index eb236dcb32..e894f8d921 100644 --- a/docs/Packets/ServerToClient.md +++ b/docs/Packets/ServerToClient.md @@ -81,7 +81,7 @@ * [C1 3F 06 - PlayerShopBuyResult (by server)](C1-3F-06-PlayerShopBuyResult_by-server.md) * [C1 3F 06 - PlayerShopBuyResultExtended (by server)](C1-3F-06-PlayerShopBuyResultExtended_by-server.md) * [C1 3F 08 - PlayerShopItemSoldToPlayer (by server)](C1-3F-08-PlayerShopItemSoldToPlayer_by-server.md) - * [C1 3F 0A - PlayerShopCurrenciesExtended (by server)](C1-3F-0A-PlayerShopCurrenciesExtended_by-server.md) + * [C1 3F 0A - PlayerShopPriceTypesExtended (by server)](C1-3F-0A-PlayerShopPriceTypesExtended_by-server.md) * [C1 3F 12 - ClosePlayerShopDialog (by server)](C1-3F-12-ClosePlayerShopDialog_by-server.md) * [C1 3F 2 - PlayerShopOpenSuccessful (by server)](C1-3F-2-PlayerShopOpenSuccessful_by-server.md) * [C1 3F 3 - PlayerShopClosed (by server)](C1-3F-3-PlayerShopClosed_by-server.md) diff --git a/src/Network/Packets/ClientToServer/ClientToServerPackets.cs b/src/Network/Packets/ClientToServer/ClientToServerPackets.cs index 21d7b1e3e3..48208f1143 100644 --- a/src/Network/Packets/ClientToServer/ClientToServerPackets.cs +++ b/src/Network/Packets/ClientToServer/ClientToServerPackets.cs @@ -1545,8 +1545,8 @@ public string PlayerName /// -/// Is sent by the client when: The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). -/// Causes reaction on server side: The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. +/// 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. /// public readonly struct PlayerShopSetItemPriceExtended { @@ -1598,7 +1598,7 @@ private PlayerShopSetItemPriceExtended(Memory data, bool initialize) /// /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. /// - public static int Length => 10; + public static int Length => 13; /// /// Gets the header of this packet. @@ -1606,30 +1606,39 @@ private PlayerShopSetItemPriceExtended(Memory data, bool initialize) public C3HeaderWithSubCode Header => new (this._data); /// - /// Gets or sets the item slot. + /// Gets or sets the price in zen, or in account coins when PriceItemType is one of the account coin values. Encoded like the price of PlayerShopItemExtended. /// - public byte ItemSlot + public uint MoneyPrice { - get => this._data.Span[4]; - set => this._data.Span[4] = value; + get => ReadUInt32LittleEndian(this._data.Span[4..]); + set => WriteUInt32LittleEndian(this._data.Span[4..], value); } /// - /// Gets or sets the currency. + /// Gets or sets 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. /// - public ShopCurrencyType Currency + public ushort PriceItemType { - get => (ShopCurrencyType)this._data.Span[5]; - set => this._data.Span[5] = (byte)value; + get => ReadUInt16LittleEndian(this._data.Span[8..]); + set => WriteUInt16LittleEndian(this._data.Span[8..], value); } /// - /// Gets or sets the price amount in the specified currency. For jewels, it's the number of jewel pieces. + /// Gets or sets 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. /// - public uint Price + public ushort RequiredItemAmount + { + get => ReadUInt16LittleEndian(this._data.Span[10..]); + set => WriteUInt16LittleEndian(this._data.Span[10..], value); + } + + /// + /// Gets or sets the item slot. + /// + public byte ItemSlot { - get => ReadUInt32LittleEndian(this._data.Span[6..]); - set => WriteUInt32LittleEndian(this._data.Span[6..], value); + get => this._data.Span[12]; + set => this._data.Span[12] = value; } /// diff --git a/src/Network/Packets/ClientToServer/ClientToServerPackets.xml b/src/Network/Packets/ClientToServer/ClientToServerPackets.xml index 5387b8d452..965d93352a 100644 --- a/src/Network/Packets/ClientToServer/ClientToServerPackets.xml +++ b/src/Network/Packets/ClientToServer/ClientToServerPackets.xml @@ -402,28 +402,34 @@ 3F 09 PlayerShopSetItemPriceExtended - Player Shop - Set Item Price (with currency) - 10 + Player Shop - Set Item Price (extended) + 13 ClientToServer - The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). - The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. + 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). + 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. 4 - Byte - ItemSlot + IntegerLittleEndian + MoneyPrice + The price in zen, or in account coins when PriceItemType is one of the account coin values. Encoded like the price of PlayerShopItemExtended. - 5 - Enum - ShopCurrencyType - Currency + 8 + ShortLittleEndian + PriceItemType + 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. - 6 - IntegerLittleEndian - Price - The price amount in the specified currency. For jewels, it's the number of jewel pieces. + 10 + ShortLittleEndian + RequiredItemAmount + 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. + + + 12 + Byte + ItemSlot diff --git a/src/Network/Packets/ClientToServer/ClientToServerPacketsRef.cs b/src/Network/Packets/ClientToServer/ClientToServerPacketsRef.cs index 89032c724f..62a8ff59db 100644 --- a/src/Network/Packets/ClientToServer/ClientToServerPacketsRef.cs +++ b/src/Network/Packets/ClientToServer/ClientToServerPacketsRef.cs @@ -1545,8 +1545,8 @@ public string PlayerName /// -/// Is sent by the client when: The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). -/// Causes reaction on server side: The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. +/// 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. /// public readonly ref struct PlayerShopSetItemPriceExtendedRef { @@ -1598,7 +1598,7 @@ private PlayerShopSetItemPriceExtendedRef(Span data, bool initialize) /// /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. /// - public static int Length => 10; + public static int Length => 13; /// /// Gets the header of this packet. @@ -1606,30 +1606,39 @@ private PlayerShopSetItemPriceExtendedRef(Span data, bool initialize) public C3HeaderWithSubCodeRef Header => new (this._data); /// - /// Gets or sets the item slot. + /// Gets or sets the price in zen, or in account coins when PriceItemType is one of the account coin values. Encoded like the price of PlayerShopItemExtended. /// - public byte ItemSlot + public uint MoneyPrice { - get => this._data[4]; - set => this._data[4] = value; + get => ReadUInt32LittleEndian(this._data[4..]); + set => WriteUInt32LittleEndian(this._data[4..], value); } /// - /// Gets or sets the currency. + /// Gets or sets 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. /// - public ShopCurrencyType Currency + public ushort PriceItemType { - get => (ShopCurrencyType)this._data[5]; - set => this._data[5] = (byte)value; + get => ReadUInt16LittleEndian(this._data[8..]); + set => WriteUInt16LittleEndian(this._data[8..], value); } /// - /// Gets or sets the price amount in the specified currency. For jewels, it's the number of jewel pieces. + /// Gets or sets 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. /// - public uint Price + public ushort RequiredItemAmount + { + get => ReadUInt16LittleEndian(this._data[10..]); + set => WriteUInt16LittleEndian(this._data[10..], value); + } + + /// + /// Gets or sets the item slot. + /// + public byte ItemSlot { - get => ReadUInt32LittleEndian(this._data[6..]); - set => WriteUInt32LittleEndian(this._data[6..], value); + get => this._data[12]; + set => this._data[12] = value; } /// diff --git a/src/Network/Packets/ClientToServer/ConnectionExtensions.cs b/src/Network/Packets/ClientToServer/ConnectionExtensions.cs index 4e2a6b6362..d7e7126b63 100644 --- a/src/Network/Packets/ClientToServer/ConnectionExtensions.cs +++ b/src/Network/Packets/ClientToServer/ConnectionExtensions.cs @@ -516,14 +516,15 @@ int WritePacket() /// Sends a to this connection. /// /// 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. - /// The currency. - /// The price amount in the specified currency. For jewels, it's the number of jewel pieces. /// - /// Is sent by the client when: The player wants to set a price of an item which is inside his personal item shop, in one of the currencies which are enabled on the server (see PlayerShopCurrenciesExtended). - /// Causes reaction on server side: The price is set for the specified item, if the currency is enabled and the price is within the configured limits. Works only if the shop is currently closed. The server responds with a PlayerShopSetItemPriceResponse. + /// 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. /// - public static async ValueTask SendPlayerShopSetItemPriceExtendedAsync(this IConnection? connection, byte @itemSlot, ShopCurrencyType @currency, uint @price) + public static async ValueTask SendPlayerShopSetItemPriceExtendedAsync(this IConnection? connection, uint @moneyPrice, ushort @priceItemType, ushort @requiredItemAmount, byte @itemSlot) { if (connection is null) { @@ -534,9 +535,10 @@ int WritePacket() { var length = PlayerShopSetItemPriceExtendedRef.Length; var packet = new PlayerShopSetItemPriceExtendedRef(connection.Output.GetSpan(length)[..length]); + packet.MoneyPrice = @moneyPrice; + packet.PriceItemType = @priceItemType; + packet.RequiredItemAmount = @requiredItemAmount; packet.ItemSlot = @itemSlot; - packet.Currency = @currency; - packet.Price = @price; return packet.Header.Length; } diff --git a/src/Network/Packets/CommonEnums.cs b/src/Network/Packets/CommonEnums.cs index 528d089f4f..84d0cc4f01 100644 --- a/src/Network/Packets/CommonEnums.cs +++ b/src/Network/Packets/CommonEnums.cs @@ -350,54 +350,3 @@ public enum MiniGameType Doppelganger = 6, } - /// - /// Defines the currency in which the price of an item in a player shop is defined. The value is also the bit index in the currency mask of the PlayerShopCurrenciesExtended message. - /// - public enum ShopCurrencyType - { - /// - /// Zen, the money in the inventory. - /// - Zen = 0, - - /// - /// Jewel of Bless (item group 14, number 13). - /// - JewelOfBless = 1, - - /// - /// Jewel of Soul (item group 14, number 14). - /// - JewelOfSoul = 2, - - /// - /// Jewel of Chaos (item group 12, number 15). - /// - JewelOfChaos = 3, - - /// - /// Jewel of Creation (item group 14, number 22). - /// - JewelOfCreation = 4, - - /// - /// Jewel of Life (item group 14, number 16). - /// - JewelOfLife = 5, - - /// - /// WCoin (C), an account coin. - /// - WCoinC = 6, - - /// - /// WCoin (P), an account coin. - /// - WCoinP = 7, - - /// - /// Goblin Points, an account coin. - /// - GoblinPoints = 8, - } - diff --git a/src/Network/Packets/CommonEnums.xml b/src/Network/Packets/CommonEnums.xml index 859714b268..131bf8bbba 100644 --- a/src/Network/Packets/CommonEnums.xml +++ b/src/Network/Packets/CommonEnums.xml @@ -341,56 +341,5 @@ - - ShopCurrencyType - Defines the currency in which the price of an item in a player shop is defined. The value is also the bit index in the currency mask of the PlayerShopCurrenciesExtended message. - - - Zen - Zen, the money in the inventory. - 0 - - - JewelOfBless - Jewel of Bless (item group 14, number 13). - 1 - - - JewelOfSoul - Jewel of Soul (item group 14, number 14). - 2 - - - JewelOfChaos - Jewel of Chaos (item group 12, number 15). - 3 - - - JewelOfCreation - Jewel of Creation (item group 14, number 22). - 4 - - - JewelOfLife - Jewel of Life (item group 14, number 16). - 5 - - - WCoinC - WCoin (C), an account coin. - 6 - - - WCoinP - WCoin (P), an account coin. - 7 - - - GoblinPoints - Goblin Points, an account coin. - 8 - - - \ No newline at end of file diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs index 07f1f421e9..89fb20a477 100644 --- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs +++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs @@ -2494,34 +2494,6 @@ int WritePacket() await connection.SendAsync(WritePacket).ConfigureAwait(false); } - /// - /// Sends a to this connection. - /// - /// The connection. - /// The bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. - /// - /// Is sent by the server when: After entering the game world, and before the player shop of the own player or of another player is shown. - /// Causes reaction on client side: The client offers only the enabled currencies when the player sets an item price in his player shop. - /// - public static async ValueTask SendPlayerShopCurrenciesExtendedAsync(this IConnection? connection, uint @currencyMask) - { - if (connection is null) - { - return; - } - - int WritePacket() - { - var length = PlayerShopCurrenciesExtendedRef.Length; - var packet = new PlayerShopCurrenciesExtendedRef(connection.Output.GetSpan(length)[..length]); - packet.CurrencyMask = @currencyMask; - - return packet.Header.Length; - } - - await connection.SendAsync(WritePacket).ConfigureAwait(false); - } - /// /// Sends a to this connection. /// diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs index 684d3c87f7..3bca8dbae1 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs @@ -12273,28 +12273,28 @@ public byte ItemCount /// -/// Is sent by the server when: After entering the game world, and before the player shop of the own player or of another player is shown. -/// Causes reaction on client side: The client offers only the enabled currencies when the player sets an item price in his player shop. +/// Is sent by the server when: After entering the game world, and before the player shop of another player is shown, when the server allows prices in player shops which are not paid in zen. +/// Causes reaction on client side: The client offers the allowed price types, in addition to zen, when the player sets an item price in his player shop. Without this message, only zen prices are possible. /// -public readonly struct PlayerShopCurrenciesExtended +public readonly struct PlayerShopPriceTypesExtended { private readonly Memory _data; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The underlying data. - public PlayerShopCurrenciesExtended(Memory data) + public PlayerShopPriceTypesExtended(Memory data) : this(data, true) { } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The underlying data. /// If set to true, the header data is automatically initialized and written to the underlying span. - private PlayerShopCurrenciesExtended(Memory data, bool initialize) + private PlayerShopPriceTypesExtended(Memory data, bool initialize) { this._data = data; if (initialize) @@ -12302,7 +12302,7 @@ private PlayerShopCurrenciesExtended(Memory data, bool initialize) var header = this.Header; header.Type = HeaderType; header.Code = Code; - header.Length = (byte)Math.Min(data.Length, Length); + header.Length = (byte)data.Length; header.SubCode = SubCode; } } @@ -12323,38 +12323,77 @@ private PlayerShopCurrenciesExtended(Memory data, bool initialize) /// public static byte SubCode => 0x0A; - /// - /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. - /// - public static int Length => 8; - /// /// Gets the header of this packet. /// public C1HeaderWithSubCode Header => new (this._data); /// - /// Gets or sets the bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. + /// Gets or sets the count. /// - public uint CurrencyMask + public byte Count { - get => ReadUInt32LittleEndian(this._data.Span[4..]); - set => WriteUInt32LittleEndian(this._data.Span[4..], value); + get => this._data.Span[4]; + set => this._data.Span[4] = value; } /// - /// Performs an implicit conversion from a Memory of bytes to a . + /// Gets the of the specified index. + /// + public PlayerShopPriceType this[int index] => new (this._data.Slice(5 + index * PlayerShopPriceType.Length)); + + /// + /// Performs an implicit conversion from a Memory of bytes to a . /// /// The packet as span. /// The packet as struct. - public static implicit operator PlayerShopCurrenciesExtended(Memory packet) => new (packet, false); + public static implicit operator PlayerShopPriceTypesExtended(Memory packet) => new (packet, false); /// - /// Performs an implicit conversion from to a Memory of bytes. + /// Performs an implicit conversion from to a Memory of bytes. /// /// The packet as struct. /// The packet as byte span. - public static implicit operator Memory(PlayerShopCurrenciesExtended packet) => packet._data; + public static implicit operator Memory(PlayerShopPriceTypesExtended packet) => packet._data; + + /// + /// Calculates the size of the packet for the specified count of . + /// + /// The count of from which the size will be calculated. + + public static int GetRequiredSize(int priceTypesCount) => priceTypesCount * PlayerShopPriceType.Length + 5; + + +/// +/// A price type which is allowed for prices in player shops, in addition to zen.. +/// +public readonly struct PlayerShopPriceType +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public PlayerShopPriceType(Memory data) + { + this._data = data; + } + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 2; + + /// + /// Gets or sets the value of PriceItemType of a price of this type, like in PlayerShopItemExtended: the item (group in the highest 4 bits, number in the remaining ones) with which the price is paid, or 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P), 0xFFF2 = Goblin Points. + /// + public ushort PriceItemType + { + get => ReadUInt16LittleEndian(this._data.Span); + set => WriteUInt16LittleEndian(this._data.Span, value); + } +} } diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml index 3fabb011a8..f3396b3919 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml @@ -4566,19 +4566,39 @@ C1HeaderWithSubCode 3F 0A - PlayerShopCurrenciesExtended - 8 + PlayerShopPriceTypesExtended ServerToClient - After entering the game world, and before the player shop of the own player or of another player is shown. - The client offers only the enabled currencies when the player sets an item price in his player shop. + After entering the game world, and before the player shop of another player is shown, when the server allows prices in player shops which are not paid in zen. + The client offers the allowed price types, in addition to zen, when the player sets an item price in his player shop. Without this message, only zen prices are possible. 4 - IntegerLittleEndian - CurrencyMask - The bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. + Byte + Count + + + 5 + Structure[] + PlayerShopPriceType + PriceTypes + Count + + + PlayerShopPriceType + A price type which is allowed for prices in player shops, in addition to zen. + 2 + + + 0 + ShortLittleEndian + PriceItemType + The value of PriceItemType of a price of this type, like in PlayerShopItemExtended: the item (group in the highest 4 bits, number in the remaining ones) with which the price is paid, or 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P), 0xFFF2 = Goblin Points. + + + + C2HeaderWithSubCode diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs index c92246bc90..4f1268e957 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs @@ -11489,28 +11489,28 @@ public byte ItemCount /// -/// Is sent by the server when: After entering the game world, and before the player shop of the own player or of another player is shown. -/// Causes reaction on client side: The client offers only the enabled currencies when the player sets an item price in his player shop. +/// Is sent by the server when: After entering the game world, and before the player shop of another player is shown, when the server allows prices in player shops which are not paid in zen. +/// Causes reaction on client side: The client offers the allowed price types, in addition to zen, when the player sets an item price in his player shop. Without this message, only zen prices are possible. /// -public readonly ref struct PlayerShopCurrenciesExtendedRef +public readonly ref struct PlayerShopPriceTypesExtendedRef { private readonly Span _data; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The underlying data. - public PlayerShopCurrenciesExtendedRef(Span data) + public PlayerShopPriceTypesExtendedRef(Span data) : this(data, true) { } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The underlying data. /// If set to true, the header data is automatically initialized and written to the underlying span. - private PlayerShopCurrenciesExtendedRef(Span data, bool initialize) + private PlayerShopPriceTypesExtendedRef(Span data, bool initialize) { this._data = data; if (initialize) @@ -11518,7 +11518,7 @@ private PlayerShopCurrenciesExtendedRef(Span data, bool initialize) var header = this.Header; header.Type = HeaderType; header.Code = Code; - header.Length = (byte)Math.Min(data.Length, Length); + header.Length = (byte)data.Length; header.SubCode = SubCode; } } @@ -11539,38 +11539,77 @@ private PlayerShopCurrenciesExtendedRef(Span data, bool initialize) /// public static byte SubCode => 0x0A; - /// - /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. - /// - public static int Length => 8; - /// /// Gets the header of this packet. /// public C1HeaderWithSubCodeRef Header => new (this._data); /// - /// Gets or sets the bit mask of the currencies which are enabled for prices in player shops. Bit N corresponds to the ShopCurrencyType with value N, e.g. bit 0 is Zen. A value of 0 means that the feature is disabled and only Zen prices with PlayerShopSetItemPrice are supported. + /// Gets or sets the count. /// - public uint CurrencyMask + public byte Count { - get => ReadUInt32LittleEndian(this._data[4..]); - set => WriteUInt32LittleEndian(this._data[4..], value); + get => this._data[4]; + set => this._data[4] = value; } /// - /// Performs an implicit conversion from a Span of bytes to a . + /// Gets the of the specified index. + /// + public PlayerShopPriceTypeRef this[int index] => new (this._data[(5 + index * PlayerShopPriceTypeRef.Length)..]); + + /// + /// Performs an implicit conversion from a Span of bytes to a . /// /// The packet as span. /// The packet as struct. - public static implicit operator PlayerShopCurrenciesExtendedRef(Span packet) => new (packet, false); + public static implicit operator PlayerShopPriceTypesExtendedRef(Span packet) => new (packet, false); /// - /// Performs an implicit conversion from to a Span of bytes. + /// Performs an implicit conversion from to a Span of bytes. /// /// The packet as struct. /// The packet as byte span. - public static implicit operator Span(PlayerShopCurrenciesExtendedRef packet) => packet._data; + public static implicit operator Span(PlayerShopPriceTypesExtendedRef packet) => packet._data; + + /// + /// Calculates the size of the packet for the specified count of . + /// + /// The count of from which the size will be calculated. + + public static int GetRequiredSize(int priceTypesCount) => priceTypesCount * PlayerShopPriceTypeRef.Length + 5; + + +/// +/// A price type which is allowed for prices in player shops, in addition to zen.. +/// +public readonly ref struct PlayerShopPriceTypeRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public PlayerShopPriceTypeRef(Span data) + { + this._data = data; + } + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 2; + + /// + /// Gets or sets the value of PriceItemType of a price of this type, like in PlayerShopItemExtended: the item (group in the highest 4 bits, number in the remaining ones) with which the price is paid, or 0xFFF0 = WCoin (C), 0xFFF1 = WCoin (P), 0xFFF2 = Goblin Points. + /// + public ushort PriceItemType + { + get => ReadUInt16LittleEndian(this._data); + set => WriteUInt16LittleEndian(this._data, value); + } +} } diff --git a/tests/MUnique.OpenMU.Network.Packets.Tests/ClientToServerPacketTests.cs b/tests/MUnique.OpenMU.Network.Packets.Tests/ClientToServerPacketTests.cs index cc2227915e..c2b1c4652f 100644 --- a/tests/MUnique.OpenMU.Network.Packets.Tests/ClientToServerPacketTests.cs +++ b/tests/MUnique.OpenMU.Network.Packets.Tests/ClientToServerPacketTests.cs @@ -383,23 +383,27 @@ public void PlayerShopCloseOther_PacketSizeValidation() public void PlayerShopSetItemPriceExtended_PacketSizeValidation() { // Fixed-length packet validation - const int expectedLength = 10; + const int expectedLength = 13; var actualLength = PlayerShopSetItemPriceExtendedRef.Length; Assert.That(actualLength, Is.EqualTo(expectedLength), "Packet length mismatch: declared length does not match calculated size"); - // Validate field 'ItemSlot' boundary - Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength), - "Field 'ItemSlot' exceeds packet boundary"); + // Validate field 'MoneyPrice' boundary + Assert.That(4 + 4, Is.LessThanOrEqualTo(expectedLength), + "Field 'MoneyPrice' exceeds packet boundary"); - // Validate field 'Currency' boundary - Assert.That(5 + 1, Is.LessThanOrEqualTo(expectedLength), - "Field 'Currency' exceeds packet boundary"); + // Validate field 'PriceItemType' boundary + Assert.That(8 + 2, Is.LessThanOrEqualTo(expectedLength), + "Field 'PriceItemType' exceeds packet boundary"); - // Validate field 'Price' boundary - Assert.That(6 + 4, Is.LessThanOrEqualTo(expectedLength), - "Field 'Price' exceeds packet boundary"); + // Validate field 'RequiredItemAmount' boundary + Assert.That(10 + 2, Is.LessThanOrEqualTo(expectedLength), + "Field 'RequiredItemAmount' exceeds packet boundary"); + + // Validate field 'ItemSlot' boundary + Assert.That(12 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'ItemSlot' exceeds packet boundary"); } /// diff --git a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs index 69567bb761..c3d0744ad2 100644 --- a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs +++ b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs @@ -2425,21 +2425,17 @@ public void PlayerShopItemListExtended_PacketSizeValidation() } /// - /// Tests the packet size calculation for PlayerShopCurrenciesExtended. + /// Tests the packet size calculation for PlayerShopPriceTypesExtended. /// [Test] - public void PlayerShopCurrenciesExtended_PacketSizeValidation() + public void PlayerShopPriceTypesExtended_PacketSizeValidation() { - // Fixed-length packet validation - const int expectedLength = 8; - var actualLength = PlayerShopCurrenciesExtendedRef.Length; - - Assert.That(actualLength, Is.EqualTo(expectedLength), - "Packet length mismatch: declared length does not match calculated size"); + // Basic packet validation + // Validate header type and field boundaries - // Validate field 'CurrencyMask' boundary - Assert.That(4 + 4, Is.LessThanOrEqualTo(expectedLength), - "Field 'CurrencyMask' exceeds packet boundary"); + // Field 'Count' starts at index 4 with size 1 + Assert.That(4, Is.GreaterThanOrEqualTo(0), + "Field 'Count' has invalid negative index"); } ///