Решили: ArenaLayoutData печёт только мир. Боевой скоуп свою копию не строит и не регистрирует —
он резолвит готовую из предка, а границы поля и зону камеры отдаёт симуляции лямбдой от
резолвера, а не значением.
Почему лямбдой: значение нужно в момент Configure, то есть его пришлось бы держать в боевом
скоупе — и второй владелец layout вернулся бы через заднюю дверь. Лямбда резолвит его в момент
создания симуляции, когда родительский контейнер уже отдаёт готовое.
Грабли, которых не видно из кода (обе проверены по исходнику VContainer 1.18 в
Library/PackageCache, а не по памяти и не по сайту — сайт показывает только две простые перегрузки):
RegistrationBuilder.WithParameter(string name, Func<IObjectResolver, object> value)существует (и ещё четыре родственные формы). Без неё пришлось бы регистрироватьCombatSimulationручной фабрикой и перечислять все её зависимости — то есть терять свойство «добавил систему в ctor, в скоупе править нечего».LifetimeScope.Parentзаполняется до вызоваConfigure(строки 192 и 290). Значит дочерний скоуп имеет право спросить, есть ли над ним родитель, и по-разному собраться. Мы этим пользуемся: боевая сцена, поднятая в одиночку (dev-арена без мира), регистрирует бесконечное поле явно и говорит об этом вслух — это другой состав сцены, а не фолбэк на отказ.
Чем дефект был опасен: до правки боевой скоуп искал авторинг сам и регистрировал вторую
ArenaLayoutData — при том что комментарий в WorldLifetimeScope утверждал обратное. Никто не
замечал, потому что VContainer отдаёт ближайшую регистрацию, а значения совпадали: оба скоупа
находили один и тот же объект в загруженных сценах. Разошлись бы они в первый же день, когда арен
станет больше одной.
Владелец правды: Game/WorldLifetimeScope.cs (единственный, кто печёт арену), тест
ArenaHasOneOwnerTests (ровно один файл ищет авторинг, и это мир).