From 4d14134e3e50b97f8e13d1f9dda3769249410475 Mon Sep 17 00:00:00 2001 From: Sven Nicolai Date: Sun, 11 Oct 2026 11:38:15 +0000 Subject: [PATCH 1/3] feat(player-shop): prices in items and account coins Players can define the price of each item of their personal store in zen, in configured items (by default the Jewels of Bless, Soul, Chaos, Creation and Life) or in the account coins WCoin C/P and Goblin Points. Closes #1186. - Item.StorePriceItemDefinition (reference to the price item) and Item.StorePriceCoinType, both null for zen prices, with migration, compiled models and generated loader code. - PlayerShopCurrenciesFeaturePlugIn (disabled by default) with the list of price items, one flag per account coin and maximum amounts. A new database gets the five jewels as price items. While the plugin is off, the protocol and the behavior stay as before. - ShopPrice describes a price; ShopPrices decides which prices are allowed, names them and parses them in chat commands. - One payment exchange per price kind: zen as before; items move as whole items (singles and jewel mix bundles, exact amount, no change) within the trade context which also moves the sold item; coins are taken from the buyer's account and saved right away, with a rollback when the save fails. - Set price, open store and buy re-validate the price server-side. The original client only sees zen items and can't buy others. - Extended client: the 3F 09 set price handler and the extended item list share one price encoding (MoneyPrice, PriceItemType, RequiredItemAmount); 3F 0A lists the allowed price types on world entry and before a shop list. - /shopprice chat command (disabled by default) for every client. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7XufeAZ22z7ocXAXUqUsE --- .../server-features/player-shop-currencies.md | 80 + docs-website/sidebars.js | 1 + src/DataModel/Entities/Item.cs | 13 + .../PlayerStore/BuyRequestAction.cs | 122 +- .../PlayerStore/OpenStoreAction.cs | 9 + .../PlayerStore/SetItemPriceAction.cs | 83 +- .../PlayerStore/StoreItemListRequestAction.cs | 2 + .../AccountCoinShopCurrencyExchange.cs | 97 + .../IShopCurrencyExchange.cs | 44 + .../ItemShopCurrencyExchange.cs | 204 + .../ItemStorePriceExtensions.cs | 35 + .../PlayerShopCurrenciesConfiguration.cs | 74 + .../PlayerShopCurrenciesFeaturePlugIn.cs | 45 + .../PlayerShopCurrencies/ShopPaymentResult.cs | 42 + .../PlayerShopCurrencies/ShopPrice.cs | 51 + .../PlayerShopCurrencies/ShopPriceKind.cs | 26 + .../PlayerShopCurrencies/ShopPrices.cs | 257 + .../PlayerShopCurrencies/ShopPurchase.cs | 26 + ...ShowShopCurrenciesAfterEnterWorldPlugIn.cs | 30 + .../ZenShopCurrencyExchange.cs | 48 + .../Arguments/ShopPriceChatCommandArgs.cs | 31 + .../ShopPriceChatCommandPlugIn.cs | 133 + .../Properties/PlayerMessage.Designer.cs | 180 + .../Properties/PlayerMessage.de.resx | 60 + src/GameLogic/Properties/PlayerMessage.resx | 60 + .../Properties/PlugInResources.Designer.cs | 54 + .../Properties/PlugInResources.de.resx | 18 + src/GameLogic/Properties/PlugInResources.resx | 18 + .../IPlayerShopBuyRequestResultPlugIn.cs | 5 + .../PlayerShop/IShowShopCurrenciesPlugIn.cs | 24 + ...SetItemPriceExtendedPacketHandlerPlugIn.cs | 57 + .../Properties/PlugInResources.Designer.cs | 36 + .../Properties/PlugInResources.de.resx | 12 + .../Properties/PlugInResources.resx | 12 + ...layerShopBuyRequestResultExtendedPlugIn.cs | 1 + .../PlayerShopBuyRequestResultPlugIn.cs | 2 +- .../PlayerShop/PlayerShopPriceEncoding.cs | 99 + .../ShowShopCurrenciesExtendedPlugIn.cs | 71 + .../ShowShopItemListExtendedPlugIn.cs | 20 +- .../PlayerShop/ShowShopItemListPlugIn.cs | 5 +- src/Persistence/BasicModel/Item.Generated.cs | 18 + .../AccountContextModelBuilder.cs | 2 +- .../AccountContext/ItemEntityType.cs | 19 +- .../EntityDataContextModelBuilder.cs | 3 +- .../EntityDataContext/ItemEntityType.cs | 43 +- .../TradeContext/ItemEntityType.cs | 19 +- .../TradeContext/TradeContextModelBuilder.cs | 2 +- .../GameConfigurationLoader.Generated.cs | 16 +- ...0042_AddItemStorePriceCurrency.Designer.cs | 6312 +++++++++++++++++ ...0261011120042_AddItemStorePriceCurrency.cs | 68 + .../EntityDataContextModelSnapshot.cs | 14 + .../Model/BasicModelConverter.Generated.cs | 2 + .../EntityFramework/Model/Item.Generated.cs | 26 + .../Initialization/DataInitializationBase.cs | 30 + .../PlayerShopCurrenciesDataTest.cs | 40 + .../PlayerShopCurrenciesRemoteViewTests.cs | 101 + .../PlayerShopCurrenciesTest.cs | 358 + 57 files changed, 9196 insertions(+), 64 deletions(-) create mode 100644 docs-website/docs/server-features/player-shop-currencies.md create mode 100644 src/GameLogic/PlayerShopCurrencies/AccountCoinShopCurrencyExchange.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/IShopCurrencyExchange.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ItemStorePriceExtensions.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesConfiguration.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesFeaturePlugIn.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ShopPaymentResult.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ShopPrice.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ShopPriceKind.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ShopPrices.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ShopPurchase.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ShowShopCurrenciesAfterEnterWorldPlugIn.cs create mode 100644 src/GameLogic/PlayerShopCurrencies/ZenShopCurrencyExchange.cs create mode 100644 src/GameLogic/PlugIns/ChatCommands/Arguments/ShopPriceChatCommandArgs.cs create mode 100644 src/GameLogic/PlugIns/ChatCommands/ShopPriceChatCommandPlugIn.cs create mode 100644 src/GameLogic/Views/PlayerShop/IShowShopCurrenciesPlugIn.cs create mode 100644 src/GameServer/MessageHandler/PlayerShop/PlayerShopSetItemPriceExtendedPacketHandlerPlugIn.cs create mode 100644 src/GameServer/RemoteView/PlayerShop/PlayerShopPriceEncoding.cs create mode 100644 src/GameServer/RemoteView/PlayerShop/ShowShopCurrenciesExtendedPlugIn.cs create mode 100644 src/Persistence/EntityFramework/Migrations/20261011120042_AddItemStorePriceCurrency.Designer.cs create mode 100644 src/Persistence/EntityFramework/Migrations/20261011120042_AddItemStorePriceCurrency.cs create mode 100644 tests/MUnique.OpenMU.Persistence.Initialization.Tests/PlayerShopCurrenciesDataTest.cs create mode 100644 tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesRemoteViewTests.cs create mode 100644 tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesTest.cs diff --git a/docs-website/docs/server-features/player-shop-currencies.md b/docs-website/docs/server-features/player-shop-currencies.md new file mode 100644 index 0000000000..518037efb2 --- /dev/null +++ b/docs-website/docs/server-features/player-shop-currencies.md @@ -0,0 +1,80 @@ +--- +title: Player shop currencies +sidebar_label: Player shop currencies +sidebar_position: 5 +description: Prices of items in personal stores in items like jewels, and in account coins, in addition to zen. +--- + +# Player shop currencies + +In a personal store, the price of an item is usually defined in zen. With the +plugin **Player shop currencies**, players can define the price of each item in +one of these currencies, too: + +* items which are 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 (see [Cash shop](cash-shop.md)). + +The plugin is deactivated by default. Then, the personal stores work exactly as +before: only zen prices are possible, and the server sends nothing new to the +clients. + +## Clients + +Only the open source client ([MuMain](https://github.com/sven-n/MuMain)) shows +and buys items with prices in other currencies than zen. When the plugin is +active, the server tells it which price items and coins are allowed, so it +offers them when the player sets the prices of the store. + +The original Season 6 client can only show zen prices. Items with prices in +other currencies are hidden in its store list, and the server refuses to sell +them to it. + +## Settings + +The plugin is configured on the [Plugins](../admin-panel/plugins.md) page: + +| Setting | Default | Meaning | +|---|---|---| +| Price Items | the five jewels | The items with which prices can be paid. | +| Is WCoin C/WCoin P/Goblin Points Allowed | no | Prices in this account coin are allowed. The coins are usually bought with real money, so trading them has to be allowed explicitly. | +| Maximum Item Amount | 1000 | The maximum number of item pieces of a price; at most 65535. | +| Maximum Coin Amount | 1000000 | The maximum amount of account coins of a price. | + +A new database gets the five jewels as price items. On an existing database, the +plugin is created without price items, because its default configuration can't +reference items: add them when you activate the plugin. + +When a price item or coin is disallowed while players have prices in it, their +stores can't be opened until the prices are changed, and these items can't be +bought from stores which are already open. + +## Paying with items + +The buyer pays with the price items of the inventory. A single item counts as +one piece, and the bundles of the jewel mix count as their number of pieces +(10, 20 or 30). Only tradable items without level and options count. + +Bundles are never split, and there is no change: the buyer needs a combination +of items and bundles which matches the price exactly. The items move into the +inventory of the seller, which needs a free slot for each of them. + +## Paying with account coins + +The coins move from the account of the buyer to the account of the seller. +Pending coin grants of the buyer are applied first. + +## Chat command + +The plugin **Shop price chat command** (deactivated by default) sets prices with +the chat, which works with every client: + +* `/shopprice` lists the items of the own store with their prices; +* `/shopprice [currency]` sets the price of the item at the + position 1 to 32 of the store (or its inventory slot 204 to 235). Without a + currency, the price is in zen. A price of 0 removes the price. + +The currency can be `zen`, a price item by its group and number (e.g. `14/13`), +one of the jewels by name (`bless`/`job`, `soul`/`jos`, `chaos`/`joc`, +`creation`/`jocr`, `life`/`jol`) or an account coin (`wcoinc`/`wcc`, +`wcoinp`/`wcp`, `goblin`/`gp`). The store has to be closed to change its prices. diff --git a/docs-website/sidebars.js b/docs-website/sidebars.js index 54cf95ae38..935c93cf82 100644 --- a/docs-website/sidebars.js +++ b/docs-website/sidebars.js @@ -63,6 +63,7 @@ const sidebars = { 'server-features/test-actors', 'server-features/gens', 'server-features/cash-shop', + 'server-features/player-shop-currencies', ], }, { diff --git a/src/DataModel/Entities/Item.cs b/src/DataModel/Entities/Item.cs index d822fc736a..aa02421141 100644 --- a/src/DataModel/Entities/Item.cs +++ b/src/DataModel/Entities/Item.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.DataModel.Entities; using System.Globalization; using MUnique.OpenMU.Annotations; +using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.DataModel.Properties; @@ -62,6 +63,18 @@ public partial class Item /// public int? StorePrice { get; set; } + /// + /// Gets or sets the definition of the items with which the is paid, e.g. a jewel. + /// It's null, if the price is paid in zen or account coins. + /// + public virtual ItemDefinition? StorePriceItemDefinition { get; set; } + + /// + /// Gets or sets the account coin type with which the is paid. + /// It's null, if the price is paid in zen or items. + /// + public CashShopCoinType? StorePriceCoinType { get; set; } + /// /// Gets or sets the pet experience. /// Only applies, if this item is actually a trainable pet. diff --git a/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs b/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs index 6c89163752..70a6d9aca6 100644 --- a/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs +++ b/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs @@ -4,6 +4,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; using MUnique.OpenMU.GameLogic.PlugIns; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.Inventory; @@ -67,11 +68,28 @@ public async ValueTask BuyItemAsync(Player player, Player requestedPlayer, byte return; } - var itemPrice = item.StorePrice.Value; + var price = item.GetStorePrice()!.Value; + if (price.Kind != ShopPriceKind.Zen && !player.SupportsShopCurrencies()) + { + // The client can't show such items, so it should never request them. + player.Logger.LogWarning("Player {0} tried to buy {1} with a price in {2} from the store of {3}, which its client can't show.", player, item, price.Kind, requestedPlayer); + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.ItemBlock, null)).ConfigureAwait(false); + return; + } + + if (!ShopPrices.IsAllowed(player.GameContext, price)) + { + player.Logger.LogDebug("The price of item {0} isn't allowed.", item); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopCurrencyNotAllowed), player.GetCurrencyName(price)).ConfigureAwait(false); + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.ItemBlock, null)).ConfigureAwait(false); + return; + } - if (player.Money < itemPrice) + var exchange = ShopPrices.GetExchange(price.Kind); + var fundsResult = exchange.CheckFunds(player, price); + if (fundsResult != ShopPaymentResult.Success) { - await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.LackOfMoney, null)).ConfigureAwait(false); + await this.ShowPaymentFailedAsync(player, requestedPlayer, price, fundsResult).ConfigureAwait(false); return; } @@ -101,36 +119,50 @@ public async ValueTask BuyItemAsync(Player player, Player requestedPlayer, byte return; } - player.Logger.LogDebug("BuyRequest, Item Price: {0}", itemPrice); - if (player.TryRemoveMoney(itemPrice)) + if (item.GetStorePrice() is not { } currentPrice || currentPrice.Amount != price.Amount || !currentPrice.HasSameCurrency(price)) { - if (requestedPlayer.TryAddMoney(itemPrice)) - { - using var itemContext = requestedPlayer.GameContext.PersistenceContextProvider.CreateNewTradeContext(); - itemContext.Attach(item); - await requestedPlayer.ShopStorage.RemoveItemAsync(item).ConfigureAwait(false); - await requestedPlayer.InvokeViewPlugInAsync(p => p.UpdateMoneyAsync()).ConfigureAwait(false); - await requestedPlayer.InvokeViewPlugInAsync(p => p.ItemSoldByPlayerShopAsync(slot, player)).ConfigureAwait(false); - await requestedPlayer.InvokeViewPlugInAsync(p => p.RemoveItemAsync(slot)).ConfigureAwait(false); - item.ItemSlot = (byte)freeslot; - item.StorePrice = null; - await player.Inventory!.AddItemAsync(item).ConfigureAwait(false); - requestedPlayer.PersistenceContext.Detach(item); - await itemContext.SaveChangesAsync().ConfigureAwait(false); - player.PersistenceContext.Attach(item); - await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.Success, item)).ConfigureAwait(false); - await player.InvokeViewPlugInAsync(p => p.UpdateMoneyAsync()).ConfigureAwait(false); - itemSold = true; - - player.GameContext.PlugInManager.GetPlugInPoint()?.ItemSold(requestedPlayer, item, player); - } - else - { - await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.MoneyOverflowOrNotEnoughSpace, null)).ConfigureAwait(false); - await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.SellerInventoryFull)).ConfigureAwait(false); - player.TryAddMoney(itemPrice); - } + // The seller can't change prices while the store is open, but the slot may contain another item by now. + player.Logger.LogDebug("The price of the item changed, Slot {0}", slot); + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.NameMismatchOrPriceMissing, null)).ConfigureAwait(false); + return; + } + + player.Logger.LogDebug("BuyRequest, Item Price: {0} {1}", price.Amount, price.PriceItem?.ToString() ?? price.CoinType?.ToString() ?? "zen"); + using var itemContext = requestedPlayer.GameContext.PersistenceContextProvider.CreateNewTradeContext(); + var purchase = new ShopPurchase(player, requestedPlayer, price, itemContext); + var paymentResult = await exchange.TryPayAsync(purchase).ConfigureAwait(false); + if (paymentResult != ShopPaymentResult.Success) + { + await this.ShowPaymentFailedAsync(player, requestedPlayer, price, paymentResult).ConfigureAwait(false); + return; } + + itemContext.Attach(item); + await requestedPlayer.ShopStorage.RemoveItemAsync(item).ConfigureAwait(false); + await exchange.NotifySellerAsync(purchase).ConfigureAwait(false); + await requestedPlayer.InvokeViewPlugInAsync(p => p.ItemSoldByPlayerShopAsync(slot, player)).ConfigureAwait(false); + await requestedPlayer.InvokeViewPlugInAsync(p => p.RemoveItemAsync(slot)).ConfigureAwait(false); + item.ItemSlot = (byte)freeslot; + item.SetStorePrice(null); + await player.Inventory!.AddItemAsync(item).ConfigureAwait(false); + requestedPlayer.PersistenceContext.Detach(item); + foreach (var (paymentItem, _) in purchase.PaymentItems) + { + player.PersistenceContext.Detach(paymentItem); + } + + await itemContext.SaveChangesAsync().ConfigureAwait(false); + player.PersistenceContext.Attach(item); + foreach (var (paymentItem, _) in purchase.PaymentItems) + { + requestedPlayer.PersistenceContext.Attach(paymentItem); + } + + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.Success, item)).ConfigureAwait(false); + await exchange.NotifyBuyerAsync(purchase).ConfigureAwait(false); + itemSold = true; + + player.GameContext.PlugInManager.GetPlugInPoint()?.ItemSold(requestedPlayer, item, player); } if (itemSold) @@ -146,4 +178,32 @@ public async ValueTask BuyItemAsync(Player player, Player requestedPlayer, byte } } } + + private async ValueTask ShowPaymentFailedAsync(Player player, Player seller, ShopPrice price, ShopPaymentResult paymentResult) + { + var isZen = price.Kind == ShopPriceKind.Zen; + switch (paymentResult) + { + case ShopPaymentResult.LackOfCurrency when isZen: + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(seller, ItemBuyResult.LackOfMoney, null)).ConfigureAwait(false); + break; + case ShopPaymentResult.LackOfCurrency: + // The client shows a message about zen for the result, so we tell the actual price. + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(seller, ItemBuyResult.LackOfPriceCurrency, null)).ConfigureAwait(false); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopNotEnoughCurrency), price.Amount, player.GetCurrencyName(price)).ConfigureAwait(false); + break; + case ShopPaymentResult.ExactAmountNotPayable: + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(seller, ItemBuyResult.LackOfPriceCurrency, null)).ConfigureAwait(false); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopItemPriceNotPayableExactly), price.Amount, player.GetCurrencyName(price)).ConfigureAwait(false); + break; + case ShopPaymentResult.SellerOverflow: + case ShopPaymentResult.SellerInventoryFull: + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(seller, ItemBuyResult.MoneyOverflowOrNotEnoughSpace, null)).ConfigureAwait(false); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.SellerInventoryFull)).ConfigureAwait(false); + break; + default: + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(seller, ItemBuyResult.Undefined, null)).ConfigureAwait(false); + break; + } + } } \ No newline at end of file diff --git a/src/GameLogic/PlayerActions/PlayerStore/OpenStoreAction.cs b/src/GameLogic/PlayerActions/PlayerStore/OpenStoreAction.cs index 4e2e13640e..a33d5c1b3e 100644 --- a/src/GameLogic/PlayerActions/PlayerStore/OpenStoreAction.cs +++ b/src/GameLogic/PlayerActions/PlayerStore/OpenStoreAction.cs @@ -5,6 +5,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; using MUnique.OpenMU.GameLogic.Views.PlayerShop; /// @@ -39,6 +40,14 @@ public async ValueTask OpenStoreAsync(Player player, string storeName) return; } + if (player.ShopStorage.Items.FirstOrDefault(i => i.GetStorePrice() is { } price && !ShopPrices.IsAllowed(player.GameContext, price)) is { } itemWithDisallowedPrice) + { + var price = itemWithDisallowedPrice.GetStorePrice()!.Value; + player.Logger.LogWarning("OpenStore request failed: The price of item {0} isn't allowed. Player: [{1}], StoreName: [{2}]", itemWithDisallowedPrice, character.Name, character.StoreName); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopCurrencyNotAllowed), player.GetCurrencyName(price)).ConfigureAwait(false); + return; + } + character.StoreName = storeName; player.ShopStorage.StoreOpen = true; player.Logger.LogDebug("OpenStore: Player: [{0}], StoreName: [{1}]", character.Name, character.StoreName); diff --git a/src/GameLogic/PlayerActions/PlayerStore/SetItemPriceAction.cs b/src/GameLogic/PlayerActions/PlayerStore/SetItemPriceAction.cs index 2952fd0b44..da13da5598 100644 --- a/src/GameLogic/PlayerActions/PlayerStore/SetItemPriceAction.cs +++ b/src/GameLogic/PlayerActions/PlayerStore/SetItemPriceAction.cs @@ -1,9 +1,10 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; using MUnique.OpenMU.GameLogic.Views.Inventory; using MUnique.OpenMU.GameLogic.Views.PlayerShop; @@ -13,34 +14,88 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; public class SetItemPriceAction { /// - /// Sets the price of an item. + /// Sets the price of an item in zen. /// /// The player. /// The slot of the item in the store (0 to 31). /// The price. - public async ValueTask SetPriceAsync(Player player, byte slot, int price) + public ValueTask SetPriceAsync(Player player, byte slot, int price) + { + return this.SetPriceAsync(player, slot, new ShopPrice(price)); + } + + /// + /// Sets the price of an item, which may be paid with zen, items or account coins. + /// + /// The player. + /// The slot of the item in the store (0 to 31). + /// The price. + public async ValueTask SetPriceAsync(Player player, byte slot, ShopPrice price) + { + var result = await this.TrySetPriceAsync(player, slot, price).ConfigureAwait(false); + await player.InvokeViewPlugInAsync(p => p.ItemPriceSetResponseAsync(slot, result)).ConfigureAwait(false); + } + + /// + /// Tries to set the price of an item, without sending the response. + /// + /// The player. + /// The slot of the item in the inventory. + /// The price. An amount of 0 removes the price. + /// The result. + /// + /// When a price isn't allowed, or is too high, a message is shown to the player, + /// because the result can't express it. + /// + public async ValueTask TrySetPriceAsync(Player player, byte slot, ShopPrice price) { - ItemPriceResult result; if (player.ShopStorage?.StoreOpen ?? false) { - result = ItemPriceResult.Failed; + return ItemPriceResult.Failed; } - else if (player.Level < 6) + + if (player.Level < 6) + { + return ItemPriceResult.CharacterLevelTooLow; + } + + var item = player.SelectedCharacter?.Inventory?.Items?.FirstOrDefault(i => i.ItemSlot == slot); + if (item is null) + { + return ItemPriceResult.ItemNotFound; + } + + if (price.Amount < 0) { - result = ItemPriceResult.CharacterLevelTooLow; + // Like before the other prices were introduced, a negative price removes the price. + item.SetStorePrice(null); + return ItemPriceResult.PriceNegative; } - else + + if (price.Amount == 0) { - result = ItemPriceResult.ItemNotFound; + item.SetStorePrice(null); + return ItemPriceResult.Success; + } - var item = player.SelectedCharacter?.Inventory?.Items?.FirstOrDefault(i => i.ItemSlot == slot); - if (item != null) + if (price.Kind != ShopPriceKind.Zen) + { + if (!ShopPrices.IsAllowed(player.GameContext, price)) { - item.StorePrice = price > 0 ? price : (int?)null; - result = price >= 0 ? ItemPriceResult.Success : ItemPriceResult.PriceNegative; + player.Logger.LogWarning("Player {player} tried to set a price in {currency}, which isn't allowed.", player, price.PriceItem?.ToString() ?? price.CoinType?.ToString()); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopCurrencyNotAllowed), player.GetCurrencyName(price)).ConfigureAwait(false); + return ItemPriceResult.Failed; + } + + var maximumAmount = ShopPrices.GetMaximumAmount(player.GameContext, price.Kind); + if (price.Amount > maximumAmount) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopPriceTooHigh), maximumAmount, player.GetCurrencyName(price)).ConfigureAwait(false); + return ItemPriceResult.Failed; } } - await player.InvokeViewPlugInAsync(p => p.ItemPriceSetResponseAsync(slot, result)).ConfigureAwait(false); + item.SetStorePrice(price); + return ItemPriceResult.Success; } } diff --git a/src/GameLogic/PlayerActions/PlayerStore/StoreItemListRequestAction.cs b/src/GameLogic/PlayerActions/PlayerStore/StoreItemListRequestAction.cs index f16bb6ab3c..2c94cd3c5f 100644 --- a/src/GameLogic/PlayerActions/PlayerStore/StoreItemListRequestAction.cs +++ b/src/GameLogic/PlayerActions/PlayerStore/StoreItemListRequestAction.cs @@ -4,6 +4,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.PlayerShop; using MUnique.OpenMU.Interfaces; @@ -27,6 +28,7 @@ public async ValueTask RequestStoreItemListAsync(Player player, Player requested } player.LastRequestedPlayerStore = new WeakReference(requestedPlayer); + await player.ShowAllowedPricesAsync().ConfigureAwait(false); await player.InvokeViewPlugInAsync(p => p.ShowShopItemListAsync(requestedPlayer, false)).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/GameLogic/PlayerShopCurrencies/AccountCoinShopCurrencyExchange.cs b/src/GameLogic/PlayerShopCurrencies/AccountCoinShopCurrencyExchange.cs new file mode 100644 index 0000000000..205784453a --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/AccountCoinShopCurrencyExchange.cs @@ -0,0 +1,97 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using MUnique.OpenMU.GameLogic.CashShop; + +/// +/// Pays prices in account coins (WCoin C, WCoin P, Goblin Points), by transferring them from the account +/// of the buyer to the one of the seller. +/// +/// +/// The pending coin grants of the buyer are applied first, so the balance is up to date. The account of the +/// buyer is saved right away, and the balances are restored when that fails. The account of the seller is +/// saved by its regular progress save, like the zen of a zen price: the persistence lock of the seller must +/// not be taken from the flow of the buyer (see the lock order invariant of PlayerPersistence). +/// +public sealed class AccountCoinShopCurrencyExchange : IShopCurrencyExchange +{ + /// + public ShopPaymentResult CheckFunds(Player buyer, ShopPrice price) + { + if (price.CoinType is not { } coinType || buyer.Account is not { } account) + { + return ShopPaymentResult.Failed; + } + + return account.GetCashShopCoins(coinType) < price.Amount ? ShopPaymentResult.LackOfCurrency : ShopPaymentResult.Success; + } + + /// + public async ValueTask TryPayAsync(ShopPurchase purchase) + { + if (purchase.Price.CoinType is not { } coinType + || purchase.Buyer.Account is not { } buyerAccount + || purchase.Seller.Account is not { } sellerAccount) + { + return ShopPaymentResult.Failed; + } + + await purchase.Buyer.ApplyPendingCashShopCoinGrantsAsync().ConfigureAwait(false); + + var buyerBalance = buyerAccount.GetCashShopCoins(coinType); + if (buyerBalance < purchase.Price.Amount) + { + return ShopPaymentResult.LackOfCurrency; + } + + var sellerBalance = sellerAccount.GetCashShopCoins(coinType); + if ((long)sellerBalance + purchase.Price.Amount > int.MaxValue) + { + return ShopPaymentResult.SellerOverflow; + } + + buyerAccount.SetCashShopCoins(coinType, buyerBalance - purchase.Price.Amount); + bool saved; + try + { + saved = await purchase.Buyer.SaveProgressAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + purchase.Buyer.Logger.LogError(ex, "Couldn't save the account of {buyer} after paying {price} {coinType} in the store of {seller}.", purchase.Buyer, purchase.Price.Amount, coinType, purchase.Seller); + saved = false; + } + + if (!saved) + { + buyerAccount.SetCashShopCoins(coinType, buyerBalance); + return ShopPaymentResult.Failed; + } + + // Read again, because the seller may have received coins in the meantime. + sellerAccount.SetCashShopCoins(coinType, (int)Math.Min((long)sellerAccount.GetCashShopCoins(coinType) + purchase.Price.Amount, int.MaxValue)); + purchase.Buyer.Logger.LogInformation("{buyer} paid {price} {coinType} to {seller} for an item of the player store.", purchase.Buyer, purchase.Price.Amount, coinType, purchase.Seller); + return ShopPaymentResult.Success; + } + + /// + public ValueTask NotifySellerAsync(ShopPurchase purchase) + { + return purchase.Seller.ShowLocalizedBlueMessageAsync( + nameof(PlayerMessage.PlayerShopCoinsReceived), + purchase.Price.Amount, + purchase.Seller.GetCurrencyName(purchase.Price)); + } + + /// + public ValueTask NotifyBuyerAsync(ShopPurchase purchase) + { + return purchase.Buyer.ShowLocalizedBlueMessageAsync( + nameof(PlayerMessage.PlayerShopCoinsPaid), + purchase.Price.Amount, + purchase.Buyer.GetCurrencyName(purchase.Price)); + } +} diff --git a/src/GameLogic/PlayerShopCurrencies/IShopCurrencyExchange.cs b/src/GameLogic/PlayerShopCurrencies/IShopCurrencyExchange.cs new file mode 100644 index 0000000000..5128b9cd8a --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/IShopCurrencyExchange.cs @@ -0,0 +1,44 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +/// +/// Pays the price of an item of a player shop of a specific . +/// +public interface IShopCurrencyExchange +{ + /// + /// Checks if the buyer has enough of the currency to pay the price, without changing anything. + /// + /// The buyer. + /// The price. + /// The result; , if the buyer can probably pay it. + ShopPaymentResult CheckFunds(Player buyer, ShopPrice price); + + /// + /// Tries to pay the price, i.e. to transfer it from the buyer to the seller. + /// If it fails, nothing is changed. + /// + /// + /// It's called while the store lock of the seller is held, before the sold item is moved. + /// + /// The purchase. + /// The result. + ValueTask TryPayAsync(ShopPurchase purchase); + + /// + /// Notifies the seller about the received payment, after it was successful. + /// + /// The purchase. + /// The value task. + ValueTask NotifySellerAsync(ShopPurchase purchase); + + /// + /// Notifies the buyer about the payment, after it was successful. + /// + /// The purchase. + /// The value task. + ValueTask NotifyBuyerAsync(ShopPurchase purchase); +} diff --git a/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs b/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs new file mode 100644 index 0000000000..dda342bfbf --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs @@ -0,0 +1,204 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.Views.Inventory; + +/// +/// Pays prices in items, e.g. jewels, by moving them from the inventory of the buyer to the one of the seller. +/// +/// +/// Only whole items are moved: a single item counts one piece, bundles of the jewel mix count their pieces. +/// Bundles are never split, and there is no change, so the buyer needs a combination of items which +/// matches the price exactly. No items are created or deleted: they are moved within the persistence +/// context of the , which also moves the sold item, so that +/// everything is saved together. +/// +public sealed class ItemShopCurrencyExchange : IShopCurrencyExchange +{ + /// + /// The number of pieces of a bundle per bundle level, which matches the jewel mix (ItemStackAction). + /// + private const int PiecesPerBundleLevel = 10; + + /// + public ShopPaymentResult CheckFunds(Player buyer, ShopPrice price) + { + var rows = GetPaymentRows(buyer, price.PriceItem); + return rows.Sum(r => r.Pieces) < price.Amount ? ShopPaymentResult.LackOfCurrency : ShopPaymentResult.Success; + } + + /// + public async ValueTask TryPayAsync(ShopPurchase purchase) + { + var rows = GetPaymentRows(purchase.Buyer, purchase.Price.PriceItem); + if (rows.Sum(r => r.Pieces) < purchase.Price.Amount) + { + return ShopPaymentResult.LackOfCurrency; + } + + if (SelectRows(rows, purchase.Price.Amount) is not { } selectedRows) + { + return ShopPaymentResult.ExactAmountNotPayable; + } + + if (purchase.Buyer.Inventory is not { } buyerInventory || purchase.Seller.Inventory is not { } sellerInventory) + { + return ShopPaymentResult.Failed; + } + + // The price items and bundles take one slot each. + var sellerSlots = sellerInventory.FreeSlots.Take(selectedRows.Count).ToList(); + if (sellerSlots.Count < selectedRows.Count) + { + return ShopPaymentResult.SellerInventoryFull; + } + + for (var i = 0; i < selectedRows.Count; i++) + { + var item = selectedRows[i].Item; + var buyerSlot = item.ItemSlot; + purchase.TradeContext.Attach(item); + await buyerInventory.RemoveItemAsync(item).ConfigureAwait(false); + item.ItemSlot = sellerSlots[i]; + if (!await sellerInventory.AddItemAsync(sellerSlots[i], item).ConfigureAwait(false)) + { + // Shouldn't happen, because the slot was free. Undo everything. + item.ItemSlot = buyerSlot; + await buyerInventory.AddItemAsync(buyerSlot, item).ConfigureAwait(false); + await this.UndoAsync(purchase, buyerInventory, sellerInventory).ConfigureAwait(false); + purchase.TradeContext.Detach(item); + purchase.Buyer.Logger.LogError("Couldn't move {item} to the free slot {slot} of the inventory of {seller}.", item, sellerSlots[i], purchase.Seller); + return ShopPaymentResult.Failed; + } + + purchase.PaymentItems.Add((item, buyerSlot)); + } + + return ShopPaymentResult.Success; + } + + /// + public async ValueTask NotifySellerAsync(ShopPurchase purchase) + { + foreach (var (item, _) in purchase.PaymentItems) + { + await purchase.Seller.InvokeViewPlugInAsync(p => p.ItemAppearAsync(item)).ConfigureAwait(false); + } + } + + /// + public async ValueTask NotifyBuyerAsync(ShopPurchase purchase) + { + foreach (var (_, buyerSlot) in purchase.PaymentItems) + { + await purchase.Buyer.InvokeViewPlugInAsync(p => p.RemoveItemAsync(buyerSlot)).ConfigureAwait(false); + } + } + + /// + /// Selects inventory rows whose pieces sum up exactly to the price. + /// + /// The available rows. + /// The price. + /// The selected rows; null, if no combination matches the price exactly. + internal static IReadOnlyList<(Item Item, int Pieces)>? SelectRows(IReadOnlyList<(Item Item, int Pieces)> rows, int price) + { + // A subset sum, because bundles can't be split: reachedBy[sum] is the index of the row which + // first reached the sum. The sums are iterated descending, so that each row is used at most once. + // Bigger rows come first, so that bundles are preferred over many single jewels. + var ordered = rows.OrderByDescending(r => r.Pieces).ToList(); + var reachedBy = new int[price + 1]; + Array.Fill(reachedBy, -1); + reachedBy[0] = int.MaxValue; + for (var rowIndex = 0; rowIndex < ordered.Count && reachedBy[price] < 0; rowIndex++) + { + var pieces = ordered[rowIndex].Pieces; + for (var sum = price; sum >= pieces; sum--) + { + if (reachedBy[sum] < 0 && reachedBy[sum - pieces] >= 0) + { + reachedBy[sum] = rowIndex; + } + } + } + + if (reachedBy[price] < 0) + { + return null; + } + + var result = new List<(Item Item, int Pieces)>(); + for (var sum = price; sum > 0;) + { + var row = ordered[reachedBy[sum]]; + result.Add(row); + sum -= row.Pieces; + } + + return result; + } + + /// + /// Gets the items of the inventory of the buyer with which the price can be paid. + /// + /// The buyer. + /// The definition of the items with which the price is paid. + /// The items with their number of pieces. + internal static IReadOnlyList<(Item Item, int Pieces)> GetPaymentRows(Player buyer, ItemDefinition? priceItem) + { + if (priceItem is null || buyer.Inventory is not { } inventory) + { + return []; + } + + var bundleDefinitions = buyer.GameContext.Configuration.JewelMixes + .Where(mix => ShopPrice.IsSameItem(mix.SingleJewel, priceItem)) + .Select(mix => mix.MixedJewel) + .OfType() + .ToList(); + + var rows = new List<(Item Item, int Pieces)>(); + foreach (var item in inventory.Items) + { + if (item.ItemSlot < InventoryConstants.EquippableSlotsCount + || item.Definition is not { IsTradable: true } definition + || item.ItemOptions.Any() + || item.ItemSetGroups.Any()) + { + continue; + } + + if (ShopPrice.IsSameItem(definition, priceItem) && item.Level == 0) + { + rows.Add((item, 1)); + } + else if (bundleDefinitions.Any(bundle => ShopPrice.IsSameItem(bundle, definition))) + { + rows.Add((item, (item.Level + 1) * PiecesPerBundleLevel)); + } + else + { + // not an item of the price + } + } + + return rows; + } + + private async ValueTask UndoAsync(ShopPurchase purchase, IStorage buyerInventory, IStorage sellerInventory) + { + foreach (var (item, buyerSlot) in purchase.PaymentItems) + { + await sellerInventory.RemoveItemAsync(item).ConfigureAwait(false); + item.ItemSlot = buyerSlot; + await buyerInventory.AddItemAsync(buyerSlot, item).ConfigureAwait(false); + purchase.TradeContext.Detach(item); + } + + purchase.PaymentItems.Clear(); + } +} diff --git a/src/GameLogic/PlayerShopCurrencies/ItemStorePriceExtensions.cs b/src/GameLogic/PlayerShopCurrencies/ItemStorePriceExtensions.cs new file mode 100644 index 0000000000..e611fb8de5 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ItemStorePriceExtensions.cs @@ -0,0 +1,35 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +/// +/// Extensions to get and set the price of an item in a player shop. +/// +public static class ItemStorePriceExtensions +{ + /// + /// Gets the price of the item in the player shop. + /// + /// The item. + /// The price; null, if the item has no price. + public static ShopPrice? GetStorePrice(this Item item) + { + return item.StorePrice is { } amount + ? new ShopPrice(amount, item.StorePriceItemDefinition, item.StorePriceCoinType) + : null; + } + + /// + /// Sets or removes the price of the item in the player shop. + /// + /// The item. + /// The price; null removes the price. + public static void SetStorePrice(this Item item, ShopPrice? price) + { + item.StorePrice = price?.Amount; + item.StorePriceItemDefinition = price?.PriceItem; + item.StorePriceCoinType = price?.CoinType; + } +} diff --git a/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesConfiguration.cs b/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesConfiguration.cs new file mode 100644 index 0000000000..dc1dc7d7b1 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesConfiguration.cs @@ -0,0 +1,74 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using MUnique.OpenMU.DataModel.Configuration.Items; + +/// +/// The configuration of the . +/// +/// +/// Zen is always allowed. The account coins have one flag each, because the admin panel has no editor for +/// collections of enum values. +/// +public class PlayerShopCurrenciesConfiguration +{ + /// + /// The default of . + /// + internal const int DefaultMaximumItemAmount = 1000; + + /// + /// The default of . + /// + internal const int DefaultMaximumCoinAmount = 1_000_000; + + /// + /// Gets or sets the items with which prices can be paid, e.g. jewels. + /// + /// + /// A new database gets the Jewels of Bless, Soul, Chaos, Creation and Life. The default configuration which + /// is created for an existing database can't reference items, so they have to be added when activating the plugin. + /// + public ICollection PriceItems { get; set; } = new List(); + + /// + /// Gets or sets a value indicating whether prices in WCoin (C) are allowed. + /// + /// + /// The account coins are usually bought with real money, so trading them has to be allowed explicitly. + /// + public bool IsWCoinCAllowed { get; set; } + + /// + /// Gets or sets a value indicating whether prices in WCoin (P) are allowed. + /// + /// + /// The account coins are usually bought with real money, so trading them has to be allowed explicitly. + /// + public bool IsWCoinPAllowed { get; set; } + + /// + /// Gets or sets a value indicating whether prices in Goblin Points are allowed. + /// + public bool IsGoblinPointsAllowed { get; set; } + + /// + /// Gets or sets the maximum number of item pieces of a price. + /// + /// + /// The extended client protocol transfers the amount in a 16-bit field, so values above 65535 are treated as 65535. + /// A value of 0 or less is treated as the default value. + /// + public int MaximumItemAmount { get; set; } = DefaultMaximumItemAmount; + + /// + /// Gets or sets the maximum amount of account coins of a price. + /// + /// + /// A value of 0 or less is treated as the default value. + /// + public int MaximumCoinAmount { get; set; } = DefaultMaximumCoinAmount; +} diff --git a/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesFeaturePlugIn.cs b/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesFeaturePlugIn.cs new file mode 100644 index 0000000000..f4a1c38667 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesFeaturePlugIn.cs @@ -0,0 +1,45 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.PlugIns; + +/// +/// The feature plugin which allows players to define the prices of the items in their +/// personal store in other currencies than zen, e.g. in jewels or account coins. +/// When it's deactivated, only zen prices are possible. +/// +/// +/// Only the open source client offers the currencies in its user interface. With the original client, +/// the prices can be set with a chat command (see ), but it can't +/// see or buy items with other prices than zen. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.PlayerShopCurrenciesFeaturePlugIn_Name), Description = nameof(PlugInResources.PlayerShopCurrenciesFeaturePlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("FF970BE2-DF92-4371-9FE0-C16E9DCA7AB9")] +public class PlayerShopCurrenciesFeaturePlugIn : IFeaturePlugIn, IDisabledByDefault, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration +{ + /// + public PlayerShopCurrenciesConfiguration? Configuration { get; set; } + + /// + /// Gets the configuration of the player shop currencies. + /// + /// The game context. + /// The configuration; null, if the feature is deactivated. + public static PlayerShopCurrenciesConfiguration? GetConfiguration(IGameContext gameContext) + { + return gameContext.FeaturePlugIns.GetPlugIn() is { } plugIn + ? plugIn.Configuration ?? new PlayerShopCurrenciesConfiguration() + : null; + } + + /// + public object CreateDefaultConfig() + { + return new PlayerShopCurrenciesConfiguration(); + } +} diff --git a/src/GameLogic/PlayerShopCurrencies/ShopPaymentResult.cs b/src/GameLogic/PlayerShopCurrencies/ShopPaymentResult.cs new file mode 100644 index 0000000000..483ebbc902 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ShopPaymentResult.cs @@ -0,0 +1,42 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +/// +/// The result of paying the price of an item of a player shop. +/// +public enum ShopPaymentResult +{ + /// + /// The price has been paid. + /// + Success, + + /// + /// The buyer doesn't have enough of the currency. + /// + LackOfCurrency, + + /// + /// The buyer has enough jewels, but they can't pay the exact price, because bundles are not split + /// and there is no change. + /// + ExactAmountNotPayable, + + /// + /// The seller can't receive the payment, because it would exceed the maximum amount of the currency. + /// + SellerOverflow, + + /// + /// The seller can't receive the payment, because the inventory has not enough space for the jewels. + /// + SellerInventoryFull, + + /// + /// The payment failed for another reason, e.g. because the progress of the buyer couldn't be saved. + /// + Failed, +} diff --git a/src/GameLogic/PlayerShopCurrencies/ShopPrice.cs b/src/GameLogic/PlayerShopCurrencies/ShopPrice.cs new file mode 100644 index 0000000000..036b4fca35 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ShopPrice.cs @@ -0,0 +1,51 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using MUnique.OpenMU.DataModel.Configuration.Items; + +/// +/// The price of an item in a player shop. +/// +/// The amount: zen, pieces of the or account coins. +/// The definition of the items with which the price is paid; null, if it's not paid with items. +/// The account coin type with which the price is paid; null, if it's not paid with account coins. +public readonly record struct ShopPrice(int Amount, ItemDefinition? PriceItem = null, CashShopCoinType? CoinType = null) +{ + /// + /// Gets the kind of the price. + /// + public ShopPriceKind Kind => this.PriceItem is not null + ? ShopPriceKind.Item + : this.CoinType is not null ? ShopPriceKind.AccountCoin : ShopPriceKind.Zen; + + /// + /// Determines whether this price is paid with the same items or coins as another one, regardless of the amount. + /// + /// The other price. + /// true, if both prices are paid the same way. + public bool HasSameCurrency(ShopPrice other) + { + return this.Kind == other.Kind + && this.CoinType == other.CoinType + && IsSameItem(this.PriceItem, other.PriceItem); + } + + /// + /// Determines whether two item definitions describe the same item, by group and number. + /// + /// The left definition. + /// The right definition. + /// true, if both describe the same item. + internal static bool IsSameItem(ItemDefinition? left, ItemDefinition? right) + { + if (left is null || right is null) + { + return left is null && right is null; + } + + return left.Group == right.Group && left.Number == right.Number; + } +} diff --git a/src/GameLogic/PlayerShopCurrencies/ShopPriceKind.cs b/src/GameLogic/PlayerShopCurrencies/ShopPriceKind.cs new file mode 100644 index 0000000000..2074ec2294 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ShopPriceKind.cs @@ -0,0 +1,26 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +/// +/// The kind of a , which decides how it's paid. +/// +public enum ShopPriceKind +{ + /// + /// The price is paid with the zen of the inventory. + /// + Zen, + + /// + /// The price is paid with items of the inventory, e.g. jewels. + /// + Item, + + /// + /// The price is paid with coins of the account. + /// + AccountCoin, +} diff --git a/src/GameLogic/PlayerShopCurrencies/ShopPrices.cs b/src/GameLogic/PlayerShopCurrencies/ShopPrices.cs new file mode 100644 index 0000000000..10b319c647 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ShopPrices.cs @@ -0,0 +1,257 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using System.Globalization; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.Views.PlayerShop; + +/// +/// Functions about the prices of items in player shops: which prices are allowed, how they're paid, shown and +/// named in chat commands. +/// +public static class ShopPrices +{ + /// + /// The maximum amount of item pieces which can be transferred to the client. + /// + /// + /// The extended client protocol transfers the amount of required items in a 16-bit field. A higher price + /// would be shown differently than it's charged. + /// + internal const int MaximumTransferableItemAmount = ushort.MaxValue; + + /// + /// The separator between group and number of a price item in chat commands, e.g. "14/13". + /// + private const char ItemIdentifierSeparator = '/'; + + private static readonly ZenShopCurrencyExchange ZenExchange = new(); + private static readonly ItemShopCurrencyExchange ItemExchange = new(); + private static readonly AccountCoinShopCurrencyExchange CoinExchange = new(); + + private static readonly IReadOnlyList<(CashShopCoinType CoinType, string NameResourceKey, string[] ChatAliases, Func IsAllowed)> Coins = + [ + (CashShopCoinType.WCoinC, nameof(PlayerMessage.ShopCurrencyWCoinC), ["wcoinc", "wcc"], c => c.IsWCoinCAllowed), + (CashShopCoinType.WCoinP, nameof(PlayerMessage.ShopCurrencyWCoinP), ["wcoinp", "wcp"], c => c.IsWCoinPAllowed), + (CashShopCoinType.GoblinPoints, nameof(PlayerMessage.ShopCurrencyGoblinPoints), ["goblin", "gp"], c => c.IsGoblinPointsAllowed), + ]; + + /// + /// Short chat names of the jewels, which are commonly used as price items. + /// Other items are named by group and number, e.g. "14/42". + /// + private static readonly IReadOnlyList<(ItemIdentifier Item, string[] ChatAliases)> JewelChatAliases = + [ + (ItemConstants.JewelOfBless, ["bless", "job"]), + (ItemConstants.JewelOfSoul, ["soul", "jos"]), + (ItemConstants.JewelOfChaos, ["chaos", "joc"]), + (ItemConstants.JewelOfCreation, ["creation", "jocr"]), + (ItemConstants.JewelOfLife, ["life", "jol"]), + ]; + + /// + /// Determines whether the price can currently be used in player shops. + /// + /// The game context. + /// The price. + /// true, if the price can be used. + public static bool IsAllowed(IGameContext gameContext, ShopPrice price) + { + if (price.Kind == ShopPriceKind.Zen) + { + return true; + } + + return PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(gameContext) is { } configuration + && (price.Kind == ShopPriceKind.Item + ? configuration.PriceItems.Any(item => ShopPrice.IsSameItem(item, price.PriceItem)) + : Coins.Any(coin => coin.CoinType == price.CoinType && coin.IsAllowed(configuration))); + } + + /// + /// Gets the items with which prices can currently be paid. + /// + /// The game context. + /// The item definitions; empty, if the feature is deactivated. + public static IReadOnlyList GetAllowedPriceItems(IGameContext gameContext) + { + return PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(gameContext)?.PriceItems.ToList() ?? []; + } + + /// + /// Gets the account coin types with which prices can currently be paid. + /// + /// The game context. + /// The coin types; empty, if the feature is deactivated. + public static IReadOnlyList GetAllowedCoinTypes(IGameContext gameContext) + { + if (PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(gameContext) is not { } configuration) + { + return []; + } + + return Coins.Where(coin => coin.IsAllowed(configuration)).Select(coin => coin.CoinType).ToList(); + } + + /// + /// Gets the maximum amount of a price of the specified kind. + /// + /// The game context. + /// The kind of the price. + /// The maximum amount. + public static int GetMaximumAmount(IGameContext gameContext, ShopPriceKind kind) + { + var configuration = PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(gameContext) ?? new PlayerShopCurrenciesConfiguration(); + return kind switch + { + ShopPriceKind.Item => Math.Min( + configuration.MaximumItemAmount > 0 ? configuration.MaximumItemAmount : PlayerShopCurrenciesConfiguration.DefaultMaximumItemAmount, + MaximumTransferableItemAmount), + ShopPriceKind.AccountCoin => configuration.MaximumCoinAmount > 0 ? configuration.MaximumCoinAmount : PlayerShopCurrenciesConfiguration.DefaultMaximumCoinAmount, + _ => int.MaxValue, + }; + } + + /// + /// Tries to parse the currency of a price of a chat command: "zen", the name of an account coin or a jewel, + /// or the group and number of an item, e.g. "14/13". + /// + /// The game context, which defines the items. + /// The text. + /// The amount of the price. + /// The parsed price. + /// true, if the text describes a currency. + public static bool TryParse(IGameContext gameContext, string? text, int amount, out ShopPrice price) + { + price = new ShopPrice(amount); + if (string.IsNullOrWhiteSpace(text) || string.Equals(text.Trim(), "zen", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + var trimmed = text.Trim(); + if (Coins.FirstOrDefault(coin => coin.ChatAliases.Contains(trimmed, StringComparer.OrdinalIgnoreCase)) is { NameResourceKey: not null } namedCoin) + { + price = new ShopPrice(amount, CoinType: namedCoin.CoinType); + return true; + } + + if (GetItemIdentifier(trimmed) is not { } identifier + || gameContext.Configuration.Items.FirstOrDefault(item => item.Group == identifier.Group && item.Number == identifier.Number) is not { } definition) + { + return false; + } + + price = new ShopPrice(amount, definition); + return true; + } + + /// + /// Gets the chat names of the currencies which are currently allowed, e.g. to show them to the player. + /// + /// The game context. + /// The chat names. + public static IEnumerable GetAllowedChatNames(IGameContext gameContext) + { + yield return "zen"; + foreach (var item in GetAllowedPriceItems(gameContext)) + { + yield return JewelChatAliases.FirstOrDefault(jewel => jewel.Item == new ItemIdentifier(item.Number, item.Group)).ChatAliases?[0] + ?? FormattableString.Invariant($"{item.Group}{ItemIdentifierSeparator}{item.Number}"); + } + + foreach (var coinType in GetAllowedCoinTypes(gameContext)) + { + yield return Coins.First(coin => coin.CoinType == coinType).ChatAliases[0]; + } + } + + /// + /// Gets the localized name of the currency of a price, e.g. "Jewel of Bless" or "Goblin Points". + /// + /// The player to which the name is shown. + /// The price. + /// The localized name. + public static string GetCurrencyName(this Player player, ShopPrice price) + { + switch (price.Kind) + { + case ShopPriceKind.Item: + using (CultureHelper.SetTemporaryCulture(player.Culture)) + { + return price.PriceItem!.GetNameForLevel(0); + } + + case ShopPriceKind.AccountCoin: + return player.GetLocalizedMessage(Coins.First(coin => coin.CoinType == price.CoinType).NameResourceKey); + default: + return player.GetLocalizedMessage(nameof(PlayerMessage.ShopCurrencyZen)); + } + } + + /// + /// Gets the exchange which pays prices of the specified kind. + /// + /// The kind of the price. + /// The exchange. + public static IShopCurrencyExchange GetExchange(ShopPriceKind kind) + { + return kind switch + { + ShopPriceKind.Item => ItemExchange, + ShopPriceKind.AccountCoin => CoinExchange, + _ => ZenExchange, + }; + } + + /// + /// Shows the allowed prices to the player, if the feature is activated. + /// + /// + /// Nothing is sent when the feature is deactivated, so that the protocol stays the same as without it. + /// + /// The player. + /// The value task. + public static async ValueTask ShowAllowedPricesAsync(this Player player) + { + if (PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(player.GameContext) is null) + { + return; + } + + var priceItems = GetAllowedPriceItems(player.GameContext); + var coinTypes = GetAllowedCoinTypes(player.GameContext); + await player.InvokeViewPlugInAsync(p => p.ShowShopCurrenciesAsync(priceItems, coinTypes)).ConfigureAwait(false); + } + + /// + /// Determines whether the client of the player can show and buy items with other prices than zen. + /// + /// The player. + /// true, if the client supports other prices. + public static bool SupportsShopCurrencies(this Player player) + { + return player.ViewPlugIns.GetPlugIn() is not null; + } + + private static ItemIdentifier? GetItemIdentifier(string text) + { + if (JewelChatAliases.FirstOrDefault(jewel => jewel.ChatAliases.Contains(text, StringComparer.OrdinalIgnoreCase)) is { ChatAliases: not null } jewel) + { + return jewel.Item; + } + + var parts = text.Split(ItemIdentifierSeparator); + if (parts.Length == 2 + && byte.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var group) + && short.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var number)) + { + return new ItemIdentifier(number, group); + } + + return null; + } +} diff --git a/src/GameLogic/PlayerShopCurrencies/ShopPurchase.cs b/src/GameLogic/PlayerShopCurrencies/ShopPurchase.cs new file mode 100644 index 0000000000..a17d140e96 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ShopPurchase.cs @@ -0,0 +1,26 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using MUnique.OpenMU.Persistence; + +/// +/// The purchase of an item from a player shop, for which the price is paid. +/// +/// The buyer. +/// The seller, which owns the shop. +/// The price. +/// +/// The persistence context in which the sold item is moved from the seller to the buyer. +/// Items which are paid with are moved in it, too, so that everything is saved together. +/// +public sealed record ShopPurchase(Player Buyer, Player Seller, ShopPrice Price, IContext TradeContext) +{ + /// + /// Gets the items which the buyer paid with, together with their previous slots in the inventory of the buyer. + /// They are already moved into the inventory of the seller and attached to the . + /// + public IList<(Item Item, byte BuyerSlot)> PaymentItems { get; } = new List<(Item Item, byte BuyerSlot)>(); +} diff --git a/src/GameLogic/PlayerShopCurrencies/ShowShopCurrenciesAfterEnterWorldPlugIn.cs b/src/GameLogic/PlayerShopCurrencies/ShowShopCurrenciesAfterEnterWorldPlugIn.cs new file mode 100644 index 0000000000..e1b44b9906 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ShowShopCurrenciesAfterEnterWorldPlugIn.cs @@ -0,0 +1,30 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.PlugIns; +using MUnique.OpenMU.PlugIns; + +/// +/// Shows the items and account coins which can be used for prices in player shops to the player when it enters +/// the game, so that the client offers them when the player sets the prices of its shop. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.ShowShopCurrenciesAfterEnterWorldPlugIn_Name), Description = nameof(PlugInResources.ShowShopCurrenciesAfterEnterWorldPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("C9DF78E0-AC93-42A0-AD0C-2A83165A6522")] +public class ShowShopCurrenciesAfterEnterWorldPlugIn : IPlayerStateChangedPlugIn +{ + /// + public async ValueTask PlayerStateChangedAsync(Player player, State previousState, State currentState) + { + if (previousState != PlayerState.CharacterSelection || currentState != PlayerState.EnteredWorld) + { + return; + } + + await player.ShowAllowedPricesAsync().ConfigureAwait(false); + } +} diff --git a/src/GameLogic/PlayerShopCurrencies/ZenShopCurrencyExchange.cs b/src/GameLogic/PlayerShopCurrencies/ZenShopCurrencyExchange.cs new file mode 100644 index 0000000000..733d2a3579 --- /dev/null +++ b/src/GameLogic/PlayerShopCurrencies/ZenShopCurrencyExchange.cs @@ -0,0 +1,48 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +using MUnique.OpenMU.GameLogic.Views.Inventory; + +/// +/// Pays prices in zen, by transferring the zen of the inventory of the buyer to the one of the seller. +/// +public sealed class ZenShopCurrencyExchange : IShopCurrencyExchange +{ + /// + public ShopPaymentResult CheckFunds(Player buyer, ShopPrice price) + { + return buyer.Money < price.Amount ? ShopPaymentResult.LackOfCurrency : ShopPaymentResult.Success; + } + + /// + public ValueTask TryPayAsync(ShopPurchase purchase) + { + if (!purchase.Buyer.TryRemoveMoney(purchase.Price.Amount)) + { + return ValueTask.FromResult(ShopPaymentResult.LackOfCurrency); + } + + if (!purchase.Seller.TryAddMoney(purchase.Price.Amount)) + { + purchase.Buyer.TryAddMoney(purchase.Price.Amount); + return ValueTask.FromResult(ShopPaymentResult.SellerOverflow); + } + + return ValueTask.FromResult(ShopPaymentResult.Success); + } + + /// + public ValueTask NotifySellerAsync(ShopPurchase purchase) + { + return purchase.Seller.InvokeViewPlugInAsync(p => p.UpdateMoneyAsync()); + } + + /// + public ValueTask NotifyBuyerAsync(ShopPurchase purchase) + { + return purchase.Buyer.InvokeViewPlugInAsync(p => p.UpdateMoneyAsync()); + } +} diff --git a/src/GameLogic/PlugIns/ChatCommands/Arguments/ShopPriceChatCommandArgs.cs b/src/GameLogic/PlugIns/ChatCommands/Arguments/ShopPriceChatCommandArgs.cs new file mode 100644 index 0000000000..6390c7d017 --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/Arguments/ShopPriceChatCommandArgs.cs @@ -0,0 +1,31 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands.Arguments; + +/// +/// Arguments used by . +/// +public class ShopPriceChatCommandArgs : ArgumentsBase +{ + /// + /// Gets or sets the position of the item in the store (1 to 32), or its inventory slot. + /// When it's 0, the prices of the store are listed. + /// + [Argument("slot", false)] + public int Slot { get; set; } + + /// + /// Gets or sets the price amount. A value of 0 removes the price. + /// + [Argument("amount", false)] + public int Amount { get; set; } = -1; + + /// + /// Gets or sets the currency of the price, e.g. "zen", "bless", "gp" or the group and number of an item like "14/13". + /// When it's empty, it's zen. + /// + [Argument("currency", false)] + public string? Currency { get; set; } +} diff --git a/src/GameLogic/PlugIns/ChatCommands/ShopPriceChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/ShopPriceChatCommandPlugIn.cs new file mode 100644 index 0000000000..4355757468 --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/ShopPriceChatCommandPlugIn.cs @@ -0,0 +1,133 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; +using MUnique.OpenMU.GameLogic.PlugIns.ChatCommands.Arguments; +using MUnique.OpenMU.GameLogic.Views.PlayerShop; +using MUnique.OpenMU.PlugIns; + +/// +/// A chat command plugin which sets the prices of the items in the own player store, in any of the enabled +/// currencies (see ). Without arguments, it lists the prices. +/// The currency is "zen", an account coin ("wcc", "wcp", "gp"), a jewel ("bless", "soul", ...) or the group and +/// number of an item, e.g. "14/13". +/// +/// +/// It works with every client, so also players of the original client can set prices in other currencies than zen. +/// Their client doesn't show these prices, though. +/// +[Guid("A5A3B2CE-7D58-43D6-81F3-804F078F0990")] +[PlugIn] +[Display(Name = nameof(PlugInResources.ShopPriceChatCommandPlugIn_Name), Description = nameof(PlugInResources.ShopPriceChatCommandPlugIn_Description), ResourceType = typeof(PlugInResources))] +[ChatCommandHelp(Command, typeof(ShopPriceChatCommandArgs), CharacterStatus.Normal)] +public class ShopPriceChatCommandPlugIn : ChatCommandPlugInBase, IDisabledByDefault +{ + private const string Command = "/shopprice"; + + private readonly SetItemPriceAction _setItemPriceAction = new(); + + /// + public override string Key => Command; + + /// + public override CharacterStatus MinCharacterStatusRequirement => CharacterStatus.Normal; + + /// + protected override async ValueTask DoHandleCommandAsync(Player player, ShopPriceChatCommandArgs arguments) + { + if (player.ShopStorage is not { } shopStorage) + { + return; + } + + if (arguments.Slot == 0) + { + await ShowPricesAsync(player, shopStorage).ConfigureAwait(false); + return; + } + + if (GetStoreSlot(arguments.Slot) is not { } slot || arguments.Amount < 0) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopPriceCommandUsage), Command).ConfigureAwait(false); + return; + } + + if (!ShopPrices.TryParse(player.GameContext, arguments.Currency, arguments.Amount, out var price)) + { + var allowedCurrencies = ShopPrices.GetAllowedChatNames(player.GameContext); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopUnknownCurrency), arguments.Currency, string.Join(", ", allowedCurrencies)).ConfigureAwait(false); + return; + } + + if (shopStorage.StoreOpen) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopCloseToChangePrices)).ConfigureAwait(false); + return; + } + + var position = slot - InventoryConstants.FirstStoreItemSlotIndex + 1; + var result = await this._setItemPriceAction.TrySetPriceAsync(player, slot, price).ConfigureAwait(false); + switch (result) + { + case ItemPriceResult.Success when arguments.Amount > 0: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopPriceSet), position, arguments.Amount, player.GetCurrencyName(price)).ConfigureAwait(false); + break; + case ItemPriceResult.Success: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopPriceRemoved), position).ConfigureAwait(false); + break; + case ItemPriceResult.ItemNotFound: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopPriceItemNotFound), position).ConfigureAwait(false); + break; + case ItemPriceResult.CharacterLevelTooLow: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopPriceLevelTooLow)).ConfigureAwait(false); + break; + default: + // The action already showed a message about the reason. + break; + } + } + + private static byte? GetStoreSlot(int slotArgument) + { + if (slotArgument >= 1 && slotArgument <= InventoryConstants.StoreSize) + { + return (byte)(InventoryConstants.FirstStoreItemSlotIndex + slotArgument - 1); + } + + if (slotArgument >= InventoryConstants.FirstStoreItemSlotIndex + && slotArgument < InventoryConstants.FirstStoreItemSlotIndex + InventoryConstants.StoreSize) + { + return (byte)slotArgument; + } + + return null; + } + + private static async ValueTask ShowPricesAsync(Player player, IShopStorage shopStorage) + { + var items = shopStorage.Items.OrderBy(i => i.ItemSlot).ToList(); + if (items.Count == 0) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopIsEmpty)).ConfigureAwait(false); + return; + } + + foreach (var item in items) + { + var position = item.ItemSlot - InventoryConstants.FirstStoreItemSlotIndex + 1; + if (item.GetStorePrice() is { } price) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopPriceListEntry), position, item.ToString(player.Culture), price.Amount, player.GetCurrencyName(price)).ConfigureAwait(false); + } + else + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerShopPriceListEntryWithoutPrice), position, item.ToString(player.Culture)).ConfigureAwait(false); + } + } + } +} diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index a50d4c7acf..6323f628bd 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -1246,6 +1246,150 @@ public static string KillAllMonstersFormat { } } + /// + /// Looks up a localized string similar to Close your store to change its prices.. + /// + public static string PlayerShopCloseToChangePrices { + get { + return ResourceManager.GetString("PlayerShopCloseToChangePrices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You paid {0} {1}.. + /// + public static string PlayerShopCoinsPaid { + get { + return ResourceManager.GetString("PlayerShopCoinsPaid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You received {0} {1} for an item of your store.. + /// + public static string PlayerShopCoinsReceived { + get { + return ResourceManager.GetString("PlayerShopCoinsReceived", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prices in {0} are not allowed in personal stores.. + /// + public static string PlayerShopCurrencyNotAllowed { + get { + return ResourceManager.GetString("PlayerShopCurrencyNotAllowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your store is empty.. + /// + public static string PlayerShopIsEmpty { + get { + return ResourceManager.GetString("PlayerShopIsEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your items can't pay exactly {0} {1}. Bundles are not split, and there is no change.. + /// + public static string PlayerShopItemPriceNotPayableExactly { + get { + return ResourceManager.GetString("PlayerShopItemPriceNotPayableExactly", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need {0} {1} to buy this item.. + /// + public static string PlayerShopNotEnoughCurrency { + get { + return ResourceManager.GetString("PlayerShopNotEnoughCurrency", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Usage: {0} [position 1-32] [amount] [currency]. Without arguments, the prices of your store are listed.. + /// + public static string PlayerShopPriceCommandUsage { + get { + return ResourceManager.GetString("PlayerShopPriceCommandUsage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is no item at position {0} of your store.. + /// + public static string PlayerShopPriceItemNotFound { + get { + return ResourceManager.GetString("PlayerShopPriceItemNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your character level is too low to sell items in a personal store.. + /// + public static string PlayerShopPriceLevelTooLow { + get { + return ResourceManager.GetString("PlayerShopPriceLevelTooLow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to #{0} {1} - {2} {3}. + /// + public static string PlayerShopPriceListEntry { + get { + return ResourceManager.GetString("PlayerShopPriceListEntry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to #{0} {1} - no price. + /// + public static string PlayerShopPriceListEntryWithoutPrice { + get { + return ResourceManager.GetString("PlayerShopPriceListEntryWithoutPrice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The price of the item at position {0} has been removed.. + /// + public static string PlayerShopPriceRemoved { + get { + return ResourceManager.GetString("PlayerShopPriceRemoved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The price of the item at position {0} is now {1} {2}.. + /// + public static string PlayerShopPriceSet { + get { + return ResourceManager.GetString("PlayerShopPriceSet", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The price can be at most {0} {1}.. + /// + public static string PlayerShopPriceTooHigh { + get { + return ResourceManager.GetString("PlayerShopPriceTooHigh", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown currency '{0}'. Available: {1}.. + /// + public static string PlayerShopUnknownCurrency { + get { + return ResourceManager.GetString("PlayerShopUnknownCurrency", resourceCulture); + } + } + /// /// Looks up a localized string similar to The battle against Selupan has failed. The Hatchery Gates will open in {0}.. /// @@ -1327,6 +1471,42 @@ public static string RaklionSelupanDefeated { } } + /// + /// Looks up a localized string similar to Goblin Points. + /// + public static string ShopCurrencyGoblinPoints { + get { + return ResourceManager.GetString("ShopCurrencyGoblinPoints", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WCoin (C). + /// + public static string ShopCurrencyWCoinC { + get { + return ResourceManager.GetString("ShopCurrencyWCoinC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WCoin (P). + /// + public static string ShopCurrencyWCoinP { + get { + return ResourceManager.GetString("ShopCurrencyWCoinP", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Zen. + /// + public static string ShopCurrencyZen { + get { + return ResourceManager.GetString("ShopCurrencyZen", resourceCulture); + } + } + /// /// Looks up a localized string similar to You need to be inside a mini game event to skip the waiting time.. /// diff --git a/src/GameLogic/Properties/PlayerMessage.de.resx b/src/GameLogic/Properties/PlayerMessage.de.resx index bd9ea59ae8..7527f006a2 100644 --- a/src/GameLogic/Properties/PlayerMessage.de.resx +++ b/src/GameLogic/Properties/PlayerMessage.de.resx @@ -925,4 +925,64 @@ Inventarerweiterungen: {18} Sie können jetzt einen Charakter der Klasse {0} erstellen. + + Zen + + + WCoin (C) + + + WCoin (P) + + + Goblin-Punkte + + + Preise in {0} sind in persönlichen Läden nicht erlaubt. + + + Der Preis darf höchstens {0} {1} betragen. + + + Sie benötigen {0} {1}, um diesen Gegenstand zu kaufen. + + + Ihre Gegenstände ergeben nicht genau {0} {1}. Bündel werden nicht aufgeteilt, und es gibt kein Wechselgeld. + + + Sie haben {0} {1} für einen Gegenstand Ihres Ladens erhalten. + + + Sie haben {0} {1} bezahlt. + + + Verwendung: {0} [Position 1-32] [Betrag] [Währung]. Ohne Argumente werden die Preise Ihres Ladens aufgelistet. + + + Unbekannte Währung '{0}'. Verfügbar: {1}. + + + Schließen Sie Ihren Laden, um die Preise zu ändern. + + + Der Preis des Gegenstands an Position {0} beträgt jetzt {1} {2}. + + + Der Preis des Gegenstands an Position {0} wurde entfernt. + + + An Position {0} Ihres Ladens befindet sich kein Gegenstand. + + + Ihre Charakterstufe ist zu niedrig, um Gegenstände in einem persönlichen Laden zu verkaufen. + + + Ihr Laden ist leer. + + + #{0} {1} - {2} {3} + + + #{0} {1} - kein Preis + diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index 9d2c868158..60602f8c4e 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -925,4 +925,64 @@ Inventory Extensions: {18} You can now create a character of the class {0}. + + Zen + + + WCoin (C) + + + WCoin (P) + + + Goblin Points + + + Prices in {0} are not allowed in personal stores. + + + The price can be at most {0} {1}. + + + You need {0} {1} to buy this item. + + + Your items can't pay exactly {0} {1}. Bundles are not split, and there is no change. + + + You received {0} {1} for an item of your store. + + + You paid {0} {1}. + + + Usage: {0} [position 1-32] [amount] [currency]. Without arguments, the prices of your store are listed. + + + Unknown currency '{0}'. Available: {1}. + + + Close your store to change its prices. + + + The price of the item at position {0} is now {1} {2}. + + + The price of the item at position {0} has been removed. + + + There is no item at position {0} of your store. + + + Your character level is too low to sell items in a personal store. + + + Your store is empty. + + + #{0} {1} - {2} {3} + + + #{0} {1} - no price + diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index e9ed4fb979..8a0fc0d25c 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -2364,6 +2364,24 @@ public static string PlayerLosesMoneyAfterDeathPlugInConfiguration_VaultMoneyLos } } + /// + /// Looks up a localized string similar to Allows players to set the prices of the items in their personal store in configured items like jewels, or in account coins, too. Only the open source client shows these prices; with other clients, they can be set by chat command.. + /// + public static string PlayerShopCurrenciesFeaturePlugIn_Description { + get { + return ResourceManager.GetString("PlayerShopCurrenciesFeaturePlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Player shop currencies. + /// + public static string PlayerShopCurrenciesFeaturePlugIn_Name { + get { + return ResourceManager.GetString("PlayerShopCurrenciesFeaturePlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Handles the pollution skill (book of lagle) of the summoner class. Based on a chance, it may push the targets 2 squares away from the attacker.. /// @@ -2969,6 +2987,24 @@ public static string ShieldRecoveryHiatusPlugIn_Name { } } + /// + /// Looks up a localized string similar to Sets the price of an item of the own personal store in one of the enabled currencies, e.g. '/shopprice 1 10 bless'. Without arguments, it lists the prices.. + /// + public static string ShopPriceChatCommandPlugIn_Description { + get { + return ResourceManager.GetString("ShopPriceChatCommandPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shop price chat command. + /// + public static string ShopPriceChatCommandPlugIn_Name { + get { + return ResourceManager.GetString("ShopPriceChatCommandPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Handles the chat command '/fireworks <x> <y>'. Shows an fireworks effect at the specified coordinates.. /// @@ -3023,6 +3059,24 @@ public static string ShowNpcIdsChatCommand_ShowNpcIdsChatCommand_Name { } } + /// + /// Looks up a localized string similar to Tells the client which currencies can be used for the prices of personal stores, when the player enters the game.. + /// + public static string ShowShopCurrenciesAfterEnterWorldPlugIn_Description { + get { + return ResourceManager.GetString("ShowShopCurrenciesAfterEnterWorldPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show player shop currencies after entering the game. + /// + public static string ShowShopCurrenciesAfterEnterWorldPlugIn_Name { + get { + return ResourceManager.GetString("ShowShopCurrenciesAfterEnterWorldPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Handles the chat command '/xmasfireworks <x> <y>'. Shows an christmas fireworks effect at the specified coordinates.. /// diff --git a/src/GameLogic/Properties/PlugInResources.de.resx b/src/GameLogic/Properties/PlugInResources.de.resx index 77aa1bfff5..3d97f68bc2 100644 --- a/src/GameLogic/Properties/PlugInResources.de.resx +++ b/src/GameLogic/Properties/PlugInResources.de.resx @@ -1815,4 +1815,22 @@ Schaltet die Klasse Rage Fighter für das Konto frei, wenn ein Spieler die Rage-Fighter-Charakterkarte aus dem Cash-Shop-Lager verwendet. + + Währungen für persönliche Läden + + + Erlaubt Spielern, die Preise der Gegenstände in ihrem persönlichen Laden auch in konfigurierten Gegenständen wie Juwelen oder in Konto-Münzen festzulegen. Nur der Open-Source-Client zeigt diese Preise an; bei anderen Clients können sie per Chat-Befehl festgelegt werden. + + + Währungen für persönliche Läden nach dem Betreten des Spiels anzeigen + + + Teilt dem Client beim Betreten des Spiels mit, welche Währungen für die Preise persönlicher Läden verwendet werden können. + + + Chatbefehl Ladenpreis + + + Legt den Preis eines Gegenstands des eigenen persönlichen Ladens in einer der aktivierten Währungen fest, z. B. '/shopprice 1 10 bless'. Ohne Argumente werden die Preise aufgelistet. + diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index 56a9ea82de..91bd3bb568 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -1595,4 +1595,22 @@ Publish ancient item drops + + Player shop currencies + + + Allows players to set the prices of the items in their personal store in configured items like jewels, or in account coins, too. Only the open source client shows these prices; with other clients, they can be set by chat command. + + + Show player shop currencies after entering the game + + + Tells the client which currencies can be used for the prices of personal stores, when the player enters the game. + + + Shop price chat command + + + Sets the price of an item of the own personal store in one of the enabled currencies, e.g. '/shopprice 1 10 bless'. Without arguments, it lists the prices. + diff --git a/src/GameLogic/Views/Inventory/IPlayerShopBuyRequestResultPlugIn.cs b/src/GameLogic/Views/Inventory/IPlayerShopBuyRequestResultPlugIn.cs index de4f0eb46a..2e0bcb501a 100644 --- a/src/GameLogic/Views/Inventory/IPlayerShopBuyRequestResultPlugIn.cs +++ b/src/GameLogic/Views/Inventory/IPlayerShopBuyRequestResultPlugIn.cs @@ -58,6 +58,11 @@ public enum ItemBuyResult /// The requested player has item block active. /// ItemBlock = 9, + + /// + /// The player has not enough of the currency (items or account coins) in which the price of the item is defined. + /// + LackOfPriceCurrency = 10, } /// diff --git a/src/GameLogic/Views/PlayerShop/IShowShopCurrenciesPlugIn.cs b/src/GameLogic/Views/PlayerShop/IShowShopCurrenciesPlugIn.cs new file mode 100644 index 0000000000..24285d8b06 --- /dev/null +++ b/src/GameLogic/Views/PlayerShop/IShowShopCurrenciesPlugIn.cs @@ -0,0 +1,24 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Views.PlayerShop; + +using MUnique.OpenMU.DataModel.Configuration.Items; + +/// +/// Interface of a view whose implementation informs about the items and account coins which can be used for the +/// prices of items in player shops, in addition to zen. +/// +/// +/// Only clients which can show and buy items with other prices than zen have an implementation of it. +/// +public interface IShowShopCurrenciesPlugIn : IViewPlugIn +{ + /// + /// Shows the items and account coins which can be used for prices in player shops, in addition to zen. + /// + /// The items with which prices can be paid. + /// The account coins with which prices can be paid. + ValueTask ShowShopCurrenciesAsync(IReadOnlyCollection priceItems, IReadOnlyCollection coinTypes); +} diff --git a/src/GameServer/MessageHandler/PlayerShop/PlayerShopSetItemPriceExtendedPacketHandlerPlugIn.cs b/src/GameServer/MessageHandler/PlayerShop/PlayerShopSetItemPriceExtendedPacketHandlerPlugIn.cs new file mode 100644 index 0000000000..edf6c53ba4 --- /dev/null +++ b/src/GameServer/MessageHandler/PlayerShop/PlayerShopSetItemPriceExtendedPacketHandlerPlugIn.cs @@ -0,0 +1,57 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.MessageHandler.PlayerShop; + +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; +using MUnique.OpenMU.GameLogic.Views.Inventory; +using MUnique.OpenMU.GameLogic.Views.PlayerShop; +using MUnique.OpenMU.GameServer.RemoteView.PlayerShop; +using MUnique.OpenMU.Network.Packets.ClientToServer; +using MUnique.OpenMU.Network.PlugIns; +using MUnique.OpenMU.PlugIns; + +/// +/// Packet handler which sets prices in zen, items or account coins for an item in the player shop (3F 09). +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.PlayerShopSetItemPriceExtendedPacketHandlerPlugIn_Name), Description = nameof(PlugInResources.PlayerShopSetItemPriceExtendedPacketHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("E7528764-B648-43D1-A8B4-FD167CD3EF6F")] +[BelongsToGroup(StoreHandlerGroupPlugIn.GroupKey)] +[MinimumClient(106, 3, ClientLanguage.Invariant)] +internal class PlayerShopSetItemPriceExtendedPacketHandlerPlugIn : ISubPacketHandlerPlugIn +{ + private readonly SetItemPriceAction _setPriceAction = new(); + + /// + public bool IsEncryptionExpected => true; + + /// + public byte Key => PlayerShopSetItemPriceExtended.SubCode; + + /// + public async ValueTask HandlePacketAsync(Player player, Memory packet) + { + PlayerShopSetItemPriceExtended message = packet; + player.Logger.LogDebug( + "Player [{0}] sets price of slot {1} to {2}/{3}/{4}", + player.SelectedCharacter?.Name, + message.ItemSlot, + message.MoneyPrice, + message.PriceItemType, + message.RequiredItemAmount); + + if (PlayerShopPriceEncoding.Decode(message.MoneyPrice, message.PriceItemType, message.RequiredItemAmount, player.GameContext.Configuration) is not { } price) + { + player.Logger.LogWarning("Player [{0}] sent a price with the unknown item type {1}.", player.SelectedCharacter?.Name, message.PriceItemType); + await player.InvokeViewPlugInAsync(p => p.ItemPriceSetResponseAsync(message.ItemSlot, ItemPriceResult.Failed)).ConfigureAwait(false); + return; + } + + await this._setPriceAction.SetPriceAsync(player, message.ItemSlot, price).ConfigureAwait(false); + } +} diff --git a/src/GameServer/Properties/PlugInResources.Designer.cs b/src/GameServer/Properties/PlugInResources.Designer.cs index fc9f9753bd..f191657c4f 100644 --- a/src/GameServer/Properties/PlugInResources.Designer.cs +++ b/src/GameServer/Properties/PlugInResources.Designer.cs @@ -4758,6 +4758,24 @@ public static string PlayerShopRequestItemListPacketHandlerPlugIn_Name { } } + /// + /// Looks up a localized string similar to Packet handler which sets prices in a currency for an item in the player shop (3F 09).. + /// + public static string PlayerShopSetItemPriceExtendedPacketHandlerPlugIn_Description { + get { + return ResourceManager.GetString("PlayerShopSetItemPriceExtendedPacketHandlerPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Player Shop - Set Price (with currency). + /// + public static string PlayerShopSetItemPriceExtendedPacketHandlerPlugIn_Name { + get { + return ResourceManager.GetString("PlayerShopSetItemPriceExtendedPacketHandlerPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Packet handler which sets prices for an item in the player shop (3F 01).. /// @@ -6360,6 +6378,24 @@ public static string ShowRageAttackRangePlugIn_Name { } } + /// + /// Looks up a localized string similar to The extended implementation of the IShowShopCurrenciesPlugIn which is forwarding everything to the game client with specific data packets.. + /// + public static string ShowShopCurrenciesExtendedPlugIn_Description { + get { + return ResourceManager.GetString("ShowShopCurrenciesExtendedPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show Shop Currencies Extended. + /// + public static string ShowShopCurrenciesExtendedPlugIn_Name { + get { + return ResourceManager.GetString("ShowShopCurrenciesExtendedPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to The extended implementation of the IShowShopItemListPlugIn which is forwarding everything to the game client with specific data packets.. /// diff --git a/src/GameServer/Properties/PlugInResources.de.resx b/src/GameServer/Properties/PlugInResources.de.resx index 58da19e1a7..0b90c30f54 100644 --- a/src/GameServer/Properties/PlugInResources.de.resx +++ b/src/GameServer/Properties/PlugInResources.de.resx @@ -2613,4 +2613,16 @@ Paket-Handler für Geschenkanfragen im Cash-Shop (Kennung 0xD2, 0x04). + + Spielerladen - Preis festlegen (mit Währung) + + + Paket-Handler, der Preise in einer Währung für einen Gegenstand im Spielerladen festlegt (3F 09). + + + Währungen des Ladens anzeigen (erweitert) + + + Die erweiterte Implementierung von IShowShopCurrenciesPlugIn, die alles mit spezifischen Datenpaketen an den Spielclient weiterleitet. + diff --git a/src/GameServer/Properties/PlugInResources.resx b/src/GameServer/Properties/PlugInResources.resx index e78ac27545..172acf14bc 100644 --- a/src/GameServer/Properties/PlugInResources.resx +++ b/src/GameServer/Properties/PlugInResources.resx @@ -2615,4 +2615,16 @@ Packet handler for cash shop gift requests (0xD2, 0x04 identifier). + + Player Shop - Set Price (with currency) + + + Packet handler which sets prices in a currency for an item in the player shop (3F 09). + + + Show Shop Currencies Extended + + + The extended implementation of the IShowShopCurrenciesPlugIn which is forwarding everything to the game client with specific data packets. + diff --git a/src/GameServer/RemoteView/Inventory/PlayerShopBuyRequestResultExtendedPlugIn.cs b/src/GameServer/RemoteView/Inventory/PlayerShopBuyRequestResultExtendedPlugIn.cs index a4b865ea4e..f4cc5fbe32 100644 --- a/src/GameServer/RemoteView/Inventory/PlayerShopBuyRequestResultExtendedPlugIn.cs +++ b/src/GameServer/RemoteView/Inventory/PlayerShopBuyRequestResultExtendedPlugIn.cs @@ -64,6 +64,7 @@ private static PlayerShopBuyResultExtended.ResultKind Convert(ItemBuyResult resu ItemBuyResult.LackOfMoney => PlayerShopBuyResultExtended.ResultKind.LackOfMoney, ItemBuyResult.MoneyOverflowOrNotEnoughSpace => PlayerShopBuyResultExtended.ResultKind.MoneyOverflowOrNotEnoughSpace, ItemBuyResult.ItemBlock => PlayerShopBuyResultExtended.ResultKind.ItemBlock, + ItemBuyResult.LackOfPriceCurrency => PlayerShopBuyResultExtended.ResultKind.LackOfPriceCurrency, _ => PlayerShopBuyResultExtended.ResultKind.Undefined, }; } diff --git a/src/GameServer/RemoteView/Inventory/PlayerShopBuyRequestResultPlugIn.cs b/src/GameServer/RemoteView/Inventory/PlayerShopBuyRequestResultPlugIn.cs index 2f8bc25def..2db366d6fd 100644 --- a/src/GameServer/RemoteView/Inventory/PlayerShopBuyRequestResultPlugIn.cs +++ b/src/GameServer/RemoteView/Inventory/PlayerShopBuyRequestResultPlugIn.cs @@ -59,7 +59,7 @@ private static PlayerShopBuyResult.ResultKind Convert(ItemBuyResult result) ItemBuyResult.InTransaction => PlayerShopBuyResult.ResultKind.InTransaction, ItemBuyResult.InvalidShopSlot => PlayerShopBuyResult.ResultKind.InvalidShopSlot, ItemBuyResult.NameMismatchOrPriceMissing => PlayerShopBuyResult.ResultKind.NameMismatchOrPriceMissing, - ItemBuyResult.LackOfMoney => PlayerShopBuyResult.ResultKind.LackOfMoney, + ItemBuyResult.LackOfMoney or ItemBuyResult.LackOfPriceCurrency => PlayerShopBuyResult.ResultKind.LackOfMoney, ItemBuyResult.MoneyOverflowOrNotEnoughSpace => PlayerShopBuyResult.ResultKind.MoneyOverflowOrNotEnoughSpace, ItemBuyResult.ItemBlock => PlayerShopBuyResult.ResultKind.ItemBlock, _ => PlayerShopBuyResult.ResultKind.Undefined, diff --git a/src/GameServer/RemoteView/PlayerShop/PlayerShopPriceEncoding.cs b/src/GameServer/RemoteView/PlayerShop/PlayerShopPriceEncoding.cs new file mode 100644 index 0000000000..a4ff14afc7 --- /dev/null +++ b/src/GameServer/RemoteView/PlayerShop/PlayerShopPriceEncoding.cs @@ -0,0 +1,99 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.RemoteView.PlayerShop; + +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; + +/// +/// Encodes and decodes the prices of player shop items in the fields of the extended client protocol: +/// MoneyPrice, PriceItemType and RequiredItemAmount. +/// +/// +/// The same encoding is used by the item list, the set price request and the allowed price types. +/// +internal static class PlayerShopPriceEncoding +{ + /// + /// The value of PriceItemType for prices in . + /// The other coin types follow in the order of , as defined by the packet description. + /// + private const int FirstAccountCoinPriceItemType = 0xFFF0; + + /// + /// The number of bits of the item number in PriceItemType; the group is in the bits above. + /// + private const int ItemGroupShift = 12; + + /// + /// The mask of the item number in PriceItemType. + /// + private const int ItemNumberMask = (1 << ItemGroupShift) - 1; + + /// + /// Gets the value of PriceItemType for prices which are paid with the item. + /// + /// The price item. + /// The value of PriceItemType. + public static ushort GetPriceItemType(ItemDefinition priceItem) + { + return (ushort)((priceItem.Group << ItemGroupShift) | (priceItem.Number & ItemNumberMask)); + } + + /// + /// Gets the value of PriceItemType for prices which are paid with the account coin. + /// + /// The coin type. + /// The value of PriceItemType. + public static ushort GetPriceItemType(CashShopCoinType coinType) + { + return (ushort)(FirstAccountCoinPriceItemType + (int)coinType); + } + + /// + /// Encodes the price into the fields of the protocol. + /// + /// The price. + /// The values of the fields. + public static (uint MoneyPrice, ushort PriceItemType, ushort RequiredItemAmount) Encode(ShopPrice price) + { + return price.Kind switch + { + ShopPriceKind.Item => (0, GetPriceItemType(price.PriceItem!), (ushort)Math.Clamp(price.Amount, 0, ushort.MaxValue)), + ShopPriceKind.AccountCoin => ((uint)Math.Max(price.Amount, 0), GetPriceItemType(price.CoinType!.Value), 0), + _ => ((uint)Math.Max(price.Amount, 0), 0, 0), + }; + } + + /// + /// Decodes the price from the fields of the protocol. + /// + /// The value of MoneyPrice. + /// The value of PriceItemType. + /// The value of RequiredItemAmount. + /// The game configuration, which defines the items. + /// The price; null, if the price item is unknown. + /// + /// An amount which doesn't fit into an is decoded as negative, so that it's rejected. + /// + public static ShopPrice? Decode(uint moneyPrice, ushort priceItemType, ushort requiredItemAmount, GameConfiguration gameConfiguration) + { + if (requiredItemAmount > 0) + { + var group = priceItemType >> ItemGroupShift; + var number = priceItemType & ItemNumberMask; + return gameConfiguration.Items.FirstOrDefault(item => item.Group == group && item.Number == number) is { } definition + ? new ShopPrice(requiredItemAmount, definition) + : null; + } + + var amount = moneyPrice > int.MaxValue ? -1 : (int)moneyPrice; + var coinType = (CashShopCoinType)(priceItemType - FirstAccountCoinPriceItemType); + return priceItemType >= FirstAccountCoinPriceItemType && Enum.IsDefined(coinType) + ? new ShopPrice(amount, CoinType: coinType) + : new ShopPrice(amount); + } +} diff --git a/src/GameServer/RemoteView/PlayerShop/ShowShopCurrenciesExtendedPlugIn.cs b/src/GameServer/RemoteView/PlayerShop/ShowShopCurrenciesExtendedPlugIn.cs new file mode 100644 index 0000000000..0713af7e11 --- /dev/null +++ b/src/GameServer/RemoteView/PlayerShop/ShowShopCurrenciesExtendedPlugIn.cs @@ -0,0 +1,71 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.RemoteView.PlayerShop; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.Views.PlayerShop; +using MUnique.OpenMU.Network; +using MUnique.OpenMU.Network.Packets.ServerToClient; +using MUnique.OpenMU.Network.PlugIns; +using MUnique.OpenMU.PlugIns; + +/// +/// The extended implementation of the which is forwarding everything to the game client with specific data packets. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.ShowShopCurrenciesExtendedPlugIn_Name), Description = nameof(PlugInResources.ShowShopCurrenciesExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("ED74E1F8-85EC-42E8-BC03-07CB7221E767")] +[MinimumClient(106, 3, ClientLanguage.Invariant)] +public class ShowShopCurrenciesExtendedPlugIn : IShowShopCurrenciesPlugIn +{ + /// + /// The maximum number of price types which fit into the message, which has a length of at most 255 bytes. + /// + private const int MaximumPriceTypes = (byte.MaxValue - 5) / 2; + + private readonly RemotePlayer _player; + + /// + /// Initializes a new instance of the class. + /// + /// The player. + public ShowShopCurrenciesExtendedPlugIn(RemotePlayer player) => this._player = player; + + /// + public async ValueTask ShowShopCurrenciesAsync(IReadOnlyCollection priceItems, IReadOnlyCollection coinTypes) + { + if (this._player.Connection is not { } connection) + { + return; + } + + var priceItemTypes = priceItems.Select(PlayerShopPriceEncoding.GetPriceItemType) + .Concat(coinTypes.Select(PlayerShopPriceEncoding.GetPriceItemType)) + .Distinct() + .Take(MaximumPriceTypes) + .ToList(); + + int Write() + { + var size = PlayerShopPriceTypesExtendedRef.GetRequiredSize(priceItemTypes.Count); + var packet = new PlayerShopPriceTypesExtendedRef(connection.Output.GetSpan(size)[..size]) + { + Count = (byte)priceItemTypes.Count, + }; + + for (var i = 0; i < priceItemTypes.Count; i++) + { + var priceType = packet[i]; + priceType.PriceItemType = priceItemTypes[i]; + } + + return size; + } + + await connection.SendAsync(Write).ConfigureAwait(false); + } +} diff --git a/src/GameServer/RemoteView/PlayerShop/ShowShopItemListExtendedPlugIn.cs b/src/GameServer/RemoteView/PlayerShop/ShowShopItemListExtendedPlugIn.cs index c46dc355fa..c6cd7945b1 100644 --- a/src/GameServer/RemoteView/PlayerShop/ShowShopItemListExtendedPlugIn.cs +++ b/src/GameServer/RemoteView/PlayerShop/ShowShopItemListExtendedPlugIn.cs @@ -5,7 +5,9 @@ namespace MUnique.OpenMU.GameServer.RemoteView.PlayerShop; using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.PlayerShop; using MUnique.OpenMU.Network; @@ -44,7 +46,9 @@ public async ValueTask ShowShopItemListAsync(Player requestedPlayer, bool isUpda var itemSerializer = this._player.ItemSerializer; var playerId = requestedPlayer.GetId(this._player); - var items = requestedPlayer.ShopStorage.Items.ToList(); + + // Items with prices in currencies which aren't enabled (anymore) can't be bought, so they're hidden. + var items = requestedPlayer.ShopStorage.Items.Where(item => item.GetStorePrice() is not { } price || ShopPrices.IsAllowed(this._player.GameContext, price)).ToList(); int Write() { var size = PlayerShopItemListExtendedRef.GetRequiredSize(items.Count, PlayerShopItemExtendedRef.GetRequiredSize(itemSerializer.NeededSpace)); @@ -67,9 +71,7 @@ int Write() { var itemBlock = new PlayerShopItemExtendedRef(span[actualSize..]); itemBlock.ItemSlot = item.ItemSlot; - itemBlock.MoneyPrice = (uint)(item.StorePrice ?? 0); - - // todo: when we can define a price in items, set PriceItemType and RequiredItemAmount + SetPrice(ref itemBlock, item); var itemSize = itemSerializer.SerializeItem(itemBlock.ItemData, item); actualSize += PlayerShopItemExtendedRef.GetRequiredSize(itemSize); i++; @@ -81,4 +83,12 @@ int Write() await connection.SendAsync(Write).ConfigureAwait(false); } -} \ No newline at end of file + + private static void SetPrice(ref PlayerShopItemExtendedRef itemBlock, Item item) + { + var (moneyPrice, priceItemType, requiredItemAmount) = PlayerShopPriceEncoding.Encode(item.GetStorePrice() ?? new ShopPrice(0)); + itemBlock.MoneyPrice = moneyPrice; + itemBlock.PriceItemType = priceItemType; + itemBlock.RequiredItemAmount = requiredItemAmount; + } +} diff --git a/src/GameServer/RemoteView/PlayerShop/ShowShopItemListPlugIn.cs b/src/GameServer/RemoteView/PlayerShop/ShowShopItemListPlugIn.cs index 48b5505ab5..2fe339cad3 100644 --- a/src/GameServer/RemoteView/PlayerShop/ShowShopItemListPlugIn.cs +++ b/src/GameServer/RemoteView/PlayerShop/ShowShopItemListPlugIn.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.GameServer.RemoteView.PlayerShop; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.PlayerShop; using MUnique.OpenMU.Network; @@ -42,7 +43,9 @@ public async ValueTask ShowShopItemListAsync(Player requestedPlayer, bool isUpda var itemSerializer = this._player.ItemSerializer; var playerId = requestedPlayer.GetId(this._player); - var items = requestedPlayer.ShopStorage.Items.ToList(); + + // The client can only show zen prices, so items with other prices are hidden. + var items = requestedPlayer.ShopStorage.Items.Where(item => item.GetStorePrice() is not { Kind: not ShopPriceKind.Zen }).ToList(); int Write() { var size = PlayerShopItemListRef.GetRequiredSize(items.Count); diff --git a/src/Persistence/BasicModel/Item.Generated.cs b/src/Persistence/BasicModel/Item.Generated.cs index e027b656a2..a7d5c53c56 100644 --- a/src/Persistence/BasicModel/Item.Generated.cs +++ b/src/Persistence/BasicModel/Item.Generated.cs @@ -85,6 +85,24 @@ public override MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition Defi set => base.Definition = value; } + /// + /// Gets the raw object of . + /// + [System.Text.Json.Serialization.JsonPropertyName("storePriceItemDefinition")] + public ItemDefinition RawStorePriceItemDefinition + { + get => base.StorePriceItemDefinition as ItemDefinition; + set => base.StorePriceItemDefinition = value; + } + + /// + [System.Text.Json.Serialization.JsonIgnore] + public override MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition StorePriceItemDefinition + { + get => base.StorePriceItemDefinition; + set => base.StorePriceItemDefinition = value; + } + /// public override MUnique.OpenMU.DataModel.Entities.Item Clone(MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration) { diff --git a/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs index fb2a4fdf8f..ff556134b4 100644 --- a/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs +++ b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForAccountCo public partial class AccountContextModel { private AccountContextModel() - : base(skipDetectChanges: false, modelId: new Guid("8b99359b-a111-4b0d-b8f0-fca6f18076e4"), entityTypeCount: 45) + : base(skipDetectChanges: false, modelId: new Guid("3cf7e2f3-00d9-4337-a868-60ad3a22fce2"), entityTypeCount: 45) { } diff --git a/src/Persistence/EntityFramework/CompiledModels/AccountContext/ItemEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/AccountContext/ItemEntityType.cs index d5f856f981..56eaca2f36 100644 --- a/src/Persistence/EntityFramework/CompiledModels/AccountContext/ItemEntityType.cs +++ b/src/Persistence/EntityFramework/CompiledModels/AccountContext/ItemEntityType.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.Persistence.EntityFramework.Model; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -22,7 +23,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "MUnique.OpenMU.Persistence.EntityFramework.Model.Item", typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item), baseEntityType, - propertyCount: 10, + propertyCount: 12, navigationCount: 2, foreignKeyCount: 1, unnamedIndexCount: 1, @@ -111,6 +112,22 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas nullable: true); storePrice.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var storePriceCoinType = runtimeEntityType.AddProperty( + "StorePriceCoinType", + typeof(MUnique.OpenMU.DataModel.Configuration.CashShopCoinType?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.Item).GetProperty("StorePriceCoinType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.Item).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + storePriceCoinType.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var storePriceItemDefinitionId = runtimeEntityType.AddProperty( + "StorePriceItemDefinitionId", + typeof(Guid?), + propertyInfo: typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item).GetProperty("StorePriceItemDefinitionId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + storePriceItemDefinitionId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var key = runtimeEntityType.AddKey( new[] { id }); runtimeEntityType.SetPrimaryKey(key); diff --git a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs index ffaa85fe92..fca7f93831 100644 --- a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs +++ b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForEntityDat public partial class EntityDataContextModel { private EntityDataContextModel() - : base(skipDetectChanges: false, modelId: new Guid("4a60a1d5-f676-4193-90ba-ccdedaa4b496"), entityTypeCount: 119) + : base(skipDetectChanges: false, modelId: new Guid("2b02a42f-5887-48e2-b545-ec3c679dd947"), entityTypeCount: 119) { } @@ -247,6 +247,7 @@ partial void Initialize() IncreasableItemOptionEntityType.CreateForeignKey3(increasableItemOption, powerUpDefinition); ItemEntityType.CreateForeignKey1(item, itemDefinition); ItemEntityType.CreateForeignKey2(item, itemStorage); + ItemEntityType.CreateForeignKey3(item, itemDefinition); ItemAppearanceEntityType.CreateForeignKey1(itemAppearance, appearanceData); ItemAppearanceEntityType.CreateForeignKey2(itemAppearance, itemDefinition); ItemAppearanceItemOptionTypeEntityType.CreateForeignKey1(itemAppearanceItemOptionType, itemAppearance); diff --git a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/ItemEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/ItemEntityType.cs index 99703ff997..aaf6cc6b68 100644 --- a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/ItemEntityType.cs +++ b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/ItemEntityType.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.Persistence.EntityFramework.Model; using Microsoft.EntityFrameworkCore; @@ -23,10 +24,10 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "MUnique.OpenMU.Persistence.EntityFramework.Model.Item", typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item), baseEntityType, - propertyCount: 10, - navigationCount: 4, - foreignKeyCount: 2, - unnamedIndexCount: 2, + propertyCount: 12, + navigationCount: 5, + foreignKeyCount: 3, + unnamedIndexCount: 3, keyCount: 1); var id = runtimeEntityType.AddProperty( @@ -112,6 +113,22 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas nullable: true); storePrice.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var storePriceCoinType = runtimeEntityType.AddProperty( + "StorePriceCoinType", + typeof(MUnique.OpenMU.DataModel.Configuration.CashShopCoinType?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.Item).GetProperty("StorePriceCoinType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.Item).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + storePriceCoinType.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var storePriceItemDefinitionId = runtimeEntityType.AddProperty( + "StorePriceItemDefinitionId", + typeof(Guid?), + propertyInfo: typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item).GetProperty("StorePriceItemDefinitionId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + storePriceItemDefinitionId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var key = runtimeEntityType.AddKey( new[] { id }); runtimeEntityType.SetPrimaryKey(key); @@ -122,6 +139,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas var index0 = runtimeEntityType.AddIndex( new[] { itemStorageId }); + var index1 = runtimeEntityType.AddIndex( + new[] { storePriceItemDefinitionId }); + return runtimeEntityType; } @@ -163,6 +183,21 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt return runtimeForeignKey; } + public static RuntimeForeignKey CreateForeignKey3(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("StorePriceItemDefinitionId") }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty("Id") }), + principalEntityType); + + var rawStorePriceItemDefinition = declaringEntityType.AddNavigation("RawStorePriceItemDefinition", + runtimeForeignKey, + onDependent: true, + typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition), + propertyInfo: typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item).GetProperty("RawStorePriceItemDefinition", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + return runtimeForeignKey; + } + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) { runtimeEntityType.AddAnnotation("Relational:FunctionName", null); diff --git a/src/Persistence/EntityFramework/CompiledModels/TradeContext/ItemEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/TradeContext/ItemEntityType.cs index 7fe19b2274..3b451587f5 100644 --- a/src/Persistence/EntityFramework/CompiledModels/TradeContext/ItemEntityType.cs +++ b/src/Persistence/EntityFramework/CompiledModels/TradeContext/ItemEntityType.cs @@ -1,6 +1,7 @@ // using System; using System.Reflection; +using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.Persistence.EntityFramework.Model; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -21,7 +22,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "MUnique.OpenMU.Persistence.EntityFramework.Model.Item", typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item), baseEntityType, - propertyCount: 10, + propertyCount: 12, navigationCount: 2, keyCount: 1); @@ -108,6 +109,22 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas nullable: true); storePrice.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var storePriceCoinType = runtimeEntityType.AddProperty( + "StorePriceCoinType", + typeof(MUnique.OpenMU.DataModel.Configuration.CashShopCoinType?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.Item).GetProperty("StorePriceCoinType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.Item).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + storePriceCoinType.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var storePriceItemDefinitionId = runtimeEntityType.AddProperty( + "StorePriceItemDefinitionId", + typeof(Guid?), + propertyInfo: typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item).GetProperty("StorePriceItemDefinitionId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.Item).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + storePriceItemDefinitionId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var key = runtimeEntityType.AddKey( new[] { id }); runtimeEntityType.SetPrimaryKey(key); diff --git a/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs index dd749cdc11..b5fcf8c44b 100644 --- a/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs +++ b/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForTradeCont public partial class TradeContextModel { private TradeContextModel() - : base(skipDetectChanges: false, modelId: new Guid("e017e9d8-60cc-47ee-be81-ee7a7352c760"), entityTypeCount: 44) + : base(skipDetectChanges: false, modelId: new Guid("75013418-e637-4e21-9f2a-6e399c0bf7fe"), entityTypeCount: 44) { } diff --git a/src/Persistence/EntityFramework/Loading/GameConfigurationLoader.Generated.cs b/src/Persistence/EntityFramework/Loading/GameConfigurationLoader.Generated.cs index 72dceef21d..b4fe1485d3 100644 --- a/src/Persistence/EntityFramework/Loading/GameConfigurationLoader.Generated.cs +++ b/src/Persistence/EntityFramework/Loading/GameConfigurationLoader.Generated.cs @@ -71,7 +71,7 @@ internal sealed partial class GameConfigurationLoader "select \"Id\", \"AddPercentage\", \"FailResult\", \"MaximumAmount\", \"MaximumItemLevel\", \"MinimumAmount\", \"MinimumItemLevel\", \"NpcPriceDivisor\", \"Reference\", \"SimpleCraftingSettingsId\", \"SuccessResult\" from config.\"ItemCraftingRequiredItem\"", "select \"Id\", \"AddLevel\", \"Durability\", \"ItemDefinitionId\", \"RandomMaximumLevel\", \"RandomMinimumLevel\", \"Reference\", \"SimpleCraftingSettingsId\" from config.\"ItemCraftingResultItem\"", "select \"Id\", \"Money\" from data.\"ItemStorage\" where (\"Id\" in (select \"MerchantStoreId\" from config.\"MonsterDefinition\"))", - "select \"Id\", \"DefinitionId\", \"Durability\", \"HasSkill\", \"ItemSlot\", \"ItemStorageId\", \"Level\", \"PetExperience\", \"SocketCount\", \"StorePrice\" from data.\"Item\" where (\"ItemStorageId\" in (select \"Id\" from (select * from data.\"ItemStorage\" where (\"Id\" in (select \"MerchantStoreId\" from config.\"MonsterDefinition\"))) \"ItemStorage\") or \"Id\" in (select \"ItemRewardId\" from config.\"QuestReward\"))", + "select \"Id\", \"DefinitionId\", \"Durability\", \"HasSkill\", \"ItemSlot\", \"ItemStorageId\", \"Level\", \"PetExperience\", \"SocketCount\", \"StorePrice\", \"StorePriceCoinType\", \"StorePriceItemDefinitionId\" from data.\"Item\" where (\"ItemStorageId\" in (select \"Id\" from (select * from data.\"ItemStorage\" where (\"Id\" in (select \"MerchantStoreId\" from config.\"MonsterDefinition\"))) \"ItemStorage\") or \"Id\" in (select \"ItemRewardId\" from config.\"QuestReward\"))", "select \"Id\", \"Index\", \"ItemId\", \"ItemOptionId\", \"Level\" from data.\"ItemOptionLink\" where (\"ItemId\" in (select \"Id\" from (select * from data.\"Item\" where (\"ItemStorageId\" in (select \"Id\" from (select * from data.\"ItemStorage\" where (\"Id\" in (select \"MerchantStoreId\" from config.\"MonsterDefinition\"))) \"ItemStorage\") or \"Id\" in (select \"ItemRewardId\" from config.\"QuestReward\"))) \"Item\"))", "select \"Id\", \"Group\", \"MaximumCharacterLevel\", \"MinimumCharacterLevel\", \"MonsterDefinitionId\", \"Name\", \"Number\", \"QualifiedCharacterId\", \"QuestGiverId\", \"RefuseNumber\", \"Repeatable\", \"RequiredStartMoney\", \"RequiresClientAction\", \"StartingNumber\" from config.\"QuestDefinition\"", "select \"Id\", \"DropItemGroupId\", \"ItemId\", \"MinimumNumber\", \"QuestDefinitionId\" from config.\"QuestItemRequirement\"", @@ -213,7 +213,7 @@ internal sealed partial class GameConfigurationLoader private readonly global::System.Collections.Generic.Dictionary _ItemStorage = new(); private readonly global::System.Collections.Generic.List _ItemStorageRows = new(); private readonly global::System.Collections.Generic.Dictionary _Item = new(); - private readonly global::System.Collections.Generic.List<(global::MUnique.OpenMU.Persistence.EntityFramework.Model.Item Entity, global::System.Guid? Key0, global::System.Guid? Key1)> _ItemRows = new(); + private readonly global::System.Collections.Generic.List<(global::MUnique.OpenMU.Persistence.EntityFramework.Model.Item Entity, global::System.Guid? Key0, global::System.Guid? Key1, global::System.Guid? Key2)> _ItemRows = new(); private readonly global::System.Collections.Generic.Dictionary _ItemOptionLink = new(); private readonly global::System.Collections.Generic.List<(global::MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink Entity, global::System.Guid? Key0, global::System.Guid? Key1)> _ItemOptionLinkRows = new(); private readonly global::System.Collections.Generic.Dictionary _QuestDefinition = new(); @@ -1381,8 +1381,10 @@ internal sealed partial class GameConfigurationLoader entity.PetExperience = reader.GetInt32(7); entity.SocketCount = reader.GetInt32(8); if (!reader.IsDBNull(9)) { entity.StorePrice = reader.GetInt32(9); } + if (!reader.IsDBNull(10)) { entity.StorePriceCoinType = (global::MUnique.OpenMU.DataModel.Configuration.CashShopCoinType)reader.GetInt32(10); } + if (!reader.IsDBNull(11)) { entity.StorePriceItemDefinitionId = reader.GetGuid(11); } this._Item[entity.Id] = entity; - this._ItemRows.Add((entity, reader.IsDBNull(1) ? null : reader.GetGuid(1), reader.IsDBNull(5) ? null : reader.GetGuid(5))); + this._ItemRows.Add((entity, reader.IsDBNull(1) ? null : reader.GetGuid(1), reader.IsDBNull(5) ? null : reader.GetGuid(5), reader.IsDBNull(11) ? null : reader.GetGuid(11))); } } @@ -2168,6 +2170,14 @@ private void Fixup() } } + foreach (var row in this._ItemRows) + { + if (row.Key2 is { } id) + { + row.Entity.RawStorePriceItemDefinition = this.GetItemDefinition(id); + } + } + foreach (var row in this._MonsterDefinitionRows) { if (row.Key1 is { } id && this._ItemStorage.TryGetValue(id, out var target)) diff --git a/src/Persistence/EntityFramework/Migrations/20261011120042_AddItemStorePriceCurrency.Designer.cs b/src/Persistence/EntityFramework/Migrations/20261011120042_AddItemStorePriceCurrency.Designer.cs new file mode 100644 index 0000000000..e15ca3e5c4 --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20261011120042_AddItemStorePriceCurrency.Designer.cs @@ -0,0 +1,6312 @@ +// +using System; +using MUnique.OpenMU.Persistence.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + [DbContext(typeof(EntityDataContext))] + [Migration("20261011120042_AddItemStorePriceCurrency")] + partial class AddItemStorePriceCurrency + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatBanUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("EMail") + .IsRequired() + .HasColumnType("text"); + + b.Property("GoblinPoints") + .HasColumnType("integer"); + + b.Property("IsBot") + .HasColumnType("boolean"); + + b.Property("IsNetworkObservationActive") + .HasColumnType("boolean"); + + b.Property("IsTemplate") + .HasColumnType("boolean"); + + b.Property("IsVaultExtended") + .HasColumnType("boolean"); + + b.Property("LanguageIsoCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasDefaultValue("en"); + + b.Property("LoginName") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegistrationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("TimeZone") + .HasColumnType("smallint"); + + b.Property("VaultId") + .HasColumnType("uuid"); + + b.Property("VaultPassword") + .IsRequired() + .HasColumnType("text"); + + b.Property("WCoinC") + .HasColumnType("integer"); + + b.Property("WCoinP") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LoginName") + .IsUnique(); + + b.HasIndex("VaultId") + .IsUnique(); + + b.ToTable("Account", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("AccountId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("AccountCharacterClass", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("FullAncientSetEquipped") + .HasColumnType("boolean"); + + b.Property("Pose") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("AppearanceData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DelayBetweenHits") + .HasColumnType("interval"); + + b.Property("DelayPerOneDistance") + .HasColumnType("interval"); + + b.Property("EffectRange") + .HasColumnType("integer"); + + b.Property("FrustumDistance") + .HasColumnType("real"); + + b.Property("FrustumEndWidth") + .HasColumnType("real"); + + b.Property("FrustumStartWidth") + .HasColumnType("real"); + + b.Property("HitChancePerDistanceMultiplier") + .HasColumnType("real"); + + b.Property("MaximumNumberOfHitsPerAttack") + .HasColumnType("integer"); + + b.Property("MaximumNumberOfHitsPerTarget") + .HasColumnType("integer"); + + b.Property("MinimumNumberOfHitsPerAttack") + .HasColumnType("integer"); + + b.Property("MinimumNumberOfHitsPerTarget") + .HasColumnType("integer"); + + b.Property("ProjectileCount") + .HasColumnType("integer"); + + b.Property("TargetAreaDiameter") + .HasColumnType("real"); + + b.Property("UseDeferredHits") + .HasColumnType("boolean"); + + b.Property("UseFrustumFilter") + .HasColumnType("boolean"); + + b.Property("UseTargetAreaFilter") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("AreaSkillSettings", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Designation") + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MaximumValue") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("AttributeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("InputAttributeId") + .HasColumnType("uuid"); + + b.Property("InputOperand") + .HasColumnType("real"); + + b.Property("InputOperator") + .HasColumnType("integer"); + + b.Property("OperandAttributeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionValueId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("InputAttributeId"); + + b.HasIndex("OperandAttributeId"); + + b.HasIndex("PowerUpDefinitionValueId"); + + b.HasIndex("SkillId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("AttributeRelationship", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("MinimumValue") + .HasColumnType("integer"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("SkillId1") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AttributeId"); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("SkillId"); + + b.HasIndex("SkillId1"); + + b.ToTable("AttributeRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroundId") + .HasColumnType("uuid"); + + b.Property("LeftGoalId") + .HasColumnType("uuid"); + + b.Property("LeftTeamSpawnPointX") + .HasColumnType("smallint"); + + b.Property("LeftTeamSpawnPointY") + .HasColumnType("smallint"); + + b.Property("RightGoalId") + .HasColumnType("uuid"); + + b.Property("RightTeamSpawnPointX") + .HasColumnType("smallint"); + + b.Property("RightTeamSpawnPointY") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GroundId") + .IsUnique(); + + b.HasIndex("LeftGoalId") + .IsUnique(); + + b.HasIndex("RightGoalId") + .IsUnique(); + + b.ToTable("BattleZoneDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId") + .HasColumnType("uuid"); + + b.Property("MaximumLevel") + .HasColumnType("integer"); + + b.Property("MinimumLevel") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MagicEffectDefinitionId") + .IsUnique(); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("Buff", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopCoinGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("AppliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CoinType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GrantedBy") + .HasColumnType("text"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Reference") + .IsUnique(); + + b.HasIndex("AccountId", "AppliedAt"); + + b.ToTable("CashShopCoinGrant", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannerSaleZone") + .HasColumnType("smallint"); + + b.Property("BannerYear") + .HasColumnType("smallint"); + + b.Property("BannerYearId") + .HasColumnType("smallint"); + + b.Property("ScriptSaleZone") + .HasColumnType("smallint"); + + b.Property("ScriptYear") + .HasColumnType("smallint"); + + b.Property("ScriptYearId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("CashShopConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CashShopConfigurationId") + .HasColumnType("uuid"); + + b.Property("CoinType") + .HasColumnType("integer"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("IsForSale") + .HasColumnType("boolean"); + + b.Property("IsGiftable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PackageSequence") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CashShopConfigurationId"); + + b.ToTable("CashShopPackage", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CashShopPackageId") + .HasColumnType("uuid"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("Price") + .HasColumnType("integer"); + + b.Property("PriceSequence") + .HasColumnType("integer"); + + b.Property("ProductSequence") + .HasColumnType("integer"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CashShopPackageId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("CashShopProduct", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopStorageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GiftMessage") + .HasColumnType("text"); + + b.Property("GiftSenderName") + .HasColumnType("text"); + + b.Property("IsGift") + .HasColumnType("boolean"); + + b.Property("PriceSequence") + .HasColumnType("integer"); + + b.Property("ProductSequence") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("CashShopStorageItem", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttackRespawnAreaId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("CrownHoldTimeSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30); + + b.Property("DefenseRespawnAreaId") + .HasColumnType("uuid"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("GateBuyPrice") + .HasColumnType("integer"); + + b.Property("GateRepairCostPerHealthPoint") + .HasColumnType("integer"); + + b.Property("GuildScoreCastleSiege") + .HasColumnType("integer"); + + b.Property("GuildScoreCastleSiegeMembers") + .HasColumnType("integer"); + + b.Property("LandOfTrialsMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("MaxAttackingGuilds") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3); + + b.Property("ParticipantRewardMinSeconds") + .HasColumnType("integer"); + + b.Property("RegisterMinLevel") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(200); + + b.Property("RegisterMinMembers") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(20); + + b.Property("RepairCostPerUpgradeLevel") + .HasColumnType("integer"); + + b.Property("RewardItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("SignOfLordItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("SignOfLordItemLevel") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)3); + + b.Property("StatueBuyPrice") + .HasColumnType("integer"); + + b.Property("StatueRepairCostPerHealthPoint") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttackRespawnAreaId") + .IsUnique(); + + b.HasIndex("CastleSiegeMapDefinitionId"); + + b.HasIndex("DefenseRespawnAreaId") + .IsUnique(); + + b.HasIndex("LandOfTrialsMapDefinitionId"); + + b.HasIndex("RewardItemDefinitionId"); + + b.HasIndex("SignOfLordItemDefinitionId"); + + b.ToTable("CastleSiegeConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsHuntZoneEnabled") + .HasColumnType("boolean"); + + b.Property("IsOccupied") + .HasColumnType("boolean"); + + b.Property("OwnerGuildId") + .HasColumnType("uuid"); + + b.Property("TaxChaos") + .HasColumnType("smallint"); + + b.Property("TaxHunt") + .HasColumnType("integer"); + + b.Property("TaxStore") + .HasColumnType("smallint"); + + b.Property("TributeMoney") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OwnerGuildId"); + + b.ToTable("CastleSiegeData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeDataId") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("GuildName") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("IsAllianceMaster") + .HasColumnType("boolean"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Side") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeDataId"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("CastleSiegeGuild", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("GuildName") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Marks") + .HasColumnType("integer"); + + b.Property("RegistrationOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("CastleSiegeGuildRegistration", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("DefaultSide") + .HasColumnType("smallint"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("smallint"); + + b.Property("IsPersistedToDatabase") + .HasColumnType("boolean"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("SpawnX") + .HasColumnType("smallint"); + + b.Property("SpawnY") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("MonsterDefinitionId", "InstanceId"); + + b.ToTable("CastleSiegeNpcDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeDataId") + .HasColumnType("uuid"); + + b.Property("CurrentHp") + .HasColumnType("integer"); + + b.Property("DefenseLevel") + .HasColumnType("smallint"); + + b.Property("InstanceId") + .HasColumnType("smallint"); + + b.Property("LifeLevel") + .HasColumnType("smallint"); + + b.Property("MonsterNumber") + .HasColumnType("smallint"); + + b.Property("RegenLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeDataId"); + + b.HasIndex("MonsterNumber", "InstanceId") + .IsUnique(); + + b.ToTable("CastleSiegeNpcState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegePendingReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("CastleSiegePendingReward", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("Hour") + .HasColumnType("smallint"); + + b.Property("Minute") + .HasColumnType("smallint"); + + b.Property("State") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.ToTable("CastleSiegeStateScheduleEntry", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId1") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId2") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId3") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId4") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("RequiredJewelOfGuardianCount") + .HasColumnType("integer"); + + b.Property("RequiredZen") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("CastleSiegeConfigurationId1"); + + b.HasIndex("CastleSiegeConfigurationId2"); + + b.HasIndex("CastleSiegeConfigurationId3"); + + b.HasIndex("CastleSiegeConfigurationId4"); + + b.ToTable("CastleSiegeUpgradeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId1") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("CastleSiegeConfigurationId1"); + + b.ToTable("CastleSiegeZoneDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("CharacterSlot") + .HasColumnType("smallint"); + + b.Property("CharacterStatus") + .HasColumnType("integer"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentMapId") + .HasColumnType("uuid"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("InventoryExtensions") + .HasColumnType("integer"); + + b.Property("InventoryId") + .HasColumnType("uuid"); + + b.Property("IsStoreOpened") + .HasColumnType("boolean"); + + b.Property("KeyConfiguration") + .HasColumnType("bytea"); + + b.Property("LevelUpPoints") + .HasColumnType("integer"); + + b.Property("MasterExperience") + .HasColumnType("bigint"); + + b.Property("MasterLevelUpPoints") + .HasColumnType("integer"); + + b.Property("MuHelperConfiguration") + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PlayerKillCount") + .HasColumnType("integer"); + + b.Property("Pose") + .HasColumnType("smallint"); + + b.Property("PositionX") + .HasColumnType("smallint"); + + b.Property("PositionY") + .HasColumnType("smallint"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("StateRemainingSeconds") + .HasColumnType("integer"); + + b.Property("StoreName") + .HasColumnType("text"); + + b.Property("UsedFruitPoints") + .HasColumnType("integer"); + + b.Property("UsedNegFruitPoints") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("CurrentMapId"); + + b.HasIndex("InventoryId") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Character", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanGetCreated") + .HasColumnType("boolean"); + + b.Property("ComboDefinitionId") + .HasColumnType("uuid"); + + b.Property("CreationAllowedFlag") + .HasColumnType("smallint"); + + b.Property("FruitCalculation") + .HasColumnType("integer"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("HomeMapId") + .HasColumnType("uuid"); + + b.Property("IsMasterClass") + .HasColumnType("boolean"); + + b.Property("LevelRequirementByCreation") + .HasColumnType("smallint"); + + b.Property("LevelWarpRequirementReductionPercent") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextGenerationClassId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ComboDefinitionId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("HomeMapId"); + + b.HasIndex("NextGenerationClassId"); + + b.ToTable("CharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b => + { + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("CharacterId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("CharacterDropItemGroup", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActiveQuestId") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("ClientActionPerformed") + .HasColumnType("boolean"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("LastFinishedQuestId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ActiveQuestId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("LastFinishedQuestId"); + + b.ToTable("CharacterQuestState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientCleanUpInterval") + .HasColumnType("interval"); + + b.Property("ClientTimeout") + .HasColumnType("interval"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaximumConnections") + .HasColumnType("integer"); + + b.Property("RoomCleanUpInterval") + .HasColumnType("interval"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("ChatServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatServerDefinitionId") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("NetworkPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ChatServerDefinitionId"); + + b.HasIndex("ClientId"); + + b.ToTable("ChatServerEndpoint", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemOptionCombinationBonusId") + .HasColumnType("uuid"); + + b.Property("MinimumCount") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemOptionCombinationBonusId"); + + b.HasIndex("OptionTypeId"); + + b.ToTable("CombinationBonusRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstalledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ConfigurationUpdate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdateState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("InitializationKey") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ConfigurationUpdateState", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CheckMaxConnectionsPerAddress") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientListenerPort") + .HasColumnType("integer"); + + b.Property("CurrentPatchVersion") + .HasColumnType("bytea"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisconnectOnUnknownPacket") + .HasColumnType("boolean"); + + b.Property("ListenerBacklog") + .HasColumnType("integer"); + + b.Property("MaxConnections") + .HasColumnType("integer"); + + b.Property("MaxConnectionsPerAddress") + .HasColumnType("integer"); + + b.Property("MaxFtpRequests") + .HasColumnType("integer"); + + b.Property("MaxIpRequests") + .HasColumnType("integer"); + + b.Property("MaxServerListRequests") + .HasColumnType("integer"); + + b.Property("MaximumReceiveSize") + .HasColumnType("smallint"); + + b.Property("PatchAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.Property("Timeout") + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ConnectServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ConstValueAttribute", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Chance") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("MaximumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MonsterId"); + + b.ToTable("DropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b => + { + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("DropItemGroupId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("DropItemGroupItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DuelConfigurationId") + .HasColumnType("uuid"); + + b.Property("FirstPlayerGateId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("SecondPlayerGateId") + .HasColumnType("uuid"); + + b.Property("SpectatorsGateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DuelConfigurationId"); + + b.HasIndex("FirstPlayerGateId"); + + b.HasIndex("SecondPlayerGateId"); + + b.HasIndex("SpectatorsGateId"); + + b.ToTable("DuelArea", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntranceFee") + .HasColumnType("integer"); + + b.Property("ExitId") + .HasColumnType("uuid"); + + b.Property("MaximumScore") + .HasColumnType("integer"); + + b.Property("MaximumSpectatorsPerDuelRoom") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExitId"); + + b.ToTable("DuelConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("LevelRequirement") + .HasColumnType("smallint"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("TargetGateId") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("TargetGateId"); + + b.ToTable("EnterGate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("IsSpawnGate") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.ToTable("ExitGate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Accepted") + .HasColumnType("boolean"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("FriendId") + .HasColumnType("uuid"); + + b.Property("RequestOpen") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasAlternateKey("CharacterId", "FriendId"); + + b.ToTable("Friend", "friend"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Episode") + .HasColumnType("smallint"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("Season") + .HasColumnType("smallint"); + + b.Property("Serial") + .HasColumnType("bytea"); + + b.Property("Version") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.ToTable("GameClientDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaSkillHitsPlayer") + .HasColumnType("boolean"); + + b.Property("CashShopConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CharacterNameRegex") + .HasColumnType("text"); + + b.Property("ClampMoneyOnPickup") + .HasColumnType("boolean"); + + b.Property("DamagePerOneItemDurability") + .HasColumnType("double precision"); + + b.Property("DamagePerOnePetDurability") + .HasColumnType("double precision"); + + b.Property("DuelConfigurationId") + .HasColumnType("uuid"); + + b.Property("ExcellentItemDropLevelDelta") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)25); + + b.Property("ExperienceFormula") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("if(level == 0, 0, if(level < 256, 10 * (level + 8) * (level - 1) * (level - 1), (10 * (level + 8) * (level - 1) * (level - 1)) + (1000 * (level - 247) * (level - 256) * (level - 256))))"); + + b.Property("ExperienceRate") + .HasColumnType("real"); + + b.Property("HitsPerOneItemDurability") + .HasColumnType("double precision"); + + b.Property("InfoRange") + .HasColumnType("smallint"); + + b.Property("ItemDropDuration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 1, 0, 0)); + + b.Property("LetterSendPrice") + .HasColumnType("integer"); + + b.Property("MasterExperienceFormula") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("(505 * level * level * level) + (35278500 * level) + (228045 * level * level)"); + + b.Property("MasterExperienceRate") + .HasColumnType("real"); + + b.Property("MaximumCharactersPerAccount") + .HasColumnType("smallint"); + + b.Property("MaximumInventoryMoney") + .HasColumnType("integer"); + + b.Property("MaximumItemOptionLevelDrop") + .HasColumnType("smallint"); + + b.Property("MaximumLetters") + .HasColumnType("integer"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MaximumMasterLevel") + .HasColumnType("smallint"); + + b.Property("MaximumPartySize") + .HasColumnType("smallint"); + + b.Property("MaximumPasswordLength") + .HasColumnType("integer"); + + b.Property("MaximumVaultMoney") + .HasColumnType("integer"); + + b.Property("MinimumMonsterLevelForMasterExperience") + .HasColumnType("smallint"); + + b.Property("PreventExperienceOverflow") + .HasColumnType("boolean"); + + b.Property("RecoveryInterval") + .HasColumnType("integer"); + + b.Property("ShouldDropMoney") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CashShopConfigurationId") + .IsUnique(); + + b.HasIndex("CastleSiegeConfigurationId") + .IsUnique(); + + b.HasIndex("DuelConfigurationId") + .IsUnique(); + + b.ToTable("GameConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BattleZoneId") + .HasColumnType("uuid"); + + b.Property("Discriminator") + .HasColumnType("integer"); + + b.Property("ExpMultiplier") + .HasColumnType("double precision"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SafezoneMapId") + .HasColumnType("uuid"); + + b.Property("TerrainData") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("BattleZoneId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("SafezoneMapId"); + + b.ToTable("GameMapDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinitionDropItemGroup", b => + { + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("GameMapDefinitionId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("GameMapDefinitionDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumPlayers") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("GameServerConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => + { + b.Property("GameServerConfigurationId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("GameServerConfigurationId", "GameMapDefinitionId"); + + b.HasIndex("GameMapDefinitionId"); + + b.ToTable("GameServerConfigurationGameMapDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExperienceRate") + .HasColumnType("real"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("PvpEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ServerConfigurationId") + .HasColumnType("uuid"); + + b.Property("ServerID") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("ServerConfigurationId"); + + b.ToTable("GameServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AlternativePublishedPort") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("GameServerDefinitionId") + .HasColumnType("uuid"); + + b.Property("NetworkPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("GameServerDefinitionId"); + + b.ToTable("GameServerEndpoint", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GensAbuse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("KillerId") + .HasColumnType("uuid"); + + b.Property("LastKillAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VictimId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("VictimId"); + + b.HasIndex("KillerId", "VictimId") + .IsUnique(); + + b.ToTable("GensAbuse", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GensMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Contribution") + .HasColumnType("integer"); + + b.Property("Gens") + .HasColumnType("integer"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LeftAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.Property("RankingPosition") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId") + .IsUnique(); + + b.ToTable("GensMember", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllianceGuildId") + .HasColumnType("uuid"); + + b.Property("HostilityId") + .HasColumnType("uuid"); + + b.Property("Logo") + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Notice") + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AllianceGuildId"); + + b.HasIndex("HostilityId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Guild", "guild"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GuildMember", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("GuildMember", "guild"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemOptionDefinitionId") + .HasColumnType("uuid"); + + b.Property("LevelType") + .HasColumnType("integer"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.Property("Weight") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemOptionDefinitionId"); + + b.HasIndex("OptionTypeId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("IncreasableItemOption", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("Durability") + .HasColumnType("double precision"); + + b.Property("HasSkill") + .HasColumnType("boolean"); + + b.Property("ItemSlot") + .HasColumnType("smallint"); + + b.Property("ItemStorageId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("PetExperience") + .HasColumnType("integer"); + + b.Property("SocketCount") + .HasColumnType("integer"); + + b.Property("StorePrice") + .HasColumnType("integer"); + + b.Property("StorePriceCoinType") + .HasColumnType("integer"); + + b.Property("StorePriceItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("ItemStorageId"); + + b.HasIndex("StorePriceItemDefinitionId"); + + b.ToTable("Item", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppearanceDataId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSlot") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AppearanceDataId"); + + b.HasIndex("DefinitionId"); + + b.ToTable("ItemAppearance", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearanceItemOptionType", b => + { + b.Property("ItemAppearanceId") + .HasColumnType("uuid"); + + b.Property("ItemOptionTypeId") + .HasColumnType("uuid"); + + b.HasKey("ItemAppearanceId", "ItemOptionTypeId"); + + b.HasIndex("ItemOptionTypeId"); + + b.ToTable("ItemAppearanceItemOptionType", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemBasePowerUpDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("BaseValue") + .HasColumnType("real"); + + b.Property("BonusPerLevelTableId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BonusPerLevelTableId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("ItemBasePowerUpDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemCraftingHandlerClassName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MonsterDefinitionId"); + + b.HasIndex("SimpleCraftingSettingsId") + .IsUnique(); + + b.ToTable("ItemCrafting", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddPercentage") + .HasColumnType("smallint"); + + b.Property("FailResult") + .HasColumnType("integer"); + + b.Property("MaximumAmount") + .HasColumnType("smallint"); + + b.Property("MaximumItemLevel") + .HasColumnType("smallint"); + + b.Property("MinimumAmount") + .HasColumnType("smallint"); + + b.Property("MinimumItemLevel") + .HasColumnType("smallint"); + + b.Property("NpcPriceDivisor") + .HasColumnType("integer"); + + b.Property("Reference") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.Property("SuccessResult") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SimpleCraftingSettingsId"); + + b.ToTable("ItemCraftingRequiredItem", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemDefinition", b => + { + b.Property("ItemCraftingRequiredItemId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemCraftingRequiredItemId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("ItemCraftingRequiredItemItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemOptionType", b => + { + b.Property("ItemCraftingRequiredItemId") + .HasColumnType("uuid"); + + b.Property("ItemOptionTypeId") + .HasColumnType("uuid"); + + b.HasKey("ItemCraftingRequiredItemId", "ItemOptionTypeId"); + + b.HasIndex("ItemOptionTypeId"); + + b.ToTable("ItemCraftingRequiredItemItemOptionType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingResultItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddLevel") + .HasColumnType("smallint"); + + b.Property("Durability") + .HasColumnType("smallint"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("RandomMaximumLevel") + .HasColumnType("smallint"); + + b.Property("RandomMinimumLevel") + .HasColumnType("smallint"); + + b.Property("Reference") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("SimpleCraftingSettingsId"); + + b.ToTable("ItemCraftingResultItem", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsumeEffectId") + .HasColumnType("uuid"); + + b.Property("DropLevel") + .HasColumnType("smallint"); + + b.Property("DropsFromMonsters") + .HasColumnType("boolean"); + + b.Property("Durability") + .HasColumnType("smallint"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("Height") + .HasColumnType("smallint"); + + b.Property("IsAmmunition") + .HasColumnType("boolean"); + + b.Property("IsBoundToCharacter") + .HasColumnType("boolean"); + + b.Property("IsDroppable") + .HasColumnType("boolean"); + + b.Property("IsPersonalStoreSellable") + .HasColumnType("boolean"); + + b.Property("IsQuestItem") + .HasColumnType("boolean"); + + b.Property("IsRepairable") + .HasColumnType("boolean"); + + b.Property("IsSellableToNpc") + .HasColumnType("boolean"); + + b.Property("IsStorable") + .HasColumnType("boolean"); + + b.Property("IsTradable") + .HasColumnType("boolean"); + + b.Property("ItemSlotId") + .HasColumnType("uuid"); + + b.Property("MaximumDropLevel") + .HasColumnType("smallint"); + + b.Property("MaximumItemLevel") + .HasColumnType("smallint"); + + b.Property("MaximumSockets") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("PetExperienceFormula") + .HasColumnType("text"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("StorageLimitPerCharacter") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("integer"); + + b.Property("Width") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ConsumeEffectId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("ItemSlotId"); + + b.HasIndex("SkillId"); + + b.ToTable("ItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionCharacterClass", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("ItemDefinitionCharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemOptionDefinition", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemOptionDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "ItemOptionDefinitionId"); + + b.HasIndex("ItemOptionDefinitionId"); + + b.ToTable("ItemDefinitionItemOptionDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemSetGroup", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSetGroupId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "ItemSetGroupId"); + + b.HasIndex("ItemSetGroupId"); + + b.ToTable("ItemDefinitionItemSetGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Chance") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DropEffect") + .HasColumnType("integer"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MaximumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MinimumLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MoneyAmount") + .HasColumnType("integer"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.Property("RequiredCharacterLevel") + .HasColumnType("smallint"); + + b.Property("SourceItemLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("MonsterId"); + + b.ToTable("ItemDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroupItemDefinition", b => + { + b.Property("ItemDropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemDropItemGroupId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("ItemDropItemGroupItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemItemOfItemSet", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ItemOfItemSetId") + .HasColumnType("uuid"); + + b.HasKey("ItemId", "ItemOfItemSetId"); + + b.HasIndex("ItemOfItemSetId"); + + b.ToTable("ItemItemOfItemSet", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemLevelBonusTable", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AncientSetDiscriminator") + .HasColumnType("integer"); + + b.Property("BonusOptionId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSetGroupId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BonusOptionId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("ItemSetGroupId"); + + b.ToTable("ItemOfItemSet", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OptionTypeId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("ItemOption", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppliesMultipleTimes") + .HasColumnType("boolean"); + + b.Property("BonusId") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BonusId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionCombinationBonus", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddChance") + .HasColumnType("real"); + + b.Property("AddsRandomly") + .HasColumnType("boolean"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MaximumOptionsPerItem") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ItemOptionId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.HasIndex("ItemOptionId"); + + b.ToTable("ItemOptionLink", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IncreasableItemOptionId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("RequiredItemLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IncreasableItemOptionId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("ItemOptionOfLevel", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AlwaysApplies") + .HasColumnType("boolean"); + + b.Property("CountDistinct") + .HasColumnType("boolean"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MinimumItemCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OptionsId") + .HasColumnType("uuid"); + + b.Property("SetLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("OptionsId"); + + b.ToTable("ItemSetGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("RawItemSlots") + .HasColumnType("text") + .HasColumnName("ItemSlots") + .HasJsonPropertyName("itemSlots"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemSlotType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Money") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ItemStorage", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.JewelMix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MixedJewelId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SingleJewelId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MixedJewelId"); + + b.HasIndex("SingleJewelId"); + + b.ToTable("JewelMix", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Animation") + .HasColumnType("smallint"); + + b.Property("HeaderId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rotation") + .HasColumnType("smallint"); + + b.Property("SenderAppearanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("HeaderId"); + + b.HasIndex("SenderAppearanceId") + .IsUnique(); + + b.ToTable("LetterBody", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("LetterDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadFlag") + .HasColumnType("boolean"); + + b.Property("ReceiverId") + .HasColumnType("uuid"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReceiverId"); + + b.ToTable("LetterHeader", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LevelBonus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdditionalValue") + .HasColumnType("real"); + + b.Property("ItemLevelBonusTableId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemLevelBonusTableId"); + + b.ToTable("LevelBonus", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChanceId") + .HasColumnType("uuid"); + + b.Property("ChancePvpId") + .HasColumnType("uuid"); + + b.Property("DurationDependsOnTargetLevel") + .HasColumnType("boolean"); + + b.Property("DurationId") + .HasColumnType("uuid"); + + b.Property("DurationPvpId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("InformObservers") + .HasColumnType("boolean"); + + b.Property("MonsterTargetLevelDivisor") + .HasColumnType("real"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("PlayerTargetLevelDivisor") + .HasColumnType("real"); + + b.Property("SendDuration") + .HasColumnType("boolean"); + + b.Property("StopByDeath") + .HasColumnType("boolean"); + + b.Property("SubType") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ChanceId") + .IsUnique(); + + b.HasIndex("ChancePvpId") + .IsUnique(); + + b.HasIndex("DurationId") + .IsUnique(); + + b.HasIndex("DurationPvpId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("MagicEffectDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aggregation") + .HasColumnType("integer"); + + b.Property("DisplayValueFormula") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtendsDuration") + .HasColumnType("boolean"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MinimumLevel") + .HasColumnType("smallint"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.Property("ReplacedSkillId") + .HasColumnType("uuid"); + + b.Property("RootId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.Property("ValueFormula") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReplacedSkillId"); + + b.HasIndex("RootId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("MasterSkillDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinitionSkill", b => + { + b.Property("MasterSkillDefinitionId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("MasterSkillDefinitionId", "SkillId"); + + b.HasIndex("SkillId"); + + b.ToTable("MasterSkillDefinitionSkill", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("MasterSkillRoot", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("MinimumTargetLevel") + .HasColumnType("smallint"); + + b.Property("MultiplyKillsByPlayers") + .HasColumnType("boolean"); + + b.Property("NumberOfKills") + .HasColumnType("smallint"); + + b.Property("SpawnAreaId") + .HasColumnType("uuid"); + + b.Property("Target") + .HasColumnType("integer"); + + b.Property("TargetDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameDefinitionId"); + + b.HasIndex("SpawnAreaId") + .IsUnique(); + + b.HasIndex("TargetDefinitionId"); + + b.ToTable("MiniGameChangeEvent", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowParty") + .HasColumnType("boolean"); + + b.Property("ArePlayerKillersAllowedToEnter") + .HasColumnType("boolean"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EnterDuration") + .HasColumnType("interval"); + + b.Property("EntranceFee") + .HasColumnType("integer"); + + b.Property("EntranceId") + .HasColumnType("uuid"); + + b.Property("ExitDuration") + .HasColumnType("interval"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("GameDuration") + .HasColumnType("interval"); + + b.Property("GameLevel") + .HasColumnType("smallint"); + + b.Property("MapCreationPolicy") + .HasColumnType("integer"); + + b.Property("MaximumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MaximumPlayerCount") + .HasColumnType("integer"); + + b.Property("MaximumSpecialCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumSpecialCharacterLevel") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequiresMasterClass") + .HasColumnType("boolean"); + + b.Property("SaveRankingStatistics") + .HasColumnType("boolean"); + + b.Property("TicketItemId") + .HasColumnType("uuid"); + + b.Property("TicketItemLevel") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EntranceId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("TicketItemId"); + + b.ToTable("MiniGameDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameRankingEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("GameInstanceId") + .HasColumnType("uuid"); + + b.Property("MiniGameId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("MiniGameId"); + + b.ToTable("MiniGameRankingEntry", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemRewardId") + .HasColumnType("uuid"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("RequiredKillId") + .HasColumnType("uuid"); + + b.Property("RequiredSuccess") + .HasColumnType("integer"); + + b.Property("RewardAmount") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemRewardId"); + + b.HasIndex("MiniGameDefinitionId"); + + b.HasIndex("RequiredKillId"); + + b.ToTable("MiniGameReward", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameSpawnWave", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("interval"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("interval"); + + b.Property("WaveNumber") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameDefinitionId"); + + b.ToTable("MiniGameSpawnWave", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameTerrainChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndX") + .HasColumnType("smallint"); + + b.Property("EndY") + .HasColumnType("smallint"); + + b.Property("IsClientUpdateRequired") + .HasColumnType("boolean"); + + b.Property("MiniGameChangeEventId") + .HasColumnType("uuid"); + + b.Property("SetTerrainAttribute") + .HasColumnType("boolean"); + + b.Property("StartX") + .HasColumnType("smallint"); + + b.Property("StartY") + .HasColumnType("smallint"); + + b.Property("TerrainAttribute") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameChangeEventId"); + + b.ToTable("MiniGameTerrainChange", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeDefinitionId") + .HasColumnType("uuid"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AttributeDefinitionId"); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("MonsterAttribute", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttackDelay") + .HasColumnType("interval"); + + b.Property("AttackRange") + .HasColumnType("smallint"); + + b.Property("AttackSkillId") + .HasColumnType("uuid"); + + b.Property("Attribute") + .HasColumnType("smallint"); + + b.Property("Designation") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IntelligenceTypeName") + .HasColumnType("text"); + + b.Property("MerchantStoreId") + .HasColumnType("uuid"); + + b.Property("MoveDelay") + .HasColumnType("interval"); + + b.Property("MoveRange") + .HasColumnType("smallint"); + + b.Property("NpcWindow") + .HasColumnType("integer"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("NumberOfMaximumItemDrops") + .HasColumnType("integer"); + + b.Property("ObjectKind") + .HasColumnType("integer"); + + b.Property("RespawnDelay") + .HasColumnType("interval"); + + b.Property("ViewRange") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AttackSkillId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MerchantStoreId") + .IsUnique(); + + b.ToTable("MonsterDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinitionDropItemGroup", b => + { + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("MonsterDefinitionId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("MonsterDefinitionDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("GameMapId") + .HasColumnType("uuid"); + + b.Property("MaximumHealthOverride") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("smallint"); + + b.Property("SpawnTrigger") + .HasColumnType("integer"); + + b.Property("WaveNumber") + .HasColumnType("smallint"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameMapId"); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("MonsterSpawnArea", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PlugInConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomConfiguration") + .HasColumnType("text"); + + b.Property("CustomPlugInSource") + .HasColumnType("text"); + + b.Property("ExternalAssemblyName") + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("TypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("PlugInConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BoostId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId") + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId1") + .HasColumnType("uuid"); + + b.Property("MasterSkillDefinitionId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BoostId") + .IsUnique(); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("MagicEffectDefinitionId"); + + b.HasIndex("MagicEffectDefinitionId1"); + + b.HasIndex("MasterSkillDefinitionId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("PowerUpDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("MaximumValue") + .HasColumnType("real"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.ToTable("PowerUpDefinitionValue", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("MaximumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("QualifiedCharacterId") + .HasColumnType("uuid"); + + b.Property("QuestGiverId") + .HasColumnType("uuid"); + + b.Property("RefuseNumber") + .HasColumnType("smallint"); + + b.Property("Repeatable") + .HasColumnType("boolean"); + + b.Property("RequiredStartMoney") + .HasColumnType("integer"); + + b.Property("RequiresClientAction") + .HasColumnType("boolean"); + + b.Property("StartingNumber") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MonsterDefinitionId"); + + b.HasIndex("QualifiedCharacterId"); + + b.HasIndex("QuestGiverId"); + + b.ToTable("QuestDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestItemRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("MinimumNumber") + .HasColumnType("integer"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DropItemGroupId"); + + b.HasIndex("ItemId"); + + b.HasIndex("QuestDefinitionId"); + + b.ToTable("QuestItemRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MinimumNumber") + .HasColumnType("integer"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MonsterId"); + + b.HasIndex("QuestDefinitionId"); + + b.ToTable("QuestMonsterKillRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirementState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterQuestStateId") + .HasColumnType("uuid"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("RequirementId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterQuestStateId"); + + b.HasIndex("RequirementId"); + + b.ToTable("QuestMonsterKillRequirementState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeRewardId") + .HasColumnType("uuid"); + + b.Property("ItemRewardId") + .HasColumnType("uuid"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("SkillRewardId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttributeRewardId"); + + b.HasIndex("ItemRewardId") + .IsUnique(); + + b.HasIndex("QuestDefinitionId"); + + b.HasIndex("SkillRewardId"); + + b.ToTable("QuestReward", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Rectangle", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumSuccessPercent") + .HasColumnType("smallint"); + + b.Property("Money") + .HasColumnType("integer"); + + b.Property("MoneyPerFinalSuccessPercentage") + .HasColumnType("integer"); + + b.Property("MultipleAllowed") + .HasColumnType("boolean"); + + b.Property("NpcPriceDivisor") + .HasColumnType("integer"); + + b.Property("ResultItemExcellentOptionChance") + .HasColumnType("smallint"); + + b.Property("ResultItemLuckOptionChance") + .HasColumnType("smallint"); + + b.Property("ResultItemMaxExcOptionCount") + .HasColumnType("smallint"); + + b.Property("ResultItemSelect") + .HasColumnType("integer"); + + b.Property("ResultItemSkillChance") + .HasColumnType("smallint"); + + b.Property("SuccessPercent") + .HasColumnType("smallint"); + + b.Property("SuccessPercentageAdditionForAncientItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForExcellentItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForGuardianItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForLuck") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForSocketItem") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SimpleCraftingSettings", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaSkillSettingsId") + .HasColumnType("uuid"); + + b.Property("AttackDamage") + .HasColumnType("integer"); + + b.Property("DamageType") + .HasColumnType("integer"); + + b.Property("ElementalModifierTargetId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("ImplicitTargetRange") + .HasColumnType("smallint"); + + b.Property("MagicEffectDefId") + .HasColumnType("uuid"); + + b.Property("MasterDefinitionId") + .HasColumnType("uuid"); + + b.Property("MovesTarget") + .HasColumnType("boolean"); + + b.Property("MovesToTarget") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("NumberOfHitsPerAttack") + .HasColumnType("smallint"); + + b.Property("Range") + .HasColumnType("smallint"); + + b.Property("SkillType") + .HasColumnType("integer"); + + b.Property("SkipElementalModifier") + .HasColumnType("boolean"); + + b.Property("Target") + .HasColumnType("integer"); + + b.Property("TargetRestriction") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AreaSkillSettingsId") + .IsUnique(); + + b.HasIndex("ElementalModifierTargetId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MagicEffectDefId"); + + b.HasIndex("MasterDefinitionId") + .IsUnique(); + + b.ToTable("Skill", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillCharacterClass", b => + { + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("SkillId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("SkillCharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumCompletionTime") + .HasColumnType("interval"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SkillComboDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsFinalStep") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("SkillComboDefinitionId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SkillComboDefinitionId"); + + b.HasIndex("SkillId"); + + b.ToTable("SkillComboStep", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("SkillId"); + + b.ToTable("SkillEntry", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("DefinitionId"); + + b.ToTable("StatAttribute", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeId") + .HasColumnType("uuid"); + + b.Property("BaseValue") + .HasColumnType("real"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("IncreasableByPlayer") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AttributeId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("StatAttributeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SystemConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AutoStart") + .HasColumnType("boolean"); + + b.Property("AutoUpdateSchema") + .HasColumnType("boolean"); + + b.Property("IpResolver") + .HasColumnType("integer"); + + b.Property("IpResolverParameter") + .HasColumnType("text"); + + b.Property("NetworkAnalyzerLiveBufferSize") + .HasColumnType("integer"); + + b.Property("NetworkObservationArchivePath") + .HasColumnType("text"); + + b.Property("NetworkObservationMaxSessionSizeMb") + .HasColumnType("integer"); + + b.Property("NetworkObservationMaxTotalSizeMb") + .HasColumnType("integer"); + + b.Property("NetworkObservationRetentionDays") + .HasColumnType("integer"); + + b.Property("ReadConsoleInput") + .HasColumnType("boolean"); + + b.Property("TimeZoneId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.WarpInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Costs") + .HasColumnType("integer"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("GateId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("LevelRequirement") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("GateId"); + + b.ToTable("WarpInfo", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawVault") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", "VaultId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawVault"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", "Account") + .WithMany("JoinedUnlockedCharacterClasses") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("CharacterClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId"); + + b.Navigation("RawCharacterClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawAttributes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", null) + .WithMany("RawAttributeCombinations") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawGlobalAttributeCombinations") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawInputAttribute") + .WithMany() + .HasForeignKey("InputAttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawOperandAttribute") + .WithMany() + .HasForeignKey("OperandAttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", null) + .WithMany("RawRelatedValues") + .HasForeignKey("PowerUpDefinitionValueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawAttributeRelationships") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawInputAttribute"); + + b.Navigation("RawOperandAttribute"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttribute") + .WithMany() + .HasForeignKey("AttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawMapRequirements") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawRequirements") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawConsumeRequirements") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawRequirements") + .HasForeignKey("SkillId1") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawGround") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "GroundId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawLeftGoal") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "LeftGoalId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawRightGoal") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "RightGoalId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawGround"); + + b.Navigation("RawLeftGoal"); + + b.Navigation("RawRightGoal"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawMagicEffectDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", "MagicEffectDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawBuffs") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMagicEffectDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopCoinGrant", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopPackage", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopConfiguration", null) + .WithMany("RawPackages") + .HasForeignKey("CashShopConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopProduct", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopPackage", null) + .WithMany("RawProducts") + .HasForeignKey("CashShopPackageId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.Navigation("RawItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopStorageItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", "RawAttackRespawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "AttackRespawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawCastleSiegeMapDefinition") + .WithMany() + .HasForeignKey("CastleSiegeMapDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", "RawDefenseRespawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "DefenseRespawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawLandOfTrialsMapDefinition") + .WithMany() + .HasForeignKey("LandOfTrialsMapDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawRewardItemDefinition") + .WithMany() + .HasForeignKey("RewardItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawSignOfLordItemDefinition") + .WithMany() + .HasForeignKey("SignOfLordItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("RawAttackRespawnArea"); + + b.Navigation("RawCastleSiegeMapDefinition"); + + b.Navigation("RawDefenseRespawnArea"); + + b.Navigation("RawLandOfTrialsMapDefinition"); + + b.Navigation("RawRewardItemDefinition"); + + b.Navigation("RawSignOfLordItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("OwnerGuildId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuild", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", null) + .WithMany("RawGuilds") + .HasForeignKey("CastleSiegeDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawNpcDefinitions") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonsterDefinition") + .WithMany() + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("RawMonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", null) + .WithMany("RawNpcStates") + .HasForeignKey("CastleSiegeDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegePendingReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStateSchedule") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawGateDefenseUpgrades") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawGateLifeUpgrades") + .HasForeignKey("CastleSiegeConfigurationId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~1"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueDefenseUpgrades") + .HasForeignKey("CastleSiegeConfigurationId2") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~2"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueLifeUpgrades") + .HasForeignKey("CastleSiegeConfigurationId3") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~3"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueRegenUpgrades") + .HasForeignKey("CastleSiegeConfigurationId4") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~4"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawAttackMachineZones") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawDefenseMachineZones") + .HasForeignKey("CastleSiegeConfigurationId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeZoneDefinition_CastleSiegeConfiguration_CastleS~1"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany("RawCharacters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawCurrentMap") + .WithMany() + .HasForeignKey("CurrentMapId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawInventory") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "InventoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCharacterClass"); + + b.Navigation("RawCurrentMap"); + + b.Navigation("RawInventory"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", "RawComboDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "ComboDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawCharacterClasses") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawHomeMap") + .WithMany() + .HasForeignKey("HomeMapId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawNextGenerationClass") + .WithMany() + .HasForeignKey("NextGenerationClassId"); + + b.Navigation("RawComboDefinition"); + + b.Navigation("RawHomeMap"); + + b.Navigation("RawNextGenerationClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Character") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("DropItemGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", "RawActiveQuest") + .WithMany() + .HasForeignKey("ActiveQuestId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawQuestStates") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", "RawLastFinishedQuest") + .WithMany() + .HasForeignKey("LastFinishedQuestId"); + + b.Navigation("RawActiveQuest"); + + b.Navigation("RawLastFinishedQuest"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", null) + .WithMany("RawEndpoints") + .HasForeignKey("ChatServerDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", null) + .WithMany("RawRequirements") + .HasForeignKey("ItemOptionCombinationBonusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.Navigation("RawOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany("RawBaseAttributeValues") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "GameConfiguration") + .WithMany("RawGlobalBaseAttributeValues") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("CharacterClass"); + + b.Navigation("GameConfiguration"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawDropItemGroups") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany("JoinedPossibleItems") + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", null) + .WithMany("RawDuelAreas") + .HasForeignKey("DuelConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawFirstPlayerGate") + .WithMany() + .HasForeignKey("FirstPlayerGateId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawSecondPlayerGate") + .WithMany() + .HasForeignKey("SecondPlayerGateId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawSpectatorsGate") + .WithMany() + .HasForeignKey("SpectatorsGateId"); + + b.Navigation("RawFirstPlayerGate"); + + b.Navigation("RawSecondPlayerGate"); + + b.Navigation("RawSpectatorsGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawExit") + .WithMany() + .HasForeignKey("ExitId"); + + b.Navigation("RawExit"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawEnterGates") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawTargetGate") + .WithMany() + .HasForeignKey("TargetGateId"); + + b.Navigation("RawTargetGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawMap") + .WithMany("RawExitGates") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMap"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopConfiguration", "RawCashShopConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "CashShopConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "RawCastleSiegeConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", "RawDuelConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "DuelConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCashShopConfiguration"); + + b.Navigation("RawCastleSiegeConfiguration"); + + b.Navigation("RawDuelConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "RawBattleZone") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "BattleZoneId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMaps") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawSafezoneMap") + .WithMany() + .HasForeignKey("SafezoneMapId"); + + b.Navigation("RawBattleZone"); + + b.Navigation("RawSafezoneMap"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinitionDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("GameMapDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") + .WithMany() + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", "GameServerConfiguration") + .WithMany("JoinedMaps") + .HasForeignKey("GameServerConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GameMapDefinition"); + + b.Navigation("GameServerConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "RawGameConfiguration") + .WithMany() + .HasForeignKey("GameConfigurationId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", "RawServerConfiguration") + .WithMany() + .HasForeignKey("ServerConfigurationId"); + + b.Navigation("RawGameConfiguration"); + + b.Navigation("RawServerConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerEndpoint", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", null) + .WithMany("RawEndpoints") + .HasForeignKey("GameServerDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GensAbuse", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("KillerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GensMember", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", "RawAllianceGuild") + .WithMany() + .HasForeignKey("AllianceGuildId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", "RawHostility") + .WithMany() + .HasForeignKey("HostilityId"); + + b.Navigation("RawAllianceGuild"); + + b.Navigation("RawHostility"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GuildMember", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany("RawMembers") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Character") + .WithMany() + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", null) + .WithMany("RawPossibleOptions") + .HasForeignKey("ItemOptionDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawOptionType"); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawItemStorage") + .WithMany("RawItems") + .HasForeignKey("ItemStorageId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawStorePriceItemDefinition") + .WithMany() + .HasForeignKey("StorePriceItemDefinitionId"); + + b.Navigation("RawDefinition"); + + b.Navigation("RawItemStorage"); + + b.Navigation("RawStorePriceItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", null) + .WithMany("RawEquippedItems") + .HasForeignKey("AppearanceDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearanceItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", "ItemAppearance") + .WithMany("JoinedVisibleOptions") + .HasForeignKey("ItemAppearanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "ItemOptionType") + .WithMany() + .HasForeignKey("ItemOptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemAppearance"); + + b.Navigation("ItemOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemBasePowerUpDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", "RawBonusPerLevelTable") + .WithMany() + .HasForeignKey("BonusPerLevelTableId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawBasePowerUpAttributes") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawBonusPerLevelTable"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawItemCraftings") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", "RawSimpleCraftingSettings") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", "SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawSimpleCraftingSettings"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", null) + .WithMany("RawRequiredItems") + .HasForeignKey("SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", "ItemCraftingRequiredItem") + .WithMany("JoinedPossibleItems") + .HasForeignKey("ItemCraftingRequiredItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemCraftingRequiredItem"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", "ItemCraftingRequiredItem") + .WithMany("JoinedRequiredItemOptions") + .HasForeignKey("ItemCraftingRequiredItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "ItemOptionType") + .WithMany() + .HasForeignKey("ItemOptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemCraftingRequiredItem"); + + b.Navigation("ItemOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingResultItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", null) + .WithMany("RawResultItems") + .HasForeignKey("SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawConsumeEffect") + .WithMany() + .HasForeignKey("ConsumeEffectId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItems") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", "RawItemSlot") + .WithMany() + .HasForeignKey("ItemSlotId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawConsumeEffect"); + + b.Navigation("RawItemSlot"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedQualifiedCharacters") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CharacterClass"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemOptionDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedPossibleItemOptions") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", "ItemOptionDefinition") + .WithMany() + .HasForeignKey("ItemOptionDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemOptionDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemSetGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedPossibleItemSetGroups") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", "ItemSetGroup") + .WithMany() + .HasForeignKey("ItemSetGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemSetGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawDropItems") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroupItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", "ItemDropItemGroup") + .WithMany("JoinedPossibleItems") + .HasForeignKey("ItemDropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemDropItemGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemItemOfItemSet", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", "Item") + .WithMany("JoinedItemSetGroups") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", "ItemOfItemSet") + .WithMany() + .HasForeignKey("ItemOfItemSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemOfItemSet"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemLevelBonusTables") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "RawBonusOption") + .WithMany() + .HasForeignKey("BonusOptionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", "RawItemSetGroup") + .WithMany("RawItems") + .HasForeignKey("ItemSetGroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawBonusOption"); + + b.Navigation("RawItemDefinition"); + + b.Navigation("RawItemSetGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawOptionType"); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawBonus") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", "BonusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptionCombinationBonuses") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawBonus"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptions") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", null) + .WithMany("RawItemOptions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "RawItemOption") + .WithMany() + .HasForeignKey("ItemOptionId"); + + b.Navigation("RawItemOption"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", null) + .WithMany("RawLevelDependentOptions") + .HasForeignKey("IncreasableItemOptionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptionTypes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemSetGroups") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", "RawOptions") + .WithMany() + .HasForeignKey("OptionsId"); + + b.Navigation("RawOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemSlotTypes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.JewelMix", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawJewelMixes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawMixedJewel") + .WithMany() + .HasForeignKey("MixedJewelId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawSingleJewel") + .WithMany() + .HasForeignKey("SingleJewelId"); + + b.Navigation("RawMixedJewel"); + + b.Navigation("RawSingleJewel"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", "RawHeader") + .WithMany() + .HasForeignKey("HeaderId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", "RawSenderAppearance") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", "SenderAppearanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawHeader"); + + b.Navigation("RawSenderAppearance"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Receiver") + .WithMany("RawLetters") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receiver"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LevelBonus", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", null) + .WithMany("RawBonusPerLevel") + .HasForeignKey("ItemLevelBonusTableId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawChance") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "ChanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawChancePvp") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "ChancePvpId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawDuration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "DurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawDurationPvp") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "DurationPvpId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMagicEffects") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawChance"); + + b.Navigation("RawChancePvp"); + + b.Navigation("RawDuration"); + + b.Navigation("RawDurationPvp"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawReplacedSkill") + .WithMany() + .HasForeignKey("ReplacedSkillId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", "RawRoot") + .WithMany() + .HasForeignKey("RootId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawReplacedSkill"); + + b.Navigation("RawRoot"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinitionSkill", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", "MasterSkillDefinition") + .WithMany("JoinedRequiredMasterSkills") + .HasForeignKey("MasterSkillDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MasterSkillDefinition"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMasterSkillRoots") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawChangeEvents") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", "RawSpawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", "SpawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawTargetDefinition") + .WithMany() + .HasForeignKey("TargetDefinitionId"); + + b.Navigation("RawSpawnArea"); + + b.Navigation("RawTargetDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawEntrance") + .WithMany() + .HasForeignKey("EntranceId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMiniGameDefinitions") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawTicketItem") + .WithMany() + .HasForeignKey("TicketItemId"); + + b.Navigation("RawEntrance"); + + b.Navigation("RawTicketItem"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameRankingEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "RawCharacter") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", "RawMiniGame") + .WithMany() + .HasForeignKey("MiniGameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCharacter"); + + b.Navigation("RawMiniGame"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "RawItemReward") + .WithMany() + .HasForeignKey("ItemRewardId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawRewards") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawRequiredKill") + .WithMany() + .HasForeignKey("RequiredKillId"); + + b.Navigation("RawItemReward"); + + b.Navigation("RawRequiredKill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameSpawnWave", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawSpawnWaves") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameTerrainChange", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", null) + .WithMany("RawTerrainChanges") + .HasForeignKey("MiniGameChangeEventId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttributeDefinition") + .WithMany() + .HasForeignKey("AttributeDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawAttributes") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttributeDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawAttackSkill") + .WithMany() + .HasForeignKey("AttackSkillId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMonsters") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawMerchantStore") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "MerchantStoreId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttackSkill"); + + b.Navigation("RawMerchantStore"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinitionDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "MonsterDefinition") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("MonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawGameMap") + .WithMany("RawMonsterSpawns") + .HasForeignKey("GameMapId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonsterDefinition") + .WithMany() + .HasForeignKey("MonsterDefinitionId"); + + b.Navigation("RawGameMap"); + + b.Navigation("RawMonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PlugInConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawPlugInConfigurations") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawBoost") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "BoostId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawCharacterPowerUpDefinitions") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", null) + .WithMany("RawPowerUpDefinitions") + .HasForeignKey("MagicEffectDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", null) + .WithMany("RawPowerUpDefinitionsPvp") + .HasForeignKey("MagicEffectDefinitionId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_PowerUpDefinition_MagicEffectDefinition_MagicEffectDefinit~1"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", null) + .WithMany("RawPassivePowerUps") + .HasForeignKey("MasterSkillDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawBoost"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawQuests") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawQualifiedCharacter") + .WithMany() + .HasForeignKey("QualifiedCharacterId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawQuestGiver") + .WithMany() + .HasForeignKey("QuestGiverId"); + + b.Navigation("RawQualifiedCharacter"); + + b.Navigation("RawQuestGiver"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestItemRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "RawDropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItem") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRequiredItems") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawDropItemGroup"); + + b.Navigation("RawItem"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRequiredMonsterKills") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirementState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", null) + .WithMany("RawRequirementStates") + .HasForeignKey("CharacterQuestStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", "RawRequirement") + .WithMany() + .HasForeignKey("RequirementId"); + + b.Navigation("RawRequirement"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttributeReward") + .WithMany() + .HasForeignKey("AttributeRewardId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", "RawItemReward") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", "ItemRewardId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRewards") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkillReward") + .WithMany() + .HasForeignKey("SkillRewardId"); + + b.Navigation("RawAttributeReward"); + + b.Navigation("RawItemReward"); + + b.Navigation("RawSkillReward"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", "RawAreaSkillSettings") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "AreaSkillSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawElementalModifierTarget") + .WithMany() + .HasForeignKey("ElementalModifierTargetId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawSkills") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawMagicEffectDef") + .WithMany() + .HasForeignKey("MagicEffectDefId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", "RawMasterDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "MasterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAreaSkillSettings"); + + b.Navigation("RawElementalModifierTarget"); + + b.Navigation("RawMagicEffectDef"); + + b.Navigation("RawMasterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "Skill") + .WithMany("JoinedQualifiedCharacters") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CharacterClass"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboStep", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", null) + .WithMany("RawSteps") + .HasForeignKey("SkillComboDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawLearnedSkills") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany("RawAttributes") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawAttributes") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttributeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttribute") + .WithMany() + .HasForeignKey("AttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", null) + .WithMany("RawStatAttributes") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.WarpInfo", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawWarpList") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawGate") + .WithMany() + .HasForeignKey("GateId"); + + b.Navigation("RawGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.Navigation("JoinedUnlockedCharacterClasses"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawCharacters"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.Navigation("RawEquippedItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopConfiguration", b => + { + b.Navigation("RawPackages"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopPackage", b => + { + b.Navigation("RawProducts"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.Navigation("RawAttackMachineZones"); + + b.Navigation("RawDefenseMachineZones"); + + b.Navigation("RawGateDefenseUpgrades"); + + b.Navigation("RawGateLifeUpgrades"); + + b.Navigation("RawNpcDefinitions"); + + b.Navigation("RawStateSchedule"); + + b.Navigation("RawStatueDefenseUpgrades"); + + b.Navigation("RawStatueLifeUpgrades"); + + b.Navigation("RawStatueRegenUpgrades"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.Navigation("RawGuilds"); + + b.Navigation("RawNpcStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawLearnedSkills"); + + b.Navigation("RawLetters"); + + b.Navigation("RawQuestStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.Navigation("RawAttributeCombinations"); + + b.Navigation("RawBaseAttributeValues"); + + b.Navigation("RawStatAttributes"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.Navigation("RawRequirementStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b => + { + b.Navigation("RawEndpoints"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.Navigation("JoinedPossibleItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.Navigation("RawDuelAreas"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.Navigation("RawAttributes"); + + b.Navigation("RawCharacterClasses"); + + b.Navigation("RawDropItemGroups"); + + b.Navigation("RawGlobalAttributeCombinations"); + + b.Navigation("RawGlobalBaseAttributeValues"); + + b.Navigation("RawItemLevelBonusTables"); + + b.Navigation("RawItemOptionCombinationBonuses"); + + b.Navigation("RawItemOptionTypes"); + + b.Navigation("RawItemOptions"); + + b.Navigation("RawItemSetGroups"); + + b.Navigation("RawItemSlotTypes"); + + b.Navigation("RawItems"); + + b.Navigation("RawJewelMixes"); + + b.Navigation("RawMagicEffects"); + + b.Navigation("RawMaps"); + + b.Navigation("RawMasterSkillRoots"); + + b.Navigation("RawMiniGameDefinitions"); + + b.Navigation("RawMonsters"); + + b.Navigation("RawPlugInConfigurations"); + + b.Navigation("RawSkills"); + + b.Navigation("RawWarpList"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawCharacterPowerUpDefinitions"); + + b.Navigation("RawEnterGates"); + + b.Navigation("RawExitGates"); + + b.Navigation("RawMapRequirements"); + + b.Navigation("RawMonsterSpawns"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => + { + b.Navigation("JoinedMaps"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.Navigation("RawEndpoints"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.Navigation("RawMembers"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.Navigation("RawLevelDependentOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.Navigation("JoinedItemSetGroups"); + + b.Navigation("RawItemOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.Navigation("JoinedVisibleOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.Navigation("JoinedPossibleItems"); + + b.Navigation("JoinedRequiredItemOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.Navigation("JoinedPossibleItemOptions"); + + b.Navigation("JoinedPossibleItemSetGroups"); + + b.Navigation("JoinedQualifiedCharacters"); + + b.Navigation("RawBasePowerUpAttributes"); + + b.Navigation("RawDropItems"); + + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.Navigation("JoinedPossibleItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.Navigation("RawBonusPerLevel"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.Navigation("RawPossibleOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.Navigation("RawItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", b => + { + b.Navigation("RawItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.Navigation("RawPowerUpDefinitions"); + + b.Navigation("RawPowerUpDefinitionsPvp"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.Navigation("JoinedRequiredMasterSkills"); + + b.Navigation("RawPassivePowerUps"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.Navigation("RawTerrainChanges"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.Navigation("RawChangeEvents"); + + b.Navigation("RawRewards"); + + b.Navigation("RawSpawnWaves"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawBuffs"); + + b.Navigation("RawItemCraftings"); + + b.Navigation("RawQuests"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", b => + { + b.Navigation("RawRelatedValues"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.Navigation("RawRequiredItems"); + + b.Navigation("RawRequiredMonsterKills"); + + b.Navigation("RawRewards"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", b => + { + b.Navigation("RawRequiredItems"); + + b.Navigation("RawResultItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.Navigation("JoinedQualifiedCharacters"); + + b.Navigation("RawAttributeRelationships"); + + b.Navigation("RawConsumeRequirements"); + + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", b => + { + b.Navigation("RawSteps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/20261011120042_AddItemStorePriceCurrency.cs b/src/Persistence/EntityFramework/Migrations/20261011120042_AddItemStorePriceCurrency.cs new file mode 100644 index 0000000000..308cefebd7 --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20261011120042_AddItemStorePriceCurrency.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + /// + public partial class AddItemStorePriceCurrency : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "StorePriceCoinType", + schema: "data", + table: "Item", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "StorePriceItemDefinitionId", + schema: "data", + table: "Item", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Item_StorePriceItemDefinitionId", + schema: "data", + table: "Item", + column: "StorePriceItemDefinitionId"); + + migrationBuilder.AddForeignKey( + name: "FK_Item_ItemDefinition_StorePriceItemDefinitionId", + schema: "data", + table: "Item", + column: "StorePriceItemDefinitionId", + principalSchema: "config", + principalTable: "ItemDefinition", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Item_ItemDefinition_StorePriceItemDefinitionId", + schema: "data", + table: "Item"); + + migrationBuilder.DropIndex( + name: "IX_Item_StorePriceItemDefinitionId", + schema: "data", + table: "Item"); + + migrationBuilder.DropColumn( + name: "StorePriceCoinType", + schema: "data", + table: "Item"); + + migrationBuilder.DropColumn( + name: "StorePriceItemDefinitionId", + schema: "data", + table: "Item"); + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs index 82860a33a6..2be5999506 100644 --- a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs +++ b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs @@ -2101,12 +2101,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("StorePrice") .HasColumnType("integer"); + b.Property("StorePriceCoinType") + .HasColumnType("integer"); + + b.Property("StorePriceItemDefinitionId") + .HasColumnType("uuid"); + b.HasKey("Id"); b.HasIndex("DefinitionId"); b.HasIndex("ItemStorageId"); + b.HasIndex("StorePriceItemDefinitionId"); + b.ToTable("Item", "data"); }); @@ -5007,9 +5015,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("ItemStorageId") .OnDelete(DeleteBehavior.Cascade); + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawStorePriceItemDefinition") + .WithMany() + .HasForeignKey("StorePriceItemDefinitionId"); + b.Navigation("RawDefinition"); b.Navigation("RawItemStorage"); + + b.Navigation("RawStorePriceItemDefinition"); }); modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => diff --git a/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs b/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs index aba30ffdcd..b9a4d75e06 100644 --- a/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs +++ b/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs @@ -1671,6 +1671,8 @@ private BasicModel.Item Convert(Item source) target.SocketCount = source.SocketCount; target.StorePrice = source.StorePrice; + target.StorePriceItemDefinition = (MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition)this.ConvertObject(source.StorePriceItemDefinition)!; + target.StorePriceCoinType = source.StorePriceCoinType; target.PetExperience = source.PetExperience; return target; } diff --git a/src/Persistence/EntityFramework/Model/Item.Generated.cs b/src/Persistence/EntityFramework/Model/Item.Generated.cs index 450717b35c..b227a09836 100644 --- a/src/Persistence/EntityFramework/Model/Item.Generated.cs +++ b/src/Persistence/EntityFramework/Model/Item.Generated.cs @@ -68,6 +68,32 @@ public override MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition Defi } } + /// + /// Gets or sets the identifier of . + /// + public Guid? StorePriceItemDefinitionId { get; set; } + + /// + /// Gets the raw object of . + /// + [ForeignKey(nameof(StorePriceItemDefinitionId))] + public ItemDefinition RawStorePriceItemDefinition + { + get => base.StorePriceItemDefinition as ItemDefinition; + set => base.StorePriceItemDefinition = value; + } + + /// + [NotMapped] + public override MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition StorePriceItemDefinition + { + get => base.StorePriceItemDefinition;set + { + base.StorePriceItemDefinition = value; + this.StorePriceItemDefinitionId = this.RawStorePriceItemDefinition?.Id; + } + } + /// public override MUnique.OpenMU.DataModel.Entities.Item Clone(MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration) { diff --git a/src/Persistence/Initialization/DataInitializationBase.cs b/src/Persistence/Initialization/DataInitializationBase.cs index b2f4fc5757..ef7a80e625 100644 --- a/src/Persistence/Initialization/DataInitializationBase.cs +++ b/src/Persistence/Initialization/DataInitializationBase.cs @@ -12,6 +12,7 @@ namespace MUnique.OpenMU.Persistence.Initialization; using MUnique.OpenMU.GameLogic.MiniGames.Doppelganger; using MUnique.OpenMU.GameLogic.MiniGames.Kanturu; using MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; using MUnique.OpenMU.GameLogic.PlugIns; using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using MUnique.OpenMU.GameLogic.Resets; @@ -164,6 +165,12 @@ public async Task CreateInitialDataAsync(byte numberOfGameServers, bool createTe plugInConfiguration.SetConfiguration(config, referenceHandler); } + if (plugInType == typeof(PlayerShopCurrenciesFeaturePlugIn)) + { + // The default configuration of the plug-in can't reference the items, because it's created without a game configuration. + plugInConfiguration.SetConfiguration(this.CreatePlayerShopCurrenciesConfiguration(), referenceHandler); + } + if (plugInType.IsAssignableTo(typeof(NpcTalkPlugInBase))) { // The default configuration of the plug-in can't reference the NPC, because it's created without a game configuration. @@ -212,6 +219,29 @@ public async Task CreateInitialDataAsync(byte numberOfGameServers, bool createTe /// protected abstract void CreateGameClientDefinition(); + private PlayerShopCurrenciesConfiguration CreatePlayerShopCurrenciesConfiguration() + { + var configuration = new PlayerShopCurrenciesConfiguration(); + ItemIdentifier[] jewels = + [ + ItemConstants.JewelOfBless, + ItemConstants.JewelOfSoul, + ItemConstants.JewelOfChaos, + ItemConstants.JewelOfCreation, + ItemConstants.JewelOfLife, + ]; + + foreach (var jewel in jewels) + { + if (this.GameConfiguration.Items.FirstOrDefault(item => item.Group == jewel.Group && item.Number == jewel.Number) is { } definition) + { + configuration.PriceItems.Add(definition); + } + } + + return configuration; + } + private void CreateDefaultPlugInConfiguration(Type plugInType, PlugInConfiguration plugInConfiguration, ReferenceHandler referenceHandler) { try diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/PlayerShopCurrenciesDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/PlayerShopCurrenciesDataTest.cs new file mode 100644 index 0000000000..a03b1bc483 --- /dev/null +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/PlayerShopCurrenciesDataTest.cs @@ -0,0 +1,40 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Tests; + +using Microsoft.Extensions.Logging.Abstractions; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests the initial configuration of the player shop currencies. +/// +[TestFixture] +internal class PlayerShopCurrenciesDataTest +{ + /// + /// Tests that a new database contains the deactivated plugin, configured with the jewels as price items, + /// which are referenced instead of being copied. + /// + [Test] + public async Task NewDatabaseContainsJewelsAsPriceItemsAsync() + { + var contextProvider = new InMemoryPersistenceContextProvider(); + var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); + await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false); + using var context = contextProvider.CreateNewContext(); + var gameConfiguration = (await context.GetAsync().ConfigureAwait(false)).First(); + + var plugInConfiguration = gameConfiguration.PlugInConfigurations.Single(c => c.TypeId == typeof(PlayerShopCurrenciesFeaturePlugIn).GUID); + var json = plugInConfiguration.CustomConfiguration; + TestContext.Out.WriteLine(json); + + Assert.That(plugInConfiguration.IsActive, Is.False); + Assert.That(json, Does.Contain("\"PriceItems\"")); + Assert.That(json, Does.Not.Contain("\"Name\""), "The items should be referenced instead of being serialized completely."); + Assert.That(json!.Split("\"$ref\"").Length - 1, Is.EqualTo(5), "Jewels of Bless, Soul, Chaos, Creation and Life"); + } +} diff --git a/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesRemoteViewTests.cs b/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesRemoteViewTests.cs new file mode 100644 index 0000000000..689fe290ec --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesRemoteViewTests.cs @@ -0,0 +1,101 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; +using MUnique.OpenMU.GameLogic.Views; +using MUnique.OpenMU.GameServer.RemoteView; +using MUnique.OpenMU.GameServer.RemoteView.PlayerShop; +using MUnique.OpenMU.Network.Packets.ServerToClient; +using MUnique.OpenMU.Network.PlugIns; + +/// +/// Tests for the remote views of the player shop currencies. +/// +[TestFixture] +public class PlayerShopCurrenciesRemoteViewTests +{ + private static readonly ItemDefinition JewelOfBless = new Persistence.BasicModel.ItemDefinition { Number = 13, Group = 14, Width = 1, Height = 1, Durability = 1 }; + + /// + /// Tests that the allowed price items and account coins are sent with their price item types. + /// + [Test] + public async Task PriceTypesAsync() + { + var (player, output) = CastleSiegeRemoteViewTestHelper.CreatePlayer(); + var view = new ShowShopCurrenciesExtendedPlugIn(player); + + await view.ShowShopCurrenciesAsync([JewelOfBless], [CashShopCoinType.GoblinPoints]).ConfigureAwait(false); + + Assert.That(output.ToArray(), Is.EqualTo(new byte[] { 0xC1, 9, 0x3F, 0x0A, 2, 0x0D, 0xE0, 0xF2, 0xFF })); + } + + /// + /// Tests that the prices are decoded like they're encoded, and that unknown price items are not decoded. + /// + [Test] + public void PriceEncodingRoundTrip() + { + var configuration = new Persistence.BasicModel.GameConfiguration(); + configuration.Items.Add(JewelOfBless); + ShopPrice[] prices = [new(1000), new(12, JewelOfBless), new(500, CoinType: CashShopCoinType.WCoinP)]; + + foreach (var price in prices) + { + var (moneyPrice, priceItemType, requiredItemAmount) = PlayerShopPriceEncoding.Encode(price); + Assert.That(PlayerShopPriceEncoding.Decode(moneyPrice, priceItemType, requiredItemAmount, configuration), Is.EqualTo(price)); + } + + Assert.That(PlayerShopPriceEncoding.Decode(0, 0xE00E, 1, configuration), Is.Null, "the jewel of soul is not defined"); + Assert.That(PlayerShopPriceEncoding.Decode(uint.MaxValue, 0, 0, configuration)!.Value.Amount, Is.Negative); + } + + /// + /// Tests that the prices in jewels, account coins and zen are written into the fields of the extended item list, + /// and that items with prices in disabled currencies are hidden. + /// + [Test] + public async Task ItemListWithCurrenciesAsync() + { + var (viewer, output) = CastleSiegeRemoteViewTestHelper.CreatePlayer(); + viewer.GameContext.PlugInManager.RegisterPlugIn(); + viewer.ClientVersion = new ClientVersion(106, 3, ClientLanguage.English); + var configuration = new PlayerShopCurrenciesConfiguration { IsGoblinPointsAllowed = true }; + configuration.PriceItems.Add(JewelOfBless); + viewer.GameContext.FeaturePlugIns.AddPlugIn(new PlayerShopCurrenciesFeaturePlugIn { Configuration = configuration }, true); + var seller = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + seller.SelectedCharacter!.Name = "Seller"; + seller.SelectedCharacter.StoreName = "Store"; + var definition = new ItemDefinition { Number = 1, Group = 0, Width = 1, Height = 1, Durability = 20 }; + await AddItemAsync(seller, 204, definition, new ShopPrice(12, JewelOfBless)).ConfigureAwait(false); + await AddItemAsync(seller, 205, definition, new ShopPrice(500, CoinType: CashShopCoinType.GoblinPoints)).ConfigureAwait(false); + await AddItemAsync(seller, 206, definition, new ShopPrice(1000)).ConfigureAwait(false); + await AddItemAsync(seller, 207, definition, new ShopPrice(7, CoinType: CashShopCoinType.WCoinC)).ConfigureAwait(false); + var view = new ShowShopItemListExtendedPlugIn(viewer); + + await view.ShowShopItemListAsync(seller, false).ConfigureAwait(false); + + var data = output.ToArray(); + var list = new PlayerShopItemListExtended(data); + Assert.That(list.ItemCount, Is.EqualTo(3), "the item with the price in WCoin (C) is hidden, because it's not allowed"); + var itemSize = (data.Length - PlayerShopItemListExtended.GetRequiredSize(0, 0)) / 3; + var items = Enumerable.Range(0, 3) + .Select(i => new PlayerShopItemExtended(data.AsMemory(PlayerShopItemListExtended.GetRequiredSize(0, 0) + (i * itemSize), itemSize))) + .ToDictionary(i => i.ItemSlot); + Assert.That((items[204].MoneyPrice, items[204].PriceItemType, items[204].RequiredItemAmount), Is.EqualTo((0u, (ushort)0xE00D, (ushort)12))); + Assert.That((items[205].MoneyPrice, items[205].PriceItemType, items[205].RequiredItemAmount), Is.EqualTo((500u, (ushort)0xFFF2, (ushort)0))); + Assert.That((items[206].MoneyPrice, items[206].PriceItemType, items[206].RequiredItemAmount), Is.EqualTo((1000u, (ushort)0, (ushort)0))); + } + + private static async ValueTask AddItemAsync(GameLogic.Player seller, byte slot, ItemDefinition definition, ShopPrice price) + { + var item = new Persistence.BasicModel.Item { Definition = definition, ItemSlot = slot, Durability = 20 }; + item.SetStorePrice(price); + await seller.ShopStorage!.AddItemAsync(slot, item).ConfigureAwait(false); + } +} diff --git a/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesTest.cs b/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesTest.cs new file mode 100644 index 0000000000..c30eef4b6b --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesTest.cs @@ -0,0 +1,358 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using Moq; +using MUnique.OpenMU.DataModel; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; +using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; +using MUnique.OpenMU.GameLogic.Views.Inventory; +using MUnique.OpenMU.GameLogic.Views.PlayerShop; +using MUnique.OpenMU.PlugIns; + +/// +/// Tests for the prices of player shops in other currencies than zen. +/// +[TestFixture] +public class PlayerShopCurrenciesTest +{ + private const byte StoreSlot = 204; + + private readonly ItemDefinition _jewelOfBless = new Persistence.BasicModel.ItemDefinition { Number = 13, Group = 14, Width = 1, Height = 1, Durability = 1 }; + private readonly ItemDefinition _jewelOfBlessBundle = new Persistence.BasicModel.ItemDefinition { Number = 30, Group = 12, Width = 1, Height = 1, Durability = 1 }; + private readonly ItemDefinition _jewelOfSoul = new Persistence.BasicModel.ItemDefinition { Number = 14, Group = 14, Width = 1, Height = 1, Durability = 1 }; + private readonly ItemDefinition _soldItemDefinition = new Persistence.BasicModel.ItemDefinition { Number = 1, Group = 0, Width = 1, Height = 2, Durability = 20 }; + + /// + /// Tests that a price in jewels can't be set when the feature is deactivated. + /// + [Test] + public async ValueTask JewelPriceIsRejectedWhenFeatureIsDeactivatedAsync() + { + var (seller, _) = await this.CreatePlayersAsync(isFeatureActive: false).ConfigureAwait(false); + var item = await this.AddStoreItemAsync(seller).ConfigureAwait(false); + + await new SetItemPriceAction().SetPriceAsync(seller, StoreSlot, new ShopPrice(10, this._jewelOfBless)).ConfigureAwait(false); + + Assert.That(item.StorePrice, Is.Null); + Mock.Get(seller.ViewPlugIns.GetPlugIn()!).Verify(v => v.ItemPriceSetResponseAsync(StoreSlot, ItemPriceResult.Failed), Times.Once); + } + + /// + /// Tests that a price in jewels is set when the feature is activated. + /// + [Test] + public async ValueTask JewelPriceIsSetWhenFeatureIsActivatedAsync() + { + var (seller, _) = await this.CreatePlayersAsync().ConfigureAwait(false); + var item = await this.AddStoreItemAsync(seller).ConfigureAwait(false); + + await new SetItemPriceAction().SetPriceAsync(seller, StoreSlot, new ShopPrice(10, this._jewelOfBless)).ConfigureAwait(false); + + Assert.That(item.StorePrice, Is.EqualTo(10)); + Assert.That(item.StorePriceItemDefinition, Is.SameAs(this._jewelOfBless)); + Assert.That(item.StorePriceCoinType, Is.Null); + Mock.Get(seller.ViewPlugIns.GetPlugIn()!).Verify(v => v.ItemPriceSetResponseAsync(StoreSlot, ItemPriceResult.Success), Times.Once); + } + + /// + /// Tests that the account coins are not allowed by default, that prices above the configured maximum and + /// prices in items which are not configured are rejected. + /// + /// The case of the price. + [TestCase("coin")] + [TestCase("tooHigh")] + [TestCase("notConfiguredItem")] + public async ValueTask PriceIsRejectedAsync(string priceCase) + { + var (seller, _) = await this.CreatePlayersAsync().ConfigureAwait(false); + var item = await this.AddStoreItemAsync(seller).ConfigureAwait(false); + var price = priceCase switch + { + "coin" => new ShopPrice(10, CoinType: CashShopCoinType.WCoinC), + "tooHigh" => new ShopPrice(PlayerShopCurrenciesConfiguration.DefaultMaximumItemAmount + 1, this._jewelOfBless), + _ => new ShopPrice(10, this._soldItemDefinition), + }; + + await new SetItemPriceAction().SetPriceAsync(seller, StoreSlot, price).ConfigureAwait(false); + + Assert.That(item.StorePrice, Is.Null); + Mock.Get(seller.ViewPlugIns.GetPlugIn()!).Verify(v => v.ItemPriceSetResponseAsync(StoreSlot, ItemPriceResult.Failed), Times.Once); + } + + /// + /// Tests that a price of 0 removes the price and resets the currency. + /// + [Test] + public async ValueTask PriceOfZeroRemovesPriceAsync() + { + var (seller, _) = await this.CreatePlayersAsync().ConfigureAwait(false); + var item = await this.AddStoreItemAsync(seller, new ShopPrice(10, this._jewelOfSoul)).ConfigureAwait(false); + + await new SetItemPriceAction().SetPriceAsync(seller, StoreSlot, new ShopPrice(0, this._jewelOfSoul)).ConfigureAwait(false); + + Assert.That(item.StorePrice, Is.Null); + Assert.That(item.StorePriceItemDefinition, Is.Null); + } + + /// + /// Tests that the store can't be opened when it contains an item with a price in a currency which isn't enabled. + /// + [Test] + public async ValueTask StoreWithDisabledCurrencyIsNotOpenedAsync() + { + var (seller, _) = await this.CreatePlayersAsync(isFeatureActive: false).ConfigureAwait(false); + await this.AddStoreItemAsync(seller, new ShopPrice(10, this._jewelOfBless)).ConfigureAwait(false); + + await new OpenStoreAction().OpenStoreAsync(seller, "Test").ConfigureAwait(false); + + Assert.That(seller.ShopStorage!.StoreOpen, Is.False); + } + + /// + /// Tests that an item is bought with jewels: a bundle and single jewels which match the price exactly + /// are moved from the buyer to the seller. + /// + [Test] + public async ValueTask BuyWithJewelsMovesJewelsToSellerAsync() + { + var (seller, buyer) = await this.CreatePlayersAsync().ConfigureAwait(false); + var soldItem = await this.AddStoreItemAsync(seller, new ShopPrice(13, this._jewelOfBless)).ConfigureAwait(false); + seller.ShopStorage!.StoreOpen = true; + var bundle = await this.AddInventoryItemAsync(buyer, 12, this._jewelOfBlessBundle).ConfigureAwait(false); + var singles = new List(); + for (byte slot = 13; slot < 17; slot++) + { + singles.Add(await this.AddInventoryItemAsync(buyer, slot, this._jewelOfBless).ConfigureAwait(false)); + } + + var otherJewel = await this.AddInventoryItemAsync(buyer, 17, this._jewelOfSoul).ConfigureAwait(false); + + await new BuyRequestAction().BuyItemAsync(buyer, seller, StoreSlot).ConfigureAwait(false); + + Mock.Get(buyer.ViewPlugIns.GetPlugIn()!).Verify(v => v.ShowResultAsync(seller, ItemBuyResult.Success, soldItem), Times.Once); + Assert.That(buyer.Inventory!.Items, Does.Contain(soldItem)); + Assert.That(seller.Inventory!.Items, Does.Contain(bundle)); + Assert.That(seller.Inventory.Items.Count(i => i.Definition == this._jewelOfBless), Is.EqualTo(3)); + Assert.That(buyer.Inventory.Items.Count(i => i.Definition == this._jewelOfBless), Is.EqualTo(1)); + Assert.That(buyer.Inventory.Items, Does.Contain(otherJewel)); + Assert.That(soldItem.StorePrice, Is.Null); + Assert.That(soldItem.StorePriceItemDefinition, Is.Null); + } + + /// + /// Tests that nothing is moved, when the jewels of the buyer can't pay the price exactly, because bundles are not split. + /// + [Test] + public async ValueTask BuyWithJewelsFailsWithoutExactCombinationAsync() + { + var (seller, buyer) = await this.CreatePlayersAsync().ConfigureAwait(false); + var soldItem = await this.AddStoreItemAsync(seller, new ShopPrice(5, this._jewelOfBless)).ConfigureAwait(false); + seller.ShopStorage!.StoreOpen = true; + var bundle = await this.AddInventoryItemAsync(buyer, 12, this._jewelOfBlessBundle).ConfigureAwait(false); + + await new BuyRequestAction().BuyItemAsync(buyer, seller, StoreSlot).ConfigureAwait(false); + + Mock.Get(buyer.ViewPlugIns.GetPlugIn()!).Verify(v => v.ShowResultAsync(seller, ItemBuyResult.LackOfPriceCurrency, null), Times.Once); + Assert.That(buyer.Inventory!.Items, Does.Contain(bundle)); + Assert.That(seller.ShopStorage.Items, Does.Contain(soldItem)); + } + + /// + /// Tests that an item is bought with account coins. + /// + [Test] + public async ValueTask BuyWithCoinsTransfersCoinsAsync() + { + var (seller, buyer) = await this.CreatePlayersAsync(new PlayerShopCurrenciesConfiguration { IsWCoinCAllowed = true }).ConfigureAwait(false); + var soldItem = await this.AddStoreItemAsync(seller, new ShopPrice(30, CoinType: CashShopCoinType.WCoinC)).ConfigureAwait(false); + seller.ShopStorage!.StoreOpen = true; + buyer.Account!.WCoinC = 100; + seller.Account!.WCoinC = 5; + + await new BuyRequestAction().BuyItemAsync(buyer, seller, StoreSlot).ConfigureAwait(false); + + Mock.Get(buyer.ViewPlugIns.GetPlugIn()!).Verify(v => v.ShowResultAsync(seller, ItemBuyResult.Success, soldItem), Times.Once); + Assert.That(buyer.Account.WCoinC, Is.EqualTo(70)); + Assert.That(seller.Account.WCoinC, Is.EqualTo(35)); + } + + /// + /// Tests that nothing is transferred, when the buyer doesn't have enough coins. + /// + [Test] + public async ValueTask BuyWithCoinsFailsWithoutEnoughCoinsAsync() + { + var (seller, buyer) = await this.CreatePlayersAsync(new PlayerShopCurrenciesConfiguration { IsWCoinCAllowed = true }).ConfigureAwait(false); + var soldItem = await this.AddStoreItemAsync(seller, new ShopPrice(30, CoinType: CashShopCoinType.WCoinC)).ConfigureAwait(false); + seller.ShopStorage!.StoreOpen = true; + buyer.Account!.WCoinC = 29; + + await new BuyRequestAction().BuyItemAsync(buyer, seller, StoreSlot).ConfigureAwait(false); + + Mock.Get(buyer.ViewPlugIns.GetPlugIn()!).Verify(v => v.ShowResultAsync(seller, ItemBuyResult.LackOfPriceCurrency, null), Times.Once); + Assert.That(buyer.Account.WCoinC, Is.EqualTo(29)); + Assert.That(seller.ShopStorage.Items, Does.Contain(soldItem)); + } + + /// + /// Tests that an item with a price in a currency which got disabled after the store was opened can't be bought. + /// + [Test] + public async ValueTask BuyWithDisabledCurrencyFailsAsync() + { + var (seller, buyer) = await this.CreatePlayersAsync(isFeatureActive: false).ConfigureAwait(false); + var soldItem = await this.AddStoreItemAsync(seller, new ShopPrice(1, this._jewelOfBless)).ConfigureAwait(false); + seller.ShopStorage!.StoreOpen = true; + await this.AddInventoryItemAsync(buyer, 12, this._jewelOfBless).ConfigureAwait(false); + + await new BuyRequestAction().BuyItemAsync(buyer, seller, StoreSlot).ConfigureAwait(false); + + Mock.Get(buyer.ViewPlugIns.GetPlugIn()!).Verify(v => v.ShowResultAsync(seller, ItemBuyResult.ItemBlock, null), Times.Once); + Assert.That(seller.ShopStorage.Items, Does.Contain(soldItem)); + } + + /// + /// Tests that zen prices still work like before. + /// + [Test] + public async ValueTask BuyWithZenTransfersMoneyAsync() + { + var (seller, buyer) = await this.CreatePlayersAsync(isFeatureActive: false).ConfigureAwait(false); + var soldItem = await this.AddStoreItemAsync(seller, new ShopPrice(1000)).ConfigureAwait(false); + seller.ShopStorage!.StoreOpen = true; + buyer.Money = 1500; + + await new BuyRequestAction().BuyItemAsync(buyer, seller, StoreSlot).ConfigureAwait(false); + + Mock.Get(buyer.ViewPlugIns.GetPlugIn()!).Verify(v => v.ShowResultAsync(seller, ItemBuyResult.Success, soldItem), Times.Once); + Assert.That(buyer.Money, Is.EqualTo(500)); + Assert.That(seller.Money, Is.EqualTo(1000)); + } + + /// + /// Tests the selection of jewel items which pay a price exactly. + /// + /// The pieces of the available items. + /// The price. + /// The expected number of selected rows; -1, if no combination is expected. + [TestCase(new[] { 30, 20, 20 }, 40, 2)] + [TestCase(new[] { 10, 1, 1, 1 }, 12, 3)] + [TestCase(new[] { 10, 1, 1, 1 }, 11, 2)] + [TestCase(new[] { 10, 10 }, 15, -1)] + [TestCase(new[] { 1, 1 }, 3, -1)] + [TestCase(new[] { 20, 10, 1 }, 21, 2)] + public void SelectRowsMatchesThePriceExactly(int[] pieces, int price, int expectedRows) + { + var rows = pieces.Select(p => ((Item)new Persistence.BasicModel.Item(), p)).ToList(); + + var selected = ItemShopCurrencyExchange.SelectRows(rows, price); + + if (expectedRows < 0) + { + Assert.That(selected, Is.Null); + } + else + { + Assert.That(selected, Is.Not.Null); + Assert.That(selected!.Count, Is.EqualTo(expectedRows)); + Assert.That(selected.Sum(r => r.Pieces), Is.EqualTo(price)); + Assert.That(selected.Select(r => r.Item).Distinct(ReferenceEqualityComparer.Instance).Count(), Is.EqualTo(selected.Count)); + } + } + + /// + /// Tests the parsing of the currencies of the chat command. + /// + /// The text. + /// The expected kind of the price; null, if the text isn't a currency. + [TestCase("zen", ShopPriceKind.Zen)] + [TestCase("BLESS", ShopPriceKind.Item)] + [TestCase("jos", ShopPriceKind.Item)] + [TestCase("14/13", ShopPriceKind.Item)] + [TestCase("gp", ShopPriceKind.AccountCoin)] + [TestCase("14/99", null)] + [TestCase("gold", null)] + public async ValueTask ParseCurrencyAsync(string text, ShopPriceKind? expectedKind) + { + var (seller, _) = await this.CreatePlayersAsync().ConfigureAwait(false); + + var success = ShopPrices.TryParse(seller.GameContext, text, 3, out var price); + + Assert.That(success, Is.EqualTo(expectedKind.HasValue)); + if (expectedKind is { } kind) + { + Assert.That(price.Kind, Is.EqualTo(kind)); + Assert.That(price.Amount, Is.EqualTo(3)); + } + } + + /// + /// Tests that the allowed prices follow the configuration. + /// + [Test] + public async ValueTask AllowedPricesFollowConfigurationAsync() + { + var configuration = new PlayerShopCurrenciesConfiguration { IsGoblinPointsAllowed = true }; + configuration.PriceItems.Add(this._jewelOfSoul); + var (seller, _) = await this.CreatePlayersAsync(configuration).ConfigureAwait(false); + + Assert.That(ShopPrices.GetAllowedPriceItems(seller.GameContext), Is.EquivalentTo(new[] { this._jewelOfSoul })); + Assert.That(ShopPrices.GetAllowedCoinTypes(seller.GameContext), Is.EquivalentTo(new[] { CashShopCoinType.GoblinPoints })); + Assert.That(ShopPrices.IsAllowed(seller.GameContext, new ShopPrice(1, this._jewelOfBless)), Is.False); + Assert.That(ShopPrices.IsAllowed(seller.GameContext, new ShopPrice(1, this._jewelOfSoul)), Is.True); + } + + private async ValueTask<(Player Seller, Player Buyer)> CreatePlayersAsync(PlayerShopCurrenciesConfiguration? configuration = null, bool isFeatureActive = true) + { + // All plugins are discovered and activated by the test helper, so the feature plugin is deactivated first, + // and then activated with the configuration of the test. + var gameContext = GameContextTestHelper.CreateGameContext( + [ + new PlugInConfiguration { TypeId = typeof(PlayerShopCurrenciesFeaturePlugIn).GUID, IsActive = false }, + ]); + if (isFeatureActive) + { + gameContext.FeaturePlugIns.AddPlugIn(new PlayerShopCurrenciesFeaturePlugIn { Configuration = configuration ?? this.CreateConfiguration() }, true); + } + + gameContext.Configuration.Items.Add(this._jewelOfBless); + gameContext.Configuration.Items.Add(this._jewelOfSoul); + gameContext.Configuration.JewelMixes.Add(new Persistence.BasicModel.JewelMix { Number = 0, SingleJewel = this._jewelOfBless, MixedJewel = this._jewelOfBlessBundle }); + var seller = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + var buyer = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + seller.Attributes![Stats.Level] = 10; + buyer.Attributes![Stats.Level] = 10; + return (seller, buyer); + } + + private PlayerShopCurrenciesConfiguration CreateConfiguration() + { + var configuration = new PlayerShopCurrenciesConfiguration(); + configuration.PriceItems.Add(this._jewelOfBless); + configuration.PriceItems.Add(this._jewelOfSoul); + return configuration; + } + + private async ValueTask AddStoreItemAsync(Player seller, ShopPrice? price = null) + { + var item = new Persistence.BasicModel.Item { Definition = this._soldItemDefinition, ItemSlot = StoreSlot, Durability = 20 }; + item.SetStorePrice(price); + await seller.ShopStorage!.AddItemAsync(StoreSlot, item).ConfigureAwait(false); + return item; + } + + private async ValueTask AddInventoryItemAsync(Player player, byte slot, ItemDefinition definition, byte level = 0) + { + var item = new Persistence.BasicModel.Item { Definition = definition, ItemSlot = slot, Durability = 1, Level = level }; + await player.Inventory!.AddItemAsync(slot, item).ConfigureAwait(false); + return item; + } +} From ca9f1f205c95a8b87ad0d38788028ff43c8b00c6 Mon Sep 17 00:00:00 2001 From: Sven Nicolai Date: Mon, 12 Oct 2026 09:15:53 +0000 Subject: [PATCH 2/3] fix(player-shop): review follow-ups of the shop currencies - Captions and descriptions for the settings of the plugin, so that the admin panel shows "Is WCoin (C) Allowed" instead of words split at the wrong places. - A stored price of 0 or less counts as no price, so such an item can't be bought for nothing; the buy and the store opening check it. - Bundles with a higher level than the jewel mix creates (10, 20 or 30 pieces) don't count as payment, because they couldn't be unstacked into their pieces. - A truncated set price request is ignored with a warning instead of throwing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7XufeAZ22z7ocXAXUqUsE --- .../PlayerStore/BuyRequestAction.cs | 3 +- .../PlayerStore/OpenStoreAction.cs | 2 +- .../ItemShopCurrencyExchange.cs | 15 +++++- .../ItemStorePriceExtensions.cs | 6 ++- .../Properties/ModelResources.de.resx | 13 +++++ src/GameLogic/Properties/ModelResources.resx | 13 +++++ ...SetItemPriceExtendedPacketHandlerPlugIn.cs | 6 +++ .../PlayerShopCurrenciesRemoteViewTests.cs | 19 +++++++ .../PlayerShopCurrenciesTest.cs | 51 +++++++++++++++++++ 9 files changed, 123 insertions(+), 5 deletions(-) diff --git a/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs b/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs index 70a6d9aca6..762f6bed08 100644 --- a/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs +++ b/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs @@ -47,7 +47,7 @@ public async ValueTask BuyItemAsync(Player player, Player requestedPlayer, byte } var item = requestedPlayer.ShopStorage.GetItem(slot); - if (item?.StorePrice is null) + if (item?.GetStorePrice() is not { } price) { player.Logger.LogDebug("Item unavailable, Slot {0}", slot); await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.NameMismatchOrPriceMissing, null)).ConfigureAwait(false); @@ -68,7 +68,6 @@ public async ValueTask BuyItemAsync(Player player, Player requestedPlayer, byte return; } - var price = item.GetStorePrice()!.Value; if (price.Kind != ShopPriceKind.Zen && !player.SupportsShopCurrencies()) { // The client can't show such items, so it should never request them. diff --git a/src/GameLogic/PlayerActions/PlayerStore/OpenStoreAction.cs b/src/GameLogic/PlayerActions/PlayerStore/OpenStoreAction.cs index a33d5c1b3e..d0552df031 100644 --- a/src/GameLogic/PlayerActions/PlayerStore/OpenStoreAction.cs +++ b/src/GameLogic/PlayerActions/PlayerStore/OpenStoreAction.cs @@ -28,7 +28,7 @@ public async ValueTask OpenStoreAsync(Player player, string storeName) return; } - if (player.ShopStorage?.Items.Any(i => !i.StorePrice.HasValue) ?? true) + if (player.ShopStorage?.Items.Any(i => i.GetStorePrice() is null) ?? true) { player.Logger.LogWarning("OpenStore request failed: Not all store items have a price assigned. Player: [{0}], StoreName: [{1}]", character.Name, character.StoreName); return; diff --git a/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs b/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs index dda342bfbf..036385032f 100644 --- a/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs +++ b/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs @@ -24,6 +24,15 @@ public sealed class ItemShopCurrencyExchange : IShopCurrencyExchange /// private const int PiecesPerBundleLevel = 10; + /// + /// The highest level of a bundle which the jewel mix creates: bundles of 10, 20 or 30 pieces have the levels 0 to 2. + /// + /// + /// Bundles with a higher level can't be created by players, and unstacking them doesn't give their pieces back, + /// so they're not accepted as payment. + /// + private const byte MaximumBundleLevel = 2; + /// public ShopPaymentResult CheckFunds(Player buyer, ShopPrice price) { @@ -105,6 +114,10 @@ public async ValueTask NotifyBuyerAsync(ShopPurchase purchase) /// The available rows. /// The price. /// The selected rows; null, if no combination matches the price exactly. + /// + /// The work and memory grow with the price, so it's only called when the rows sum up to at least the price. + /// That bounds the price by the items which the buyer actually has, not by a configured maximum. + /// internal static IReadOnlyList<(Item Item, int Pieces)>? SelectRows(IReadOnlyList<(Item Item, int Pieces)> rows, int price) { // A subset sum, because bundles can't be split: reachedBy[sum] is the index of the row which @@ -176,7 +189,7 @@ public async ValueTask NotifyBuyerAsync(ShopPurchase purchase) { rows.Add((item, 1)); } - else if (bundleDefinitions.Any(bundle => ShopPrice.IsSameItem(bundle, definition))) + else if (item.Level <= MaximumBundleLevel && bundleDefinitions.Any(bundle => ShopPrice.IsSameItem(bundle, definition))) { rows.Add((item, (item.Level + 1) * PiecesPerBundleLevel)); } diff --git a/src/GameLogic/PlayerShopCurrencies/ItemStorePriceExtensions.cs b/src/GameLogic/PlayerShopCurrencies/ItemStorePriceExtensions.cs index e611fb8de5..aee3f11ce6 100644 --- a/src/GameLogic/PlayerShopCurrencies/ItemStorePriceExtensions.cs +++ b/src/GameLogic/PlayerShopCurrencies/ItemStorePriceExtensions.cs @@ -14,9 +14,13 @@ public static class ItemStorePriceExtensions /// /// The item. /// The price; null, if the item has no price. + /// + /// An amount of 0 or less is treated as no price, so that such an item can't be bought for nothing. + /// Setting a price never stores it, but a modified database could contain it. + /// public static ShopPrice? GetStorePrice(this Item item) { - return item.StorePrice is { } amount + return item.StorePrice is { } amount and > 0 ? new ShopPrice(amount, item.StorePriceItemDefinition, item.StorePriceCoinType) : null; } diff --git a/src/GameLogic/Properties/ModelResources.de.resx b/src/GameLogic/Properties/ModelResources.de.resx index 943f99ca17..9d6f989d3a 100644 --- a/src/GameLogic/Properties/ModelResources.de.resx +++ b/src/GameLogic/Properties/ModelResources.de.resx @@ -354,4 +354,17 @@ Start-Y + Konfiguration der Währungen für persönliche Läden + Preis-Gegenstände + Die Gegenstände, mit denen Preise bezahlt werden können, z. B. Juwelen. + WCoin (C) erlaubt + Preise in WCoin (C) sind erlaubt. + WCoin (P) erlaubt + Preise in WCoin (P) sind erlaubt. + Goblin-Punkte erlaubt + Preise in Goblin-Punkten sind erlaubt. + Maximale Gegenstandsanzahl + Die maximale Anzahl an Gegenständen eines Preises; höchstens 65535. + Maximale Münzanzahl + Die maximale Anzahl an Konto-Münzen eines Preises. diff --git a/src/GameLogic/Properties/ModelResources.resx b/src/GameLogic/Properties/ModelResources.resx index c59faef8ed..1cce283a19 100644 --- a/src/GameLogic/Properties/ModelResources.resx +++ b/src/GameLogic/Properties/ModelResources.resx @@ -128,4 +128,17 @@ NightmareBattle Tower End + Player Shop Currencies Configuration + Price Items + The items with which prices can be paid, e.g. jewels. + Is WCoin (C) Allowed + Prices in WCoin (C) are allowed. + Is WCoin (P) Allowed + Prices in WCoin (P) are allowed. + Is Goblin Points Allowed + Prices in Goblin Points are allowed. + Maximum Item Amount + The maximum number of item pieces of a price; at most 65535. + Maximum Coin Amount + The maximum amount of account coins of a price. diff --git a/src/GameServer/MessageHandler/PlayerShop/PlayerShopSetItemPriceExtendedPacketHandlerPlugIn.cs b/src/GameServer/MessageHandler/PlayerShop/PlayerShopSetItemPriceExtendedPacketHandlerPlugIn.cs index edf6c53ba4..3fd5921e22 100644 --- a/src/GameServer/MessageHandler/PlayerShop/PlayerShopSetItemPriceExtendedPacketHandlerPlugIn.cs +++ b/src/GameServer/MessageHandler/PlayerShop/PlayerShopSetItemPriceExtendedPacketHandlerPlugIn.cs @@ -36,6 +36,12 @@ internal class PlayerShopSetItemPriceExtendedPacketHandlerPlugIn : ISubPacketHan /// public async ValueTask HandlePacketAsync(Player player, Memory packet) { + if (packet.Length < PlayerShopSetItemPriceExtended.Length) + { + player.Logger.LogWarning("Ignoring truncated player shop set item price packet of length {Length}.", packet.Length); + return; + } + PlayerShopSetItemPriceExtended message = packet; player.Logger.LogDebug( "Player [{0}] sets price of slot {1} to {2}/{3}/{4}", diff --git a/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesRemoteViewTests.cs b/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesRemoteViewTests.cs index 689fe290ec..381b0cf4bd 100644 --- a/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesRemoteViewTests.cs +++ b/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesRemoteViewTests.cs @@ -8,6 +8,9 @@ namespace MUnique.OpenMU.Tests; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.GameLogic.PlayerShopCurrencies; using MUnique.OpenMU.GameLogic.Views; +using MUnique.OpenMU.GameLogic.Views.Inventory; +using MUnique.OpenMU.GameLogic.Views.PlayerShop; +using MUnique.OpenMU.GameServer.MessageHandler.PlayerShop; using MUnique.OpenMU.GameServer.RemoteView; using MUnique.OpenMU.GameServer.RemoteView.PlayerShop; using MUnique.OpenMU.Network.Packets.ServerToClient; @@ -35,6 +38,22 @@ public async Task PriceTypesAsync() Assert.That(output.ToArray(), Is.EqualTo(new byte[] { 0xC1, 9, 0x3F, 0x0A, 2, 0x0D, 0xE0, 0xF2, 0xFF })); } + /// + /// Tests that a truncated set price request is ignored instead of throwing. + /// + [Test] + public async Task TruncatedSetPriceRequestIsIgnoredAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var handler = new PlayerShopSetItemPriceExtendedPacketHandlerPlugIn(); + var packet = new byte[] { 0xC3, 8, 0x3F, 0x09, 0, 0, 0, 0 }; + + await handler.HandlePacketAsync(player, packet).ConfigureAwait(false); + + Moq.Mock.Get(player.ViewPlugIns.GetPlugIn()!) + .Verify(v => v.ItemPriceSetResponseAsync(Moq.It.IsAny(), Moq.It.IsAny()), Moq.Times.Never); + } + /// /// Tests that the prices are decoded like they're encoded, and that unknown price items are not decoded. /// diff --git a/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesTest.cs b/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesTest.cs index c30eef4b6b..b6ff3915ee 100644 --- a/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesTest.cs +++ b/tests/MUnique.OpenMU.Tests/PlayerShopCurrenciesTest.cs @@ -237,6 +237,57 @@ public async ValueTask BuyWithZenTransfersMoneyAsync() Assert.That(seller.Money, Is.EqualTo(1000)); } + /// + /// Tests that an item with a stored price of 0, which can only come from a modified database, + /// is not sold for nothing. + /// + [Test] + public async ValueTask ItemWithZeroPriceIsNotSoldAsync() + { + var (seller, buyer) = await this.CreatePlayersAsync().ConfigureAwait(false); + var soldItem = await this.AddStoreItemAsync(seller, new ShopPrice(1, this._jewelOfBless)).ConfigureAwait(false); + soldItem.StorePrice = 0; + seller.ShopStorage!.StoreOpen = true; + + await new BuyRequestAction().BuyItemAsync(buyer, seller, StoreSlot).ConfigureAwait(false); + + Mock.Get(buyer.ViewPlugIns.GetPlugIn()!).Verify(v => v.ShowResultAsync(seller, ItemBuyResult.NameMismatchOrPriceMissing, null), Times.Once); + Assert.That(seller.ShopStorage.Items, Does.Contain(soldItem)); + Assert.That(soldItem.GetStorePrice(), Is.Null); + } + + /// + /// Tests that a bundle with a higher level than the jewel mix creates doesn't count as payment, + /// because it couldn't be unstacked into its pieces. + /// + [Test] + public async ValueTask BundleAboveMixLevelsIsNotAcceptedAsync() + { + var (seller, buyer) = await this.CreatePlayersAsync().ConfigureAwait(false); + var soldItem = await this.AddStoreItemAsync(seller, new ShopPrice(40, this._jewelOfBless)).ConfigureAwait(false); + seller.ShopStorage!.StoreOpen = true; + var bundle = await this.AddInventoryItemAsync(buyer, 12, this._jewelOfBlessBundle, level: 3).ConfigureAwait(false); + + await new BuyRequestAction().BuyItemAsync(buyer, seller, StoreSlot).ConfigureAwait(false); + + Mock.Get(buyer.ViewPlugIns.GetPlugIn()!).Verify(v => v.ShowResultAsync(seller, ItemBuyResult.LackOfPriceCurrency, null), Times.Once); + Assert.That(buyer.Inventory!.Items, Does.Contain(bundle)); + Assert.That(seller.ShopStorage.Items, Does.Contain(soldItem)); + } + + /// + /// Tests that the admin panel shows readable captions for the account coin settings, which can't be derived + /// from the property names. + /// + [Test] + public void ConfigurationHasCaptions() + { + var english = System.Globalization.CultureInfo.GetCultureInfo("en"); + + Assert.That(typeof(PlayerShopCurrenciesConfiguration).GetPropertyCaption(nameof(PlayerShopCurrenciesConfiguration.IsWCoinCAllowed), english), Is.EqualTo("Is WCoin (C) Allowed")); + Assert.That(typeof(PlayerShopCurrenciesConfiguration).GetPropertyCaption(nameof(PlayerShopCurrenciesConfiguration.IsWCoinPAllowed), english), Is.EqualTo("Is WCoin (P) Allowed")); + } + /// /// Tests the selection of jewel items which pay a price exactly. /// From 4cd5b428c5bd06726dbbc91735759c688d4c6f31 Mon Sep 17 00:00:00 2001 From: Sven Nicolai Date: Wed, 14 Oct 2026 11:14:07 +0000 Subject: [PATCH 3/3] fix(player-shop): Codacy findings of the shop currencies - Rename PlayerShopCurrenciesFeaturePlugIn.GetConfiguration to GetActiveConfiguration, which can't be confused with its Configuration property (S4059). - Walk back the subset-sum selection with a while loop instead of a for loop which changes its counter in the body (S127). - Leave out the heading of the docs page, which the title of the front matter already gives (MD025), and quote the new sidebar entry as the linter wants it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7XufeAZ22z7ocXAXUqUsE --- .../docs/server-features/player-shop-currencies.md | 2 -- docs-website/sidebars.js | 2 +- .../PlayerShopCurrencies/ItemShopCurrencyExchange.cs | 7 ++++--- .../PlayerShopCurrenciesFeaturePlugIn.cs | 2 +- src/GameLogic/PlayerShopCurrencies/ShopPrices.cs | 10 +++++----- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs-website/docs/server-features/player-shop-currencies.md b/docs-website/docs/server-features/player-shop-currencies.md index 518037efb2..2829424e10 100644 --- a/docs-website/docs/server-features/player-shop-currencies.md +++ b/docs-website/docs/server-features/player-shop-currencies.md @@ -5,8 +5,6 @@ sidebar_position: 5 description: Prices of items in personal stores in items like jewels, and in account coins, in addition to zen. --- -# Player shop currencies - In a personal store, the price of an item is usually defined in zen. With the plugin **Player shop currencies**, players can define the price of each item in one of these currencies, too: diff --git a/docs-website/sidebars.js b/docs-website/sidebars.js index 935c93cf82..51cdc843cb 100644 --- a/docs-website/sidebars.js +++ b/docs-website/sidebars.js @@ -63,7 +63,7 @@ const sidebars = { 'server-features/test-actors', 'server-features/gens', 'server-features/cash-shop', - 'server-features/player-shop-currencies', + "server-features/player-shop-currencies", ], }, { diff --git a/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs b/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs index 036385032f..a120a569ee 100644 --- a/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs +++ b/src/GameLogic/PlayerShopCurrencies/ItemShopCurrencyExchange.cs @@ -145,11 +145,12 @@ public async ValueTask NotifyBuyerAsync(ShopPurchase purchase) } var result = new List<(Item Item, int Pieces)>(); - for (var sum = price; sum > 0;) + var remaining = price; + while (remaining > 0) { - var row = ordered[reachedBy[sum]]; + var row = ordered[reachedBy[remaining]]; result.Add(row); - sum -= row.Pieces; + remaining -= row.Pieces; } return result; diff --git a/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesFeaturePlugIn.cs b/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesFeaturePlugIn.cs index f4a1c38667..c133414047 100644 --- a/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesFeaturePlugIn.cs +++ b/src/GameLogic/PlayerShopCurrencies/PlayerShopCurrenciesFeaturePlugIn.cs @@ -30,7 +30,7 @@ public class PlayerShopCurrenciesFeaturePlugIn : IFeaturePlugIn, IDisabledByDefa /// /// The game context. /// The configuration; null, if the feature is deactivated. - public static PlayerShopCurrenciesConfiguration? GetConfiguration(IGameContext gameContext) + public static PlayerShopCurrenciesConfiguration? GetActiveConfiguration(IGameContext gameContext) { return gameContext.FeaturePlugIns.GetPlugIn() is { } plugIn ? plugIn.Configuration ?? new PlayerShopCurrenciesConfiguration() diff --git a/src/GameLogic/PlayerShopCurrencies/ShopPrices.cs b/src/GameLogic/PlayerShopCurrencies/ShopPrices.cs index 10b319c647..cbacc9e4b2 100644 --- a/src/GameLogic/PlayerShopCurrencies/ShopPrices.cs +++ b/src/GameLogic/PlayerShopCurrencies/ShopPrices.cs @@ -65,7 +65,7 @@ public static bool IsAllowed(IGameContext gameContext, ShopPrice price) return true; } - return PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(gameContext) is { } configuration + return PlayerShopCurrenciesFeaturePlugIn.GetActiveConfiguration(gameContext) is { } configuration && (price.Kind == ShopPriceKind.Item ? configuration.PriceItems.Any(item => ShopPrice.IsSameItem(item, price.PriceItem)) : Coins.Any(coin => coin.CoinType == price.CoinType && coin.IsAllowed(configuration))); @@ -78,7 +78,7 @@ public static bool IsAllowed(IGameContext gameContext, ShopPrice price) /// The item definitions; empty, if the feature is deactivated. public static IReadOnlyList GetAllowedPriceItems(IGameContext gameContext) { - return PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(gameContext)?.PriceItems.ToList() ?? []; + return PlayerShopCurrenciesFeaturePlugIn.GetActiveConfiguration(gameContext)?.PriceItems.ToList() ?? []; } /// @@ -88,7 +88,7 @@ public static IReadOnlyList GetAllowedPriceItems(IGameContext ga /// The coin types; empty, if the feature is deactivated. public static IReadOnlyList GetAllowedCoinTypes(IGameContext gameContext) { - if (PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(gameContext) is not { } configuration) + if (PlayerShopCurrenciesFeaturePlugIn.GetActiveConfiguration(gameContext) is not { } configuration) { return []; } @@ -104,7 +104,7 @@ public static IReadOnlyList GetAllowedCoinTypes(IGameContext g /// The maximum amount. public static int GetMaximumAmount(IGameContext gameContext, ShopPriceKind kind) { - var configuration = PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(gameContext) ?? new PlayerShopCurrenciesConfiguration(); + var configuration = PlayerShopCurrenciesFeaturePlugIn.GetActiveConfiguration(gameContext) ?? new PlayerShopCurrenciesConfiguration(); return kind switch { ShopPriceKind.Item => Math.Min( @@ -217,7 +217,7 @@ public static IShopCurrencyExchange GetExchange(ShopPriceKind kind) /// The value task. public static async ValueTask ShowAllowedPricesAsync(this Player player) { - if (PlayerShopCurrenciesFeaturePlugIn.GetConfiguration(player.GameContext) is null) + if (PlayerShopCurrenciesFeaturePlugIn.GetActiveConfiguration(player.GameContext) is null) { return; }