Microwave Refactor and Prediction (#43129)

* adjust fields of meal recipe prototype

* document microwave recipe datafields

* use file scoped namespace for microwave recipes

* more of this

* localize every microwave recipe

* make microwave system shared

* refactor recipe reagent contents

* refactor recipe solids + add material ingredient datafield

* im making this a bit less silly

* awa

* move this shit out

* move this shit out also

* refactor Wzhzhzh

* im gonna shorten this parameter by 1 actually

* move this into recipes

* make processcontents a little better(?)

* clean imports

* rename a struct

* use cooking ingredients struct

* comment

* use whitelist for this

* unused dependency

* rearranged the parameters of get recipe portions

* rearrange some stuff

* clean up more imports

* move container related functions into separate file

* move this function out of ProcessContents

* prevent microwaves from having items inserted while active

* use IsPowered in microwave system

* use byref syntax and Entity<>

* moving the override up
sorry i like it being up here

* refactor microwave updating

* clean up recipe finish logic

* im renaming this function actually

* move some functions around

* move more stuff around

* some comments

* fix compiler error

* fix the yml of all microwave recipes

* use materials instead of solids for this stuff

* oops

* fix this

* turn microwave sparks into datafield

* move activelymicrowaved events into separate folder

* make UpdateUserInterfaceState use entity only

* rearrange some functions

* move UI functions out

* comment microwave UI stuff

* more comments

* comment recipe functions

* refactor GetTotalIngredients to use an operator

* give CookingIngredients addition methods

* comment the rest of CookingIngredients

* make subtraction functions clearer?

* remove redundant comment

* handle zero/negative ingredient cases

* simplify subtract contents logic

* fix these comments

* typo

* add microwave recipe test

* move active microwave methods

* comment active microwave methods

* comment cooking methods

* comment microwave methods

* change setappearance to use entity<>

* add some public api decorators

* comment all of MicrowaveComponent

* use file-scoped namespace for MicrowaveComponent

* comment

* fix baguette sword

* maybe...?

* oh oops

* document the rest of the owl

* kill the massive YML breaking change

* fix protoman dependency

* move shared microwave stuff to SharedMicrowaveSystem

* fix usings

* move microwave components to shared

* fix up microwave datafields

* move APIs to shared

* move activemicrowave methods to shared

* initialize on shared

* move ui functions to shared

* add actor here

* stuff

* predict microwave update loop

* make clientside microwave system

* adjust the update loop for microwaves

* clean up

* add field deltas to microwaves

* dirty fields instead of whole component

* move this to the partial

* move GetRecipesForMicrowave to shared

* comments

* predict microwave BUI

* oops

* refactor microwave bui update

* move UI messages to shared

* move messages to UI file

* move init to shared

* move container methosd to shared

* oops my using

* move cooking functions into partial

* move cooking methods to shared

* move microwave recipe methods to shared

* move the rest of the microwave events to shared

* update imports

* remove custom type serializer

* better (?) microwave UI prediction

* fix list notations

* OK IT WAS THE DATAFIELDS

* add IOC inject

* dont use IOC for htis

* sealed

* prediction fixes pt1

* make microwave cooking updates more consistent

* datafield tweaks

* test for multiple portions in microwave recipes

* test for multiple recipe portions

* better(?) prediction for malfunctions

* remove redundant comment

* prevent solution reactions in microwave

* cleanup some microwave stuff

* eh i think this should be container actually

* this can be API i think

* reduce system redundancy

* this has to be public

* use resolve pattern for explode

* use GetMicrowaveContents in microwave system

* this can be an API too

* add conversion script for unlocalized recipes

* use EnsureComp instead of AddComp

* suffering

* fix recipe prediction

* fix the audio

* reduce redundant networking

* i think this should be autopaused?

* nevermind we use dynamic portions in the test

* oops i wrote the test wrong

* make lemoon cake take 2 lemoons again
sorry test fail

* rename "materials" to "stacks"
realized theyre different

* add stack guidebook quantities

* Kill audio

* fix lemoon cake recipe for real?

* mark PortionedRecipe as a datadefinition

* fix the microwave timer

* review

* cleanup

* Update SharedMicrowaveSystem.Recipes.cs

* update

* add audio params

* review: inject microwavesystem

* review: named variable

* review: move dependencies

* review: use gametest for recipe test

* review: make access attributes standalone

* rename prototypes and datafields

* review: rename this var

* review: move this function down

* review: remove these scripts

* actually this should stay

* refactor cook time buttons

* review: use a color name for this

* fix timer reset

* Dirty

* review: make ActivelyMicrowavedComponent networked

* review: slarti comments part 1

* review: remove serializable attribute from CookingIngredients

* rename file

* review: more microwave audio boilerplate. yippee!

* review: use resolve pattern for TryGetStackId
i dont like the way this makes the rest of the code look but we ball maybe. i dont know what else to do here

* review: move ui update to constructor

* review: kill LastCookUpdated

* actually i do this instead

* fixed up the PR

* cleanup

* ///

* Update microwave.yml

---------

Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com>
This commit is contained in:
portfiend 2026-10-04 23:38:48 +00:00 • committed by GitHub
parent 09ac03b932
commit b9b794deef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 2947 additions and 1515 deletions

View file

@ -39,10 +39,10 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
public GuideMicrowaveEmbed(string recipe) : this()
{
GenerateControl(_prototype.Index<FoodRecipePrototype>(recipe));
GenerateControl(_prototype.Index<MicrowaveMealRecipePrototype>(recipe));
}
public GuideMicrowaveEmbed(FoodRecipePrototype recipe) : this()
public GuideMicrowaveEmbed(MicrowaveMealRecipePrototype recipe) : this()
{
GenerateControl(recipe);
}
@ -66,7 +66,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
return false;
}
if (!_prototype.TryIndex<FoodRecipePrototype>(id, out var recipe))
if (!_prototype.TryIndex<MicrowaveMealRecipePrototype>(id, out var recipe))
{
_sawmill.Error($"Specified recipe prototype \"{id}\" is not a valid recipe prototype");
return false;
@ -78,7 +78,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
return true;
}
private void GenerateHeader(FoodRecipePrototype recipe)
private void GenerateHeader(MicrowaveMealRecipePrototype recipe)
{
var entity = _prototype.Index<EntityPrototype>(recipe.Result);
@ -89,9 +89,9 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
ResultDescription.SetMarkup(entity.Description);
}
private void GenerateSolidIngredients(FoodRecipePrototype recipe)
private void GenerateSolidIngredients(MicrowaveMealRecipePrototype recipe)
{
foreach (var (product, amount) in recipe.IngredientsSolids.OrderByDescending(p => p.Value))
foreach (var (product, amount) in recipe.Ingredients.Solids.OrderByDescending(p => p.Value))
{
var ingredient = _prototype.Index<EntityPrototype>(product);
@ -122,9 +122,9 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
}
}
private void GenerateLiquidIngredients(FoodRecipePrototype recipe)
private void GenerateLiquidIngredients(MicrowaveMealRecipePrototype recipe)
{
foreach (var (product, amount) in recipe.IngredientsReagents.OrderByDescending(p => p.Value))
foreach (var (product, amount) in recipe.Ingredients.Reagents.OrderByDescending(p => p.Value))
{
var reagent = _prototype.Index<ReagentPrototype>(product);
@ -165,22 +165,58 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
}
}
private void GenerateIngredients(FoodRecipePrototype recipe)
private void GenerateStackIngredients(MicrowaveMealRecipePrototype recipe)
{
foreach (var (product, amount) in recipe.Ingredients.Stacks.OrderByDescending(p => p.Value))
{
var stack = _prototype.Index(product);
// stack icon
IngredientsGrid.AddChild(new GuideEntityEmbed(stack.Spawn, false, false));
// stack name
var stackName = Loc.GetString(stack.Name);
var stackNameMsg = new FormattedMessage();
stackNameMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-stack-name-display", ("stack", stackName)));
stackNameMsg.Pop();
var stackNameLabel = new RichTextLabel();
stackNameLabel.SetMessage(stackNameMsg);
IngredientsGrid.AddChild(stackNameLabel);
// stack quantity
var stackQuantityMsg = new FormattedMessage();
stackQuantityMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-stack-quantity-display", ("amount", amount)));
stackQuantityMsg.Pop();
var stackQuantityLabel = new RichTextLabel();
stackQuantityLabel.SetMessage(stackQuantityMsg);
IngredientsGrid.AddChild(stackQuantityLabel);
}
}
private void GenerateIngredients(MicrowaveMealRecipePrototype recipe)
{
GenerateLiquidIngredients(recipe);
GenerateStackIngredients(recipe);
GenerateSolidIngredients(recipe);
}
private void GenerateCookTime(FoodRecipePrototype recipe)
private void GenerateCookTime(MicrowaveMealRecipePrototype recipe)
{
var msg = new FormattedMessage();
msg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-cook-time", ("time", recipe.CookTime)));
msg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-cook-time", ("time", recipe.Time)));
msg.Pop();
CookTimeLabel.SetMessage(msg);
}
private void GenerateControl(FoodRecipePrototype recipe)
private void GenerateControl(MicrowaveMealRecipePrototype recipe)
{
GenerateHeader(recipe);
GenerateIngredients(recipe);

View file

@ -50,7 +50,7 @@ public sealed partial class GuideMicrowaveGroupEmbed : BoxContainer, IDocumentTa
private void CreateEntries(string group)
{
var prototypes = _prototype.EnumeratePrototypes<FoodRecipePrototype>()
var prototypes = _prototype.EnumeratePrototypes<MicrowaveMealRecipePrototype>()
.Where(p => p.Group.Equals(group))
.OrderBy(p => p.Name);

View file

@ -0,0 +1,52 @@
using Content.Shared.Kitchen.Components;
using Content.Shared.Kitchen.EntitySystems;
using Robust.Client.Player;
namespace Content.Client.Kitchen.EntitySystems;
/// <inheritdoc />
public sealed partial class ClientMicrowaveSystem : MicrowaveSystem
{
[Dependency] private IPlayerManager _player = default!;
[Dependency] private SharedUserInterfaceSystem _userInterface = default!;
public override void UpdateUI(Entity<MicrowaveComponent?> microwave)
{
base.UpdateUI(microwave);
if (!MicrowaveQuery.Resolve(microwave.Owner, ref microwave.Comp))
return;
if (_userInterface.TryGetOpenUi(microwave.Owner, MicrowaveUiKey.Key, out var bui))
bui.Update();
}
protected override void ActivateMicrowaveCycle(Entity<MicrowaveComponent> ent)
{
base.ActivateMicrowaveCycle(ent);
if (!Timing.IsFirstTimePredicted)
return;
var audioParams = ent.Comp.LoopingSound.Params;
audioParams = audioParams.WithLoop(true).WithMaxDistance(5);
ent.Comp.PlayingStream ??= Audio.PlayLocal(ent.Comp.LoopingSound, ent, _player.LocalEntity, audioParams)?.Entity;
}
protected override void DeactivateMicrowaveCycle(Entity<MicrowaveComponent> ent)
{
base.DeactivateMicrowaveCycle(ent);
// We have this check here so Audio.Stop doesn't mistakenly tell us that the entity has been deleted when it hasn't been!!!
if (!Timing.IsFirstTimePredicted)
return;
ent.Comp.PlayingStream = Audio.Stop(ent.Comp.PlayingStream);
foreach (var solid in GetMicrowaveContents(ent.AsNullable()))
{
RemComp<ActivelyMicrowavedComponent>(solid);
}
}
}

View file

@ -1,151 +1,128 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Kitchen.Components;
using Content.Shared.Kitchen.EntitySystems;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Kitchen.UI
namespace Content.Client.Kitchen.UI;
[UsedImplicitly]
public sealed partial class MicrowaveBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[UsedImplicitly]
public sealed class MicrowaveBoundUserInterface : BoundUserInterface
[Dependency] private SpriteSystem _sprite = default!;
[Dependency] private MicrowaveSystem _microwave = default!;
[ViewVariables]
private MicrowaveMenu? _menu;
[ViewVariables]
private readonly Dictionary<int, EntityUid> _solids = new();
protected override void Open()
{
[ViewVariables]
private MicrowaveMenu? _menu;
base.Open();
[ViewVariables]
private readonly Dictionary<int, EntityUid> _solids = new();
[ViewVariables]
private readonly Dictionary<int, ReagentQuantity> _reagents = new();
public MicrowaveBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
_menu = this.CreateWindow<MicrowaveMenu>();
_menu.StartButton.OnPressed += _ => SendPredictedMessage(new MicrowaveStartCookMessage());
_menu.EjectButton.OnPressed += _ => SendPredictedMessage(new MicrowaveEjectMessage());
_menu.IngredientsList.OnItemSelected += args =>
{
}
SendPredictedMessage(new MicrowaveEjectSolidIndexedMessage(EntMan.GetNetEntity(_solids[args.ItemIndex])));
};
protected override void Open()
_menu.OnCookTimeSelected += (name, index, cookTime) =>
{
base.Open();
_menu = this.CreateWindow<MicrowaveMenu>();
_menu.StartButton.OnPressed += _ => SendPredictedMessage(new MicrowaveStartCookMessage());
_menu.EjectButton.OnPressed += _ => SendPredictedMessage(new MicrowaveEjectMessage());
_menu.IngredientsList.OnItemSelected += args =>
{
SendPredictedMessage(new MicrowaveEjectSolidIndexedMessage(EntMan.GetNetEntity(_solids[args.ItemIndex])));
};
_menu.OnCookTimeSelected += (args, buttonIndex) =>
{
var selectedCookTime = (uint) 0;
if (args.Button is MicrowaveMenu.MicrowaveCookTimeButton microwaveCookTimeButton)
{
// args.Button is a MicrowaveCookTimeButton
var actualButton = (MicrowaveMenu.MicrowaveCookTimeButton) args.Button;
selectedCookTime = actualButton.CookTime == 0 ? 0 : actualButton.CookTime;
// SendMessage(new MicrowaveSelectCookTimeMessage((int) selectedCookTime / 5, actualButton.CookTime));
SendPredictedMessage(new MicrowaveSelectCookTimeMessage((int) selectedCookTime / 5, actualButton.CookTime));
_menu.CookTimeInfoLabel.Text = Loc.GetString("microwave-bound-user-interface-cook-time-label",
("time", selectedCookTime));
}
else
{
// args.Button is a normal button aka instant cook button
SendPredictedMessage(new MicrowaveSelectCookTimeMessage((int) selectedCookTime, 0));
_menu.CookTimeInfoLabel.Text = Loc.GetString("microwave-bound-user-interface-cook-time-label",
("time", Loc.GetString("microwave-menu-instant-button")));
}
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not MicrowaveUpdateUserInterfaceState cState || _menu == null)
{
return;
}
_menu.IsBusy = cState.IsMicrowaveBusy;
_menu.CurrentCooktimeEnd = cState.CurrentCookTimeEnd;
_menu.ToggleBusyDisableOverlayPanel(cState.IsMicrowaveBusy || cState.ContainedSolids.Length == 0);
// TODO move this to a component state and ensure the net ids.
RefreshContentsDisplay(EntMan.GetEntityArray(cState.ContainedSolids));
//Set the cook time info label
var cookTime = cState.ActiveButtonIndex == 0
? Loc.GetString("microwave-menu-instant-button")
: cState.CurrentCookTime.ToString();
SendPredictedMessage(new MicrowaveSelectCookTimeMessage(index, cookTime));
_menu.CookTimeInfoLabel.Text = Loc.GetString("microwave-bound-user-interface-cook-time-label",
("time", cookTime));
_menu.StartButton.Disabled = cState.IsMicrowaveBusy || cState.ContainedSolids.Length == 0;
_menu.EjectButton.Disabled = cState.IsMicrowaveBusy || cState.ContainedSolids.Length == 0;
("time", name));
};
Update();
}
//Set the correct button active button
if (cState.ActiveButtonIndex == 0)
{
_menu.InstantCookButton.Pressed = true;
}
else
{
var currentlySelectedTimeButton = (Button) _menu.CookTimeButtonVbox.GetChild(cState.ActiveButtonIndex - 1);
currentlySelectedTimeButton.Pressed = true;
}
public override void Update()
{
base.Update();
//Set the "micowave light" ui color to indicate if the microwave is busy or not
if (cState.IsMicrowaveBusy && cState.ContainedSolids.Length > 0)
{
_menu.IngredientsPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#947300") };
}
else
{
_menu.IngredientsPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#1B1B1E") };
}
}
if (_menu is null || !EntMan.TryGetComponent<MicrowaveComponent>(Owner, out var comp))
return;
private void RefreshContentsDisplay(EntityUid[] containedSolids)
RefreshContentsDisplay();
UpdateActiveDisplay(comp);
// Update the currently-selected cook time label and button
var buttonIndex = comp.CurrentCookTimeButtonIndex;
if (_menu.TryGetCookTimeButton(buttonIndex, out var button))
button.Pressed = true;
var timeLabel = buttonIndex == 0
? Loc.GetString("microwave-menu-instant-button")
: comp.CurrentCookTimerTime.ToString();
_menu.CookTimeInfoLabel.Text = Loc.GetString("microwave-bound-user-interface-cook-time-label",
("time", timeLabel));
}
/// <summary>
/// Update the state of various controls in this menu based on the active / empty status of the microwave.
/// </summary>
private void UpdateActiveDisplay(MicrowaveComponent? comp)
{
if (_menu is null)
return;
// Disable various UI controls if the microwave is active or empty
var isActive = EntMan.TryGetComponent<ActiveMicrowaveComponent>(Owner, out var activeComp);
var isEmpty = !_microwave.HasContents((Owner, comp));
_menu.IsBusy = isActive;
_menu.ToggleBusyDisableOverlayPanel(isActive);
_menu.StartButton.Disabled = isActive || isEmpty;
_menu.EjectButton.Disabled = isActive || isEmpty;
if (activeComp != null)
_menu.CurrentCooktimeEnd = activeComp.CookTimeEnd;
// Set the "microwave light" panel color
_menu.SetIngredientPanelLight(isActive && !isEmpty);
}
/// <summary>
/// Update the panel containing all of the microwave's contents.
/// </summary>
private void RefreshContentsDisplay()
{
if (_menu == null)
return;
_solids.Clear();
_menu.IngredientsList.Clear();
foreach (var entity in _microwave.GetMicrowaveContents(Owner))
{
_reagents.Clear();
if (EntMan.Deleted(entity))
continue;
if (_menu == null) return;
_solids.Clear();
_menu.IngredientsList.Clear();
foreach (var entity in containedSolids)
{
if (EntMan.Deleted(entity))
{
return;
}
// TODO just use sprite view
Texture? texture;
if (EntMan.TryGetComponent<IconComponent>(entity, out var iconComponent))
{
texture = EntMan.System<SpriteSystem>().GetIcon(iconComponent);
}
else if (EntMan.TryGetComponent<SpriteComponent>(entity, out var spriteComponent))
{
texture = spriteComponent.Icon?.Default;
}
else
{
continue;
}
var solidItem = _menu.IngredientsList.AddItem(EntMan.GetComponent<MetaDataComponent>(entity).EntityName, texture);
var solidIndex = _menu.IngredientsList.IndexOf(solidItem);
_solids.Add(solidIndex, entity);
}
// TODO just use sprite view
var itemIcon = GetEntityIcon(entity);
var itemName = EntMan.GetComponent<MetaDataComponent>(entity).EntityName;
var solidItem = _menu.IngredientsList.AddItem(itemName, itemIcon);
var solidIndex = _menu.IngredientsList.IndexOf(solidItem);
_solids.Add(solidIndex, entity);
}
}
/// <summary>
/// Get the texture associated with an ingredient.
/// </summary>
/// <param name="uid">The ingredient entity.</param>
private Texture? GetEntityIcon(EntityUid uid)
{
if (EntMan.TryGetComponent<IconComponent>(uid, out var icon))
return _sprite.GetIcon(icon);
if (EntMan.TryGetComponent<SpriteComponent>(uid, out var sprite))
return sprite.Icon?.Default;
return null;
}
}

