Table of Contents

Class CharacterInventorySystem

Namespace
FishMMO.Server.Implementation.World.SceneServer
Assembly
FishMMO.Server.dll

Manages player character inventory, equipment, and bank operations with validation and database persistence.

[CreateAssetMenu(fileName = "CharacterInventorySystem", menuName = "FishMMO/Server/SceneServer/Character Inventory System", order = 1)]
[RequiresDataContainer(typeof(CharacterInventorySystemRuntimeData))]
[RequiresDataContainer(typeof(CharacterInventorySystemMainThreadQueueData))]
[RequiresDataContainer(typeof(AsyncWorkerData))]
public class CharacterInventorySystem : ServerBehaviour, IServerBehaviour<INetworkManagerWrapper, ServerManager, NetworkConnection, IServerBehaviour>, IServerComponent<INetworkManagerWrapper, ServerManager, NetworkConnection, IServerBehaviour>, ICharacterInventorySystem, IServerBehaviour, IServerComponent
Inheritance
Object
ScriptableObject
CharacterInventorySystem
Implements
IServerBehaviour<INetworkManagerWrapper, ServerManager, NetworkConnection, IServerBehaviour>
IServerComponent<INetworkManagerWrapper, ServerManager, NetworkConnection, IServerBehaviour>
Inherited Members
ScriptableObject.SetDirty()
ScriptableObject.CreateInstance<T>()
Object.GetEntityId()
Object.GetInstanceID()
Object.GetHashCode()
Object.InstantiateAsync<T>(T)
Object.InstantiateAsync<T>(T, Transform)
Object.InstantiateAsync<T>(T, Vector3, Quaternion)
Object.InstantiateAsync<T>(T, Transform, Vector3, Quaternion)
Object.Instantiate(Object, Vector3, Quaternion)
Object.Instantiate(Object, Vector3, Quaternion, Transform)
Object.Instantiate(Object)
Object.Instantiate(Object, Scene)
Object.Instantiate<T>(T, InstantiateParameters)
Object.Instantiate<T>(T, Vector3, Quaternion, InstantiateParameters)
Object.Instantiate(Object, Transform)
Object.Instantiate<T>(T)
Object.Instantiate<T>(T, Vector3, Quaternion)
Object.Instantiate<T>(T, Vector3, Quaternion, Transform)
Object.Instantiate<T>(T, Transform)
Object.Destroy(Object)
Object.DestroyImmediate(Object)
Object.DontDestroyOnLoad(Object)
Object.DestroyObject(Object)
Object.FindObjectsOfType<T>()
Object.FindObjectsByType<T>(FindObjectsSortMode)
Object.FindObjectsByType<T>(FindObjectsInactive, FindObjectsSortMode)
Object.FindObjectOfType<T>()
Object.FindFirstObjectByType<T>()
Object.FindAnyObjectByType<T>()
Object.FindFirstObjectByType<T>(FindObjectsInactive)
Object.FindAnyObjectByType<T>(FindObjectsInactive)
Object.FindObjectsByType<T>()
Object.FindObjectsByType<T>(FindObjectsInactive)
Object.ToString()
Object.name
Object.hideFlags

Methods

CaptureDespawnFlush(IPlayerCharacter, CharacterSessionInfo?)

Captures a full snapshot of a departing character's containers, to be awaited by the character system before it releases the session.

public Func<Task> CaptureDespawnFlush(IPlayerCharacter character, CharacterSessionInfo? lease)

Parameters

character IPlayerCharacter

The departing character.

lease CharacterSessionInfo?

The session this server still holds for it, already taken out of the live token map.

Returns

Func<Task>

The flush to await, or null.

Remarks

Main thread only, while the character is still resident. The returned work writes the snapshot in one transaction under lease; the caller awaits it before the release so the next owner reads it. Returns null when there is nothing to capture.

InitializeOnce()

Initializes the character inventory system, registering broadcast handlers for inventory, equipment, and bank actions.

public override ServerComponentInitializationStatus InitializeOnce()

Returns

ServerComponentInitializationStatus

NotifyInventorySlots(IPlayerCharacter, IReadOnlyList<Item>, IReadOnlyList<int>)

Tells the owning client which inventory slots an operation outside this system changed: emptied slots are cleared, then set items are written to their slots.

