Skip to content

Наночат by zekins#531

Closed
Evgencheg wants to merge 2 commits intocorvax-team:masterfrom
Evgencheg:ports/nanochat
Closed

Наночат by zekins#531
Evgencheg wants to merge 2 commits intocorvax-team:masterfrom
Evgencheg:ports/nanochat

Conversation

@Evgencheg
Copy link
Copy Markdown

@Evgencheg Evgencheg commented Mar 28, 2026

Описание PR

Добавлена система NanoChat - чат-картридж для КПК, позволяющий игрокам обмениваться текстовыми сообщениями в пределах станции. Реализован полный цикл: от добавления контактов до отправки сообщений с учетом различных помех.

Код взят и лицензирован на wega-team/ss14-wega#174 и wega-team/ss14-wega#179 (GPLv3)

Ну ещё кармашки агосту дал, тоже попросили.

Почему / Баланс

Альтернатива радио

Технические детали

Я ленивый, меня просто попросили портировать

Медиа

изображение

Требования

Согласие с условиями

  • Я согласен с условиями LICENSE и CLA.

Критические изменения

Отсутствуют

Список изменений
🆑 Evgencheg, Zekins

  • wl-add: Добавлен наночат в кпк! Теперь общаться веселее!
  • wl-tweak: Изменено количество карманов у АГоста, теперь их 4!

Summary by CodeRabbit

  • New Features

    • NanoChat cartridge: full PDA chat UI with contact list, group chats, message history, unread indicators, emoji picker, and send controls
    • Contact and group management: add/remove contacts, create/join/leave groups
    • Message delivery tracking, sound notifications, and mute control
  • Chores

    • Forensic scanner: print chat history reports from PDAs
    • Added prototype and preinstalled NanoChat cartridge entries for PDAs
    • Localization entries for NanoChat and forensics (en-US, ru-RU)
    • Inventory template: added four pocket slots to aghost template

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 28, 2026

RSI Diff Bot; head commit 63c4b8e merging into 5dc7575
This PR makes changes to 1 or more RSIs. Here is a summary of all changes:

Resources/Textures/_Wega/Interface/Misc/program_icons.rsi

State Old New Status
nano_chat Added

Edit: diff updated after 63c4b8e

@Evgencheg
Copy link
Copy Markdown
Author

Запрошено @cfif126

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

Adds a complete NanoChat PDA cartridge: client UI (fragment, controls, popups, emoji picker), shared networked state/events/components, server-side messaging system (delivery, groups, EMP/power distortion, range, cooldown), forensic chat printing, prototypes, assets, and localization.

Changes

Cohort / File(s) Summary
Client UI - Popups & Controls
Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatAddContactPopup.xaml, .../NanoChatAddContactPopup.xaml.cs, NanoChatCreateGroupPopup.xaml, .../NanoChatCreateGroupPopup.xaml.cs, NanoChatJoinGroupPopup.xaml, .../NanoChatJoinGroupPopup.xaml.cs, NanoChatEmojiPopup.xaml, .../NanoChatEmojiPopup.xaml.cs
New XAML windows and code-behind for add-contact, create/join-group, and emoji picker; input validation, normalization, callbacks for selected data/emojis.
Client UI - List/Message Controls
Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatContactControl.xaml, .../NanoChatContactControl.xaml.cs, NanoChatGroupControl.xaml, .../NanoChatGroupControl.cs, NanoChatMessageControl.xaml, .../NanoChatMessageControl.xaml.cs
Controls for rendering contacts, groups, and messages; bind to ChatContact/ChatGroup/ChatMessage models; unread indicators and press callbacks.
Client UI - Main Fragment & Orchestrator
Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml, .../NanoChatUiFragment.xaml.cs, NanoChatUi.cs
New UI fragment and UIFragment adapter that manage tabs, contact/group lists, message history, input, emoji integration, and forward UI actions as cartridge messages to the server.
Shared - Components, Messages, State
Content.Shared/_Wega/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs, .../NanoChatUiMessageEvent.cs, .../NanoChatUiState.cs, SharedNanoChatCartridgeSystem.cs
Networked component for storing chat/groups/messages, serializable UI payloads/events, UI state transfer types, and shared base system type.
Server - NanoChat System
Content.Server/_Wega/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs
New server system managing chat lifecycle: ID generation, message sending, cooldowns, power/EMP distortion, range checks, group join/create/leave, message storage, UI state pushes, audio/logging, and EMP distortion of stored text.
Server - Integrations
Content.Server/Forensics/Systems/ForensicScannerSystem.cs, Content.Server/PDA/PdaSystem.cs
Forensics: utility verb to print chat history to paper and formatting helpers; PDA system: new OwnerNameChangedEvent raised on owner changes.
Prototypes & Preinstallation
Resources/Prototypes/_Wega/Entities/Objects/Devices/cartridges.yml, Resources/Prototypes/Entities/Objects/Devices/pda.yml
Adds NanoChat cartridge prototype and adds NanoChatCartridge to multiple PDA preinstalled lists.
Localization
Resources/Locale/en-US/_Wega/nanochat/nanochatui.ftl, .../cartridges.ftl, .../forensics/forensics.ftl, Resources/Locale/ru-RU/_wega/nanochat/nanochatui.ftl, .../cartridges.ftl, .../forensics/forensics.ftl, Resources/Locale/en-US/ss14-ru/.../cartridges.ftl, Resources/Locale/ru-RU/ss14-ru/.../cartridges.ftl
New English and Russian localization entries for NanoChat UI, system messages, forensic printing, and cartridge names/descriptions.
Assets & Metadata
Resources/Textures/_Wega/Interface/Misc/program_icons.rsi/meta.json, Resources/Textures/_Wega/Interface/sound.svg.192dpi.png copy.yml, Resources/Textures/_Wega/Interface/soundoff.svg.192dpi.png.yml
RSI metadata for program icon and sample filter YAMLs added.
Inventory Templates
Resources/Prototypes/InventoryTemplates/aghost_inventory_template.yml
Adds four new pocket inventory slots to the aghost template.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NanoChatUi
    participant ServerSystem
    participant RecipientCartridge
    participant RecipientUI

    Client->>NanoChatUi: User sends message(recipientId, text)
    NanoChatUi->>ServerSystem: Emit NanoChatSendMessage event
    activate ServerSystem
    ServerSystem->>ServerSystem: Validate cooldown, sanitize, truncate
    ServerSystem->>ServerSystem: Check power/solar/EMP -> distort or block
    ServerSystem->>ServerSystem: Range check (map + distance)
    alt Delivery allowed
        ServerSystem->>RecipientCartridge: Append message to history
        ServerSystem->>RecipientCartridge: Mark unread, play audio (optional)
        ServerSystem->>RecipientUI: Push updated NanoChatUiState
    else Distorted/undelivered
        ServerSystem->>ServerSystem: Store distorted/undelivered copy in sender history
        ServerSystem->>RecipientUI: No delivery
    end
    ServerSystem->>Client: Push updated NanoChatUiState for sender
    deactivate ServerSystem
Loading
sequenceDiagram
    participant User
    participant NanoChatUiFragment
    participant ServerSystem
    participant GroupMembers

    User->>NanoChatUiFragment: CreateGroup(name)
    NanoChatUiFragment->>ServerSystem: NanoChatCreateGroup event
    activate ServerSystem
    ServerSystem->>ServerSystem: Generate groupId, add creator as member
    ServerSystem->>GroupMembers: Broadcast group state and system message
    deactivate ServerSystem
Loading
sequenceDiagram
    participant ForensicScanner
    participant ForensicSystem
    participant PDAContainer
    participant Paper

    ForensicScanner->>ForensicSystem: Invoke PrintChatHistory(targetPDA)
    activate ForensicSystem
    ForensicSystem->>PDAContainer: Access program-container and cartridges
    ForensicSystem->>ForensicSystem: Aggregate chat/contact/group messages
    ForensicSystem->>Paper: Spawn paper, set content to formatted history
    ForensicSystem->>ForensicScanner: Show success/failure popup
    deactivate ForensicSystem
