Conversation
📝 WalkthroughWalkthroughThis pull request introduces a comprehensive Maid subsystem featuring an authentication panel with vote-based action confirmation, ERT (Emergency Response Team) recruitment mechanics, and ghost recruitment UI flows. New client/server systems manage button voting, state synchronization, recruitment timers, and EUI interactions, alongside supporting components, events, maps, and localization resources. Changes
Sequence Diagram(s)sequenceDiagram
actor Player
participant ClientUI as Client: AuthPanel UI
participant ClientBUI as Client: BoundUserInterface
participant Server as Server: AuthPanelSystem
participant UI as Server: UI Update
Player->>ClientUI: Press Action Button
ClientUI->>ClientBUI: SendButtonPressed(action, reason)
ClientBUI->>Server: AuthPanelButtonPressedMessage
Server->>Server: ValidateAccess()
Server->>Server: ValidateReason()
Server->>Server: CheckVoteLimits()
Server->>Server: RecordVote()
Server->>UI: UpdateUserInterface()
UI->>ClientUI: AuthPanelConfirmationActionState
ClientUI->>ClientUI: UpdateCount & Reason
alt MinCount Reached
Server->>Server: StartTimeout
Server->>Server: RaiseActionEvent()
end
sequenceDiagram
actor Ghost
participant ClientGUI as Client: Ghost Recruitment UI
participant ClientEUI as Client: GhostRecruitmentEUI
participant Server as Server: GhostRecruitmentSystem
participant Recruitment as Server: Recruitment Handler
Ghost->>ClientGUI: View Accept/Deny Window
alt Ghost Accepts
Ghost->>ClientGUI: Click Accept
ClientGUI->>ClientEUI: AcceptRecruitmentChoiceMessage(Accept)
ClientEUI->>Server: Message Handler
Server->>Recruitment: Recruit(ghost_uid, recruitment_name)
Recruitment->>Server: Queue Ghost for Spawning
else Ghost Denies
Ghost->>ClientGUI: Click Deny
ClientGUI->>ClientEUI: AcceptRecruitmentChoiceMessage(Deny)
ClientEUI->>Server: Close EUI
end
sequenceDiagram
participant RoundStart as Round: Start Event
participant ERTRule as Server: ERTRecruitmentRule
participant MapSystem as Server: Map System
participant GhostRecruitment as Server: GhostRecruitmentSystem
participant Chat as Server: Chat System
RoundStart->>ERTRule: OnStarting()
ERTRule->>ERTRule: ValidateDisabled()
ERTRule->>ERTRule: CheckPlayerCount()
alt Map Loading Enabled
ERTRule->>MapSystem: SpawnOutpostMap()
MapSystem->>MapSystem: Load ERTStation.yml
MapSystem->>ERTRule: Map Spawned
end
ERTRule->>GhostRecruitment: StartRecruitment()
GhostRecruitment->>GhostRecruitment: FilterGhosts(playtime)
GhostRecruitment->>GhostRecruitment: OpenEUI per Ghost
ERTRule->>Chat: Broadcast Wait Message
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (8)
Content.Client/_Maid/AuthPanel/AuthPanelMenu.xaml (1)
27-35: Consider removing or tracking commented-out UI sections.The commented-out
AccessContainerandBluespaceWeaponContainerblocks suggest planned features. If these are intended for future implementation, consider removing them from the codebase and tracking them as GitHub issues or TODO comments in the code-behind file instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/_Maid/AuthPanel/AuthPanelMenu.xaml` around lines 27 - 35, The XAML contains commented-out UI blocks for AccessContainer/AccessButton and BluespaceWeaponContainer/BluespaceWeaponButton; remove these commented sections from AuthPanelMenu.xaml if they are not needed, or alternatively move them into a tracked TODO (or GitHub issue) and add a short TODO comment in the code-behind (e.g., AuthPanelMenu.xaml.cs) referencing that issue ID so the intent is preserved; ensure any related names (AccessCount, BluespaceWeaponCount) are also removed or documented to avoid dead identifiers.Content.Server/_Maid/ERTRecruitment/ERTRecruitedReasonEvent.cs (1)
3-12: Consider using a constructor parameter instead of mutable setter.The event class uses a mutable
SetReasonmethod, which deviates from the typical pattern seen in similar events likeGhostRecruitmentSuccessEvent(inGhostRecruitmentEvents.cs) that uses constructor parameters. Events are generally immutable after creation.Additionally, verify whether this event needs the
[NetSerializable]attribute if it will be sent over the network. Other similar events use[Serializable, NetSerializable].♻️ Proposed refactor using constructor parameter
[Serializable] public sealed class ERTRecruitedReasonEvent : EntityEventArgs { - public string Reason = ""; + public string Reason { get; } - public void SetReason(string reason) + public ERTRecruitedReasonEvent(string reason) { Reason = reason; } - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Server/_Maid/ERTRecruitment/ERTRecruitedReasonEvent.cs` around lines 3 - 12, ERTRecruitedReasonEvent currently exposes a mutable SetReason method which breaks the usual immutable event pattern; change the class to accept the reason via a constructor parameter and make the Reason field read-only or a get-only property (replace SetReason with a constructor that sets Reason) so the event is immutable after creation, and if this event is transmitted over the network add the [NetSerializable] attribute alongside [Serializable] to match other event types like GhostRecruitmentSuccessEvent.Content.Shared/_Maid/CVars/CVars.cs (1)
56-60: Minor documentation grammar improvement.The XML doc comment has a minor grammatical issue.
📝 Proposed fix
/// <summary> - /// Do generate Ert map on round start or not + /// Whether to generate ERT map on round start. /// </summary>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Shared/_Maid/CVars/CVars.cs` around lines 56 - 60, Update the XML doc comment for the public static readonly CVarDef<bool> LoadErtMap so the grammar is correct and reads clearly (e.g., "Whether to generate the ERT map on round start."). Locate the declaration of LoadErtMap (CVarDef.Create("maid.load_ert_map", true, CVar.SERVERONLY)) and replace the existing summary text with the corrected sentence.Content.Server/_Maid/ERTRecruitment/ERTMapComponent.cs (1)
14-14: Make outpost map path immutable.Line 14 exposes a mutable static path. This should be immutable to prevent accidental runtime reassignment.
♻️ Proposed fix
- public static ResPath OutpostMap = new("/Maps/_Maid/ERTStation.yml"); + public static readonly ResPath OutpostMap = new("/Maps/_Maid/ERTStation.yml");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Server/_Maid/ERTRecruitment/ERTMapComponent.cs` at line 14, OutpostMap is declared as a mutable static field; change it to an immutable static readonly field by updating the declaration of OutpostMap (the public static ResPath OutpostMap in ERTMapComponent) to be static readonly so the ResPath cannot be reassigned at runtime while keeping the same initialization value.Content.Shared/_Maid/GhostRecruitment/RecruitedComponent.cs (1)
4-10: Consolidate recruited-marker semantics to avoid split state.
RecruitedComponent(Line 6) duplicates the samerecruitmentNamedata shape already present inGhostRecruitedComponent(Content.Shared/_Maid/GhostRecruitment/GhostRecruitedComponent.cs, Line 5-Line 9). Keeping both in the same domain makes it easy for systems/prototypes to attach or query the wrong marker.♻️ Suggested direction
- // this for spawned prototype - [RegisterComponent] - public sealed partial class RecruitedComponent : Component + /// <summary> + /// Marks spawned entities created by ghost recruitment flow. + /// </summary> + [RegisterComponent] + public sealed partial class GhostRecruitmentSpawnedComponent : Component { [DataField("recruitmentName")] public string RecruitmentName = "default"; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Shared/_Maid/GhostRecruitment/RecruitedComponent.cs` around lines 4 - 10, RecruitedComponent duplicates the recruitmentName field already defined in GhostRecruitedComponent, causing split marker semantics; consolidate by removing the duplicate state: delete the RecruitmentName field (and/or the entire RecruitedComponent) and standardize on GhostRecruitedComponent as the single recruited marker, then update any systems/prototypes that construct, attach, or query RecruitedComponent to use GhostRecruitedComponent instead so all recruitment logic references the same component type.Content.Shared/_Maid/GhostRecruitment/GhostRecruitmentSpawnPointComponent.cs (1)
1-19: LGTM with minor inconsistency note.The component structure is well-defined and follows the pattern established by related components (
RecruitedComponent,GhostRecruitedComponent). The use ofPrototypeIdSerializer<EntityPrototype>for prototype validation is correct.One minor inconsistency: Line 17 uses
[DataField]without an explicit key name, while other fields specify keys explicitly (e.g.,"prototype","recruitmentName","priority"). Consider adding"jobId"for consistency:- [DataField] + [DataField("jobId")] public string JobId = "Passenger";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Shared/_Maid/GhostRecruitment/GhostRecruitmentSpawnPointComponent.cs` around lines 1 - 19, The DataField on JobId is missing an explicit key which is inconsistent with other fields; update the attribute on the JobId field in GhostRecruitmentSpawnPointComponent (the public string JobId = "Passenger") to use [DataField("jobId")] so it matches the explicit keys used for EntityPrototype, RecruitmentName and Priority and ensures consistent serialization/deserialization.Content.Client/_Maid/AuthPanel/AuthPanelMenu.xaml.cs (1)
15-53: Remove or track the large commented-out UI branches.Keeping inactive button/container code in-place makes the real behavior harder to scan and maintain.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/_Maid/AuthPanel/AuthPanelMenu.xaml.cs` around lines 15 - 53, Remove or formalize the large commented-out UI branches instead of leaving them inline: delete the commented methods OnAccessButtonPressed and OnBluespaceWeaponButtonPressed and the commented SetAccessCount and SetWeaponCount blocks (and the commented visibility lines inside SetRedCount referencing AccessContainer and BluespaceWeaponContainer), or replace each block with a short TODO/comment referencing a tracking ticket or feature flag if the code must be preserved for future work; ensure any preserved notes mention the exact symbol (e.g., OnAccessButtonPressed, OnBluespaceWeaponButtonPressed, SetAccessCount, SetWeaponCount, AccessContainer, BluespaceWeaponContainer) so callers and reviewers can find the rationale.Content.Server/_Maid/AuthPanel/AuthPanelSystem.cs (1)
32-34: Encapsulation: Public mutable fields expose internal state.
Counter,CardIndexes, andReasonare public fields that can be modified by any external code, which could lead to inconsistent state. Consider making these private and exposing read-only access if needed externally.Proposed encapsulation fix
- public Dictionary<AuthPanelAction, HashSet<EntityUid>> Counter = new(); - public Dictionary<AuthPanelAction, HashSet<int>> CardIndexes = new(); - public string Reason = ""; + private Dictionary<AuthPanelAction, HashSet<EntityUid>> _counter = new(); + private Dictionary<AuthPanelAction, HashSet<EntityUid>> _usedCards = new(); + private string _reason = ""; + + public IReadOnlyDictionary<AuthPanelAction, HashSet<EntityUid>> Counter => _counter; + public string Reason => _reason;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Server/_Maid/AuthPanel/AuthPanelSystem.cs` around lines 32 - 34, Make the mutable public fields Counter, CardIndexes and Reason private (e.g. rename to _counter, _cardIndexes, _reason) and expose read-only accessors instead: add public properties like IReadOnlyDictionary<AuthPanelAction, IReadOnlyCollection<EntityUid>> Counter { get; } and IReadOnlyDictionary<AuthPanelAction, IReadOnlyCollection<int>> CardIndexes { get; } (or return IReadOnlyDictionary<AuthPanelAction, IReadOnlyCollection<T>> by wrapping the private Dictionary), and public string Reason { get; private set; } (or IReadOnlyProperty). Keep the internal collections as Dictionary<AuthPanelAction, HashSet<...>> for mutation inside AuthPanelSystem methods (use the private fields _counter/_cardIndexes) and return read-only wrappers (e.g. .ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection<T>)kv.Value) or ReadOnlyCollection) so external code cannot mutate internal state directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Content.Client/_Maid/GhostRecruitment/GhostRecruitmentEuiAccept.cs`:
- Around line 15-27: The Deny flow sends duplicate messages because
DenyButton.OnPressed sends
AcceptRecruitmentChoiceMessage(AcceptRecruitmentUiButton.Deny) then calls
_window.Close(), and _window.OnClose also sends the same message; to fix,
prevent the OnClose handler from re-sending when Close() is invoked manually by
either (a) removing/unsubscribing the OnClose handler before calling
_window.Close() inside the DenyButton.OnPressed handler, or (b) add a
short-lived flag (e.g. bool _suppressCloseHandler) set to true in
DenyButton.OnPressed before calling _window.Close() and have the _window.OnClose
lambda check that flag and return without sending if set; update the
corresponding AcceptButton flow similarly if needed and reference the handlers
named DenyButton.OnPressed, _window.OnClose, AcceptRecruitmentChoiceMessage and
AcceptButton.OnPressed.
In `@Content.Server/_Maid/AuthPanel/AuthPanelSystem.cs`:
- Around line 38-43: MinCount and EarliestStart are using temporary hardcoded
values; replace them with configurable CVars or finalize production defaults.
Update AuthPanelSystem to read these settings from configuration (e.g., declare
CVars like authpanel.minCount and authpanel.earliestStart or equivalent engine
config entries), use those CVar values instead of the hardcoded MinCount and
EarliestStart, and remove the "TEMP" comments; ensure sensible defaults (e.g., 3
and 60) are set in the CVar registration so the values are configurable at
runtime and during testing.
- Around line 70-77: OnRestart currently clears Counter and CardIndexes and
resets _delay/_timeout but leaves Reason unchanged; update the OnRestart method
to reset Reason (e.g., set Reason = string.Empty) so any stale reason from a
previous round is cleared; reference the OnRestart method and the Reason
field/property along with Counter and CardIndexes when making the change.
- Around line 124-131: The aliveCount calculation and ghost threshold are too
simplistic: instead of using _playerManager.PlayerCount and aliveCount =
playerCount - ghostCount (which counts observers, lobby users, or disconnected
sessions as "alive"), compute the participant set explicitly (e.g., iterate
_playerManager.GetAllPlayers() or use a connected/active players API and filter
out admin observers/lobby/disconnected) and derive aliveCount from that filtered
list; also stop reusing MinCount for ghost threshold—introduce a distinct
constant (e.g., MinGhostsThreshold) and replace the condition ghostList.Count >
MinCount with ghostList.Count >= MinGhostsThreshold before calling
_gameTicker.AddGameRule(ERTRecruitmentRuleComponent.EventName).
- Around line 188-208: The uniqueness check currently uses access.Count (in the
OnButtonPressed flow where CardIndexes, args.Button and cardSet are referenced),
which is not a unique card identifier; change the logic to record a true unique
ID per card (e.g., the card entity's EntityUid or a hash of its access tags)
instead of access.Count: locate the place where you read the card (the code that
populates access and uses hashSet and CardIndexes), obtain the card entity UID
(or compute a stable hash of its access list), and use that UID/hash when
checking/adding to cardSet and when checking hashSet so duplicate physical cards
are detected correctly; preserve the popup behavior
(auth-panel-used-ID/auth-panel-pressed) but operate on the UID/hash rather than
access.Count.
In `@Content.Server/_Maid/ERTRecruitment/Commands/BlockERT.cs`:
- Around line 17-31: The Execute method currently sets isDisabled by toggling
ertsys.IsDisabled and then checks args[0] == "true", which misparses values like
"false" or different casing; update Execute to parse the first arg using
bool.TryParse (case-insensitive/invariant) into a local bool (e.g., parsed) and,
if parsing succeeds, set ertsys.IsDisabled = parsed, otherwise preserve the
existing toggle behavior when no valid arg is provided; apply this change in the
Execute method where args, isDisabled, and ertsys (ERTRecruitmentRule) are
handled and keep the existing shell.WriteLine and
_chatManager.SendAdminAnnouncement calls.
In `@Content.Server/_Maid/ERTRecruitment/ERTRecruitmentRule.cs`:
- Around line 76-89: The Started handler currently calls DeclineERT before
calling ForceEndSelf when component.IsBlocked/IsDisabled or when spawner count
is too low, which leads to duplicate decline behavior because ForceEndSelf
triggers Ended which also declines; remove the pre-decline calls and let
ForceEndSelf/Ended handle the DeclineERT path (i.e., delete the DeclineERT(...)
calls in the Started flow for the checks around component.IsBlocked/IsDisabled
and the spawner count so only ForceEndSelf(uid, gameRule) is invoked), ensuring
DeclineERT is called exclusively from Ended/failure handling to avoid double
announcements/logs.
- Around line 134-144: The handler OnRecruitmentSuccess currently sends ERT
messages for every recruitment event; modify it to only proceed when
args.RecruitmentName matches the ERT recruitment identifier (e.g., compare
args.RecruitmentName to the expected string like "ERT" or the defined constant),
returning early otherwise; keep the existing logic (raising
ERTRecruitedReasonEvent, checking args.PlayerSession, and dispatching messages)
unchanged but guarded by this recruitment-name check.
In `@Content.Server/_Maid/GhostRecruitment/GhostRecruitmentSystem.cs`:
- Around line 101-107: The code increments count and raises the
GhostRecruitmentSuccessEvent immediately after calling TransferMind, which can
emit false successes if transfer fails; update the GhostRecruitmentSystem to
only set the RecruitedComponent, increment count, and RaiseLocalEvent after
verifying TransferMind actually succeeded—either by using a boolean/try-return
from TransferMind (or its TryTransferMind equivalent) or by wrapping
TransferMind in a try/catch and proceeding only on success; apply this change to
the block using TransferMind(ghostUid, spawnerUid, spawnerComponent) and the
similar block in lines 132-156, so
EnsureComp<RecruitedComponent>(ghostUid).RecruitmentName, count++, and
RaiseLocalEvent(ghostUid, new GhostRecruitmentSuccessEvent(...,
actorComponent.PlayerSession)) occur only when the transfer succeeded.
- Around line 206-210: The loop over _openUis mutates the dictionary because
CloseEui (and ClearEui) remove entries, causing InvalidOperationException; fix
by materializing the collection before iterating (e.g. iterate over
_openUis.Keys or _openUis.ToList()) so you call
CloseEui(session.AttachedEntity.Value, recruitmentName) on a copied list rather
than the dictionary being enumerated; update the code referencing _openUis in
the foreach around CloseEui to use the materialized collection.
- Around line 165-168: The current check in GhostRecruitmentSystem using
HasComp<RandomHumanoidSpawnerComponent> then doing uid = new EntityUid((int) uid
+ 1) is wrong; instead call RandomHumanoidSystem.SpawnRandomHumanoid() and use
its returned EntityUid for the spawned humanoid (or store that return value on
the RandomHumanoidSpawnerComponent) and attach the player's mind to that
returned UID; if spawning fails, return null/abort recruitment rather than
guessing IDs. Ensure you reference SpawnRandomHumanoid() from
RandomHumanoidSystem and replace the uid arithmetic logic and any subsequent use
of that guessed uid with the actual spawned entity UID.
In `@Content.Shared/_Maid/GhostRecruitment/GhostRecruitmentEvents.cs`:
- Around line 18-35: Fix the typo in the XML comment for the CancelableEventArgs
class: change the summary text from "Whether this even has been cancelled." to
"Whether this event has been cancelled." and ensure the comment sits above the
Cancelled property (public bool Cancelled) in the CancelableEventArgs
definition; also scan for duplicate copies of the CancelableEventArgs class (and
its members Cancel(), Uncancel()) elsewhere in the repo and remove or
consolidate duplicates to avoid multiple definitions.
- Around line 5-14: The shared event class GhostsRecruitmentSuccessEvent has a
naming mismatch and lacks the required base class: rename
GhostsRecruitmentSuccessEvent to GhostRecruitmentSuccessEvent (to match the
server-side symbol) and change its declaration to inherit from EntityEventArgs;
update the constructor signature and any usages to the new name, keep the
[Serializable, NetSerializable] attributes and the RecruitmentName field intact,
and ensure all references (e.g., event firing/listening sites) are updated to
the new class name.
In `@Resources/Locale/en-US/_Maid/auth-panel.ftl`:
- Line 5: Replace the misspelled faction name "NanoTrisen" with the correct
"NanoTrasen" in the localized string within
Resources/Locale/en-US/_Maid/auth-panel.ftl (the line containing "belonging to
NanoTrisen, you are tasked with deploying to a distressed station") so the
player-facing copy shows the correct faction name.
In `@Resources/Locale/ru-RU/_Maid/auth-panel.ftl`:
- Line 7: Replace the awkward phrasing on Line 7 "Особое распространение может
это изменить" with a more natural Russian construction (e.g., "Однако особое
распространение может это изменить" or "Это может измениться при широком
распространении") to fit the surrounding text, and correct the typo on Line 26
by fixing "артилерии" to the intended word (e.g., "артиллерии") so spelling and
grammar are correct throughout the locale file.
In `@Resources/Locale/ru-RU/_Maid/recruitment.ftl`:
- Line 2: The Russian prompt string for the key
accept-ert-window-prompt-text-part is missing a comma for readability; update
the message value to insert a comma after "уверены" so it reads "Если вы не
уверены, то лучше скажите НЕТ!" ensuring only the punctuation change is made and
the key name accept-ert-window-prompt-text-part remains unchanged.
In `@Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.yml`:
- Around line 46-63: The Janitor GhostRecruitmentSpawnPoint entries are missing
an explicit priority and thus default to 5; update the two
GhostRecruitmentSpawnPoint components (the ones using prototype
RandomHumanoidSpawnerERTJanitor and RandomHumanoidSpawnerERTJanitorEVA in
entities SpawnPointERTEventERTJanitor and SpawnPointERTEventERTJanitorEVA) to
include priority: 2 so they sort alongside other core roles
(Engineer/Security/Medical) in GhostRecruitmentSystem.
In `@Resources/Textures/_Maid/Structures/Wallmounts/auth.rsi/meta.json`:
- Around line 3-4: Update the metadata to provide a proper, specific attribution
for CC-BY-SA-3.0 compliance: replace the current sloppy value in the "copyright"
field with the actual source and author (for example, "Taken from tgstation at
commit <hash>" or "Original art by <artist name> (source: <URL/handle>)"), or if
the exact author/commit is unknown, add a clear note indicating the best-known
source and that the original author is unknown; ensure the "license" and
"copyright" fields remain intact and accurate.
---
Nitpick comments:
In `@Content.Client/_Maid/AuthPanel/AuthPanelMenu.xaml`:
- Around line 27-35: The XAML contains commented-out UI blocks for
AccessContainer/AccessButton and BluespaceWeaponContainer/BluespaceWeaponButton;
remove these commented sections from AuthPanelMenu.xaml if they are not needed,
or alternatively move them into a tracked TODO (or GitHub issue) and add a short
TODO comment in the code-behind (e.g., AuthPanelMenu.xaml.cs) referencing that
issue ID so the intent is preserved; ensure any related names (AccessCount,
BluespaceWeaponCount) are also removed or documented to avoid dead identifiers.
In `@Content.Client/_Maid/AuthPanel/AuthPanelMenu.xaml.cs`:
- Around line 15-53: Remove or formalize the large commented-out UI branches
instead of leaving them inline: delete the commented methods
OnAccessButtonPressed and OnBluespaceWeaponButtonPressed and the commented
SetAccessCount and SetWeaponCount blocks (and the commented visibility lines
inside SetRedCount referencing AccessContainer and BluespaceWeaponContainer), or
replace each block with a short TODO/comment referencing a tracking ticket or
feature flag if the code must be preserved for future work; ensure any preserved
notes mention the exact symbol (e.g., OnAccessButtonPressed,
OnBluespaceWeaponButtonPressed, SetAccessCount, SetWeaponCount, AccessContainer,
BluespaceWeaponContainer) so callers and reviewers can find the rationale.
In `@Content.Server/_Maid/AuthPanel/AuthPanelSystem.cs`:
- Around line 32-34: Make the mutable public fields Counter, CardIndexes and
Reason private (e.g. rename to _counter, _cardIndexes, _reason) and expose
read-only accessors instead: add public properties like
IReadOnlyDictionary<AuthPanelAction, IReadOnlyCollection<EntityUid>> Counter {
get; } and IReadOnlyDictionary<AuthPanelAction, IReadOnlyCollection<int>>
CardIndexes { get; } (or return IReadOnlyDictionary<AuthPanelAction,
IReadOnlyCollection<T>> by wrapping the private Dictionary), and public string
Reason { get; private set; } (or IReadOnlyProperty). Keep the internal
collections as Dictionary<AuthPanelAction, HashSet<...>> for mutation inside
AuthPanelSystem methods (use the private fields _counter/_cardIndexes) and
return read-only wrappers (e.g. .ToDictionary(kv => kv.Key, kv =>
(IReadOnlyCollection<T>)kv.Value) or ReadOnlyCollection) so external code cannot
mutate internal state directly.
In `@Content.Server/_Maid/ERTRecruitment/ERTMapComponent.cs`:
- Line 14: OutpostMap is declared as a mutable static field; change it to an
immutable static readonly field by updating the declaration of OutpostMap (the
public static ResPath OutpostMap in ERTMapComponent) to be static readonly so
the ResPath cannot be reassigned at runtime while keeping the same
initialization value.
In `@Content.Server/_Maid/ERTRecruitment/ERTRecruitedReasonEvent.cs`:
- Around line 3-12: ERTRecruitedReasonEvent currently exposes a mutable
SetReason method which breaks the usual immutable event pattern; change the
class to accept the reason via a constructor parameter and make the Reason field
read-only or a get-only property (replace SetReason with a constructor that sets
Reason) so the event is immutable after creation, and if this event is
transmitted over the network add the [NetSerializable] attribute alongside
[Serializable] to match other event types like GhostRecruitmentSuccessEvent.
In `@Content.Shared/_Maid/CVars/CVars.cs`:
- Around line 56-60: Update the XML doc comment for the public static readonly
CVarDef<bool> LoadErtMap so the grammar is correct and reads clearly (e.g.,
"Whether to generate the ERT map on round start."). Locate the declaration of
LoadErtMap (CVarDef.Create("maid.load_ert_map", true, CVar.SERVERONLY)) and
replace the existing summary text with the corrected sentence.
In
`@Content.Shared/_Maid/GhostRecruitment/GhostRecruitmentSpawnPointComponent.cs`:
- Around line 1-19: The DataField on JobId is missing an explicit key which is
inconsistent with other fields; update the attribute on the JobId field in
GhostRecruitmentSpawnPointComponent (the public string JobId = "Passenger") to
use [DataField("jobId")] so it matches the explicit keys used for
EntityPrototype, RecruitmentName and Priority and ensures consistent
serialization/deserialization.
In `@Content.Shared/_Maid/GhostRecruitment/RecruitedComponent.cs`:
- Around line 4-10: RecruitedComponent duplicates the recruitmentName field
already defined in GhostRecruitedComponent, causing split marker semantics;
consolidate by removing the duplicate state: delete the RecruitmentName field
(and/or the entire RecruitedComponent) and standardize on
GhostRecruitedComponent as the single recruited marker, then update any
systems/prototypes that construct, attach, or query RecruitedComponent to use
GhostRecruitedComponent instead so all recruitment logic references the same
component type.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
Resources/Textures/_Maid/Structures/Wallmounts/auth.rsi/auth_off.pngis excluded by!**/*.pngResources/Textures/_Maid/Structures/Wallmounts/auth.rsi/auth_on.pngis excluded by!**/*.png
📒 Files selected for processing (35)
Content.Client/_Maid/AuthPanel/AuthPanelBoundUserInterface.csContent.Client/_Maid/AuthPanel/AuthPanelMenu.xamlContent.Client/_Maid/AuthPanel/AuthPanelMenu.xaml.csContent.Client/_Maid/GhostRecruitment/GhostRecruitmentEuiAccept.csContent.Client/_Maid/GhostRecruitment/GhostRecruitmentEuiAcceptWindow.csContent.Client/_White/RadialSelector/RadialSelectorMenuBUI.csContent.Server/Mapping/MappingSystem.csContent.Server/_Maid/AuthPanel/AuthPanelSystem.csContent.Server/_Maid/ERTRecruitment/Commands/BlockERT.csContent.Server/_Maid/ERTRecruitment/ERTMapComponent.csContent.Server/_Maid/ERTRecruitment/ERTRecruitedReasonEvent.csContent.Server/_Maid/ERTRecruitment/ERTRecruitmentRule.csContent.Server/_Maid/ERTRecruitment/ERTRecruitmentRuleComponent.csContent.Server/_Maid/GhostRecruitment/GhostRecruitmentEuiAccept.csContent.Server/_Maid/GhostRecruitment/GhostRecruitmentEvents.csContent.Server/_Maid/GhostRecruitment/GhostRecruitmentSystem.csContent.Shared/_Maid/AuthPanel/AuthPanelComponent.csContent.Shared/_Maid/AuthPanel/SharedAuthPanel.csContent.Shared/_Maid/CVars/CVars.csContent.Shared/_Maid/GhostRecruitment/GhostRecruitedComponent.csContent.Shared/_Maid/GhostRecruitment/GhostRecruitmentEuiAccept.csContent.Shared/_Maid/GhostRecruitment/GhostRecruitmentEvents.csContent.Shared/_Maid/GhostRecruitment/GhostRecruitmentSpawnPointComponent.csContent.Shared/_Maid/GhostRecruitment/RecruitedComponent.csResources/ConfigPresets/Build/development.tomlResources/Locale/en-US/_Maid/auth-panel.ftlResources/Locale/en-US/_Maid/recruitment.ftlResources/Locale/ru-RU/_Maid/auth-panel.ftlResources/Locale/ru-RU/_Maid/recruitment.ftlResources/Maps/_Maid/ERTStation.ymlResources/Maps/_Maid/Shuttles/ert_shuttle.ymlResources/Prototypes/_Maid/Entities/Markers/Spawners/ert.ymlResources/Prototypes/_Maid/Entities/Structures/Wallmounts/auth.ymlResources/Prototypes/_Maid/events.ymlResources/Textures/_Maid/Structures/Wallmounts/auth.rsi/meta.json
| var playerCount = _playerManager.PlayerCount; | ||
| var ghostCount = ghostList.Count; | ||
| var aliveCount = playerCount - ghostCount; | ||
|
|
||
| if (aliveCount < playerCount / 2 && ghostList.Count > MinCount) | ||
| { | ||
| _gameTicker.AddGameRule(ERTRecruitmentRuleComponent.EventName); | ||
| } |
There was a problem hiding this comment.
Verify player count calculation assumptions.
The calculation aliveCount = playerCount - ghostCount assumes all non-ghost players are "alive" game participants. This may not account for:
- Admin observers
- Players in lobby
- Disconnected sessions
Also, the condition ghostList.Count > MinCount reuses MinCount (vote threshold) for ghost count, which seems like a different concept. Consider a separate threshold constant.
Suggested clarification
+ /// <summary>
+ /// Minimum ghost count required for ERT recruitment.
+ /// </summary>
+ public static int MinGhostCount = 3;
+
// In OnPerformAction:
- if (aliveCount < playerCount / 2 && ghostList.Count > MinCount)
+ if (aliveCount < playerCount / 2 && ghostList.Count > MinGhostCount)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Server/_Maid/AuthPanel/AuthPanelSystem.cs` around lines 124 - 131,
The aliveCount calculation and ghost threshold are too simplistic: instead of
using _playerManager.PlayerCount and aliveCount = playerCount - ghostCount
(which counts observers, lobby users, or disconnected sessions as "alive"),
compute the participant set explicitly (e.g., iterate
_playerManager.GetAllPlayers() or use a connected/active players API and filter
out admin observers/lobby/disconnected) and derive aliveCount from that filtered
list; also stop reusing MinCount for ghost threshold—introduce a distinct
constant (e.g., MinGhostsThreshold) and replace the condition ghostList.Count >
MinCount with ghostList.Count >= MinGhostsThreshold before calling
_gameTicker.AddGameRule(ERTRecruitmentRuleComponent.EventName).
| .desc = If something goes wrong... | ||
|
|
||
| ert-description = As a member of the Emergency Response Team, | ||
| belonging to NanoTrisen, you are tasked with deploying to a distressed station |
There was a problem hiding this comment.
Fix faction name typo in localized text.
Line 5 uses NanoTrisen; this should be NanoTrasen in player-facing copy.
📝 Proposed fix
- belonging to NanoTrisen, you are tasked with deploying to a distressed station
+ belonging to NanoTrasen, you are tasked with deploying to a distressed station📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| belonging to NanoTrisen, you are tasked with deploying to a distressed station | |
| belonging to NanoTrasen, you are tasked with deploying to a distressed station |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Resources/Locale/en-US/_Maid/auth-panel.ftl` at line 5, Replace the
misspelled faction name "NanoTrisen" with the correct "NanoTrasen" in the
localized string within Resources/Locale/en-US/_Maid/auth-panel.ftl (the line
containing "belonging to NanoTrisen, you are tasked with deploying to a
distressed station") so the player-facing copy shows the correct faction name.
| @@ -0,0 +1,6 @@ | |||
| accept-ert-window-title = Набор в отряды ОБР! | |||
| accept-ert-window-prompt-text-part = Готовы ли вы стать частью отряда ОБР? Если вы не уверены то лучше скажите НЕТ! | |||
There was a problem hiding this comment.
Add missing comma in RU prompt text.
For readability, add a comma after уверены.
📝 Proposed fix
-accept-ert-window-prompt-text-part = Готовы ли вы стать частью отряда ОБР? Если вы не уверены то лучше скажите НЕТ!
+accept-ert-window-prompt-text-part = Готовы ли вы стать частью отряда ОБР? Если вы не уверены, то лучше скажите НЕТ!📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| accept-ert-window-prompt-text-part = Готовы ли вы стать частью отряда ОБР? Если вы не уверены то лучше скажите НЕТ! | |
| accept-ert-window-prompt-text-part = Готовы ли вы стать частью отряда ОБР? Если вы не уверены, то лучше скажите НЕТ! |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Resources/Locale/ru-RU/_Maid/recruitment.ftl` at line 2, The Russian prompt
string for the key accept-ert-window-prompt-text-part is missing a comma for
readability; update the message value to insert a comma after "уверены" so it
reads "Если вы не уверены, то лучше скажите НЕТ!" ensuring only the punctuation
change is made and the key name accept-ert-window-prompt-text-part remains
unchanged.
| - type: GhostRecruitmentSpawnPoint | ||
| prototype: RandomHumanoidSpawnerERTJanitor | ||
| recruitmentName: ERTRecruitment | ||
| jobId: ERTJanitor | ||
| - type: Sprite | ||
| layers: | ||
| - state: green | ||
| - state: ertjanitor | ||
|
|
||
| - type: entity | ||
| parent: SpawnPointEventBase | ||
| id: SpawnPointERTEventERTJanitorEVA | ||
| name: ERTJanitorEVA | ||
| components: | ||
| - type: GhostRecruitmentSpawnPoint | ||
| prototype: RandomHumanoidSpawnerERTJanitorEVA | ||
| recruitmentName: ERTRecruitment | ||
| jobId: ERTJanitor |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the sorting logic in GhostRecruitmentSystem.cs
echo "=== Sorting logic in GhostRecruitmentSystem.cs ==="
rg -n "OrderBy.*Priority" Content.Server/_Maid/GhostRecruitment/GhostRecruitmentSystem.cs
echo ""
echo "=== GhostRecruitmentSpawnPointComponent definition ==="
rg -A 10 "class GhostRecruitmentSpawnPointComponent" -g '*.cs'
echo ""
echo "=== Priority field in component ==="
rg -B 2 -A 2 "public.*Priority|Priority.*=" -g '*.cs' | grep -A 2 -B 2 "GhostRecruitmentSpawnPoint"
echo ""
echo "=== YAML spawner entries (context around priority) ==="
rg -n -C 3 "SpawnPointERTEventERTJanitor\|priority:" Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.ymlRepository: TheMaidDev/Maid-14
Length of output: 2249
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List the file to confirm it exists
ls -lh Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.yml
echo ""
echo "=== Full YAML file content ==="
cat -n Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.yml
echo ""
echo "=== Search for all priority fields ==="
rg -n "priority:" Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.yml
echo ""
echo "=== Search for all SpawnPointERT entries ==="
rg -n "id: SpawnPointERT" Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.ymlRepository: TheMaidDev/Maid-14
Length of output: 5859
Janitor spawners missing explicit priority default to 5, placing them after other core roles.
Spawners are sorted by ascending priority in Content.Server/_Maid/GhostRecruitment/GhostRecruitmentSystem.cs (Line 77). Leader roles have priority: 1, and Engineer/Security/Medical all have priority: 2, but Janitor entries on Lines 46 and 60 lack explicit priority, causing them to default to 5, which places them at the lowest priority. Add priority: 2 to make the assignment order consistent with other core roles.
🔧 Proposed fix
- type: entity
parent: SpawnPointEventBase
id: SpawnPointERTEventERTJanitor
name: ERTJanitor
components:
- type: GhostRecruitmentSpawnPoint
prototype: RandomHumanoidSpawnerERTJanitor
recruitmentName: ERTRecruitment
+ priority: 2
jobId: ERTJanitor - type: entity
parent: SpawnPointEventBase
id: SpawnPointERTEventERTJanitorEVA
name: ERTJanitorEVA
components:
- type: GhostRecruitmentSpawnPoint
prototype: RandomHumanoidSpawnerERTJanitorEVA
recruitmentName: ERTRecruitment
+ priority: 2
jobId: ERTJanitor📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - type: GhostRecruitmentSpawnPoint | |
| prototype: RandomHumanoidSpawnerERTJanitor | |
| recruitmentName: ERTRecruitment | |
| jobId: ERTJanitor | |
| - type: Sprite | |
| layers: | |
| - state: green | |
| - state: ertjanitor | |
| - type: entity | |
| parent: SpawnPointEventBase | |
| id: SpawnPointERTEventERTJanitorEVA | |
| name: ERTJanitorEVA | |
| components: | |
| - type: GhostRecruitmentSpawnPoint | |
| prototype: RandomHumanoidSpawnerERTJanitorEVA | |
| recruitmentName: ERTRecruitment | |
| jobId: ERTJanitor | |
| - type: GhostRecruitmentSpawnPoint | |
| prototype: RandomHumanoidSpawnerERTJanitor | |
| recruitmentName: ERTRecruitment | |
| priority: 2 | |
| jobId: ERTJanitor | |
| - type: Sprite | |
| layers: | |
| - state: green | |
| - state: ertjanitor | |
| - type: entity | |
| parent: SpawnPointEventBase | |
| id: SpawnPointERTEventERTJanitorEVA | |
| name: ERTJanitorEVA | |
| components: | |
| - type: GhostRecruitmentSpawnPoint | |
| prototype: RandomHumanoidSpawnerERTJanitorEVA | |
| recruitmentName: ERTRecruitment | |
| priority: 2 | |
| jobId: ERTJanitor |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.yml` around lines 46
- 63, The Janitor GhostRecruitmentSpawnPoint entries are missing an explicit
priority and thus default to 5; update the two GhostRecruitmentSpawnPoint
components (the ones using prototype RandomHumanoidSpawnerERTJanitor and
RandomHumanoidSpawnerERTJanitorEVA in entities SpawnPointERTEventERTJanitor and
SpawnPointERTEventERTJanitorEVA) to include priority: 2 so they sort alongside
other core roles (Engineer/Security/Medical) in GhostRecruitmentSystem.
There was a problem hiding this comment.
All good, that's how it should be
There was a problem hiding this comment.
@RedBurningPhoenix, understood! The Janitor spawners intentionally use the default priority: 5, placing them at a lower recruitment priority than other core ERT roles. I'll keep this in mind for future reviews.
( 🐰 nods thoughtfully )
✏️ Learnings added
Learnt from: RedBurningPhoenix
Repo: TheMaidDev/Maid-14 PR: 31
File: Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.yml:46-63
Timestamp: 2026-03-11T10:18:43.655Z
Learning: In `Resources/Prototypes/_Maid/Entities/Markers/Spawners/ert.yml` (repo: TheMaidDev/Maid-14), ERT Janitor spawners (`SpawnPointERTEventERTJanitor` and `SpawnPointERTEventERTJanitorEVA`) intentionally omit an explicit `priority` field, defaulting to `5`. This is by design — Janitor roles have lower ghost recruitment priority than other core ERT roles (Leader: 1, Engineer/Security/Medical: 2).
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| "license": "CC-BY-SA-3.0", | ||
| "copyright": "from TG ya hui znaet", |
There was a problem hiding this comment.
Clarify the copyright attribution for license compliance.
The copyright field "from TG ya hui znaet" (colloquial Russian for "from TG, who knows") does not provide proper attribution. CC-BY-SA-3.0 requires clear attribution to the original author. Please update with the actual source and author information (e.g., "Taken from tgstation at commit X" or the original artist's name).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Resources/Textures/_Maid/Structures/Wallmounts/auth.rsi/meta.json` around
lines 3 - 4, Update the metadata to provide a proper, specific attribution for
CC-BY-SA-3.0 compliance: replace the current sloppy value in the "copyright"
field with the actual source and author (for example, "Taken from tgstation at
commit <hash>" or "Original art by <artist name> (source: <URL/handle>)"), or if
the exact author/commit is unknown, add a clear note indicating the best-known
source and that the original author is unknown; ensure the "license" and
"copyright" fields remain intact and accurate.
There was a problem hiding this comment.
Поменяем панель
There was a problem hiding this comment.
@RedBurningPhoenix, понял — если панель будет заменена на собственный ассет, проблема с атрибуцией исчезнет сама собой. Если же будет использоваться ассет из другого репозитория (например, tgstation), не забудьте указать корректную атрибуцию в copyright поле, например:
"copyright": "Taken from tgstation, original author <name> (https://github.com/tgstation/tgstation/commit/<hash>)"|
Linter в параллели отвечает что нет определённых полей |
| _resMan.UserData.CreateDir(new ResPath(saveDir).ToRootedPath()); | ||
|
|
||
| var path = new ResPath(Path.Combine(saveDir, $"{DateTime.Now:yyyy-M-dd_HH.mm.ss}-AUTO.yml")); | ||
| var path = new ResPath($"{saveDir}/{DateTime.Now:yyyy-M-dd_HH.mm.ss}-AUTO.yml"); |
There was a problem hiding this comment.
А, забыл про это, у меня крашалась игра из за этого, убрал
|
|
||
| _menu.OnRedButtonPressed(_ => SendButtonPressed(AuthPanelAction.ERTRecruit)); | ||
| // _menu.OnAccessButtonPressed(_ => SendButtonPressed(AuthPanelAction.AddAccess)); | ||
| // _menu.OnBluespaceWeaponButtonPressed(_ => SendButtonPressed(AuthPanelAction.BluespaceWeapon)); |
There was a problem hiding this comment.
Блюспейс арты точно не планируется, вырезать и всё.
| // if (action.Action is AuthPanelAction.BluespaceWeapon) | ||
| // _menu?.SetWeaponCount(action.ConfirmedPeopleCount, action.MaxConfirmedPeopleCount); |
There was a problem hiding this comment.
Аналогично про блюспейс
| <!-- <BoxContainer HorizontalExpand="True" Name="AccessContainer"> | ||
| <Button Name="AccessButton" MinWidth="410" Margin="0 5 0 0" HorizontalAlignment="Left" Text="{Loc 'auth-panel-access-button'}" Disabled="True"/> | ||
| <Label Name="AccessCount" Margin="25 0 4 0" HorizontalAlignment="Right" Visible="False"/> | ||
| </BoxContainer> | ||
|
|
||
| <BoxContainer HorizontalExpand="True" Name="BluespaceWeaponContainer"> | ||
| <Button Name="BluespaceWeaponButton" MinWidth="410" Margin="0 5 0 5" HorizontalAlignment="Left" Text="{Loc 'auth-panel-unlock-weapon'}" Disabled="True"/> | ||
| <Label Name="BluespaceWeaponCount" Margin="25 0 4 0" HorizontalAlignment="Right" Visible="False"/> | ||
| </BoxContainer> --> |
| // public void OnAccessButtonPressed(Action<BaseButton.ButtonEventArgs> func) | ||
| // { | ||
| // AccessButton.OnPressed += func; | ||
| // } | ||
|
|
||
| // public void OnBluespaceWeaponButtonPressed(Action<BaseButton.ButtonEventArgs> func) | ||
| // { | ||
| // BluespaceWeaponButton.OnPressed += func; | ||
| // } |
| public void SetRedCount(int conf, int maxconf) | ||
| { | ||
| SetCount(RedCount, conf, maxconf); | ||
| RedButton.Disabled = conf >= maxconf; | ||
| // AccessContainer.Visible = false; | ||
| // BluespaceWeaponContainer.Visible = false; | ||
| } |
There was a problem hiding this comment.
Самой красной кнопки нет, под корень
| // public void SetAccessCount(int conf, int maxconf) | ||
| // { | ||
| // SetCount(AccessCount, conf, maxconf); | ||
| // AccessButton.Disabled = conf >= maxconf; | ||
| // RedContainer.Visible = false; | ||
| // BluespaceWeaponContainer.Visible = false; | ||
| // } | ||
|
|
||
| // public void SetWeaponCount(int conf, int maxconf) | ||
| // { | ||
| // SetCount(BluespaceWeaponCount, conf, maxconf); | ||
| // BluespaceWeaponButton.Disabled = conf >= maxconf; | ||
| // RedContainer.Visible = false; | ||
| // AccessContainer.Visible = false; | ||
| // } |


Добавлена консоль вызова ERT (панель авторизации)
Переносил из последнего коммита: https://github.com/frosty-dev/ss14-core
Изменения
🆑 CREAsTIVE
TODO: Мапперам надо отредактировать
Resources/Maps/_Maid/ERTStation.ymlи сделать полноценную ERT станцию и полноценный шаттл (Resources/Maps/_Maid/Shuttles/ert_shuttle.yml)Можно украсть из старого билда, но там используется много "эксклюзивных" ресурсов, у меня даже не получилось загрузить карту, надо фиксить
Как я уже упоминал в дискорде: система просто ужас какой то. Я не стал заниматься модификацией логики (т. к. это не входило в мою задачу). Однако я бы не рекомендовал "фикс" данной логики, будто легче с нуля переписать ИМХО.
Summary by CodeRabbit
New Features
Documentation & Localization
Configuration