Re-organize all projects (#4166)

This commit is contained in:
DrSmugleaf
2021-06-09 22:19:39 +02:00
committed by GitHub
parent 9f50e4061b
commit ff1a2d97ea
1773 changed files with 5258 additions and 5508 deletions

View File

@@ -0,0 +1,442 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Solution;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Random.Helpers;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
/// <summary>
/// Contains all the server-side logic for chem masters. See also <see cref="SharedChemMasterComponent"/>.
/// This includes initializing the component based on prototype data, and sending and receiving messages from the client.
/// Messages sent to the client are used to update update the user interface for a component instance.
/// Messages sent from the client are used to handle ui button presses.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IInteractUsing))]
public class ChemMasterComponent : SharedChemMasterComponent, IActivate, IInteractUsing, ISolutionChange
{
[ViewVariables] private ContainerSlot _beakerContainer = default!;
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private bool _bufferModeTransfer = true;
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private readonly Solution BufferSolution = new();
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ChemMasterUiKey.Key);
/// <summary>
/// Called once per instance of this component. Gets references to any other components needed
/// by this component and initializes it's UI and other data.
/// </summary>
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
_beakerContainer =
ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-reagentContainerContainer");
//BufferSolution = Owner.BufferSolution
BufferSolution.RemoveAllSolution();
UpdateUserInterface();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
OnPowerChanged(powerChanged);
break;
}
}
private void OnPowerChanged(PowerChangedMessage e)
{
UpdateUserInterface();
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
var msg = (UiActionMessage) obj.Message;
var needsPower = msg.action switch
{
UiAction.Eject => false,
_ => true,
};
if (!PlayerCanUseChemMaster(obj.Session.AttachedEntity, needsPower))
return;
switch (msg.action)
{
case UiAction.Eject:
TryEject(obj.Session.AttachedEntity);
break;
case UiAction.ChemButton:
TransferReagent(msg.id, msg.amount, msg.isBuffer);
break;
case UiAction.Transfer:
_bufferModeTransfer = true;
UpdateUserInterface();
break;
case UiAction.Discard:
_bufferModeTransfer = false;
UpdateUserInterface();
break;
case UiAction.CreatePills:
case UiAction.CreateBottles:
TryCreatePackage(obj.Session.AttachedEntity, msg.action, msg.pillAmount, msg.bottleAmount);
break;
default:
throw new ArgumentOutOfRangeException();
}
ClickSound();
}
/// <summary>
/// Checks whether the player entity is able to use the chem master.
/// </summary>
/// <param name="playerEntity">The player entity.</param>
/// <returns>Returns true if the entity can use the chem master, and false if it cannot.</returns>
private bool PlayerCanUseChemMaster(IEntity? playerEntity, bool needsPower = true)
{
//Need player entity to check if they are still able to use the chem master
if (playerEntity == null)
return false;
//Check if player can interact in their current state
if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity))
return false;
//Check if device is powered
if (needsPower && !Powered)
return false;
return true;
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="SharedChemMasterComponent.ChemMasterBoundUserInterfaceState"/></returns>
private ChemMasterBoundUserInterfaceState GetUserInterfaceState()
{
var beaker = _beakerContainer.ContainedEntity;
if (beaker == null)
{
return new ChemMasterBoundUserInterfaceState(Powered, false, ReagentUnit.New(0), ReagentUnit.New(0),
"", Owner.Name, new List<Solution.ReagentQuantity>(), BufferSolution.Contents, _bufferModeTransfer, BufferSolution.TotalVolume);
}
var solution = beaker.GetComponent<SolutionContainerComponent>();
return new ChemMasterBoundUserInterfaceState(Powered, true, solution.CurrentVolume, solution.MaxVolume,
beaker.Name, Owner.Name, solution.ReagentList, BufferSolution.Contents, _bufferModeTransfer, BufferSolution.TotalVolume);
}
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
UserInterface?.SetState(state);
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, eject it.
/// Tries to eject into user's hands first, then ejects onto chem master if both hands are full.
/// </summary>
private void TryEject(IEntity user)
{
if (!HasBeaker)
return;
var beaker = _beakerContainer.ContainedEntity;
if(beaker is null)
return;
_beakerContainer.Remove(beaker);
UpdateUserInterface();
if(!user.TryGetComponent<HandsComponent>(out var hands) || !beaker.TryGetComponent<ItemComponent>(out var item))
return;
if (hands.CanPutInHand(item))
hands.PutInHand(item);
}
private void TransferReagent(string id, ReagentUnit amount, bool isBuffer)
{
if (!HasBeaker && _bufferModeTransfer) return;
var beaker = _beakerContainer.ContainedEntity;
if(beaker is null)
return;
var beakerSolution = beaker.GetComponent<SolutionContainerComponent>();
if (isBuffer)
{
foreach (var reagent in BufferSolution.Contents)
{
if (reagent.ReagentId == id)
{
ReagentUnit actualAmount;
if (amount == ReagentUnit.New(-1)) //amount is ReagentUnit.New(-1) when the client sends a message requesting to remove all solution from the container
{
actualAmount = ReagentUnit.Min(reagent.Quantity, beakerSolution.EmptyVolume);
}
else
{
actualAmount = ReagentUnit.Min(reagent.Quantity, amount, beakerSolution.EmptyVolume);
}
BufferSolution.RemoveReagent(id, actualAmount);
if (_bufferModeTransfer)
{
beakerSolution.TryAddReagent(id, actualAmount, out var _);
// beakerSolution.Solution.AddReagent(id, actualAmount);
}
break;
}
}
}
else
{
foreach (var reagent in beakerSolution.Solution.Contents)
{
if (reagent.ReagentId == id)
{
ReagentUnit actualAmount;
if (amount == ReagentUnit.New(-1))
{
actualAmount = reagent.Quantity;
}
else
{
actualAmount = ReagentUnit.Min(reagent.Quantity, amount);
}
beakerSolution.TryRemoveReagent(id, actualAmount);
BufferSolution.AddReagent(id, actualAmount);
break;
}
}
}
UpdateUserInterface();
}
private void TryCreatePackage(IEntity user, UiAction action, int pillAmount, int bottleAmount)
{
if (BufferSolution.TotalVolume == 0)
return;
if (action == UiAction.CreateBottles)
{
var individualVolume = BufferSolution.TotalVolume / ReagentUnit.New(bottleAmount);
if (individualVolume < ReagentUnit.New(1))
return;
var actualVolume = ReagentUnit.Min(individualVolume, ReagentUnit.New(30));
for (int i = 0; i < bottleAmount; i++)
{
var bottle = Owner.EntityManager.SpawnEntity("ChemistryEmptyBottle01", Owner.Transform.Coordinates);
var bufferSolution = BufferSolution.SplitSolution(actualVolume);
bottle.TryGetComponent<SolutionContainerComponent>(out var bottleSolution);
bottleSolution?.TryAddSolution(bufferSolution);
//Try to give them the bottle
if (user.TryGetComponent<HandsComponent>(out var hands) &&
bottle.TryGetComponent<ItemComponent>(out var item))
{
if (hands.CanPutInHand(item))
{
hands.PutInHand(item);
continue;
}
}
//Put it on the floor
bottle.Transform.Coordinates = user.Transform.Coordinates;
//Give it an offset
bottle.RandomOffset(0.2f);
}
}
else //Pills
{
var individualVolume = BufferSolution.TotalVolume / ReagentUnit.New(pillAmount);
if (individualVolume < ReagentUnit.New(1))
return;
var actualVolume = ReagentUnit.Min(individualVolume, ReagentUnit.New(50));
for (int i = 0; i < pillAmount; i++)
{
var pill = Owner.EntityManager.SpawnEntity("pill", Owner.Transform.Coordinates);
var bufferSolution = BufferSolution.SplitSolution(actualVolume);
pill.TryGetComponent<SolutionContainerComponent>(out var pillSolution);
pillSolution?.TryAddSolution(bufferSolution);
//Try to give them the bottle
if (user.TryGetComponent<HandsComponent>(out var hands) &&
pill.TryGetComponent<ItemComponent>(out var item))
{
if (hands.CanPutInHand(item))
{
hands.PutInHand(item);
continue;
}
}
//Put it on the floor
pill.Transform.Coordinates = user.Transform.Coordinates;
//Give it an offset
pill.RandomOffset(0.2f);
}
}
UpdateUserInterface();
}
/// <summary>
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
void IActivate.Activate(ActivateEventArgs args)
{
if (!args.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = hands.GetActiveHand?.Owner;
if (activeHandEntity == null)
{
UserInterface?.Open(actor.PlayerSession);
}
}
/// <summary>
/// Called when you click the owner entity with something in your active hand. If the entity in your hand
/// contains a <see cref="SolutionContainerComponent"/>, if you have hands, and if the chem master doesn't already
/// hold a container, it will be added to the chem master.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
/// <returns></returns>
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs args)
{
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands!"));
return true;
}
if (hands.GetActiveHand == null)
{
Owner.PopupMessage(args.User, Loc.GetString("You have nothing in your hand!"));
return false;
}
var activeHandEntity = hands.GetActiveHand.Owner;
if (activeHandEntity.TryGetComponent<SolutionContainerComponent>(out var solution))
{
if (HasBeaker)
{
Owner.PopupMessage(args.User, Loc.GetString("This ChemMaster already has a container in it."));
}
else if (!solution.CanUseWithChemDispenser)
{
//If it can't fit in the chem master, don't put it in. For example, buckets and mop buckets can't fit.
Owner.PopupMessage(args.User, Loc.GetString("The {0:theName} is too large for the ChemMaster!", activeHandEntity));
}
else
{
_beakerContainer.Insert(activeHandEntity);
UpdateUserInterface();
}
}
else
{
Owner.PopupMessage(args.User, Loc.GetString("You can't put {0:theName} in the ChemMaster!", activeHandEntity));
}
return true;
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) => UpdateUserInterface();
private void ClickSound()
{
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
[Verb]
public sealed class EjectBeakerVerb : Verb<ChemMasterComponent>
{
protected override void GetData(IEntity user, ChemMasterComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Eject Beaker");
data.Visibility = component.HasBeaker ? VerbVisibility.Visible : VerbVisibility.Invisible;
}
protected override void Activate(IEntity user, ChemMasterComponent component)
{
component.TryEject(user);
}
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using Content.Server.Body.Circulatory;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
public class ChemicalInjectionProjectileComponent : Component, IStartCollide
{
public override string Name => "ChemicalInjectionProjectile";
[ViewVariables]
private SolutionContainerComponent _solutionContainer = default!;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("transferAmount")]
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(1);
[ViewVariables(VVAccess.ReadWrite)]
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
[DataField("transferEfficiency")]
private float _transferEfficiency = 1f;
public override void Initialize()
{
base.Initialize();
_solutionContainer = Owner.EnsureComponent<SolutionContainerComponent>();
}
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
{
if (!otherFixture.Body.Owner.TryGetComponent<BloodstreamComponent>(out var bloodstream))
return;
var solution = _solutionContainer.Solution;
var solRemoved = solution.SplitSolution(TransferAmount);
var solRemovedVol = solRemoved.TotalVolume;
var solToInject = solRemoved.SplitSolution(solRemovedVol * TransferEfficiency);
bloodstream.TryTransferSolution(solToInject);
}
}
}

