--- /dev/null
+using Content.Shared.Chemistry.EntitySystems;
+
+namespace Content.Client.Chemistry.EntitySystems;
+
+/// <inheritdoc/>
+public sealed class SolutionContainerMixerSystem : SharedSolutionContainerMixerSystem
+{
+
+}
--- /dev/null
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+
+namespace Content.Server.Chemistry.EntitySystems;
+
+/// <inheritdoc/>
+public sealed class SolutionContainerMixerSystem : SharedSolutionContainerMixerSystem
+{
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<SolutionContainerMixerComponent, PowerChangedEvent>(OnPowerChanged);
+ }
+
+ private void OnPowerChanged(Entity<SolutionContainerMixerComponent> ent, ref PowerChangedEvent args)
+ {
+ if (!args.Powered)
+ StopMix(ent);
+ }
+
+ protected override bool HasPower(Entity<SolutionContainerMixerComponent> entity)
+ {
+ return this.IsPowered(entity, EntityManager);
+ }
+}
--- /dev/null
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reaction;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Chemistry.Components;
+
+/// <summary>
+/// This is used for an entity that uses <see cref="ReactionMixerComponent"/> to mix any container with a solution after a period of time.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedSolutionContainerMixerSystem))]
+public sealed partial class SolutionContainerMixerComponent : Component
+{
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public string ContainerId = "mixer";
+
+ [DataField, AutoNetworkedField]
+ public bool Mixing;
+
+ /// <summary>
+ /// How long it takes for mixing to occurs.
+ /// </summary>
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public TimeSpan MixDuration;
+
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public TimeSpan MixTimeEnd;
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? MixingSound;
+
+ [DataField]
+ public Entity<AudioComponent>? MixingSoundEntity;
+}
+
+[Serializable, NetSerializable]
+public enum SolutionContainerMixerVisuals : byte
+{
+ Mixing
+}
--- /dev/null
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reaction;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Network;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Chemistry.EntitySystems;
+
+/// <summary>
+/// This handles <see cref="SolutionContainerMixerComponent"/>
+/// </summary>
+public abstract class SharedSolutionContainerMixerSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly ChemicalReactionSystem _chemicalReaction = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SolutionContainerSystem _solution = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<SolutionContainerMixerComponent, ActivateInWorldEvent>(OnActivateInWorld);
+ SubscribeLocalEvent<SolutionContainerMixerComponent, ContainerIsRemovingAttemptEvent>(OnRemoveAttempt);
+ }
+
+ private void OnActivateInWorld(Entity<SolutionContainerMixerComponent> entity, ref ActivateInWorldEvent args)
+ {
+ TryStartMix(entity, args.User);
+ }
+
+ private void OnRemoveAttempt(Entity<SolutionContainerMixerComponent> ent, ref ContainerIsRemovingAttemptEvent args)
+ {
+ if (args.Container.ID == ent.Comp.ContainerId && ent.Comp.Mixing)
+ args.Cancel();
+ }
+
+ protected virtual bool HasPower(Entity<SolutionContainerMixerComponent> entity)
+ {
+ return true;
+ }
+
+ public void TryStartMix(Entity<SolutionContainerMixerComponent> entity, EntityUid? user)
+ {
+ var (uid, comp) = entity;
+ if (comp.Mixing)
+ return;
+
+ if (!HasPower(entity))
+ {
+ if (user != null)
+ _popup.PopupClient(Loc.GetString("solution-container-mixer-no-power"), entity, user.Value);
+ return;
+ }
+
+ if (!_container.TryGetContainer(uid, comp.ContainerId, out var container) || container.Count == 0)
+ {
+ if (user != null)
+ _popup.PopupClient(Loc.GetString("solution-container-mixer-popup-nothing-to-mix"), entity, user.Value);
+ return;
+ }
+
+ comp.Mixing = true;
+ if (_net.IsServer)
+ comp.MixingSoundEntity = _audio.PlayPvs(comp.MixingSound, entity, comp.MixingSound?.Params.WithLoop(true));
+ comp.MixTimeEnd = _timing.CurTime + comp.MixDuration;
+ _appearance.SetData(entity, SolutionContainerMixerVisuals.Mixing, true);
+ Dirty(uid, comp);
+ }
+
+ public void StopMix(Entity<SolutionContainerMixerComponent> entity)
+ {
+ var (uid, comp) = entity;
+ if (!comp.Mixing)
+ return;
+ _audio.Stop(comp.MixingSoundEntity);
+ _appearance.SetData(entity, SolutionContainerMixerVisuals.Mixing, false);
+ comp.Mixing = false;
+ comp.MixingSoundEntity = null;
+ Dirty(uid, comp);
+ }
+
+ public void FinishMix(Entity<SolutionContainerMixerComponent> entity)
+ {
+ var (uid, comp) = entity;
+ if (!comp.Mixing)
+ return;
+ StopMix(entity);
+
+ if (!TryComp<ReactionMixerComponent>(entity, out var reactionMixer)
+ || !_container.TryGetContainer(uid, comp.ContainerId, out var container))
+ return;
+
+ foreach (var ent in container.ContainedEntities)
+ {
+ if (!_solution.TryGetFitsInDispenser(ent, out var solution))
+ continue;
+
+ _chemicalReaction.FullyReactSolution(solution, ent, solution.MaxVolume, reactionMixer);
+ }
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator<SolutionContainerMixerComponent>();
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (!comp.Mixing)
+ continue;
+
+ if (_timing.CurTime < comp.MixTimeEnd)
+ continue;
+
+ FinishMix((uid, comp));
+ }
+ }
+}
{
lowestUnitReactions = FixedPoint2.Zero;
return false;
- } else if(solution.Temperature > reaction.MaximumTemperature)
+ }
+ if (solution.Temperature > reaction.MaximumTemperature)
{
lowestUnitReactions = FixedPoint2.Zero;
return false;
}
var attempt = new ReactionAttemptEvent(reaction, solution);
- RaiseLocalEvent(owner, attempt, false);
+ RaiseLocalEvent(owner, attempt);
if (attempt.Cancelled)
{
lowestUnitReactions = FixedPoint2.Zero;
--- /dev/null
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Chemistry.Reaction;
+
+/// <summary>
+/// This is a prototype for a method of chemical mixing, to be used by <see cref="ReactionMixerComponent"/>
+/// </summary>
+[Prototype("mixingCategory")]
+public sealed partial class MixingCategoryPrototype : IPrototype
+{
+ /// <inheritdoc/>
+ [IdDataField]
+ public string ID { get; } = default!;
+}
using Content.Shared.Chemistry.Components;
+using Robust.Shared.Prototypes;
namespace Content.Shared.Chemistry.Reaction;
/// </summary>
[ViewVariables]
[DataField]
- public List<string> ReactionTypes = default!;
+ public List<ProtoId<MixingCategoryPrototype>> ReactionTypes = default!;
/// <summary>
/// A string which identifies the string to be sent when successfully mixing a solution
/// The required mixing categories for an entity to mix the solution with for the reaction to occur
/// </summary>
[DataField("requiredMixerCategories")]
- public List<string>? MixingCategories = null;
+ public List<ProtoId<MixingCategoryPrototype>>? MixingCategories;
/// <summary>
/// Reagents created when the reaction occurs.
copyright: "Created by dotY21."
source: "https://freesound.org/people/dotY21/sounds/330726/"
+- files: ["buzz_loop.ogg"]
+ license: "CC0-1.0"
+ copyright: "Created by Duasun, converted to OGG by EmoGarbage404 (github)."
+ source: "https://freesound.org/people/Duasun/sounds/712127/"
+
+- files: ["spinning.ogg"]
+ license: "CC0-1.0"
+ copyright: "Created by MrLindstrom, modified and converted to OGG by EmoGarbage404 (github)."
+ source: "https://freesound.org/people/MrLindstrom/sounds/543964/"
+
- files: ["vending_restock_start.ogg"]
license: "CC0-1.0"
copyright: "https://freesound.org/people/Defaultv/"
--- /dev/null
+solution-container-mixer-activate = Activate
+solution-container-mixer-no-power = No power!
+solution-container-mixer-popup-nothing-to-mix = Nothing inside!
--- /dev/null
+- type: mixingCategory
+ id: Centrifuge
+
+- type: mixingCategory
+ id: Electrolysis
+
+- type: mixingCategory
+ id: Holy
Cable: 3
Steel: 2
+- type: entity
+ id: ElectrolysisUnitMachineCircuitboard
+ parent: BaseMachineCircuitboard
+ name: electrolysis unit machine board
+ description: A machine printed circuit board for an electrolysis unit.
+ components:
+ - type: Sprite
+ state: medical
+ - type: MachineBoard
+ prototype: MachineElectrolysisUnit
+ requirements:
+ Capacitor: 2
+ materialRequirements:
+ Cable: 1
+
+- type: entity
+ id: CentrifugeMachineCircuitboard
+ parent: BaseMachineCircuitboard
+ name: centrifuge machine board
+ description: A machine printed circuit board for a centrifuge.
+ components:
+ - type: Sprite
+ state: medical
+ - type: MachineBoard
+ prototype: MachineCentrifuge
+ requirements:
+ Manipulator: 1
+ materialRequirements:
+ Steel: 1
+
- type: entity
id: MaterialReclaimerMachineCircuitboard
parent: BaseMachineCircuitboard
--- /dev/null
+- type: entity
+ id: BaseTabletopChemicalMachine
+ parent: [ BaseMachinePowered, ConstructibleMachine ]
+ abstract: true
+ components:
+ - type: Transform
+ anchored: true
+ - type: SolutionContainerMixer
+ - type: ReactionMixer
+ - type: Sprite
+ drawdepth: SmallObjects
+ snapCardinals: true
+ - type: Appearance
+ - type: Physics
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.17,0,0.20,0.4"
+ mask:
+ - TabletopMachineMask
+ layer:
+ - TabletopMachineLayer
+ - type: ItemSlots
+ slots:
+ mixer:
+ whitelist:
+ components:
+ - FitsInDispenser
+ - type: Machine
+ - type: Wires
+ layoutId: chem
+ - type: WiresPanel
+ - type: WiresVisuals
+ - type: ContainerContainer
+ containers:
+ mixer: !type:ContainerSlot
+ machine_board: !type:Container
+ machine_parts: !type:Container
+
+- type: entity
+ id: MachineElectrolysisUnit
+ parent: BaseTabletopChemicalMachine
+ name: electrolysis unit
+ description: The latest in medicinal electrocution technology.
+ components:
+ - type: SolutionContainerMixer
+ mixDuration: 5
+ mixingSound:
+ path: /Audio/Machines/buzz_loop.ogg
+ params:
+ volume: -5
+ - type: ReactionMixer
+ reactionTypes:
+ - Electrolysis
+ - type: Sprite
+ sprite: Structures/Machines/Medical/electrolysis.rsi
+ offset: "0.0,0.4"
+ layers:
+ - state: base
+ - state: panel
+ map: ["enum.WiresVisualLayers.MaintenancePanel"]
+ visible: false
+ - state: unshaded
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ shader: unshaded
+ - type: GenericVisualizer
+ visuals:
+ enum.SolutionContainerMixerVisuals.Mixing:
+ enum.PowerDeviceVisualLayers.Powered:
+ True: {state: "spinning"}
+ False: {state: "unshaded"}
+ enum.PowerDeviceVisuals.Powered:
+ enum.PowerDeviceVisualLayers.Powered:
+ True: { visible: True }
+ False: { visible: False }
+ - type: Machine
+ board: ElectrolysisUnitMachineCircuitboard
+
+- type: entity
+ id: MachineCentrifuge
+ parent: BaseTabletopChemicalMachine
+ name: tabletop centrifuge
+ description: Around and around it goes...
+ components:
+ - type: SolutionContainerMixer
+ mixDuration: 10
+ mixingSound:
+ path: /Audio/Machines/spinning.ogg
+ params:
+ volume: -4
+ - type: ReactionMixer
+ reactionTypes:
+ - Centrifuge
+ - type: Sprite
+ sprite: Structures/Machines/Medical/centrifuge.rsi
+ offset: "0.0,0.4"
+ layers:
+ - state: base
+ map: ["beyblade"]
+ - state: panel
+ map: ["enum.WiresVisualLayers.MaintenancePanel"]
+ visible: false
+ - state: unshaded
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ shader: unshaded
+ - type: GenericVisualizer
+ visuals:
+ enum.SolutionContainerMixerVisuals.Mixing:
+ beyblade:
+ True: {state: "base-spinning"}
+ False: {state: "base"}
+ enum.PowerDeviceVisuals.Powered:
+ enum.PowerDeviceVisualLayers.Powered:
+ True: { visible: True }
+ False: { visible: False }
+ - type: Machine
+ board: CentrifugeMachineCircuitboard
idleState: icon
runningState: building
staticRecipes:
+ - ElectrolysisUnitMachineCircuitboard
+ - CentrifugeMachineCircuitboard
- CondenserMachineCircuitBoard
dynamicRecipes:
- ThermomachineFreezerMachineCircuitBoard
Glass: 900
Gold: 100
+- type: latheRecipe
+ id: ElectrolysisUnitMachineCircuitboard
+ result: ElectrolysisUnitMachineCircuitboard
+ completetime: 4
+ materials:
+ Steel: 100
+ Glass: 900
+
+- type: latheRecipe
+ id: CentrifugeMachineCircuitboard
+ result: CentrifugeMachineCircuitboard
+ completetime: 4
+ materials:
+ Steel: 100
+ Glass: 900
+
- type: latheRecipe
id: MaterialReclaimerMachineCircuitboard
result: MaterialReclaimerMachineCircuitboard
--- /dev/null
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Adapted from CEV-Eris by EmoGarbage404 (github) for Space Station 14.",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "base"
+ },
+ {
+ "name": "base-spinning",
+ "delays":
+ [
+ [
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "panel"
+ },
+ {
+ "name": "unshaded"
+ }
+ ]
+}
--- /dev/null
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Adapted from CEV-Eris by EmoGarbage404 (github) for Space Station 14.",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "base"
+ },
+ {
+ "name": "spinning",
+ "delays":
+ [
+ [
+ 0.25,
+ 0.25,
+ 0.25,
+ 0.25
+ ]
+ ]
+ },
+ {
+ "name": "panel"
+ },
+ {
+ "name": "unshaded"
+ }
+ ]
+}