View file

@ -0,0 +1,9 @@
<Button xmlns="https://spacestation14.io"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
TextAlign="Center"
ToggleMode="True"
HorizontalExpand="True">
<Button.StyleClasses>
<system:String>OpenBoth</system:String>
</Button.StyleClasses>
</Button>

View file

@ -0,0 +1,39 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Kitchen.UI;
[GenerateTypedNameReferences]
public sealed partial class MicrowaveCookTimeButton : Button
{
public event Action<string, uint>? OnCookTimeSelected;
private uint _cookTime = 0;
public uint CookTime
{
get => _cookTime;
set
{
_cookTime = value;
UpdateLabel();
}
}
public MicrowaveCookTimeButton()
{
RobustXamlLoader.Load(this);
UpdateLabel();
OnPressed += _ => OnCookTimeSelected?.Invoke(Text ?? string.Empty, _cookTime);
}
private void UpdateLabel()
{
var labelText = _cookTime == 0
? Loc.GetString("microwave-menu-instant-button")
: _cookTime.ToString();
Text = labelText;
}
}

View file

@ -2,6 +2,7 @@
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:kitchen="clr-namespace:Content.Client.Kitchen.UI"
Title="{Loc 'microwave-menu-title'}"
MinWidth="512"
MinSize="512 256">
@ -49,15 +50,11 @@
Align="Center"
Modulate="#FFFFFF"
VAlign="Center" />
<BoxContainer Orientation="Vertical">
<Button
<BoxContainer Orientation="Vertical" SeparationOverride="1">
<kitchen:MicrowaveCookTimeButton
Name="InstantCookButton"
Access="Public"
Text="{Loc 'microwave-menu-instant-button'}"
StyleClasses="OpenLeft"
ToggleMode="True"
Margin="0 0 2 0"
TextAlign="Center" />
CookTime="0"
StyleClasses="OpenBoth"/>
<GridContainer
Name="CookTimeButtonVbox"
HSeparationOverride="1"
@ -71,6 +68,7 @@
<BoxContainer
Orientation="Vertical"
Align="Center"
SeparationOverride="1"
SizeFlagsStretchRatio="3">
<Control MinSize="0 15" />
<Button

View file

@ -1,83 +1,118 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Kitchen.UI
namespace Content.Client.Kitchen.UI;
[GenerateTypedNameReferences]
public sealed partial class MicrowaveMenu : FancyWindow
{
[GenerateTypedNameReferences]
public sealed partial class MicrowaveMenu : FancyWindow
[Dependency] private IGameTiming _timing = default!;
public event Action<string, int, uint>? OnCookTimeSelected;
public ButtonGroup CookTimeButtonGroup { get; }
public bool IsBusy;
public TimeSpan CurrentCooktimeEnd;
private readonly StyleBoxFlat _ingredientsStylebox = new();
public MicrowaveMenu()
{
[Dependency] private IGameTiming _timing = default!;
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
public event Action<BaseButton.ButtonEventArgs, int>? OnCookTimeSelected;
IngredientsPanel.PanelOverride = _ingredientsStylebox;
public ButtonGroup CookTimeButtonGroup { get; }
CookTimeButtonGroup = new ButtonGroup();
InstantCookButton.Group = CookTimeButtonGroup;
InstantCookButton.OnCookTimeSelected += (name, cookTime) => OnCookTimeButtonPressed(name, 0, cookTime);
public bool IsBusy;
public TimeSpan CurrentCooktimeEnd;
public MicrowaveMenu()
for (var i = 1; i <= 6; i++)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
CookTimeButtonGroup = new ButtonGroup();
InstantCookButton.Group = CookTimeButtonGroup;
InstantCookButton.OnPressed += args =>
var cookTime = i * 5;
var newButton = new MicrowaveCookTimeButton
{
OnCookTimeSelected?.Invoke(args, 0);
CookTime = (uint)cookTime,
Group = CookTimeButtonGroup
};
for (var i = 1; i <= 6; i++)
{
var newButton = new MicrowaveCookTimeButton
{
Text = (i * 5).ToString(),
TextAlign = Label.AlignMode.Center,
ToggleMode = true,
CookTime = (uint) (i * 5),
Group = CookTimeButtonGroup,
HorizontalExpand = true,
};
if (i == 4)
{
newButton.StyleClasses.Add("OpenRight");
}
else
{
newButton.StyleClasses.Add("OpenBoth");
}
CookTimeButtonVbox.AddChild(newButton);
newButton.OnPressed += args =>
{
OnCookTimeSelected?.Invoke(args, i);
};
}
}
public void ToggleBusyDisableOverlayPanel(bool shouldDisable)
{
DisableCookingPanelOverlay.Visible = shouldDisable;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!IsBusy)
return;
if (CurrentCooktimeEnd > _timing.CurTime)
{
CookTimeInfoLabel.Text = Loc.GetString("microwave-bound-user-interface-cook-time-label",
("time", CurrentCooktimeEnd.Subtract(_timing.CurTime).Seconds));
}
}
public sealed class MicrowaveCookTimeButton : Button
{
public uint CookTime;
newButton.OnCookTimeSelected += (name, cookTime) => OnCookTimeButtonPressed(name, i, cookTime);
CookTimeButtonVbox.AddChild(newButton);
}
}
private void OnCookTimeButtonPressed(string name, int index, uint cookTime)
{
OnCookTimeSelected?.Invoke(name, index, cookTime);
}
/// <summary>
/// Set the visibility of the "busy overlay panel" over the microwave's ingredients,
/// indicating to the player that the ingredients list cannot be interacted with
/// (namely while the microwave is active.)
/// </summary>
/// <param name="shouldDisable">Whether or not this microwave is "busy".</param>
public void ToggleBusyDisableOverlayPanel(bool shouldDisable)
{
DisableCookingPanelOverlay.Visible = shouldDisable;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!IsBusy)
return;
var remaining = CurrentCooktimeEnd - _timing.CurTime;
if (remaining > TimeSpan.Zero)
{
CookTimeInfoLabel.Text = Loc.GetString("microwave-bound-user-interface-cook-time-label",
("time", Math.Ceiling(remaining.TotalSeconds)));
}
}
/// <summary>
/// Set the "color light" state of the ingredient panel's ingredient box.
/// </summary>
/// <param name="value">Wheether or not the panel is "lit" (yellow) or "unlit" (gray).</param>
public void SetIngredientPanelLight(bool value)
{
var panelColor = value
? Color.DarkGoldenrod
: Color.FromHex("#1B1B1E");
_ingredientsStylebox.BackgroundColor = panelColor;
}
/// <summary>
/// Attempt to get the cook time button at the given index.
/// </summary>
/// <param name="index">The index to select.</param>
/// <param name="button">The cook time button, if found.</param>
/// <returns>Whether or not this operation was successful.</returns>
public bool TryGetCookTimeButton(int index,
[NotNullWhen(true)] out Button? button)
{
button = null;
if (index == 0)
button = InstantCookButton;
else
{
if (index <= CookTimeButtonVbox.ChildCount)
{
var control = CookTimeButtonVbox.GetChild(index - 1);
if (control is Button timeButton)
button = timeButton;
}
}
return button != null;
}
}

View file

@ -0,0 +1,86 @@
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.IntegrationTests.Utility;
using Content.Server.Kitchen.EntitySystems;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Kitchen;
/// <summary>
/// Integration tests related to microwaves and microwave recipes.
/// </summary>
public sealed class MicrowaveRecipeTest : GameTest
{
[SidedDependency(Side.Server)] private readonly ServerMicrowaveSystem _microwave = null!;
private static readonly string[] Recipes = GameDataScrounger.PrototypesOfKind<MicrowaveMealRecipePrototype>();
private static readonly EntProtoId MicrowavePrototype = "KitchenMicrowave";
private const uint MaxSeconds = 30;
[Test]
[TestOf(typeof(ServerMicrowaveSystem))]
[TestCaseSource(nameof(Recipes))]
[Description("Checks whether a microwave recipe's ingredients will create that recipe in the microwave.")]
public async Task AllRecipeIngredientsMakeRecipe(string protoKey)
{
var server = Pair.Server;
// Spawn the microwave we will use for our recipes.
var microwave = await Spawn(MicrowavePrototype);
var microwaveString = SEntMan.ToPrettyString(microwave);
await server.WaitAssertion(() =>
{
Assert.That(SEntMan.HasComponent<MicrowaveComponent>(microwave),
$"Microwave entity {microwaveString} lacks a {nameof(MicrowaveComponent)}!");
// Get the parameters we need to make this recipe.
var proto = SProtoMan.Index<MicrowaveMealRecipePrototype>(protoKey);
var maxPortions = MaxSeconds / proto.Time;
// Ensure this recipe is provided to the microwave if this is a secret recipe.
if (proto.SecretRecipe)
{
var recipeProvider = SEntMan.EnsureComponent<FoodRecipeProviderComponent>(microwave);
recipeProvider.ProvidedRecipes.Add(protoKey);
}
// First, test that a single portion works.
ValidateRecipePortions(proto, 1, microwave);
// Then, test that making multiple portions of the same recipe works.
if (maxPortions > 1)
ValidateRecipePortions(proto, maxPortions, microwave);
});
}
private void ValidateRecipePortions(MicrowaveMealRecipePrototype prototype, uint portions, EntityUid microwave)
{
var ingredients = prototype.Ingredients * portions;
var cookTime = prototype.Time * portions;
var portionedRecipe = _microwave.GetRecipe(microwave, ingredients, cookTime);
var microwaveString = SEntMan.ToPrettyString(microwave);
var recipeDebugString = $"Ingredients for {nameof(MicrowaveMealRecipePrototype)} {prototype.ID}";
using (Assert.EnterMultipleScope())
{
// Tried to get a recipe for these ingredients, but no valid recipe was found.
Assert.That(portionedRecipe, Is.Not.Null,
$"{recipeDebugString} did not resolve to a recipe in {microwaveString} in {portions} portions!");
var recipe = portionedRecipe.Value.Recipe;
var count = portionedRecipe.Value.Count;
// Resulted in a different recipe instead.
Assert.That(recipe, Is.EqualTo(prototype.ID),
$"{recipeDebugString} resulted in an incorrect recipe for {microwaveString} in {portions} portions!");
// Recipe portion count does not match the amount we're trying to make.
Assert.That(count, Is.EqualTo(portions),
$"{recipeDebugString} resulted in {count} recipe portions for {microwaveString}! Expected: {portions}");
}
}
}

View file

@ -24,7 +24,7 @@ public sealed class WizdenContentFreeze : GameTest
var protoMan = server.ProtoMan;
var recipesCount = protoMan.Count<FoodRecipePrototype>();
var recipesCount = protoMan.Count<MicrowaveMealRecipePrototype>();
var recipesLimit = 218;
if (recipesCount > recipesLimit)

View file

@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Chat.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Access;
using Content.Shared.Access.Components;
@ -9,9 +8,10 @@ using Content.Shared.Access.Systems;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Kitchen.EntitySystems;
using Content.Shared.Popups;
using Robust.Shared.Random;
using Content.Server.Kitchen.EntitySystems;
namespace Content.Server.Access.Systems;

View file

@ -1,25 +0,0 @@
using Content.Shared.Kitchen;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Kitchen.Components;
/// <summary>
/// Attached to a microwave that is currently in the process of cooking
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class ActiveMicrowaveComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
public float CookTimeRemaining;
[ViewVariables(VVAccess.ReadWrite)]
public float TotalTime;
[ViewVariables(VVAccess.ReadWrite)]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan MalfunctionTime = TimeSpan.Zero;
[ViewVariables]
public (FoodRecipePrototype?, int) PortionedRecipe;
}

View file

@ -1,113 +0,0 @@
using Content.Shared.Construction.Prototypes;
using Content.Shared.DeviceLinking;
using Content.Shared.Item;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.Server.Kitchen.Components
{
[RegisterComponent]
public sealed partial class MicrowaveComponent : Component
{
[DataField]
public float CookTimeMultiplier = 1;
[DataField]
public float BaseHeatMultiplier = 100;
[DataField]
public float ObjectHeatMultiplier = 100;
[DataField("failureResult")]
public EntProtoId BadRecipeEntityId = "FoodBadRecipe";
#region audio
[DataField("beginCookingSound")]
public SoundSpecifier StartCookingSound = new SoundPathSpecifier("/Audio/Machines/microwave_start_beep.ogg");
[DataField]
public SoundSpecifier FoodDoneSound = new SoundPathSpecifier("/Audio/Machines/microwave_done_beep.ogg");
[DataField]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
[DataField("ItemBreakSound")]
public SoundSpecifier ItemBreakSound = new SoundPathSpecifier("/Audio/Effects/clang.ogg");
public EntityUid? PlayingStream;
[DataField]
public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg");
#endregion
[DataField]
public bool Broken;
[DataField]
public ProtoId<SinkPortPrototype> OnPort = "On";
/// <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>
[DataField]
public uint CurrentCookTimerTime = 0;
/// <summary>
/// Tracks the elapsed time of the current cook timer.
/// </summary>
[DataField]
public TimeSpan CurrentCookTimeEnd = TimeSpan.Zero;
/// <summary>
/// The maximum number of seconds a microwave can be set to.
/// This is currently only used for validation and the client does not check this.
/// </summary>
[DataField]
public uint MaxCookTime = 30;
/// <summary>
/// The max temperature that this microwave can heat objects to.
/// </summary>
[DataField]
public float TemperatureUpperThreshold = 373.15f;
public int CurrentCookTimeButtonIndex;
public Container Storage = default!;
[DataField]
public string ContainerId = "microwave_entity_container";
[DataField]
public int Capacity = 10;
[DataField]
public ProtoId<ItemSizePrototype> MaxItemSize = "Normal";
/// <summary>
/// How frequently the microwave can malfunction.
/// </summary>
[DataField]
public float MalfunctionInterval = 1.0f;
/// <summary>
/// Chance of an explosion occurring when we microwave a metallic object
/// </summary>
[DataField]
public float ExplosionChance = .1f;
/// <summary>
/// Chance of lightning occurring when we microwave a metallic object
[DataField]
public float LightningChance = .75f;
/// <summary>
/// If this microwave can give ids accesses without exploding
/// </summary>
[DataField]
public bool CanMicrowaveIdsSafely = true;
}
}

View file

