Skip to content
Open
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
59 changes: 59 additions & 0 deletions docs-website/docs/server-features/inventory-charms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: Figurines and charms
sidebar_label: Figurines and charms
description: Figurines and charms give a bonus while used in the inventory.
---

The figurines and charms are items of the in-game shop. They are not worn in
an equipment slot. Instead, the player right clicks them inside the inventory
to use them, and the bonus applies as long as they stay in use there. Another
right click stops the use.

| Item | Number | Bonus |
| ------------------------ | ------ | ------------------------------ |
| Hawk Figurine | 13/128 | +10 critical damage |
| Goat Figurine | 13/129 | +10 excellent damage |
| Oak Charm | 13/130 | +50 maximum health |
| Maple Charm | 13/131 | +50 maximum mana |
| Golden Oak Charm | 13/132 | +150 health, +50 shield |
| Golden Maple Charm | 13/133 | +150 mana, +50 ability (AG) |

The values are the ones which the game client shows in the item tooltip. They
are stored as power-ups of the item definitions, so they can be changed in the
admin panel.

## Rules

* Only one item of the same kind can be in use at the same time. Using a
second one is refused with a message.
* The item keeps its state when it is moved inside the inventory.
* When the item leaves the inventory, for example when it is dropped or moved
into the vault, it is no longer in use, and the bonus is removed.
* Like in the original game, the state is stored in the durability of the
item: 254 means in use, 255 means not in use. A new item starts with 255
when it is added to the inventory. The item definitions have a durability
of 1, so the items are not stackable.

## Configuration

The *Items used inside the inventory* feature plugin defines which items can
be used inside the inventory. By default, these are the items 13/128 to
13/134. When the plugin is deactivated, no item can be used this way.

## Update of existing databases

The update *Add the figurines and charms* adds the items, and links the
products of the cash shop to them. Their packages were not for sale before,
because their products had no item. The update leaves them that way, so the
administrator can put them on sale in the admin panel.

## Protocol

The client sends the request `C1 BF 20` with the inventory slot and the
requested state. The server answers with the same packet and the new state of
the item.

:::note
The Worn Horseshoe (13/134) is not added yet, because there is no attribute
for the item drop rate yet.
:::
1 change: 1 addition & 0 deletions docs-website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const sidebars = {
"server-features/anti-cheat",
"server-features/test-actors",
"server-features/gens",
"server-features/inventory-charms",
"server-features/cash-shop",
"server-features/discord",
"server-features/player-shop-currencies",
Expand Down
1 change: 1 addition & 0 deletions docs/Progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ complexity and effort). Complexity 0 means we wont implement it.
| *JewelMix* | 0xBC | 100% | 4 | |
| *CrywolfGroup* | 0xBD | 100% | 10 | |
| *IllusionTempleGroup* | 0xBF | 100% | 10 | Team-based PvP mini game: relic pickup/delivery, scoring, skill points, special skills, rewards |
| *InventoryItemEquip* | 0xBF20 | 100% | 2 | Using figurines and charms inside the inventory |
| FriendListRequest | 0xC0 | 0% | 0 | Not needed, friend list is sent automatically |
| *FriendAdd* | 0xC1 | 100% | 2 | |
| *WaitFriendAdd* | 0xC2 | 100% | 2 | |
Expand Down
39 changes: 39 additions & 0 deletions src/GameLogic/InventoryItemUseConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// <copyright file="InventoryItemUseConfiguration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic;

using MUnique.OpenMU.DataModel.Configuration.Items;

/// <summary>
/// The configuration of the items which are used inside the inventory, like the figurines and charms.
/// </summary>
public class InventoryItemUseConfiguration
{
/// <summary>
/// Gets or sets the items which are used inside the inventory instead of being equipped.
/// </summary>
/// <remarks>
/// By default, these are the figurines (group 13, number 128 and 129), the charms (130 to 133)
/// and the worn horseshoe (134), which the game client requests to use with a right click in the inventory.
/// </remarks>
public IList<InventoryUsedItemIdentifier> Items { get; set; } = CreateDefaultItems();

/// <summary>
/// Determines whether the item definition belongs to an item which is used inside the inventory.
/// </summary>
/// <param name="definition">The item definition.</param>
/// <returns><c>true</c>, if items of this definition are used inside the inventory; otherwise, <c>false</c>.</returns>
public bool Contains(ItemDefinition definition)
{
return this.Items.Any(id => id.Group == definition.Group && id.Number == definition.Number);
}

private static IList<InventoryUsedItemIdentifier> CreateDefaultItems()
{
return Enumerable.Range(128, 7)
.Select(number => new InventoryUsedItemIdentifier { Group = 13, Number = (short)number })
.ToList();
}
}
40 changes: 40 additions & 0 deletions src/GameLogic/InventoryItemUseFeaturePlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// <copyright file="InventoryItemUseFeaturePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic;

