mirror of
https://github.com/DeltaV-Station/Delta-v.git
synced 2026-09-30 16:17:27 +00:00
* Fix the sensor monitoring console forcing a GC every 3 seconds (#38146) * Optimize sensor monitoring window graph drawing * Add shared static Vector2 pool for all GraphView instances * Address requested changes * remove lock * Update submodule to 264.0.0 (#38629) * Toy/Plushie Inhands and Wearables (#38514) * Fixed dependency injection and some other issues in a few places. * More issue fixes * patchfor broken NetworkConfiguratorLinkMenu (#38632) fix * Fix solutions flickering when transferring contents (#34838) * Use Solution clones when applying SolutionComponent states * Revert "Use Solution clones when applying SolutionComponent states" This reverts commit 013fd111cf92b22562e00f98a7aaa49bc4b4ed62. * Make Solution implement ICloneable and rename Clone method. * Copy CanReact value when cloning a Solution * Convert to IRobustCloneable * Fix typos in guidebook: Buisness → Business (#38636) * Fix typo in `MinorAntagonists.xml` Buisness → Business * Fix typo in `YourFirstCharacter.xml` Buisness → Business * Various Headphones Fixes and Tweaks (#38479) * sprites, wearables * neck * icon-on sprite * Cleanup prototype instantiation in `DamageTest` (#38639) Cleanup prototype instantiation in DamageTest * Cleanup prototype instantiation in `ExplosionSystem` (#38642) Cleanup prototype instantiation in ExplosionSystem * AddBodyPartCommand localization. (#38612) commit * fix ItemSlotsSystem debug assert (#38655) * Allow the Command & Super door remotes to use the access of their user. (Re-creation of PR due to changes to game balance) (#35536) * Added directional beacons (#38284) * Added directional beacons Signed-off-by: Nox38 <nebulousnox38@gmail.com> * Fixed names Signed-off-by: Nox38 <nebulousnox38@gmail.com> --------- Signed-off-by: Nox38 <nebulousnox38@gmail.com> * Power stat and nuke codes commands get some LEC love. (#38585) * commit * requested changes. * Dsay Dirty and Follow commands converted to LEC and localized. (#38666) * commit * whoopwhoopwhoop * Retro laser sprite fix (#38676) * Fixed everything except the icon Signed-off-by: Nox38 <nebulousnox38@gmail.com> * fixed icon Signed-off-by: Nox38 <nebulousnox38@gmail.com> --------- Signed-off-by: Nox38 <nebulousnox38@gmail.com> * fix water coolers (#38681) * Monochromacy typo fix (#38686) * fixes the typo * Fixed cloning looking for the trait, not the component, RE https://github.com/space-wizards/space-station-14/pull/38686#issuecomment-3025093504 * Apply suggestions from code review --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Validate `CloningSettingsPrototype`s (#38688) * Validate CloningSettingsPrototypes * Update Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Check EventComponents too --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Add test of objective-related console commands (#36400) * Add test of objective add/list/remove commands * Not sure why we're validating test prototypes, but sure * We don't need a map * Improvements and fixups for New Status Effect API (#38660) * Predict healing and bloodstream (#38690) * initial commit * reapply 38126 * fix rootable * someone missed an important minus sign here * try this * fix * fix * reenable crit hits * cleanup * fix status time dirtying * fix * camelCase * Healing and bloodstream prediction fixes. * Resolving Wizard casting recall on nuke disk making it impossible to disarm (#38661) * Resolving Wizard Recall on Nuke disk making it impossible to disarm - Adding a DisarmBomb case to nuke status update loop - Changing a few methods and parameters to properly follow formatting standards - Updating some names to follow camelCase * Updating missed tag * Reverting DataField change Should prevent this preventative bugfix being a breaking change. * Switch HSV to the default colorspace for character customization (#38434) * Made HSV default for character editor * Adds/fixes comments to HSV defaulting * Added dropbox fix, potentially cursed * Revert "Added dropbox fix, potentially cursed" This reverts commit a709883366fbee813e839742125e70844672af29. --------- Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com> * Fix: Don't deploy foldables when clicking on items inside containers (#38709) * Fix * Apply suggestions from code review --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Pressure Relief Valve (#36708) * initial system (this math is probably WRONG) * General code cleanup and OnExamined support (holy moly this code sucks) * UICode and related events foundation TODO: - Actually write the XAML UI and the underlying system - Un-shitcode the entire thing - Actually test everything... * Working UI code TODO: Make predicted, as this certainly isn't predicted. Even though I said it was. It isn't. * Remove one TODO for unshitcoding the examine code * Add reminder yea * Make predicted (defenitely isn't) (also defenitely isn't a copypaste from pressure pump code) * It's predicted! TODO: - Give it snazzy predicted visuals! - Have a different field for pressure entry, lest it gets bulldozed every UI update. * Improve gas pressure relief valve UI TODO: Reminder to reduce amount of dirties using deltafields * Implement DirtyField prediction * Entity<T> cleanup A lot of Entity<T> conversions and lukewarm cleanup. Also got caught copy pasting code in 4K UHD but it's not like you couldn't tell. * More cleanup and comments * Remove TODO comment on bulldozing window title * """refactoring""" - Move appearance out of shared and finally fix it. Pointless to predict appearance in this instance. - More Entity<T> conversions because I like them. - Move UI creation handling over entirely to the ActivatableUI system. - Fix a hardcoded locale string (why????). * Add visuals * Revert debugging variable replacememt yea * Revert skissue * Remove unused using directives and remove TODO * Localize, cleanup, document * Fix adminlogging discrepancy * Add ability to construct, add guidebook entry * Clear up comment * Add guidebook tooltip to valve * Convert GasPressureReliefValveBoundUserInterface declaration into primary constructor * Adds more input handling and adds autofill on open * Un-deepfry input validator shitcode Genuinely what was I smoking * improve visuals logic * Refactor again - Update math to the correct implementation - Moved code that could be re-used in the future into a helper method under AtmosphereSystem.Gases.cs * I'm sorry but I hate warnings * Remove unused using directive in AtmosphereSystem.Gases.cs * Review and cleanup * Lukewarm UI glossup * Maintainer for the upstream project btw * Remove redundant state sets and messy logic * Unduplicate valve updater code * Redo UI (im sorry Slarti) * run tests * Test refactored UI messaging * Second round of UI improvements - God please find a way to improve this system. Feels bad. * Update loop implementation * Further predict UI * Clear up SetToCurrentThreshold * cleanup * Update to master + pipe layers and bug fixes want to run tests * fixes * Deploy rename pipebomb * Documentation and requested changes * Rename the method that wiggled away * Undo rounding changes * Fix comment * Rename and cleanup * Apply suggestions from code review --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Fix TextLinkTag (#32203) * Adjust uplink buy button to be under item icon (#38596) * Adjusted uplink buy button to be under item icon * Put the discount subtext under the icon * Indent fixes, added margin --------- Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com> * Scurrets - Audio Improvements (#38482) * Scurret audio tuning * Add new sfx * Update sneezing sfx * YAML support * Rename a folder * make telesci wreck easier (#37569) rel * Validate `ShaderPrototype` IDs (#38728) * Convert all shader prototype string literals to protoids in overlays * Convert more shader prototype literal strings to protoids * Convert ValidatePrototypeId to ProtoId * Later * [BUGFIX] Fixed revenant malfunction ability not working properly only MediBots and Stasis bed (#38664) * fixed * clean up * orks fix smart * review fix 1 * more requested changes * less cursed * more descriptive description * better wording * Tiny Tiny Cleanup of the EyeClosingSystem. (#38734) Update EyeClosingSystem.cs * Fix human skin tone distribution (#38701) * fix: Use PredictedQueueDel for gib spell (#38729) * fix: don't default to uncharged sprite state for cells (#38730) * Fix Hristov description - remove inaccurate technical specs (#38746) - Removes inaccurate 'armor piercing 14.5mm shells' reference - Replaces it with a more funny description, matching the style of the other snipers and guns - Keeps ammunition type '.60 anti-materiel ammo' specification - Fixes issue #38590 Co-authored-by: Arthur Fiorese de Andrade <aandrade@cmcxs.gov.br> * feat: allow mopping evaporating puddles (#38743) * Validate remaining `ProtoId` strings (#38747) Validate remaining ProtoId strings * Validate `ProtoId`s in tests (#38745) * Convert string literals to protoids in Content.Tests * Convert string literals to protoids or consts in Content.IntegrationTests * Fix linter failures Tricksy static using misled me * Cleanup warning in StomachSystem (#38748) you did not see this * Vox scars (#38592) * Added vox scars n'stuff, renamed vox_tattoos.ftl to just vox.ftl * Revert "Added vox scars n'stuff, renamed vox_tattoos.ftl to just vox.ftl" This reverts commit c73da55ba3b39ddf93b493aecd85604c54dd8a15. * locale key fix * Changed top surgery scar names to be more generalized * Adjusted face scars * Formatting fixes --------- Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com> * UnlockNode command to LEC. (#38751) * commit * Update UnlockNodeCommand.cs * commit * move command locale to its own file. * Update Content.Server/Xenoarchaeology/Artifact/UnlockNodeCommand.cs --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Typofixes for figurine dialogue (#38737) * Typofixes for figurine dialogue * Forgot two * Janitor Tool: Wire Brush (#38667) * Wow! It's -brush- * spacing. * Update Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * fixed changes --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Minor escape menu UX improvements (#38650) * fix: spellbooks can have infinite charges (#38376) * fix: spellbooks can have infinite charges * refactor: indicate infinite spellbook charges with null Not sure if I like this much better...
1237 lines
48 KiB
C#
1237 lines
48 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using Content.Shared.Chemistry.Components;
|
|
using Content.Shared.Chemistry.Components.SolutionManager;
|
|
using Content.Shared.Chemistry.Reaction;
|
|
using Content.Shared.Chemistry.Reagent;
|
|
using Content.Shared.Containers;
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.FixedPoint;
|
|
using Content.Shared.Hands.Components;
|
|
using Content.Shared.Hands.EntitySystems;
|
|
using Content.Shared.Verbs;
|
|
using JetBrains.Annotations;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Utility;
|
|
using Dependency = Robust.Shared.IoC.DependencyAttribute;
|
|
|
|
namespace Content.Shared.Chemistry.EntitySystems;
|
|
|
|
/// <summary>
|
|
/// The event raised whenever a solution entity is modified.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Raised after chemcial reactions and <see cref="SolutionOverflowEvent"/> are handled.
|
|
/// </remarks>
|
|
/// <param name="Solution">The solution entity that has been modified.</param>
|
|
[ByRefEvent]
|
|
public readonly partial record struct SolutionChangedEvent(Entity<SolutionComponent> Solution);
|
|
|
|
/// <summary>
|
|
/// The event raised whenever a solution entity is filled past its capacity.
|
|
/// </summary>
|
|
/// <param name="Solution">The solution entity that has been overfilled.</param>
|
|
/// <param name="Overflow">The amount by which the solution entity has been overfilled.</param>
|
|
[ByRefEvent]
|
|
public partial record struct SolutionOverflowEvent(Entity<SolutionComponent> Solution, FixedPoint2 Overflow)
|
|
{
|
|
/// <summary>The solution entity that has been overfilled.</summary>
|
|
public readonly Entity<SolutionComponent> Solution = Solution;
|
|
/// <summary>The amount by which the solution entity has been overfilled.</summary>
|
|
public readonly FixedPoint2 Overflow = Overflow;
|
|
/// <summary>Whether any of the event handlers for this event have handled overflow behaviour.</summary>
|
|
public bool Handled = false;
|
|
}
|
|
|
|
[ByRefEvent]
|
|
public partial record struct SolutionAccessAttemptEvent(string SolutionName)
|
|
{
|
|
public bool Cancelled;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Part of Chemistry system deal with SolutionContainers
|
|
/// </summary>
|
|
[UsedImplicitly]
|
|
public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
|
{
|
|
[Robust.Shared.IoC.Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
|
[Robust.Shared.IoC.Dependency] protected readonly ChemicalReactionSystem ChemicalReactionSystem = default!;
|
|
[Robust.Shared.IoC.Dependency] protected readonly ExamineSystemShared ExamineSystem = default!;
|
|
[Robust.Shared.IoC.Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!;
|
|
[Robust.Shared.IoC.Dependency] protected readonly SharedHandsSystem Hands = default!;
|
|
[Robust.Shared.IoC.Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
|
|
[Robust.Shared.IoC.Dependency] protected readonly MetaDataSystem MetaDataSys = default!;
|
|
[Robust.Shared.IoC.Dependency] protected readonly INetManager NetManager = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
InitializeRelays();
|
|
|
|
SubscribeLocalEvent<SolutionComponent, ComponentInit>(OnComponentInit);
|
|
SubscribeLocalEvent<SolutionComponent, ComponentStartup>(OnSolutionStartup);
|
|
SubscribeLocalEvent<SolutionComponent, ComponentShutdown>(OnSolutionShutdown);
|
|
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentInit>(OnContainerManagerInit);
|
|
SubscribeLocalEvent<ExaminableSolutionComponent, ExaminedEvent>(OnExamineSolution);
|
|
SubscribeLocalEvent<ExaminableSolutionComponent, GetVerbsEvent<ExamineVerb>>(OnSolutionExaminableVerb);
|
|
SubscribeLocalEvent<SolutionContainerManagerComponent, MapInitEvent>(OnMapInit);
|
|
|
|
if (NetManager.IsServer)
|
|
{
|
|
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentShutdown>(OnContainerManagerShutdown);
|
|
SubscribeLocalEvent<ContainedSolutionComponent, ComponentShutdown>(OnContainedSolutionShutdown);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Attempts to resolve a solution associated with an entity.
|
|
/// </summary>
|
|
/// <param name="container">The entity that holdes the container the solution entity is in.</param>
|
|
/// <param name="name">The name of the solution entities container.</param>
|
|
/// <param name="entity">A reference to a solution entity to load the associated solution entity into. Will be unchanged if not null.</param>
|
|
/// <param name="solution">Returns the solution state of the solution entity.</param>
|
|
/// <returns>Whether the solution was successfully resolved.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool ResolveSolution(Entity<SolutionContainerManagerComponent?> container, string? name, [NotNullWhen(true)] ref Entity<SolutionComponent>? entity, [NotNullWhen(true)] out Solution? solution)
|
|
{
|
|
if (!ResolveSolution(container, name, ref entity))
|
|
{
|
|
solution = null;
|
|
return false;
|
|
}
|
|
|
|
solution = entity.Value.Comp.Solution;
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc cref="ResolveSolution"/>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool ResolveSolution(Entity<SolutionContainerManagerComponent?> container, string? name, [NotNullWhen(true)] ref Entity<SolutionComponent>? entity)
|
|
{
|
|
if (entity is not null)
|
|
{
|
|
DebugTools.Assert(TryGetSolution(container, name, out var debugEnt)
|
|
&& debugEnt.Value.Owner == entity.Value.Owner);
|
|
return true;
|
|
}
|
|
|
|
return TryGetSolution(container, name, out entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to fetch a solution entity associated with an entity.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the solution entity will be frequently accessed please use the equivalent <see cref="ResolveSolution"/> method and cache the result.
|
|
/// </remarks>
|
|
/// <param name="container">The entity the solution entity should be associated with.</param>
|
|
/// <param name="name">The name of the solution entity to fetch.</param>
|
|
/// <param name="entity">Returns the solution entity that was fetched.</param>
|
|
/// <param name="solution">Returns the solution state of the solution entity that was fetched.</param>
|
|
/// /// <param name="errorOnMissing">Should we print an error if the solution specified by name is missing</param>
|
|
/// <returns></returns>
|
|
public bool TryGetSolution(
|
|
Entity<SolutionContainerManagerComponent?> container,
|
|
string? name,
|
|
[NotNullWhen(true)] out Entity<SolutionComponent>? entity,
|
|
[NotNullWhen(true)] out Solution? solution,
|
|
bool errorOnMissing = false)
|
|
{
|
|
if (!TryGetSolution(container, name, out entity, errorOnMissing: errorOnMissing))
|
|
{
|
|
solution = null;
|
|
return false;
|
|
}
|
|
|
|
solution = entity.Value.Comp.Solution;
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc cref="TryGetSolution"/>
|
|
public bool TryGetSolution(
|
|
Entity<SolutionContainerManagerComponent?> container,
|
|
string? name,
|
|
[NotNullWhen(true)] out Entity<SolutionComponent>? entity,
|
|
bool errorOnMissing = false)
|
|
{
|
|
// use connected container instead of entity from arguments, if it exists.
|
|
var ev = new GetConnectedContainerEvent();
|
|
RaiseLocalEvent(container, ref ev);
|
|
if (ev.ContainerEntity.HasValue)
|
|
container = ev.ContainerEntity.Value;
|
|
|
|
EntityUid uid;
|
|
if (name is null)
|
|
uid = container;
|
|
else if (
|
|
ContainerSystem.TryGetContainer(container, $"solution@{name}", out var solutionContainer) &&
|
|
solutionContainer is ContainerSlot solutionSlot &&
|
|
solutionSlot.ContainedEntity is { } containedSolution
|
|
)
|
|
{
|
|
var attemptEv = new SolutionAccessAttemptEvent(name);
|
|
RaiseLocalEvent(container, ref attemptEv);
|
|
|
|
if (attemptEv.Cancelled)
|
|
{
|
|
entity = null;
|
|
return false;
|
|
}
|
|
|
|
uid = containedSolution;
|
|
}
|
|
else
|
|
{
|
|
entity = null;
|
|
if (!errorOnMissing)
|
|
return false;
|
|
Log.Error($"{ToPrettyString(container)} does not have a solution with ID: {name}");
|
|
return false;
|
|
}
|
|
|
|
if (!TryComp(uid, out SolutionComponent? comp))
|
|
{
|
|
entity = null;
|
|
if (!errorOnMissing)
|
|
return false;
|
|
Log.Error($"{ToPrettyString(container)} does not have a solution with ID: {name}");
|
|
return false;
|
|
}
|
|
|
|
entity = (uid, comp);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Version of TryGetSolution that doesn't take or return an entity.
|
|
/// Used for prototypes and with old code parity.
|
|
public bool TryGetSolution(SolutionContainerManagerComponent container,
|
|
string name,
|
|
[NotNullWhen(true)] out Solution? solution,
|
|
bool errorOnMissing = false)
|
|
{
|
|
solution = null;
|
|
if (container.Solutions != null)
|
|
return container.Solutions.TryGetValue(name, out solution);
|
|
if (!errorOnMissing)
|
|
return false;
|
|
Log.Error($"{container} does not have a solution with ID: {name}");
|
|
return false;
|
|
}
|
|
|
|
public IEnumerable<(string? Name, Entity<SolutionComponent> Solution)> EnumerateSolutions(Entity<SolutionContainerManagerComponent?> container, bool includeSelf = true)
|
|
{
|
|
if (includeSelf && TryComp(container, out SolutionComponent? solutionComp))
|
|
yield return (null, (container.Owner, solutionComp));
|
|
|
|
if (!Resolve(container, ref container.Comp, logMissing: false))
|
|
yield break;
|
|
|
|
foreach (var name in container.Comp.Containers)
|
|
{
|
|
var attemptEv = new SolutionAccessAttemptEvent(name);
|
|
RaiseLocalEvent(container, ref attemptEv);
|
|
|
|
if (attemptEv.Cancelled)
|
|
continue;
|
|
|
|
if (ContainerSystem.GetContainer(container, $"solution@{name}") is ContainerSlot slot && slot.ContainedEntity is { } solutionId)
|
|
yield return (name, (solutionId, Comp<SolutionComponent>(solutionId)));
|
|
}
|
|
}
|
|
|
|
public IEnumerable<(string Name, Solution Solution)> EnumerateSolutions(SolutionContainerManagerComponent container)
|
|
{
|
|
if (container.Solutions is not { Count: > 0 } solutions)
|
|
yield break;
|
|
|
|
foreach (var (name, solution) in solutions)
|
|
{
|
|
yield return (name, solution);
|
|
}
|
|
}
|
|
|
|
|
|
protected void UpdateAppearance(Entity<AppearanceComponent?> container, Entity<SolutionComponent, ContainedSolutionComponent> soln)
|
|
{
|
|
var (uid, appearanceComponent) = container;
|
|
if (!HasComp<SolutionContainerVisualsComponent>(uid) || !Resolve(uid, ref appearanceComponent, logMissing: false))
|
|
return;
|
|
|
|
var (_, comp, relation) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
AppearanceSystem.SetData(uid, SolutionContainerVisuals.FillFraction, solution.FillFraction, appearanceComponent);
|
|
AppearanceSystem.SetData(uid, SolutionContainerVisuals.Color, solution.GetColor(PrototypeManager), appearanceComponent);
|
|
AppearanceSystem.SetData(uid, SolutionContainerVisuals.SolutionName, relation.ContainerName, appearanceComponent);
|
|
|
|
if (solution.GetPrimaryReagentId() is { } reagent)
|
|
AppearanceSystem.SetData(uid, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearanceComponent);
|
|
}
|
|
|
|
|
|
public FixedPoint2 GetTotalPrototypeQuantity(EntityUid owner, string reagentId)
|
|
{
|
|
var reagentQuantity = FixedPoint2.New(0);
|
|
if (Exists(owner)
|
|
&& TryComp(owner, out SolutionContainerManagerComponent? managerComponent))
|
|
{
|
|
foreach (var (_, soln) in EnumerateSolutions((owner, managerComponent)))
|
|
{
|
|
var solution = soln.Comp.Solution;
|
|
reagentQuantity += solution.GetTotalPrototypeQuantity(reagentId);
|
|
}
|
|
}
|
|
|
|
return reagentQuantity;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Dirties a solution entity that has been modified and prompts updates to chemical reactions and overflow state.
|
|
/// Should be invoked whenever a solution entity is modified.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 90% of this system is ensuring that this proc is invoked whenever a solution entity is changed. The other 10% <i>is</i> this proc.
|
|
/// </remarks>
|
|
/// <param name="soln"></param>
|
|
/// <param name="needsReactionsProcessing"></param>
|
|
/// <param name="mixerComponent"></param>
|
|
public void UpdateChemicals(Entity<SolutionComponent> soln, bool needsReactionsProcessing = true, ReactionMixerComponent? mixerComponent = null)
|
|
{
|
|
Dirty(soln);
|
|
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
// Process reactions
|
|
if (needsReactionsProcessing && solution.CanReact)
|
|
ChemicalReactionSystem.FullyReactSolution(soln, mixerComponent);
|
|
|
|
var overflow = solution.Volume - solution.MaxVolume;
|
|
if (overflow > FixedPoint2.Zero)
|
|
{
|
|
var overflowEv = new SolutionOverflowEvent(soln, overflow);
|
|
RaiseLocalEvent(uid, ref overflowEv);
|
|
}
|
|
|
|
UpdateAppearance((uid, comp, null));
|
|
|
|
var changedEv = new SolutionChangedEvent(soln);
|
|
RaiseLocalEvent(uid, ref changedEv);
|
|
}
|
|
|
|
public void UpdateAppearance(Entity<SolutionComponent, AppearanceComponent?> soln)
|
|
{
|
|
var (uid, comp, appearanceComponent) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (!Exists(uid) || !Resolve(uid, ref appearanceComponent, false))
|
|
return;
|
|
|
|
AppearanceSystem.SetData(uid, SolutionContainerVisuals.FillFraction, solution.FillFraction, appearanceComponent);
|
|
AppearanceSystem.SetData(uid, SolutionContainerVisuals.Color, solution.GetColor(PrototypeManager), appearanceComponent);
|
|
|
|
if (solution.GetPrimaryReagentId() is { } reagent)
|
|
AppearanceSystem.SetData(uid, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearanceComponent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes part of the solution in the container.
|
|
/// </summary>
|
|
/// <param name="targetUid"></param>
|
|
/// <param name="solutionHolder"></param>
|
|
/// <param name="quantity">the volume of solution to remove.</param>
|
|
/// <returns>The solution that was removed.</returns>
|
|
public Solution SplitSolution(Entity<SolutionComponent> soln, FixedPoint2 quantity)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
var splitSol = solution.SplitSolution(quantity);
|
|
UpdateChemicals(soln);
|
|
return splitSol;
|
|
}
|
|
|
|
public Solution SplitStackSolution(Entity<SolutionComponent> soln, FixedPoint2 quantity, int stackCount)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
var splitSol = solution.SplitSolution(quantity / stackCount);
|
|
solution.SplitSolution(quantity - splitSol.Volume);
|
|
UpdateChemicals(soln);
|
|
return splitSol;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits a solution without the specified reagent(s).
|
|
/// </summary>
|
|
[Obsolete("Use SplitSolutionWithout with params ProtoId<ReagentPrototype>")]
|
|
public Solution SplitSolutionWithout(Entity<SolutionComponent> soln, FixedPoint2 quantity, params string[] reagents)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
var splitSol = solution.SplitSolutionWithout(quantity, reagents);
|
|
UpdateChemicals(soln);
|
|
return splitSol;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits a solution without the specified reagent(s).
|
|
/// </summary>
|
|
public Solution SplitSolutionWithout(Entity<SolutionComponent> soln, FixedPoint2 quantity, params ProtoId<ReagentPrototype>[] reagents)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
var splitSol = solution.SplitSolutionWithout(quantity, reagents);
|
|
UpdateChemicals(soln);
|
|
return splitSol;
|
|
}
|
|
|
|
public void RemoveAllSolution(Entity<SolutionComponent> soln)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (solution.Volume == 0)
|
|
return;
|
|
|
|
solution.RemoveAllSolution();
|
|
UpdateChemicals(soln);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the capacity (maximum volume) of a solution to a new value.
|
|
/// </summary>
|
|
/// <param name="targetUid">The entity containing the solution.</param>
|
|
/// <param name="targetSolution">The solution to set the capacity of.</param>
|
|
/// <param name="capacity">The value to set the capacity of the solution to.</param>
|
|
public void SetCapacity(Entity<SolutionComponent> soln, FixedPoint2 capacity)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (solution.MaxVolume == capacity)
|
|
return;
|
|
|
|
solution.MaxVolume = capacity;
|
|
UpdateChemicals(soln);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds reagent of an Id to the container.
|
|
/// </summary>
|
|
/// <param name="targetUid"></param>
|
|
/// <param name="targetSolution">Container to which we are adding reagent</param>
|
|
/// <param name="reagentQuantity">The reagent to add.</param>
|
|
/// <param name="acceptedQuantity">The amount of reagent successfully added.</param>
|
|
/// <returns>If all the reagent could be added.</returns>
|
|
public bool TryAddReagent(Entity<SolutionComponent> soln, ReagentQuantity reagentQuantity, out FixedPoint2 acceptedQuantity, float? temperature = null)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
acceptedQuantity = solution.AvailableVolume > reagentQuantity.Quantity
|
|
? reagentQuantity.Quantity
|
|
: solution.AvailableVolume;
|
|
|
|
if (acceptedQuantity <= 0)
|
|
return reagentQuantity.Quantity == 0;
|
|
|
|
if (temperature == null)
|
|
{
|
|
solution.AddReagent(reagentQuantity.Reagent, acceptedQuantity);
|
|
}
|
|
else
|
|
{
|
|
var proto = PrototypeManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
|
|
solution.AddReagent(proto, acceptedQuantity, temperature.Value, PrototypeManager);
|
|
}
|
|
|
|
UpdateChemicals(soln);
|
|
return acceptedQuantity == reagentQuantity.Quantity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds reagent of an Id to the container.
|
|
/// </summary>
|
|
/// <param name="targetUid"></param>
|
|
/// <param name="targetSolution">Container to which we are adding reagent</param>
|
|
/// <param name="prototype">The Id of the reagent to add.</param>
|
|
/// <param name="quantity">The amount of reagent to add.</param>
|
|
/// <returns>If all the reagent could be added.</returns>
|
|
[PublicAPI]
|
|
public bool TryAddReagent(Entity<SolutionComponent> soln, string prototype, FixedPoint2 quantity, float? temperature = null, List<ReagentData>? data = null)
|
|
=> TryAddReagent(soln, new ReagentQuantity(prototype, quantity, data), out _, temperature);
|
|
|
|
/// <summary>
|
|
/// Adds reagent of an Id to the container.
|
|
/// </summary>
|
|
/// <param name="targetUid"></param>
|
|
/// <param name="targetSolution">Container to which we are adding reagent</param>
|
|
/// <param name="prototype">The Id of the reagent to add.</param>
|
|
/// <param name="quantity">The amount of reagent to add.</param>
|
|
/// <param name="acceptedQuantity">The amount of reagent successfully added.</param>
|
|
/// <returns>If all the reagent could be added.</returns>
|
|
public bool TryAddReagent(Entity<SolutionComponent> soln, string prototype, FixedPoint2 quantity, out FixedPoint2 acceptedQuantity, float? temperature = null, List<ReagentData>? data = null)
|
|
{
|
|
var reagent = new ReagentQuantity(prototype, quantity, data);
|
|
return TryAddReagent(soln, reagent, out acceptedQuantity, temperature);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds reagent of an Id to the container.
|
|
/// </summary>
|
|
/// <param name="targetUid"></param>
|
|
/// <param name="targetSolution">Container to which we are adding reagent</param>
|
|
/// <param name="reagentId">The reagent to add.</param>
|
|
/// <param name="quantity">The amount of reagent to add.</param>
|
|
/// <param name="acceptedQuantity">The amount of reagent successfully added.</param>
|
|
/// <returns>If all the reagent could be added.</returns>
|
|
public bool TryAddReagent(Entity<SolutionComponent> soln, ReagentId reagentId, FixedPoint2 quantity, out FixedPoint2 acceptedQuantity, float? temperature = null)
|
|
{
|
|
var quant = new ReagentQuantity(reagentId, quantity);
|
|
return TryAddReagent(soln, quant, out acceptedQuantity, temperature);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes reagent from a container.
|
|
/// </summary>
|
|
/// <param name="targetUid"></param>
|
|
/// <param name="container">Solution container from which we are removing reagent.</param>
|
|
/// <param name="reagentQuantity">The reagent to remove.</param>
|
|
/// <returns>The amount of reagent that was removed.</returns>
|
|
public FixedPoint2 RemoveReagent(Entity<SolutionComponent> soln, ReagentQuantity reagentQuantity)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
var quant = solution.RemoveReagent(reagentQuantity);
|
|
if (quant <= FixedPoint2.Zero)
|
|
return FixedPoint2.Zero;
|
|
|
|
UpdateChemicals(soln);
|
|
return quant;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes reagent from a container.
|
|
/// </summary>
|
|
/// <param name="targetUid"></param>
|
|
/// <param name="container">Solution container from which we are removing reagent</param>
|
|
/// <param name="prototype">The Id of the reagent to remove.</param>
|
|
/// <param name="quantity">The amount of reagent to remove.</param>
|
|
/// <returns>The amount of reagent that was removed.</returns>
|
|
public FixedPoint2 RemoveReagent(Entity<SolutionComponent> soln, string prototype, FixedPoint2 quantity, List<ReagentData>? data = null)
|
|
{
|
|
return RemoveReagent(soln, new ReagentQuantity(prototype, quantity, data));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes reagent from a container.
|
|
/// </summary>
|
|
/// <param name="targetUid"></param>
|
|
/// <param name="container">Solution container from which we are removing reagent</param>
|
|
/// <param name="reagentId">The reagent to remove.</param>
|
|
/// <param name="quantity">The amount of reagent to remove.</param>
|
|
/// <returns>The amount of reagent that was removed.</returns>
|
|
public FixedPoint2 RemoveReagent(Entity<SolutionComponent> soln, ReagentId reagentId, FixedPoint2 quantity)
|
|
{
|
|
return RemoveReagent(soln, new ReagentQuantity(reagentId, quantity));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves some quantity of a solution from one solution to another.
|
|
/// </summary>
|
|
/// <param name="sourceUid">entity holding the source solution</param>
|
|
/// <param name="targetUid">entity holding the target solution</param>
|
|
/// <param name="source">source solution</param>
|
|
/// <param name="target">target solution</param>
|
|
/// <param name="quantity">quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed.</param>
|
|
public bool TryTransferSolution(Entity<SolutionComponent> soln, Solution source, FixedPoint2 quantity)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (quantity < 0)
|
|
throw new InvalidOperationException("Quantity must be positive");
|
|
|
|
quantity = FixedPoint2.Min(quantity, solution.AvailableVolume, source.Volume);
|
|
if (quantity == 0)
|
|
return false;
|
|
|
|
// TODO This should be made into a function that directly transfers reagents.
|
|
// Currently this is quite inefficient.
|
|
solution.AddSolution(source.SplitSolution(quantity), PrototypeManager);
|
|
|
|
UpdateChemicals(soln);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a solution to the container, if it can fully fit.
|
|
/// </summary>
|
|
/// <param name="targetUid">entity holding targetSolution</param>
|
|
/// <param name="targetSolution">entity holding targetSolution</param>
|
|
/// <param name="toAdd">solution being added</param>
|
|
/// <returns>If the solution could be added.</returns>
|
|
public bool TryAddSolution(Entity<SolutionComponent> soln, Solution toAdd)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (toAdd.Volume == FixedPoint2.Zero)
|
|
return true;
|
|
if (toAdd.Volume > solution.AvailableVolume)
|
|
return false;
|
|
|
|
ForceAddSolution(soln, toAdd);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds as much of a solution to a container as can fit.
|
|
/// </summary>
|
|
/// <param name="targetUid">The entity containing <paramref cref="targetSolution"/></param>
|
|
/// <param name="targetSolution">The solution being added to.</param>
|
|
/// <param name="toAdd">The solution being added to <paramref cref="targetSolution"/></param>
|
|
/// <returns>The quantity of the solution actually added.</returns>
|
|
public FixedPoint2 AddSolution(Entity<SolutionComponent> soln, Solution toAdd)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (toAdd.Volume == FixedPoint2.Zero)
|
|
return FixedPoint2.Zero;
|
|
|
|
var quantity = FixedPoint2.Max(FixedPoint2.Zero, FixedPoint2.Min(toAdd.Volume, solution.AvailableVolume));
|
|
if (quantity < toAdd.Volume)
|
|
TryTransferSolution(soln, toAdd, quantity);
|
|
else
|
|
ForceAddSolution(soln, toAdd);
|
|
|
|
return quantity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a solution to a container and updates the container.
|
|
/// </summary>
|
|
/// <param name="targetUid">The entity containing <paramref cref="targetSolution"/></param>
|
|
/// <param name="targetSolution">The solution being added to.</param>
|
|
/// <param name="toAdd">The solution being added to <paramref cref="targetSolution"/></param>
|
|
/// <returns>Whether any reagents were added to the solution.</returns>
|
|
public bool ForceAddSolution(Entity<SolutionComponent> soln, Solution toAdd)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (toAdd.Volume == FixedPoint2.Zero)
|
|
return false;
|
|
|
|
solution.AddSolution(toAdd, PrototypeManager);
|
|
UpdateChemicals(soln);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a solution to the container, removing the overflow.
|
|
/// Unlike <see cref="TryAddSolution"/> it will ignore size limits.
|
|
/// </summary>
|
|
/// <param name="targetUid">The entity containing <paramref cref="targetSolution"/></param>
|
|
/// <param name="targetSolution">The solution being added to.</param>
|
|
/// <param name="toAdd">The solution being added to <paramref cref="targetSolution"/></param>
|
|
/// <param name="overflowThreshold">The combined volume above which the overflow will be returned.
|
|
/// If the combined volume is below this an empty solution is returned.</param>
|
|
/// <param name="overflowingSolution">Solution that exceeded overflowThreshold</param>
|
|
/// <returns>Whether any reagents were added to <paramref cref="targetSolution"/>.</returns>
|
|
public bool TryMixAndOverflow(Entity<SolutionComponent> soln, Solution toAdd, FixedPoint2 overflowThreshold, [MaybeNullWhen(false)] out Solution overflowingSolution)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (toAdd.Volume == 0 || overflowThreshold > solution.MaxVolume)
|
|
{
|
|
overflowingSolution = null;
|
|
return false;
|
|
}
|
|
|
|
solution.AddSolution(toAdd, PrototypeManager);
|
|
overflowingSolution = solution.SplitSolution(FixedPoint2.Max(FixedPoint2.Zero, solution.Volume - overflowThreshold));
|
|
UpdateChemicals(soln);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an amount from all reagents in a solution, adding it to a new solution.
|
|
/// </summary>
|
|
/// <param name="uid">The entity containing the solution.</param>
|
|
/// <param name="solution">The solution to remove reagents from.</param>
|
|
/// <param name="quantity">The amount to remove from every reagent in the solution.</param>
|
|
/// <returns>A new solution containing every removed reagent from the original solution.</returns>
|
|
public Solution RemoveEachReagent(Entity<SolutionComponent> soln, FixedPoint2 quantity)
|
|
{
|
|
var (uid, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (quantity <= 0)
|
|
return new Solution();
|
|
|
|
var removedSolution = new Solution();
|
|
|
|
// RemoveReagent does a RemoveSwap, meaning we don't have to copy the list if we iterate it backwards.
|
|
for (var i = solution.Contents.Count - 1; i >= 0; i--)
|
|
{
|
|
var (reagent, _) = solution.Contents[i];
|
|
var removedQuantity = solution.RemoveReagent(reagent, quantity);
|
|
removedSolution.AddReagent(reagent, removedQuantity);
|
|
}
|
|
|
|
UpdateChemicals(soln);
|
|
return removedSolution;
|
|
}
|
|
|
|
// Thermal energy and temperature management.
|
|
|
|
#region Thermal Energy and Temperature
|
|
|
|
/// <summary>
|
|
/// Sets the temperature of a solution to a new value and then checks for reaction processing.
|
|
/// </summary>
|
|
/// <param name="owner">The entity in which the solution is located.</param>
|
|
/// <param name="solution">The solution to set the temperature of.</param>
|
|
/// <param name="temperature">The new value to set the temperature to.</param>
|
|
public void SetTemperature(Entity<SolutionComponent> soln, float temperature)
|
|
{
|
|
var (_, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (temperature == solution.Temperature)
|
|
return;
|
|
|
|
solution.Temperature = temperature;
|
|
UpdateChemicals(soln);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the thermal energy of a solution to a new value and then checks for reaction processing.
|
|
/// </summary>
|
|
/// <param name="owner">The entity in which the solution is located.</param>
|
|
/// <param name="solution">The solution to set the thermal energy of.</param>
|
|
/// <param name="thermalEnergy">The new value to set the thermal energy to.</param>
|
|
public void SetThermalEnergy(Entity<SolutionComponent> soln, float thermalEnergy)
|
|
{
|
|
var (_, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
var heatCap = solution.GetHeatCapacity(PrototypeManager);
|
|
solution.Temperature = heatCap == 0 ? 0 : thermalEnergy / heatCap;
|
|
UpdateChemicals(soln);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds some thermal energy to a solution and then checks for reaction processing.
|
|
/// </summary>
|
|
/// <param name="owner">The entity in which the solution is located.</param>
|
|
/// <param name="solution">The solution to set the thermal energy of.</param>
|
|
/// <param name="thermalEnergy">The new value to set the thermal energy to.</param>
|
|
public void AddThermalEnergy(Entity<SolutionComponent> soln, float thermalEnergy)
|
|
{
|
|
var (_, comp) = soln;
|
|
var solution = comp.Solution;
|
|
|
|
if (thermalEnergy == 0.0f)
|
|
return;
|
|
|
|
var heatCap = solution.GetHeatCapacity(PrototypeManager);
|
|
solution.Temperature += heatCap == 0 ? 0 : thermalEnergy / heatCap;
|
|
UpdateChemicals(soln);
|
|
}
|
|
|
|
#endregion Thermal Energy and Temperature
|
|
|
|
#region Event Handlers
|
|
|
|
private void OnComponentInit(Entity<SolutionComponent> entity, ref ComponentInit args)
|
|
{
|
|
entity.Comp.Solution.ValidateSolution();
|
|
}
|
|
|
|
private void OnSolutionStartup(Entity<SolutionComponent> entity, ref ComponentStartup args)
|
|
{
|
|
UpdateChemicals(entity);
|
|
}
|
|
|
|
private void OnSolutionShutdown(Entity<SolutionComponent> entity, ref ComponentShutdown args)
|
|
{
|
|
RemoveAllSolution(entity);
|
|
}
|
|
|
|
private void OnContainerManagerInit(Entity<SolutionContainerManagerComponent> entity, ref ComponentInit args)
|
|
{
|
|
if (entity.Comp.Containers is not { Count: > 0 } containers)
|
|
return;
|
|
|
|
var containerManager = EnsureComp<ContainerManagerComponent>(entity);
|
|
foreach (var name in containers)
|
|
{
|
|
// The actual solution entity should be directly held within the corresponding slot.
|
|
ContainerSystem.EnsureContainer<ContainerSlot>(entity.Owner, $"solution@{name}", containerManager);
|
|
}
|
|
}
|
|
|
|
private void OnExamineSolution(Entity<ExaminableSolutionComponent> entity, ref ExaminedEvent args)
|
|
{
|
|
if (!TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanSeeHiddenSolution(entity, args.Examiner))
|
|
return;
|
|
|
|
var primaryReagent = solution.GetPrimaryReagentId();
|
|
|
|
if (string.IsNullOrEmpty(primaryReagent?.Prototype))
|
|
{
|
|
args.PushText(Loc.GetString("shared-solution-container-component-on-examine-empty-container"));
|
|
return;
|
|
}
|
|
|
|
if (!PrototypeManager.TryIndex(primaryReagent.Value.Prototype, out ReagentPrototype? primary))
|
|
{
|
|
Log.Error($"{nameof(Solution)} could not find the prototype associated with {primaryReagent}.");
|
|
return;
|
|
}
|
|
|
|
var colorHex = solution.GetColor(PrototypeManager)
|
|
.ToHexNoAlpha(); //TODO: If the chem has a dark color, the examine text becomes black on a black background, which is unreadable.
|
|
var messageString = "shared-solution-container-component-on-examine-main-text";
|
|
|
|
using (args.PushGroup(nameof(ExaminableSolutionComponent)))
|
|
{
|
|
args.PushMarkup(Loc.GetString(messageString,
|
|
("color", colorHex),
|
|
("wordedAmount", Loc.GetString(solution.Contents.Count == 1
|
|
? "shared-solution-container-component-on-examine-worded-amount-one-reagent"
|
|
: "shared-solution-container-component-on-examine-worded-amount-multiple-reagents")),
|
|
("desc", primary.LocalizedPhysicalDescription)));
|
|
|
|
var reagentPrototypes = solution.GetReagentPrototypes(PrototypeManager);
|
|
|
|
// Sort the reagents by amount, descending then alphabetically
|
|
var sortedReagentPrototypes = reagentPrototypes
|
|
.OrderByDescending(pair => pair.Value.Value)
|
|
.ThenBy(pair => pair.Key.LocalizedName);
|
|
|
|
// Add descriptions of immediately recognizable reagents, like water or beer
|
|
var recognized = new List<ReagentPrototype>();
|
|
foreach (var keyValuePair in sortedReagentPrototypes)
|
|
{
|
|
var proto = keyValuePair.Key;
|
|
if (!proto.Recognizable)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
recognized.Add(proto);
|
|
}
|
|
|
|
// Skip if there's nothing recognizable
|
|
if (recognized.Count == 0)
|
|
return;
|
|
|
|
var msg = new StringBuilder();
|
|
foreach (var reagent in recognized)
|
|
{
|
|
string part;
|
|
if (reagent == recognized[0])
|
|
{
|
|
part = "examinable-solution-recognized-first";
|
|
}
|
|
else if (reagent == recognized[^1])
|
|
{
|
|
// this loc specifically requires space to be appended, fluent doesnt support whitespace
|
|
msg.Append(' ');
|
|
part = "examinable-solution-recognized-last";
|
|
}
|
|
else
|
|
{
|
|
part = "examinable-solution-recognized-next";
|
|
}
|
|
|
|
msg.Append(Loc.GetString(part, ("color", reagent.SubstanceColor.ToHexNoAlpha()),
|
|
("chemical", reagent.LocalizedName)));
|
|
}
|
|
|
|
args.PushMarkup(Loc.GetString("examinable-solution-has-recognizable-chemicals",
|
|
("recognizedString", msg.ToString())));
|
|
}
|
|
}
|
|
|
|
private void OnSolutionExaminableVerb(Entity<ExaminableSolutionComponent> entity, ref GetVerbsEvent<ExamineVerb> args)
|
|
{
|
|
if (!args.CanInteract || !args.CanAccess)
|
|
return;
|
|
|
|
var scanEvent = new SolutionScanEvent();
|
|
RaiseLocalEvent(args.User, scanEvent);
|
|
if (!scanEvent.CanScan)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryGetSolution(args.Target, entity.Comp.Solution, out _, out var solutionHolder))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanSeeHiddenSolution(entity, args.User))
|
|
return;
|
|
|
|
var target = args.Target;
|
|
var user = args.User;
|
|
var verb = new ExamineVerb()
|
|
{
|
|
Act = () =>
|
|
{
|
|
var markup = GetSolutionExamine(solutionHolder);
|
|
ExamineSystem.SendExamineTooltip(user, target, markup, false, false);
|
|
},
|
|
Text = Loc.GetString("scannable-solution-verb-text"),
|
|
Message = Loc.GetString("scannable-solution-verb-message"),
|
|
Category = VerbCategory.Examine,
|
|
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
|
|
};
|
|
|
|
args.Verbs.Add(verb);
|
|
}
|
|
|
|
private FormattedMessage GetSolutionExamine(Solution solution)
|
|
{
|
|
var msg = new FormattedMessage();
|
|
|
|
if (solution.Volume == 0)
|
|
{
|
|
msg.AddMarkupOrThrow(Loc.GetString("scannable-solution-empty-container"));
|
|
return msg;
|
|
}
|
|
|
|
msg.AddMarkupOrThrow(Loc.GetString("scannable-solution-main-text"));
|
|
|
|
var reagentPrototypes = solution.GetReagentPrototypes(PrototypeManager);
|
|
|
|
// Sort the reagents by amount, descending then alphabetically
|
|
var sortedReagentPrototypes = reagentPrototypes
|
|
.OrderByDescending(pair => pair.Value.Value)
|
|
.ThenBy(pair => pair.Key.LocalizedName);
|
|
|
|
foreach (var (proto, quantity) in sortedReagentPrototypes)
|
|
{
|
|
msg.PushNewline();
|
|
msg.AddMarkupOrThrow(Loc.GetString("scannable-solution-chemical"
|
|
, ("type", proto.LocalizedName)
|
|
, ("color", proto.SubstanceColor.ToHexNoAlpha())
|
|
, ("amount", quantity)));
|
|
}
|
|
|
|
msg.PushNewline();
|
|
msg.AddMarkupOrThrow(Loc.GetString("scannable-solution-temperature", ("temperature", Math.Round(solution.Temperature))));
|
|
|
|
return msg;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if examinable solution requires you to hold the item in hand.
|
|
/// </summary>
|
|
private bool CanSeeHiddenSolution(Entity<ExaminableSolutionComponent> entity, EntityUid examiner)
|
|
{
|
|
// If not held-only then it's always visible.
|
|
if (!entity.Comp.HeldOnly)
|
|
return true;
|
|
|
|
return Hands.IsHolding(examiner, entity, out _);
|
|
}
|
|
|
|
private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)
|
|
{
|
|
EnsureAllSolutions(entity);
|
|
}
|
|
|
|
private void OnContainerManagerShutdown(Entity<SolutionContainerManagerComponent> entity, ref ComponentShutdown args)
|
|
{
|
|
foreach (var name in entity.Comp.Containers)
|
|
{
|
|
if (ContainerSystem.TryGetContainer(entity, $"solution@{name}", out var solutionContainer))
|
|
ContainerSystem.ShutdownContainer(solutionContainer);
|
|
}
|
|
entity.Comp.Containers.Clear();
|
|
}
|
|
|
|
private void OnContainedSolutionShutdown(Entity<ContainedSolutionComponent> entity, ref ComponentShutdown args)
|
|
{
|
|
if (TryComp(entity.Comp.Container, out SolutionContainerManagerComponent? container))
|
|
{
|
|
container.Containers.Remove(entity.Comp.ContainerName);
|
|
Dirty(entity.Comp.Container, container);
|
|
}
|
|
|
|
if (ContainerSystem.TryGetContainer(entity, $"solution@{entity.Comp.ContainerName}", out var solutionContainer))
|
|
ContainerSystem.ShutdownContainer(solutionContainer);
|
|
}
|
|
|
|
#endregion Event Handlers
|
|
|
|
public bool EnsureSolution(
|
|
Entity<MetaDataComponent?> entity,
|
|
string name,
|
|
[NotNullWhen(true)]out Solution? solution,
|
|
FixedPoint2 maxVol = default)
|
|
{
|
|
return EnsureSolution(entity, name, maxVol, null, out _, out solution);
|
|
}
|
|
|
|
public bool EnsureSolution(
|
|
Entity<MetaDataComponent?> entity,
|
|
string name,
|
|
out bool existed,
|
|
[NotNullWhen(true)]out Solution? solution,
|
|
FixedPoint2 maxVol = default)
|
|
{
|
|
return EnsureSolution(entity, name, maxVol, null, out existed, out solution);
|
|
}
|
|
|
|
public bool EnsureSolution(
|
|
Entity<MetaDataComponent?> entity,
|
|
string name,
|
|
FixedPoint2 maxVol,
|
|
Solution? prototype,
|
|
out bool existed,
|
|
[NotNullWhen(true)] out Solution? solution)
|
|
{
|
|
solution = null;
|
|
existed = false;
|
|
|
|
var (uid, meta) = entity;
|
|
if (!Resolve(uid, ref meta))
|
|
throw new InvalidOperationException("Attempted to ensure solution on invalid entity.");
|
|
var manager = EnsureComp<SolutionContainerManagerComponent>(uid);
|
|
if (meta.EntityLifeStage >= EntityLifeStage.MapInitialized)
|
|
{
|
|
EnsureSolutionEntity((uid, manager), name, out existed,
|
|
out var solEnt, maxVol, prototype);
|
|
solution = solEnt!.Value.Comp.Solution;
|
|
return true;
|
|
}
|
|
solution = EnsureSolutionPrototype((uid, manager), name, maxVol, prototype, out existed);
|
|
return true;
|
|
}
|
|
|
|
public void EnsureAllSolutions(Entity<SolutionContainerManagerComponent> entity)
|
|
{
|
|
if (NetManager.IsClient)
|
|
return;
|
|
|
|
if (entity.Comp.Solutions is not { } prototypes)
|
|
return;
|
|
|
|
foreach (var (name, prototype) in prototypes)
|
|
{
|
|
EnsureSolutionEntity((entity.Owner, entity.Comp), name, out _, out _, prototype.MaxVolume, prototype);
|
|
}
|
|
|
|
entity.Comp.Solutions = null;
|
|
Dirty(entity);
|
|
}
|
|
|
|
public bool EnsureSolutionEntity(
|
|
Entity<SolutionContainerManagerComponent?> entity,
|
|
string name,
|
|
[NotNullWhen(true)] out Entity<SolutionComponent>? solutionEntity,
|
|
FixedPoint2 maxVol = default) =>
|
|
EnsureSolutionEntity(entity, name, out _, out solutionEntity, maxVol);
|
|
|
|
public bool EnsureSolutionEntity(
|
|
Entity<SolutionContainerManagerComponent?> entity,
|
|
string name,
|
|
out bool existed,
|
|
[NotNullWhen(true)] out Entity<SolutionComponent>? solutionEntity,
|
|
FixedPoint2 maxVol = default,
|
|
Solution? prototype = null
|
|
)
|
|
{
|
|
existed = true;
|
|
solutionEntity = null;
|
|
|
|
var (uid, container) = entity;
|
|
|
|
var solutionSlot = ContainerSystem.EnsureContainer<ContainerSlot>(uid, $"solution@{name}", out existed);
|
|
if (!Resolve(uid, ref container, logMissing: false))
|
|
{
|
|
existed = false;
|
|
container = AddComp<SolutionContainerManagerComponent>(uid);
|
|
container.Containers.Add(name);
|
|
if (NetManager.IsClient)
|
|
return false;
|
|
}
|
|
else if (!existed)
|
|
{
|
|
container.Containers.Add(name);
|
|
Dirty(uid, container);
|
|
}
|
|
|
|
var needsInit = false;
|
|
SolutionComponent solutionComp;
|
|
if (solutionSlot.ContainedEntity is not { } solutionId)
|
|
{
|
|
if (NetManager.IsClient)
|
|
return false;
|
|
prototype ??= new() { MaxVolume = maxVol };
|
|
prototype.Name = name;
|
|
(solutionId, solutionComp, _) = SpawnSolutionUninitialized(solutionSlot, name, maxVol, prototype);
|
|
existed = false;
|
|
needsInit = true;
|
|
Dirty(uid, container);
|
|
}
|
|
else
|
|
{
|
|
solutionComp = Comp<SolutionComponent>(solutionId);
|
|
DebugTools.Assert(TryComp(solutionId, out ContainedSolutionComponent? relation) && relation.Container == uid && relation.ContainerName == name);
|
|
DebugTools.Assert(solutionComp.Solution.Name == name);
|
|
|
|
var solution = solutionComp.Solution;
|
|
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
|
|
|
|
// Depending on MapInitEvent order some systems can ensure solution empty solutions and conflict with the prototype solutions.
|
|
// We want the reagents from the prototype to exist even if something else already created the solution.
|
|
if (prototype is { Volume.Value: > 0 })
|
|
solution.AddSolution(prototype, PrototypeManager);
|
|
|
|
Dirty(solutionId, solutionComp);
|
|
}
|
|
|
|
if (needsInit)
|
|
EntityManager.InitializeAndStartEntity(solutionId, Transform(solutionId).MapID);
|
|
solutionEntity = (solutionId, solutionComp);
|
|
return true;
|
|
}
|
|
|
|
private Solution EnsureSolutionPrototype(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
|
|
{
|
|
existed = true;
|
|
|
|
var (uid, container) = entity;
|
|
if (!Resolve(uid, ref container, logMissing: false))
|
|
{
|
|
container = AddComp<SolutionContainerManagerComponent>(uid);
|
|
existed = false;
|
|
}
|
|
|
|
if (container.Solutions is null)
|
|
container.Solutions = new(SolutionContainerManagerComponent.DefaultCapacity);
|
|
|
|
if (!container.Solutions.TryGetValue(name, out var solution))
|
|
{
|
|
solution = prototype ?? new() { Name = name, MaxVolume = maxVol };
|
|
container.Solutions.Add(name, solution);
|
|
existed = false;
|
|
}
|
|
else
|
|
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
|
|
|
|
Dirty(uid, container);
|
|
return solution;
|
|
}
|
|
|
|
private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 maxVol, Solution prototype)
|
|
{
|
|
var coords = new EntityCoordinates(container.Owner, Vector2.Zero);
|
|
var uid = EntityManager.CreateEntityUninitialized(null, coords, null);
|
|
|
|
var solution = new SolutionComponent() { Solution = prototype };
|
|
AddComp(uid, solution);
|
|
|
|
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
|
|
AddComp(uid, relation);
|
|
|
|
MetaDataSys.SetEntityName(uid, $"solution - {name}");
|
|
ContainerSystem.Insert(uid, container, force: true);
|
|
|
|
return (uid, solution, relation);
|
|
}
|
|
|
|
public void AdjustDissolvedReagent(
|
|
Entity<SolutionComponent> dissolvedSolution,
|
|
FixedPoint2 volume,
|
|
ReagentId reagent,
|
|
float concentrationChange)
|
|
{
|
|
if (concentrationChange == 0)
|
|
return;
|
|
var dissolvedSol = dissolvedSolution.Comp.Solution;
|
|
var amtChange =
|
|
GetReagentQuantityFromConcentration(dissolvedSolution, volume, MathF.Abs(concentrationChange));
|
|
if (concentrationChange > 0)
|
|
{
|
|
dissolvedSol.AddReagent(reagent, amtChange);
|
|
}
|
|
else
|
|
{
|
|
dissolvedSol.RemoveReagent(reagent,amtChange);
|
|
}
|
|
UpdateChemicals(dissolvedSolution);
|
|
}
|
|
|
|
public FixedPoint2 GetReagentQuantityFromConcentration(Entity<SolutionComponent> dissolvedSolution,
|
|
FixedPoint2 volume,float concentration)
|
|
{
|
|
var dissolvedSol = dissolvedSolution.Comp.Solution;
|
|
if (volume == 0
|
|
|| dissolvedSol.Volume == 0)
|
|
return 0;
|
|
return concentration * volume;
|
|
}
|
|
|
|
public float GetReagentConcentration(Entity<SolutionComponent> dissolvedSolution,
|
|
FixedPoint2 volume, ReagentId dissolvedReagent)
|
|
{
|
|
var dissolvedSol = dissolvedSolution.Comp.Solution;
|
|
if (volume == 0
|
|
|| dissolvedSol.Volume == 0
|
|
|| !dissolvedSol.TryGetReagentQuantity(dissolvedReagent, out var dissolvedVol))
|
|
return 0;
|
|
return (float)dissolvedVol / volume.Float();
|
|
}
|
|
|
|
public FixedPoint2 ClampReagentAmountByConcentration(
|
|
Entity<SolutionComponent> dissolvedSolution,
|
|
FixedPoint2 volume,
|
|
ReagentId dissolvedReagent,
|
|
FixedPoint2 dissolvedReagentAmount,
|
|
float maxConcentration = 1f)
|
|
{
|
|
var dissolvedSol = dissolvedSolution.Comp.Solution;
|
|
if (volume == 0
|
|
|| dissolvedSol.Volume == 0
|
|
|| !dissolvedSol.TryGetReagentQuantity(dissolvedReagent, out var dissolvedVol))
|
|
return 0;
|
|
volume *= maxConcentration;
|
|
dissolvedVol += dissolvedReagentAmount;
|
|
var overflow = volume - dissolvedVol;
|
|
if (overflow < 0)
|
|
dissolvedReagentAmount += overflow;
|
|
return dissolvedReagentAmount;
|
|
}
|
|
}
|