Решили: завести в RunState склад предметов забега (ItemInventory) и провести все правки снаряжения через один глагол SetSlotItem(slotIndex, itemSlot, itemId): надетое уходит со склада, снятое возвращается туда же. Отдельной команды «переложить от одного к другому» нет — это снятие плюс надевание.

Почему: страница «Предметы» показывает склад всех доступных вещей (дизайн §2.2), а хранить его было негде: в RunState жили только RelicInventory (реликвии) и PartyItemIds. Предметы «Сосуда» существовали исключительно надетыми — то есть вещь, снятая с бойца, исчезала из мира.

  • Прямая команда «переложить» отвергнута: ей нужны четыре числа (откуда, из какого слота, куда, в какой слот), а команда забега плоская — SlotIndex, Amount, Text — потому что едет по сети и ложится в лог. Пятое поле ради одного глагола удорожило бы каждую команду.
  • Проверка «вещь есть в складе» стоит у владельца, а не у нажавшего. Без неё гость, чей снимок состояния отстал, надел бы уже надетую вещь — и предмет размножился бы. Теперь SetSlotItem отказывает, если в складе такой вещи нет.
  • Лимита у склада нет (в отличие от реликвий с RelicCapacity): дефицит у предметов создают три слота на «Сосуде», а не вместимость запаса — так решено в ГДД про предметы.

Грабли: VesselItemIds в старых забегах короче, чем GameConfig.VesselItemSlots, и обращение по индексу слота падало бы. Массив дорастает до нужной длины прямо в мутаторе — на чтении это незаметно, а на записи ломается ровно один раз.

Владелец правды: Scripts/Guild/RunState.cs, Scripts/Guild/RunStateService.cs, тест RosterCompositionTests.Item_TravelsThroughStash_AndNeverDuplicates.