View File

@@ -0,0 +1,82 @@
#nullable enable
using Content.Server.Body.Circulatory;
using Content.Server.Inventory.Components;
using Content.Server.Items;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Foam;
using Content.Shared.Inventory;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
[ComponentReference(typeof(SolutionAreaEffectComponent))]
public class FoamSolutionAreaEffectComponent : SolutionAreaEffectComponent
{
public override string Name => "FoamSolutionAreaEffect";
[DataField("foamedMetalPrototype")] private string? _foamedMetalPrototype;
protected override void UpdateVisuals()
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
SolutionContainerComponent != null)
{
appearance.SetData(FoamVisuals.Color, SolutionContainerComponent.Color.WithAlpha(0.80f));
}
}
protected override void ReactWithEntity(IEntity entity, double solutionFraction)
{
if (SolutionContainerComponent == null)
return;
if (!entity.TryGetComponent(out BloodstreamComponent? bloodstream))
return;
// TODO: Add a permeability property to clothing
// For now it just adds to protection for each clothing equipped
var protection = 0f;
if (entity.TryGetComponent(out InventoryComponent? inventory))
{
foreach (var slot in inventory.Slots)
{
if (slot == EquipmentSlotDefines.Slots.BACKPACK ||
slot == EquipmentSlotDefines.Slots.POCKET1 ||
slot == EquipmentSlotDefines.Slots.POCKET2 ||
slot == EquipmentSlotDefines.Slots.IDCARD)
continue;
if (inventory.TryGetSlotItem(slot, out ItemComponent _))
protection += 0.025f;
}
}
var cloneSolution = SolutionContainerComponent.Solution.Clone();
var transferAmount = ReagentUnit.Min(cloneSolution.TotalVolume * solutionFraction * (1 - protection), bloodstream.EmptyVolume);
var transferSolution = cloneSolution.SplitSolution(transferAmount);
bloodstream.TryTransferSolution(transferSolution);
}
protected override void OnKill()
{
if (Owner.Deleted)
return;
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(FoamVisuals.State, true);
}
Owner.SpawnTimer(600, () =>
{
if (!string.IsNullOrEmpty(_foamedMetalPrototype))
{
Owner.EntityManager.SpawnEntity(_foamedMetalPrototype, Owner.Transform.Coordinates);
}
Owner.QueueDelete();
});
}
}
}

