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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/DataModel/Configuration/Items/ItemDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,39 @@ public partial class ItemDefinition
/// </summary>
public int StorageLimitPerCharacter { get; set; }

/// <summary>
/// Gets or sets a value indicating whether items of this kind can be put into a trade with another player.
/// </summary>
public bool IsTradable { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether items of this kind can be dropped to the ground.
/// </summary>
public bool IsDroppable { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether items of this kind can be stored in the vault.
/// </summary>
public bool IsStorable { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether items of this kind can be sold to an NPC merchant.
/// </summary>
public bool IsSellableToNpc { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether items of this kind can be offered in a personal store.
/// </summary>
public bool IsPersonalStoreSellable { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether items of this kind can be repaired the normal way:
/// by an NPC, or with the repair of the inventory.
/// Other ways to restore the durability don't depend on it, e.g. repairing a Horn of Fenrir
/// with a Jewel of Bless (see the repair target items of the jewel of bless consume handler).
/// </summary>
public bool IsRepairable { get; set; } = true;

/// <summary>
/// Gets or sets the name of the item.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions src/DataModel/ItemExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ public static int GetDarkRavenLeadershipRequirement(this Item item, int petLevel
/// </returns>
public static bool IsWearable(this Item item) => item.Definition?.ItemSlot != null;

/// <summary>
/// Determines whether this item can be repaired the normal way (by an NPC, or with the repair of the
/// inventory): it must be wearable, and its definition must allow it (<see cref="ItemDefinition.IsRepairable"/>).
/// The durability of an item which can't be worn is its number of pieces, so repairing it would reduce a
/// stack to one piece. Other ways to restore the durability, like a Jewel of Bless on a Horn of Fenrir,
/// don't use this check.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>
/// <c>true</c> if the specified item can be repaired; otherwise, <c>false</c>.
/// </returns>
public static bool CanBeRepaired(this Item item) => item.IsWearable() && item.Definition!.IsRepairable;

/// <summary>
/// Determines whether this item is stackable.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/GameLogic/Bots/BotShoppingHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ private static List<Item> GetSellableJunk(OfflinePlayer player, IStorage invento
continue; // equipped, or an archer's arrows - selling those would disarm the bow.
}

if (!definition.IsSellableToNpc)
{
continue; // the merchant refuses it, and as unsold junk it would be destroyed under slot pressure.
}

var identifier = new ItemIdentifier(definition.Number, definition.Group);
if (HealingHandler.HealthPotionPriority.Contains(identifier)
|| HealingHandler.ManaPotionPriority.Contains(identifier))
Expand Down Expand Up @@ -360,6 +365,7 @@ private static async ValueTask<int> RepairGearAsync(OfflinePlayer player)
}

if (inventory.GetItem(slot) is { } item
&& item.CanBeRepaired()
&& item.Durability() < item.GetMaximumDurabilityOfOnePiece())
{
damaged.Add((slot, PriceCalculator.CalculateRepairPrice(item, true)));
Expand Down
2 changes: 1 addition & 1 deletion src/GameLogic/Offline/RepairHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public async ValueTask PerformRepairsAsync()
}

var item = this._player.Inventory?.GetItem(i);
if (item is null)
if (item is null || !item.CanBeRepaired())
{
continue;
}
Expand Down
11 changes: 11 additions & 0 deletions src/GameLogic/PlayerActions/Items/DropItemAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ public async ValueTask DropItemAsync(Player player, byte slot, Point target)
return;
}

if (item.Definition is { IsDroppable: false })
{
player.Logger.LogWarning(
"Player {0} tried to drop {1}, which its item definition doesn't allow. The client item data may differ from the server.",
player,
item);
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCannotBeDropped)).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IItemDropResultPlugIn>(p => p.ItemDropResultAsync(slot, false)).ConfigureAwait(false);
return;
}

if (player.GameContext.PlugInManager.GetPlugInPoint<IItemDropPlugIn>() is { } plugInPoint)
{
var dropArguments = new ItemDropArguments();
Expand Down
12 changes: 11 additions & 1 deletion src/GameLogic/PlayerActions/Items/ItemRepairAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ public async ValueTask RepairItemAsync(Player player, byte slot)
return;
}

