Table of Contents

Interface ICharacterItemService

Namespace
FishMMO.Database.Npgsql.Services.Interfaces
Assembly
FishMMO-DB.dll

Service for a character's items across every container.

public interface ICharacterItemService : IPersistAction<CharacterItemData, long>, IPersistManyAction<CharacterItemData>, IDeleteByKeyVersionedAction<long>, IFetchCollectionByKeyAction<long, CharacterItemData>
Inherited Members

Remarks

Replaces ICharacterInventoryService, ICharacterEquipmentService and ICharacterBankService, which were three copies of one interface over three tables.

IDENTITY CONTRACT — read this before touching the SQL. A row represents an ITEM, keyed by id. container and slot are ordinary mutable columns, so moving an item updates the row it already had rather than creating a new one. The previous schema keyed rows (character_id, slot) in three separate tables, which had three consequences that together made Item.ID useless as an identity and forced a second process-local id alongside it: an item that moved slots became a different row, two items through one slot shared a row, and the three tables' independent identity sequences handed the same number to three different items. None of those survive here.

A CharacterItemData.ID of zero means "never written". Both write paths accept one: they draw the next identity from the table's own sequence and return it, and the caller must write that value back onto the runtime item. An item whose id is never written back gets a fresh row on every save, which is churn rather than corruption — but it also means the item's attribute-ledger key changes underneath it, so the write-back is not optional.

Methods

DeleteItemAsync(long, long, long, CancellationToken)

Deletes one item by its identity, if incomingVersion is newer.

Task<DatabaseResult> DeleteItemAsync(long characterId, long itemId, long incomingVersion, CancellationToken cancellationToken = default)

Parameters

characterId long

The character that owns the item. Checked, so one character cannot delete another's row.

itemId long

The item's identity.

incomingVersion long

The authoritative, monotonic version for this delete.

cancellationToken CancellationToken

Token to cancel the operation.

Returns

Task<DatabaseResult>

Remarks

Addressed by the item rather than by the slot it happens to be sitting in. Under the old per-slot schema a delete had to name a (character, slot) pair and quote a version that belonged to whatever item occupied it, which is how a vacated slot ended up holding a tombstone stamped long.MaxValue and became permanently unwritable. There is no such ambiguity when the row and the item are the same thing.

SaveSnapshotAsync(long, IReadOnlyCollection<ItemContainerType>, IEnumerable<CharacterItemData>, CancellationToken)

Replaces every item this character owns with an authoritative snapshot of the server's in-memory state.

Task<DatabaseResult<IReadOnlyList<CharacterItemIdAssignment>>> SaveSnapshotAsync(long characterId, IReadOnlyCollection<ItemContainerType> containers, IEnumerable<CharacterItemData> items, CancellationToken cancellationToken = default)

Parameters

characterId long

The character whose items are being replaced.

containers IReadOnlyCollection<ItemContainerType>

Which containers this snapshot speaks for. Rows in a container NOT listed here are left untouched, so a caller that could only read two of the three containers does not prune the third. Listing a container and supplying none of its items is the legitimate way to say "this container is empty"; omitting it says "I do not know".

items IEnumerable<CharacterItemData>

Every item the character holds in containers.

cancellationToken CancellationToken

Token to cancel the operation.

Returns

Task<DatabaseResult<IReadOnlyList<CharacterItemIdAssignment>>>

The identity assigned to each row that was supplied without one.

Remarks

The backstop for the incremental per-item writes, which can be silently rejected (a stale version, a dropped async work item, a handler that returned early). Without it, a rejection meant permanent loss at the next login; with it, the worst case is a glitch that survives until the next save tick.

Every row for the character is deleted and re-inserted. Existing identities are supplied explicitly and preserved, so an item keeps its number; rows whose id is zero draw a new one and are reported back through the return value. Deleting first is what makes the statement immune to the (character_id, container, slot) unique index: two items swapping slots have no intermediate state in which both hold the same one.

Deliberately NOT version-gated. Version gating is the mechanism that makes an incremental write disappear, so gating the backstop as well would defeat its purpose. It is safe because the snapshot is authoritative by construction: it states, for one character at one instant, exactly which items exist and where they are. It cannot mint an item, and the delete cannot lose one, because every row it removes is a row the server has just re-stated or believes does not exist.

ORDERING REQUIREMENT: the caller must enqueue this through the same per-character key as the incremental writes, so the two are serialised FIFO — otherwise an in-flight snapshot can land after a newer incremental write and roll it back. It must also only be issued by a server that currently owns the character's session; there is no session guard here.