Loading

Estimated Code Review Effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested Labels

size/XL

Suggested Reviewers

  • tau27
  • Litogin

Poem

🐰 I hopped through code with tiny paws so bright,
I stitched up chats, emojis, and a forensic write,
Contacts, groups, and paper trails in tow —
NanoChat blooms where little rabbits go! 🥕📱

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Наночат by zekins' is a non-English, vague reference that does not clearly describe the main change. While it references the feature name, it lacks specificity and context that would help someone scanning PR history understand the primary purpose. Consider using a more descriptive English title that clearly summarizes the main change, such as 'Add NanoChat cartridge system for PDA messaging' or similar, to improve clarity for team members reviewing PR history.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (4)
Content.Shared/_Wega/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs (1)

37-48: Consider documenting expected message length limits.

The Message property accepts arbitrary string content. Ensure that the server-side handler in NanoChatCartridgeSystem.cs validates message length to prevent abuse (excessively long messages, spam).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Wega/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs`
around lines 37 - 48, Add server-side validation for NanoChatSendMessage.Message
in the NanoChatCartridgeSystem.cs handler: enforce a configurable maximum length
(e.g., MAX_NANOCHAT_MESSAGE_LENGTH constant), trim or reject messages that
exceed it, and return/log a clear error or drop the message to prevent abuse;
perform this check when handling INanoChatUiMessagePayload/NanoChatSendMessage
before broadcasting or persisting the message and ensure empty or
whitespace-only messages are also rejected.
Content.Server/PDA/PdaSystem.cs (1)

344-346: Event declaration is outside namespace scope.

The OwnerNameChangedEvent is declared after the namespace closing brace, placing it in the global namespace. This is unconventional and may cause namespace resolution issues. Consider moving it inside the Content.Server.PDA namespace or into a separate file.

♻️ Proposed fix to move event inside namespace
     }
 }
-
-[ByRefEvent] // Corvax-Wega-NanoChat
-public record struct OwnerNameChangedEvent(); // Corvax-Wega-NanoChat
+
+[ByRefEvent] // Corvax-Wega-NanoChat
+public record struct OwnerNameChangedEvent(); // Corvax-Wega-NanoChat
+} // end namespace Content.Server.PDA

Or better yet, move the event definition inside the namespace block before the closing brace:

         private string? GetDeviceNetAddress(EntityUid uid)
         {
             // ... existing code ...
         }
+
+        [ByRefEvent] // Corvax-Wega-NanoChat
+        public record struct OwnerNameChangedEvent(); // Corvax-Wega-NanoChat
     }
 }
-
-[ByRefEvent] // Corvax-Wega-NanoChat
-public record struct OwnerNameChangedEvent(); // Corvax-Wega-NanoChat
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/PDA/PdaSystem.cs` around lines 344 - 346, The
OwnerNameChangedEvent record struct is declared outside the Content.Server.PDA
namespace — move its declaration inside the Content.Server.PDA namespace block
(or into a new file within that namespace) so it is not in the global namespace;
update the declaration of OwnerNameChangedEvent (the [ByRefEvent] public record
struct OwnerNameChangedEvent()) to reside before the namespace closing brace (or
create a new file with the same namespace and place the event there) to restore
proper namespace resolution.
Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml (1)

7-7: Set MaxLength on the group name input to match backend/client limits.

Line 7 should cap length in the field itself (e.g., 16) so users don’t lose characters silently on submit.