using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// The use of items inside the inventory, like the figurines and charms.
/// While such an item is in use, its power-ups apply, although it isn't equipped.
/// When it's deactivated, no item can be used inside the inventory.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.InventoryItemUseFeaturePlugIn_Name), Description = nameof(PlugInResources.InventoryItemUseFeaturePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("0C9C3D52-7B0E-4E55-9D1B-2E6A35C4F1A8")]
public class InventoryItemUseFeaturePlugIn : IFeaturePlugIn, ISupportCustomConfiguration<InventoryItemUseConfiguration>, ISupportDefaultCustomConfiguration
{
/// <inheritdoc />
public InventoryItemUseConfiguration? Configuration { get; set; }

/// <summary>
/// Gets the configuration of the items which are used inside the inventory.
/// </summary>
/// <param name="gameContext">The game context.</param>
/// <returns>The configuration; <c>null</c>, if the feature is deactivated.</returns>
public static InventoryItemUseConfiguration? GetActiveConfiguration(IGameContext gameContext)
{
return gameContext.FeaturePlugIns.GetPlugIn<InventoryItemUseFeaturePlugIn>() is { } plugIn
? plugIn.Configuration ?? new InventoryItemUseConfiguration()
: null;
}

/// <inheritdoc />
public object CreateDefaultConfig()
{
return new InventoryItemUseConfiguration();
}
}
112 changes: 110 additions & 2 deletions src/GameLogic/InventoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class InventoryStorage : Storage, IInventoryStorage

private readonly Player _player;

private Item? _itemMovedInsideInventory;

/// <summary>
/// Initializes a new instance of the <see cref="InventoryStorage" /> class.
/// </summary>
Expand Down Expand Up @@ -113,6 +115,22 @@ public override async ValueTask<bool> AddItemAsync(byte slot, Item item)
{
await this.RaiseEquippedItemsChangedAsync(convertedItem ?? item, true).ConfigureAwait(false);
}
else
{
this.OnItemAddedToInventory(convertedItem ?? item);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Synchronize temporary pickups with the normalized use state

When a player picks up a GM-created charm, GameMasterItemFactory supplies a TemporaryItem with durability 1. This new call normalizes only its persistent copy to 255. DroppedItem.TryPickUpAsync copies only the slot back to the temporary item, and PickupItemAction.PickupItemAsync passes that original object to IItemAppearPlugIn, so the item-added packet contains durability 1 while the server's inventory contains 255. Normalize the source too, or serialize the inserted persistent item, so the pickup response carries the actual inventory-use state.

Useful? React with 👍 / 👎.

}
}

return success;
}

/// <inheritdoc />
public override async ValueTask<bool> AddItemAsync(Item item)
{
var success = await base.AddItemAsync(item).ConfigureAwait(false);
if (success && !this.IsWearingSlot(item.ItemSlot))
{
this.OnItemAddedToInventory(item);
}

return success;
Expand All @@ -127,6 +145,23 @@ public override async ValueTask RemoveItemAsync(Item item)
{
await this.RaiseEquippedItemsChangedAsync(item, false).ConfigureAwait(false);
}
else
{
this.OnItemRemovedFromInventory(item);
}
}

/// <summary>
/// Keeps the use state and the power-ups of an item which is used inside the inventory,
/// while it's moved to another slot of the inventory, until the returned object is disposed.
/// Otherwise, removing the item would stop its use, like when it leaves the inventory.
/// </summary>
/// <param name="item">The item which is moved inside the inventory.</param>
/// <returns>The object which ends keeping the use state when it's disposed.</returns>
public IDisposable KeepInventoryUseWhileMoving(Item item)
{
this._itemMovedInsideInventory = item;
return new MoveInsideInventoryScope(this);
}

