Skip to content

Chemestry dispenser rework#21

Open
CREAsTIVE wants to merge 12 commits intoTheMaidDev:mainfrom
CREAsTIVE:chemestry-dispenser-rework
Open

Chemestry dispenser rework#21
CREAsTIVE wants to merge 12 commits intoTheMaidDev:mainfrom
CREAsTIVE:chemestry-dispenser-rework

Conversation

@CREAsTIVE
Copy link
Copy Markdown

@CREAsTIVE CREAsTIVE commented Feb 2, 2026

About the PR

Откат раздатчика химикатов (до этого обновления)

Changelog
🆑 CREAsTIVE

  • add: UI лайаут переделан для соответствия со старым лайаутом (см. ниже)
  • remove: Убран контейнер из раздатчика химикатов (теперь раздатчик может предоставить бесконечное количество любого типа химикатов)
  • add: Добавлен тип пака химикатов (набор химикатов, которые может раздовать раздатчик)
  • tweak: Имеется совместимость со старой версией (с конечными химикатами), если раздатчик имеет StorageFill компонент, то типы химиката будут спаршены из данного контейнера (не уверен, что это необходимо, но оно уже есть, см. Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs L137)
  • add: Добавлена поддержка "заряда" для раздатчика химиката (через компонент LimitedCharges)

Новый UI:
image

Summary by CodeRabbit

  • Refactor

    • Redesigned dispenser window to a cleaner, grid-based layout with narrower default width and revamped reagent cards (horizontal layout, centered names, visible cost; eject/color controls removed).
    • Container info, sprite view, and action bar reorganized for clearer info and controls.
  • New Features

    • Dispensers now use predefined inventory packs and show remaining charges.
    • Added limited charges with auto-recharge and a localized label for charge amount.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces storage/jug-based dispenser UI and messages with prototype-driven reagent inventories and ReagentId-based actions. Server builds inventory from new inventory prototypes, integrates charges, and dispenses by ReagentId; client UIs and shared messages/state updated accordingly; storage/eject flows removed.

Changes

Cohort / File(s) Summary
Client: Reagent Card
Content.Client/Chemistry/UI/ReagentCardControl.xaml, Content.Client/Chemistry/UI/ReagentCardControl.xaml.cs
Removed color panel and eject button; switched inner layout to horizontal; control now constructed with ReagentId, optional ReagentPrototype, and int cost. Exposes ReagentId and Action<ReagentId>? OnPressed; shows reagent cost.
Client: Dispenser UI & Binding
Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml, Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml.cs, Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs
Window layout reworked (adds public GridContainer ReagentList, updates SpriteView View, info panel). Bound UI events and APIs changed: dispense binding uses ReagentId, UpdateReagentsList now accepts List<(ReagentId,int)> plus amount; eject-jug event removed; charge amount display added.
Shared: Protocols & State
Content.Shared/Chemistry/SharedReagentDispenser.cs
Switched messages/BUI state from storage/item types to ReagentId and List<(ReagentId reagent, int cost)>. Removed ReagentInventoryItem and ReagentDispenserEjectContainerMessage. Added optional Charges to BUI state.
Server: Component
Content.Server/Chemistry/Components/ReagentDispenserComponent.cs
Added DefaultReagentCost, PackPrototypeId, and EmagPackPrototypeId fields (DataField/serializers, ViewVariables).
Server: System
Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs
Adds periodic UI refresh, integrates ChargesSystem, builds inventory from ReagentDispenserInventoryPrototype + pack prototypes and legacy storage fills, computes costs/amounts, and dispenses by ReagentId. Refactors inventory/state signatures and removes eject-storage handling.
Shared: Inventory Prototype
Content.Shared/Chemistry/Dispenser/ReagentDispenserInventoryPrototype.cs
New reagentDispenserInventory prototype mapping reagent IDs to quantities for dispenser inventories.
Prototypes / Catalog
Resources/Prototypes/Catalog/reagent_dispenser.yml
Adds multiple reagentDispenserInventory entries (chem, soda, booze, emag variants) defining dispenser inventories.
Prototype Entities: Dispensers
Resources/Prototypes/Entities/Structures/Dispensers/* (base_structuredispensers.yml, booze.yml, chem.yml, soda.yml)
Removed Storage/StorageFill and Storage UI bindings; added ReagentDispenser component referencing pack/emagPack, plus LimitedCharges, AutoRecharge, and related component updates to dispenser entities (Maid comment blocks present).
Localization
Resources/Locale/*/chemistry/components/reagent-dispenser-component.ftl
Added reagent-dispenser-window-charge-amount localization entries (en-US, ru-RU).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client as "Player Client"
  participant UI as "Dispenser UI (Client)"
  participant BUI as "Bound UI (Client→Server)"
  participant Server as "ReagentDispenserSystem (Server)"
  participant Prototype as "PrototypeManager"
  participant Charges as "ChargesSystem"
  participant Solution as "SolutionContainerSystem"

  Client->>UI: Click Reagent Card (ReagentId)
  UI->>BUI: Send ReagentDispenserDispenseReagentMessage(ReagentId)
  BUI->>Server: Forward message
  Server->>Prototype: Resolve ReagentId → ReagentPrototype/base cost
  Server->>Charges: Check/consume charges (affect amount/cost)
  Server->>Solution: Transfer computed reagent amount to container
  Solution-->>Server: Transfer result
  Server-->>BUI: Push updated UI state (inventory, charges)
  BUI-->>UI: Update reagent list and charge display
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