if (!item.CanBeRepaired())
{
player.Logger.LogWarning(
"Player {0} tried to repair {1}, which its item definition doesn't allow. The client item data may differ from the server.",
player,
item);
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCannotBeRepaired)).ConfigureAwait(false);
return;
}

if ((byte)item.Durability == item.GetMaximumDurabilityOfOnePiece())
{
return;
Expand Down Expand Up @@ -77,7 +87,7 @@ public async ValueTask RepairAllItemsAsync(Player player)
}

var item = player.Inventory?.GetItem(i);
if (item is null)
if (item is null || !item.CanBeRepaired())
{
continue;
}
Expand Down
33 changes: 33 additions & 0 deletions src/GameLogic/PlayerActions/Items/MoveItemAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ public async ValueTask MoveItemAsync(Player player, byte fromSlot, Storages from
return;
}

if (fromStorage != toStorage && GetRuleBlockingMove(item, toStorage) is { } blockedMessage)
{
player.Logger.LogWarning(
"Player {0} tried to move {1} into the {2}, which its item definition doesn't allow. The client item data may differ from the server.",
player,
item,
toStorage);
await player.ShowLocalizedBlueMessageAsync(blockedMessage).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IItemMoveFailedPlugIn>(p => p.ItemMoveFailedAsync(item)).ConfigureAwait(false);
return;
}

var toStorageInfo = this.GetStorageInfo(player, toStorage);
var toItemStorage = toStorageInfo?.Storage;

Expand Down Expand Up @@ -100,6 +112,27 @@ public async ValueTask MoveItemAsync(Player player, byte fromSlot, Storages from
}
}

/// <summary>
/// Gets the message to show when the item rules of the item definition don't allow
/// moving it into the target storage, or <c>null</c> when they do.
/// </summary>
private static string? GetRuleBlockingMove(Item item, Storages toStorage)
{
var definition = item.Definition;
if (definition is null)
{
return null;
}

return toStorage switch
{
Storages.Trade when !definition.IsTradable => nameof(PlayerMessage.ItemCannotBeTraded),
Storages.Vault when !definition.IsStorable => nameof(PlayerMessage.ItemCannotBeStored),
Storages.PersonalStore when !definition.IsPersonalStoreSellable => nameof(PlayerMessage.ItemCannotBeSoldInPersonalStore),
_ => null,
};
}

private async ValueTask FullStackAsync(Player player, IStorage sourceStorage, Item sourceItem, Item targetItem)
{
targetItem.Durability += sourceItem.Durability;
Expand Down
11 changes: 11 additions & 0 deletions src/GameLogic/PlayerActions/Items/SellItemToNpcAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ public async ValueTask<bool> SellItemAsync(Player player, byte slot)
return false;
}

if (item.Definition is { IsSellableToNpc: false })
{
player.Logger.LogWarning(
"Player {0} tried to sell {1} to an npc, which its item definition doesn't allow. The client item data may differ from the server.",
player,
item);
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCannotBeSoldToNpc)).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IItemSoldToNpcPlugIn>(p => p.ItemSoldToNpcAsync(false)).ConfigureAwait(false);
return false;
}

if (item.Definition is null || (item.Definition.IsBoundToCharacter && (item.Definition.Durability == 0 || item.Durability > 0)))
{
await player.InvokeViewPlugInAsync<IItemSoldToNpcPlugIn>(p => p.ItemSoldToNpcAsync(false)).ConfigureAwait(false);
Expand Down
14 changes: 14 additions & 0 deletions src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ public async ValueTask BuyItemAsync(Player player, Player requestedPlayer, byte
return;
}

// Checked here, and not only when the item is moved into the store: the store may
// contain it since before the item rules existed.
if (item.Definition is { IsPersonalStoreSellable: false })
{
player.Logger.LogWarning(
"Player {0} tried to buy {1} from the store of {2}, which its item definition doesn't allow to sell in a personal store.",
player,
item,
requestedPlayer);
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCannotBeSoldInPersonalStore)).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IPlayerShopBuyRequestResultPlugIn>(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.ItemBlock, null)).ConfigureAwait(false);
return;
}

var itemPrice = item.StorePrice.Value;

if (player.Money < itemPrice)
Expand Down
10 changes: 10 additions & 0 deletions src/GameLogic/PlayerActions/Trade/TradeAcceptAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ internal async ValueTask OpenTradeAsync(ITrader trader)
// first make sure that all items which could be transferred are already present in the database
await trader.SaveProgressAsync().ConfigureAwait(false);

