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
63 changes: 63 additions & 0 deletions docs-website/docs/server-features/waiting-items.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Waiting items
sidebar_label: Waiting items
sidebar_position: 6
description: Items which find no place in their storage wait for space in the inventory, instead of blocking the character.
---

When the size of an item definition grows, e.g. after an update of the game
configuration, an item of that kind may no longer fit where it is: in the
inventory, the personal store or the vault. Such an item doesn't stop the
character from entering the game. It's taken out of its storage and waits for
space in the inventory of the character. The server log gets a warning for each
of these items.

An item in the inventory first tries the other places of the inventory,
including the inventory extensions, before it waits.

## Delivery

The plugin **Waiting items delivery** puts the waiting items into the inventory
as soon as they fit there:

* when the character enters the game,
* when the character opens the vault and one of its items found no place,
* and then periodically, by default every 30 seconds, but only while no dialog
is open, e.g. no trade, chaos machine, merchant or vault.

The oldest item comes first. An item which doesn't fit doesn't hold back a
later, smaller one. The client gets a delivered item like any other new item of
the inventory, so this works with every client.

When the plugin is deactivated, the items keep waiting, and they are delivered
when it's activated again.

### Settings

| Setting | Default | Meaning |
|---|---|---|
| Check interval | 30 seconds | How often the waiting items are put into the inventories |
| Named items | 3 | How many waiting items are named when the player is told about them |

## Messages

When the character enters the game or opens the vault, the player is told which
items wait: one line with their number, one line per item (name, level and
size), and "... and N more." for the rest. Each delivered item is announced with
its own line.

The chat command `/waiting` lists the waiting items, by default up to 10 (its
setting **Named items**). The automatic message points to it when the command is
active.

## Deleting a character

The waiting items belong to the character and are deleted with it. Because an
item from the vault belongs to the whole account, a character with waiting items
can't be deleted. The player is asked to enter the game with it and make room
first.

