* A big hecking chemistry-related refactor. Changed SolutionContainerCaps. It now describes "stock" behavior for interacting with solutions that is pre-implemented by SolutionContainerComponent. As such things like syringes do not check it anymore (on themselves) to see "can we remove reagent from ourselves". That's assumed by it... being a syringe. SolutionContainerCaps now has different flags more accurately describing possible reagent interaction behaviors. ISolutionInteractionsComponent is the interface that describes the common behaviors like "what happens when injected with a syringe". This is implemented by SolutionContainerComponent but could be implemented by other classes. One notable example that drove me to making this interface was the /vg/station circuit imprinter which splits reagent poured in into its two reservoir beakers. Having this interface allows us to do this "proxying" behavior hack-free. (the hacks in /vg/ code were somewhat dirty...). PourableComponent has been replaced SolutionTransferComponent. It now describes both give-and-take behavior for the common reagent containers. This is in line with /vg/'s /obj/item/weapon/reagent_containers architecture. "Taking" in this context is ONLY from reagent tanks like fuel tanks. Oh, should I mention that fuel tanks and such have a proper component now? They do. Because of this behavioral change, reagent tanks DO NOT have Pourable anymore. Removing from reagent tanks is now in the hands of the item used on them. Welders and fire extinguishers now have code for removing from them. This sounds bad at first but remember that all have quite unique behavior related to this: Welders cause explosions if lit and can ONLY be fueled at fuel tanks. Extinguishers can be filled at any tank, etc... The code for this is also simpler due to ISolutionInteractionsComponent now so... IAfterInteract now works like IInteractUsing with the Priority levels and "return true to block further handlers" behavior. This was necessary to make extinguishers prioritize taking from tanks over spraying. Explicitly coded interactions like welders refueling also means they refuse instantly to full now, which they didn't before. And it plays the sound. Etc... Probably more stuff I'm forgetting. * Review improvements.
283 lines
9.9 KiB
C#
283 lines
9.9 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Content.Server.GameObjects.Components.Body.Surgery.Messages;
|
|
using Content.Server.Utility;
|
|
using Content.Shared.GameObjects;
|
|
using Content.Shared.GameObjects.Components.Body;
|
|
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
|
using Content.Shared.GameObjects.Components.Body.Part;
|
|
using Content.Shared.GameObjects.Components.Body.Surgery;
|
|
using Content.Shared.Interfaces;
|
|
using Content.Shared.Interfaces.GameObjects.Components;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Server.GameObjects.Components.UserInterface;
|
|
using Robust.Server.Interfaces.GameObjects;
|
|
using Robust.Server.Interfaces.Player;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.Interfaces.GameObjects;
|
|
using Robust.Shared.Localization;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.ViewVariables;
|
|
|
|
namespace Content.Server.GameObjects.Components.Body.Surgery
|
|
{
|
|
/// <summary>
|
|
/// Server-side component representing a generic tool capable of performing surgery.
|
|
/// For instance, the scalpel.
|
|
/// </summary>
|
|
[RegisterComponent]
|
|
public class SurgeryToolComponent : Component, ISurgeon, IAfterInteract
|
|
{
|
|
public override string Name => "SurgeryTool";
|
|
public override uint? NetID => ContentNetIDs.SURGERY;
|
|
|
|
private readonly Dictionary<int, object> _optionsCache = new();
|
|
|
|
private float _baseOperateTime;
|
|
|
|
private ISurgeon.MechanismRequestCallback? _callbackCache;
|
|
|
|
private int _idHash;
|
|
|
|
private SurgeryType _surgeryType;
|
|
|
|
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
|
|
|
|
public IBody? BodyCache { get; private set; }
|
|
|
|
public IEntity? PerformerCache { get; private set; }
|
|
|
|
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
|
{
|
|
if (eventArgs.Target == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CloseAllSurgeryUIs();
|
|
|
|
// Attempt surgery on a body by sending a list of operable parts for the client to choose from
|
|
if (eventArgs.Target.TryGetComponent(out IBody? body))
|
|
{
|
|
// Create dictionary to send to client (text to be shown : data sent back if selected)
|
|
var toSend = new Dictionary<string, int>();
|
|
|
|
foreach (var (key, value) in body.Parts)
|
|
{
|
|
// For each limb in the target, add it to our cache if it is a valid option.
|
|
if (value.SurgeryCheck(_surgeryType))
|
|
{
|
|
_optionsCache.Add(_idHash, value);
|
|
toSend.Add(key + ": " + value.Name, _idHash++);
|
|
}
|
|
}
|
|
|
|
if (_optionsCache.Count > 0)
|
|
{
|
|
OpenSurgeryUI(actor.playerSession);
|
|
UpdateSurgeryUIBodyPartRequest(actor.playerSession, toSend);
|
|
PerformerCache = eventArgs.User; // Also, cache the data.
|
|
BodyCache = body;
|
|
}
|
|
else // If surgery cannot be performed, show message saying so.
|
|
{
|
|
NotUsefulPopup();
|
|
}
|
|
}
|
|
else if (eventArgs.Target.TryGetComponent<IBodyPart>(out var part))
|
|
{
|
|
// Attempt surgery on a DroppedBodyPart - there's only one possible target so no need for selection UI
|
|
PerformerCache = eventArgs.User;
|
|
|
|
// If surgery can be performed...
|
|
if (!part.SurgeryCheck(_surgeryType))
|
|
{
|
|
NotUsefulPopup();
|
|
return true;
|
|
}
|
|
|
|
// ...do the surgery.
|
|
if (part.AttemptSurgery(_surgeryType, part, this,
|
|
eventArgs.User))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Log error if the surgery fails somehow.
|
|
Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}");
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; }
|
|
|
|
public void RequestMechanism(IEnumerable<IMechanism> options, ISurgeon.MechanismRequestCallback callback)
|
|
{
|
|
var toSend = new Dictionary<string, int>();
|
|
foreach (var mechanism in options)
|
|
{
|
|
_optionsCache.Add(_idHash, mechanism);
|
|
toSend.Add(mechanism.Name, _idHash++);
|
|
}
|
|
|
|
if (_optionsCache.Count > 0 && PerformerCache != null)
|
|
{
|
|
OpenSurgeryUI(PerformerCache.GetComponent<BasicActorComponent>().playerSession);
|
|
UpdateSurgeryUIMechanismRequest(PerformerCache.GetComponent<BasicActorComponent>().playerSession,
|
|
toSend);
|
|
_callbackCache = callback;
|
|
}
|
|
else
|
|
{
|
|
Logger.Debug("Error on callback from mechanisms: there were no viable options to choose from!");
|
|
throw new InvalidOperationException();
|
|
}
|
|
}
|
|
|
|
public override void ExposeData(ObjectSerializer serializer)
|
|
{
|
|
base.ExposeData(serializer);
|
|
|
|
serializer.DataField(ref _surgeryType, "surgeryType", SurgeryType.Incision);
|
|
serializer.DataField(ref _baseOperateTime, "baseOperateTime", 5);
|
|
}
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
if (UserInterface != null)
|
|
{
|
|
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
|
}
|
|
}
|
|
|
|
private void OpenSurgeryUI(IPlayerSession session)
|
|
{
|
|
UserInterface?.Open(session);
|
|
|
|
var message = new SurgeryWindowOpenMessage(this);
|
|
|
|
SendMessage(message);
|
|
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message);
|
|
}
|
|
|
|
private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
|
|
{
|
|
UserInterface?.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
|
|
}
|
|
|
|
private void UpdateSurgeryUIMechanismRequest(IPlayerSession session, Dictionary<string, int> options)
|
|
{
|
|
UserInterface?.SendMessage(new RequestMechanismSurgeryUIMessage(options), session);
|
|
}
|
|
|
|
private void ClearUIData()
|
|
{
|
|
_optionsCache.Clear();
|
|
|
|
PerformerCache = null;
|
|
BodyCache = null;
|
|
_callbackCache = null;
|
|
}
|
|
|
|
private void CloseSurgeryUI(IPlayerSession session)
|
|
{
|
|
UserInterface?.Close(session);
|
|
ClearUIData();
|
|
}
|
|
|
|
public void CloseAllSurgeryUIs()
|
|
{
|
|
UserInterface?.CloseAll();
|
|
ClearUIData();
|
|
}
|
|
|
|
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
|
|
{
|
|
switch (message.Message)
|
|
{
|
|
case ReceiveBodyPartSurgeryUIMessage msg:
|
|
HandleReceiveBodyPart(msg.SelectedOptionId);
|
|
break;
|
|
case ReceiveMechanismSurgeryUIMessage msg:
|
|
HandleReceiveMechanism(msg.SelectedOptionId);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after the client chooses from a list of possible
|
|
/// <see cref="IBodyPart"/> that can be operated on.
|
|
/// </summary>
|
|
private void HandleReceiveBodyPart(int key)
|
|
{
|
|
if (PerformerCache == null ||
|
|
!PerformerCache.TryGetComponent(out IActorComponent? actor))
|
|
{
|
|
return;
|
|
}
|
|
|
|
CloseSurgeryUI(actor.playerSession);
|
|
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
|
|
if (!_optionsCache.TryGetValue(key, out var targetObject) ||
|
|
BodyCache == null)
|
|
{
|
|
NotUsefulAnymorePopup();
|
|
return;
|
|
}
|
|
|
|
var target = (IBodyPart) targetObject!;
|
|
|
|
// TODO BODY Reconsider
|
|
if (!target.AttemptSurgery(_surgeryType, BodyCache, this, PerformerCache))
|
|
{
|
|
NotUsefulAnymorePopup();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after the client chooses from a list of possible
|
|
/// <see cref="IMechanism"/> to choose from.
|
|
/// </summary>
|
|
private void HandleReceiveMechanism(int key)
|
|
{
|
|
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
|
|
if (!_optionsCache.TryGetValue(key, out var targetObject) ||
|
|
PerformerCache == null ||
|
|
!PerformerCache.TryGetComponent(out IActorComponent? actor))
|
|
{
|
|
NotUsefulAnymorePopup();
|
|
return;
|
|
}
|
|
|
|
var target = targetObject as MechanismComponent;
|
|
|
|
CloseSurgeryUI(actor.playerSession);
|
|
_callbackCache?.Invoke(target, BodyCache, this, PerformerCache);
|
|
}
|
|
|
|
private void NotUsefulPopup()
|
|
{
|
|
BodyCache?.Owner.PopupMessage(PerformerCache,
|
|
Loc.GetString("You see no useful way to use {0:theName}.", Owner));
|
|
}
|
|
|
|
private void NotUsefulAnymorePopup()
|
|
{
|
|
BodyCache?.Owner.PopupMessage(PerformerCache,
|
|
Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner));
|
|
}
|
|
}
|
|
}
|