Reagent dispensers (#360)
* Expose more private values of Solution and SolutionComponent Expose SolutionComponent.ContainedSolution and Solution.Contents. Both needed by ReagentDispenserComponent. * Implement IExamine for SolutionComponent Allows players to see the contents of a solution by examining the entity which contains it. * Implement ReagentDispenserComponent Adds ReagentDispenserComponent. A component which can add or remove reagents from a solution container. It's written in a general way so that it can be used for things such as the Chemical dispensers in chemistry, but also the booze and soda dispensers in the bar. The chemicals it may dispense are defined in yaml, similar to the way that vending machines define which entities they can dispense, by defining a reagent pack. * Add chemical dispenser and equipment Adds the chemical dispenser, beaker, large beaker, dropper, and a few more chemicals. * Add booze and soda dispensers. Adds the booze and soda dispensers, and a few chemicals for them to dispense. There's no drink mixing or drunkenness yet. * Update engine submodule. * Remove unneeded and commented out code Had a few WIP notes and debug code bits I forgot to remove beforehand. * Make SolutionComponent._containedSolution and it's values private again - Remove `SolutionComponent.ContainedSolution` property, replace with specific access functions to maintain safety. - Make Solution.Contents return a `ReadOnlyCollection` instead of `_contents` to prevent uncontrolled access to the Solution values. - Add `SolutionComponent.RemoveAllSolution()` * Update Content.Shared/Chemistry/Solution.cs Commits a suggestion from RemieRichards to match the coding style of the rest of the codebase. Using `IReadOnlyList` instead of `IReadOnlyCollection`. Co-Authored-By: Remie Richards <remierichards@gmail.com> * Update Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs Commits a suggestion from RemieRichards to match the coding style of the rest of the codebase. Using `IReadOnlyList` instead of `IReadOnlyCollection`. Co-Authored-By: Remie Richards <remierichards@gmail.com> * Add import for IReadOnlyList to Shared/SolutionComponent.cs * Add documentation * Improve localization Improve use of ILocalizationManager. * Resolve ReagentDispenserWindow._localizationManager before using it Forgot to do this in the last commit, resulting in a crash. Oops. * Add SolutionCaps.FitsInDispenser. Use in ReagentDispenserComponent. Used to limit large containers like buckets or mop buckets from being placed in a dispenser. Both have large capacities (500) and weren't designed to hold certain chemicals like a beaker is, so for now they can be blocked from being put in a dispenser by giving them that flag. * Add colors to new reagents * Update engine submodule
This commit is contained in:
committed by
Pieter-Jan Briers
parent
0cc980b26a
commit
963bb28f0f
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Interfaces;
|
||||
using Content.Server.Interfaces.GameObjects;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.Components.UserInterface;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <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(IAttackBy))]
|
||||
public class ReagentDispenserComponent : SharedReagentDispenserComponent, IActivate, IAttackBy
|
||||
{
|
||||
#pragma warning disable 649
|
||||
[Dependency] private readonly IServerNotifyManager _notifyManager;
|
||||
[Dependency] private readonly ILocalizationManager _localizationManager;
|
||||
#pragma warning restore 649
|
||||
|
||||
private BoundUserInterface _userInterface;
|
||||
private ContainerSlot _beakerContainer;
|
||||
private string _packPrototypeId;
|
||||
|
||||
public bool HasBeaker => _beakerContainer.ContainedEntity != null;
|
||||
public int DispenseAmount = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the serializer how to save/load this components yaml prototype.
|
||||
/// </summary>
|
||||
/// <param name="serializer">Yaml serializer</param>
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _packPrototypeId, "pack", string.Empty);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>().GetBoundUserInterface(ReagentDispenserUiKey.Key);
|
||||
_userInterface.OnReceiveMessage += OnUiReceiveMessage;
|
||||
|
||||
_beakerContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-reagentContainerContainer", Owner);
|
||||
|
||||
InitializeFromPrototype();
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
if (!prototypeManager.TryIndex(_packPrototypeId, out ReagentDispenserInventoryPrototype packPrototype))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var entry in packPrototype.Inventory)
|
||||
{
|
||||
Inventory.Add(new ReagentDispenserInventoryEntry(entry));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var msg = (UiButtonPressedMessage)obj.Message;
|
||||
switch (msg.Button)
|
||||
{
|
||||
case UiButton.Eject:
|
||||
TryEject();
|
||||
break;
|
||||
case UiButton.Clear:
|
||||
TryClear();
|
||||
break;
|
||||
case UiButton.SetDispenseAmount1:
|
||||
DispenseAmount = 1;
|
||||
break;
|
||||
case UiButton.SetDispenseAmount5:
|
||||
DispenseAmount = 5;
|
||||
break;
|
||||
case UiButton.SetDispenseAmount10:
|
||||
DispenseAmount = 10;
|
||||
break;
|
||||
case UiButton.SetDispenseAmount25:
|
||||
DispenseAmount = 25;
|
||||
break;
|
||||
case UiButton.SetDispenseAmount50:
|
||||
DispenseAmount = 50;
|
||||
break;
|
||||
case UiButton.SetDispenseAmount100:
|
||||
DispenseAmount = 100;
|
||||
break;
|
||||
case UiButton.Dispense:
|
||||
if (HasBeaker)
|
||||
{
|
||||
TryDispense(msg.DispenseIndex);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(false, 0,0,
|
||||
"", Inventory, Owner.Name, null);
|
||||
}
|
||||
|
||||
var solution = beaker.GetComponent<SolutionComponent>();
|
||||
return new ReagentDispenserBoundUserInterfaceState(true, solution.CurrentVolume, solution.MaxVolume,
|
||||
beaker.Name, Inventory, Owner.Name, solution.ReagentList.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current component data as a <see cref="SharedReagentDispenserComponent.ReagentDispenserBoundUserInterfaceState"/> and sends it to the client.
|
||||
/// </summary>
|
||||
private void UpdateUserInterface()
|
||||
{
|
||||
var state = GetUserInterfaceState();
|
||||
_userInterface.SetState(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this component contains an entity with a <see cref="SolutionComponent"/>, eject it.
|
||||
/// </summary>
|
||||
private void TryEject()
|
||||
{
|
||||
if(!HasBeaker) return;
|
||||
_beakerContainer.Remove(_beakerContainer.ContainedEntity);
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this component contains an entity with a <see cref="SolutionComponent"/>, remove all of it's reagents / solutions.
|
||||
/// </summary>
|
||||
private void TryClear()
|
||||
{
|
||||
if (!HasBeaker) return;
|
||||
var solution = _beakerContainer.ContainedEntity.GetComponent<SolutionComponent>();
|
||||
solution.RemoveAllSolution();
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this component contains an entity with a <see cref="SolutionComponent"/>, 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<SolutionComponent>();
|
||||
solution.TryAddReagent(Inventory[dispenseIndex].ID, DispenseAmount, out int acceptedQuantity);
|
||||
|
||||
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>
|
||||
public void Activate(ActivateEventArgs args)
|
||||
{
|
||||
if (!args.User.TryGetComponent(out IActorComponent actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!args.User.TryGetComponent(out IHandsComponent hands))
|
||||
{
|
||||
_notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User,
|
||||
_localizationManager.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="SolutionComponent"/>, 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>
|
||||
public bool AttackBy(AttackByEventArgs args)
|
||||
{
|
||||
if (!args.User.TryGetComponent(out IHandsComponent hands))
|
||||
{
|
||||
_notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User,
|
||||
_localizationManager.GetString("You have no hands."));
|
||||
return true;
|
||||
}
|
||||
|
||||
var activeHandEntity = hands.GetActiveHand.Owner;
|
||||
if (activeHandEntity.TryGetComponent<SolutionComponent>(out var solution))
|
||||
{
|
||||
if (HasBeaker)
|
||||
{
|
||||
_notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User,
|
||||
_localizationManager.GetString("This dispenser already has a container in it."));
|
||||
}
|
||||
else if ((solution.Capabilities & SolutionCaps.FitsInDispenser) == 0)
|
||||
{
|
||||
//If it can't fit in the dispenser, don't put it in. For example, buckets and mop buckets can't fit.
|
||||
_notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User,
|
||||
_localizationManager.GetString("That can't fit in the dispenser."));
|
||||
}
|
||||
else
|
||||
{
|
||||
_beakerContainer.Insert(activeHandEntity);
|
||||
UpdateUserInterface();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User,
|
||||
_localizationManager.GetString("You can't put this in the dispenser."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user