/// <summary>
Expand All @@ -151,6 +186,52 @@ public async ValueTask RaiseEquippedItemsChangedAsync(Item item, bool isEquipped
}
}

/// <summary>
/// Handles an item which was added to an inventory slot which isn't equippable.
/// An item which is used inside the inventory gets its power-ups while it's in use.
/// </summary>
/// <param name="item">The item.</param>
private void OnItemAddedToInventory(Item item)
{
if (this._player.Attributes is not { } attributes
|| !item.IsUsedInsideInventory(this._gameContext))
{
return;
}

if (item.IsInUseInsideInventory(this._gameContext) && attributes.ItemPowerUps.ContainsKey(item))
{
// It's still in use, e.g. after it was moved inside the inventory.
return;
}

if ((int)item.Durability != InventoryUsedItemExtensions.InUseDurability
|| this.Items.Any(other => other != item && other.Definition == item.Definition && other.IsInUseInsideInventory(this._gameContext)))
{
// A new item starts without being used, and only one item of the same kind can be used at the same time.
item.Durability = InventoryUsedItemExtensions.NotInUseDurability;
}

attributes.UpdateInventoryUsedItemPowerUps(item, this._gameContext);
}

/// <summary>
/// Handles an item which was removed from an inventory slot which isn't equippable.
/// An item which is used inside the inventory stops being used when it leaves the inventory, e.g. when it's dropped.
/// </summary>
/// <param name="item">The item.</param>
private void OnItemRemovedFromInventory(Item item)
{
if (item == this._itemMovedInsideInventory
|| !item.IsUsedInsideInventory(this._gameContext))
{
return;
Comment on lines +225 to +228

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove applied bonuses even after the feature is disabled

If an operator disables the feature or removes an item from its configuration while a player has an active charm, the live plugin configuration makes IsUsedInsideInventory return false. This guard then skips cleanup when the player drops or vaults that charm, leaving its power-ups in ItemPowerUps and granting the bonus after the item has left the inventory. Remove already-applied power-ups independently of current eligibility, and reset the item's use state on actual inventory exits.

Useful? React with 👍 / 👎.

}

item.Durability = InventoryUsedItemExtensions.NotInUseDurability;
this._player.Attributes?.RemoveInventoryUsedItemPowerUps(item);
}

private bool IsWearingSlot(int slot)
{
return slot >= FirstEquippableItemSlotIndex && slot <= LastEquippableItemSlotIndex;
Expand Down Expand Up @@ -205,14 +286,18 @@ private void InitializePowerUps()
powerUp.Dispose();
}

var factory = this._gameContext?.ItemPowerUpFactory;
if (factory != null)
if (this._gameContext?.ItemPowerUpFactory is { } factory)
{
foreach (var item in this.EquippedItems)
{
this._player.Attributes.ItemPowerUps.Add(item, factory.GetPowerUps(item, this._player.Attributes).ToList());
}

foreach (var item in this.Items.Where(item => item.IsInUseInsideInventory(this._gameContext)))
{
this._player.Attributes.ItemPowerUps.Add(item, factory.GetPowerUps(item, this._player.Attributes).ToList());
}

this.UpdateSetPowerUps();
}
else
Expand All @@ -239,4 +324,27 @@ private void UpdateSetPowerUps()
var factory = this._gameContext.ItemPowerUpFactory;
this._player.Attributes.ItemSetPowerUps = factory.GetSetPowerUps(this.EquippedItems, this._player.Attributes, this._player.GameContext.Configuration).ToList();
}

/// <summary>
/// The scope in which an item is moved inside the inventory, see <see cref="KeepInventoryUseWhileMoving"/>.
/// </summary>
private sealed class MoveInsideInventoryScope : IDisposable
{
private readonly InventoryStorage _inventory;

/// <summary>
/// Initializes a new instance of the <see cref="MoveInsideInventoryScope"/> class.
/// </summary>
/// <param name="inventory">The inventory.</param>
public MoveInsideInventoryScope(InventoryStorage inventory)
{
this._inventory = inventory;
}

/// <inheritdoc />
public void Dispose()
{
this._inventory._itemMovedInsideInventory = null;
}
}
}
Loading
Loading