View File

@@ -0,0 +1,119 @@
using Content.Server.Interaction.Components;
using Content.Server.MobState.States;
using Content.Server.Weapon.Melee;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Notification;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
public sealed class HyposprayComponent : SharedHyposprayComponent, ISolutionChange
{
[DataField("ClumsyFailChance")]
[ViewVariables(VVAccess.ReadWrite)]
public float ClumsyFailChance { get; set; } = 0.5f;
[DataField("TransferAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(5);
[ComponentDependency] private readonly SolutionContainerComponent? _solution = default!;
public override void Initialize()
{
base.Initialize();
Dirty();
}
public bool TryDoInject(IEntity? target, IEntity user)
{
if (target == null || !EligibleEntity(target))
return false;
var msgFormat = "You inject {0:TheName}.";
if (target == user)
{
msgFormat = "You inject yourself.";
}
else if (EligibleEntity(user) && ClumsyComponent.TryRollClumsy(user, ClumsyFailChance))
{
msgFormat = "Oops! You injected yourself!";
target = user;
}
if (_solution == null || _solution.CurrentVolume == 0)
{
user.PopupMessageCursor(Loc.GetString("It's empty!"));
return true;
}
user.PopupMessage(Loc.GetString(msgFormat, target));
if (target != user)
{
target.PopupMessage(Loc.GetString("You feel a tiny prick!"));
var meleeSys = EntitySystem.Get<MeleeWeaponSystem>();
var angle = Angle.FromWorldVec(target.Transform.WorldPosition - user.Transform.WorldPosition);
meleeSys.SendLunge(angle, user);
}
SoundSystem.Play(Filter.Pvs(user), "/Audio/Items/hypospray.ogg", user);
var targetSolution = target.GetComponent<SolutionContainerComponent>();
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(TransferAmount, targetSolution.EmptyVolume);
if (realTransferAmount <= 0)
{
user.PopupMessage(user, Loc.GetString("{0:TheName} is already full!", targetSolution.Owner));
return true;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solution.SplitSolution(realTransferAmount);
if (!targetSolution.CanAddSolution(removedSolution))
{
return true;
}
removedSolution.DoEntityReaction(target, ReactionMethod.Injection);
targetSolution.TryAddSolution(removedSolution);
static bool EligibleEntity(IEntity entity)
{
// TODO: Does checking for BodyComponent make sense as a "can be hypospray'd" tag?
// In SS13 the hypospray ONLY works on mobs, NOT beakers or anything else.
return entity.HasComponent<SolutionContainerComponent>() && entity.HasComponent<MobStateComponent>();
}
return true;
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
Dirty();
}
public override ComponentState GetComponentState(ICommonSession player)
{
if (_solution == null)
return new HyposprayComponentState(ReagentUnit.Zero, ReagentUnit.Zero);
return new HyposprayComponentState(_solution.CurrentVolume, _solution.MaxVolume);
}
}
}

View File

@@ -0,0 +1,302 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Content.Server.Body.Circulatory;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Solution.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Notification;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
/// <summary>
/// Server behavior for reagent injectors and syringes. Can optionally support both
/// injection and drawing or just injection. Can inject/draw reagents from solution
/// containers, and can directly inject into a mobs bloodstream.
/// </summary>
[RegisterComponent]
public class InjectorComponent : SharedInjectorComponent, IAfterInteract, IUse, ISolutionChange
{
/// <summary>
/// Whether or not the injector is able to draw from containers or if it's a single use
/// device that can only inject.
/// </summary>
[ViewVariables]
[DataField("injectOnly")]
private bool _injectOnly;
/// <summary>
/// Amount to inject or draw on each usage. If the injector is inject only, it will
/// attempt to inject it's entire contents upon use.
/// </summary>
[ViewVariables]
[DataField("transferAmount")]
private ReagentUnit _transferAmount = ReagentUnit.New(5);
/// <summary>
/// Initial storage volume of the injector
/// </summary>
[ViewVariables]
[DataField("initialMaxVolume")]
private ReagentUnit _initialMaxVolume = ReagentUnit.New(15);
private InjectorToggleMode _toggleState;
/// <summary>
/// The state of the injector. Determines it's attack behavior. Containers must have the
/// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
/// only ever be set to Inject
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public InjectorToggleMode ToggleState
{
get => _toggleState;
set
{
_toggleState = value;
Dirty();
}
}
protected override void Startup()
{
base.Startup();
Dirty();
}
/// <summary>
/// Toggle between draw/inject state if applicable
/// </summary>
private void Toggle(IEntity user)
{
if (_injectOnly)
{
return;
}
string msg;
switch (ToggleState)
{
case InjectorToggleMode.Inject:
ToggleState = InjectorToggleMode.Draw;
msg = "Now drawing";
break;
case InjectorToggleMode.Draw:
ToggleState = InjectorToggleMode.Inject;
msg = "Now injecting";
break;
default:
throw new ArgumentOutOfRangeException();
}
Owner.PopupMessage(user, Loc.GetString(msg));
}
/// <summary>
/// Called when clicking on entities while holding in active hand
/// </summary>
/// <param name="eventArgs"></param>
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return false;
//Make sure we have the attacking entity
if (eventArgs.Target == null || !Owner.HasComponent<SolutionContainerComponent>())
{
return false;
}
var targetEntity = eventArgs.Target;
// Handle injecting/drawing for solutions
if (targetEntity.TryGetComponent<ISolutionInteractionsComponent>(out var targetSolution))
{
if (ToggleState == InjectorToggleMode.Inject)
{
if (targetSolution.CanInject)
{
TryInject(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner));
}
}
else if (ToggleState == InjectorToggleMode.Draw)
{
if (targetSolution.CanDraw)
{
TryDraw(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner));
}
}
}
// Handle injecting into bloodstream
else if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) &&
ToggleState == InjectorToggleMode.Inject)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
return true;
}
/// <summary>
/// Called when use key is pressed when held in active hand
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
Toggle(eventArgs.User);
return true;
}
private void TryInjectIntoBloodstream(BloodstreamComponent targetBloodstream, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetBloodstream.EmptyVolume);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user,
Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
if (!solution.CanAddSolution(removedSolution))
{
return;
}
// TODO: Account for partial transfer.
removedSolution.DoEntityReaction(solution.Owner, ReactionMethod.Injection);
solution.TryAddSolution(removedSolution);
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
Owner.PopupMessage(user,
Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume,
targetBloodstream.Owner));
Dirty();
AfterInject();
}
private void TryInject(ISolutionInteractionsComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.InjectSpaceAvailable);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("{0:theName} is already full!", targetSolution.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
removedSolution.DoEntityReaction(targetSolution.Owner, ReactionMethod.Injection);
targetSolution.Inject(removedSolution);
Owner.PopupMessage(user,
Loc.GetString("You transfer {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
AfterInject();
}
private void AfterInject()
{
// Automatically set syringe to draw after completely draining it.
if (Owner.GetComponent<SolutionContainerComponent>().CurrentVolume == 0)
{
ToggleState = InjectorToggleMode.Draw;
}
}
private void TryDraw(ISolutionInteractionsComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.EmptyVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.DrawAvailable);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("{0:theName} is empty!", targetSolution.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = targetSolution.Draw(realTransferAmount);
if (!solution.TryAddSolution(removedSolution))
{
return;
}
Owner.PopupMessage(user,
Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
AfterDraw();
}
private void AfterDraw()
{
// Automatically set syringe to inject after completely filling it.
if (Owner.GetComponent<SolutionContainerComponent>().EmptyVolume == 0)
{
ToggleState = InjectorToggleMode.Inject;
}
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
Dirty();
}
public override ComponentState GetComponentState(ICommonSession player)
{
Owner.TryGetComponent(out SolutionContainerComponent? solution);
var currentVolume = solution?.CurrentVolume ?? ReagentUnit.Zero;
var maxVolume = solution?.MaxVolume ?? ReagentUnit.Zero;
return new InjectorComponentState(currentVolume, maxVolume, ToggleState);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
public class MeleeChemicalInjectorComponent : Component
{
public override string Name => "MeleeChemicalInjector";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("transferAmount")]
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(1);
[ViewVariables(VVAccess.ReadWrite)]
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
[DataField("transferEfficiency")]
private float _transferEfficiency = 1f;
}
}

View File

@@ -0,0 +1,112 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Body.Behavior;
using Content.Server.Nutrition.Components;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Notification;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
public class PillComponent : FoodComponent, IUse, IAfterInteract
{
public override string Name => "Pill";
[ViewVariables]
[DataField("useSound")]
protected override string? UseSound { get; set; } = default;
[ViewVariables]
[DataField("trash")]
protected override string? TrashPrototype { get; set; } = default;
[ViewVariables]
[DataField("transferAmount")]
protected override ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(1000);
[ViewVariables]
private SolutionContainerComponent _contents = default!;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn(out _contents);
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
return TryUseFood(eventArgs.User, null);
}
// Feeding someone else
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return false;
}
TryUseFood(eventArgs.User, eventArgs.Target);
return true;
}
public override bool TryUseFood(IEntity? user, IEntity? target, UtensilComponent? utensilUsed = null)
{
if (user == null)
{
return false;
}
var trueTarget = target ?? user;
if (!trueTarget.TryGetComponent(out IBody? body) ||
!body.TryGetMechanismBehaviors<StomachBehavior>(out var stomachs))
{
return false;
}
if (!user.InRangeUnobstructed(trueTarget, popup: true))
{
return false;
}
var transferAmount = ReagentUnit.Min(TransferAmount, _contents.CurrentVolume);
var split = _contents.SplitSolution(transferAmount);
var firstStomach = stomachs.FirstOrDefault(stomach => stomach.CanTransferSolution(split));
if (firstStomach == null)
{
_contents.TryAddSolution(split);
trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!"));
return false;
}
// TODO: Account for partial transfer.
split.DoEntityReaction(trueTarget, ReactionMethod.Ingestion);
firstStomach.TryTransferSolution(split);
if (UseSound != null)
{
SoundSystem.Play(Filter.Pvs(trueTarget), UseSound, trueTarget, AudioParams.Default.WithVolume(-1f));
}
trueTarget.PopupMessage(user, Loc.GetString("You swallow the pill."));
Owner.QueueDelete();
return true;
}
}
}