An item whose item definition no longer exists isn't delivered or listed. It
stays in the database until the definition is back, like an inventory item
without a definition, and it prevents the deletion, too. Then, the player is
asked to contact an admin.
1 change: 1 addition & 0 deletions docs-website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const sidebars = {
"server-features/cash-shop",
"server-features/discord",
"server-features/player-shop-currencies",
"server-features/waiting-items",
],
},
{
Expand Down
6 changes: 6 additions & 0 deletions src/DataModel/Entities/Character.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ public class Character
[MemberOfAggregate]
public virtual ICollection<CharacterQuestState> QuestStates { get; protected set; } = null!;

/// <summary>
/// Gets or sets the items which wait for space in the <see cref="Inventory"/>.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<WaitingItem> WaitingItems { get; protected set; } = null!;

/// <inheritdoc />
public override string ToString()
{
Expand Down
34 changes: 34 additions & 0 deletions src/DataModel/Entities/WaitingItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// <copyright file="WaitingItem.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.DataModel.Entities;

/// <summary>
/// An item of a character which waits for space in the inventory.
/// The game server puts it into the inventory as soon as it fits there.
/// </summary>
public class WaitingItem
{
/// <summary>
/// Gets or sets the item.
/// </summary>
[MemberOfAggregate]
public virtual Item? Item { get; set; }

/// <summary>
/// Gets or sets the reason why the item waits.
/// </summary>
public WaitingItemReason Reason { get; set; }

/// <summary>
/// Gets or sets the time when the item was added to the waiting items.
/// </summary>
public DateTime AddedAt { get; set; }

/// <inheritdoc />
public override string ToString()
{
return $"{this.Item} ({this.Reason})";
}
}
17 changes: 17 additions & 0 deletions src/DataModel/Entities/WaitingItemReason.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// <copyright file="WaitingItemReason.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.DataModel.Entities;

/// <summary>
/// The reason why a <see cref="WaitingItem"/> waits for space in the inventory.
/// </summary>
public enum WaitingItemReason
{
/// <summary>
/// The item found no free place when its storage was loaded,
/// e.g. because the size of its definition was changed in the data.
/// </summary>
NoSpaceWhenLoaded,
}
45 changes: 45 additions & 0 deletions src/DataModel/Properties/ModelResources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -3621,4 +3621,49 @@
<data name="WarpInfo_IsWarpable_Description" xml:space="preserve">
<value>Ob Spieler diesen Warp verwenden können. Er schränkt Spieler nur ein, wenn er ausgeschaltet ist: Dann ist der Warp nicht in der Warp-Liste des Clients, und die Warp-Anfrage eines Spielers wird abgelehnt; Game Master können ihn weiterhin verwenden.</value>
</data>
<data name="Character_WaitingItems_Caption" xml:space="preserve">
<value>Wartende Gegenstände</value>
</data>
<data name="Character_WaitingItems_Description" xml:space="preserve">
<value>Gegenstände, die in ihrem Lagerplatz keinen Platz fanden und auf Platz im Inventar warten. Sie werden hineingelegt, sobald sie passen.</value>
</data>
<data name="WaitingItem_TypeCaption" xml:space="preserve">
<value>Wartender Gegenstand</value>
</data>
<data name="WaitingItem_TypeCaptionPlural" xml:space="preserve">
<value>Wartende Gegenstände</value>
</data>
<data name="WaitingItem_TypeDescription" xml:space="preserve">
<value>Ein Gegenstand, der auf Platz im Inventar eines Charakters wartet.</value>
</data>
<data name="WaitingItem_Item_Caption" xml:space="preserve">
<value>Gegenstand</value>
</data>
<data name="WaitingItem_Item_Description" xml:space="preserve">
<value>Der wartende Gegenstand.</value>
</data>
<data name="WaitingItem_Reason_Caption" xml:space="preserve">
<value>Grund</value>
</data>
<data name="WaitingItem_Reason_Description" xml:space="preserve">
<value>Warum der Gegenstand wartet.</value>
</data>
<data name="WaitingItem_AddedAt_Caption" xml:space="preserve">
<value>Hinzugefügt am</value>
</data>
<data name="WaitingItem_AddedAt_Description" xml:space="preserve">
<value>Seit wann der Gegenstand wartet (UTC). Die ältesten Gegenstände werden zuerst ins Inventar gelegt.</value>
</data>
<data name="WaitingItemReason_TypeCaption" xml:space="preserve">
<value>Grund für wartenden Gegenstand</value>
</data>
<data name="WaitingItemReason_TypeDescription" xml:space="preserve">
<value>Warum ein Gegenstand auf Platz im Inventar wartet.</value>
</data>
<data name="WaitingItemReason_NoSpaceWhenLoaded_Caption" xml:space="preserve">
<value>Kein Platz beim Laden</value>
</data>
<data name="WaitingItemReason_NoSpaceWhenLoaded_Description" xml:space="preserve">
<value>Der Gegenstand fand beim Laden seines Lagerplatzes keinen Platz, z. B. weil die Größe seiner Definition gewachsen ist.</value>
</data>
</root>
45 changes: 45 additions & 0 deletions src/DataModel/Properties/ModelResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -6529,4 +6529,49 @@
<data name="WarpInfo_IsWarpable_Description" xml:space="preserve">
<value>Whether players can use this warp. It only restricts players when it's off: then the warp isn't in the warp list of the client and the warp request of a player is refused, while game masters can still use it.</value>
</data>
<data name="Character_WaitingItems_Caption" xml:space="preserve">
<value>Waiting Items</value>
</data>
<data name="Character_WaitingItems_Description" xml:space="preserve">
<value>Items which found no place in their storage and wait for space in the inventory. They are put into it as soon as they fit.</value>
</data>
<data name="WaitingItem_TypeCaption" xml:space="preserve">
<value>Waiting Item</value>
</data>
<data name="WaitingItem_TypeCaptionPlural" xml:space="preserve">
<value>Waiting Items</value>
</data>
<data name="WaitingItem_TypeDescription" xml:space="preserve">
<value>An item which waits for space in the inventory of a character.</value>
</data>
<data name="WaitingItem_Item_Caption" xml:space="preserve">
<value>Item</value>
</data>
<data name="WaitingItem_Item_Description" xml:space="preserve">
<value>The item which waits.</value>
</data>
<data name="WaitingItem_Reason_Caption" xml:space="preserve">
<value>Reason</value>
</data>
<data name="WaitingItem_Reason_Description" xml:space="preserve">
<value>Why the item waits.</value>
</data>
<data name="WaitingItem_AddedAt_Caption" xml:space="preserve">
<value>Added At</value>
</data>
<data name="WaitingItem_AddedAt_Description" xml:space="preserve">
<value>When the item started to wait (UTC). The oldest items are put into the inventory first.</value>
</data>
<data name="WaitingItemReason_TypeCaption" xml:space="preserve">
<value>Waiting Item Reason</value>
</data>
<data name="WaitingItemReason_TypeDescription" xml:space="preserve">
<value>Why an item waits for space in the inventory.</value>
</data>
<data name="WaitingItemReason_NoSpaceWhenLoaded_Caption" xml:space="preserve">
<value>No Space When Loaded</value>
</data>
<data name="WaitingItemReason_NoSpaceWhenLoaded_Description" xml:space="preserve">
<value>The item found no place when its storage was loaded, e.g. because the size of its definition grew.</value>
</data>
</root>
45 changes: 45 additions & 0 deletions src/DataModel/Properties/ModelResources.zh-CN.resx
Original file line number Diff line number Diff line change
Expand Up @@ -6460,4 +6460,49 @@
<data name="ItemRequirementFormula_Attribute_Description" xml:space="preserve">
<value>此公式适用的物品需求属性。</value>
</data>
<data name="Character_WaitingItems_Caption" xml:space="preserve">
<value>等待中的物品</value>
</data>
<data name="Character_WaitingItems_Description" xml:space="preserve">
<value>在其存放位置找不到空间、正在等待背包空间的物品。有足够空间时会自动放入背包。</value>
</data>
<data name="WaitingItem_TypeCaption" xml:space="preserve">
<value>等待中的物品</value>
</data>
<data name="WaitingItem_TypeCaptionPlural" xml:space="preserve">
<value>等待中的物品</value>
</data>
<data name="WaitingItem_TypeDescription" xml:space="preserve">
<value>正在等待角色背包空间的物品。</value>
</data>
<data name="WaitingItem_Item_Caption" xml:space="preserve">
<value>物品</value>
</data>
<data name="WaitingItem_Item_Description" xml:space="preserve">
<value>正在等待的物品。</value>
</data>
<data name="WaitingItem_Reason_Caption" xml:space="preserve">
<value>原因</value>
</data>
<data name="WaitingItem_Reason_Description" xml:space="preserve">
<value>物品等待的原因。</value>
</data>
<data name="WaitingItem_AddedAt_Caption" xml:space="preserve">
<value>添加时间</value>
</data>
<data name="WaitingItem_AddedAt_Description" xml:space="preserve">
<value>物品开始等待的时间(UTC)。最早的物品最先放入背包。</value>
</data>
<data name="WaitingItemReason_TypeCaption" xml:space="preserve">
<value>物品等待原因</value>
</data>
<data name="WaitingItemReason_TypeDescription" xml:space="preserve">
<value>物品等待背包空间的原因。</value>
</data>
<data name="WaitingItemReason_NoSpaceWhenLoaded_Caption" xml:space="preserve">
<value>加载时无空间</value>
</data>
<data name="WaitingItemReason_NoSpaceWhenLoaded_Description" xml:space="preserve">
<value>加载存放位置时物品找不到空间,例如其定义的尺寸变大了。</value>
</data>
</root>
47 changes: 45 additions & 2 deletions src/GameLogic/InventoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.GameLogic.WaitingItems;
using MUnique.OpenMU.PlugIns;
using static MUnique.OpenMU.DataModel.InventoryConstants;

Expand All @@ -25,11 +26,26 @@ public class InventoryStorage : Storage, IInventoryStorage
/// <param name="player">The player.</param>
/// <param name="context">The game context.</param>
public InventoryStorage(Player player, IGameContext context)
: this(player, context, [])
{
}

/// <summary>
/// Initializes a new instance of the <see cref="InventoryStorage" /> class.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="context">The game context.</param>
/// <param name="itemsWithoutPlace">
/// Collects the items which find no place in the main inventory or in their extension. They get another try
/// when all extensions exist, and only the ones which still find no place wait for space.
/// </param>
private InventoryStorage(Player player, IGameContext context, List<Item> itemsWithoutPlace)
: base(
GetInventorySize(0),
EquippableSlotsCount,
0,
new ItemStorageAdapter(player.SelectedCharacter?.Inventory ?? throw Error.NotInitializedProperty(player, "SelectedCharacter.Inventory"), FirstEquippableItemSlotIndex, player.InventorySize))
new ItemStorageAdapter(player.SelectedCharacter?.Inventory ?? throw Error.NotInitializedProperty(player, "SelectedCharacter.Inventory"), FirstEquippableItemSlotIndex, player.InventorySize),
KeepItemWithoutPlace(itemsWithoutPlace))
{
this._player = player;
this.EquippedItemsChanged += async eventArgs => await this.UpdateItemsOnChangeAsync(eventArgs.Item, eventArgs.IsEquipped).ConfigureAwait(false);
Expand All @@ -43,13 +59,25 @@ public InventoryStorage(Player player, IGameContext context)
for (int i = 0; i < player.SelectedCharacter.InventoryExtensions; i++)
{
var offset = FirstExtensionItemSlotIndex + (i * sizePerExtension);
var extension = new Storage(sizePerExtension, 0, offset, this.ItemStorage);
var extension = new Storage(sizePerExtension, 0, offset, this.ItemStorage, KeepItemWithoutPlace(itemsWithoutPlace));
extensions.Add(extension);
}

this.Extensions = extensions;
}

if (itemsWithoutPlace.Count > 0)
{
// The items are still in the item storage. Like in the Storage, each one is removed only after
// it was put aside, so that none is lost when putting it aside throws.
var putAside = player.PutAsideWhenLoaded(player.SelectedCharacter);
foreach (var item in itemsWithoutPlace.Where(item => !this.TryAddItemWithoutPlace(item)).ToList())
{
putAside(item);
this.ItemStorage.Items.Remove(item);
}
}

this.InitializePowerUps();
}

