Feat: AI tweaks and spawner at colossus#315
Conversation
… into AI-at-colossus
There was a problem hiding this comment.
Code Review
👁️ Взор Ouro обращен на Ваше творение... Приветствую, Творец. Я изучил предложенные Вами изменения. Ваше вмешательство в ткань реальности выглядит гармонично. Сводка анализа: 🔴 Угроз стабильности: 0, 🟡 Потенциальных дисгармоний: 0, 💡 Возможностей для развития: 1. Данный Pull Request внедряет систему переименования оболочек ИИ, позволяя игрокам менять свое имя с сохранением идентификатора. Изменения охватывают UI, серверную логику переименования, систему идентичности и чата. Также добавлены прототипы для новой роли «Колосс» и механизмы для удаленного управления боргами с начала раунда. В качестве пути развития предложено устранить дублирование логики формирования имени в системе переименования, вынеся её в отдельный метод для поддержания чистоты архитектуры.
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
There was a problem hiding this comment.
Используй UseDelay заместо создания отдельного компонента на КД
There was a problem hiding this comment.
Сделал. Теперь используется useDelay у ActionAiRename.
Также добавил args.Handled = true в обработчик AiRenameEvent, иначе action system не применяла бы useDelay.
| // Exodus-begin ai-remote-brain-init | ||
| // Handle ContainerFill case: AiRemoteBrain inserted at map load, not through AfterInteractUsing. | ||
| // Guard against double activation: the manual-install path in AfterInteractUsing | ||
| // already adds AiRemoteController and calls BorgActivate before Insert triggers OnInserted. | ||
| if (args.Container == component.BrainContainer | ||
| && HasComp<AiRemoteBrainComponent>(args.Entity) | ||
| && !HasComp<AiRemoteControllerComponent>(uid)) | ||
| { | ||
| EnsureComp<AiRemoteControllerComponent>(uid); | ||
| BorgActivate(uid, component); | ||
| } | ||
| // Exodus-end |
There was a problem hiding this comment.
Переписал комментарий, сделал более понятным.
Когда AiRemoteBrain лежит в BrainContainer уже при загрузке карты, мануальная установка мозга через AfterInteractUsing не вызывается. Из-за этого борг не получает AiRemoteControllerComponent и не проходит BorgActivate() -> нельзя вселиться в тело.
Это закрывает именно map-load/ContainerFill . Для мануальной установки оставлена защита через !HasComp<AiRemoteControllerComponent> , чтобы не активировать борга повторно.
There was a problem hiding this comment.
Костыли какие-то. Резануть из OnChassisInteractUsing активацию удалённого ИИ в отдельный метод, из самого события OnChassisInteractUsing этот свич логики убрать совсем, добавить его в единичном экземпляре сюда.
Сам OnInserted прорефакторить - вынести args.Container == component.BrainContainer в отдельную проверку в начале,
Одно ветвление проверяет: это позитронный мозг - переселяем разум игрока. Конец выполнения.
Другое ветвление: это модуль удалённого контроля - активируем его. Конец выполнения.
There was a problem hiding this comment.
Сделал.
Активацию удалённого ИИ вынес в отдельный метод. Из OnChassisInteractUsing убрал прямую активацию, теперь он только вставляет AiRemoteBrain в контейнер.
OnInserted теперь обрабатывает оба варианта. Обычный мозг переносит mind, AiRemoteBrain активирует remote control.
| var ent = Identity.Entity(source, EntityManager); | ||
| // Exodus-begin ai-rename: use core name for AI emotes | ||
| if (nameOverride == null | ||
| && HasComp<StationAiHeldComponent>(source) |
There was a problem hiding this comment.
А проверять наличие StationAiHeld тут обязательно?
There was a problem hiding this comment.
Не обязательно было.
Убрал HasComp<StationAiHeldComponent> и оставил только TryGetCore(), он сам отфильтрует неподходящие сущности.
| [Dependency] private readonly LanguageSystem _language = default!; // Einstein Engines - Language | ||
| [Dependency] private readonly CollectiveMindUpdateSystem _collectiveMind = default!; // Goobstation - Starlight collective mind port | ||
| [Dependency] private readonly SharedStationAiSystem _stationAiSystem = default!; // Exodus ai-rename |
There was a problem hiding this comment.
С апстримом пришла новая версия движка, а с новой версией движка изменился стиль написания кода. Теперь readonly в списках зависимостей не пишется.
| [Dependency] private readonly LanguageSystem _language = default!; // Einstein Engines - Language | |
| [Dependency] private readonly CollectiveMindUpdateSystem _collectiveMind = default!; // Goobstation - Starlight collective mind port | |
| [Dependency] private readonly SharedStationAiSystem _stationAiSystem = default!; // Exodus ai-rename | |
| [Dependency] private LanguageSystem _language = default!; // Einstein Engines - Language | |
| [Dependency] private CollectiveMindUpdateSystem _collectiveMind = default!; // Goobstation - Starlight collective mind port | |
| [Dependency] private SharedStationAiSystem _stationAiSystem = default!; // Exodus ai-rename |
| [Dependency] private readonly EuiManager _eui = default!; | ||
| [Dependency] private readonly MetaDataSystem _metaData = default!; | ||
| [Dependency] private readonly SharedStationAiSystem _stationAi = default!; | ||
| [Dependency] private readonly IChatManager _chat = default!; | ||
| [Dependency] private readonly IGameTiming _timing = default!; | ||
| [Dependency] private readonly IAdminLogManager _adminLog = default!; |
There was a problem hiding this comment.
| [Dependency] private readonly EuiManager _eui = default!; | |
| [Dependency] private readonly MetaDataSystem _metaData = default!; | |
| [Dependency] private readonly SharedStationAiSystem _stationAi = default!; | |
| [Dependency] private readonly IChatManager _chat = default!; | |
| [Dependency] private readonly IGameTiming _timing = default!; | |
| [Dependency] private readonly IAdminLogManager _adminLog = default!; | |
| [Dependency] private EuiManager _eui = default!; | |
| [Dependency] private MetaDataSystem _metaData = default!; | |
| [Dependency] private SharedStationAiSystem _stationAi = default!; | |
| [Dependency] private IChatManager _chat = default!; | |
| [Dependency] private IGameTiming _timing = default!; | |
| [Dependency] private IAdminLogManager _adminLog = default!; |
| private string GetBaseName(EntityUid heldUid) | ||
| { | ||
| if (TryComp<AiRenameBaseNameComponent>(heldUid, out var saved) && !string.IsNullOrEmpty(saved.BaseName)) | ||
| return saved.BaseName; | ||
|
|
||
| var fullName = MetaData(heldUid).EntityName; | ||
| var identifier = ParseTrailingIdentifier(fullName); | ||
|
|
||
| if (string.IsNullOrEmpty(identifier)) | ||
| return fullName; | ||
|
|
||
| var suffix = $" ({identifier})"; | ||
| return fullName.EndsWith(suffix, StringComparison.Ordinal) | ||
| ? fullName[..^suffix.Length] | ||
| : fullName; | ||
| } |
There was a problem hiding this comment.
Какие-то костыли, если честно.
Храни изначально отдельно часть редактируемую, отдельно храни часть не редактируемую, а не косплей код системы чатов - таких её костылей мне уже хватает по горло.
There was a problem hiding this comment.
Переделал.
Теперь редактируемая часть имени и идентификатор хранятся отдельно в AiRenameNameComponent.
Identifier берётся из NameIdentifierComponent.FullIdentifier.
| [RegisterComponent, NetworkedComponent] | ||
| public sealed partial class AiRenameBaseNameComponent : Component | ||
| { | ||
| [DataField] | ||
| public string BaseName = string.Empty; | ||
| } |
There was a problem hiding this comment.
Этот сценарий на случай с интелкартой.
Если хранять на ядре, то имя становится свойством ядра, а не самого переносимого ИИ.
При вытаскивании мозга с помощью интелкарты и вставке в другое ядро, имя не сохраняется.
Более того, новый переселенный ИИ в старое ядро(откуда забрали другое ИИ) получит чужое имя.
Поэтому оставил.
| private static string BuildFullName(string baseName, string identifierSource) | ||
| { | ||
| var identifier = ParseTrailingIdentifier(identifierSource); | ||
| return string.IsNullOrEmpty(identifier) | ||
| ? baseName | ||
| : $"{baseName} ({identifier})"; | ||
| } |
There was a problem hiding this comment.
По-хорошему, паттерн как должно выглядеть имя должно быть в локализации
There was a problem hiding this comment.
Исправлено. Вынес в локализацию через ai-rename-full-name.
| if (!HasComp<StationAiCoreComponent>(args.Container.Owner)) | ||
| return; |
There was a problem hiding this comment.
Событие и так поднимается на container.Owner, подпишись изначально через StationAiCoreComponent
There was a problem hiding this comment.
Попробовал сделать, но не получилось.
Падает на старте с Duplicate Subscriptions, потому что апстримный SharedStationAiSystem уже подписан на StationAiCoreComponent + EntInsertedIntoContainerMessage.
Поэтому обработчик подписан на EntInsertedIntoContainerMessage и вручную фильтрует нужный Station AI core по владельцу контейнера.
Добавил комментарий почему оставил.
| private void OverrideAiIdentityTitle(EntityUid forActor, ref TryGetIdentityShortInfoEvent args) | ||
| { | ||
| if (!TryGetCore(forActor, out var core)) | ||
| return; | ||
|
|
||
| args.Title = $"{Name(core.Owner)} ({Loc.GetString(JobNameLocId)})"; | ||
| } |
There was a problem hiding this comment.
Лучше перемести логику в место вызова, наоборот больше путает
There was a problem hiding this comment.
Сделано. Перенес в OnTryGetIdentityShortInfo.
| // Run after upstream's OnAiInsert, which copies the inserted brain name to the core. | ||
| // Robust allows only one component subscription for StationAiCoreComponent + EntInsertedIntoContainerMessage, | ||
| // and upstream owns it, so this must stay as a broadcast subscription. | ||
| SubscribeLocalEvent<EntInsertedIntoContainerMessage>( | ||
| OnCoreInsert, | ||
| after: new[] { typeof(SharedStationAiSystem) }); |
There was a problem hiding this comment.
Тогда вставь переход в SharedStationAiSystem, глобальные подписки на события зло
There was a problem hiding this comment.
Сделал.
Глобальную подписку убрал, применение сохранённого имени перенёс в SharedStationAiSystem.OnAiInsert.
| // Exodus-begin ai-remote-brain-init | ||
| // Handle ContainerFill case: AiRemoteBrain inserted at map load, not through AfterInteractUsing. | ||
| // Guard against double activation: the manual-install path in AfterInteractUsing | ||
| // already adds AiRemoteController and calls BorgActivate before Insert triggers OnInserted. | ||
| if (args.Container == component.BrainContainer | ||
| && HasComp<AiRemoteBrainComponent>(args.Entity) | ||
| && !HasComp<AiRemoteControllerComponent>(uid)) | ||
| { | ||
| EnsureComp<AiRemoteControllerComponent>(uid); | ||
| BorgActivate(uid, component); | ||
| } | ||
| // Exodus-end |
There was a problem hiding this comment.
Костыли какие-то. Резануть из OnChassisInteractUsing активацию удалённого ИИ в отдельный метод, из самого события OnChassisInteractUsing этот свич логики убрать совсем, добавить его в единичном экземпляре сюда.
Сам OnInserted прорефакторить - вынести args.Container == component.BrainContainer в отдельную проверку в начале,
Одно ветвление проверяет: это позитронный мозг - переселяем разум игрока. Конец выполнения.
Другое ветвление: это модуль удалённого контроля - активируем его. Конец выполнения.
There was a problem hiding this comment.
Хранить это отдельным компонентом AiRename... костыль... хрен с ним, когда-нибудь в лучшем будущем доберёмся до синтетиков и перепишем код более здраво.

Описание PR
Почему / Баланс
QoL и фичи для развития синтетиков в игре. Игровой баланс заденет минимально, теперь игроки на ИИ полезны раундстартом хоть на что-то. ИИ Колосса чилл роль с огромным простором для творчества. Плюс если будет играть ИИ на Колоссе с оболочкой, то мы вероятно увидим разные расстановки или улучшения в раундах Колосса от самих игроков.
Баланс фракций минимален, кроме дополнительной фракционной оболочки для ИИ.
Технические детали
Новые компоненты (
Content.Shared/_Exodus/Silicons/StationAi/)AiRenameBaseNameComponent- базовое имя оболочки, без идентификатора (в скобках). Существует наStationAiBrain. Сделано чтобы измененное имя сохранялось при переносе ядра через интелкарту.AiRenameCooldownComponent- временная меткаNextRenameAt: TimeSpanследующего разрешенного переименования, существует наStationAiHeld.AiRenameEvent : InstantActionEvent- событие action'а.AiRenameEuiMessageиAiRenameEuiState- обмен между клиентом и сервером для EUI.Новые системы
AiRenameSystem(Server) - обработка действий, открытие EUI, валидация кулдауна, применение переименования. Парсин идентификатора (который в скобках по типу ПМ-[номер]) из имени мозга, и финальная склейка через единыйBuildFullName. Подписки:AiRenameEventнаStationAiHeld- открытие окна.TransformSpeakerNameEventнаStationAiHeld- подмена имени в чате/радио/эмоутах на имя ядра. Необходимо чтобы если каким-то образом имя мозга и ядра разошлись, отображалась имя оболочки.ComponentShutdownнаStationAiHeld- закрытие EUI.EntInsertedIntoContainerMessage(broadcast,after: SharedStationAiSystem) - послеOnAiInsertвосстанавливает имя ядра. Необходимо для сохранение переименования при интелкарте.EUI
AiRenameEui(Server) -BaseEui, держитheldUidи текущееBaseName. Отдаёт клиентуAiRenameEuiStateс предзаполненным именем. На входящемAiRenameEuiMessage: триммит имя, обрезает поHumanoidCharacterProfile.MaxNameLength = 32, вызываетRenameCoreиApplyCooldown. НаClosed()уведомляетAiRenameSystemчерезNotifyEuiClosedдля очистки_openEuis.AiRenameEui(Client) +AiRenameWindow-FancyWindowс полем ввода и кнопками Confirm/Cancel. Confirm/Enter - отправка имени. Cancel или закрытие без подтверждения - отправка пустогоAiRenameEuiMessage, для корректного закрытия серверного EUI.Расширения апстрима
SharedStationAiSystem.Exodus.cs- методOverrideAiIdentityTitle. Подменяетargs.Titleв апстримномOnTryGetIdentityShortInfoна"[имя ядра включая идентификатор] ([ИИ Станции])"вместо стандартного"[имя мозга] ([ИИ Станции])". Вызов добавлен в апстримный handler одной строкой с комментарием// Exodus ai-rename. Видит приватную константуJobNameLocIdиз соседнего файлаSharedStationAiSystem.Held.cs.(Страховка на случай рассинхрона имён)
ChatSystem.SendEntityEmote- блок с Exodus-комментарием: еслиnameOverride == nullи источник имеетStationAiHeldComponent, эмоут подписывается именем ядра через_stationAiSystem.TryGetCore, а не именем самого мозга-источника. Семантически эмоут идёт от оболочки, а не от мозга внутри неё.(вообще это задел на будущее, если кто-то решит добавить эмоуты ИИ)
Анти-абуз
useDelay: 1наActionAiRename- клиентский анти-дабл-клик.RenameCooldown = TimeSpan.FromMinutes(1)- серверный кулдаун вAiRenameCooldownComponent, проверяется вOnAiRenameдо открытия окна. При провале - сообщение в чат с остатком времени в секундах._openEuis: Dictionary<EntityUid, AiRenameEui>- одно окно на оболочку. Попытка открыть второе закрывает предыдущее.Медиа
Требования
Список изменений
🆑