View File

@@ -0,0 +1,391 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Dispenser;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Solution;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
/// <summary>
/// Contains all the server-side logic for reagent dispensers. See also <see cref="SharedReagentDispenserComponent"/>.
/// This includes initializing the component based on prototype data, and sending and receiving messages from the client.
/// Messages sent to the client are used to update update the user interface for a component instance.
/// Messages sent from the client are used to handle ui button presses.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IInteractUsing))]
public class ReagentDispenserComponent : SharedReagentDispenserComponent, IActivate, IInteractUsing, ISolutionChange
{
private static ReagentInventoryComparer _comparer = new();
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[ViewVariables] private ContainerSlot _beakerContainer = default!;
[ViewVariables] [DataField("pack")] private string _packPrototypeId = "";
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private ReagentUnit _dispenseAmount = ReagentUnit.New(10);
[UsedImplicitly] [ViewVariables] private SolutionContainerComponent? Solution => _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentDispenserUiKey.Key);
/// <summary>
/// Called once per instance of this component. Gets references to any other components needed
/// by this component and initializes it's UI and other data.
/// </summary>
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
_beakerContainer =
ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-reagentContainerContainer");
InitializeFromPrototype();
UpdateUserInterface();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
OnPowerChanged(powerChanged);
break;
}
}
/// <summary>
/// Checks to see if the <c>pack</c> defined in this components yaml prototype
/// exists. If so, it fills the reagent inventory list.
/// </summary>
private void InitializeFromPrototype()
{
if (string.IsNullOrEmpty(_packPrototypeId)) return;
if (!_prototypeManager.TryIndex(_packPrototypeId, out ReagentDispenserInventoryPrototype? packPrototype))
{
return;
}
foreach (var entry in packPrototype.Inventory)
{
Inventory.Add(new ReagentDispenserInventoryEntry(entry));
}
Inventory.Sort(_comparer);
}
private void OnPowerChanged(PowerChangedMessage e)
{
UpdateUserInterface();
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
var msg = (UiButtonPressedMessage) obj.Message;
var needsPower = msg.Button switch
{
UiButton.Eject => false,
_ => true,
};
if(!PlayerCanUseDispenser(obj.Session.AttachedEntity, needsPower))
return;
switch (msg.Button)
{
case UiButton.Eject:
TryEject(obj.Session.AttachedEntity);
break;
case UiButton.Clear:
TryClear();
break;
case UiButton.SetDispenseAmount1:
_dispenseAmount = ReagentUnit.New(1);
break;
case UiButton.SetDispenseAmount5:
_dispenseAmount = ReagentUnit.New(5);
break;
case UiButton.SetDispenseAmount10:
_dispenseAmount = ReagentUnit.New(10);
break;
case UiButton.SetDispenseAmount15:
_dispenseAmount = ReagentUnit.New(15);
break;
case UiButton.SetDispenseAmount20:
_dispenseAmount = ReagentUnit.New(20);
break;
case UiButton.SetDispenseAmount25:
_dispenseAmount = ReagentUnit.New(25);
break;
case UiButton.SetDispenseAmount30:
_dispenseAmount = ReagentUnit.New(30);
break;
case UiButton.SetDispenseAmount50:
_dispenseAmount = ReagentUnit.New(50);
break;
case UiButton.SetDispenseAmount100:
_dispenseAmount = ReagentUnit.New(100);
break;
case UiButton.Dispense:
if (HasBeaker)
{
TryDispense(msg.DispenseIndex);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
ClickSound();
}
/// <summary>
/// Checks whether the player entity is able to use the chem dispenser.
/// </summary>
/// <param name="playerEntity">The player entity.</param>
/// <returns>Returns true if the entity can use the dispenser, and false if it cannot.</returns>
private bool PlayerCanUseDispenser(IEntity? playerEntity, bool needsPower = true)
{
//Need player entity to check if they are still able to use the dispenser
if (playerEntity == null)
return false;
//Check if player can interact in their current state
if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity))
return false;
//Check if device is powered
if (needsPower && !Powered)
return false;
return true;
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="SharedReagentDispenserComponent.ReagentDispenserBoundUserInterfaceState"/></returns>
private ReagentDispenserBoundUserInterfaceState GetUserInterfaceState()
{
var beaker = _beakerContainer.ContainedEntity;
if (beaker == null)
{
return new ReagentDispenserBoundUserInterfaceState(Powered, false, ReagentUnit.New(0), ReagentUnit.New(0),
string.Empty, Inventory, Owner.Name, null, _dispenseAmount);
}
var solution = beaker.GetComponent<SolutionContainerComponent>();
return new ReagentDispenserBoundUserInterfaceState(Powered, true, solution.CurrentVolume, solution.MaxVolume,
beaker.Name, Inventory, Owner.Name, solution.ReagentList.ToList(), _dispenseAmount);
}
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
UserInterface?.SetState(state);
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, eject it.
/// Tries to eject into user's hands first, then ejects onto dispenser if both hands are full.
/// </summary>
private void TryEject(IEntity user)
{
if (!HasBeaker)
return;
var beaker = _beakerContainer.ContainedEntity;
if(beaker is null)
return;
_beakerContainer.Remove(beaker);
UpdateUserInterface();
if(!user.TryGetComponent<HandsComponent>(out var hands) || !beaker.TryGetComponent<ItemComponent>(out var item))
return;
if (hands.CanPutInHand(item))
hands.PutInHand(item);
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, remove all of it's reagents / solutions.
/// </summary>
private void TryClear()
{
if (!HasBeaker) return;
var solution = _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
if(solution is null)
return;
solution.RemoveAllSolution();
UpdateUserInterface();
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, attempt to dispense the specified reagent to it.
/// </summary>
/// <param name="dispenseIndex">The index of the reagent in <c>Inventory</c>.</param>
private void TryDispense(int dispenseIndex)
{
if (!HasBeaker) return;
var solution = _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
if (solution is null)
return;
solution.TryAddReagent(Inventory[dispenseIndex].ID, _dispenseAmount, out _);
UpdateUserInterface();
}
/// <summary>
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
void IActivate.Activate(ActivateEventArgs args)
{
if (!args.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = hands.GetActiveHand?.Owner;
if (activeHandEntity == null)
{
UserInterface?.Open(actor.PlayerSession);
}
}
/// <summary>
/// Called when you click the owner entity with something in your active hand. If the entity in your hand
/// contains a <see cref="SolutionContainerComponent"/>, if you have hands, and if the dispenser doesn't already
/// hold a container, it will be added to the dispenser.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
/// <returns></returns>
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs args)
{
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
return true;
}
if (hands.GetActiveHand == null)
{
Owner.PopupMessage(args.User, Loc.GetString("You have nothing on your hand."));
return false;
}
var activeHandEntity = hands.GetActiveHand.Owner;
if (activeHandEntity.TryGetComponent<SolutionContainerComponent>(out var solution))
{
if (HasBeaker)
{
Owner.PopupMessage(args.User, Loc.GetString("This dispenser already has a container in it."));
}
else if ((solution.Capabilities & SolutionContainerCaps.FitsInDispenser) == 0)
{
//If it can't fit in the dispenser, don't put it in. For example, buckets and mop buckets can't fit.
Owner.PopupMessage(args.User, Loc.GetString("That can't fit in the dispenser."));
}
else
{
_beakerContainer.Insert(activeHandEntity);
UpdateUserInterface();
}
}
else
{
Owner.PopupMessage(args.User, Loc.GetString("You can't put this in the dispenser."));
}
return true;
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) => UpdateUserInterface();
private void ClickSound()
{
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
[Verb]
public sealed class EjectBeakerVerb : Verb<ReagentDispenserComponent>
{
protected override void GetData(IEntity user, ReagentDispenserComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Eject Beaker");
data.Visibility = component.HasBeaker ? VerbVisibility.Visible : VerbVisibility.Invisible;
}
protected override void Activate(IEntity user, ReagentDispenserComponent component)
{
component.TryEject(user);
}
}
private class ReagentInventoryComparer : Comparer<ReagentDispenserInventoryEntry>
{
public override int Compare(ReagentDispenserInventoryEntry x, ReagentDispenserInventoryEntry y)
{
return string.Compare(x.ID, y.ID, StringComparison.InvariantCultureIgnoreCase);
}
}
}
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
#nullable enable
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
public class ReagentTankComponent : Component
{
public override string Name => "ReagentTank";
[DataField("transferAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(10);
[DataField("tankType")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentTankType TankType { get; set; } = ReagentTankType.Unspecified;
}
public enum ReagentTankType : byte
{
Unspecified,
Fuel
}
}

View File

@@ -0,0 +1,57 @@
#nullable enable
using Content.Server.Notification;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
/// <summary>
/// Basically, monkey cubes.
/// But specifically, this component deletes the entity and spawns in a new entity when the entity is exposed to a given reagent.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(ISolutionChange))]
public class RehydratableComponent : Component, ISolutionChange
{
public override string Name => "Rehydratable";
[ViewVariables]
[DataField("catalyst")]
private string _catalystPrototype = "Water";
[ViewVariables]
[DataField("target")]
private string? _targetPrototype = default!;
private bool _expanding;
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
var solution = eventArgs.Owner.GetComponent<SolutionContainerComponent>();
if (solution.Solution.GetReagentQuantity(_catalystPrototype) > ReagentUnit.Zero)
{
Expand();
}
}
// Try not to make this public if you can help it.
private void Expand()
{
if (_expanding)
{
return;
}
_expanding = true;
Owner.PopupMessageEveryone(Loc.GetString("{0:TheName} expands!", Owner));
if (!string.IsNullOrEmpty(_targetPrototype))
{
var ent = Owner.EntityManager.SpawnEntity(_targetPrototype, Owner.Transform.Coordinates);
ent.Transform.AttachToGridOrMap();
}
Owner.Delete();
}
}
}

View File

@@ -0,0 +1,62 @@
#nullable enable
using System.Linq;
using Content.Server.Body.Circulatory;
using Content.Server.Body.Respiratory;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Smoking;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
[ComponentReference(typeof(SolutionAreaEffectComponent))]
public class SmokeSolutionAreaEffectComponent : SolutionAreaEffectComponent
{
public override string Name => "SmokeSolutionAreaEffect";
protected override void UpdateVisuals()
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
SolutionContainerComponent != null)
{
appearance.SetData(SmokeVisuals.Color, SolutionContainerComponent.Color);
}
}
protected override void ReactWithEntity(IEntity entity, double solutionFraction)
{
if (SolutionContainerComponent == null)
return;
if (!entity.TryGetComponent(out BloodstreamComponent? bloodstream))
return;
if (entity.TryGetComponent(out InternalsComponent? internals) &&
internals.AreInternalsWorking())
return;
var chemistry = EntitySystem.Get<ChemistrySystem>();
var cloneSolution = SolutionContainerComponent.Solution.Clone();
var transferAmount = ReagentUnit.Min(cloneSolution.TotalVolume * solutionFraction, bloodstream.EmptyVolume);
var transferSolution = cloneSolution.SplitSolution(transferAmount);
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
{
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
chemistry.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity.ReagentId, reagentQuantity.Quantity, transferSolution);
}
bloodstream.TryTransferSolution(transferSolution);
}
protected override void OnKill()
{
if (Owner.Deleted)
return;
Owner.Delete();
}
}
}