S: Untriaged

Suggested reviewers

  • RedBurningPhoenix

Poem

🐰 I hopped through packs and prototype trees,
Jugs shed their shells on a tinkling breeze.
IDs now scurry where bottles once stood,
Grids and charges hum — dispensing's understood.
Hop, click, and carrots for code well-brewed! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Chemestry dispenser rework" is directly related to the main changes in the PR, which involves a comprehensive rework of the chemistry/reagent dispenser system across UI, components, and configuration files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Resources/Prototypes/Entities/Structures/Dispensers/base_structuredispensers.yml (1)

81-82: ⚠️ Potential issue | 🟡 Minor

Remove the orphaned StorageUiKey interface definition.

The StorageBoundUserInterface for enum.StorageUiKey.Key (lines 81-82) is unused. The Storage component was removed from the base entity and no child dispenser entities add it back, making this interface dead configuration. Remove it to clean up the configuration.

Content.Shared/Chemistry/SharedReagentDispenser.cs (1)

111-118: ⚠️ Potential issue | 🟡 Minor

Remove unused ReagentInventoryItem class.

The class is not referenced anywhere in the codebase. Delete it from lines 111-118.

🤖 Fix all issues with AI agents
In `@Content.Client/Chemistry/UI/ReagentCardControl.xaml.cs`:
- Line 8: Remove the now-unused using directive "using Content.Shared.Storage;"
from ReagentCardControl.xaml.cs; the refactor replaced types from that namespace
with ReagentId, so delete the import to avoid an unused-usings warning and keep
the file clean.

In `@Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml`:
- Around line 68-69: The SpriteView with Name="View" is missing Access="Public"
but is referenced from code-behind (e.g., View.SetEntity()), so add
Access="Public" to the SpriteView declaration to match other named controls
(AmountGrid, ReagentList, ClearButton, EjectButton, ContainerInfoName,
ContainerInfoFill) and ensure it is accessible from the code-behind.

In `@Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs`:
- Around line 218-219: The code throws a bare Exception on a null `solution` (in
ReagentDispenserSystem) which is unhelpful; replace the null-check expression in
the call to _solutionContainerSystem.TryAddReagent so it either uses the
null-forgiving operator (solution!) since TryGetFitsInDispenser is annotated
[NotNullWhen(true]), or, if you want runtime safety, throw a more specific
exception with a clear message (e.g., InvalidOperationException with context)
before calling TryAddReagent; update the call site that currently reads
"solution ?? throw new()" to use "solution!" (or throw new
InvalidOperationException(\"Reagent dispenser solution was null despite
TryGetFitsInDispenser returning true\") if you prefer explicit error).

In `@Content.Shared/Chemistry/Dispenser/ReagentDispenserInventoryPrototype.cs`:
- Around line 13-17: The XML doc comment for ReagentDispenserInventoryPrototype
contains a typo ("despenser") — update the summary text in the
ReagentDispenserInventoryPrototype XML documentation to replace "despenser" with
"dispenser" so the comment reads "reagent dispenser <c>pack</c> value" (locate
the summary block in ReagentDispenserInventoryPrototype and fix the word).
🧹 Nitpick comments (9)
Content.Server/Chemistry/Components/ReagentDispenserComponent.cs (1)

83-89: Remove redundant default! initialization on nullable fields.

Both PackPrototypeId and EmagPackPrototypeId are declared as string? (nullable), yet they're initialized with = default!. The null-forgiving operator (!) is misleading here since the fields can legitimately be null (as indicated by the ?). For nullable reference types, omitting the initializer or using = null is clearer.

♻️ Suggested simplification
 [DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentDispenserInventoryPrototype>))]
 [ViewVariables(VVAccess.ReadWrite)]
-public string? PackPrototypeId = default!;
+public string? PackPrototypeId;

 [DataField("emagPack", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentDispenserInventoryPrototype>))]
 [ViewVariables(VVAccess.ReadWrite)]
-public string? EmagPackPrototypeId = default!;
+public string? EmagPackPrototypeId;
Content.Shared/Chemistry/Dispenser/ReagentDispenserInventoryPrototype.cs (2)

5-9: Remove unused using directives.

The imports System, System.Linq, System.Text, and System.Threading.Tasks appear unused in this file.

♻️ Suggested cleanup
 using Content.Shared.Chemistry.Reagent;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-using System;
 using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;

26-28: Acknowledge TODO: Consider using ReagentId instead of List<string>.

The TODO comment indicates intent to use ReagentId for type safety. This would provide better compile-time validation and consistency with other parts of the codebase that now use ReagentId.

Would you like me to open an issue to track this improvement, or would you prefer to address it in this PR?

Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml.cs (2)

82-88: Sorting with null prototypes may produce inconsistent results.

When x.prototype is null, the CompareTo call returns 0 regardless of y, causing items with missing prototypes to be interleaved inconsistently rather than grouped together.