public void NotifyInventorySlots(IPlayerCharacter character, IReadOnlyList<Item> set, IReadOnlyList<int> emptied)

Parameters

character IPlayerCharacter
set IReadOnlyList<Item>
emptied IReadOnlyList<int>

Remarks

Removes go first, and only for slots that are still empty, because an exchange can vacate a slot and fill it again in the same step.

OnDeinitialize()

Cleans up the character inventory system, unregistering broadcast handlers for inventory, equipment, and bank actions.

public override void OnDeinitialize()

OnUpdate(float)

Sweeps stale ingress guard entries and drives the periodic item snapshot.

protected override void OnUpdate(float deltaTime)

Parameters

deltaTime float

PersistInventoryChanges(IPlayerCharacter, IReadOnlyList<Item>, IReadOnlyList<RemovedItemRecord>)

Persists inventory rows that changed or ceased to exist outside this system's own handlers — a stack reduced by a sale or a mail attachment, an item sold or attached whole — through the same journalled batch every other item write uses.

public void PersistInventoryChanges(IPlayerCharacter character, IReadOnlyList<Item> changed, IReadOnlyList<RemovedItemRecord> removed)

Parameters

character IPlayerCharacter

The owning character.

changed IReadOnlyList<Item>

Items whose rows changed, or null.

removed IReadOnlyList<RemovedItemRecord>

Items that were removed, with the versions that authorise the deletes, or null.

Remarks

The journal is what orders these against the periodic snapshot. A delete issued around it could commit after a snapshot captured while the item still existed, and the snapshot would then put the row back. Callers have already told the client themselves.

SwapContainerItems(IItemContainer, IItemContainer, int, int, out List<Item>, out List<long>, out List<Item>)

Swaps items between two containers and collects affected items and deleted slots.

public bool SwapContainerItems(IItemContainer from, IItemContainer to, int fromIndex, int toIndex, out List<Item> affectedFromItems, out List<long> deletedFromSlots, out List<Item> affectedToItems)

Parameters

from IItemContainer

Source item container.

to IItemContainer

Target item container.

fromIndex int

Source slot index.

toIndex int

Target slot index.

affectedFromItems List<Item>

Out: items placed into the source container.

deletedFromSlots List<long>

Out: slot indices vacated in the source container.

affectedToItems List<Item>

Out: items placed into the destination container.

Returns

bool

True if swap succeeded, false otherwise.

SwapContainerItems(IItemContainer, int, int, out List<Item>)

Swaps two items within the same container and collects the affected items.

public bool SwapContainerItems(IItemContainer container, int fromIndex, int toIndex, out List<Item> affectedItems)

Parameters

container IItemContainer

Item container to swap items in.

fromIndex int

Source slot index.

toIndex int

Target slot index.

affectedItems List<Item>

Out: list of items whose slots changed.

Returns

bool

True if swap succeeded, false otherwise.

SwapContainerItems(IItemContainer, int, int, out List<Item>, out int)

Swaps two items within the same container, reporting both the affected items and any slot the swap left empty.

public bool SwapContainerItems(IItemContainer container, int fromIndex, int toIndex, out List<Item> affectedItems, out int vacatedSlot)

Parameters

container IItemContainer

Item container to swap items in.

fromIndex int

Source slot index.

toIndex int

Target slot index.

affectedItems List<Item>

Out: list of items whose slots changed.

vacatedSlot int

Out: slot the swap emptied, or -1 when both slots stay filled.

Returns

bool

True if swap succeeded, false otherwise.

Remarks

The vacated slot is the whole reason this overload exists. Item rows are keyed (character_id, slot), so a move onto an EMPTY slot writes the item at its new index and leaves the row at the old index untouched — the same item persisted twice. Only the write side was emitted here before, because affectedItems collects non-null items and a move-to-empty produces exactly one of them, which reads like a complete description of the change and is not.

The 60-second snapshot prunes the stale row, so this never survived a clean shutdown — but "duplicated until the next snapshot, permanently if the scene server dies first" is not a guarantee worth relying on when the delete costs one list entry.

TryGrantItem(IPlayerCharacter, Item, InventoryType)

Grants an item to one of a character's containers: places it, tells the owning client, persists it, and hands it the identity the database assigns.

public bool TryGrantItem(IPlayerCharacter character, Item item, InventoryType container)