@ -1,761 +0,0 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Construction;
using Content.Server.Construction.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Hands.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Lightning;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Temperature.Systems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stacks;
using Content.Shared.Suicide;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Kitchen.EntitySystems
{
public sealed partial class MicrowaveSystem : EntitySystem
{
[Dependency] private DeviceLinkSystem _deviceLink = default!;
[Dependency] private SharedPopupSystem _popupSystem = default!;
[Dependency] private PowerReceiverSystem _power = default!;
[Dependency] private RecipeManager _recipeManager = default!;
[Dependency] private SharedAppearanceSystem _appearance = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private LightningSystem _lightning = default!;
[Dependency] private IRobustRandom _random = default!;
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private ExplosionSystem _explosion = default!;
[Dependency] private SharedContainerSystem _container = default!;
[Dependency] private SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private TagSystem _tag = default!;
[Dependency] private TemperatureSystem _temperature = default!;
[Dependency] private UserInterfaceSystem _userInterface = default!;
[Dependency] private HandsSystem _handsSystem = default!;
[Dependency] private SharedItemSystem _item = default!;
[Dependency] private SharedStackSystem _stack = default!;
[Dependency] private IAdminLogManager _adminLogger = default!;
[Dependency] private SharedSuicideSystem _suicide = default!;
[Dependency] private SharedPowerStateSystem _powerState = default!;
private static readonly EntProtoId MalfunctionSpark = "Spark";
private static readonly ProtoId<TagPrototype> MetalTag = "Metal";
private static readonly ProtoId<TagPrototype> PlasticTag = "Plastic";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MicrowaveComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<MicrowaveComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<MicrowaveComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<MicrowaveComponent, EntInsertedIntoContainerMessage>(OnContentUpdate);
SubscribeLocalEvent<MicrowaveComponent, EntRemovedFromContainerMessage>(OnContentUpdate);
SubscribeLocalEvent<MicrowaveComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(AnchorableSystem) });
SubscribeLocalEvent<MicrowaveComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<MicrowaveComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<MicrowaveComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<MicrowaveComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<MicrowaveComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<MicrowaveComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<MicrowaveComponent, MicrowaveStartCookMessage>((u, c, m) => Wzhzhzh(u, c, m.Actor));
SubscribeLocalEvent<MicrowaveComponent, MicrowaveEjectMessage>(OnEjectMessage);
SubscribeLocalEvent<MicrowaveComponent, MicrowaveEjectSolidIndexedMessage>(OnEjectIndex);
SubscribeLocalEvent<MicrowaveComponent, MicrowaveSelectCookTimeMessage>(OnSelectTime);
SubscribeLocalEvent<ActiveMicrowaveComponent, ComponentStartup>(OnCookStart);
SubscribeLocalEvent<ActiveMicrowaveComponent, ComponentShutdown>(OnCookStop);
SubscribeLocalEvent<ActiveMicrowaveComponent, EntInsertedIntoContainerMessage>(OnActiveMicrowaveInsert);
SubscribeLocalEvent<ActiveMicrowaveComponent, EntRemovedFromContainerMessage>(OnActiveMicrowaveRemove);
SubscribeLocalEvent<ActivelyMicrowavedComponent, OnConstructionTemperatureEvent>(OnConstructionTemp);
SubscribeLocalEvent<ActivelyMicrowavedComponent, SolutionRelayEvent<ReactionAttemptEvent>>(OnReactionAttemptRelay);
SubscribeLocalEvent<ActivelyMicrowavedComponent, ReactionAttemptEvent>(OnReactionAttempt);
SubscribeLocalEvent<FoodRecipeProviderComponent, GetSecretRecipesEvent>(OnGetSecretRecipes);
}
private void OnCookStart(Entity<ActiveMicrowaveComponent> ent, ref ComponentStartup args)
{
if (!TryComp<MicrowaveComponent>(ent, out var microwaveComponent))
return;
SetAppearance(ent.Owner, MicrowaveVisualState.Cooking, microwaveComponent);
var audioParams = microwaveComponent.LoopingSound?.Params ?? AudioParams.Default;
audioParams = audioParams.WithLoop(true).WithMaxDistance(5);
microwaveComponent.PlayingStream = _audio.PlayPvs(microwaveComponent.LoopingSound, ent, audioParams)?.Entity;
_powerState.SetWorkingState(ent.Owner, true);
}
private void OnCookStop(Entity<ActiveMicrowaveComponent> ent, ref ComponentShutdown args)
{
if (!TryComp<MicrowaveComponent>(ent, out var microwaveComponent))
return;
SetAppearance(ent.Owner, MicrowaveVisualState.Idle, microwaveComponent);
microwaveComponent.PlayingStream = _audio.Stop(microwaveComponent.PlayingStream);
_powerState.SetWorkingState(ent.Owner, false);
}
private void OnActiveMicrowaveInsert(Entity<ActiveMicrowaveComponent> ent, ref EntInsertedIntoContainerMessage args)
{
var microwavedComp = AddComp<ActivelyMicrowavedComponent>(args.Entity);
microwavedComp.Microwave = ent.Owner;
}
private void OnActiveMicrowaveRemove(Entity<ActiveMicrowaveComponent> ent, ref EntRemovedFromContainerMessage args)
{
RemCompDeferred<ActivelyMicrowavedComponent>(args.Entity);
}
// Stop items from transforming through constructiongraphs while being microwaved.
// They might be reserved for a microwave recipe.
private void OnConstructionTemp(Entity<ActivelyMicrowavedComponent> ent, ref OnConstructionTemperatureEvent args)
{
args.Result = HandleResult.False;
}
private void OnReactionAttemptRelay(Entity<ActivelyMicrowavedComponent> ent, ref SolutionRelayEvent<ReactionAttemptEvent> args)
{
OnReactionAttempt(ent, ref args.Event);
}
// Stop reagents from reacting if they are currently reserved for a microwave recipe.
// For example Egg would cook into EggCooked, causing it to not being removed once we are done microwaving.
private void OnReactionAttempt(Entity<ActivelyMicrowavedComponent> ent, ref ReactionAttemptEvent args)
{
if (!TryComp<ActiveMicrowaveComponent>(ent.Comp.Microwave, out var activeMicrowaveComp))
return;
if (activeMicrowaveComp.PortionedRecipe.Item1 == null) // no recipe selected
return;
var recipeReagents = activeMicrowaveComp.PortionedRecipe.Item1.IngredientsReagents.Keys;
foreach (var reagent in recipeReagents)
{
if (args.Reaction.Reactants.ContainsKey(reagent))
{
args.Cancelled = true;
return;
}
}
}
/// <summary>
/// Adds temperature to every item in the microwave,
/// based on the time it took to microwave.
/// </summary>
/// <param name="component">The microwave that is heating up.</param>
/// <param name="time">The time on the microwave, in seconds.</param>
private void AddTemperature(MicrowaveComponent component, float time)
{
var heatToAdd = time * component.BaseHeatMultiplier;
foreach (var entity in component.Storage.ContainedEntities)
{
_temperature.ChangeHeat(entity, heatToAdd * component.ObjectHeatMultiplier, false);
foreach (var (_, soln) in _solutionContainer.EnumerateSolutions(entity))
{
var solution = soln.Comp.Solution;
if (solution.Temperature > component.TemperatureUpperThreshold)
continue;
_solutionContainer.AddThermalEnergy(soln, heatToAdd);
}
}
}
private void SubtractContents(MicrowaveComponent component, FoodRecipePrototype recipe)
{
// TODO Turn recipe.IngredientsReagents into a ReagentQuantity[]
var totalReagentsToRemove = new Dictionary<ProtoId<ReagentPrototype>, FixedPoint2>(recipe.IngredientsReagents);
// this is spaghetti ngl
foreach (var item in component.Storage.ContainedEntities)
{
// use the same reagents as when we selected the recipe
if (!_solutionContainer.TryGetDrainableSolution(item, out var solutionEntity, out var solution))
continue;
foreach (var (reagent, _) in recipe.IngredientsReagents)
{
// removed everything
if (!totalReagentsToRemove.ContainsKey(reagent))
continue;
var quant = solution.GetTotalPrototypeQuantity(reagent);
if (quant >= totalReagentsToRemove[reagent])
{
quant = totalReagentsToRemove[reagent];
totalReagentsToRemove.Remove(reagent);
}
else
{
totalReagentsToRemove[reagent] -= quant;
}
_solutionContainer.RemoveReagent(solutionEntity.Value, reagent, quant);
}
}
foreach (var recipeSolid in recipe.IngredientsSolids)
{
for (var i = 0; i < recipeSolid.Value; i++)
{
foreach (var item in component.Storage.ContainedEntities)
{
string? itemID = null;
// If an entity has a stack component, use the stacktype instead of prototype id
if (TryComp<StackComponent>(item, out var stackComp))
{
itemID = ProtoMan.Index(stackComp.StackTypeId).Spawn;
}
else
{
var metaData = MetaData(item);
if (metaData.EntityPrototype == null)
{
continue;
}
itemID = metaData.EntityPrototype.ID;
}
if (itemID != recipeSolid.Key)
{
continue;
}
if (stackComp is not null)
{
if (stackComp.Count == 1)
{
_container.Remove(item, component.Storage);
}
_stack.ReduceCount((item, stackComp), 1);
break;
}
else
{
_container.Remove(item, component.Storage);
Del(item);
break;
}
}
}
}
}
private void OnInit(Entity<MicrowaveComponent> ent, ref ComponentInit args)
{
// this really does have to be in ComponentInit
ent.Comp.Storage = _container.EnsureContainer<Container>(ent, ent.Comp.ContainerId);
}
private void OnMapInit(Entity<MicrowaveComponent> ent, ref MapInitEvent args)
{
_deviceLink.EnsureSinkPorts(ent, ent.Comp.OnPort);
}
/// <summary>
/// Kills the user by microwaving their head
/// TODO: Make this not awful, it keeps any items attached to your head still on and you can revive someone and cogni them so you have some dumb headless fuck running around. I've seen it happen.
/// </summary>
private void OnSuicideByEnvironment(Entity<MicrowaveComponent> ent, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
// The act of getting your head microwaved doesn't actually kill you
if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
return;
// The application of lethal damage is what kills you...
_suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Heat");
var victim = args.Victim;
var selfMessage = Loc.GetString("microwave-component-suicide-message");
var othersMessage = Loc.GetString("microwave-component-suicide-others-message", ("victim", Identity.Entity(victim, EntityManager)));
_popupSystem.PopupEntity(selfMessage, othersMessage, victim, victim);
var audioParams = ent.Comp.ClickSound?.Params ?? AudioParams.Default;
audioParams = audioParams.AddVolume(-2);
_audio.PlayPvs(ent.Comp.ClickSound, ent.Owner, audioParams);
ent.Comp.CurrentCookTimerTime = 10;
Wzhzhzh(ent.Owner, ent.Comp, args.Victim);
UpdateUserInterfaceState(ent.Owner, ent.Comp);
args.Handled = true;
}
private void OnSolutionChange(Entity<MicrowaveComponent> ent, ref SolutionChangedEvent args)
{
UpdateUserInterfaceState(ent, ent.Comp);
}
private void OnContentUpdate(EntityUid uid, MicrowaveComponent component, ContainerModifiedMessage args) // For some reason ContainerModifiedMessage just can't be used at all with Entity<T>. TODO: replace with Entity<T> syntax once that's possible
{
if (component.Storage != args.Container)
return;
UpdateUserInterfaceState(uid, component);
}
private void OnInsertAttempt(Entity<MicrowaveComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (args.Container.ID != ent.Comp.ContainerId)
return;
if (ent.Comp.Broken)
{
args.Cancel();
return;
}
if (TryComp<ItemComponent>(args.EntityUid, out var item))
{
if (_item.GetSizePrototype(item.Size) > _item.GetSizePrototype(ent.Comp.MaxItemSize))
{
args.Cancel();
return;
}
}
else
{
args.Cancel();
return;
}
if (ent.Comp.Storage.Count >= ent.Comp.Capacity)
args.Cancel();
}
private void OnInteractUsing(Entity<MicrowaveComponent> ent, ref InteractUsingEvent args)
{
if (args.Handled)
return;
if (!(TryComp<ApcPowerReceiverComponent>(ent, out var apc) && apc.Powered))
{
_popupSystem.PopupEntity(Loc.GetString("microwave-component-interact-using-no-power"), ent, args.User);
return;
}
if (ent.Comp.Broken)
{
_popupSystem.PopupEntity(Loc.GetString("microwave-component-interact-using-broken"), ent, args.User);
return;
}
if (TryComp<ItemComponent>(args.Used, out var item))
{
// check if size of an item you're trying to put in is too big
if (_item.GetSizePrototype(item.Size) > _item.GetSizePrototype(ent.Comp.MaxItemSize))
{
_popupSystem.PopupEntity(Loc.GetString("microwave-component-interact-item-too-big", ("item", args.Used)), ent, args.User);
return;
}
}
else
{
// check if thing you're trying to put in isn't an item
_popupSystem.PopupEntity(Loc.GetString("microwave-component-interact-using-transfer-fail"), ent, args.User);
return;
}
if (ent.Comp.Storage.Count >= ent.Comp.Capacity)
{
_popupSystem.PopupEntity(Loc.GetString("microwave-component-interact-full"), ent, args.User);
return;
}
args.Handled = true;
_handsSystem.TryDropIntoContainer(args.User, args.Used, ent.Comp.Storage);
UpdateUserInterfaceState(ent, ent.Comp);
}
private void OnBreak(Entity<MicrowaveComponent> ent, ref BreakageEventArgs args)
{
ent.Comp.Broken = true;
SetAppearance(ent, MicrowaveVisualState.Broken, ent.Comp);
StopCooking(ent);
_container.EmptyContainer(ent.Comp.Storage);
UpdateUserInterfaceState(ent, ent.Comp);
}
private void OnPowerChanged(Entity<MicrowaveComponent> ent, ref PowerChangedEvent args)
{
if (!args.Powered)
{
SetAppearance(ent, MicrowaveVisualState.Idle, ent.Comp);
StopCooking(ent);
}
UpdateUserInterfaceState(ent, ent.Comp);
}
private void OnAnchorChanged(EntityUid uid, MicrowaveComponent component, ref AnchorStateChangedEvent args)
{
if (!args.Anchored)
_container.EmptyContainer(component.Storage);
}
private void OnSignalReceived(Entity<MicrowaveComponent> ent, ref SignalReceivedEvent args)
{
if (args.Port != ent.Comp.OnPort)
return;
if (ent.Comp.Broken || !_power.IsPowered(ent))
return;
Wzhzhzh(ent.Owner, ent.Comp, null);
}
public void UpdateUserInterfaceState(EntityUid uid, MicrowaveComponent component)
{
_userInterface.SetUiState(uid, MicrowaveUiKey.Key, new MicrowaveUpdateUserInterfaceState(
GetNetEntityArray(component.Storage.ContainedEntities.ToArray()),
HasComp<ActiveMicrowaveComponent>(uid),
component.CurrentCookTimeButtonIndex,
component.CurrentCookTimerTime,
component.CurrentCookTimeEnd
));
}
public void SetAppearance(EntityUid uid, MicrowaveVisualState state, MicrowaveComponent? component = null, AppearanceComponent? appearanceComponent = null)
{
if (!Resolve(uid, ref component, ref appearanceComponent, false))
return;
var display = component.Broken ? MicrowaveVisualState.Broken : state;
_appearance.SetData(uid, PowerDeviceVisuals.VisualState, display, appearanceComponent);
}
public static bool HasContents(MicrowaveComponent component)
{
return component.Storage.ContainedEntities.Any();
}
/// <summary>
/// Explodes the microwave internally, turning it into a broken state, destroying its board, and spitting out its machine parts
/// </summary>
/// <param name="ent"></param>
public void Explode(Entity<MicrowaveComponent> ent)
{
ent.Comp.Broken = true; // Make broken so we stop processing stuff
_explosion.TriggerExplosive(ent);
if (TryComp<MachineComponent>(ent, out var machine))
{
_container.CleanContainer(machine.BoardContainer);
_container.EmptyContainer(machine.PartContainer);
}
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(ent)} exploded from unsafe cooking!");
}
/// <summary>
/// Handles the attempted cooking of unsafe objects
/// </summary>
/// <remarks>
/// Returns false if the microwave didn't explode, true if it exploded.
/// </remarks>
private void RollMalfunction(Entity<ActiveMicrowaveComponent, MicrowaveComponent> ent)
{
if (ent.Comp1.MalfunctionTime == TimeSpan.Zero)
return;
if (ent.Comp1.MalfunctionTime > _gameTiming.CurTime)
return;
ent.Comp1.MalfunctionTime = _gameTiming.CurTime + TimeSpan.FromSeconds(ent.Comp2.MalfunctionInterval);
if (_random.Prob(ent.Comp2.ExplosionChance))
{
Explode((ent, ent.Comp2));
return; // microwave is fucked, stop the cooking.
}
if (_random.Prob(ent.Comp2.LightningChance))
_lightning.ShootRandomLightnings(ent, 1.0f, 2, MalfunctionSpark, triggerLightningEvents: false);
}
/// <summary>
/// Starts Cooking
/// </summary>
/// <remarks>
/// It does not make a "wzhzhzh" sound, it makes a "mmmmmmmm" sound!
/// -emo
/// </remarks>
public void Wzhzhzh(EntityUid uid, MicrowaveComponent component, EntityUid? user)
{
if (!HasContents(component) || HasComp<ActiveMicrowaveComponent>(uid) || !(TryComp<ApcPowerReceiverComponent>(uid, out var apc) && apc.Powered))
return;
var solidsDict = new Dictionary<string, int>();
var reagentDict = new Dictionary<string, FixedPoint2>();
var malfunctioning = false;
// TODO use lists of Reagent quantities instead of reagent prototype ids.
foreach (var item in component.Storage.ContainedEntities.ToArray())
{
// special behavior when being microwaved ;)
var ev = new BeingMicrowavedEvent(uid, user);
RaiseLocalEvent(item, ev);
// TODO MICROWAVE SPARKS & EFFECTS
// Various microwaveable entities should probably spawn a spark, play a sound, and generate a pop=up.
// This should probably be handled by the microwave system, with fields in BeingMicrowavedEvent.
if (ev.Handled)
{
UpdateUserInterfaceState(uid, component);
return;
}
if (_tag.HasTag(item, MetalTag))
{
malfunctioning = true;
}
if (_tag.HasTag(item, PlasticTag))
{
var junk = Spawn(component.BadRecipeEntityId, Transform(uid).Coordinates);
_container.Insert(junk, component.Storage);
Del(item);
continue;
}
var microwavedComp = AddComp<ActivelyMicrowavedComponent>(item);
microwavedComp.Microwave = uid;
string? solidID = null;
int amountToAdd = 1;
// If a microwave recipe uses a stacked item, use the default stack prototype id instead of prototype id
if (TryComp<StackComponent>(item, out var stackComp))
{
solidID = ProtoMan.Index<StackPrototype>(stackComp.StackTypeId).Spawn;
amountToAdd = stackComp.Count;
}
else
{
var metaData = MetaData(item); //this simply begs for cooking refactor
if (metaData.EntityPrototype is not null)
solidID = metaData.EntityPrototype.ID;
}
if (solidID is null)
continue;
if (!solidsDict.TryAdd(solidID, amountToAdd))
solidsDict[solidID] += amountToAdd;
// only use reagents we have access to
// you have to break the eggs before we can use them!
if (!_solutionContainer.TryGetDrainableSolution(item, out var _, out var solution))
continue;
foreach (var (reagent, quantity) in solution.Contents)
{
if (!reagentDict.TryAdd(reagent.Prototype, quantity))
reagentDict[reagent.Prototype] += quantity;
}
}
// Check recipes
var getRecipesEv = new GetSecretRecipesEvent();
RaiseLocalEvent(uid, ref getRecipesEv);
List<FoodRecipePrototype> recipes = getRecipesEv.Recipes;
recipes.AddRange(_recipeManager.Recipes);
var portionedRecipe = recipes.Select(r =>
CanSatisfyRecipe(component, r, solidsDict, reagentDict)).FirstOrDefault(r => r.Item2 > 0);
_audio.PlayPvs(component.StartCookingSound, uid);
var activeComp = AddComp<ActiveMicrowaveComponent>(uid); //microwave is now cooking
activeComp.CookTimeRemaining = component.CurrentCookTimerTime * component.CookTimeMultiplier;
activeComp.TotalTime = component.CurrentCookTimerTime; //this doesn't scale so that we can have the "actual" time
activeComp.PortionedRecipe = portionedRecipe;
//Scale tiems with cook times
component.CurrentCookTimeEnd = _gameTiming.CurTime + TimeSpan.FromSeconds(component.CurrentCookTimerTime * component.CookTimeMultiplier);
if (malfunctioning)
activeComp.MalfunctionTime = _gameTiming.CurTime + TimeSpan.FromSeconds(component.MalfunctionInterval);
UpdateUserInterfaceState(uid, component);
}
private void StopCooking(Entity<MicrowaveComponent> ent)
{
RemCompDeferred<ActiveMicrowaveComponent>(ent);
foreach (var solid in ent.Comp.Storage.ContainedEntities)
{
RemCompDeferred<ActivelyMicrowavedComponent>(solid);
}
}
public static (FoodRecipePrototype, int) CanSatisfyRecipe(MicrowaveComponent component, FoodRecipePrototype recipe, Dictionary<string, int> solids, Dictionary<string, FixedPoint2> reagents)
{
var portions = 0;
if (component.CurrentCookTimerTime % recipe.CookTime != 0)
{
//can't be a multiple of this recipe
return (recipe, 0);
}
foreach (var solid in recipe.IngredientsSolids)
{
if (!solids.ContainsKey(solid.Key))
return (recipe, 0);
if (solids[solid.Key] < solid.Value)
return (recipe, 0);
portions = portions == 0
? solids[solid.Key] / solid.Value.Int()
: Math.Min(portions, solids[solid.Key] / solid.Value.Int());
}
foreach (var reagent in recipe.IngredientsReagents)
{
// TODO Turn recipe.IngredientsReagents into a ReagentQuantity[]
if (!reagents.ContainsKey(reagent.Key))
return (recipe, 0);
if (reagents[reagent.Key] < reagent.Value)
return (recipe, 0);
portions = portions == 0
? reagents[reagent.Key].Int() / reagent.Value.Int()
: Math.Min(portions, reagents[reagent.Key].Int() / reagent.Value.Int());
}
//cook only as many of those portions as time allows
return (recipe, (int) Math.Min(portions, component.CurrentCookTimerTime / recipe.CookTime));
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ActiveMicrowaveComponent, MicrowaveComponent>();
while (query.MoveNext(out var uid, out var active, out var microwave))
{
active.CookTimeRemaining -= frameTime;
RollMalfunction((uid, active, microwave));
//check if there's still cook time left
if (active.CookTimeRemaining > 0)
{
AddTemperature(microwave, frameTime);
continue;
}
//this means the microwave has finished cooking.
AddTemperature(microwave, Math.Max(frameTime + active.CookTimeRemaining, 0)); //Though there's still a little bit more heat to pump out
if (active.PortionedRecipe.Item1 != null)
{
var coords = Transform(uid).Coordinates;
for (var i = 0; i < active.PortionedRecipe.Item2; i++)
{
SubtractContents(microwave, active.PortionedRecipe.Item1);
Spawn(active.PortionedRecipe.Item1.Result, coords);
}
}
_container.EmptyContainer(microwave.Storage);
microwave.CurrentCookTimeEnd = TimeSpan.Zero;
UpdateUserInterfaceState(uid, microwave);
_audio.PlayPvs(microwave.FoodDoneSound, uid);
StopCooking((uid, microwave));
}
}
/// <summary>
/// This event tries to get secret recipes that the microwave might be capable of.
/// Currently, we only check the microwave itself, but in the future, the user might be able to learn recipes.
/// </summary>
private void OnGetSecretRecipes(Entity<FoodRecipeProviderComponent> ent, ref GetSecretRecipesEvent args)
{
foreach (ProtoId<FoodRecipePrototype> recipeId in ent.Comp.ProvidedRecipes)
{
if (ProtoMan.Resolve(recipeId, out var recipeProto))
{
args.Recipes.Add(recipeProto);
}
}
}
#region ui
private void OnEjectMessage(Entity<MicrowaveComponent> ent, ref MicrowaveEjectMessage args)
{
if (!HasContents(ent.Comp) || HasComp<ActiveMicrowaveComponent>(ent))
return;
_container.EmptyContainer(ent.Comp.Storage);
var audioParams = ent.Comp.ClickSound?.Params ?? AudioParams.Default;
audioParams = audioParams.AddVolume(-2);
_audio.PlayPvs(ent.Comp.ClickSound, ent, audioParams);
UpdateUserInterfaceState(ent, ent.Comp);
}
private void OnEjectIndex(Entity<MicrowaveComponent> ent, ref MicrowaveEjectSolidIndexedMessage args)
{
if (!HasContents(ent.Comp) || HasComp<ActiveMicrowaveComponent>(ent))
return;
_container.Remove(GetEntity(args.EntityID), ent.Comp.Storage);
UpdateUserInterfaceState(ent, ent.Comp);
}
private void OnSelectTime(Entity<MicrowaveComponent> ent, ref MicrowaveSelectCookTimeMessage args)
{
if (!HasContents(ent.Comp) || HasComp<ActiveMicrowaveComponent>(ent) || !(TryComp<ApcPowerReceiverComponent>(ent, out var apc) && apc.Powered))
return;
// some validation to prevent trollage
if (args.NewCookTime % 5 != 0 || args.NewCookTime > ent.Comp.MaxCookTime)
return;
ent.Comp.CurrentCookTimeButtonIndex = args.ButtonIndex;
ent.Comp.CurrentCookTimerTime = args.NewCookTime;
ent.Comp.CurrentCookTimeEnd = TimeSpan.Zero;
var audioParams = ent.Comp.ClickSound?.Params ?? AudioParams.Default;
audioParams = audioParams.AddVolume(-2);
_audio.PlayPvs(ent.Comp.ClickSound, ent, audioParams);
UpdateUserInterfaceState(ent, ent.Comp);
}
#endregion
}
}