♻️ Proposed fix for stable null handling in sort
-            // Sort reagents by name
-            reagentPrototypes.Sort((x, y) => x.prototype?.LocalizedName?.CompareTo(y.prototype?.LocalizedName ?? untranslatedText) ?? 0);
+            // Sort reagents by name, placing missing prototypes at the end
+            reagentPrototypes.Sort((x, y) =>
+                string.Compare(
+                    x.prototype?.LocalizedName ?? untranslatedText,
+                    y.prototype?.LocalizedName ?? untranslatedText,
+                    StringComparison.CurrentCulture));

85-85: Minor typo in comment.

"not that much reagents" should be "not that many reagents".

Resources/Prototypes/Catalog/reagent_dispenser.yml (1)

60-65: Inconsistent YAML indentation for inventory items.

SodaDispenserEmagInventory and BoozeDispenserEmagInventory use 4-space indentation for inventory items (lines 63-65, 91-93), while other inventories use 2-space indentation. While YAML parsers will accept both, consistent formatting improves readability.

♻️ Proposed fix for consistent indentation
 - type: reagentDispenserInventory
   id: SodaDispenserEmagInventory
   inventory:
-    - FourteenLoko
-    - Ephedrine
-    - Histamine
+  - FourteenLoko
+  - Ephedrine
+  - Histamine
 - type: reagentDispenserInventory
   id: BoozeDispenserEmagInventory
   inventory:
-    - AtomicBomb
-    - Ethanol
-    - Iron
+  - AtomicBomb
+  - Ethanol
+  - Iron

Also applies to: 88-93

Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs (2)

167-167: Typo in comment: "Finded" should be "Found".


137-173: Use the deserialized component object instead of manual YAML traversal.

This code manually navigates prototype YAML nodes to extract reagent IDs. A more robust approach exists in the codebase: deserialize the SolutionContainerManager component data and use the system's EnumerateSolutions() method, as demonstrated in PricingSystem.cs. The current manual traversal hardcodes the YAML structure ("solutions" → "reagents" → "ReagentId") and will silently fail if the schema changes. Consider casting data.Component to SolutionContainerManagerComponent and leveraging _solutionContainerSystem.EnumerateSolutions() for type-safe iteration instead.

Content.Shared/Chemistry/SharedReagentDispenser.cs (1)

86-88: Parameter name storageLocation is misleading for a ReagentId type.

The parameter and assignment retain the old naming from when this held an ItemStorageLocation. Rename for clarity.

♻️ Proposed fix
-        public ReagentDispenserDispenseReagentMessage(ReagentId storageLocation)
+        public ReagentDispenserDispenseReagentMessage(ReagentId reagentId)
         {
-            ReagentId = storageLocation;
+            ReagentId = reagentId;
         }

@CREAsTIVE
Copy link
Copy Markdown
Author

Changelog

  • Добавлена поддержка "заряда" для раздатчика химиката

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: 4

🤖 Fix all issues with AI agents
In `@Content.Client/Chemistry/UI/ReagentCardControl.xaml.cs`:
- Around line 19-31: The constructor of ReagentCardControl fails to assign the
incoming cost to the Cost property so internal state remains stale; inside the
ReagentCardControl(ReagentId reagentId, ReagentPrototype? prototype, int cost)
constructor assign the passed-in cost to the Cost property (e.g., set Cost =
cost) after setting ReagentId so any code that reads Cost later sees the correct
value.

In `@Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs`:
- Around line 169-237: GetInventory is using Dictionary.Add for multiple
inventory sources which throws on duplicate reagent keys; change all
inventory.Add(...) calls (inside the SolutionContainerManager loop and when
merging packPrototype.Inventory and emagPackPrototype.Inventory) to a
deterministic upsert (overwrite) pattern so later sources replace earlier ones —
e.g., replace Add with assignment/overwriting (inventory[reagentKey] = value) or
a TryAdd-if-not-present depending on desired precedence; ensure this change is
applied for the reagentId.Value insertion in the SolutionContainerManager block
and for the loops that iterate packPrototype.Inventory and
emagPackPrototype.Inventory so duplicates no longer throw.
- Around line 246-287: The calculation limiting totalDispenseAmount uses integer
division (avalaibleCharges / singleCost) which underestimates available partial
dispenses when singleCost > 1; in OnDispenseReagentMessage change that division
to float division (e.g., cast avalaibleCharges or singleCost to float) so
totalDispenseAmount = avalaibleCharges / (float) singleCost (or equivalent) to
allow fractional dispensing; keep the rest of the charge handling logic the same
and ensure the Ceil/charge deduction still rounds up correctly.

In `@Resources/Prototypes/Entities/Structures/Dispensers/chem.yml`:
- Around line 57-61: The YAML contains a dangling comment stating "Doesn't work"
for the LimitedCharges field lastCharges — either remove the non-functional
field or document and track the bug: confirm whether the LimitedCharges
component supports lastCharges (search for LimitedCharges implementation/usage),
if unsupported remove lastCharges from the chem.yml entry and rely on maxCharges
+ AutoRecharge (rechargeDuration), otherwise add a clear FIXME and create a
tracking issue that documents expected vs actual behavior and include
reproduction steps; update the YAML comment to reference the issue ID if leaving
a temporary note.
🧹 Nitpick comments (4)
Content.Shared/Chemistry/SharedReagentDispenser.cs (1)