♻️ Proposed XAML tweak
-        <LineEdit Name="GroupNameInput" PlaceHolder="{Loc 'nano-chat-ui-group-name-placeholder'}"/>
+        <LineEdit Name="GroupNameInput"
+                  PlaceHolder="{Loc 'nano-chat-ui-group-name-placeholder'}"
+                  MaxLength="16"/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml`
at line 7, Add a MaxLength to the group name input so the UI enforces the same
character limit as the backend/client (e.g., 16); update the LineEdit with
Name="GroupNameInput" to include MaxLength="16" (or the exact backend-defined
constant) so users cannot enter more characters than allowed and won’t lose
characters on submit.
Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatEmojiPopup.xaml.cs (1)

15-69: Avoid per-instance allocation of the emoji catalog.

Lines 15-69 allocate a large List<string> for every popup instance. Make it static readonly (or immutable) to reduce allocations and make intent clearer.

♻️ Proposed refactor
-    private readonly List<string> _emojis = new()
+    private static readonly string[] Emojis =
     {
         // ...
     };
@@
-        foreach (var emoji in _emojis)
+        foreach (var emoji in Emojis)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatEmojiPopup.xaml.cs`
around lines 15 - 69, The _emojis field in NanoChatEmojiPopup.xaml.cs is
allocated per instance; change the declaration of _emojis to a single shared
immutable/static readonly collection (e.g., static readonly
IReadOnlyList<string> or static readonly string[]) so the large emoji catalog is
created once, and update any code that mutates _emojis to treat it as read-only
or copy before modifying; reference the field named _emojis in the
NanoChatEmojiPopup class when making this change.
🤖 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/_Wega/CartridgeLoader/Cartridges/NanoChatAddContactPopup.xaml.cs`:
- Around line 23-27: The current logic in the NanoChatAddContactPopup.xaml.cs
silently truncates normalizedId (from NormalizeContactId(ContactIdInput.Text))
to 5 chars which can map user input to a different contact; instead validate
that normalizedId matches the required pattern ("#" followed by exactly 4
digits) and reject/notify the user when it does not. Replace the truncation
logic around normalizedId/contactId with a validation check (use
NormalizeContactId(ContactIdInput.Text) → normalizedId, then assert Regex or
explicit check for prefix '#' and length 5 with digits) and surface a validation
error or disable submission rather than slicing the string.

In
`@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml.cs`:
- Around line 19-27: Extract the magic number by adding a shared constant (e.g.,
MAX_GROUP_NAME_LENGTH) and use it in the client truncation logic (currently
using GroupNameInput and OnGroupCreated) instead of the literal 16; trim the
input (call Trim() on GroupNameInput.Text) before checking emptiness and
truncating so leading/trailing spaces are removed; then add server-side
validation in NanoChatCartridgeSystem.CreateGroup to enforce the same
MAX_GROUP_NAME_LENGTH and reject or sanitize names that are empty after trim or
exceed the limit (return an error/throw so modified clients cannot bypass the
constraint).

In
`@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatJoinGroupPopup.xaml.cs`:
- Around line 22-39: NormalizeGroupId currently trims/strips and may prepend "G"
to numeric input but allows non-canonical forms and the caller truncates IDs;
update NormalizeGroupId to return an uppercase canonical ID in the form "G"
followed by exactly 4 digits (e.g., use a regex like ^G\d{4}$ after making input
uppercase and removing spaces) and remove the caller-side truncation (the
groupId = normalizedId[..5] logic). Also, only call OnGroupJoined with the
validated canonical ID (from NormalizeGroupId) and handle invalid inputs (e.g.,
show an error or disable the join) instead of invoking OnGroupJoined with
non-canonical values; refer to NormalizeGroupId, OnGroupJoined, and
GroupIdInput.Text to locate edits.

In `@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs`:
- Around line 21-22: The CloseChat handler is currently a client-only no-op
because CloseChat is never assigned, so clicking the button only clears
ActiveChatId locally while the server still holds ActiveChat; either wire
CloseChat to send the server-side close message from the NanoChatUi controller
(assign the CloseChat Action in the NanoChatUi class to call the
transport/message method that clears ActiveChat on the server) or disable/hide
the UI button until CloseChat is non-null; also apply the same fix for
LeaveChat/its button so both actions either have server-side wiring or are
disabled when not assigned (refer to CloseChat, LeaveChat, ActiveChatId,
ActiveChat, and the NanoChatUi class to locate where to assign or gate the
button).
- Around line 228-230: The UI is calling
Loc.GetString("nano-chat-ui-unknown-chat") in NanoChatUiFragment.xaml.cs
(ChatTitle.Text assignment) but that key is missing from the NanoChat locale
resources; add the missing "nano-chat-ui-unknown-chat" entry to the NanoChat
locale files (for all supported languages), provide appropriate translated
strings (e.g., "Unknown chat" for en), and update/rebuild any localization
resource bundle so Loc.GetString resolves correctly at runtime.

In `@Content.Server/_Wega/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs`:
- Around line 379-420: SendGroupMessage appends per-recipient copies to
memberComp.Messages[groupId] but UpdateUiState currently serializes the shared
group.Messages, causing devices to see messages they never received; change
UpdateUiState (the code path called with (memberEntity, memberComp)) so when
building the UI payload for a group it prefers the per-cartridge history
memberComp.Messages[groupId] if present (falling back to group.Messages only
when the per-device list is missing), and ensure the same fix is applied for the
other occurrence around lines 431-437 where group.Messages is used instead of
memberComp.Messages; update references to ChatGroup/ChatMessage serialization so
it uses memberComp.Messages and memberComp.Groups for per-device state.
- Around line 72-76: Active chat state becomes inconsistent after
ejecting/reinserting a cartridge because MapInit only populates _activeChats
while OnCartridgeRemoved() removes the cartridge from ChatGroupData.Members but
does not clear the cartridge-side ent.Comp.Groups or remove its entry from
_activeChats; update OnCartridgeRemoved() to (1) remove the ent.Comp.ChatId key
from _activeChats, (2) clear ent.Comp.Groups (or reset to an empty collection)
so the client no longer sees stale groups, and (3) update any ChatGroupData
structures to drop references to this owner; do the same fix for the analogous
block at lines handling removal (the 105-110 area) so ejecting fully cleans both
server and ent.Comp state and reinserting repopulates _activeChats only via
MapInit/GenerateUniqueChatId as before.
- Around line 186-199: SetActiveChat currently trusts any chatId and will expose
group messages; add a membership guard so group chats can only be selected if
the user is a member. Specifically, in SetActiveChat (and the other similar
sites indicated: the block around 304-307 and the SendMessage logic around
431-437), detect when chatId refers to a group (the Groups dictionary / group id
pattern) and verify membership via the group's membership data or the user's
membership map before setting ent.Comp.ActiveChat, returning early (or
no-op/log) if the user is not a member; do the same check before returning group
messages in UpdateUiState and before broadcasting in SendMessage so forged
client packets cannot read or post to groups the user hasn’t joined.
- Around line 231-245: The code currently ignores the boolean result of
group.Members.Add and always broadcasts Join and refresh events; change the
logic in the block handling _groups.TryGetValue(groupId, out var group) to only
proceed when group.Members.Add(ent.Comp.ChatId) returns true (i.e., membership
actually changed). If Add returns false, skip creating/updating ent.Comp.Groups[
groupId ], skip calling NotifyGroupMembers(groupId, ...), and skip
UpdateAllGroupMembersUi(groupId); when Add returns true, update ent.Comp.Groups
with the new ChatGroup (using group.Members.Count) and then call
NotifyGroupMembers and UpdateAllGroupMembersUi as currently done.

In `@Content.Server/Forensics/Systems/ForensicScannerSystem.cs`:
- Around line 275-337: PrintChatHistory bypasses the scanner's print cooldown
(PrintReadyAt) and spawns paper unconditionally; add the same gating and
cooldown advance as OnPrint. At the top of PrintChatHistory check if
component.PrintReadyAt > _gameTiming.CurTime and if so call
_popupSystem.PopupEntity with the existing cooldown message and return; after
successfully spawning/setting paper (before popup "printed") advance the
cooldown by setting component.PrintReadyAt = _gameTiming.CurTime +
component.PrintCooldown (or call the existing OnPrint(uid, component) helper if
available) so the PDA chat print respects the same throttle as other prints.
- Around line 365-443: The scanner treats keys with empty message lists as "has
messages", causing blank sections; update logic to treat empty lists as "no
messages": when iterating chatComp.Messages skip/handle entries where
messages.Count == 0 (so they print the localized no-messages line instead of a
blank block), and when checking for contacts or groups use TryGetValue(...) and
consider the messages list empty if TryGetValue returns false OR the retrieved
list.Count == 0 (update the contact-check around
chatComp.Messages.ContainsKey(contact.ContactId) and the group branch that uses
chatComp.Messages.TryGetValue(groupId, out var groupMessages) to check
groupMessages.Count == 0).

In `@Resources/Locale/en-US/_Wega/forensics/forensics.ftl`:
- Around line 1-27: The en-US locale file contains Russian strings for multiple
keys (e.g., forensic-scanner-print-chat-history-text,
forensic-scanner-print-chat-history-message,
forensic-scanner-chat-history-title, forensic-scanner-chat-history-header,
forensic-scanner-chat-history-printed, forensic-scanner-no-cartridge,
forensic-scanner-no-chat-cartridge, forensic-scanner-chat-cartridge-header,
forensic-scanner-chat-no-data, forensic-scanner-chat-with-contact,
forensic-scanner-chat-with-unknown, forensic-scanner-chat-contact-no-messages,
forensic-scanner-chat-groups-header, forensic-scanner-chat-group-info,
forensic-scanner-chat-group-messages, forensic-scanner-chat-group-no-messages,
forensic-scanner-chat-archived-group, forensic-scanner-chat-you); replace each
Russian value with the correct English copy (or copy from the default/English
source) so en-US users see English UI and report text, preserving the same
message keys and interpolation placeholders like { $owner }, { $time }, { $id },
{ $name }, and { $members } exactly.

In `@Resources/Locale/en-US/_Wega/nanochat/nanochatui.ftl`:
- Around line 2-60: This file under en-US contains Russian strings—replace each
Russian value with its English translation so the en-US locale actually provides
English text; update all keys such as nano-chat-ui-add,
nano-chat-ui-add-contact-title, nano-chat-ui-contact-id-placeholder,
nano-chat-ui-contact-name-placeholder, nano-chat-ui-create-group-title,
nano-chat-ui-emoji-title, nano-chat-ui-group-name-placeholder,
nano-chat-ui-join-group-title, nano-chat-ui-message-placeholder,
nano-chat-ui-tab-contacts, nano-chat-ui-title, nano-chat-ui-unread-indicator and
the system messages nano-chat-create-group-message, nano-chat-join-message,
nano-chat-leave-message to appropriate English phrases (including translating
placeholders and preserving interpolation variables like {$name} and
{$groupName}). Ensure punctuation and placeholder formats are kept intact.

In `@Resources/Locale/ru-RU/_wega/cartridge-loader/cartridges.ftl`:
- Line 1: The Russian locale directory uses `_wega` while English uses `_Wega`;
rename the Russian directory to match the established casing (change `_wega` ->
`_Wega`) and update all references; specifically move/rename the directory
containing cartridges.ftl and update any config, import, or build references
that point to `_wega` so all locales use the same `_Wega` casing (use git mv to
preserve history and run tests/build to verify).

In `@Resources/Textures/_Wega/Interface/sound.svg.192dpi.png` copy.yml:
- Around line 1-2: The file named "sound.svg.192dpi.png copy.yml" appears to be
an accidental duplicate; either delete this file if it's redundant or rename it
to "sound.svg.192dpi.png.yml" so it matches the intended texture asset, ensuring
the YAML contents remain unchanged and any references to the texture in the
project point to the corrected filename.

---

Nitpick comments:
In
`@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml`:
- Line 7: Add a MaxLength to the group name input so the UI enforces the same
character limit as the backend/client (e.g., 16); update the LineEdit with
Name="GroupNameInput" to include MaxLength="16" (or the exact backend-defined
constant) so users cannot enter more characters than allowed and won’t lose
characters on submit.

In `@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatEmojiPopup.xaml.cs`:
- Around line 15-69: The _emojis field in NanoChatEmojiPopup.xaml.cs is
allocated per instance; change the declaration of _emojis to a single shared
immutable/static readonly collection (e.g., static readonly
IReadOnlyList<string> or static readonly string[]) so the large emoji catalog is
created once, and update any code that mutates _emojis to treat it as read-only
or copy before modifying; reference the field named _emojis in the
NanoChatEmojiPopup class when making this change.

In `@Content.Server/PDA/PdaSystem.cs`:
- Around line 344-346: The OwnerNameChangedEvent record struct is declared
outside the Content.Server.PDA namespace — move its declaration inside the
Content.Server.PDA namespace block (or into a new file within that namespace) so
it is not in the global namespace; update the declaration of
OwnerNameChangedEvent (the [ByRefEvent] public record struct
OwnerNameChangedEvent()) to reside before the namespace closing brace (or create
a new file with the same namespace and place the event there) to restore proper
namespace resolution.

In `@Content.Shared/_Wega/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs`:
- Around line 37-48: Add server-side validation for NanoChatSendMessage.Message
in the NanoChatCartridgeSystem.cs handler: enforce a configurable maximum length
(e.g., MAX_NANOCHAT_MESSAGE_LENGTH constant), trim or reject messages that
exceed it, and return/log a clear error or drop the message to prevent abuse;
perform this check when handling INanoChatUiMessagePayload/NanoChatSendMessage
before broadcasting or persisting the message and ensure empty or
whitespace-only messages are also rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd4eb385-453b-4858-ba14-4b9ccb3ced75

📥 Commits

Reviewing files that changed from the base of the PR and between 5dc7575 and e7f5555.

⛔ Files ignored due to path filters (5)
  • Resources/Textures/_Wega/Interface/Misc/program_icons.rsi/nano_chat.png is excluded by !**/*.png
  • Resources/Textures/_Wega/Interface/sound.svg.192dpi.png is excluded by !**/*.png
  • Resources/Textures/_Wega/Interface/sound.svg.192dpi.svg is excluded by !**/*.svg
  • Resources/Textures/_Wega/Interface/soundoff.svg.192dpi.png is excluded by !**/*.png
  • Resources/Textures/_Wega/Interface/soundoff.svg.192dpi.svg is excluded by !**/*.svg
📒 Files selected for processing (37)
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatAddContactPopup.xaml
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatAddContactPopup.xaml.cs
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatContactControl.xaml
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatContactControl.xaml.cs
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml.cs
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatEmojiPopup.xaml
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatEmojiPopup.xaml.cs
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatGroupControl.cs
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatGroupControl.xaml
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatJoinGroupPopup.xaml
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatJoinGroupPopup.xaml.cs
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatMessageControl.xaml
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatMessageControl.xaml.cs
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatUi.cs
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml
  • Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs
  • Content.Server/Forensics/Systems/ForensicScannerSystem.cs
  • Content.Server/PDA/PdaSystem.cs
  • Content.Server/_Wega/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs
  • Content.Shared/_Wega/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs
  • Content.Shared/_Wega/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs
  • Content.Shared/_Wega/CartridgeLoader/Cartridges/NanoChatUiState.cs
  • Content.Shared/_Wega/CartridgeLoader/Cartridges/SharedNanoChatCartridgeSystem.cs
  • Resources/Locale/en-US/_Wega/cartridge-loader/cartridges.ftl
  • Resources/Locale/en-US/_Wega/forensics/forensics.ftl
  • Resources/Locale/en-US/_Wega/nanochat/nanochatui.ftl
  • Resources/Locale/en-US/ss14-ru/prototypes/_wega/entities/objects/devices/cartridges.ftl
  • Resources/Locale/ru-RU/_wega/cartridge-loader/cartridges.ftl
  • Resources/Locale/ru-RU/_wega/forensics/forensics.ftl
  • Resources/Locale/ru-RU/_wega/nanochat/nanochatui.ftl
  • Resources/Locale/ru-RU/ss14-ru/prototypes/_wega/entities/objects/devices/cartridges.ftl
  • Resources/Prototypes/Entities/Objects/Devices/pda.yml
  • Resources/Prototypes/_Wega/Entities/Objects/Devices/cartridges.yml
  • Resources/Textures/_Wega/Interface/Misc/program_icons.rsi/meta.json
  • Resources/Textures/_Wega/Interface/sound.svg.192dpi.png copy.yml
  • Resources/Textures/_Wega/Interface/soundoff.svg.192dpi.png.yml

Comment on lines +23 to +27
var normalizedId = NormalizeContactId(ContactIdInput.Text);

var contactId = normalizedId.Length <= 5
? normalizedId
: normalizedId[..5];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not silently truncate contact IDs.

Lines 25-27 can transform user input into a different valid ID (e.g., #12345#1234), which risks adding/messaging the wrong contact. Validate format (# + 4 digits) and reject invalid input instead of truncating.

🐛 Proposed fix
                 var normalizedId = NormalizeContactId(ContactIdInput.Text);
-
-                var contactId = normalizedId.Length <= 5
-                    ? normalizedId
-                    : normalizedId[..5];
+                if (normalizedId.Length != 5 ||
+                    normalizedId[0] != '#' ||
+                    !normalizedId[1..].All(char.IsDigit))
+                    return;
@@
-                OnContactAdded?.Invoke(contactId, contactName);
+                OnContactAdded?.Invoke(normalizedId, contactName);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatAddContactPopup.xaml.cs`
around lines 23 - 27, The current logic in the NanoChatAddContactPopup.xaml.cs
silently truncates normalizedId (from NormalizeContactId(ContactIdInput.Text))
to 5 chars which can map user input to a different contact; instead validate
that normalizedId matches the required pattern ("#" followed by exactly 4
digits) and reject/notify the user when it does not. Replace the truncation
logic around normalizedId/contactId with a validation check (use
NormalizeContactId(ContactIdInput.Text) → normalizedId, then assert Regex or
explicit check for prefix '#' and length 5 with digits) and surface a validation
error or disable submission rather than slicing the string.