View file

@ -0,0 +1,115 @@
using Content.Server.Administration.Logs;
using Content.Server.Construction;
using Content.Server.Construction.Components;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Lightning;
using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.Interaction.Events;
using Content.Shared.Kitchen.Components;
using Content.Shared.Kitchen.EntitySystems;
using Content.Shared.Suicide;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Kitchen.EntitySystems;
/// <inheritdoc />
public sealed partial class ServerMicrowaveSystem : MicrowaveSystem
{
[Dependency] private IAdminLogManager _adminLogger = default!;
[Dependency] private ExplosionSystem _explosion = default!;
[Dependency] private LightningSystem _lightning = default!;
[Dependency] private IRobustRandom _random = default!;
[Dependency] private SharedSuicideSystem _suicide = default!;
[Dependency] private EntityQuery<DamageableComponent> _damageableQuery;
[Dependency] private EntityQuery<MachineComponent> _machineQuery;
/// <summary>
/// Kills the user by microwaving their head.
/// </summary>
/// <remarks>
/// TODO: Make this not awful, it keeps any items attached to your head still on and you can
/// revive someone and cogni them so you have some dumb headless fuck running around. I've seen it happen.
/// </remarks>
[SubscribeLocalEvent]
private void OnSuicideByEnvironment(Entity<MicrowaveComponent> ent, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
// The act of getting your head microwaved doesn't actually kill you
if (!_damageableQuery.TryComp(args.Victim, out var damageableComponent))
return;
// The application of lethal damage is what kills you...
_suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Heat");
var victim = args.Victim;
var othersMessage = Loc.GetString("microwave-component-suicide-others-message", ("victim", victim));
var selfMessage = Loc.GetString("microwave-component-suicide-message");
Popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
Popup.PopupEntity(selfMessage, victim, victim);
var audioParams = ent.Comp.ClickSound.Params;
Audio.PlayPvs(ent.Comp.ClickSound, ent.Owner, audioParams);
ent.Comp.CurrentCookTimerTime = 10;
DirtyField(ent.AsNullable(), nameof(ent.Comp.CurrentCookTimeButtonIndex));
StartCooking(ent, args.Victim);
UpdateUI(ent.AsNullable());
args.Handled = true;
}
/// <summary>
/// Prevents construction graph operations as a result of temperature changes.
/// </summary>
/// <remarks>
/// For example: raw meat will not turn into steak while it is actively being microwaved.
/// </remarks>
[SubscribeLocalEvent]
private void OnConstructionTemp(Entity<ActivelyMicrowavedComponent> ent, ref OnConstructionTemperatureEvent args)
{
args.Result = HandleResult.False;
}
/// <inheritdoc />
protected override void RollMalfunction(Entity<MicrowaveComponent> ent)
{
base.RollMalfunction(ent);
// this microwave sploded
if (ent.Comp.Broken)
return;
var comp = ent.Comp;
if (_random.Prob(comp.LightningChance))
_lightning.ShootRandomLightnings(ent, 1.0f, 2, comp.MalfunctionSpark, triggerLightningEvents: false);
}
/// <summary>
/// Explodes the microwave internally, turning it into a broken state, destroying its board, and spitting out its machine parts
/// </summary>
/// <param name="ent">The microwave entity.</param>
public override void Explode(Entity<MicrowaveComponent?> ent)
{
base.Explode(ent);
if (!Resolve(ent.Owner, ref ent.Comp))
return;
_explosion.TriggerExplosive(ent);
if (_machineQuery.TryComp(ent, out var machine))
{
Container.CleanContainer(machine.BoardContainer);
Container.EmptyContainer(machine.PartContainer);
}
_adminLogger.Add(LogType.Action,
LogImpact.Medium,
$"{ToPrettyString(ent)} exploded from unsafe cooking!");
}
}

View file

@ -0,0 +1,88 @@
using Content.Shared.Kitchen.EntitySystems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Attached to a microwave that is currently in the process of cooking
/// </summary>
[RegisterComponent]
[Access(typeof(MicrowaveSystem))]
[NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause]
public sealed partial class ActiveMicrowaveComponent : Component
{
/// <summary>
/// Whether or not this microwave cooking process is malfunctioning.
/// </summary>
[DataField, AutoNetworkedField]
public bool Malfunctioning;
/// <summary>
/// The recipe we are currently cooking.
/// </summary>
[DataField, AutoNetworkedField]
public PortionedRecipe? PortionedRecipe;
/// <summary>
/// The total cooking time of this operation.
/// </summary>
[DataField, AutoNetworkedField]
public float TotalTime;
/// <summary>
/// The time that this microwave will finish cooking.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan CookTimeEnd = TimeSpan.Zero;
/// <summary>
/// The last time that this microwave updated its cooking cycle..
/// </summary>
/// <remarks>
/// This is used to calculate how much heat to add to the microwave.
/// </remarks>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan LastCookUpdated = TimeSpan.Zero;
/// <summary>
/// The next time this microwave will update its cooking cycle.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan NextCookUpdate = TimeSpan.Zero;
/// <summary>
/// The next time we attempt to roll a malfunction.
/// </summary>
/// <remarks>
/// Malfunctions are rolled on an interval and generate random effects, like lightning or microwave destruction.
/// </remarks>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan NextMalfunction = TimeSpan.Zero;
}
/// <summary>
/// Represents a specific quantity of a recipe to cook.
/// </summary>
[Serializable, NetSerializable]
[DataDefinition]
public partial struct PortionedRecipe(ProtoId<MicrowaveMealRecipePrototype> recipe, uint count)
{
/// <summary>
/// The recipe to make.
/// </summary>
[DataField]
public ProtoId<MicrowaveMealRecipePrototype> Recipe = recipe;
/// <summary>
/// The number of portions to make of this recipe.
/// </summary>
[DataField]
public uint Count = count;
}

View file

@ -1,14 +1,17 @@
namespace Content.Server.Kitchen.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Attached to an object that's actively being microwaved
/// </summary>
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActivelyMicrowavedComponent : Component
{
/// <summary>
/// The microwave this entity is actively being microwaved by.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public EntityUid? Microwave;
}

View file

@ -0,0 +1,231 @@
using Content.Shared.DeviceLinking;
using Content.Shared.Item;
using Content.Shared.Kitchen.EntitySystems;
using Content.Shared.Temperature.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// A component applied to microwaves, which are used to heat entities/solutions
/// and produce microwave recipes.
/// </summary>
[RegisterComponent]
[Access(typeof(MicrowaveSystem))]
[NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true, raiseAfterAutoHandleState: true)]
public sealed partial class MicrowaveComponent : Component
{
/// <summary>
/// A multiplier applied to all microwave cooking timers.
/// </summary>
/// <remarks>
/// If you set a microwave for 30 seconds, but have a CookTimeMultiplier of 0.5,
/// recipes will be processed as if you're portioning for 30 seconds, but the
/// microwave's timer will take 15 seconds instead. So, if you have a recipe with
/// a cookTime of 10 seconds, you can still make that recipe 3 at a time.
///
/// Note that faster cooking times make the microwave less useful for actual heating
/// (temperature) as thermal energy added to the contents is based on elapsed time.
/// </remarks>
[DataField]
public float CookTimeMultiplier = 1;
/// <summary>
/// A multiplier for heat added to the contents of a microwave every cook cycle.
/// </summary>
/// <remarks>
/// The formula is (elapsed time between dt * <see cref="DeltaHeat"/>).
/// </remarks>
[DataField]
public float DeltaHeat = 100;
/// <summary>
/// A multiplier for added heat on entities with a <see cref="TemperatureComponent"/>.
/// </summary>
[DataField]
public float ObjectHeatMultiplier = 100;
/// <summary>
/// An entity that is produced when an item is melted in the microwave.
/// </summary>
/// <remarks>
/// The microwave will burn items that pass the <see cref="BurnWhenCookedWhitelist" />.
/// </remarks>
[DataField]
public EntProtoId FailureResult = "FoodBadRecipe";
#region Audio
/// <summary>
/// A sound that is played when the microwave is activated.
/// </summary>
[DataField]
public SoundSpecifier BeginCookingSound = new SoundPathSpecifier("/Audio/Machines/microwave_start_beep.ogg");
/// <summary>
/// A sound that is played when the microwave finishes.
/// </summary>
/// <remarks>
/// Beep... beep... beep
/// </remarks>
[DataField]
public SoundSpecifier FoodDoneSound = new SoundPathSpecifier("/Audio/Machines/microwave_done_beep.ogg");
/// <summary>
/// A sound that is played when a player navigates the microwave's UI - for example, selecting
/// a new cooking time.
/// </summary>
[DataField]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg")
{
Params = AudioParams.Default.AddVolume(-2),
};
/// <summary>
/// The humming sound played when a microwave is actively cooking.
/// </summary>
[DataField]
public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg");
#endregion Audio
/// <summary>
/// An audio stream for the microwave's "cooking" hum.
/// </summary>
[DataField]
public EntityUid? PlayingStream;
/// <summary>
/// Whether or not this microwave is broken.
/// </summary>
[DataField]
[AutoNetworkedField]
public bool Broken;
/// <summary>
/// A port used to activate the microwave via remote signal.
/// </summary>
[DataField]
public ProtoId<SinkPortPrototype> OnPort = "On";
/// <summary>
/// The currently-selected cook time, in seconds. This must be a multiple of 5 (or 0).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField]
[AutoNetworkedField]
[Access(typeof(MicrowaveSystem), Other = AccessPermissions.ReadExecute)]
public uint CurrentCookTimerTime;
/// <summary>
/// The index of the currently-selected "cook time" button.
/// </summary>
[DataField]
[AutoNetworkedField]
public int CurrentCookTimeButtonIndex;
/// <summary>
/// The maximum number of seconds a microwave can be set to.
/// This is currently only used for validation and the client does not check this.
/// </summary>
[DataField]
public uint MaxCookTime = 30;
/// <summary>
/// The max temperature that this microwave can heat objects to.
/// </summary>
[DataField]
public float TemperatureUpperThreshold = 373.15f;
/// <summary>
/// The microwave's contents container.
/// </summary>
public Container Storage = default!;
/// <summary>
/// The ID of the storage container for the microwave.
/// </summary>
[DataField]
public string ContainerId = "microwave_entity_container";
/// <summary>
/// How many items the microwave can hold.
/// </summary>
[DataField]
public int Capacity = 10;
/// <summary>
/// The largest item size that can fit in the microwave.
/// </summary>
[DataField]
public ProtoId<ItemSizePrototype> MaxItemSize = "Normal";
/// <summary>
/// How frequently the microwave can malfunction while active.
/// </summary>
[DataField]
public TimeSpan MalfunctionInterval = TimeSpan.FromSeconds(1.0f);
/// <summary>
/// How frequently the microwave can update (process cooking cycle) while active.
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1.0f);
/// <summary>
/// Chance of an explosion occurring when we microwave a metallic object.
/// </summary>
/// <remarks>
/// This is rolled every <see cref="MalfunctionInterval"/>.
/// </remarks>
[DataField]
public float ExplosionChance = 0.1f;
/// <summary>
/// Chance of lightning occurring when we microwave a metallic object.
/// </summary>
/// <remarks>
/// This is rolled every <see cref="MalfunctionInterval"/>.
/// </remarks>
[DataField]
public float LightningChance = 0.75f;
/// <summary>
/// If this microwave can give ID cards new accesses without exploding.
/// </summary>
[DataField]
public bool CanMicrowaveIdsSafely = true;
/// <summary>
/// Entities that fulfill this whitelist will cause the microwave to malfunction
/// on activation. By default, this is metal objects.
/// </summary>
[DataField]
public EntityWhitelist? MalfunctionWhenCookedWhitelist = new()
{
Tags = new() { "Metal" }
};
/// <summary>
/// Entities that fulfill this whitelist will create a burned mess when microwaved.
/// By default, this is plastic objects.
/// </summary>
[DataField]
public EntityWhitelist? BurnWhenCookedWhitelist = new()
{
Tags = new() { "Plastic" }
};
/// <summary>
/// A "spark" entity spawned when this microwave malfunctions.
/// </summary>
[DataField]
public EntProtoId MalfunctionSpark = "Spark";
}

View file

@ -2,6 +2,10 @@ using Robust.Shared.Prototypes;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Applied to entities that have access to secret microwave recipes.
/// See: <see cref="MicrowaveMealRecipePrototype.SecretRecipe"/>
/// </summary>
[RegisterComponent]
public sealed partial class FoodRecipeProviderComponent : Component
{
@ -9,5 +13,5 @@ public sealed partial class FoodRecipeProviderComponent : Component
/// These are additional recipes that the entity is capable of cooking.
/// </summary>
[DataField, ViewVariables]
public List<ProtoId<FoodRecipePrototype>> ProvidedRecipes = new();
public List<ProtoId<MicrowaveMealRecipePrototype>> ProvidedRecipes = new();
}

View file

@ -1,86 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Serialization;
namespace Content.Shared.Kitchen.Components
{
[Serializable, NetSerializable]
public sealed class MicrowaveStartCookMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class MicrowaveEjectMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class MicrowaveEjectSolidIndexedMessage : BoundUserInterfaceMessage
{
public NetEntity EntityID;
public MicrowaveEjectSolidIndexedMessage(NetEntity entityId)
{
EntityID = entityId;
}
}
[Serializable, NetSerializable]
public sealed class MicrowaveVaporizeReagentIndexedMessage : BoundUserInterfaceMessage
{
public ReagentQuantity ReagentQuantity;
public MicrowaveVaporizeReagentIndexedMessage(ReagentQuantity reagentQuantity)
{
ReagentQuantity = reagentQuantity;
}
}
[Serializable, NetSerializable]
public sealed class MicrowaveSelectCookTimeMessage : BoundUserInterfaceMessage
{
public int ButtonIndex;
public uint NewCookTime;
public MicrowaveSelectCookTimeMessage(int buttonIndex, uint inputTime)
{
ButtonIndex = buttonIndex;
NewCookTime = inputTime;
}
}
[NetSerializable, Serializable]
public sealed class MicrowaveUpdateUserInterfaceState : BoundUserInterfaceState
{
public NetEntity[] ContainedSolids;
public bool IsMicrowaveBusy;
public int ActiveButtonIndex;
public uint CurrentCookTime;
public TimeSpan CurrentCookTimeEnd;
public MicrowaveUpdateUserInterfaceState(NetEntity[] containedSolids,
bool isMicrowaveBusy, int activeButtonIndex, uint currentCookTime, TimeSpan currentCookTimeEnd)
{
ContainedSolids = containedSolids;
IsMicrowaveBusy = isMicrowaveBusy;
ActiveButtonIndex = activeButtonIndex;
CurrentCookTime = currentCookTime;
CurrentCookTimeEnd = currentCookTimeEnd;
}
}
[Serializable, NetSerializable]
public enum MicrowaveVisualState
{
Idle,
Cooking,
Broken,
Bloody
}
[NetSerializable, Serializable]
public enum MicrowaveUiKey
{
Key
}
}