20-20: Unused import.

Robust.Shared.Toolshed.TypeParsers.Math does not appear to be used in this file. Consider removing it.

🧹 Proposed fix
-using Robust.Shared.Toolshed.TypeParsers.Math;
Content.Server/Chemistry/Components/ReagentDispenserComponent.cs (1)

89-95: Inconsistent nullability: default! on nullable fields.

PackPrototypeId and EmagPackPrototypeId are declared as string? (nullable) but initialized with default!, which suppresses the nullable warning. This is misleading—if these fields are meant to be optional, initialize with = null instead. If they are required, remove the ?.

♻️ Proposed fix
         [DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentDispenserInventoryPrototype>))]
         [ViewVariables(VVAccess.ReadWrite)]
-        public string? PackPrototypeId = default!;
+        public string? PackPrototypeId;

         [DataField("emagPack", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentDispenserInventoryPrototype>))]
         [ViewVariables(VVAccess.ReadWrite)]
-        public string? EmagPackPrototypeId = default!;
+        public string? EmagPackPrototypeId;
Content.Shared/Chemistry/Dispenser/ReagentDispenserInventoryPrototype.cs (1)

5-9: Remove unused System.* imports.

The following imports are not used in this file: System, System.Text, System.Threading.Tasks, and System.Linq.

🧹 Proposed fix
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Collections.Generic;
Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml.cs (1)

91-92: Sorting comparison may not handle null names consistently.

When x.prototype or y.prototype is null, the entire expression evaluates to 0, meaning items with null prototypes won't sort deterministically relative to each other or to valid items. Consider using a more explicit comparison:

♻️ Proposed fix for more predictable sorting
             // Sort reagents by name
-            reagentPrototypes.Sort((x, y) => x.prototype?.LocalizedName?.CompareTo(y.prototype?.LocalizedName ?? untranslatedText) ?? 0);
+            reagentPrototypes.Sort((x, y) =>
+                string.Compare(
+                    x.prototype?.LocalizedName ?? untranslatedText,
+                    y.prototype?.LocalizedName ?? untranslatedText,
+                    StringComparison.CurrentCulture));

@RedBurningPhoenix
Copy link
Copy Markdown
Collaborator

Хсквиз далее подгонит UI.
Пока на паузе.

@CREAsTIVE
Copy link
Copy Markdown
Author

CREAsTIVE commented Feb 10, 2026

WIP, добавлю комментарии MAID (и пофикшу другие контрибут гайдлайны) на все изменения чуть позже

@CREAsTIVE
Copy link
Copy Markdown
Author

Добавил ко всем изменениям метку Maid

@CREAsTIVE
Copy link
Copy Markdown
Author

Похоже у LimitedCharges компонента нельзя указать начальное значение, поэтому lastCharges не учитывается. В прочем наверное не имеет значение

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: 5

🤖 Fix all issues with AI agents
In `@Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml.cs`:
- Line 47: The Maid START/END markers currently wrap the entire namespace/class;
remove the `// Maid START PR №21` and `// Maid END` that enclose the whole file
and instead place them only around the actual modifications: the event
declaration you changed, the UpdateReagentsList method, and the charge display
logic inside the ReagentDispenserWindow class. Ensure the markers use the exact
comments `// Maid START PR №21` and `// Maid END`, are balanced, and only
surround the minimal changed blocks (event declaration, method
UpdateReagentsList, and the charge display code) so unchanged code is not
enclosed.
- Around line 83-100: The current Sort comparator on reagentPrototypes can
return 0 when x.prototype is null, making the sort unstable; update the
comparator used on reagentPrototypes so it compares x.prototype.LocalizedName
and y.prototype.LocalizedName when both prototypes exist, and when one or both
prototypes are null fall back to comparing the reagent prototype IDs (use the
reagent.Prototype string from the tuple) or another stable reagent identifier
before returning 0; modify the Sort lambda that references
ReagentPrototype.LocalizedName to implement this three-way fallback (localized
name -> prototype id -> final tie-breaker such as reagent.Prototype.CompareTo)
so unknown prototypes sort deterministically.

In `@Resources/Prototypes/Entities/Structures/Dispensers/booze.yml`:
- Around line 159-166: The YAML uses lastCharges: 0 which is treated as
"uninitialized" by the OnChargesMapInit logic in SharedChargesSystem.cs (it
treats LastCharges == 0 as unset and resets to MaxCharges), so change the YAML
entry for the Booze dispenser (the LimitedCharges block with lastCharges) to use
lastCharges: -1 to produce an empty spawn (the map-init will clamp -1 to 0);
reference OnChargesMapInit / LastCharges / MaxCharges when locating the logic if
you instead prefer to adjust code behavior or add a clarifying comment.

In `@Resources/Prototypes/Entities/Structures/Dispensers/soda.yml`:
- Around line 164-168: The YAML sets lastCharges: 0 which is ineffective because
SharedChargesSystem.OnChargesMapInit treats 0 as a sentinel and replaces
ent.Comp.LastCharges with MaxCharges; update the Dispensers entries (soda.yml,
and the similar chem.yml and booze.yml instances) to use lastCharges: -1 if you
want an empty start (the system clamps -1 to 0), or remove the lastCharges field
entirely; reference SharedChargesSystem.OnChargesMapInit, ent.Comp.LastCharges,
and MaxCharges when making the change so you alter the correct YAML blocks.
🧹 Nitpick comments (9)
Content.Shared/Chemistry/Dispenser/ReagentDispenserInventoryPrototype.cs (1)