View File

@@ -0,0 +1,180 @@
#nullable enable
using System;
using System.Linq;
using Content.Server.Coordinates.Helpers;
using Content.Server.GameObjects.Components.Atmos;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Solution;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.Components
{
/// <summary>
/// Used to clone its owner repeatedly and group up them all so they behave like one unit, that way you can have
/// effects that cover an area. Inherited by <see cref="SmokeSolutionAreaEffectComponent"/> and <see cref="FoamSolutionAreaEffectComponent"/>.
/// </summary>
public abstract class SolutionAreaEffectComponent : Component
{
[Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[ComponentDependency] protected readonly SolutionContainerComponent? SolutionContainerComponent = default!;
public int Amount { get; set; }
public SolutionAreaEffectInceptionComponent? Inception { get; set; }
/// <summary>
/// Adds an <see cref="SolutionAreaEffectInceptionComponent"/> to owner so the effect starts spreading and reacting.
/// </summary>
/// <param name="amount">The range of the effect</param>
/// <param name="duration"></param>
/// <param name="spreadDelay"></param>
/// <param name="removeDelay"></param>
public void Start(int amount, float duration, float spreadDelay, float removeDelay)
{
if (Inception != null)
return;
if (Owner.HasComponent<SolutionAreaEffectInceptionComponent>())
return;
Amount = amount;
var inception = Owner.AddComponent<SolutionAreaEffectInceptionComponent>();
inception.Add(this);
inception.Setup(amount, duration, spreadDelay, removeDelay);
}
/// <summary>
/// Gets called by an AreaEffectInceptionComponent. "Clones" Owner into the four directions and copies the
/// solution into each of them.
/// </summary>
public void Spread()
{
if (Owner.Prototype == null)
{
Logger.Error("AreaEffectComponent needs its owner to be spawned by a prototype.");
return;
}
void SpreadToDir(Direction dir)
{
var grid = MapManager.GetGrid(Owner.Transform.GridID);
var coords = Owner.Transform.Coordinates;
foreach (var neighbor in grid.GetInDir(coords, dir))
{
if (Owner.EntityManager.ComponentManager.TryGetComponent(neighbor, out SolutionAreaEffectComponent? comp) && comp.Inception == Inception)
return;
if (Owner.EntityManager.ComponentManager.TryGetComponent(neighbor, out AirtightComponent? airtight) && airtight.AirBlocked)
return;
}
var newEffect = Owner.EntityManager.SpawnEntity(Owner.Prototype.ID, grid.DirectionToGrid(coords, dir));
if (!newEffect.TryGetComponent(out SolutionAreaEffectComponent? effectComponent))
{
newEffect.Delete();
return;
}
if (SolutionContainerComponent != null)
{
effectComponent.TryAddSolution(SolutionContainerComponent.Solution.Clone());
}
effectComponent.Amount = Amount - 1;
Inception?.Add(effectComponent);
}
SpreadToDir(Direction.North);
SpreadToDir(Direction.East);
SpreadToDir(Direction.South);
SpreadToDir(Direction.West);
}
/// <summary>
/// Gets called by an AreaEffectInceptionComponent.
/// Removes this component from its inception and calls OnKill(). The implementation of OnKill() should
/// eventually delete the entity.
/// </summary>
public void Kill()
{
Inception?.Remove(this);
OnKill();
}
protected abstract void OnKill();
/// <summary>
/// Gets called by an AreaEffectInceptionComponent.
/// Makes this effect's reagents react with the tile its on and with the entities it covers. Also calls
/// ReactWithEntity on the entities so inheritors can implement more specific behavior.
/// </summary>
/// <param name="averageExposures">How many times will this get called over this area effect's duration, averaged
/// with the other area effects from the inception.</param>
public void React(float averageExposures)
{
if (SolutionContainerComponent == null)
return;
var chemistry = EntitySystem.Get<ChemistrySystem>();
var mapGrid = MapManager.GetGrid(Owner.Transform.GridID);
var tile = mapGrid.GetTileRef(Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, MapManager));
var solutionFraction = 1 / Math.Floor(averageExposures);
foreach (var reagentQuantity in SolutionContainerComponent.ReagentList.ToArray())
{
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
var reagent = PrototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
// React with the tile the effect is on
reagent.ReactionTile(tile, reagentQuantity.Quantity * solutionFraction);
// Touch every entity on the tile
foreach (var entity in tile.GetEntitiesInTileFast().ToArray())
{
chemistry.ReactionEntity(entity, ReactionMethod.Touch, reagent, reagentQuantity.Quantity * solutionFraction, SolutionContainerComponent.Solution);
}
}
foreach (var entity in tile.GetEntitiesInTileFast().ToArray())
{
ReactWithEntity(entity, solutionFraction);
}
}
protected abstract void ReactWithEntity(IEntity entity, double solutionFraction);
public void TryAddSolution(Solution solution)
{
if (solution.TotalVolume == 0)
return;
if (SolutionContainerComponent == null)
return;
var addSolution =
solution.SplitSolution(ReagentUnit.Min(solution.TotalVolume, SolutionContainerComponent.EmptyVolume));
SolutionContainerComponent.TryAddSolution(addSolution);
UpdateVisuals();
}
protected abstract void UpdateVisuals();
public override void OnRemove()
{
base.OnRemove();
Inception?.Remove(this);
}
}
}

