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
characterIdlongThe character that owns the item. Checked, so one character cannot delete another's row.
itemIdlongThe item's identity.
incomingVersionlongThe authoritative, monotonic version for this delete.
cancellationTokenCancellationTokenToken to cancel the operation.
Returns
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
characterIdlongThe character whose items are being replaced.
containersIReadOnlyCollection<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".
itemsIEnumerable<CharacterItemData>Every item the character holds in
containers.cancellationTokenCancellationTokenToken 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.