1-31: Unused using directives and leftover TODO.

Lines 5–11 include several using directives (System.Text, System.Threading.Tasks, System.Linq, and the Prototype.List serializer) that don't appear to be referenced anywhere in this file. While the TODO on Line 28 (// TODO use ReagentId) is a valid improvement note, consider tracking it in an issue so it doesn't get lost.

Would you like me to open an issue to track the ReagentId migration for the inventory dictionary?

Resources/Prototypes/Entities/Structures/Dispensers/base_structuredispensers.yml (1)

81-82: Prefer removing dead code over commenting it out.

Multiple blocks of YAML are commented out with # Maid PR №21 markers. Version control already preserves history — leaving large commented-out blocks adds noise for future maintainers. Consider deleting them and referencing the PR number in a single short comment or commit message instead.

Also applies to: 105-110, 124-124

Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs (1)

71-77: Remove commented-out old code.

Same pattern as other files — the old bindings on lines 72–74 should be deleted rather than kept as block comments. The PR reference is sufficient context.

Content.Server/Chemistry/Components/ReagentDispenserComponent.cs (1)

91-97: default! on nullable fields is misleading.

PackPrototypeId and EmagPackPrototypeId are declared string? (nullable), yet initialized with default! (null-forgiving). This suppresses nullability warnings on a type that already permits null, which is confusing. Use = null; or omit the initializer entirely.

📝 Proposed fix
         [DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentDispenserInventoryPrototype>))]
         [ViewVariables(VVAccess.ReadWrite)]
-        public string? PackPrototypeId = default!;
+        public string? PackPrototypeId;

         [DataField("emagPack", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentDispenserInventoryPrototype>))]
         [ViewVariables(VVAccess.ReadWrite)]
-        public string? EmagPackPrototypeId = default!;
+        public string? EmagPackPrototypeId;
Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs (4)

100-116: Update loop placement and unused component variable.

The _updateTimer field is declared between methods rather than with other fields at the top of the class. Also, uiComp and charges from the query enumerator on line 109 are never read — they only serve as query filters. This is fine functionally but worth noting for clarity.


274-277: Adding float.Epsilon before int cast can over-count charges.

Line 276: (int)(MathF.Ceiling((float)dispensed * singleCost) + float.Epsilon) — if Ceiling returns an exact integer value (e.g., 3.0f), adding float.Epsilon then truncating with (int) is harmless in practice because float.Epsilon is ~1.4e-45, far too small to push past the next integer. However, the intent is unclear and the comment "should be safe" suggests uncertainty. Consider using (int)MathF.Ceiling(dispensed * singleCost) directly — Ceiling already rounds up, so the epsilon guard is unnecessary.

Proposed simplification
-                _chargesSystem.AddCharges(reagentDispenser.Owner, -((int) (MathF.Ceiling((float) dispensed * singleCost) + float.Epsilon))); // Ceil dispensed amount up (should be safe)
+                _chargesSystem.AddCharges(reagentDispenser.Owner, -(int)MathF.Ceiling((float)dispensed * singleCost));

163-201: Fragile raw YAML node traversal for backward compatibility.

The GetInventory method manually walks MappingDataNode/SequenceDataNode trees to extract reagent IDs from StorageFill entity prototypes. This is brittle — any structural change to SolutionContainerManager serialization will silently break it without compile-time errors.

A few minor items in this block:

  • Line 197: // Finded!// Found! (typo)
  • Line 258: "less then required""less than required"
  • Line 264: avalaibleChargesavailableCharges

Given that the PR comments mention this is WIP and will be cleaned up later, flagging for awareness.


240-253: TryFirstOrNull with LINQ on IEnumerable re-enumerates GetInventory.

GetInventory returns IEnumerable (via inventory.Select(...)) built on top of Dictionary. On line 250, GetInventory is called and the result is iterated with TryFirstOrNull. On line 128, GetInventory is called again and .ToList() is used. In OnDispenseReagentMessage, the full dictionary is built just to find one element. Consider caching or passing the inventory if this becomes a hot path.

Resources/Prototypes/Entities/Structures/Dispensers/booze.yml (1)

114-142: Commented-out code blocks left in place.

Large blocks of commented-out YAML (lines 115–133, 160–163) are retained. Per the PR comments, this is WIP and will be cleaned up. Ensure these are removed before merging to keep prototypes clean.

using System.Linq;
using static Robust.Client.UserInterface.Controls.BoxContainer;

// Maid START PR №21
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

Maid START/END markers wrap the entire namespace — should only wrap changed sections.

The // Maid START PR №21 on Line 47 and // Maid END on Line 179 enclose the entire class, including large sections of unchanged code. This makes it impossible to distinguish what was actually modified. Move the markers to only wrap the specific changed segments (e.g., the event declaration, UpdateReagentsList, and the charge display logic).