View File

@@ -0,0 +1,142 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
/// <summary>
/// The "mastermind" of a SolutionAreaEffect group. It gets updated by the SolutionAreaEffectSystem and tells the
/// group when to spread, react and remove itself. This makes the group act like a single unit.
/// </summary>
/// <remarks> It should only be manually added to an entity by the <see cref="SolutionAreaEffectComponent"/> and not with a prototype.</remarks>
[RegisterComponent]
public class SolutionAreaEffectInceptionComponent : Component
{
public override string Name => "AreaEffectInception";
private const float ReactionDelay = 0.5f;
private readonly HashSet<SolutionAreaEffectComponent> _group = new();
[ViewVariables] private float _lifeTimer;
[ViewVariables] private float _spreadTimer;
[ViewVariables] private float _reactionTimer;
[ViewVariables] private int _amountCounterSpreading;
[ViewVariables] private int _amountCounterRemoving;
/// <summary>
/// How much time to wait after fully spread before starting to remove itself.
/// </summary>
[ViewVariables] private float _duration;
/// <summary>
/// Time between each spread step. Decreasing this makes spreading faster.
/// </summary>
[ViewVariables] private float _spreadDelay;
/// <summary>
/// Time between each remove step. Decreasing this makes removing faster.
/// </summary>
[ViewVariables] private float _removeDelay;
/// <summary>
/// How many times will the effect react. As some entities from the group last a different amount of time than
/// others, they will react a different amount of times, so we calculate the average to make the group behave
/// a bit more uniformly.
/// </summary>
[ViewVariables] private float _averageExposures;
public void Setup(int amount, float duration, float spreadDelay, float removeDelay)
{
_amountCounterSpreading = amount;
_duration = duration;
_spreadDelay = spreadDelay;
_removeDelay = removeDelay;
// So the first square reacts immediately after spawning
_reactionTimer = ReactionDelay;
/*
The group takes amount*spreadDelay seconds to fully spread, same with fully disappearing.
The outer squares will last duration seconds.
The first square will last duration + how many seconds the group takes to fully spread and fully disappear, so
it will last duration + amount*(spreadDelay+removeDelay).
Thus, the average lifetime of the smokes will be (outerSmokeLifetime + firstSmokeLifetime)/2 = duration + amount*(spreadDelay+removeDelay)/2
*/
_averageExposures = (duration + amount * (spreadDelay+removeDelay) / 2)/ReactionDelay;
}
public void InceptionUpdate(float frameTime)
{
_group.RemoveWhere(effect => effect.Deleted);
if (_group.Count == 0)
return;
// Make every outer square from the group spread
if (_amountCounterSpreading > 0)
{
_spreadTimer += frameTime;
if (_spreadTimer > _spreadDelay)
{
_spreadTimer -= _spreadDelay;
var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterSpreading));
foreach (var effect in outerEffects)
{
effect.Spread();
}
_amountCounterSpreading -= 1;
}
}
// Start counting for _duration after fully spreading
else
{
_lifeTimer += frameTime;
}
// Delete every outer square
if (_lifeTimer > _duration)
{
_spreadTimer += frameTime;
if (_spreadTimer > _removeDelay)
{
_spreadTimer -= _removeDelay;
var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterRemoving));
foreach (var effect in outerEffects)
{
effect.Kill();
}
_amountCounterRemoving += 1;
}
}
// Make every square from the group react with the tile and entities
_reactionTimer += frameTime;
if (_reactionTimer > ReactionDelay)
{
_reactionTimer -= ReactionDelay;
foreach (var effect in _group)
{
effect.React(_averageExposures);
}
}
}
public void Add(SolutionAreaEffectComponent effect)
{
_group.Add(effect);
effect.Inception = this;
}
public void Remove(SolutionAreaEffectComponent effect)
{
_group.Remove(effect);
effect.Inception = null;
}
}
}