View file

@ -0,0 +1,154 @@
using System.Linq;
using Content.Shared.Kitchen.Components;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Content.Shared.Kitchen.EntitySystems;
public abstract partial class MicrowaveSystem
{
/// <summary>
/// Helper function to get if an entity is an active microwave.
/// </summary>
/// <param name="microwave">The microwave entity.</param>
/// <returns>Whether or not this entity is an active microwave.</returns>
[PublicAPI]
public bool IsActiveMicrowave(Entity<MicrowaveComponent?> microwave)
{
return MicrowaveQuery.Resolve(microwave.Owner, ref microwave.Comp)
&& ActiveMicrowaveQuery.HasComp(microwave.Owner);
}
/// <summary>
/// Helper function to get the contents of a microwave.
/// </summary>
/// <param name="microwave">The microwave entity.</param>
/// <returns>A list of microwave contents.</returns>
[PublicAPI]
public IReadOnlyList<EntityUid> GetMicrowaveContents(Entity<MicrowaveComponent?> microwave)
{
if (!MicrowaveQuery.Resolve(microwave.Owner, ref microwave.Comp))
return new List<EntityUid>();
return microwave.Comp.Storage.ContainedEntities;
}
/// <summary>
/// Helper function to check if a microwave has ingredient contents.
/// </summary>
/// <param name="microwave">The microwave entity.</param>
/// <returns>Whether or not this microwave contains anything.</returns>
[PublicAPI]
public bool HasContents(Entity<MicrowaveComponent?> microwave)
{
return GetMicrowaveContents(microwave).Any();
}
/// <summary>
/// Gets a complete ordered list of usable recipes for this appliance.
/// </summary>
/// <remarks>
/// Note that the order of recipes is meaningful. When a valid recipe is chosen, the first item
/// in the list that satisfies the conditions of the recipe is selected.
///
/// Recipe prototypes in the recipe manager are pre-sorted based on complexity, so more "specific"
/// recipes will be selected first. Secret recipes come before all non-secret prototype recipes.
/// Do not sort the result of this function!
/// </remarks>
/// <param name="uid">The appliance to get recipes for.</param>
/// <returns>A complete list of usable recipe prototypes.</returns>
[PublicAPI]
public IReadOnlyList<MicrowaveMealRecipePrototype> GetAvailableRecipes(EntityUid uid)
{
var getRecipesEv = new GetSecretRecipesEvent();
RaiseLocalEvent(uid, ref getRecipesEv);
var recipes = getRecipesEv.Recipes
.OrderByDescending(recipe => recipe.Priority)
.ThenByDescending(recipe => recipe.Ingredients.Count())
.ToList();
recipes.AddRange(_recipes.Recipes);
return recipes;
}
/// <summary>
/// Given a recipe, a list of available ingredients, and a cooking time, this functions
/// gets how many times we can make this given recipe.
/// </summary>
/// <param name="recipe">A cooking recipe.</param>
/// <param name="ingredients">The ingredients we have available.</param>
/// <param name="cookTime">How long we are cooking for.</param>
/// <returns>How many portions of the recipe can be made.</returns>
[PublicAPI]
public static uint GetRecipePortions(MicrowaveMealRecipePrototype recipe,
CookingIngredients ingredients,
uint cookTime)
{
// Our cooking time must be a multiple of the recipe's cooking time.
// For example: If a recipe takes 10 seconds to cook, then you can't make it with a 15 second timer.
// However, if you use a 30 second timer, you could make three of that recipe on one timer.
if (cookTime % recipe.Time != 0)
return 0;
// TODO: there's actually a kind of nasty edge case microwave economics issue here,
// all reagents / stacks / solids will be included, but when the recipe is actually made,
// solids are used first, then stacks, then reagents.
// thus, recipe detection might thing you have "more" ingredients than you actually do.
//
// moral of the story: I hate microwaves
var portionCount = cookTime / recipe.Time;
var ingredientPortions = ingredients.PortionForRecipe(recipe.Ingredients);
portionCount = Math.Min(portionCount, ingredientPortions);
return portionCount;
}
/// <summary>
/// Given an appliance entity and a list of available ingredients, this function gets the first valid
/// usable recipe for cooking.
/// </summary>
/// <remarks>
/// The appliance entity itself is used to get secret recipes.
/// </remarks>
/// <param name="uid">An appliance to fetch a recipe for.</param>
/// <param name="ingredients">A list of available ingredients.</param>
/// <param name="cookTime">How long we plan to cook for.</param>
/// <returns>
/// The first valid recipe we can use. If there is none, this is (null, 0).
/// </returns>
[PublicAPI]
public PortionedRecipe? GetRecipe(EntityUid uid,
CookingIngredients ingredients,
uint cookTime)
{
var recipes = GetAvailableRecipes(uid);
var recipePortions = recipes.Select(recipe =>
{
var portions = GetRecipePortions(recipe, ingredients, cookTime);
return new PortionedRecipe(recipe.ID, portions);
});
return recipePortions.FirstOrNull(r => r.Count > 0)
?? null;
}
/// <summary>
/// Gets a complete list of recipe-usable ingredients from a list of items, including solids,
/// stacks, and reagents.
/// </summary>
/// <param name="items">The list of items to use as ingredients.</param>
/// <returns>Cooking ingredient quantities representing the total usable ingredient list.</returns>
[PublicAPI]
public CookingIngredients GetTotalIngredients(List<EntityUid> items)
{
var ingredients = new CookingIngredients();
foreach (var item in items)
{
AddItemIngredients(item, ref ingredients);
}
return ingredients;
}
}

View file

@ -0,0 +1,123 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Kitchen.Components;
using Robust.Shared.Containers;
namespace Content.Shared.Kitchen.EntitySystems;
public abstract partial class MicrowaveSystem
{
/// <summary>
/// Adjusts a microwave's visuals, audio, and power draw when activated.
/// </summary>
[SubscribeLocalEvent]
private void OnCookStart(Entity<ActiveMicrowaveComponent> ent, ref ComponentStartup args)
{
if (!MicrowaveQuery.TryComp(ent, out var microwaveComponent))
return;
ent.Comp.NextCookUpdate = Timing.CurTime + microwaveComponent.UpdateInterval;
ent.Comp.LastCookUpdated = Timing.CurTime;
DirtyFields(ent.AsNullable(), null, nameof(ent.Comp.NextCookUpdate), nameof(ent.Comp.LastCookUpdated));
ActivateMicrowaveCycle((ent, microwaveComponent));
}
/// <summary>
/// Adjusts a microwave's visuals, audio, and power draw when activated.
/// </summary>
[SubscribeLocalEvent]
private void OnCookEnd(Entity<ActiveMicrowaveComponent> ent, ref ComponentShutdown args)
{
if (!MicrowaveQuery.TryComp(ent, out var microwaveComponent))
return;
DeactivateMicrowaveCycle((ent, microwaveComponent));
}
/// <summary>
/// Adds ActivelyMicrowavedComponent to entities inserted into an active microwave.
/// </summary>
[SubscribeLocalEvent]
private void OnActiveMicrowaveInsert(Entity<ActiveMicrowaveComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (Timing.ApplyingState)
return;
BeginActivelyMicrowaving(args.Entity, ent.Owner);
}
/// <summary>
/// Removes ActivelyMicrowavedComponent from entities removed from an active microwave.
/// </summary>
[SubscribeLocalEvent]
private void OnActiveMicrowaveRemove(Entity<ActiveMicrowaveComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (Timing.ApplyingState)
return;
RemCompDeferred<ActivelyMicrowavedComponent>(args.Entity);
}
/// <summary>
/// Prevents reagent reactions in entitites that are actively being microwaved.
/// </summary>
/// <remarks>
/// For example, raw egg would otherwise turn into cooked egg during the process, preventing it from being
/// "spent" when the microwave is finished cooking.
/// </remarks>
[SubscribeLocalEvent]
private void OnReactionAttempt(Entity<ActivelyMicrowavedComponent> ent, ref SolutionRelayEvent<ReactionAttemptEvent> args)
{
if (!ActiveMicrowaveQuery.TryComp(ent.Comp.Microwave, out var activeMicrowaveComp))
return;
var portionedRecipe = activeMicrowaveComp.PortionedRecipe;
if (portionedRecipe == null // no recipe selected
|| !ProtoMan.TryIndex(portionedRecipe.Value.Recipe, out var recipe))
return;
var recipeReagents = recipe.Ingredients.Reagents.Keys;
foreach (var reagent in recipeReagents)
{
if (args.Event.Reaction.Reactants.ContainsKey(reagent))
{
args.Event.Cancelled = true;
return;
}
}
}
/// <summary>
/// Adjusts a microwave's visuals, audio, and power draw when activated.
/// </summary>
protected virtual void ActivateMicrowaveCycle(Entity<MicrowaveComponent> ent)
{
SetAppearance(ent, MicrowaveVisualState.Cooking);
_powerState.SetWorkingState(ent.Owner, true);
}
/// <summary>
/// Adjusts a microwave's visuals, audio, and power draw when deactivated.
/// </summary>
protected virtual void DeactivateMicrowaveCycle(Entity<MicrowaveComponent> ent)
{
SetAppearance(ent, MicrowaveVisualState.Idle);
_powerState.SetWorkingState(ent.Owner, false);
foreach (var uid in GetMicrowaveContents(ent.AsNullable()))
{
RemCompDeferred<ActivelyMicrowavedComponent>(uid);
}
}
/// <summary>
/// Add ActivelyMicrowavedComponent to items that are being actively microwaved.
/// </summary>
/// <param name="uid">The entity being microwaved.</param>
/// <param name="microwave">That dastardly microwave</param>
private void BeginActivelyMicrowaving(EntityUid uid, EntityUid microwave)
{
var comp = new ActivelyMicrowavedComponent { Microwave = microwave };
AddComp(uid, comp);
}
}

View file

@ -0,0 +1,136 @@
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Kitchen.Components;
using Robust.Shared.Containers;
namespace Content.Shared.Kitchen.EntitySystems;
public abstract partial class MicrowaveSystem
{
/// <summary>
/// Initializes the microwave's storage container.
/// </summary>
[SubscribeLocalEvent]
private void OnComponentInit(Entity<MicrowaveComponent> ent, ref ComponentInit args)
{
// this really does have to be in ComponentInit
ent.Comp.Storage = Container.EnsureContainer<Container>(ent, ent.Comp.ContainerId);
}
/// <summary>
/// Prevents inserting entities into the microwave if the microwave is broken, active,
/// or the item is invalid.
/// </summary>
[SubscribeLocalEvent]
private void OnInsertAttempt(Entity<MicrowaveComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (Timing.ApplyingState)
return;
if (args.Container.ID != ent.Comp.ContainerId)
return;
if (ent.Comp.Broken
|| IsActiveMicrowave(ent.AsNullable())
|| !CanFitInMicrowave(ent.AsNullable(), args.EntityUid))
{
args.Cancel();
}
}
/// <summary>
/// Attempt to insert an entity into the microwave, resulting in a pop-up message if this is not possible.
/// </summary>
[SubscribeLocalEvent(after:[typeof(AnchorableSystem)])]
private void OnInteractUsing(Entity<MicrowaveComponent> ent, ref InteractUsingEvent args)
{
if (args.Handled)
return;
// Power is turned off
if (!_power.IsPowered(ent.Owner))
{
var message = Loc.GetString("microwave-component-interact-using-no-power");
Popup.PopupEntity(message, ent, args.User);
return;
}
// Microwave is broken
if (ent.Comp.Broken)
{
var message = Loc.GetString("microwave-component-interact-using-broken");
Popup.PopupEntity(message, ent, args.User);
return;
}
// Only items can be inserted into the microwave
if (!ItemQuery.TryComp(args.Used, out var item))
{
var message = Loc.GetString("microwave-component-interact-using-transfer-fail");
Popup.PopupEntity(message, ent, args.User);
return;
}
// Item is too big for the microwave
if (_item.GetSizePrototype(item.Size) > _item.GetSizePrototype(ent.Comp.MaxItemSize))
{
var message = Loc.GetString("microwave-component-interact-item-too-big", ("item", args.Used));
Popup.PopupEntity(message, ent, args.User);
return;
}
// The microwave is full
if (ent.Comp.Storage.Count >= ent.Comp.Capacity)
{
var message = Loc.GetString("microwave-component-interact-full");
Popup.PopupEntity(message, ent, args.User);
return;
}
_hands.TryDropIntoContainer(args.User, args.Used, ent.Comp.Storage);
args.Handled = true;
}
[SubscribeLocalEvent]
private void OnContentsAdded(Entity<MicrowaveComponent> entity, ref EntInsertedIntoContainerMessage args)
=> OnContentsUpdated(entity, ref args);
[SubscribeLocalEvent]
private void OnContentsRemoved(Entity<MicrowaveComponent> entity, ref EntRemovedFromContainerMessage args)
=> OnContentsUpdated(entity, ref args);
/// <summary>
/// Updates the microwave UI when entities are added/removed from the microwave.
/// </summary>
// For some reason ContainerModifiedMessage just can't be used at all with Entity<T>.
private void OnContentsUpdated<T>(Entity<MicrowaveComponent> entity, ref T args) where T : ContainerModifiedMessage
{
if (entity.Comp.Storage != args.Container)
return;
UpdateUI(entity.AsNullable());
}
/// <summary>
/// Check whether or not this microwave has space for the item, in both capacity and item size.
/// </summary>
/// <param name="ent">The microwave entity.</param>
/// <param name="item">The item to attempt to insert.</param>
/// <returns>Whether or not the item fits in the microwave. False if the item is too big or the microwave is full.</returns>
private bool CanFitInMicrowave(Entity<MicrowaveComponent?> ent, Entity<ItemComponent?> item)
{
if (!Resolve(ent.Owner, ref ent.Comp) || !Resolve(item.Owner, ref item.Comp))
return false;
var microwave = ent.Comp;
if (microwave.Storage.Count >= microwave.Capacity)
return false;
var maxSize = _item.GetSizePrototype(microwave.MaxItemSize);
var itemSize = _item.GetSizePrototype(item.Comp.Size);
if (itemSize > maxSize)
return false;
return true;
}
}

View file

