Files
tbd-station-14/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs
Pieter-Jan Briers c40ac26ced A big hecking chemistry-related refactor. (#3055)
* 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.
2021-02-04 00:05:31 +11:00

521 lines
18 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameObjects;
using Content.Server.Utility;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.Components.Power;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Kitchen;
using Content.Shared.Prototypes.Kitchen;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Kitchen
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class MicrowaveComponent : SharedMicrowaveComponent, IActivate, IInteractUsing, ISolutionChange, ISuicideAct
{
[Dependency] private readonly RecipeManager _recipeManager = default!;
#region YAMLSERIALIZE
private int _cookTimeDefault;
private int _cookTimeMultiplier; //For upgrades and stuff I guess?
private string _badRecipeName = "";
private string _startCookingSound = "";
private string _cookingCompleteSound = "";
#endregion
[ViewVariables]
private bool _busy = false;
/// <summary>
/// This is a fixed offset of 5.
/// The cook times for all recipes should be divisible by 5,with a minimum of 1 second.
/// For right now, I don't think any recipe cook time should be greater than 60 seconds.
/// </summary>
[ViewVariables]
private uint _currentCookTimerTime = 1;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private bool _hasContents => Owner.TryGetComponent(out SolutionContainerComponent? solution) && (solution.ReagentList.Count > 0 || _storage.ContainedEntities.Count > 0);
private bool _uiDirty = true;
private bool _lostPower = false;
private int _currentCookTimeButtonIndex = 0;
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) => _uiDirty = true;
private AudioSystem _audioSystem = default!;
private Container _storage = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(MicrowaveUiKey.Key);
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _badRecipeName, "failureResult", "FoodBadRecipe");
serializer.DataField(ref _cookTimeDefault, "cookTime", 5);
serializer.DataField(ref _cookTimeMultiplier, "cookTimeMultiplier", 1000);
serializer.DataField(ref _startCookingSound, "beginCookingSound","/Audio/Machines/microwave_start_beep.ogg" );
serializer.DataField(ref _cookingCompleteSound, "foodDoneSound","/Audio/Machines/microwave_done_beep.ogg" );
}
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponent<SolutionContainerComponent>();
_storage = ContainerManagerComponent.Ensure<Container>("microwave_entity_container", Owner, out var existed);
_audioSystem = EntitySystem.Get<AudioSystem>();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
}
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
if (!Powered || _busy)
{
return;
}
switch (message.Message)
{
case MicrowaveStartCookMessage msg :
wzhzhzh();
break;
case MicrowaveEjectMessage msg :
if (_hasContents)
{
VaporizeReagents();
EjectSolids();
ClickSound();
_uiDirty = true;
}
break;
case MicrowaveEjectSolidIndexedMessage msg:
if (_hasContents)
{
EjectSolid(msg.EntityID);
ClickSound();
_uiDirty = true;
}
break;
case MicrowaveVaporizeReagentIndexedMessage msg:
if (_hasContents)
{
VaporizeReagentQuantity(msg.ReagentQuantity);
ClickSound();
_uiDirty = true;
}
break;
case MicrowaveSelectCookTimeMessage msg:
_currentCookTimeButtonIndex = msg.ButtonIndex;
_currentCookTimerTime = msg.NewCookTime;
ClickSound();
_uiDirty = true;
break;
}
}
public void OnUpdate()
{
if (!Powered)
{
//TODO:If someone cuts power currently, microwave magically keeps going. FIX IT!
SetAppearance(MicrowaveVisualState.Idle);
}
if (_busy && !Powered)
{
//we lost power while we were cooking/busy!
_lostPower = true;
VaporizeReagents();
EjectSolids();
_busy = false;
_uiDirty = true;
}
if (_uiDirty && Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
UserInterface?.SetState(new MicrowaveUpdateUserInterfaceState
(
solution.Solution.Contents.ToArray(),
_storage.ContainedEntities.Select(item => item.Uid).ToArray(),
_busy,
_currentCookTimeButtonIndex,
_currentCookTimerTime
));
_uiDirty = false;
}
}
private void SetAppearance(MicrowaveVisualState state)
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(PowerDeviceVisuals.VisualState, state);
}
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor) || !Powered)
{
return;
}
_uiDirty = true;
UserInterface?.Toggle(actor.playerSession);
}
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!Powered)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("It has no power!"));
return false;
}
var itemEntity = eventArgs.User.GetComponent<HandsComponent>().GetActiveHand?.Owner;
if (itemEntity == null)
{
eventArgs.User.PopupMessage(Loc.GetString("You have no active hand!"));
return false;
}
if (itemEntity.TryGetComponent<SolutionTransferComponent>(out var attackPourable))
{
if (!itemEntity.TryGetComponent<ISolutionInteractionsComponent>(out var attackSolution)
|| !attackSolution.CanDrain)
{
return false;
}
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return false;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(attackPourable.TransferAmount, solution.EmptyVolume);
if (realTransferAmount <= 0) //Special message if container is full
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Container is full"));
return false;
}
//Move units from attackSolution to targetSolution
var removedSolution = attackSolution.Drain(realTransferAmount);
if (!solution.TryAddSolution(removedSolution))
{
return false;
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("Transferred {0}u", removedSolution.TotalVolume));
return true;
}
if (!itemEntity.TryGetComponent(typeof(ItemComponent), out var food))
{
Owner.PopupMessage(eventArgs.User, "That won't work!");
return false;
}
var ent = food.Owner; //Get the entity of the ItemComponent.
_storage.Insert(ent);
_uiDirty = true;
return true;
}
// ReSharper disable once InconsistentNaming
// ReSharper disable once IdentifierTypo
private void wzhzhzh()
{
if (!_hasContents)
{
return;
}
_busy = true;
// Convert storage into Dictionary of ingredients
var solidsDict = new Dictionary<string, int>();
foreach(var item in _storage.ContainedEntities)
{
if (item.Prototype == null)
{
continue;
}
if(solidsDict.ContainsKey(item.Prototype.ID))
{
solidsDict[item.Prototype.ID]++;
}
else
{
solidsDict.Add(item.Prototype.ID, 1);
}
}
var failState = MicrowaveSuccessState.RecipeFail;
foreach(var id in solidsDict.Keys)
{
if(_recipeManager.SolidAppears(id))
{
continue;
}
failState = MicrowaveSuccessState.UnwantedForeignObject;
break;
}
// Check recipes
FoodRecipePrototype? recipeToCook = null;
foreach (var r in _recipeManager.Recipes.Where(r => CanSatisfyRecipe(r, solidsDict) == MicrowaveSuccessState.RecipePass))
{
recipeToCook = r;
}
var goodMeal = (recipeToCook != null)
&&
(_currentCookTimerTime == (uint)recipeToCook.CookTime);
SetAppearance(MicrowaveVisualState.Cooking);
_audioSystem.PlayFromEntity(_startCookingSound, Owner, AudioParams.Default);
Owner.SpawnTimer((int)(_currentCookTimerTime * _cookTimeMultiplier), (Action)(() =>
{
if (_lostPower)
{
return;
}
if(failState == MicrowaveSuccessState.UnwantedForeignObject)
{
VaporizeReagents();
EjectSolids();
}
else
{
if (goodMeal)
{
SubtractContents(recipeToCook!);
}
else
{
VaporizeReagents();
VaporizeSolids();
}
if (recipeToCook != null)
{
var entityToSpawn = goodMeal ? recipeToCook.Result : _badRecipeName;
Owner.EntityManager.SpawnEntity(entityToSpawn, Owner.Transform.Coordinates);
}
}
_audioSystem.PlayFromEntity(_cookingCompleteSound, Owner, AudioParams.Default.WithVolume(-1f));
SetAppearance(MicrowaveVisualState.Idle);
_busy = false;
_uiDirty = true;
}));
_lostPower = false;
_uiDirty = true;
}
private void VaporizeReagents()
{
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
solution.RemoveAllSolution();
}
}
private void VaporizeReagentQuantity(Solution.ReagentQuantity reagentQuantity)
{
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
solution?.TryRemoveReagent(reagentQuantity.ReagentId, reagentQuantity.Quantity);
}
}
private void VaporizeSolids()
{
for(var i = _storage.ContainedEntities.Count-1; i>=0; i--)
{
var item = _storage.ContainedEntities.ElementAt(i);
_storage.Remove(item);
item.Delete();
}
}
private void EjectSolids()
{
for(var i = _storage.ContainedEntities.Count-1; i>=0; i--)
{
_storage.Remove(_storage.ContainedEntities.ElementAt(i));
}
}
private void EjectSolid(EntityUid entityID)
{
if (Owner.EntityManager.EntityExists(entityID))
{
_storage.Remove(Owner.EntityManager.GetEntity(entityID));
}
}
private void SubtractContents(FoodRecipePrototype recipe)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return;
}
foreach(var recipeReagent in recipe.IngredientsReagents)
{
solution?.TryRemoveReagent(recipeReagent.Key, ReagentUnit.New(recipeReagent.Value));
}
foreach (var recipeSolid in recipe.IngredientsSolids)
{
for (var i = 0; i < recipeSolid.Value; i++)
{
foreach (var item in _storage.ContainedEntities)
{
if (item.Prototype == null)
{
continue;
}
if (item.Prototype.ID == recipeSolid.Key)
{
_storage.Remove(item);
item.Delete();
break;
}
}
}
}
}
private MicrowaveSuccessState CanSatisfyRecipe(FoodRecipePrototype recipe, Dictionary<string,int> solids)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return MicrowaveSuccessState.RecipeFail;
}
foreach (var reagent in recipe.IngredientsReagents)
{
if (!solution.Solution.ContainsReagent(reagent.Key, out var amount))
{
return MicrowaveSuccessState.RecipeFail;
}
if (amount.Int() < reagent.Value)
{
return MicrowaveSuccessState.RecipeFail;
}
}
foreach (var solid in recipe.IngredientsSolids)
{
if (!solids.ContainsKey(solid.Key))
{
return MicrowaveSuccessState.RecipeFail;
}
if (solids[solid.Key] < solid.Value)
{
return MicrowaveSuccessState.RecipeFail;
}
}
return MicrowaveSuccessState.RecipePass;
}
private void ClickSound()
{
_audioSystem.PlayFromEntity("/Audio/Machines/machine_switch.ogg",Owner,AudioParams.Default.WithVolume(-2f));
}
public SuicideKind Suicide(IEntity victim, IChatManager chat)
{
var headCount = 0;
if (victim.TryGetComponent<IBody>(out var body))
{
var heads = body.GetPartsOfType(BodyPartType.Head);
foreach (var head in heads)
{
if (!body.TryDropPart(head, out var dropped))
{
continue;
}
var droppedHeads = dropped.Where(p => p.PartType == BodyPartType.Head);
foreach (var droppedHead in droppedHeads)
{
_storage.Insert(droppedHead.Owner);
headCount++;
}
}
}
var othersMessage = headCount > 1
? Loc.GetString("{0:theName} is trying to cook {0:their} heads!", victim)
: Loc.GetString("{0:theName} is trying to cook {0:their} head!", victim);
victim.PopupMessageOtherClients(othersMessage);
var selfMessage = headCount > 1
? Loc.GetString("You cook your heads!")
: Loc.GetString("You cook your head!");
victim.PopupMessage(selfMessage);
_currentCookTimerTime = 10;
ClickSound();
_uiDirty = true;
wzhzhzh();
return SuicideKind.Heat;
}
}
}