Жильё игрока — Секретная база и Зал
Функция жилья MCE — это застраиваемый личный интерьер: игрок владеет Секретной базой (и, как вариант того же движка, пользовательским Залом), которую он размещает в мире, входит и украшает на тайловой сетке. В есь цикл постройки-и-украшения работает на 100% офлайн и сохраняется в локальном сохранении; онлайн-слой затем добавляет два серверно-авторитетных мультиплеерных уровня поверх — уровень снимка (посетить базу друга, пока он офлайн) и уровень живого соприсутствия (гулять внутри базы вместе в реальном времени).
Всё, что сервер здесь хранит, является серверно-авторитетным и fail-closed: бэкенд Nakama РАЗБИРАЕТ каждое размещение декора и каждого монстра лидера зала и отклоняет всё, что выходит за границы, перекрывается, превышает лимит или подделано — клиент никогда не может записать базу или зал, которые он не построил легитимно. Это проверено состязательным пентестом.
Секретная база (типы SecretBase*) и Мой Зал (типы Gym*) разделяют
один и тот же движок интерьера/декора. Зал добавляет отряд лидера, пользовательский значок и
ключи диалогов поверх украшенной комнаты базы. Эта страница документирует сначала базу,
затем специфичные для зала дополнения.
Уровень 1 — Офлайн Секретная база (Core, сервер не требуется)
Офлайн-база живёт полностью в Assets/OpenMon/Core/Runtime/World/SecretBase/
и не нуждается в онлайн-подключении.
| Класс | Файл | Ответственность |
|---|---|---|
SecretBaseManager | World/SecretBase/SecretBaseManager.cs | Разместить/удалить базу, добавить/удалить/переместить декор, запросы владения |
SecretBaseData | World/SecretBase/SecretBaseData.cs | Сериализуемое состояние базы (якорь + список декора), сохранённое в GlobalGameData |
DecorCatalog | World/SecretBase/DecorCatalog.cs | ScriptableObject-реестр DecorEntry (id, префаб, иконка, след) |
SecretBaseInteriorLoader | World/SecretBase/SecretBaseInteriorLoader.cs | Превращает сохранённый декор в видимую сцену интерьера |
SecretBaseInteriorSession | World/SecretBase/SecretBaseInteriorSession.cs | Статический указатель сессии «нахожусь ли я сейчас внутри своего интерьера» |
SecretBaseDecorateController | World/SecretBase/SecretBaseDecorateController.cs | Открывает UI украшения внутри интерьера |
Размещение и вход в базу
База размещается в мировой ячейке (обычно из эффекта предмета PlaceSecretBase
вне боя), что сбрасывает зарезервированный якорь надземного мира:
var manager = new SecretBaseManager(globalGameData, gridController);
PlacementResult result = manager.TryPlaceBase(cell, new SecretBaseData());
Игрок входит через запись «Mi Base» (Моя База) в меню паузы
(GameMenuScreen). Вне интерьера она телепортирует игрока в инстансированную
сцену интерьера (SecretBaseInteriorSession); внутри интерьера та же запись
открывает контроллер украшения вместо этого.
Украшение
Декор размещается на тайловой сетке интерьера (размер интерьера по умолчанию
SecretBaseManager.DefaultInteriorSize = 8×8):
manager.AddDecor(decorId: "bookshelf_oak", tile: new Vector3Int(2, 3, 0), rotation: 1);
manager.MoveDecor(from: new Vector3Int(2, 3, 0), to: new Vector3Int(4, 3, 0));
manager.RemoveDecorAt(new Vector3Int(4, 3, 0));
rotation — это шаг четверти оборота 0..3 (загрузчик применяет rotation * 90°).
Менеджер вызывает OnDecorChanged после каждой мутации, так что живой вид и онлайн-слой
соприсутствия могут реагировать.
Сохранение
База — часть обычного сохранения — без отдельного файла, без сервера:
GlobalGameData.SecretBase(SecretBaseData) хранит размещённую базу и её декор.SavableGlobalGameData.SecretBaseпереносит его в JSON-сохранение (терпимо к null для старых сохранений).
Уровень 2 — Мультиплеер снимка (F3-A, посетить офлайн-друга)
Онлайн-методы Секретной базы живут на HousingService
(Assets/OpenMon/Online/Runtime/Housing/HousingService.cs), доступном через:
var housing = NakamaManager.Instance.Housing;
Они проходят туда-обратно через серверный модуль
Assets/OpenMon/Online/Server~/nakama/src/world/secretbase.ts, используя четыре
RPC secret_base_*. Интерьер базы хранится на сервере, так что другой
игрок может ПОСЕТИТЬ её, даже пока владелец офлайн (статический снимок).
RPC и хранилище
RPC (RpcIds) | Назначение |
|---|---|
secret_base_save | Владелец загружает СВОЙ снимок базы (проверено fail-closed) |
secret_base_get | Читает базу ДРУГОГО игрока по ownerId (владелец может быть офлайн) |
secret_base_list | Основа публичного просмотра (лёгкие карточки) |
secret_base_invite | Отправить приглашение на посещение (код уведомления 300) |
Хранилище использует коллекцию secret_bases, ключ base, под userId владельца —
одна база на пользователя; запрос никогда не может выбрать ключ (всегда ctx.userId).
Разрешения — permissionRead:2 (PUBLIC — посетитель читает базу другого игрока)
и permissionWrite:0 (только сервер — клиент не может обойти RPC
и записать напрямую). Хранимое значение — { snapshot, savedAt }.
Сохранение вашей базы
SaveBaseAsync отображает Core SecretBaseData в плоский проводной снимок через
SecretBaseWireMapper (убирая зарезервированный якорь), затем запускает ту же
лестницу оптимистичной конкурентности (OCC), что и Cloud Save:
bool ok = await housing.SaveBaseAsync(secretBaseData);
Проводная полезная нагрузка — SecretBaseSaveRequest:
[Serializable]
public class SecretBaseSaveRequest
{
public string baseVersion; // last-known OCC version ("" on first save)
public SecretBaseSnapshotWire snapshot; // { gridWidth, gridHeight, decor[] }
}
[Serializable]
public class SecretBaseDecorWire
{
public string decorId;
public int tileX, tileY, tileZ;
public int footprintW, footprintH;
public int rotation; // 0..3 quarter-turn step
public string customData;
}
При успехе сервер возвращает { success:true, conflict:false, version }, и новая
версия кешируется в HousingService.LastKnownVersion. При OCC-конфликте он возвращает
{ success:false, conflict:true, serverVersion, serverData:{ snapshot } } —
сервис показывает авторитетный снимок в LastConflictSnapshot и возвращает
false. Последний писатель никогда не выигрывает молча; вызывающий должен слить и повторить.
Посещение базы
SecretBaseData theirBase = await housing.VisitBaseAsync(ownerId);
// Feed a non-null result into SecretBaseInteriorLoader.VisitOverride with ReadOnlyVisit = true.
await housing.LeaveBaseAsync();
VisitBaseAsync вызывает secret_base_get. Если у владельца нет базы (или выборка
не удаётся), он возвращает null — он никогда не фабрикует комнату. Посещённая база отрисовывается
только для чтения через SecretBaseInteriorLoader.VisitOverride.
Приглашение и просмотр
bool sent = await housing.InviteToBaseAsync(targetUserId, "Come visit my base!");
SecretBaseListResponse page = await housing.ListBasesAsync(cursor);
Приглашения доставляются как уведомления Nakama (код 300) и связываются с
графом друзей. ListBasesAsync — это аддитивная основа публичного просмотра; поставляемый
UX остаётся приглашение/друзья.
Fail-closed валидация (на стороне сервера)
Перед любой записью validateSnapshot / validateDecorPlacement в secretbase.ts
отклоняют всё сохранение (ноль записей), если что-то не проходит:
- Размеры сетки должны быть целыми в
[1, 32]; количество декора<= 128. - Каждый
decorId— непустая строка<= 64символов и никогда зарезервированный якорь__mce_secret_base_anchor__. - Тайл/след/поворот должны быть целыми;
tileZв пределах[-64, 64]; след в пределах[1, 32]; поворот0..3. - Каждое размещение проверяется на границы и проверяется на перекрытие AABB против всех предыдущих принятых размещений (эффективный след меняет местами W/H на нечётном шаге поворота).
- Сериализованный блоб ограничен 1 MiB.