@ -0,0 +1,285 @@
using System.Linq;
using Content.Shared.Kitchen.Components;
using Content.Shared.Random.Helpers;
namespace Content.Shared.Kitchen.EntitySystems;
public abstract partial class MicrowaveSystem
{
/// <summary>
/// Adds temperature to every item in the microwave based on the time it took to microwave.
/// </summary>
/// <param name="ent">The microwave entity.</param>
/// <param name="time">The heating time that has elapsed, in seconds.</param>
private void AddTemperature(Entity<MicrowaveComponent> ent, float time)
{
var component = ent.Comp;
var heatToAdd = time * component.DeltaHeat;
var objHeatToAdd = heatToAdd * component.ObjectHeatMultiplier;
// TODO: Divide heat amongst contents?
foreach (var entity in GetMicrowaveContents(ent.AsNullable()))
{
_temperature.ChangeHeat(entity, objHeatToAdd);
foreach (var (_, soln) in Solution.EnumerateSolutions(entity))
{
var solution = soln.Comp.Solution;
if (solution.Temperature > component.TemperatureUpperThreshold)
continue;
Solution.AddThermalEnergy(soln, heatToAdd);
}
}
}
/// <summary>
/// Attempts to roll random "malfunction" events on a malfunctioning microwave.
/// </summary>
protected virtual void RollMalfunction(Entity<MicrowaveComponent> ent)
{
if (SharedRandomExtensions.PredictedProb(Timing, ent.Comp.ExplosionChance, GetNetEntity(ent)))
Explode(ent.AsNullable());
}
public virtual void Explode(Entity<MicrowaveComponent?> ent)
{
if (!MicrowaveQuery.Resolve(ent.Owner, ref ent.Comp))
return;
ent.Comp.Broken = true;
DirtyField(ent.AsNullable(), nameof(ent.Comp.Broken));
}
/// <summary>
/// Start up the microwave. This processes each item in the microwave to experience microwave "on-cooking"
/// effects, attempts to retrieve a microwave recipe with its valid ingredients, and activates the
/// microwave visually.
/// </summary>
public void StartCooking(Entity<MicrowaveComponent> microwave, EntityUid? user)
{
if (!HasContents(microwave.AsNullable())
|| IsActiveMicrowave(microwave.AsNullable())
|| !_power.IsPowered(microwave.Owner))
return;
var contents = GetMicrowaveContents(microwave.AsNullable());
var malfunctioning = false;
var shouldContinue = TryStartMicrowavingContents(microwave,
contents,
user,
ref malfunctioning,
out var ingredientContents);
if (!shouldContinue)
return;
var ingredients = GetTotalIngredients(ingredientContents);
var cookTime = microwave.Comp.CurrentCookTimerTime;
var recipe = GetRecipe(microwave, ingredients, cookTime);
ActivateMicrowave(microwave, recipe, malfunctioning, user);
}
/// <summary>
/// Starts up the microwave cooking operation, setting the starting time and recipe of the microwave.
/// </summary>
/// <param name="microwave">The microwave entity.</param>
/// <param name="recipe">The recipe and portion count associated with this operaton.</param>
/// <param name="malfunctioning">Whether or not this microwave is malfunctioning.</param>
/// <param name="user">The entity that is activating the microwave.</param>
private void ActivateMicrowave(Entity<MicrowaveComponent> microwave,
PortionedRecipe? recipe,
bool malfunctioning,
EntityUid? user)
{
var uid = microwave.Owner;
var component = microwave.Comp;
var curTime = Timing.CurTime;
var cookTime = component.CurrentCookTimerTime * component.CookTimeMultiplier;
Audio.PlayPredicted(component.BeginCookingSound, uid, user);
var activeComp = new ActiveMicrowaveComponent
{
PortionedRecipe = recipe,
TotalTime = component.CurrentCookTimerTime,
CookTimeEnd = curTime + TimeSpan.FromSeconds(cookTime),
Malfunctioning = malfunctioning,
NextMalfunction = malfunctioning
? curTime + component.MalfunctionInterval
: TimeSpan.Zero,
};
AddComp(uid, activeComp);
UpdateUI(microwave.AsNullable());
}
/// <summary>
/// Iterates over the contents of a microwave and performs some on-microwaved effects.
/// Plastic items will melt and turn into burned messes.
/// Metal items will cause the microwave to malfunction.
/// </summary>
/// <remarks>
/// This also raises a BeingMicrowavedEvent on each item in the microwave. The result of
/// this event may cause us to exit early and proceed with cooking - for example, an
/// entity that causes the microwave to explode when microwaved.
///
/// If cooking is not cancelled, the items inside will gain `ActiveMicrowaveComponent.`.
/// </remarks>
/// <param name="microwave">The microwave entity.</param>
/// <param name="contents">The contents of the microwave.</param>
/// <param name="user">The entity that is activating the microwave.</param>
/// <param name="malfunctioning">Whether or not the microwave is malfunctioning.</param>
/// <param name="ingredientContents">A list of entities usable as ingredients.</param>
/// <returns>Wheteher or not we can proceed to use the microwave.</returns>
private bool TryStartMicrowavingContents(Entity<MicrowaveComponent> microwave,
IReadOnlyList<EntityUid> contents,
EntityUid? user,
ref bool malfunctioning,
out List<EntityUid> ingredientContents)
{
ingredientContents = contents.ToList();
foreach (var item in contents)
{
MicrowaveItem(microwave,
item,
user,
ref malfunctioning,
out var shouldExit,
out var shouldRemoveFromIngredients);
if (shouldExit)
return false;
if (shouldRemoveFromIngredients)
ingredientContents.Remove(item);
}
foreach (var item in ingredientContents)
{
BeginActivelyMicrowaving(item, microwave.Owner);
}
return true;
}
/// <summary>
/// Processes a single entity in the microwave. This modifies / results in three
/// different parameters related to the operation: if the microwave should malfunction,
/// if the microwave should stop, and if the entity is still usable as an ingredient.
/// </summary>
/// <param name="microwave">The microwave entity.</param>
/// <param name="item">The entity being microwaved.</param>
/// <param name="user">The entity activating the microwave.</param>
/// <param name="malfunctioning">Whether or not this microwave is malfunctioning.</param>
/// <param name="shouldStopMicrowave">Whether or not this microwave should stop.</param>
/// <param name="shouldRemoveFromIngredients">Whether or not this entity remains a valid ingredient.</param>
private void MicrowaveItem(Entity<MicrowaveComponent> microwave,
EntityUid item,
EntityUid? user,
ref bool malfunctioning,
out bool shouldStopMicrowave,
out bool shouldRemoveFromIngredients)
{
shouldStopMicrowave = false;
shouldRemoveFromIngredients = false;
// Special item-in-microwave interactions. Certain "being microwaved' interactions
// may cancel out any actual cooking, so this may early exit.
var beingMicrowaved = new BeingMicrowavedEvent(microwave.Owner, user);
RaiseLocalEvent(item, beingMicrowaved);
if (beingMicrowaved.Handled)
{
UpdateUI(microwave.AsNullable());
shouldStopMicrowave = true;
return;
}
if (_whitelist.IsWhitelistPass(microwave.Comp.MalfunctionWhenCookedWhitelist, item))
malfunctioning = true;
if (_whitelist.IsWhitelistPass(microwave.Comp.BurnWhenCookedWhitelist, item))
{
shouldRemoveFromIngredients = true;
CreateBurnedMess(microwave, item);
}
}
/// <summary>
/// Turns a single entity in the microwave into a failed "burned mess" recipe.
/// </summary>
/// <remarks>
/// This happens to entities that pass <see cref="MicrowaveComponent.BurnWhenCookedWhitelist"/>
/// when microwaved.
/// </remarks>
/// <param name="microwave">The microwave entity.</param>
/// <param name="item">The entity to burn.</param>
private void CreateBurnedMess(Entity<MicrowaveComponent> microwave, EntityUid item)
{
var entProto = microwave.Comp.FailureResult;
var coords = Transform(microwave).Coordinates;
var junk = PredictedSpawnAtPosition(entProto, coords);
Container.Insert(junk, microwave.Comp.Storage);
PredictedDel(item);
}
/// <summary>
/// Completes a recipe in a microwave, removing its relevant ingredient contents
/// from the microwave and producing finished dish entities in their place.
/// </summary>
/// <param name="microwave">The microwave entity.</param>
/// <param name="portionedRecipe">The recipe we are using.</param>
private void SpawnFinishedRecipe(Entity<MicrowaveComponent> microwave, PortionedRecipe portionedRecipe)
{
SubtractContents(microwave, portionedRecipe);
var coords = Transform(microwave).Coordinates;
for (var i = 0; i < portionedRecipe.Count; i++)
{
if (!ProtoMan.Resolve(portionedRecipe.Recipe, out var recipe))
continue;
PredictedSpawnAtPosition(recipe.Result, coords);
}
}
/// <summary>
/// Finishes a cooking operation in the microwave, resulting in a finished food recipe,
/// the ejection of all remaining ingredients, and a sound cue.
/// </summary>
/// <param name="ent">The micorawve entity.</param>
private void CompleteCooking(Entity<ActiveMicrowaveComponent, MicrowaveComponent> ent)
{
var active = ent.Comp1;
var microwave = ent.Comp2;
var microwaveEnt = (ent.Owner, microwave);
// Spawn a finished recipe, if there is one.
if (active.PortionedRecipe != null)
SpawnFinishedRecipe(microwaveEnt, active.PortionedRecipe.Value);
// TODO: Audio prediction sucks. Predict this properly when audio prediction is less broken
// https://github.com/space-wizards/RobustToolbox/issues/6436
if (_net.IsServer)
Audio.PlayPvs(microwave.FoodDoneSound, ent); // beep... beep... beep
// Clean up the microwave.
Container.EmptyContainer(microwave.Storage);
StopCooking(microwaveEnt);
}
/// <summary>
/// Removes components from a microwave and its contents related to active microwave use.
/// </summary>
/// <remarks>
/// When the ActiveMicrowaveComponent is removed, it will trigger <see cref="OnCookEnd"/> on shutdown.
/// </remarks>
/// <param name="ent">The microwave entity.</param>
private void StopCooking(Entity<MicrowaveComponent> ent)
{
RemComp<ActiveMicrowaveComponent>(ent);
}
}

View file

@ -0,0 +1,282 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Kitchen.Components;
using Content.Shared.Stacks;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.Shared.Kitchen.EntitySystems;
public abstract partial class MicrowaveSystem
{
/// <summary>
/// This event tries to get secret recipes that the microwave might be capable of.
/// Currently, we only check the microwave itself, but in the future, the user might be able to learn recipes.
/// </summary>
[SubscribeLocalEvent]
private void OnGetSecretRecipes(Entity<FoodRecipeProviderComponent> ent, ref GetSecretRecipesEvent args)
{
foreach (var recipeId in ent.Comp.ProvidedRecipes)
{
if (ProtoMan.Resolve(recipeId, out var recipeProto))
args.Recipes.Add(recipeProto);
}
}
/// <summary>
/// Adds all usable ingredients from a given entity to an ingredient list.
/// </summary>
/// <remarks>
/// The entity itself is a "solid".
/// If it's a stack, then that stack is its "stacks".
/// If it has a usable ingredient solution, then the solution's contents are "reagents".
/// </remarks>
/// <param name="item">The entity to use as ingredients.</param>
/// <param name="ingredients">A dictionary of available ingredients to add to.</param>
private void AddItemIngredients(EntityUid item, ref CookingIngredients ingredients)
{
if (TryGetSolidId(item, out var solidId))
ingredients.AddSolid(solidId.Value);
if (StackQuery.TryComp(item, out var stack) && TryGetStackId(item, out var stackId))
ingredients.AddStack(stackId.Value, stack.Count);
if (TryGetUsableIngredientSolution(item, out _, out var solution))
{
foreach (var (reagent, quantity) in solution.Contents)
{
ingredients.AddReagent(reagent.Prototype, quantity);
}
}
}
/// <summary>
/// Attempts to get a solution from an entity that can be used as viable ingredients in a recipe.
/// </summary>
/// <remarks>
/// For example, a beaker's contents will work, but not the contents of an uncracked egg.
/// </remarks>
/// <param name="uid">The entity to attempt to get a usable ingredient solution for.</param>
/// <param name="solutionEntity">A usable solution entity, if available.</param>
/// <param name="solution">A usable solution, if available.</param>
/// <returns>Whether or not a usable ingredient solution was successfully retrieved.</returns>
private bool TryGetUsableIngredientSolution(EntityUid uid,
[NotNullWhen(true)] out Entity<SolutionComponent>? solutionEntity,
[NotNullWhen(true)] out Solution? solution)
{
return Solution.TryGetDrainableSolution(uid, out solutionEntity, out solution);
}
/// <summary>
/// Given a dictionary of stacks that need to be spent in a recipe, and the amount of stacks
/// of a stack we have available, this function gets the number of stacks we need to remove
/// from the stack entity. It also removes this amount from the remaining stacks dictionary.
/// </summary>
/// <param name="availableStacks">How many stacks we have available.</param>
/// <param name="stackId">The stack ID associated with the stack.</param>
/// <param name="ingredientsToSpend">How many stacks still need to be spent in the recipe.</param>
/// <returns>How many stacks we should remove from the available stack entity.</returns>
private static int SpendStackQuantity(int availableStacks,
ProtoId<StackPrototype> stackId,
CookingIngredients ingredientsToSpend)
{
if (!ingredientsToSpend.Stacks.TryGetValue(stackId, out var remaining))
return 0;
var spent = Math.Min(availableStacks, remaining);
ingredientsToSpend.AddStack(stackId, -spent);
return spent;
}
/// <summary>
/// Given a dictionary of reagents that need to be spent in a recipe, and a quantity of a reagent
/// that we have available in a solution, this function gets the amount of reagents we need to
/// remove from the solution. This also removes that amount from the "reagents to spend" dictionary.
/// </summary>
/// <param name="availableQuantity">How much reagent we have available.</param>
/// <param name="reagent">The ID of the reagent we are spending.</param>
/// <param name="ingredientsToSpend">A dictionary of reagents that still need to be spent.</param>
/// <returns>How much we should reduce the available reagent volume by.</returns>
private static FixedPoint2 SpendReagentQuantity(FixedPoint2 availableQuantity,
ProtoId<ReagentPrototype> reagent,
CookingIngredients ingredientsToSpend)
{
if (!ingredientsToSpend.Reagents.TryGetValue(reagent, out var remaining))
return 0;
var spent = FixedPoint2.Min(availableQuantity, remaining);
ingredientsToSpend.AddReagent(reagent, -spent);
return spent;
}
/// <summary>
/// Removes a solid ingredient that is used in a recipe, removing it from the dictionary of
/// remaining solids that still need to be spent in the recipe.
/// </summary>
/// <param name="item">The entity to remove.</param>
/// <param name="itemProto">The solid ID of the ingredient.</param>
/// <param name="container">The microwave's ingredient storage container.</param>
/// <param name="ingredientsToSpend">The struct representing ingredients we still need to spend.</param>
private void SubtractSolidContents(EntityUid item,
EntProtoId itemProto,
Container container,
CookingIngredients ingredientsToSpend)
{
if (!ingredientsToSpend.Solids.ContainsKey(itemProto))
return;
ingredientsToSpend.AddSolid(itemProto, -1);
Container.Remove(item, container);
PredictedQueueDel(item);
}
/// <summary>
/// Given a dictionary of remaining stacks that need to be spent in a recipe, this function
/// reduces a stack entity's count by however many stacks need to be spent. This also removes the
/// stack count from our remaining ingredients.
/// </summary>
/// <remarks>
/// If a recipe calls for two stacks of plasma sheets, and you put 5 sheets in the microwave, then
/// the plasma sheet stack would reduce by 2 (leaving you with 3 remaining), and plasma will
/// be removed from the remaining stacks dictionary.
/// </remarks>
/// <param name="ent">The stack entity.</param>
/// <param name="ingredientsToSpend">The struct representing ingredients we still need to spend.</param>
private void SubtractStackContents(Entity<StackComponent?> ent,
CookingIngredients ingredientsToSpend)
{
if (!Resolve(ent.Owner, ref ent.Comp))
return;
var stack = ent.Comp;
var stackId = stack.StackTypeId;
var startingQuantity = stack.Count;
var quantityToRemove = SpendStackQuantity(startingQuantity, stackId, ingredientsToSpend);
_stack.ReduceCount(ent.AsNullable(), quantityToRemove);
}
/// <summary>
/// Given a dictionary of remaining reagents that still need to be spent in a recipe, this function iterates
/// over a solution's contents and subtracts reagents according to the reagents to spend. This also removes
/// it from our remaining ingredients.
/// </summary>
/// <remarks>
/// Say you still have 6u mayonnaise remaining, and the solution has 10u mayonnaise, then 6u of
/// mayonnaise would be removed from the solution, leaving you with 4u left over. Then, mayonnaise
/// is removed from the dictionary of remaining reagents.
/// </remarks>
/// <param name="solutionEntity">The solution entity.</param>
/// <param name="solution">The solution itself.</param>
/// <param name="ingredientsToSpend">The struct representing ingredients we still need to spend.</param>
private void SubtractReagentContents(Entity<SolutionComponent> solutionEntity,
Solution solution,
CookingIngredients ingredientsToSpend)
{
var reagentsToProcess = ingredientsToSpend.Reagents.Keys.ToList();
foreach (var reagent in reagentsToProcess)
{
var availableQuantity = solution.GetTotalPrototypeQuantity(reagent);
if (availableQuantity == 0)
continue;
var quantityToRemove = SpendReagentQuantity(availableQuantity, reagent, ingredientsToSpend);
Solution.RemoveReagent(solutionEntity, reagent, quantityToRemove);
}
}
/// <summary>
/// Attempt to get the solid ID of a given entity.
/// </summary>
/// <param name="item">The entity to retrieve a solid ID for.</param>
/// <param name="solidId">The solid ID of the entity, if any.</param>
/// <returns>
/// Whether or not the solid ID was successfully retrieved. False if entity lacks an entity prototype ID.
/// </returns>
// TODO: Solids should be tag-based, or something like that. Not prototype-based.
private bool TryGetSolidId(EntityUid item,
[NotNullWhen(true)] out EntProtoId? solidId)
{
solidId = MetaData(item).EntityPrototype?.ID;
return solidId != null;
}
/// <summary>
/// Attempt to get the stack ID of a given entity.
/// </summary>
/// <param name="ent">The stack entity.</param>
/// <param name="stack">The stack prototype associated with this entity, if any.</param>
/// <returns>
/// Whether or not a stack ID is successfully retrieved. False if this entity is not a stack.
/// </returns>
private bool TryGetStackId(Entity<StackComponent?> ent,
[NotNullWhen(true)] out ProtoId<StackPrototype>? stack)
{
stack = null;
if (!Resolve(ent.Owner, ref ent.Comp, logMissing: false))
return false;
stack = ent.Comp.StackTypeId;
return true;
}
/// <summary>
/// Spend ingredients in the microwave based on a given recipe. This deletes ingredient entities (solids),
/// subtracts respective stack counts, and removes reagents from ingredient containers (reagents).
/// </summary>
/// <remarks>
/// This function does not check whether or not the contents have *enough* ingredients to
/// subtract - it simply performs the subtraction.
/// </remarks>
/// <param name="ent">The microwave entity.</param>
/// <param name="portionedRecipe">The recipe used to spend ingredients.</param>
private void SubtractContents(Entity<MicrowaveComponent> ent, PortionedRecipe portionedRecipe)
{
if (!ProtoMan.Resolve(portionedRecipe.Recipe, out var recipe))
return;
var ingredientsToSpend = recipe.Ingredients * portionedRecipe.Count;
var solidsToSpend = ingredientsToSpend.Solids;
var stacksToSpend = ingredientsToSpend.Stacks;
var reagentsToSpend = ingredientsToSpend.Reagents;
var microwave = ent.Comp;
var microwaveItems = GetMicrowaveContents(ent.AsNullable()).ToArray();
foreach (var item in microwaveItems)
{
if (solidsToSpend.Count > 0
&& TryGetSolidId(item, out var solidId)
&& solidsToSpend.ContainsKey(solidId.Value))
{
SubtractSolidContents(item, solidId.Value, microwave.Storage, ingredientsToSpend);
continue;
// We're exiting early here; if the solid ingredient is removed from the container,
// then we shouldn't be attempting to use its stack or reagents.
}
if (stacksToSpend.Count > 0
&& StackQuery.TryComp(item, out var stack)
&& TryGetStackId((item, stack), out var stackId)
&& stacksToSpend.ContainsKey(stackId.Value))
{
SubtractStackContents((item, stack), ingredientsToSpend);
if (Deleted(item) || stack.Count <= 0)
continue;
// We're exiting early here - if the stack is empty, then the stack entity
// is gonna be deleted. Which means we shouldn't be using its reagents.
}
if (reagentsToSpend.Count > 0
&& TryGetUsableIngredientSolution(item, out var solutionEntity, out var solution)
&& solution.Volume > 0)
SubtractReagentContents(solutionEntity.Value, solution, ingredientsToSpend);
}
}
}

View file