View File

@@ -0,0 +1,13 @@
#nullable enable
using Content.Shared.Chemistry.Solution.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
[ComponentReference(typeof(SharedSolutionContainerComponent))]
[ComponentReference(typeof(ISolutionInteractionsComponent))]
public class SolutionContainerComponent : SharedSolutionContainerComponent
{
}
}

View File

@@ -0,0 +1,123 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Solution.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Notification;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
/// <summary>
/// Gives click behavior for transferring to/from other reagent containers.
/// </summary>
[RegisterComponent]
public sealed class SolutionTransferComponent : Component, IAfterInteract
{
// Behavior is as such:
// If it's a reagent tank, TAKE reagent.
// If it's anything else, GIVE reagent.
// Of course, only if possible.
public override string Name => "SolutionTransfer";
/// <summary>
/// The amount of solution to be transferred from this solution when clicking on other solutions with it.
/// </summary>
[DataField("transferAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(5);
/// <summary>
/// Can this entity take reagent from reagent tanks?
/// </summary>
[DataField("canReceive")]
[ViewVariables(VVAccess.ReadWrite)]
public bool CanReceive { get; set; } = true;
/// <summary>
/// Can this entity give reagent to other reagent containers?
/// </summary>
[DataField("canSend")]
[ViewVariables(VVAccess.ReadWrite)]
public bool CanSend { get; set; } = true;
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed() || eventArgs.Target == null)
return false;
if (!Owner.TryGetComponent(out ISolutionInteractionsComponent? ownerSolution))
return false;
var target = eventArgs.Target;
if (!target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution))
{
return false;
}
if (CanReceive && target.TryGetComponent(out ReagentTankComponent? tank)
&& ownerSolution.CanRefill && targetSolution.CanDrain)
{
var transferred = DoTransfer(targetSolution, ownerSolution, tank.TransferAmount, eventArgs.User);
if (transferred > 0)
{
var toTheBrim = ownerSolution.RefillSpaceAvailable == 0;
var msg = toTheBrim
? "You fill {0:TheName} to the brim with {1}u from {2:theName}"
: "You fill {0:TheName} with {1}u from {2:theName}";
target.PopupMessage(eventArgs.User, Loc.GetString(msg, Owner, transferred, target));
return true;
}
}
if (CanSend && targetSolution.CanRefill && ownerSolution.CanDrain)
{
var transferred = DoTransfer(ownerSolution, targetSolution, TransferAmount, eventArgs.User);
if (transferred > 0)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You transfer {0}u to {1:theName}.",
transferred, target));
return true;
}
}
return true;
}
/// <returns>The actual amount transferred.</returns>
private static ReagentUnit DoTransfer(
ISolutionInteractionsComponent source,
ISolutionInteractionsComponent target,
ReagentUnit amount,
IEntity user)
{
if (source.DrainAvailable == 0)
{
source.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is empty!", source.Owner));
return ReagentUnit.Zero;
}
if (target.RefillSpaceAvailable == 0)
{
target.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is full!", target.Owner));
return ReagentUnit.Zero;
}
var actualAmount =
ReagentUnit.Min(amount, ReagentUnit.Min(source.DrainAvailable, target.RefillSpaceAvailable));
var solution = source.Drain(actualAmount);
target.Refill(solution);
return actualAmount;
}
}
}