Comment on lines +19 to +27
if (!string.IsNullOrWhiteSpace(GroupNameInput.Text))
{
var groupName = GroupNameInput.Text.Length <= 16
? GroupNameInput.Text
: GroupNameInput.Text[..16];

OnGroupCreated?.Invoke(groupName);
Close();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check server-side CreateGroup implementation for validation
ast-grep --pattern $'private void CreateGroup($_, string groupName)
{
  $$$
}'

Repository: corvax-team/ss14-wl

Length of output: 45


🏁 Script executed:

fd -i "NanoChatCartridgeSystem" --type f

Repository: corvax-team/ss14-wl

Length of output: 218


🏁 Script executed:

rg "CreateGroup" -A 15 --type cs

Repository: corvax-team/ss14-wl

Length of output: 15335


🏁 Script executed:

fd -i "nanochat" --type f

Repository: corvax-team/ss14-wl

Length of output: 1826


🏁 Script executed:

cat -n Content.Server/_Wega/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs | sed -n '200,250p'

Repository: corvax-team/ss14-wl

Length of output: 1942


🏁 Script executed:

rg -i "const.*16\|const.*group.*name\|const.*max" Content.Server/_Wega/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs

Repository: corvax-team/ss14-wl

Length of output: 45


🏁 Script executed:

rg -i "const.*16\|const.*group.*name\|const.*max" Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml.cs

Repository: corvax-team/ss14-wl

Length of output: 45


🏁 Script executed:

cat -n Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml.cs

Repository: corvax-team/ss14-wl

Length of output: 1128


Extract magic number to constant and add server-side validation.

Three issues:

  1. Magic number 16: The truncation limit should be defined as a named constant in shared code to keep client and server in sync.

  2. Missing server-side validation: The CreateGroup method in NanoChatCartridgeSystem.cs (lines 202-227) accepts the group name without any length validation. A malicious or modified client could bypass the client-side truncation and send excessively long names. Add validation on the server.

  3. No whitespace trimming: Input like " GroupName " passes the whitespace check but is not trimmed before truncation, resulting in names with unwanted spacing.

🛡️ Proposed client-side fix with trim
         CreateButton.OnPressed += _ =>
         {
-            if (!string.IsNullOrWhiteSpace(GroupNameInput.Text))
+            var trimmedName = GroupNameInput.Text?.Trim();
+            if (!string.IsNullOrEmpty(trimmedName))
             {
-                var groupName = GroupNameInput.Text.Length <= 16
-                    ? GroupNameInput.Text
-                    : GroupNameInput.Text[..16];
+                var groupName = trimmedName.Length <= 16
+                    ? trimmedName
+                    : trimmedName[..16];
 
                 OnGroupCreated?.Invoke(groupName);
                 Close();
             }
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatCreateGroupPopup.xaml.cs`
around lines 19 - 27, Extract the magic number by adding a shared constant
(e.g., MAX_GROUP_NAME_LENGTH) and use it in the client truncation logic
(currently using GroupNameInput and OnGroupCreated) instead of the literal 16;
trim the input (call Trim() on GroupNameInput.Text) before checking emptiness
and truncating so leading/trailing spaces are removed; then add server-side
validation in NanoChatCartridgeSystem.CreateGroup to enforce the same
MAX_GROUP_NAME_LENGTH and reject or sanitize names that are empty after trim or
exceed the limit (return an error/throw so modified clients cannot bypass the
constraint).

Comment on lines +22 to +39
var normalizedId = NormalizeGroupId(GroupIdInput.Text.Trim());
var groupId = normalizedId.Length <= 5
? normalizedId
: normalizedId[..5];

OnGroupJoined?.Invoke(groupId);
Close();
}
};
}

private string NormalizeGroupId(string contactId)
{
var cleanedId = contactId.Trim().Replace(" ", "");
if (!cleanedId.StartsWith("G") && cleanedId.All(char.IsDigit))
return "G" + cleanedId;

return cleanedId;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate canonical group ID instead of truncating.

Lines 23-25 may rewrite input into a different ID, and Lines 33-39 can pass non-canonical IDs (e.g., lowercase g1234) through. Normalize to uppercase and require exact G + 4 digits before OnGroupJoined.

🐛 Proposed fix
             if (!string.IsNullOrWhiteSpace(GroupIdInput.Text))
             {
-                var normalizedId = NormalizeGroupId(GroupIdInput.Text.Trim());
-                var groupId = normalizedId.Length <= 5
-                    ? normalizedId
-                    : normalizedId[..5];
+                var normalizedId = NormalizeGroupId(GroupIdInput.Text).ToUpperInvariant();
+                if (normalizedId.Length != 5 ||
+                    normalizedId[0] != 'G' ||
+                    !normalizedId[1..].All(char.IsDigit))
+                    return;
 
-                OnGroupJoined?.Invoke(groupId);
+                OnGroupJoined?.Invoke(normalizedId);
                 Close();
             }
📝 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.

Suggested change
var normalizedId = NormalizeGroupId(GroupIdInput.Text.Trim());
var groupId = normalizedId.Length <= 5
? normalizedId
: normalizedId[..5];
OnGroupJoined?.Invoke(groupId);
Close();
}
};
}
private string NormalizeGroupId(string contactId)
{
var cleanedId = contactId.Trim().Replace(" ", "");
if (!cleanedId.StartsWith("G") && cleanedId.All(char.IsDigit))
return "G" + cleanedId;
return cleanedId;
var normalizedId = NormalizeGroupId(GroupIdInput.Text).ToUpperInvariant();
if (normalizedId.Length != 5 ||
normalizedId[0] != 'G' ||
!normalizedId[1..].All(char.IsDigit))
return;
OnGroupJoined?.Invoke(normalizedId);
Close();
}
};
}
private string NormalizeGroupId(string contactId)
{
var cleanedId = contactId.Trim().Replace(" ", "");
if (!cleanedId.StartsWith("G") && cleanedId.All(char.IsDigit))
return "G" + cleanedId;
return cleanedId;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatJoinGroupPopup.xaml.cs`
around lines 22 - 39, NormalizeGroupId currently trims/strips and may prepend
"G" to numeric input but allows non-canonical forms and the caller truncates
IDs; update NormalizeGroupId to return an uppercase canonical ID in the form "G"
followed by exactly 4 digits (e.g., use a regex like ^G\d{4}$ after making input
uppercase and removing spaces) and remove the caller-side truncation (the
groupId = normalizedId[..5] logic). Also, only call OnGroupJoined with the
validated canonical ID (from NormalizeGroupId) and handle invalid inputs (e.g.,
show an error or disable the join) instead of invoking OnGroupJoined with
non-canonical values; refer to NormalizeGroupId, OnGroupJoined, and
GroupIdInput.Text to locate edits.

Comment on lines +21 to +22
public Action<string>? CloseChat;
public Action<string>? LeaveChat;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Close chat is a client-only no-op right now.

This handler assumes CloseChat is wired, but Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatUi.cs:18-80 never assigns that delegate. Clicking the button only clears ActiveChatId locally; the server still keeps ActiveChat, so the selection comes back on the next state push. Please either wire a close-chat message or hide/disable the button until that path exists.

Also applies to: 74-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs`
around lines 21 - 22, The CloseChat handler is currently a client-only no-op
because CloseChat is never assigned, so clicking the button only clears
ActiveChatId locally while the server still holds ActiveChat; either wire
CloseChat to send the server-side close message from the NanoChatUi controller
(assign the CloseChat Action in the NanoChatUi class to call the
transport/message method that clears ActiveChat on the server) or disable/hide
the UI button until CloseChat is non-null; also apply the same fix for
LeaveChat/its button so both actions either have server-side wiring or are
disabled when not assigned (refer to CloseChat, LeaveChat, ActiveChatId,
ActiveChat, and the NanoChatUi class to locate where to assign or gate the
button).

Comment on lines +228 to +230
else
{
ChatTitle.Text = Loc.GetString("nano-chat-ui-unknown-chat");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add the missing nano-chat-ui-unknown-chat localization key.

Loc.GetString("nano-chat-ui-unknown-chat") is used here, but neither NanoChat locale file defines it. The raw key/fallback will leak into the UI as soon as an active id no longer resolves.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Wega/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs`
around lines 228 - 230, The UI is calling
Loc.GetString("nano-chat-ui-unknown-chat") in NanoChatUiFragment.xaml.cs
(ChatTitle.Text assignment) but that key is missing from the NanoChat locale
resources; add the missing "nano-chat-ui-unknown-chat" entry to the NanoChat
locale files (for all supported languages), provide appropriate translated
strings (e.g., "Unknown chat" for en), and update/rebuild any localization
resource bundle so Loc.GetString resolves correctly at runtime.

Comment on lines +365 to +443
foreach (var (contactId, messages) in chatComp.Messages)
{
if (contactId.StartsWith("G"))
continue;

if (chatComp.Contacts.TryGetValue(contactId, out var contact))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-with-contact",
("name", contact.ContactName),
("id", contactId)));
}
else
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-with-unknown",
("id", contactId)));
}