@ -0,0 +1,120 @@
using Content.Shared.Kitchen.Components;
using Robust.Shared.Serialization;
namespace Content.Shared.Kitchen.EntitySystems;
public abstract partial class MicrowaveSystem
{
/// <summary>
/// Subscribe to UI-related events for microwaves.
/// </summary>
private void InitializeUI()
{
SubscribeLocalEvent<MicrowaveComponent, MicrowaveStartCookMessage>((e, ref m) => StartCooking(e, m.Actor));
}
/// <summary>
/// Ejects all ingredients from the microwave.
/// </summary>
[SubscribeLocalEvent]
private void OnEjectAll(Entity<MicrowaveComponent> ent, ref MicrowaveEjectMessage args)
{
if (!HasContents(ent.AsNullable()) || IsActiveMicrowave(ent.AsNullable()))
return;
Container.EmptyContainer(ent.Comp.Storage);
var audioParams = ent.Comp.ClickSound.Params;
audioParams = audioParams.AddVolume(-2);
Audio.PlayPredicted(ent.Comp.ClickSound, ent, args.Actor, audioParams);
}
/// <summary>
/// Ejects an ingredient entity from the microwave.
/// </summary>
[SubscribeLocalEvent]
private void OnEjectSolidIndexed(Entity<MicrowaveComponent> ent, ref MicrowaveEjectSolidIndexedMessage args)
{
if (!HasContents(ent.AsNullable()) || IsActiveMicrowave(ent.AsNullable()))
return;
Container.Remove(GetEntity(args.EntityId), ent.Comp.Storage);
var audioParams = ent.Comp.ClickSound.Params;
Audio.PlayPredicted(ent.Comp.ClickSound, ent, args.Actor, audioParams);
}
/// <summary>
/// Change the cook time of the microwave by selecting a new button index.
/// </summary>
[SubscribeLocalEvent]
private void OnSelectCookTime(Entity<MicrowaveComponent> ent, ref MicrowaveSelectCookTimeMessage args)
{
if (IsActiveMicrowave(ent.AsNullable())
|| !_power.IsPowered(ent.Owner))
return;
// some validation to prevent trollage
if (args.NewCookTime % 5 != 0 || args.NewCookTime > ent.Comp.MaxCookTime)
return;
ent.Comp.CurrentCookTimeButtonIndex = args.ButtonIndex;
ent.Comp.CurrentCookTimerTime = args.NewCookTime;
Dirty(ent);
var audioParams = ent.Comp.ClickSound.Params;
Audio.PlayPredicted(ent.Comp.ClickSound, ent, args.Actor, audioParams);
UpdateUI(ent.AsNullable());
}
/// <summary>
/// Update the UI state of the microwave, including the microwave's current contents, cook time,
/// and whether or not it is actively cooking.
/// </summary>
/// <param name="microwave">The microwave to update.</param>
public virtual void UpdateUI(Entity<MicrowaveComponent?> microwave)
{
}
}
/// <summary>
/// Sent from client to server to request the microwave to start cooking.
/// </summary>
[Serializable, NetSerializable]
public sealed class MicrowaveStartCookMessage : BoundUserInterfaceMessage;
/// <summary>
/// Sent from client to server to request ejecting all contents of the microwave.
/// </summary>
[Serializable, NetSerializable]
public sealed class MicrowaveEjectMessage : BoundUserInterfaceMessage;
/// <summary>
/// Sent from client to server to request ejecting an entity from the microwave.
/// </summary>
[Serializable, NetSerializable]
public sealed class MicrowaveEjectSolidIndexedMessage(NetEntity entityId) : BoundUserInterfaceMessage
{
/// <summary>
/// The entity to eject from the microwave.
/// </summary>
public NetEntity EntityId = entityId;
}
/// <summary>
/// Sent from client to server to request changing the selected cook time button of the microwave.
/// </summary>
[Serializable, NetSerializable]
public sealed class MicrowaveSelectCookTimeMessage(int buttonIndex, uint inputTime) : BoundUserInterfaceMessage
{
/// <summary>
/// The index of the cook time button to select.
/// </summary>
public int ButtonIndex = buttonIndex;
/// <summary>
/// The cooking time associated with the newly-selected button.
/// </summary>
public uint NewCookTime = inputTime;
}

View file

@ -0,0 +1,191 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Destructible;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Item;
using Content.Shared.Kitchen.Components;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stacks;
using Content.Shared.Temperature.Systems;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.Kitchen.EntitySystems;
/// <summary>
/// A system that handles microwave logic, such as activation, malfunctions, and producing cooked recipes.
/// TODO: Replace with a more sophisticated(?) cooking system.
/// </summary>
public abstract partial class MicrowaveSystem : EntitySystem
{
[Dependency] protected IGameTiming Timing = default!;
[Dependency] protected SharedAppearanceSystem Appearance = default!;
[Dependency] protected SharedAudioSystem Audio = default!;
[Dependency] protected SharedContainerSystem Container = default!;
[Dependency] protected SharedPopupSystem Popup = default!;
[Dependency] protected SharedSolutionContainerSystem Solution = default!;
[Dependency] private INetManager _net = default!;
[Dependency] private EntityWhitelistSystem _whitelist = default!;
[Dependency] private RecipeSystem _recipes = default!;
[Dependency] private SharedDeviceLinkSystem _deviceLink = default!;
[Dependency] private SharedHandsSystem _hands = default!;
[Dependency] private SharedItemSystem _item = default!;
[Dependency] private SharedPowerReceiverSystem _power = default!;
[Dependency] private SharedPowerStateSystem _powerState = default!;
[Dependency] private SharedStackSystem _stack = default!;
[Dependency] private SharedTemperatureSystem _temperature = default!;
[Dependency] protected EntityQuery<MicrowaveComponent> MicrowaveQuery;
[Dependency] protected EntityQuery<ActiveMicrowaveComponent> ActiveMicrowaveQuery;
[Dependency] protected EntityQuery<ItemComponent> ItemQuery;
[Dependency] protected EntityQuery<StackComponent> StackQuery;
public override void Initialize()
{
base.Initialize();
InitializeUI();
}
/// <summary>
/// Processes every active microwave's ongoing cooking operation.
/// </summary>
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = Timing.CurTime;
var query = EntityQueryEnumerator<ActiveMicrowaveComponent, MicrowaveComponent>();
while (query.MoveNext(out var uid, out var active, out var microwave))
{
// Roll malfunctions
if (active.Malfunctioning && active.NextMalfunction < curTime)
{
active.NextMalfunction += microwave.MalfunctionInterval;
DirtyField(uid, active, nameof(active.NextMalfunction));
RollMalfunction((uid, microwave));
}
var timeSinceUpdate = curTime - active.LastCookUpdated;
var timeElapsed = (float)timeSinceUpdate.TotalSeconds;
// Finish cooking
if (active.CookTimeEnd < curTime)
{
AddTemperature((uid, microwave), timeElapsed);
CompleteCooking((uid, active, microwave));
continue;
}
// Otherwise, process the cooking cycle
if (active.NextCookUpdate > curTime)
continue;
active.NextCookUpdate += microwave.UpdateInterval;
active.LastCookUpdated = curTime;
DirtyFields(uid, active, null, nameof(active.NextCookUpdate), nameof(active.LastCookUpdated));
AddTemperature((uid, microwave), timeElapsed);
}
}
/// <summary>
/// Adds an "on" port to this microwave.
/// </summary>
[SubscribeLocalEvent]
private void OnMapInit(Entity<MicrowaveComponent> ent, ref MapInitEvent args)
{
_deviceLink.EnsureSinkPorts(ent, ent.Comp.OnPort);
}
/// <summary>
/// When a microwave is broken, its appearance changes and it stops being usable for cooking.
/// It will stop any ongoing cooking operations and empty its contents.
/// </summary>
[SubscribeLocalEvent]
private void OnBreak(Entity<MicrowaveComponent> ent, ref BreakageEventArgs args)
{
ent.Comp.Broken = true;
DirtyField(ent.AsNullable(), nameof(ent.Comp.Broken));
SetAppearance(ent, MicrowaveVisualState.Broken);
StopCooking(ent);
Container.EmptyContainer(ent.Comp.Storage);
UpdateUI(ent.AsNullable());
}
/// <summary>
/// Stop cooking if the microwave loses power.
/// </summary>
[SubscribeLocalEvent]
private void OnPowerChanged(Entity<MicrowaveComponent> ent, ref PowerChangedEvent args)
{
if (!args.Powered)
{
SetAppearance(ent, MicrowaveVisualState.Idle);
StopCooking(ent);
}
UpdateUI(ent.AsNullable());
}
/// <summary>
/// Empty the microwave if it is unanchored.
/// </summary>
[SubscribeLocalEvent]
private void OnAnchorChanged(Entity<MicrowaveComponent> ent, ref AnchorStateChangedEvent args)
{
if (!args.Anchored)
Container.EmptyContainer(ent.Comp.Storage);
}
/// <summary>
/// Turns the microwave on if its "on" port is activated.
/// </summary>
[SubscribeLocalEvent]
private void OnSignalReceived(Entity<MicrowaveComponent> ent, ref SignalReceivedEvent args)
{
if (ent.Comp.Broken || !_power.IsPowered(ent.Owner))
return;
if (args.Port == ent.Comp.OnPort)
StartCooking(ent, null);
}
/// <summary>
/// Updates the microwave's appearance state.
/// </summary>
/// <param name="ent">The microwave entity.</param>
/// <param name="state">The visual state of the microwave.</param>
private void SetAppearance(Entity<MicrowaveComponent> ent,
MicrowaveVisualState state)
{
var display = ent.Comp.Broken ? MicrowaveVisualState.Broken : state;
Appearance.SetData(ent.Owner,
PowerDeviceVisuals.VisualState,
display);
}
}
[Serializable, NetSerializable]
public enum MicrowaveVisualState
{
Idle,
Cooking,
Broken,
Bloody
}
[NetSerializable, Serializable]
public enum MicrowaveUiKey
{
Key
}

View file

@ -6,5 +6,5 @@ namespace Content.Shared.Kitchen;
[ByRefEvent]
public struct GetSecretRecipesEvent()
{
public List<FoodRecipePrototype> Recipes = new();
public List<MicrowaveMealRecipePrototype> Recipes = new();
}

View file

@ -1,63 +1,274 @@
using System.Linq;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Stacks;
using Robust.Shared.Prototypes;
namespace Content.Shared.Kitchen
namespace Content.Shared.Kitchen;
/// <summary>
/// A microwave-based cooking recipe to produce an entity.
/// </summary>
[Prototype]
public sealed partial class MicrowaveMealRecipePrototype : IPrototype
{
/// <inheritdoc />
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// A recipe for space microwaves.
/// The name of the recipe.
/// </summary>
[Prototype("microwaveMealRecipe")]
public sealed partial class FoodRecipePrototype : IPrototype
/// <remarks>
/// This is used to sort recipes in alphabetical order in the guidebook.
/// </remarks>
[DataField("name")]
private LocId _name = string.Empty;
public string Name => Loc.GetString(_name);
/// <summary>
/// The guidebook grouping for this recipe.
/// </summary>
[DataField]
public string Group = "Other";
/// <summary>
/// The cooking ingredients used in this recipe.
/// </summary>
[IncludeDataField]
public CookingIngredients Ingredients;
/// <summary>
/// The resulting entity made from this recipe.
/// </summary>
[DataField]
public EntProtoId Result { get; private set; } = string.Empty;
/// <summary>
/// The cooking time of this recipe.
/// </summary>
[DataField]
public uint Time { get; private set; } = 5;
/// <summary>
/// Is this recipe unavailable in normal circumstances?
/// </summary>
[DataField]
public bool SecretRecipe;
/// <summary>
/// Manual priority field for recipes that take precedent over others. Higher number = picked over others.
/// </summary>
/// <remarks>
/// Secret recipes always have higher priority over non-secret recipes. Only needed when a recipe's
/// ingredients may overlap with another in a way that'll cause that recipe to be chosen over this one
/// erroneously. This field should be used as minimally as possible.
/// </remarks>
[DataField]
public int Priority;
}
/// <summary>
/// A data value representing ingredients for an appliance recipe.
/// </summary>
[DataDefinition]
public partial record struct CookingIngredients
{
public CookingIngredients(Dictionary<EntProtoId, int> solids,
Dictionary<ProtoId<StackPrototype>, int> stacks,
Dictionary<ProtoId<ReagentPrototype>, FixedPoint2> reagents)
{
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
Solids = solids;
Stacks = stacks;
Reagents = reagents;
}
[DataField("name")]
private string _name = string.Empty;
/// <summary>
/// A dictionary of solid item ingredient quantities - actual items used in a recipe.
/// </summary>
// TODO: This should use tags or whitelists instead of entity prototype IDs
[DataField]
public Dictionary<EntProtoId, int> Solids { get; private set; } = new();
[DataField]
public string Group = "Other";
/// <summary>
/// A dictionary of stack quantities, such as plastic sheets or cloth rolls.
/// </summary>
[DataField]
public Dictionary<ProtoId<StackPrototype>, int> Stacks { get; private set; } = new();
[DataField("reagents")]
private Dictionary<ProtoId<ReagentPrototype>, FixedPoint2> _ingsReagents = new();
/// <summary>
/// A dictionary of reagent quantities.
/// </summary>
[DataField]
// TODO: Use ReagentQuantity[]
public Dictionary<ProtoId<ReagentPrototype>, FixedPoint2> Reagents { get; private set; } = new();
[DataField("solids")]
private Dictionary<EntProtoId, FixedPoint2> _ingsSolids = new();
/// <summary>
/// Adds a quantity of a solid ingredient to this ingredients list.
/// </summary>
/// <param name="solidId">The ID of the solid to add.</param>
/// <param name="count">How much of the solid to add. 1 by default.</param>
public readonly void AddSolid(EntProtoId solidId, int count = 1)
{
if (count == 0)
return;
[DataField(required: true)]
public EntProtoId Result;
var newCount = Solids.GetValueOrDefault(solidId) + count;
if (newCount > 0)
Solids[solidId] = newCount;
else
Solids.Remove(solidId);
}
[DataField("time")]
public uint CookTime { get; private set; } = 5;
/// <summary>
/// Adds a quantity of a stack to this ingredients list.
/// </summary>
/// <param name="stackId">The ID of the stack to add.</param>
/// <param name="count">How many stacks to add.</param>
public readonly void AddStack(ProtoId<StackPrototype> stackId, int count)
{
if (count == 0)
return;
public string Name => Loc.GetString(_name);
var newCount = Stacks.GetValueOrDefault(stackId) + count;
if (newCount > 0)
Stacks[stackId] = newCount;
else
Stacks.Remove(stackId);
}
// TODO Turn this into a ReagentQuantity[]
public IReadOnlyDictionary<ProtoId<ReagentPrototype>, FixedPoint2> IngredientsReagents => _ingsReagents;
public IReadOnlyDictionary<EntProtoId, FixedPoint2> IngredientsSolids => _ingsSolids;
/// <summary>
/// Adds a quantity of a reagent to this ingredients list.
/// </summary>
/// <param name="reagentId">The ID of the reagent to add.</param>
/// <param name="quantity">The volume of the reagent to add.</param>
public readonly void AddReagent(ProtoId<ReagentPrototype> reagentId, FixedPoint2 quantity)
{
if (quantity == 0)
return;
/// <summary>
/// Is this recipe unavailable in normal circumstances?
/// </summary>
[DataField]
public bool SecretRecipe = false;
var newQuantity = Reagents.GetValueOrDefault(reagentId) + quantity;
if (newQuantity > 0)
Reagents[reagentId] = newQuantity;
else
Reagents.Remove(reagentId);
}
/// <summary>
/// Count the number of ingredients in a recipe for sorting the recipe list.
/// This makes sure that where ingredient lists overlap, the more complex
/// recipe is picked first.
/// </summary>
public FixedPoint2 IngredientCount()
/// <summary>
/// Count the number of ingredients in a recipe for sorting the recipe list.
/// This makes sure that where ingredient lists overlap, the more complex
/// recipe is picked first.
/// </summary>
public readonly FixedPoint2 Count()
{
var solidCount = Solids.Sum(s => s.Value);
var reagentCount = Reagents.Count;
var stackCount = Stacks.Sum(s => s.Value);
return solidCount + reagentCount + stackCount;
}
/// <summary>
/// Get the number of times a given recipe can be made with this struct's ingredients.
/// </summary>
/// <param name="recipe">The recipe to attempt to make with these ingredients.</param>
/// <returns>How many times the given recipe can be made.</returns>
public readonly uint PortionForRecipe(CookingIngredients recipe)
{
var solidPortions = GetTimesFulfilled(Solids, recipe.Solids,
(available, count) => (uint)(available / count));
if (solidPortions == 0)
return 0;
var stackPortions = GetTimesFulfilled(Stacks, recipe.Stacks,
(available, count) => (uint)(available / count));
if (stackPortions == 0)
return 0;
var reagentPortions = GetTimesFulfilled(Reagents, recipe.Reagents,
(available, count) => (uint)(available / count).Int());
if (reagentPortions == 0)
return 0;
return new[] { solidPortions, stackPortions, reagentPortions }.Min();
}
/// <summary>
/// Given an ingredient dictionary, and a recipe's ingredient dictionary, gets the maximum
/// amount of times the recipe can be fulfilled with our available ingredients.
/// </summary>
/// <typeparam name="T">The key of the ingredient dictionary.</typeparam>
/// <typeparam name="TCount">A numerical quantity of the ingredient in the dictionary.</typeparam>
/// <param name="ingredients">A dictionary of available ingredients.</param>
/// <param name="recipe">A recipe's dictionary of required ingredients.</param>
/// <param name="divide">Function to divide the recipe's ingredient count by our ingredient count.</param>
/// <returns>How many times the given recipe's ingredients can be fulfilled.</returns>
private static uint GetTimesFulfilled<T, TCount>(Dictionary<T, TCount> ingredients,
Dictionary<T, TCount> recipe,
Func<TCount, TCount, uint> divide)
where T : notnull
{
var portions = uint.MaxValue;
foreach (var (ingredient, count) in recipe)
{
FixedPoint2 n = 0;
n += _ingsReagents.Count; // number of distinct reagents
foreach (FixedPoint2 i in _ingsSolids.Values) // sum the number of solid ingredients
{
n += i;
}
return n;
if (!ingredients.TryGetValue(ingredient, out var available))
return 0;
var ingredientPortions = divide(available, count);
portions = Math.Min(portions, ingredientPortions);
}
return portions;
}
/// <summary>
/// Sum the ingredient counts of two ingredient structs and return the result.
/// </summary>
public static CookingIngredients operator +(CookingIngredients c1, CookingIngredients c2)
{
var newIngredients = c1;
foreach (var (key, count) in c2.Solids)
{
newIngredients.AddSolid(key, count);
}
foreach (var (key, count) in c2.Stacks)
{
newIngredients.AddStack(key, count);
}
foreach (var (key, quantity) in c2.Reagents)
{
newIngredients.AddReagent(key, quantity);
}
return newIngredients;
}
/// <summary>
/// Multiply the amount of ingredients by a scalar and return the result.
/// </summary>
public static CookingIngredients operator *(CookingIngredients c1, int scalar)
{
var scaledSolids = c1.Solids.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value * scalar);
var scaledStacks = c1.Stacks.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value * scalar);
var scaledReagents = c1.Reagents.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value * scalar);
return new(scaledSolids, scaledStacks, scaledReagents);
}
/// <summary>
/// Multiply the amount of ingredients by a scalar and return the result.
/// </summary>
public static CookingIngredients operator *(CookingIngredients c1, uint scalar)
{
return c1 * (int)scalar;
}
}

View file

@ -1,32 +0,0 @@
using System.Linq;
using Robust.Shared.Prototypes;
namespace Content.Shared.Kitchen;
public sealed partial class RecipeManager : EntitySystem
{
public List<FoodRecipePrototype> Recipes { get; private set; } = new();
public override void Initialize()
{
base.Initialize();
ReloadRecipes();
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (args.WasModified<FoodRecipePrototype>())
ReloadRecipes();
}
private void ReloadRecipes()
{
Recipes = ProtoMan
.EnumeratePrototypes<FoodRecipePrototype>()
.Where(x => !x.SecretRecipe)
.OrderByDescending(x => x.IngredientCount())
.ToList();
}
}

View file