View File

@@ -0,0 +1,102 @@
#nullable enable
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Solution;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
public class TransformableContainerComponent : Component, ISolutionChange
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Name => "TransformableContainer";
private SpriteSpecifier? _initialSprite;
private string _initialName = default!;
private string _initialDescription = default!;
private ReagentPrototype? _currentReagent;
public bool Transformed { get; private set; }
public override void Initialize()
{
base.Initialize();
if (Owner.TryGetComponent(out SpriteComponent? sprite) &&
sprite.BaseRSIPath != null)
{
_initialSprite = new SpriteSpecifier.Rsi(new ResourcePath(sprite.BaseRSIPath), "icon");
}
_initialName = Owner.Name;
_initialDescription = Owner.Description;
}
protected override void Startup()
{
base.Startup();
Owner.EnsureComponentWarn(out SolutionContainerComponent solution);
solution.Capabilities |= SolutionContainerCaps.FitsInDispenser;
}
public void CancelTransformation()
{
_currentReagent = null;
Transformed = false;
if (Owner.TryGetComponent(out SpriteComponent? sprite) &&
_initialSprite != null)
{
sprite.LayerSetSprite(0, _initialSprite);
}
Owner.Name = _initialName;
Owner.Description = _initialDescription;
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
var solution = eventArgs.Owner.GetComponent<SolutionContainerComponent>();
//Transform container into initial state when emptied
if (_currentReagent != null && solution.ReagentList.Count == 0)
{
CancelTransformation();
}
//the biggest reagent in the solution decides the appearance
var reagentId = solution.Solution.GetPrimaryReagentId();
//If biggest reagent didn't changed - don't change anything at all
if (_currentReagent != null && _currentReagent.ID == reagentId)
{
return;
}
//Only reagents with spritePath property can change appearance of transformable containers!
if (!string.IsNullOrWhiteSpace(reagentId) &&
_prototypeManager.TryIndex(reagentId, out ReagentPrototype? proto) &&
!string.IsNullOrWhiteSpace(proto.SpriteReplacementPath))
{
var spriteSpec = new SpriteSpecifier.Rsi(new ResourcePath("Objects/Consumable/Drinks/" + proto.SpriteReplacementPath),"icon");
if (Owner.TryGetComponent(out SpriteComponent? sprite))
{
sprite?.LayerSetSprite(0, spriteSpec);
}
Owner.Name = proto.Name + " glass";
Owner.Description = proto.Description;
_currentReagent = proto;
Transformed = true;
}
}
}
}

View File

@@ -0,0 +1,131 @@
using System.Linq;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Solution;
using Content.Shared.Physics;
using Content.Shared.Vapor;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
class VaporComponent : SharedVaporComponent, IStartCollide
{
public const float ReactTime = 0.125f;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[ViewVariables]
[DataField("transferAmount")]
private ReagentUnit _transferAmount = ReagentUnit.New(0.5);
private bool _reached;
private float _reactTimer;
private float _timer;
private EntityCoordinates _target;
private bool _running;
private float _aliveTime;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn(out SolutionContainerComponent _);
}
public void Start(Vector2 dir, float speed, EntityCoordinates target, float aliveTime)
{
_running = true;
_target = target;
_aliveTime = aliveTime;
// Set Move
if (Owner.TryGetComponent(out PhysicsComponent? physics))
{
physics.BodyStatus = BodyStatus.InAir;
physics.ApplyLinearImpulse(dir * speed);
}
}
public void Update(float frameTime)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
return;
if (!_running)
return;
_timer += frameTime;
_reactTimer += frameTime;
if (_reactTimer >= ReactTime && Owner.Transform.GridID.IsValid())
{
_reactTimer = 0;
var mapGrid = _mapManager.GetGrid(Owner.Transform.GridID);
var tile = mapGrid.GetTileRef(Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, _mapManager));
foreach (var reagentQuantity in contents.ReagentList.ToArray())
{
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
var reagent = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
contents.TryRemoveReagent(reagentQuantity.ReagentId, reagent.ReactionTile(tile, (reagentQuantity.Quantity / _transferAmount) * 0.25f));
}
}
// Check if we've reached our target.
if(!_reached && _target.TryDistance(Owner.EntityManager, Owner.Transform.Coordinates, out var distance) && distance <= 0.5f)
{
_reached = true;
}
if (contents.CurrentVolume == 0 || _timer > _aliveTime)
{
// Delete this
Owner.Delete();
}
}
internal bool TryAddSolution(Solution solution)
{
if (solution.TotalVolume == 0)
{
return false;
}
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
{
return false;
}
var result = contents.TryAddSolution(solution);
if (!result)
{
return false;
}
return true;
}
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
return;
contents.Solution.DoEntityReaction(otherFixture.Body.Owner, ReactionMethod.Touch);
// Check for collision with a impassable object (e.g. wall) and stop
if ((otherFixture.CollisionLayer & (int) CollisionGroup.Impassable) != 0 && otherFixture.Hard)
{
Owner.QueueDelete();
}
}
}
}