Also applies to: 177-179

🤖 Prompt for AI Agents
In `@Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml.cs` at line 47, The
Maid START/END markers currently wrap the entire namespace/class; remove the `//
Maid START PR №21` and `// Maid END` that enclose the whole file and instead
place them only around the actual modifications: the event declaration you
changed, the UpdateReagentsList method, and the charge display logic inside the
ReagentDispenserWindow class. Ensure the markers use the exact comments `// Maid
START PR №21` and `// Maid END`, are balanced, and only surround the minimal
changed blocks (event declaration, method UpdateReagentsList, and the charge
display code) so unchanged code is not enclosed.

Comment on lines +83 to 100
var reagentPrototypes = inventory
// Convert reagentId[] to (reagentId, prototype)[]
.Select(reagentPair => (
reagentPair.reagent,
prototype: _prototypeManager.TryIndex(reagentPair.reagent.Prototype, out ReagentPrototype? prototype) ? prototype : null,
reagentPair.cost
))
.ToList(); // Copy, but should be fine since there not that much reagents

// Sort reagents by name
reagentPrototypes.Sort((x, y) => x.prototype?.LocalizedName?.CompareTo(y.prototype?.LocalizedName ?? untranslatedText) ?? 0);

foreach (var item in reagentPrototypes)
{
var card = new ReagentCardControl(item);
var card = new ReagentCardControl(item.reagent, item.prototype, item.cost * amount);
card.OnPressed += OnDispenseReagentButtonPressed;
card.OnEjectButtonPressed += OnEjectJugButtonPressed;
ReagentList.Children.Add(card);
}
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

Sort comparator doesn't handle null prototypes consistently.

On Line 93, when x.prototype is null, the null-conditional chain (x.prototype?.LocalizedName?.CompareTo(...)) evaluates to null, and ?? 0 makes the comparison return 0 — meaning unknown reagents are "equal" to every other reagent, producing an unstable/arbitrary sort position. Consider falling back to a string comparison on the reagent ID for null prototypes.

📝 Proposed fix
-            reagentPrototypes.Sort((x, y) => x.prototype?.LocalizedName?.CompareTo(y.prototype?.LocalizedName ?? untranslatedText) ?? 0);
+            reagentPrototypes.Sort((x, y) =>
+                string.Compare(
+                    x.prototype?.LocalizedName ?? x.reagent.Prototype,
+                    y.prototype?.LocalizedName ?? y.reagent.Prototype,
+                    StringComparison.CurrentCulture));
📝 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 reagentPrototypes = inventory
// Convert reagentId[] to (reagentId, prototype)[]
.Select(reagentPair => (
reagentPair.reagent,
prototype: _prototypeManager.TryIndex(reagentPair.reagent.Prototype, out ReagentPrototype? prototype) ? prototype : null,
reagentPair.cost
))
.ToList(); // Copy, but should be fine since there not that much reagents
// Sort reagents by name
reagentPrototypes.Sort((x, y) => x.prototype?.LocalizedName?.CompareTo(y.prototype?.LocalizedName ?? untranslatedText) ?? 0);
foreach (var item in reagentPrototypes)
{
var card = new ReagentCardControl(item);
var card = new ReagentCardControl(item.reagent, item.prototype, item.cost * amount);
card.OnPressed += OnDispenseReagentButtonPressed;
card.OnEjectButtonPressed += OnEjectJugButtonPressed;
ReagentList.Children.Add(card);
}
var reagentPrototypes = inventory
// Convert reagentId[] to (reagentId, prototype)[]
.Select(reagentPair => (
reagentPair.reagent,
prototype: _prototypeManager.TryIndex(reagentPair.reagent.Prototype, out ReagentPrototype? prototype) ? prototype : null,
reagentPair.cost
))
.ToList(); // Copy, but should be fine since there not that much reagents
// Sort reagents by name
reagentPrototypes.Sort((x, y) =>
string.Compare(
x.prototype?.LocalizedName ?? x.reagent.Prototype,
y.prototype?.LocalizedName ?? y.reagent.Prototype,
StringComparison.CurrentCulture));
foreach (var item in reagentPrototypes)
{
var card = new ReagentCardControl(item.reagent, item.prototype, item.cost * amount);
card.OnPressed += OnDispenseReagentButtonPressed;
ReagentList.Children.Add(card);
}
🤖 Prompt for AI Agents
In `@Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml.cs` around lines 83 -
100, The current Sort comparator on reagentPrototypes can return 0 when
x.prototype is null, making the sort unstable; update the comparator used on
reagentPrototypes so it compares x.prototype.LocalizedName and
y.prototype.LocalizedName when both prototypes exist, and when one or both
prototypes are null fall back to comparing the reagent prototype IDs (use the
reagent.Prototype string from the tuple) or another stable reagent identifier
before returning 0; modify the Sort lambda that references
ReagentPrototype.LocalizedName to implement this three-way fallback (localized
name -> prototype id -> final tie-breaker such as reagent.Prototype.CompareTo)
so unknown prototypes sort deterministically.

Comment on lines +159 to +166
## Maid START PR №21
# - type: Storage
# whitelist:
# tags:
# - DrinkBottle
- type: LimitedCharges
lastCharges: 0 # Doesn't work, search for comment in PR 21 for more info
## Maid END
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the booze.yml file
find . -name "booze.yml" -type f