text.AppendLine(new string('-', 40));

foreach (var message in messages)
{
var time = $"{(int)message.Timestamp.TotalHours:00}:{message.Timestamp.Minutes:00}";
var sender = message.IsOwnMessage ? Loc.GetString("forensic-scanner-chat-you") : message.SenderName;

var status = message.Delivered ? "✓" : "✗";

text.AppendLine($"[{time}] {sender} ({status}): {message.Message}");
}

text.AppendLine();
}

foreach (var contact in chatComp.Contacts.Values)
{
if (contact.ContactId.StartsWith("G"))
continue;

if (!chatComp.Messages.ContainsKey(contact.ContactId))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-contact-no-messages",
("name", contact.ContactName),
("id", contact.ContactId)));
text.AppendLine();
}
}

if (chatComp.Groups.Count > 0)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-groups-header"));
text.AppendLine(new string('=', 36));

foreach (var (groupId, group) in chatComp.Groups)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-info",
("name", group.GroupName),
("id", groupId),
("members", group.MemberCount)));

if (chatComp.Messages.TryGetValue(groupId, out var groupMessages))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-messages"));
text.AppendLine(new string('-', 30));

foreach (var message in groupMessages)
{
var time = $"{(int)message.Timestamp.TotalHours:00}:{message.Timestamp.Minutes:00}";
var sender = message.IsOwnMessage ?
Loc.GetString("forensic-scanner-chat-you") :
message.SenderName;

var status = message.Delivered ? "✓" : "✗";

text.AppendLine($"[{time}] {sender} ({status}): {message.Message}");
}
}
else
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-no-messages"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Treat empty message lists as "no messages".

