Table of Contents

Interface ICharacterInventorySystem

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

Engine-agnostic public API for character inventory operations. Implementations perform item container manipulations and coordinate any necessary database updates or client notifications.

public interface ICharacterInventorySystem : IServerBehaviour, IServerComponent

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.

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.

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.

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.

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.

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 the affected items along with any slot deletions that occurred during the cross-container move.

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 container.

to IItemContainer

Destination container.

fromIndex int

Source slot index.

toIndex int

Destination slot index.

affectedFromItems List<Item>

Out: items placed into the source container that need persistence.

deletedFromSlots List<long>

Out: slot indices that were vacated in the source container and need deletion.

affectedToItems List<Item>

Out: items placed into the destination container that need persistence.

Returns

bool

True when the cross-container swap succeeded; otherwise false.

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

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

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

Parameters

container IItemContainer

The container instance in which the swap occurs.

fromIndex int

Source slot index.

toIndex int

Target slot index.

affectedItems List<Item>

Out: list of items whose slot assignments changed and need persistence.

Returns

bool

True when the swap succeeded; otherwise false.

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.

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.

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.

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.