@ -0,0 +1,45 @@
using System.Linq;
using Robust.Shared.Prototypes;
namespace Content.Shared.Kitchen;
/// <summary>
/// A system that caches all available non-secret microwave recipes.
/// </summary>
public sealed partial class RecipeSystem : EntitySystem
{
/// <summary>
/// A list of all recipes available to the recipe manager.
/// </summary>
/// <remarks>
/// Order matters! The microwave system will use the *first* valid recipe.
/// For this reason, this list gets sorted by "complexity" - recipes with more ingredients
/// are sorted first. We make the assumption that more complex recipes are more "specific"
/// than less complex recipes whose requirements are also fulfilled.
/// </remarks>
public List<MicrowaveMealRecipePrototype> Recipes { get; private set; } = new();
public override void Initialize()
{
base.Initialize();
ReloadRecipes();
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (args.WasModified<MicrowaveMealRecipePrototype>())
ReloadRecipes();
}
private void ReloadRecipes()
{
Recipes = ProtoMan
.EnumeratePrototypes<MicrowaveMealRecipePrototype>()
.Where(x => !x.SecretRecipe)
.OrderByDescending(x => x.Priority)
.ThenByDescending(x => x.Ingredients.Count())
.ToList();
}
}

View file

@ -13,3 +13,6 @@ guidebook-microwave-reagent-quantity-display = × {$amount}u
guidebook-microwave-solid-name-display = [bold]{$ingredient}[/bold]
guidebook-microwave-solid-quantity-display = × {$amount}
guidebook-microwave-stack-name-display = [bold]{$stack}[/bold]
guidebook-microwave-stack-quantity-display = × {$amount}

View file

@ -0,0 +1,221 @@
microwave-meal-recipe-bun-name = bun recipe
microwave-meal-recipe-cotton-bun-name = cotton bun recipe
microwave-meal-recipe-bagel-name = bagel recipe
microwave-meal-recipe-bagel-poppy-name = poppyseed bagel recipe
microwave-meal-recipe-bagel-cotton-name = cotton bagel recipe
microwave-meal-recipe-appendix-burger-name = appendix burger recipe
microwave-meal-recipe-bacon-burger-name = bacon burger recipe
microwave-meal-recipe-baseball-burger-name = baseball burger recipe
microwave-meal-recipe-bearger-name = bearger recipe
microwave-meal-recipe-big-bite-burger-name = big bite burger recipe
microwave-meal-recipe-brain-burger-name = brain burger recipe
microwave-meal-recipe-cat-burger-name = cat burger recipe
microwave-meal-recipe-cheeseburger-name = cheeseburger recipe
microwave-meal-recipe-chicken-sandiwch-name = chicken sandwich recipe
microwave-meal-recipe-clown-burger-name = clownburger recipe
microwave-meal-recipe-corgi-burger-name = corgi burger recipe
microwave-meal-recipe-crab-burger-name = crab burger recipe
microwave-meal-recipe-crazy-hamburger-name = crazy hamburger recipe
microwave-meal-recipe-duck-burger-name = duck sandwich recipe
microwave-meal-recipe-empowered-burger-name = empowered burger recipe
microwave-meal-recipe-carp-burger-name = fillet-o-carp burger recipe
microwave-meal-recipe-five-burger-name = five alarm burger recipe
microwave-meal-recipe-ghost-burger-name = ghost burger recipe
microwave-meal-recipe-human-burger-name = human burger recipe
microwave-meal-recipe-jelly-burger-name = jelly burger recipe
microwave-meal-recipe-burger-mcguffin-name = McGuffin recipe
microwave-meal-recipe-burger-mcrib-name = BBQ rib sandwich recipe
microwave-meal-recipe-mime-burger-name = mime burger recipe
microwave-meal-recipe-plain-burger-name = plain burger recipe
microwave-meal-recipe-rat-burger-name = rat burger recipe
microwave-meal-recipe-robot-burger-name = roburger recipe
microwave-meal-recipe-soylent-burger-name = soylent burger recipe
microwave-meal-recipe-spell-burger-name = spell burger recipe
microwave-meal-recipe-super-bite-burger-name = super bite burger recipe
microwave-meal-recipe-tofu-burger-name = tofu burger recipe
microwave-meal-recipe-xenoburger-name = xenoburger recipe
microwave-meal-recipe-moth-roachburger-name = mothroachburger recipe
microwave-meal-recipe-banana-bread-name = banana bread recipe
microwave-meal-recipe-cornbread-name = cornbread recipe
microwave-meal-recipe-cream-cheese-bread-name = cream cheese bread recipe
microwave-meal-recipe-meat-bread-name = meat bread recipe
microwave-meal-recipe-mimana-bread-name = mimana bread recipe
microwave-meal-recipe-bread-name = bread recipe
microwave-meal-recipe-bread-cotton-name = cotton bread recipe
microwave-meal-recipe-sausage-bread-name = sausage bread recipe
microwave-meal-recipe-spider-meat-bread-name = spider meat bread recipe
microwave-meal-recipe-tofu-bread-name = tofu bread recipe
microwave-meal-recipe-xeno-meat-bread-name = xeno meat bread recipe
microwave-meal-recipe-baguette-name = baguette recipe
microwave-meal-recipe-baguette-cotton-name = baguette recipe
microwave-meal-recipe-baguette-sword-name = baguette sword recipe
microwave-meal-recipe-buttered-toast-name = buttered toast recipe
microwave-meal-recipe-french-toast-name = french toast recipe
microwave-meal-recipe-garlic-bread-name = garlic bread slice recipe
microwave-meal-recipe-jelly-toast-name = jelly toast recipe
microwave-meal-recipe-moldy-bread-slice-name = moldy bread slice recipe
microwave-meal-recipe-two-bread-slice-name = two slice recipe
microwave-meal-recipe-onion-rings-name = onion rings recipe
microwave-meal-recipe-margherita-pizza-name = margherita pizza recipe
microwave-meal-recipe-mushroom-pizza-name = mushroom pizza recipe
microwave-meal-recipe-meat-pizza-name = meat pizza recipe
microwave-meal-recipe-vegetable-pizza-name = vegetable pizza recipe
microwave-meal-recipe-hawaiian-pizza-name = Hawaiian pizza recipe
microwave-meal-recipe-dank-pizza-name = dank pizza recipe
microwave-meal-recipe-donkpocket-pizza-name = donk-pocket pizza recipe
microwave-meal-recipe-uranium-pizza-name = spicy rock pizza recipe
microwave-meal-recipe-cotton-pizza-name = cotton pizza recipe
microwave-meal-recipe-worldpeas-pizza-name = world peazza recipe
microwave-meal-recipe-boiled-spaghetti-name = boiled spaghetti recipe
microwave-meal-recipe-pasta-tomato-name = pasta tomato recipe
microwave-meal-recipe-meatball-spaghetti-name = spaghetti & meatballs recipe
microwave-meal-recipe-butter-noodles-name = butter noodles recipe
microwave-meal-recipe-chow-mein-name = chow mein recipe
microwave-meal-recipe-oatmeal-name = oatmeal recipe
microwave-meal-recipe-boiled-rice-name = boiled rice recipe
microwave-meal-recipe-rice-pudding-name = rice pudding recipe
microwave-meal-recipe-rice-pork-name = rice and pork recipe
microwave-meal-recipe-rice-gumbo-name = black-eyed gumbo recipe
microwave-meal-recipe-egg-rice-name = egg-fried rice recipe
microwave-meal-recipe-copypasta-name = copypasta recipe
microwave-meal-recipe-bisque-name = bisque recipe
microwave-meal-recipe-meatball-soup-name = meatball soup recipe
microwave-meal-recipe-nettle-soup-name = nettle soup recipe
microwave-meal-recipe-eyeball-soup-name = eyeball soup recipe
microwave-meal-recipe-amanita-jelly-name = amanita jelly recipe
microwave-meal-recipe-onion-soup-name = onion soup recipe
microwave-meal-recipe-mushroom-soup-name = mushroom soup recipe
microwave-meal-recipe-stew-soup-name = stew recipe
microwave-meal-recipe-tomato-soup-name = tomato soup recipe
microwave-meal-recipe-tomato-blood-soup-name = tomato blood soup recipe
microwave-meal-recipe-wing-fang-chu-soup-name = wing fang chu recipe
microwave-meal-recipe-wing-fang-chu-soup-spider-name = wing fang chu recipe
microwave-meal-recipe-vegetable-soup-name = vegetable soup recipe
microwave-meal-recipe-clown-tears-soup-name = clown tears soup recipe
microwave-meal-recipe-monkeys-delight-soup-name = monkeys delight recipe
microwave-meal-recipe-bungo-soup-name = bungo soup recipe
microwave-meal-recipe-boiled-snail-name = boiled snail recipe
microwave-meal-recipe-escargot-soup-name = escargot recipe
microwave-meal-recipe-amanita-pie-name = amanita pie recipe
microwave-meal-recipe-apple-pie-name = apple pie recipe
microwave-meal-recipe-baklava-name = baklava recipe
microwave-meal-recipe-banana-cream-pie-name = banana cream pie recipe
microwave-meal-recipe-berry-clafoutis-name = berry clafoutis recipe
microwave-meal-recipe-cherry-pie-name = cherry pie recipe
microwave-meal-recipe-frosty-pie-name = frosty pie recipe
microwave-meal-recipe-meat-pie-name = meat pie recipe
microwave-meal-recipe-pumpkin-pie-name = pumpkin pie recipe
microwave-meal-recipe-xeno-pie-name = xeno pie recipe
microwave-meal-recipe-coco-tart-name = chocolate lava tart recipe
microwave-meal-recipe-gapple-tart-name = golden apple streusel tart recipe
microwave-meal-recipe-grape-tart-name = grape tart recipe
microwave-meal-recipe-mime-tart-name = mime tart recipe
microwave-meal-recipe-cuban-carp-name = Cuban carp recipe
microwave-meal-recipe-sashimi-name = sashimi recipe
microwave-meal-recipe-miso-cola-soup-name = salty sweet misocola soup recipe
microwave-meal-recipe-loaded-baked-potato-name = loaded baked potato recipe
microwave-meal-recipe-fries-name = space fries recipe
microwave-meal-recipe-cheesy-fries-name = cheesy fries recipe
microwave-meal-recipe-carrot-fries-name = carrot fries recipe
microwave-meal-recipe-nachos-name = nachos recipe
microwave-meal-recipe-nachos-cheesy-name = cheesy nachos recipe
microwave-meal-recipe-nachos-cuban-name = cuban nachos recipe
microwave-meal-recipe-popcorn-name = popcorn recipe
microwave-meal-recipe-pancake-name = pancake recipe
microwave-meal-recipe-blueberry-pancake-name = blueberry pancake recipe
microwave-meal-recipe-waffles-name = waffle recipe
microwave-meal-recipe-waffle-soy-name = soy waffle recipe
microwave-meal-recipe-cookie-name = cookie recipe
microwave-meal-recipe-sugar-cookie-name = sugar cookie recipe
microwave-meal-recipe-raisin-cookie-name = raisin cookie recipe
microwave-meal-recipe-cookie-oatmeal-name = oatmeal cookie recipe
microwave-meal-recipe-chocolate-chip-pancake-name = chocolate chip pancake recipe
microwave-meal-recipe-apple-cake-name = apple cake recipe
microwave-meal-recipe-carrot-cake-name = carrot cake recipe
microwave-meal-recipe-lemon-cake-name = lemon cake recipe
microwave-meal-recipe-lemoon-cake-name = lemoon cake recipe
microwave-meal-recipe-orange-cake-name = orange cake recipe
microwave-meal-recipe-blueberry-cake-name = blueberry cake recipe
microwave-meal-recipe-lime-cake-name = lime cake recipe
microwave-meal-recipe-cheese-cake-name = cheese cake recipe
microwave-meal-recipe-pumpkin-cake-name = pumpkin cake recipe
microwave-meal-recipe-clown-cake-name = clown cake recipe
microwave-meal-recipe-cake-name = cake recipe
microwave-meal-recipe-birthday-cake-name = birthday cake recipe
microwave-meal-recipe-chocolate-cake-name = chocolate cake recipe
microwave-meal-recipe-brain-cake-name = brain cake recipe
microwave-meal-recipe-slime-cake-name = slime cake recipe
microwave-meal-recipe-cat-cake-name = cat cake recipe
microwave-meal-recipe-berry-delight-name = berry delight recipe
microwave-meal-recipe-bread-dog-name = bread dog recipe
microwave-meal-recipe-dumplings-name = dumplings recipe
microwave-meal-recipe-brownies-name = brownie recipe
microwave-meal-recipe-warm-donkpocket-name = warm donk pocket recipe
microwave-meal-recipe-warm-dankpocket-name = warm dank pocket recipe
microwave-meal-recipe-warm-donkpocket-spicy-name = warm spicy donk-pocket recipe
microwave-meal-recipe-warm-donkpocket-teriyaki-name = warm teriyaki-pocket recipe
microwave-meal-recipe-warm-donkpocket-pizza-name = warm pizza-pocket recipe
microwave-meal-recipe-donkpocket-honk-name = warm honk-pocket recipe
microwave-meal-recipe-donkpocket-berry-name = warm berry-pocket recipe
microwave-meal-recipe-donkpocket-stonk-name = warm stonk-pocket recipe
microwave-meal-recipe-donkpocket-carp-name = warm carp-pocket recipe
microwave-meal-recipe-donkpocket-moth-name = warm moth-pocket recipe
microwave-meal-recipe-hot-chili-name = hot chili recipe
microwave-meal-recipe-cold-chili-name = cold chili recipe
microwave-meal-recipe-clown-tears-name = clown's tears recipe
microwave-meal-recipe-chili-clown-name = chili con carnival recipe
microwave-meal-recipe-queso-name = queso recipe
microwave-meal-recipe-ribs-name = BBQ ribs recipe
microwave-meal-recipe-enchiladas-name = enchiladas recipe
microwave-meal-recipe-herb-salad-name = herb salad recipe
microwave-meal-recipe-valid-salad-name = valid salad recipe
microwave-meal-recipe-coleslaw-name = coleslaw recipe
microwave-meal-recipe-caesar-salad-name = caesar salad recipe
microwave-meal-recipe-citrus-salad-name = citrus salad recipe
microwave-meal-recipe-kimchi-salad-name = kimchi salad recipe
microwave-meal-recipe-fruit-salad-name = fruit salad recipe
microwave-meal-recipe-jungle-salad-name = jungle salad recipe
microwave-meal-recipe-watermelon-fruit-bowl-salad-name = watermelon fruit bowl recipe
microwave-meal-recipe-muffin-name = muffin recipe
microwave-meal-recipe-muffin-chocolate-name = chocolate muffin recipe
microwave-meal-recipe-muffin-berry-name = berry muffin recipe
microwave-meal-recipe-muffin-banana-name = banana muffin recipe
microwave-meal-recipe-muffin-cherry-name = cherry muffin recipe
microwave-meal-recipe-dried-tea-leaves-name = dried tea leaves recipe
microwave-meal-recipe-dried-tobacco-name = dried tobacco leaves recipe
microwave-meal-recipe-dried-cannabis-name = dried cannabis leaves recipe
microwave-meal-recipe-dried-cannabis-rainbow-name = dried rainbow cannabis leaves recipe
microwave-meal-recipe-trash-baked-banana-peel-name = baked banana peel recipe
microwave-meal-recipe-suppermatter-name = suppermatter recipe
microwave-meal-recipe-food-baked-chevre-chaud-name = chevre chaud recipe
microwave-meal-recipe-food-baked-chevre-chaud-cotton-name = cotton chevre chaud recipe
microwave-meal-recipe-cannabis-butter-name = cannabis butter recipe
microwave-meal-recipe-cannabis-brownies-name = cannabis brownie recipe
microwave-meal-recipe-corn-in-butter-name = corn in butter recipe
microwave-meal-recipe-pea-soup-name = pea soup recipe
microwave-meal-recipe-taco-shell-name = taco shell recipe
microwave-meal-recipe-taco-beef-name = beef taco recipe
microwave-meal-recipe-taco-chicken-name = chicken taco recipe
microwave-meal-recipe-taco-fish-name = fish taco recipe
microwave-meal-recipe-taco-rat-name = rat taco recipe
microwave-meal-recipe-taco-beef-supreme-name = beef taco supreme recipe
microwave-meal-recipe-taco-chicken-supreme-name = beef taco supreme recipe
microwave-meal-recipe-taco-dragon-name = draco recipe
microwave-meal-recipe-croissant-name = croissant recipe
microwave-meal-recipe-croissant-cotton-name = cotton croissant recipe
microwave-meal-recipe-throwing-croissant-name = throwing croissant recipe
microwave-meal-recipe-inert-anomaly-meat-name = inert meat anomaly recipe
microwave-meal-recipe-anomaly-meat-name = meat anomaly recipe
microwave-meal-recipe-anomaly-meat-cooked-name = cooked meat anomaly recipe
microwave-meal-recipe-grilled-cheese-sandwich-name = grilled cheese sandwich recipe
microwave-meal-recipe-grilled-cheese-sandwich-cotton-name = cotton grilled cheese sandwich recipe
microwave-meal-recipe-breakfast-american-name = full american breakfast recipe
microwave-meal-recipe-breakfast-english-name = full english breakfast recipe
microwave-meal-recipe-moproach-name = moproach recipe
microwave-meal-recipe-cotton-cake-name = cotton cake recipe
# medical recipes
microwave-meal-recipe-aloe-cream-name = aloe cream recipe
microwave-meal-recipe-medicated-suture-name = medicated suture recipe
microwave-meal-recipe-regenerative-mesh-name = regenerative mesh recipe

View file

@ -160,7 +160,7 @@
components:
- type: Microwave
cookTimeMultiplier: 0.5
baseHeatMultiplier: 200
objectHeatMultiplier: 200
capacity: 10
canMicrowaveIdsSafely: false
explosionChance: 0.3

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
- type: microwaveMealRecipe
id: RecipeAloeCream
name: aloe cream recipe
name: microwave-meal-recipe-aloe-cream-name
result: AloeCream
time: 10
group: Medicinal
@ -9,28 +9,30 @@
- type: microwaveMealRecipe
id: RecipeMedicatedSuture
name: medicated suture recipe
name: microwave-meal-recipe-medicated-suture-name
result: MedicatedSuture
time: 10
group: Medicinal
solids:
FoodPoppy: 1
Brutepack: 1
MaterialCloth1: 1
stacks:
Brutepack: 10
Cloth: 1
reagents:
TranexamicAcid: 20
Cryptobiolin: 20
- type: microwaveMealRecipe
id: RecipeRegenerativeMesh
name: regenerative mesh recipe
name: microwave-meal-recipe-regenerative-mesh-name
result: RegenerativeMesh
time: 10
group: Medicinal
solids:
FoodAloe: 1
Ointment: 1
MaterialCloth1: 1
stacks:
Ointment: 10
Cloth: 1
reagents:
Sigynate: 20
Dermaline: 20