Repository: TheMaidDev/Maid-14

Length of output: 125


🏁 Script executed:

# Search for LimitedChargesComponent to understand how lastCharges is used
rg -l "LimitedCharges" --type cs | head -20

Repository: TheMaidDev/Maid-14

Length of output: 1119


🏁 Script executed:

# Check the specific file content around the flagged lines
cat -n Resources/Prototypes/Entities/Structures/Dispensers/booze.yml | sed -n '150,170p'

Repository: TheMaidDev/Maid-14

Length of output: 547


🏁 Script executed:

cat -n Content.Shared/Charges/Components/LimitedChargesComponent.cs

Repository: TheMaidDev/Maid-14

Length of output: 2321


🏁 Script executed:

cat -n Content.Shared/Charges/Systems/SharedChargesSystem.cs

Repository: TheMaidDev/Maid-14

Length of output: 11590


lastCharges: 0 does not work due to MapInit override logic.

The OnChargesMapInit event in SharedChargesSystem.cs (lines 90-105) treats LastCharges == 0 as "uninitialized" and resets it to MaxCharges. This means setting lastCharges: 0 in the YAML will have no effect—BoozeDispenserEmpty will spawn with full charges instead of empty.

To spawn empty, use lastCharges: -1 instead (which is clamped to 0 on MapInit).

🤖 Prompt for AI Agents
In `@Resources/Prototypes/Entities/Structures/Dispensers/booze.yml` around lines
159 - 166, The YAML uses lastCharges: 0 which is treated as "uninitialized" by
the OnChargesMapInit logic in SharedChargesSystem.cs (it treats LastCharges == 0
as unset and resets to MaxCharges), so change the YAML entry for the Booze
dispenser (the LimitedCharges block with lastCharges) to use lastCharges: -1 to
produce an empty spawn (the map-init will clamp -1 to 0); reference
OnChargesMapInit / LastCharges / MaxCharges when locating the logic if you
instead prefer to adjust code behavior or add a clarifying comment.

Comment on lines +137 to +138
- type: LimitedCharges
lastCharges: 1000
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find the LimitedChargesComponent definition
ast-grep --pattern $'public sealed partial class LimitedChargesComponent : Component {
  $$$
}'

Repository: TheMaidDev/Maid-14

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Also search for LimitedChargesComponent more broadly
rg -A 30 "class LimitedChargesComponent" --type csharp

Repository: TheMaidDev/Maid-14

Length of output: 1653


🏁 Script executed:

#!/bin/bash
# Read the YAML file to understand the context around lines 137-138 and line 69
cat -n Resources/Prototypes/Entities/Structures/Dispensers/chem.yml | sed -n '60,150p'

Repository: TheMaidDev/Maid-14

Length of output: 2518


The lastCharges override may not work on either variant—investigate why the field isn't being applied despite being properly decorated with [DataField].

The field is correctly marked with [DataField, AutoNetworkedField] in LimitedChargesComponent, so YAML override should be supported. However, the comment on Line 69 states lastCharges: 0 # Doesn't work, indicating the override isn't being applied on ChemDispenserEmpty. Since both ChemDispenser (Filled) and ChemDispenserEmpty use the same component with identical field decoration, the same issue affects the override on Line 138. The problem likely lies in how LastCharges is initialized or used elsewhere in the code, not in the field declaration itself.

Comment on lines +164 to +168
components:
## Maid START PR №21
- type: LimitedCharges
lastCharges: 0 # Doesn't work, search for comment in PR 21 for more info
# Maid END
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

🧩 Analysis chain

🏁 Script executed:

rg -n "lastCharges|LastCharges|_charges" --type cs -C3 -g '!obj/**' | head -100

Repository: TheMaidDev/Maid-14

Length of output: 11011


🏁 Script executed:

# Search for PR 21 references or related comments in YAML files
rg -n "PR 21|PR №21" --type yaml

Repository: TheMaidDev/Maid-14

Length of output: 1579


🏁 Script executed:

# Find LimitedCharges component definition
fd -e cs | xargs grep -l "class LimitedCharges\|struct LimitedCharges" 2>/dev/null | head -5

Repository: TheMaidDev/Maid-14

Length of output: 122


🏁 Script executed:

cat -n Content.Shared/Charges/Components/LimitedChargesComponent.cs

Repository: TheMaidDev/Maid-14

Length of output: 2321


🏁 Script executed:

# Also search for SharedChargesSystem to understand deserialization behavior
rg -n "class SharedChargesSystem\|lastCharges" --type cs Content.Shared/Charges/ -A 5 -B 2

Repository: TheMaidDev/Maid-14

Length of output: 44


🏁 Script executed:

# Find SharedChargesSystem
fd -e cs -name "*ChargesSystem*" | head -10

Repository: TheMaidDev/Maid-14

Length of output: 292


🏁 Script executed:

# Search for component initialization or registration patterns in the charges system
rg -n "public.*SharedChargesSystem|LastCharges|OnComponentInit" --type cs -g '*Charges*' -A 3 -B 1 | head -80

Repository: TheMaidDev/Maid-14

Length of output: 5727