The current branches only distinguish missing keys from present keys. If a contact or group already has an empty list entry, the report prints a blank section instead of the localized empty-state line.

🛠️ Suggested fix
 foreach (var (contactId, messages) in chatComp.Messages)
 {
-    if (contactId.StartsWith("G"))
+    if (contactId.StartsWith("G") || messages.Count == 0)
         continue;
@@
-    if (!chatComp.Messages.ContainsKey(contact.ContactId))
+    if (!chatComp.Messages.TryGetValue(contact.ContactId, out var contactMessages) ||
+        contactMessages.Count == 0)
     {
         text.AppendLine(Loc.GetString("forensic-scanner-chat-contact-no-messages",
             ("name", contact.ContactName),
             ("id", contact.ContactId)));
         text.AppendLine();
@@
-    if (chatComp.Messages.TryGetValue(groupId, out var groupMessages))
+    if (chatComp.Messages.TryGetValue(groupId, out var groupMessages) &&
+        groupMessages.Count > 0)
     {
         text.AppendLine(Loc.GetString("forensic-scanner-chat-group-messages"));
         text.AppendLine(new string('-', 30));
📝 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.

Suggested change
foreach (var (contactId, messages) in chatComp.Messages)
{
if (contactId.StartsWith("G"))
continue;
if (chatComp.Contacts.TryGetValue(contactId, out var contact))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-with-contact",
("name", contact.ContactName),
("id", contactId)));
}
else
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-with-unknown",
("id", contactId)));
}
text.AppendLine(new string('-', 40));
foreach (var message in messages)
{
var time = $"{(int)message.Timestamp.TotalHours:00}:{message.Timestamp.Minutes:00}";
var sender = message.IsOwnMessage ? Loc.GetString("forensic-scanner-chat-you") : message.SenderName;
var status = message.Delivered ? "✓" : "✗";
text.AppendLine($"[{time}] {sender} ({status}): {message.Message}");
}
text.AppendLine();
}
foreach (var contact in chatComp.Contacts.Values)
{
if (contact.ContactId.StartsWith("G"))
continue;
if (!chatComp.Messages.ContainsKey(contact.ContactId))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-contact-no-messages",
("name", contact.ContactName),
("id", contact.ContactId)));
text.AppendLine();
}
}
if (chatComp.Groups.Count > 0)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-groups-header"));
text.AppendLine(new string('=', 36));
foreach (var (groupId, group) in chatComp.Groups)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-info",
("name", group.GroupName),
("id", groupId),
("members", group.MemberCount)));
if (chatComp.Messages.TryGetValue(groupId, out var groupMessages))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-messages"));
text.AppendLine(new string('-', 30));
foreach (var message in groupMessages)
{
var time = $"{(int)message.Timestamp.TotalHours:00}:{message.Timestamp.Minutes:00}";
var sender = message.IsOwnMessage ?
Loc.GetString("forensic-scanner-chat-you") :
message.SenderName;
var status = message.Delivered ? "✓" : "✗";
text.AppendLine($"[{time}] {sender} ({status}): {message.Message}");
}
}
else
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-no-messages"));
}
foreach (var (contactId, messages) in chatComp.Messages)
{
if (contactId.StartsWith("G") || messages.Count == 0)
continue;
if (chatComp.Contacts.TryGetValue(contactId, out var contact))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-with-contact",
("name", contact.ContactName),
("id", contactId)));
}
else
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-with-unknown",
("id", contactId)));
}
text.AppendLine(new string('-', 40));
foreach (var message in messages)
{
var time = $"{(int)message.Timestamp.TotalHours:00}:{message.Timestamp.Minutes:00}";
var sender = message.IsOwnMessage ? Loc.GetString("forensic-scanner-chat-you") : message.SenderName;
var status = message.Delivered ? "✓" : "✗";
text.AppendLine($"[{time}] {sender} ({status}): {message.Message}");
}
text.AppendLine();
}
foreach (var contact in chatComp.Contacts.Values)
{
if (contact.ContactId.StartsWith("G"))
continue;
if (!chatComp.Messages.TryGetValue(contact.ContactId, out var contactMessages) ||
contactMessages.Count == 0)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-contact-no-messages",
("name", contact.ContactName),
("id", contact.ContactId)));
text.AppendLine();
}
}
if (chatComp.Groups.Count > 0)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-groups-header"));
text.AppendLine(new string('=', 36));
foreach (var (groupId, group) in chatComp.Groups)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-info",
("name", group.GroupName),
("id", groupId),
("members", group.MemberCount)));
if (chatComp.Messages.TryGetValue(groupId, out var groupMessages) &&
groupMessages.Count > 0)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-messages"));
text.AppendLine(new string('-', 30));
foreach (var message in groupMessages)
{
var time = $"{(int)message.Timestamp.TotalHours:00}:{message.Timestamp.Minutes:00}";
var sender = message.IsOwnMessage ?
Loc.GetString("forensic-scanner-chat-you") :
message.SenderName;
var status = message.Delivered ? "✓" : "✗";
text.AppendLine($"[{time}] {sender} ({status}): {message.Message}");
}
}
else
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-no-messages"));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/Forensics/Systems/ForensicScannerSystem.cs` around lines 365 -
443, The scanner treats keys with empty message lists as "has messages", causing
blank sections; update logic to treat empty lists as "no messages": when
iterating chatComp.Messages skip/handle entries where messages.Count == 0 (so
they print the localized no-messages line instead of a blank block), and when
checking for contacts or groups use TryGetValue(...) and consider the messages
list empty if TryGetValue returns false OR the retrieved list.Count == 0 (update
the contact-check around chatComp.Messages.ContainsKey(contact.ContactId) and
the group branch that uses chatComp.Messages.TryGetValue(groupId, out var
groupMessages) to check groupMessages.Count == 0).

Comment on lines +1 to +27
forensic-scanner-print-chat-history-text = Напечатать историю чатов
forensic-scanner-print-chat-history-message = Распечатать все сообщения чатов из этого КПК

forensic-scanner-chat-history-title = Отчет истории чатов КПК
forensic-scanner-chat-history-header =
ОТЧЕТ ИСТОРИИ ЧАТОВ
Владелец: { $owner }
Время: { $time }
forensic-scanner-chat-history-printed = История чатов распечатана

forensic-scanner-no-cartridge = В КПК картриджи не найдены
forensic-scanner-no-chat-cartridge = В КПК не найдены картриджи чата