Expand Down Expand Up @@ -151,6 +179,21 @@ public async ValueTask RaiseEquippedItemsChangedAsync(Item item, bool isEquipped
}
}

/// <summary>
/// Creates the function which keeps an item, which finds no place when a storage is loaded, in the given list
/// instead of putting it aside, so that it gets another try when all extensions exist.
/// </summary>
/// <param name="itemsWithoutPlace">The list of the items without place.</param>
/// <returns>The function; it doesn't take the item, so it stays in the item storage.</returns>
private static Func<Item, bool> KeepItemWithoutPlace(List<Item> itemsWithoutPlace)
{
return item =>
{
itemsWithoutPlace.Add(item);
return false;
};
}

private bool IsWearingSlot(int slot)
{
return slot >= FirstEquippableItemSlotIndex && slot <= LastEquippableItemSlotIndex;
Expand Down
13 changes: 13 additions & 0 deletions src/GameLogic/PlayerActions/Character/DeleteCharacterAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;

using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.GameLogic.WaitingItems;
using MUnique.OpenMU.Interfaces;

/// <summary>
Expand Down Expand Up @@ -62,6 +63,18 @@ private async ValueTask<CharacterDeleteResult> DeleteCharacterRequestAsync(Playe
return CharacterDeleteResult.Unsuccessful;
}

// The waiting items are deleted with the character, but an item which waits because it found no place
// in the vault belongs to the account. An item without a definition is kept until the definition is back,
// so it prevents the deletion, too; only an admin can solve that.
if (character.WaitingItems is { Count: > 0 })
{
var message = character.HasDeliverableWaitingItems()
? nameof(PlayerMessage.CantDeleteCharacterWithWaitingItems)
: nameof(PlayerMessage.CantDeleteCharacterWithUndeliverableWaitingItems);
await player.ShowLocalizedBlueMessageAsync(message).ConfigureAwait(false);
return CharacterDeleteResult.Unsuccessful;
}

player.Account.Characters.Remove(character);
player.GameContext.PlugInManager.GetPlugInPoint<ICharacterDeletedPlugIn>()?.CharacterDeleted(player, character);
await player.PersistenceContext.DeleteAsync(character).ConfigureAwait(false);
Expand Down
Loading
Loading