🏁 Script executed:

# Look at the full SharedChargesSystem initialization logic
cat -n Content.Shared/Charges/Systems/SharedChargesSystem.cs | sed -n '85,110p'

Repository: TheMaidDev/Maid-14

Length of output: 1032


🏁 Script executed:

# Check what MaxCharges value is set for the Empty variants in soda.yml
cat -n Resources/Prototypes/Entities/Structures/Dispensers/soda.yml | sed -n '160,175p'

Repository: TheMaidDev/Maid-14

Length of output: 352


🏁 Script executed:

# Check SodaDispenser parent configuration for MaxCharges
cat -n Resources/Prototypes/Entities/Structures/Dispensers/soda.yml | sed -n '100,135p'

Repository: TheMaidDev/Maid-14

Length of output: 1286


🏁 Script executed:

# Also check chem.yml to see if it has the same issue
cat -n Resources/Prototypes/Entities/Structures/Dispensers/chem.yml | sed -n '55,75p'

Repository: TheMaidDev/Maid-14

Length of output: 800


🏁 Script executed:

# Check the parent entity for ChemDispenserEmpty to see if it has MaxCharges override
cat -n Resources/Prototypes/Entities/Structures/Dispensers/chem.yml | sed -n '65,80p'

Repository: TheMaidDev/Maid-14

Length of output: 621


🏁 Script executed:

# Check booze.yml for similar pattern to confirm this affects all dispensers
cat -n Resources/Prototypes/Entities/Structures/Dispensers/booze.yml | sed -n '110,170p'

Repository: TheMaidDev/Maid-14

Length of output: 1855


lastCharges: 0 cannot work due to MapInit logic treating 0 as a sentinel value.

In SharedChargesSystem.OnChargesMapInit (lines 93-96), the code explicitly checks if (ent.Comp.LastCharges == 0) and replaces it with MaxCharges. This means setting lastCharges: 0 in the YAML does nothing—the field is overwritten to 1000 during initialization, defeating the purpose of the Empty variant.

To actually start with 0 charges, use lastCharges: -1 instead (which the system correctly clamps to 0 per lines 98-100). Alternatively, remove this non-functional field and address the underlying API issue: 0 should not be reserved as a sentinel value.

This pattern repeats across chem.yml, booze.yml, and soda.yml with identical comments.

🤖 Prompt for AI Agents
In `@Resources/Prototypes/Entities/Structures/Dispensers/soda.yml` around lines
164 - 168, The YAML sets lastCharges: 0 which is ineffective because
SharedChargesSystem.OnChargesMapInit treats 0 as a sentinel and replaces
ent.Comp.LastCharges with MaxCharges; update the Dispensers entries (soda.yml,
and the similar chem.yml and booze.yml instances) to use lastCharges: -1 if you
want an empty start (the system clamps -1 to 0), or remove the lastCharges field
entirely; reference SharedChargesSystem.OnChargesMapInit, ent.Comp.LastCharges,
and MaxCharges when making the change so you alter the correct YAML blocks.

@HSKveez
Copy link
Copy Markdown
Collaborator

HSKveez commented Feb 16, 2026

Все //Maid edit (start\end) должны помечать измененные строки/блоки кода, а не пол файла

SomeOldMethod()
{
    //...
    
    //Maid edit start
    SomeNewLogic(blah blah blah);
    if(something != null)
        return;
    //Maid edit end
     
    //...
     
    AnotherNewLogic(blah blah blah);//Maid edit
     
    //...
}

Comment on lines +71 to +77
// Maid START PR №21
/*
_window.OnDispenseReagentButtonPressed += (location) => SendMessage(new ReagentDispenserDispenseReagentMessage(location));
_window.OnEjectJugButtonPressed += (location) => SendMessage(new ReagentDispenserEjectContainerMessage(location));
*/
_window.OnDispenseReagentButtonPressed += (reagent) => SendMessage(new ReagentDispenserDispenseReagentMessage(reagent));
// Maid END
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Старый код можно удалять если новый окружен //Maid edit

if (hasCharges && dispensed > float.Epsilon)
{
return;
_chargesSystem.AddCharges(reagentDispenser.Owner, -((int) (MathF.Ceiling((float) dispensed * singleCost) + float.Epsilon))); // Ceil dispensed amount up (should be safe)
Copy link
Copy Markdown
Collaborator

@HSKveez HSKveez Feb 16, 2026

Choose a reason for hiding this comment

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

Почему -((int) (MathF.Ceiling((float) dispensed * singleCost) + float.Epsilon)), распиши зачем ты прибавляешь float.Epsilon после MathF.Ceiling()

Copy link
Copy Markdown

@RedFoxIV RedFoxIV left a comment

Choose a reason for hiding this comment

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

дальше платинового foreach (var item in storageFillComp.Contents) не смотрел

по-хорошему, тебе надо создать новую ветку и начать с нуля; то, что у тебя есть сейчас - уже не спасти.

@CREAsTIVE
Copy link
Copy Markdown
Author

Updated

@RedBurningPhoenix
Copy link
Copy Markdown
Collaborator

Ожидаем зарядку при подключении к сети, а ещё "очень интересные" комментарии всё ещё на месте.

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.

4 participants