forensic-scanner-chat-cartridge-header = === Картридж чата: { $id } ===
forensic-scanner-chat-no-data = Данные чатов отсутствуют

forensic-scanner-chat-with-contact = Чат с: { $name } ({ $id })
forensic-scanner-chat-with-unknown = Чат с: Неизвестный ({ $id })
forensic-scanner-chat-contact-no-messages = Контакт: { $name } ({ $id }) - нет сообщений

forensic-scanner-chat-groups-header = ГРУППОВЫЕ ЧАТЫ
forensic-scanner-chat-group-info = Группа: { $name } (ID: { $id }, Участников: { $members })
forensic-scanner-chat-group-messages = Сообщения группы:
forensic-scanner-chat-group-no-messages = Нет сообщений в группе
forensic-scanner-chat-archived-group = Архивная группа: { $id }

forensic-scanner-chat-you = Вы
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace the Russian copy in the en-US bundle.

This is the English locale file, but every new value is Russian. As-is, English/default-locale users will get Russian popups and printed report text for this feature.

🌐 Suggested English strings
-forensic-scanner-print-chat-history-text = Напечатать историю чатов
-forensic-scanner-print-chat-history-message = Распечатать все сообщения чатов из этого КПК
+forensic-scanner-print-chat-history-text = Print chat history
+forensic-scanner-print-chat-history-message = Print all chat messages from this PDA

-forensic-scanner-chat-history-title = Отчет истории чатов КПК
+forensic-scanner-chat-history-title = PDA chat history report
 forensic-scanner-chat-history-header =
-    ОТЧЕТ ИСТОРИИ ЧАТОВ
-    Владелец: { $owner }
-    Время: { $time }
-forensic-scanner-chat-history-printed = История чатов распечатана
+    CHAT HISTORY REPORT
+    Owner: { $owner }
+    Time: { $time }
+forensic-scanner-chat-history-printed = Chat history printed

-forensic-scanner-no-cartridge = В КПК картриджи не найдены
-forensic-scanner-no-chat-cartridge = В КПК не найдены картриджи чата
+forensic-scanner-no-cartridge = No cartridges found in the PDA
+forensic-scanner-no-chat-cartridge = No chat cartridges found in the PDA

-forensic-scanner-chat-cartridge-header = === Картридж чата: { $id } ===
-forensic-scanner-chat-no-data = Данные чатов отсутствуют
+forensic-scanner-chat-cartridge-header = === Chat cartridge: { $id } ===
+forensic-scanner-chat-no-data = No chat data available

-forensic-scanner-chat-with-contact = Чат с: { $name } ({ $id })
-forensic-scanner-chat-with-unknown = Чат с: Неизвестный ({ $id })
-forensic-scanner-chat-contact-no-messages = Контакт: { $name } ({ $id }) - нет сообщений
+forensic-scanner-chat-with-contact = Chat with: { $name } ({ $id })
+forensic-scanner-chat-with-unknown = Chat with: Unknown ({ $id })
+forensic-scanner-chat-contact-no-messages = Contact: { $name } ({ $id }) - no messages

-forensic-scanner-chat-groups-header = ГРУППОВЫЕ ЧАТЫ
-forensic-scanner-chat-group-info = Группа: { $name } (ID: { $id }, Участников: { $members })
-forensic-scanner-chat-group-messages = Сообщения группы:
-forensic-scanner-chat-group-no-messages = Нет сообщений в группе
-forensic-scanner-chat-archived-group = Архивная группа: { $id }
+forensic-scanner-chat-groups-header = GROUP CHATS
+forensic-scanner-chat-group-info = Group: { $name } (ID: { $id }, Members: { $members })
+forensic-scanner-chat-group-messages = Group messages:
+forensic-scanner-chat-group-no-messages = No messages in group
+forensic-scanner-chat-archived-group = Archived group: { $id }

-forensic-scanner-chat-you = Вы
+forensic-scanner-chat-you = You
📝 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.

Suggested change
forensic-scanner-print-chat-history-text = Напечатать историю чатов
forensic-scanner-print-chat-history-message = Распечатать все сообщения чатов из этого КПК
forensic-scanner-chat-history-title = Отчет истории чатов КПК
forensic-scanner-chat-history-header =
ОТЧЕТ ИСТОРИИ ЧАТОВ
Владелец: { $owner }
Время: { $time }
forensic-scanner-chat-history-printed = История чатов распечатана
forensic-scanner-no-cartridge = В КПК картриджи не найдены
forensic-scanner-no-chat-cartridge = В КПК не найдены картриджи чата
forensic-scanner-chat-cartridge-header = === Картридж чата: { $id } ===
forensic-scanner-chat-no-data = Данные чатов отсутствуют
forensic-scanner-chat-with-contact = Чат с: { $name } ({ $id })
forensic-scanner-chat-with-unknown = Чат с: Неизвестный ({ $id })
forensic-scanner-chat-contact-no-messages = Контакт: { $name } ({ $id }) - нет сообщений
forensic-scanner-chat-groups-header = ГРУППОВЫЕ ЧАТЫ
forensic-scanner-chat-group-info = Группа: { $name } (ID: { $id }, Участников: { $members })
forensic-scanner-chat-group-messages = Сообщения группы:
forensic-scanner-chat-group-no-messages = Нет сообщений в группе
forensic-scanner-chat-archived-group = Архивная группа: { $id }
forensic-scanner-chat-you = Вы
forensic-scanner-print-chat-history-text = Print chat history
forensic-scanner-print-chat-history-message = Print all chat messages from this PDA
forensic-scanner-chat-history-title = PDA chat history report
forensic-scanner-chat-history-header =
CHAT HISTORY REPORT
Owner: { $owner }
Time: { $time }
forensic-scanner-chat-history-printed = Chat history printed
forensic-scanner-no-cartridge = No cartridges found in the PDA
forensic-scanner-no-chat-cartridge = No chat cartridges found in the PDA
forensic-scanner-chat-cartridge-header = === Chat cartridge: { $id } ===
forensic-scanner-chat-no-data = No chat data available
forensic-scanner-chat-with-contact = Chat with: { $name } ({ $id })
forensic-scanner-chat-with-unknown = Chat with: Unknown ({ $id })
forensic-scanner-chat-contact-no-messages = Contact: { $name } ({ $id }) - no messages
forensic-scanner-chat-groups-header = GROUP CHATS
forensic-scanner-chat-group-info = Group: { $name } (ID: { $id }, Members: { $members })
forensic-scanner-chat-group-messages = Group messages:
forensic-scanner-chat-group-no-messages = No messages in group
forensic-scanner-chat-archived-group = Archived group: { $id }
forensic-scanner-chat-you = You
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Locale/en-US/_Wega/forensics/forensics.ftl` around lines 1 - 27,
The en-US locale file contains Russian strings for multiple keys (e.g.,
forensic-scanner-print-chat-history-text,
forensic-scanner-print-chat-history-message,
forensic-scanner-chat-history-title, forensic-scanner-chat-history-header,
forensic-scanner-chat-history-printed, forensic-scanner-no-cartridge,
forensic-scanner-no-chat-cartridge, forensic-scanner-chat-cartridge-header,
forensic-scanner-chat-no-data, forensic-scanner-chat-with-contact,
forensic-scanner-chat-with-unknown, forensic-scanner-chat-contact-no-messages,
forensic-scanner-chat-groups-header, forensic-scanner-chat-group-info,
forensic-scanner-chat-group-messages, forensic-scanner-chat-group-no-messages,
forensic-scanner-chat-archived-group, forensic-scanner-chat-you); replace each
Russian value with the correct English copy (or copy from the default/English
source) so en-US users see English UI and report text, preserving the same
message keys and interpolation placeholders like { $owner }, { $time }, { $id },
{ $name }, and { $members } exactly.

Comment on lines +2 to +60
nano-chat-ui-add = Добавить
nano-chat-ui-add-contact-title = Добавить контакт
nano-chat-ui-add-contact-tooltip = Добавить контакт

nano-chat-ui-cancel = Отмена

nano-chat-ui-close-chat-tooltip = Закрыть чат

nano-chat-ui-contact-id = ID контакта
nano-chat-ui-contact-id-placeholder = Введите ID контакта(5 символов)...
nano-chat-ui-contact-name = Имя контакта
nano-chat-ui-contact-name-placeholder = Введите имя контакта(9 символов)...

nano-chat-ui-create = Создать
nano-chat-ui-create-group-title = Создать группу
nano-chat-ui-create-group-tooltip = Создать новую группу

nano-chat-ui-emoji-select = Выберите эмодзи:
nano-chat-ui-emoji-title = Выбор эмодзи
nano-chat-ui-emoji-tooltip = Эмодзи

nano-chat-ui-erase-chat-tooltip = Стереть контакт

nano-chat-ui-group-id = ID группы
nano-chat-ui-group-id-placeholder = Введите ID группы(5 символов)...
nano-chat-ui-group-name = Название группы
nano-chat-ui-group-name-placeholder = Введите название группы(16 символов)...

nano-chat-ui-join = Присоединиться
nano-chat-ui-join-group-title = Присоединиться к группе
nano-chat-ui-join-group-tooltip = Присоединиться к группе

nano-chat-ui-leave-chat-tooltip = Покинуть группу

nano-chat-ui-message-placeholder = Введите сообщение...

nano-chat-ui-mute-tooltip = Отключить звук

nano-chat-ui-no-contacts = Нет контактов
nano-chat-ui-no-groups = Нет групп

nano-chat-ui-select-chat = Выберите чат
nano-chat-ui-select-chat-input = Выберите чат для сообщения

nano-chat-ui-send-tooltip = Отправить

nano-chat-ui-tab-contacts = Конт.
nano-chat-ui-tab-groups = Групп.

nano-chat-ui-title = NanoChat

nano-chat-ui-unmute-tooltip = Включить звук

nano-chat-ui-unread-indicator = (У)

# System
nano-chat-create-group-message = {$name} создал группу '{$groupName}'.
nano-chat-join-message = -> {$name} присоединился!
nano-chat-leave-message = <- {$name} покинул группу.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Populate the en-US locale with English strings.

This file lives under en-US, but every added value is still Russian. English clients will ship the feature with untranslated UI because this file already claims to satisfy the locale.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Locale/en-US/_Wega/nanochat/nanochatui.ftl` around lines 2 - 60,
This file under en-US contains Russian strings—replace each Russian value with
its English translation so the en-US locale actually provides English text;
update all keys such as nano-chat-ui-add, nano-chat-ui-add-contact-title,
nano-chat-ui-contact-id-placeholder, nano-chat-ui-contact-name-placeholder,
nano-chat-ui-create-group-title, nano-chat-ui-emoji-title,
nano-chat-ui-group-name-placeholder, nano-chat-ui-join-group-title,
nano-chat-ui-message-placeholder, nano-chat-ui-tab-contacts, nano-chat-ui-title,
nano-chat-ui-unread-indicator and the system messages
nano-chat-create-group-message, nano-chat-join-message, nano-chat-leave-message
to appropriate English phrases (including translating placeholders and
preserving interpolation variables like {$name} and {$groupName}). Ensure
punctuation and placeholder formats are kept intact.