// Items which another window (e.g. the chaos machine) left in the temporary storage go back to
// the inventory first. The trade uses the same storage, so they would be offered in the trade,
// and lost when it is cancelled, because a cancel restores the inventory backup made below.
if (trader.TemporaryStorage is { } temporaryStorage
&& temporaryStorage.Items.Any()
&& !await trader.Inventory!.TryTakeAllAsync(temporaryStorage).ConfigureAwait(false))
{
trader.Logger.LogWarning("Could not return the items of the temporary storage of {trader} to the inventory before opening a trade.", trader);
}

trader.BackupInventory = new BackupItemStorage(trader.Inventory!.ItemStorage);
trader.TradingMoney = 0;
await trader.InvokeViewPlugInAsync<IShowTradeRequestAnswerPlugIn>(p => p.ShowTradeRequestAnswerAsync(true)).ConfigureAwait(false);
Expand Down
27 changes: 27 additions & 0 deletions src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ private async ValueTask<TradeResult> InternalFinishTradeAsync(ITrader trader, IT
using var itemContext = trader.GameContext.PersistenceContextProvider.CreateNewTradeContext();
var traderItems = trader.TemporaryStorage!.Items.ToList();
var tradePartnerItems = tradingPartner.TemporaryStorage!.Items.ToList();

// Checked here, and not only when an item is moved into the trade: the temporary storage
// is shared with other windows (e.g. the chaos machine), so it may already contain items.
// Both are checked, so that each trader gets the message about their own items.
var traderItemsAreTradable = await this.CheckItemsAreTradableAsync(trader, traderItems).ConfigureAwait(false);
var partnerItemsAreTradable = await this.CheckItemsAreTradableAsync(tradingPartner, tradePartnerItems).ConfigureAwait(false);
if (!traderItemsAreTradable || !partnerItemsAreTradable)
{
return TradeResult.Cancelled;
}

this.AttachItemsToPersistenceContext(traderItems, itemContext);
this.AttachItemsToPersistenceContext(tradePartnerItems, itemContext);

Expand Down Expand Up @@ -119,6 +130,22 @@ private async ValueTask<TradeResult> InternalFinishTradeAsync(ITrader trader, IT
}
}

private async ValueTask<bool> CheckItemsAreTradableAsync(ITrader trader, IEnumerable<Item> items)
{
var notTradable = items.FirstOrDefault(item => item.Definition is { IsTradable: false });
if (notTradable is null)
{
return true;
}

(trader as Player)?.Logger.LogWarning(
"Trader {0} offered {1}, which its item definition doesn't allow to trade. The trade is cancelled.",
trader.Name,
notTradable);
await this.SendMessageAsync(trader, nameof(PlayerMessage.ItemCannotBeTraded)).ConfigureAwait(false);
return false;
}

private void CallPlugIn(IEnumerable<Item> items, ITrader source, ITrader target)
{
var point = target.GameContext.PlugInManager.GetPlugInPoint<IItemTradedToOtherPlayerPlugIn>();
Expand Down
54 changes: 54 additions & 0 deletions src/GameLogic/Properties/PlayerMessage.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/GameLogic/Properties/PlayerMessage.resx
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,24 @@
<data name="ItemIsBoundToInventoryOfCharacter" xml:space="preserve">
<value>This item is bound to the inventory of this character.</value>
</data>
<data name="ItemCannotBeDropped" xml:space="preserve">
<value>This item can't be dropped.</value>
</data>
<data name="ItemCannotBeRepaired" xml:space="preserve">
<value>This item can't be repaired.</value>
</data>
<data name="ItemCannotBeSoldInPersonalStore" xml:space="preserve">
<value>This item can't be sold in a personal store.</value>
</data>
<data name="ItemCannotBeSoldToNpc" xml:space="preserve">
<value>This item can't be sold.</value>
</data>
<data name="ItemCannotBeStored" xml:space="preserve">
<value>This item can't be stored in the vault.</value>
</data>
<data name="ItemCannotBeTraded" xml:space="preserve">
<value>This item can't be traded.</value>
</data>
<data name="NotEnoughMoneyToSendLetter" xml:space="preserve">
<value>Not enough Zen to send a letter.</value>
</data>
Expand Down
Loading