Parameters

character IPlayerCharacter

The character receiving the item.

item Item

The item. May be merged into stacks the character already holds.

container InventoryType

Which container receives it.

Returns

bool

True when the whole item was placed.

Remarks

THE grant path. Every server-side source of a new item — pickups, corpse and chest loot, merchant purchases, mail, quest and achievement rewards, the ECA give action — comes through here, because it is the only path that completes an item: a fresh item has no database identity, and until its first write returns one it cannot be equipped (the attribute ledger declines an id of zero), used, moved, or safely written a second time. The slot it lands in stays locked until that identity arrives; see TryPersistGrantedItems(IPlayerCharacter, List<Item>, InventoryType, string).

All-or-nothing on the container: the whole stack fits or nothing is placed, so a caller that reserved the item elsewhere (took it off a corpse, charged for it) can put it back on a false return.

TryPersistGrantedItems(IPlayerCharacter, List<Item>, InventoryType, string)

Persists a grant of items — freshly created items and the existing stacks they merged into — through the atomic item-batch machinery, so an item the database has never seen (ID 0) gets the identity the write returns assigned onto the live Item and the owning client is told the final instance id and seed.

public bool TryPersistGrantedItems(IPlayerCharacter character, List<Item> modifiedItems, InventoryType container, string operation)

Parameters

character IPlayerCharacter

The character the items were granted to.

modifiedItems List<Item>

The items the grant touched, as returned by the container add.

container InventoryType

The container the items are in.

operation string

Short operation name used in persistence log lines.

Returns

bool

True when the batch was enqueued normally; false when the bounded queue was full and the write ran on the fallback path. Never a rollback signal — memory is already authoritative.

Remarks

An item with no identity has its slot LOCKED from here until the identity lands, so nothing can move, merge, consume or equip it in between. That window is one database round trip. Without the lock a second write captured in that window inserted a second row for the same item, which the load path then handed back as two items.

TryRunExchange(IPlayerCharacter, IPlayerCharacter, Func<ItemExchangeLeg[]>, Action<bool>, string)

Runs a two-character exchange — a completed player trade — as ONE database transaction whose commit is the point of truth, so a crash at any instant leaves the database holding either the whole trade or none of it, and memory never diverges from that in a direction that duplicates anything.

public bool TryRunExchange(IPlayerCharacter first, IPlayerCharacter second, Func<ItemExchangeLeg[]> applyOnMainThread, Action<bool> onFinished, string operation)

Parameters

first IPlayerCharacter

One party.

second IPlayerCharacter

The other party.

applyOnMainThread Func<ItemExchangeLeg[]>

Called on the main thread while both row locks are held. Returns the two legs (first's then second's) after mutating memory, or null to abort before anything was mutated.

onFinished Action<bool>

Called on the main thread once, with true when the transaction committed and false when it was aborted or rolled back. Only called with false after a null apply, or with the memory mutation still in place and every touched slot already unlocked, so the caller can undo it.

operation string

Short operation name used in persistence log lines.

Returns

bool

True when the exchange was accepted for execution. False means nothing will run and nothing was changed.

Remarks

THE PROTOCOL, in order, because the order is the guarantee:

  1. Worker: open the transaction and take BOTH characters' session row locks, in ascending id order. From here no other write for either character can commit until this transaction ends: every write takes the same lock.
  2. Main thread, while the locks are held: applyOnMainThread re-validates and mutates memory (or returns null to abort with nothing changed), and the resulting rows are captured with their journal sequences and versions stamped NOW. Every write captured before this instant therefore carries a lower sequence and lower versions than the trade; every write captured after carries the trade's content. The slots in TouchedSlots are locked until the outcome is known.
  3. Worker: write both characters' rows and the ledger, commit.
  4. Main thread: unlock, then onFinished(true). On any refusal: roll back, unlock, onFinished(false) — the caller undoes its memory mutation exactly, which the locks make possible — then every write captured from memory while the exchange was applied is VOIDED in the journal, because its content described a trade that never happened, and both characters are reconciled from the restored memory.

Crash analysis: before the commit, memory is lost and the database rolls back — the trade never happened for either side. After the commit, the database holds the whole trade for both. A write captured before the apply that lands after the commit is older by sequence and version and is refused. There is no interleaving in which one character's half is durable and the other's is not.