@@ -0,0 +1 @@
nano-chat-program-name = НаноЧат
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check directory naming patterns in locale folders
fd -t d -i 'wega' Resources/Locale/

Repository: corvax-team/ss14-wl

Length of output: 220


🏁 Script executed:

#!/bin/bash

# Check contents of each directory
echo "=== en-US/_Wega/ ==="
ls -la Resources/Locale/en-US/_Wega/ 2>/dev/null | head -20

echo -e "\n=== ru-RU/_wega/ ==="
ls -la Resources/Locale/ru-RU/_wega/ 2>/dev/null | head -20

echo -e "\n=== en-US/ss14-ru/prototypes/_wega/ ==="
ls -la Resources/Locale/en-US/ss14-ru/prototypes/_wega/ 2>/dev/null | head -20

echo -e "\n=== ru-RU/ss14-ru/prototypes/_wega/ ==="
ls -la Resources/Locale/ru-RU/ss14-ru/prototypes/_wega/ 2>/dev/null | head -20

# Check if there are any case-sensitive references in code
echo -e "\n=== Checking for hardcoded path references ==="
rg '_Wega|_wega' --type yaml --type xml --type json Resources/ | grep -v "\.ftl" | head -20

Repository: corvax-team/ss14-wl

Length of output: 1301


🏁 Script executed:

#!/bin/bash

# Search for references to wega paths in code files (not just prototypes)
echo "=== References to _Wega or _wega in code ==="
rg -i 'wega' --type yaml --type rust --type csharp --type json -B 2 -A 2 | head -60

# Check if there's any build or loading logic that references these paths
echo -e "\n=== Checking for directory/path loading references ==="
rg 'Locale|locale' --type rust --type csharp -i | grep -i 'path\|load\|directory' | head -20

# Check the actual file in question for context
echo -e "\n=== Content of cartridges.ftl files ==="
find Resources/Locale -name "cartridges.ftl" -exec echo "=== {} ===" \; -exec cat {} \;

Repository: corvax-team/ss14-wl

Length of output: 18399


Align directory naming conventions across locales.

The Russian locale uses lowercase _wega (ru-RU/_wega/) while the English locale uses _Wega (en-US/_Wega/) at the top level. This inconsistency can cause issues on case-sensitive filesystems (Linux) and makes the codebase harder to maintain. Ensure both locales use the same casing convention.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Locale/ru-RU/_wega/cartridge-loader/cartridges.ftl` at line 1, The
Russian locale directory uses `_wega` while English uses `_Wega`; rename the
Russian directory to match the established casing (change `_wega` -> `_Wega`)
and update all references; specifically move/rename the directory containing
cartridges.ftl and update any config, import, or build references that point to
`_wega` so all locales use the same `_Wega` casing (use git mv to preserve
history and run tests/build to verify).

Comment on lines +1 to +2
sample:
filter: true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect filename — appears to be an accidental copy.

The filename sound.svg.192dpi.png copy.yml contains " copy" which suggests this file was accidentally created when duplicating another file. The correct filename should be sound.svg.192dpi.png.yml (without " copy") to properly associate with the corresponding PNG texture.

Please either remove this file if it's a duplicate, or rename it to match the intended texture asset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Textures/_Wega/Interface/sound.svg.192dpi.png` copy.yml around
lines 1 - 2, The file named "sound.svg.192dpi.png copy.yml" appears to be an
accidental duplicate; either delete this file if it's redundant or rename it to
"sound.svg.192dpi.png.yml" so it matches the intended texture asset, ensuring
the YAML contents remain unchanged and any references to the texture in the
project point to the corrected filename.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Resources/Prototypes/InventoryTemplates/aghost_inventory_template.yml`:
- Line 53: The uiWindowPos for pocket1 conflicts with the eyes slot (both set to
0,3); update the pocket1 entry's uiWindowPos value to a non-overlapping
coordinate (e.g., shift row or column such as 0,4 or 1,3) so it no longer shares
the same position as the eyes slot; locate the pocket1 definition and change its
uiWindowPos accordingly while keeping the eyes uiWindowPos unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec1a08d6-96b8-46c4-b3c0-192a852b29b3

📥 Commits

Reviewing files that changed from the base of the PR and between e7f5555 and 63c4b8e.

📒 Files selected for processing (1)
  • Resources/Prototypes/InventoryTemplates/aghost_inventory_template.yml

@tau27 tau27 closed this Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants