Перейти к содержанию

Interaction System — TODO / известные проблемы

Список актуальных находок по итогам аудита после рефакторинга. Отсортировано по важности. Ничего из этого пока не исправлено — это бэклог, а не описание текущего поведения.

1. Первичная синхронизация непустого инвентаря = ложные reward-события

Где: Inventory/NetworkInventory.csOnItemsChanged. Суть: PurrNet SyncDictionary.HandleInitialState при первом получении словаря шлёт Cleared, затем Added на каждый ключ. Обработчик считает delta = count - 0 > 0IsRewardChanged(reward) на каждый предмет. RebuildSnapshot() в OnSpawned не спасает: на момент OnSpawned словарь ещё пуст (initial-state приходит отдельным RPC позже), снапшот строится пустым. Когда стреляет: - Reconnect уже сейчас — объект игрока не деспавнится, инвентарь на сервере полный; при повторном входе клиент заново становится observer → шквал ложных «+N gained» (лог-спам + ложные reward-попапы, когда будет подключён UI). - Save/Load в будущем — загрузка непустого инвентаря даёт то же самое. Решение: флаг «первичный initial-state получен»; до него не эмитить reward/spend, а просто пересобрать снапшот. ~5 строк. Обязательно до сейва, желательно до релиза.

2. Инвариант «инвентарь пуст до NoVisibility» держится на дисциплине, не на коде

Где: Inventory/NetworkInventory.cs + NetworkOwnershipVisibility/. Суть: пока identity ещё AlwaysVisible (до Privatize()), PurrNet сериализует состояние SyncDictionary в spawn-пакет всем текущим наблюдателям. Если будущий код положит предметы в инвентарь в OnSpawned(asServer) до cull — содержимое утечёт чужим за один кадр, отменить отправленные байты нельзя. Сейчас соблюдается (инвентарь стартует пустым), но кодом не защищено. Когда стреляет: окно spawn→cull возникает у уже подключённых удалённых клиентов на вновь входящего (не наоборот — поздний получает чужую приватку уже отфильтрованной, не частями). Значит нужны ≥2 удалённых клиента; при «хост + 1 клиент» единственный старый наблюдатель = сервер, его не куллит → не воспроизводится. Подробности — NetworkOwnershipVisibility/SpawnFlow.md. Решение: assert/guard, запрещающий запись до применения приватного правила видимости.

3. NetworkOwnershipVisibility.targets заполняется руками в инспекторе

Где: NetworkOwnershipVisibility/NetworkOwnershipVisibility.cs. Суть: забыли добавить NetworkInventory в targets[] → тихая утечка приватности, ноль предупреждений (класс «silent misconfiguration»). Решение: на сервере при спавне проверять, что все NetworkInventory/PlayerInteractor из префаба реально попали в targets, иначе LogError.

4. BusyLockTracker: single-slot lock + глобальный stateEpoch

Где: Interactor/BusyLockTracker.cs. Суть: лок держится в одном наборе полей (lockedTargetId/lockStart/lockCooldown/ lockPressEpoch/lockIsPickup) + глобальный stateEpoch. При смене цели / втором нажатии слот затирается. Два проявления: - Ложный Waiting: нажал A → быстро нажал B (перезатёр слот на B, но запрос к A ещё в pending) → вернулся на A: locked=(A==lockedTargetId)=false → cooldown=0 вместо родного cooldown A → A прыгает в Waiting через waitingAfter (0.25с), хотя это нормальный round-trip. - Ложный ранний reveal: скользнул взглядом по B, B реплицировался → глобальный stateEpoch сдвинулся → вернулся на A: stateEpoch != lockPressEpoch → A раскрывается до прихода своего SyncVar. Почему не воспроизводится в песочнице: оба требуют ненулевого round-trip; на host RPC синхронный, pending для A закрывается в том же кадре, ветки не срабатывают. Нужен реальный клиент с пингом. Оценка: косметика тултипа на доли секунды, самоисправляется, состояние сервера не трогает — но именно на реальном пинге (ради чего anti-flicker и делался) вылезет. Решение: перевести лок в per-target (Dictionary<EntityId, LockEntry>, каждый со своим cooldown/pressEpoch; stateEpoch инкрементить пер-таргет). Чинит оба проявления, строк примерно столько же, поведение на счастливом пути 1:1.

5. Мелкое / известное (низкий приоритет)

  • Hud/PlayerHud.csSetWalletText — ноль вызовов, кошелёк в HUD не обновляется. Недоделанная фича.
  • Interactables/Interactable.csCacheModules — вызывается только в Awake; модули, добавленные в рантайме, молча игнорируются. Не задокументировано.
  • Interactables/NetworkSeat.csRelease(PlayerID) — публичный server-only API без вызовов (задел под будущий exit-UI, ок; просто ни разу не протестирован).