Merge remote-tracking branch 'upstream/master' into upstream-sync

# Conflicts:
#	Resources/Prototypes/Maps/game.yml
This commit is contained in:
Morbo 2022-08-31 23:44:39 +03:00
commit 7d12bbd1a1
322 changed files with 17471 additions and 208692 deletions

View file

@ -26,7 +26,7 @@ namespace Content.Client.Clickable
/// <returns>True if the click worked, false otherwise.</returns>
public bool CheckClick(Vector2 worldPos, out int drawDepth, out uint renderOrder)
{
if (!_entMan.TryGetComponent(Owner, out ISpriteComponent? sprite) || !sprite.Visible)
if (!_entMan.TryGetComponent(Owner, out SpriteComponent? sprite) || !sprite.Visible)
{
drawDepth = default;
renderOrder = default;
@ -54,8 +54,10 @@ namespace Content.Client.Clickable
// This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites.
var relativeRotation = worldRot + _eyeManager.CurrentEye.Rotation;
Angle cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero;
// First we get `localPos`, the clicked location in the sprite-coordinate frame.
var entityXform = Matrix3.CreateInverseTransform(transform.WorldPosition, sprite.NoRotation ? -_eyeManager.CurrentEye.Rotation : worldRot);
var entityXform = Matrix3.CreateInverseTransform(transform.WorldPosition, sprite.NoRotation ? -_eyeManager.CurrentEye.Rotation : worldRot - cardinalSnapping);
var localPos = invSpriteMatrix.Transform(entityXform.Transform(worldPos));
// Check explicitly defined click-able bounds

View file

@ -22,7 +22,6 @@ namespace Content.Client.CloningConsole.UI
};
_window.OnClose += Close;
_window.CloneButton.OnPressed += _ => SendMessage(new UiButtonPressedMessage(UiButton.Clone));
_window.EjectButton.OnPressed += _ => SendMessage(new UiButtonPressedMessage(UiButton.Eject));
_window.OpenCentered();
}
@ -43,7 +42,6 @@ namespace Content.Client.CloningConsole.UI
{
_window.OnClose -= Close;
_window.CloneButton.OnPressed -= _ => SendMessage(new UiButtonPressedMessage(UiButton.Clone));
_window.EjectButton.OnPressed -= _ => SendMessage(new UiButtonPressedMessage(UiButton.Eject));
}
_window?.Dispose();
}

View file

@ -46,11 +46,6 @@
<RichTextLabel Name="ClonerBrainActivity"
Access="Public"
HorizontalExpand="True"/>
<Button Name="EjectButton"
Access="Public"
Text="{Loc 'cloning-console-eject-body-button'}"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
</BoxContainer>
</BoxContainer>
<BoxContainer Name="CloningPodMissing" Margin="5 5 5 5" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">

View file

@ -11,7 +11,6 @@ namespace Content.Client.Decals
public sealed class DecalOverlay : Overlay
{
private readonly DecalSystem _decals;
private readonly SharedTransformSystem _transform;
private readonly SpriteSystem _sprites;
private readonly IEntityManager _entManager;
private readonly IPrototypeManager _prototypeManager;
@ -22,13 +21,11 @@ namespace Content.Client.Decals
public DecalOverlay(
DecalSystem decals,
SharedTransformSystem transforms,
SpriteSystem sprites,
IEntityManager entManager,
IPrototypeManager prototypeManager)
{
_decals = decals;
_transform = transforms;
_sprites = sprites;
_entManager = entManager;
_prototypeManager = prototypeManager;
@ -39,10 +36,12 @@ namespace Content.Client.Decals
// Shouldn't need to clear cached textures unless the prototypes get reloaded.
var handle = args.WorldHandle;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var eyeAngle = args.Viewport.Eye?.Rotation ?? Angle.Zero;
foreach (var (gridId, zIndexDictionary) in _decals.DecalRenderIndex)
{
if (zIndexDictionary.Count == 0) continue;
if (zIndexDictionary.Count == 0)
continue;
if (!xformQuery.TryGetComponent(gridId, out var xform))
{
@ -53,7 +52,9 @@ namespace Content.Client.Decals
if (xform.MapID != args.MapId)
continue;
handle.SetTransform(_transform.GetWorldMatrix(xform, xformQuery));
var (_, worldRot, worldMatrix) = xform.GetWorldPositionRotationMatrix(xformQuery);
handle.SetTransform(worldMatrix);
foreach (var (_, decals) in zIndexDictionary)
{
@ -66,10 +67,23 @@ namespace Content.Client.Decals
_cachedTextures[decal.Id] = texture;
}
if (decal.Angle.Equals(Angle.Zero))
if (!_prototypeManager.TryIndex<DecalPrototype>(decal.Id, out var decalProto))
continue;
var cardinal = Angle.Zero;
if (decalProto.SnapCardinals)
{
var worldAngle = eyeAngle + worldRot;
cardinal = worldAngle.GetCardinalDir().ToAngle();
}
var angle = decal.Angle - cardinal;
if (angle.Equals(Angle.Zero))
handle.DrawTexture(texture, decal.Coordinates, decal.Color);
else
handle.DrawTexture(texture, decal.Coordinates, decal.Angle, decal.Color);
handle.DrawTexture(texture, decal.Coordinates, angle, decal.Color);
}
}
}
@ -81,11 +95,9 @@ namespace Content.Client.Decals
{
if (_prototypeManager.TryIndex<DecalPrototype>(id, out var proto))
return proto.Sprite;
else
{
Logger.Error($"Unknown decal prototype: {id}");
return new SpriteSpecifier.Texture(new ResourcePath("/Textures/noSprite.png"));
}
Logger.Error($"Unknown decal prototype: {id}");
return new SpriteSpecifier.Texture(new ResourcePath("/Textures/noSprite.png"));
}
}
}

View file

@ -8,18 +8,17 @@ namespace Content.Client.Decals
public sealed class DecalSystem : SharedDecalSystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SharedTransformSystem _transforms = default!;
[Dependency] private readonly SpriteSystem _sprites = default!;
private DecalOverlay _overlay = default!;
public Dictionary<EntityUid, SortedDictionary<int, SortedDictionary<uint, Decal>>> DecalRenderIndex = new();
private Dictionary<EntityUid, Dictionary<uint, int>> _decalZIndexIndex = new();
public readonly Dictionary<EntityUid, SortedDictionary<int, SortedDictionary<uint, Decal>>> DecalRenderIndex = new();
private readonly Dictionary<EntityUid, Dictionary<uint, int>> _decalZIndexIndex = new();
public override void Initialize()
{
base.Initialize();
_overlay = new DecalOverlay(this, _transforms, _sprites, EntityManager, PrototypeManager);
_overlay = new DecalOverlay(this, _sprites, EntityManager, PrototypeManager);
_overlayManager.AddOverlay(_overlay);
SubscribeNetworkEvent<DecalChunkUpdateEvent>(OnChunkUpdate);
@ -108,7 +107,7 @@ namespace Content.Client.Decals
chunkCollection[indices] = newChunkData;
foreach (var (uid, decal) in newChunkData)
{
{
if (zIndexIndex.TryGetValue(uid, out var zIndex))
renderIndex[zIndex].Remove(uid);

View file

@ -105,6 +105,11 @@ namespace Content.Client.EscapeMenu.UI.Tabs
AddButton(EngineKeyFunctions.MoveRight);
AddButton(EngineKeyFunctions.Walk);
AddHeader("ui-options-header-camera");
AddButton(EngineKeyFunctions.CameraRotateLeft);
AddButton(EngineKeyFunctions.CameraRotateRight);
AddButton(EngineKeyFunctions.CameraReset);
AddHeader("ui-options-header-interaction-basic");
AddButton(EngineKeyFunctions.Use);
AddButton(ContentKeyFunctions.UseItemInHand);

View file

@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using Content.Shared.Eui;
using Robust.Client.GameStates;
using Robust.Client.State;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
namespace Content.Client.Eui
{
@ -20,6 +23,16 @@ namespace Content.Client.Eui
_net.RegisterNetMessage<MsgEuiCtl>(RxMsgCtl);
_net.RegisterNetMessage<MsgEuiState>(RxMsgState);
_net.RegisterNetMessage<MsgEuiMessage>(RxMsgMessage);
_net.Disconnect += NetOnDisconnect;
}
private void NetOnDisconnect(object? sender, NetDisconnectedArgs e)
{
foreach (var openUi in _openUis)
{
openUi.Value.Eui.Closed();
}
_openUis.Clear();
}
private void RxMsgMessage(MsgEuiMessage message)
@ -36,26 +49,22 @@ namespace Content.Client.Eui
private void RxMsgCtl(MsgEuiCtl message)
{
switch (message.Type)
// Will always close the window first when getting a control message
if (_openUis.TryGetValue(message.Id, out var openEui))
{
case MsgEuiCtl.CtlType.Open:
var euiType = _refl.LooseGetType(message.OpenType);
var instance = _dtf.CreateInstance<BaseEui>(euiType);
instance.Initialize(this, message.Id);
instance.Opened();
_openUis.Add(message.Id, new EuiData(instance));
break;
case MsgEuiCtl.CtlType.Close:
var dat = _openUis[message.Id];
dat.Eui.Closed();
_openUis.Remove(message.Id);
break;
default:
throw new ArgumentOutOfRangeException();
openEui.Eui.Closed();
_openUis.Remove(message.Id);
}
if (message.Type != MsgEuiCtl.CtlType.Open)
return;
// Will open/re-open the window if the server wants the eui opened.
var euiType = _refl.LooseGetType(message.OpenType);
var instance = _dtf.CreateInstance<BaseEui>(euiType);
instance.Initialize(this, message.Id);
instance.Opened();
_openUis.Add(message.Id, new EuiData(instance));
}
private sealed class EuiData

View file

@ -1,41 +1,31 @@
using System;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Eye;
public sealed class EyeLerpingSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
// How fast the camera rotates in radians / s
private const float CameraRotateSpeed = MathF.PI;
// Safety override
private const float LerpTimeMax = 1.5f;
// Lerping information for the player's active eye.
private readonly EyeLerpInformation _playerActiveEye = new();
[Dependency] private readonly SharedMoverController _mover = default!;
// Eyes other than the primary eye that are currently active.
private readonly Dictionary<EntityUid, EyeLerpInformation> _activeEyes = new();
private readonly List<EntityUid> _toRemove = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EyeComponent, ComponentStartup>(OnEyeStartup);
SubscribeLocalEvent<EyeComponent, ComponentShutdown>(OnEyeShutdown);
UpdatesAfter.Add(typeof(TransformSystem));
@ -43,6 +33,27 @@ public sealed class EyeLerpingSystem : EntitySystem
UpdatesBefore.Add(typeof(EyeUpdateSystem));
}
private void OnEyeStartup(EntityUid uid, EyeComponent component, ComponentStartup args)
{
if (component.Eye == null)
return;
// If the eye starts up then don't lerp at all.
var xformQuery = GetEntityQuery<TransformComponent>();
TryComp<InputMoverComponent>(uid, out var mover);
xformQuery.TryGetComponent(uid, out var xform);
var lerpInfo = _activeEyes.GetOrNew(uid);
lerpInfo.TargetRotation = GetRotation(xformQuery, mover, xform);
lerpInfo.LastRotation = lerpInfo.TargetRotation;
if (xform != null)
{
lerpInfo.MapId = xform.MapID;
}
component.Eye.Rotation = lerpInfo.TargetRotation;
}
private void OnEyeShutdown(EntityUid uid, EyeComponent component, ComponentShutdown args)
{
RemoveEye(uid);
@ -50,145 +61,131 @@ public sealed class EyeLerpingSystem : EntitySystem
public void AddEye(EntityUid uid)
{
if (!_activeEyes.ContainsKey(uid))
{
_activeEyes.Add(uid, new());
}
_activeEyes.TryAdd(uid, new EyeLerpInformation());
}
public void RemoveEye(EntityUid uid)
{
if (_activeEyes.ContainsKey(uid))
_activeEyes.Remove(uid);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_gameTiming.IsFirstTimePredicted)
return;
var moverQuery = GetEntityQuery<InputMoverComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var foundEyes = new ValueList<EntityUid>(1);
// Set all of our eye rotations to the relevant values.
foreach (var (eye, entity) in GetEyes())
{
_activeEyes.Remove(uid);
var lerpInfo = _activeEyes.GetOrNew(entity);
foundEyes.Add(entity);
moverQuery.TryGetComponent(entity, out var mover);
xformQuery.TryGetComponent(entity, out var xform);
lerpInfo.LastRotation = eye.Rotation;
lerpInfo.TargetRotation = GetRotation(xformQuery, mover, xform);
if (xform != null)
{
// If we traverse maps then don't lerp.
if (xform.MapID != lerpInfo.MapId)
{
lerpInfo.LastRotation = lerpInfo.TargetRotation;
}
}
}
foreach (var eye in foundEyes)
{
if (!_activeEyes.ContainsKey(eye))
{
_activeEyes.Remove(eye);
}
}
}
private Angle GetRotation(EntityQuery<TransformComponent> xformQuery, InputMoverComponent? mover = null, TransformComponent? xform = null)
{
// If we can move then tie our eye to our inputs (these also get lerped so it should be fine).
if (mover != null)
{
return -_mover.GetParentGridAngle(mover);
}
// if not tied to a mover then lock it to map / grid
if (xform != null)
{
var relative = xform.GridUid;
relative ??= xform.MapUid;
if (xformQuery.TryGetComponent(relative, out var relativeXform))
{
return relativeXform.WorldRotation;
}
}
return Angle.Zero;
}
private IEnumerable<(IEye Eye, EntityUid Entity)> GetEyes()
{
if (_playerManager.LocalPlayer?.ControlledEntity is { } player && !Deleted(player))
{
yield return (_eyeManager.CurrentEye, player);
}
if (_activeEyes.Count == 0)
yield break;
var eyeQuery = GetEntityQuery<EyeComponent>();
foreach (var (ent, info) in _activeEyes)
{
if (!eyeQuery.TryGetComponent(ent, out var eyeComp) ||
eyeComp.Eye == null)
{
continue;
}
yield return (eyeComp.Eye, ent);
}
}
public override void FrameUpdate(float frameTime)
{
if (!_gameTiming.IsFirstTimePredicted)
return;
var tickFraction = (float) _gameTiming.TickFraction / ushort.MaxValue;
var lerpMinimum = 0.01;
// Always do this one.
LerpPlayerEye(frameTime);
foreach (var (entity, info) in _activeEyes)
foreach (var (eye, entity) in GetEyes())
{
LerpEntityEye(entity, info, frameTime);
}
if (!_activeEyes.TryGetValue(entity, out var lerpInfo))
continue;
if (_toRemove.Count != 0)
{
foreach (var entity in _toRemove)
var shortest = Angle.ShortestDistance(lerpInfo.LastRotation, lerpInfo.TargetRotation);
if (Math.Abs(shortest.Theta) < lerpMinimum)
{
RemoveEye(entity);
eye.Rotation = lerpInfo.TargetRotation;
continue;
}
_toRemove.Clear();
}
}
private void LerpPlayerEye(float frameTime)
{
if (_playerManager.LocalPlayer?.ControlledEntity is not {} mob || Deleted(mob))
return;
// We can't lerp if the mob can't move!
if (!TryComp(mob, out InputMoverComponent? mover))
return;
LerpEye(_eyeManager.CurrentEye, frameTime, mover.LastGridAngle, _playerActiveEye);
}
private void LerpEntityEye(EntityUid uid, EyeLerpInformation info, float frameTime)
{
if (!TryComp(uid, out TransformComponent? transform)
|| !TryComp(uid, out EyeComponent? eye)
|| eye.Eye == null
|| !_mapManager.TryGetGrid(transform.GridUid, out var grid))
{
_toRemove.Add(uid);
return;
}
LerpEye(eye.Eye, frameTime, grid.WorldRotation, info);
}
private void LerpEye(IEye eye, float frameTime, Angle lastAngle, EyeLerpInformation lerpInfo)
{
// Let's not turn the camera into a washing machine when the game starts.
if (lerpInfo.LastGridAngle == null)
{
lerpInfo.LastGridAngle = lastAngle;
eye.Rotation = -lastAngle;
return;
}
// Check if the last lerp grid angle we have is not the same as the last mover grid angle...
if (!lerpInfo.LastGridAngle.Value.EqualsApprox(lastAngle))
{
// And now, we start lerping.
lerpInfo.LerpTo = lastAngle;
lerpInfo.LastGridAngle = lastAngle;
lerpInfo.LerpStartRotation = eye.Rotation;
lerpInfo.Accumulator = 0f;
}
if (lerpInfo.LerpTo != null)
{
lerpInfo.Accumulator += frameTime;
var lerpRot = -lerpInfo.LerpTo.Value.FlipPositive().Reduced();
var startRot = lerpInfo.LerpStartRotation.FlipPositive().Reduced();
var changeNeeded = Angle.ShortestDistance(startRot, lerpRot);
if (changeNeeded.EqualsApprox(Angle.Zero))
{
// Nothing to do here!
lerpInfo.Cleanup(eye);
return;
}
// Get how much the camera should have moved by now. Make it faster depending on the change needed.
var changeRot = (CameraRotateSpeed * Math.Max(1f, Math.Abs(changeNeeded) * 0.75f)) * lerpInfo.Accumulator * Math.Sign(changeNeeded);
// How close is this from reaching the end?
var percentage = (float)Math.Abs(changeRot / changeNeeded);
eye.Rotation = Angle.Lerp(startRot, lerpRot, percentage);
// Either we have overshot, or we have taken way too long on this, emergency reset time
if (percentage >= 1.0f || lerpInfo.Accumulator >= LerpTimeMax)
{
lerpInfo.Cleanup(eye);
}
}
else
{
// This makes it so rotating the camera manually is impossible...
// However, it is needed. Why? Because of a funny (hilarious, even) race condition involving
// ghosting, this system listening for attached mob changes, and the eye rotation being reset after our
// changes back to zero because of an EyeComponent state coming from the server being applied.
// At some point we'll need to come up with a solution for that. But for now, I just want to fix this.
eye.Rotation = -lastAngle;
eye.Rotation = shortest * tickFraction + lerpInfo.LastRotation;
}
}
private sealed class EyeLerpInformation
{
public Angle? LastGridAngle { get; set; }
public Angle? LerpTo { get; set; }
public Angle LerpStartRotation { get; set; }
public float Accumulator { get; set; }
public Angle LastRotation;
public Angle TargetRotation;
public void Cleanup(IEye eye)
{
eye.Rotation = -LerpTo ?? Angle.Zero;
LerpStartRotation = eye.Rotation;
LerpTo = null;
Accumulator = 0;
}
/// <summary>
/// If we go to a new map then don't lerp and snap instantly.
/// </summary>
public MapId MapId;
}
}

View file

@ -11,9 +11,12 @@ public sealed partial class InfoSection : BoxContainer
public InfoSection(string title, string text, bool markup = false)
{
RobustXamlLoader.Load(this);
SetText(title, text, markup);
}
public void SetText(string title, string text, bool markup = false)
{
TitleLabel.Text = title;
if (markup)
Content.SetMessage(FormattedMessage.FromMarkup(text.Trim()));
else

View file

@ -0,0 +1,25 @@
using Content.Shared.Info;
using Robust.Shared.Log;
namespace Content.Client.Info;
public sealed class InfoSystem : EntitySystem
{
public RulesMessage Rules = new RulesMessage("Server Rules", "The server did not send any rules.");
[Dependency] private readonly RulesManager _rules = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<RulesMessage>(OnRulesReceived);
Logger.DebugS("info", "Requested server info.");
RaiseNetworkEvent(new RequestRulesMessage());
}
protected void OnRulesReceived(RulesMessage message, EntitySessionEventArgs eventArgs)
{
Logger.DebugS("info", "Received server rules.");
Rules = message;
_rules.UpdateRules();
}
}

View file

@ -13,6 +13,8 @@ namespace Content.Client.Info
{
[Dependency] private readonly IResourceCache _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly RulesManager _rules = default!;
private OptionsMenu optionsMenu;
@ -35,7 +37,7 @@ namespace Content.Client.Info
TabContainer.SetTabTitle(rulesList, Loc.GetString("ui-info-tab-rules"));
TabContainer.SetTabTitle(tutorialList, Loc.GetString("ui-info-tab-tutorial"));
PopulateRules(rulesList);
AddSection(rulesList, _rules.RulesSection());
PopulateTutorial(tutorialList);
Contents.AddChild(rootContainer);
@ -43,11 +45,6 @@ namespace Content.Client.Info
SetSize = (650, 650);
}
private void PopulateRules(Info rulesList)
{
AddSection(rulesList, MakeRules(_cfgManager, _resourceManager));
}
private void PopulateTutorial(Info tutorialList)
{
AddSection(tutorialList, Loc.GetString("ui-info-header-intro"), "Intro.txt");
@ -74,9 +71,5 @@ namespace Content.Client.Info
return new InfoSection(title, res.ContentFileReadAllText($"/Server Info/{path}"), markup);
}
public static Control MakeRules(IConfigurationManager cfg, IResourceManager res)
{
return MakeSection(Loc.GetString(cfg.GetCVar(CCVars.RulesHeader)), cfg.GetCVar(CCVars.RulesFile), true, res);
}
}
}

View file

@ -13,12 +13,13 @@ public sealed partial class RulesControl : BoxContainer
{
[Dependency] private readonly IResourceCache _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly RulesManager _rules = default!;
public RulesControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
AddChild(RulesAndInfoWindow.MakeRules(_cfgManager, _resourceManager));
AddChild(_rules.RulesSection());
}
}

View file

@ -17,7 +17,9 @@ public sealed class RulesManager : SharedRulesManager
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
private InfoSection rulesSection = new InfoSection("", "", false);
private bool _shouldShowRules;
public void Initialize()
@ -72,4 +74,17 @@ public sealed class RulesManager : SharedRulesManager
var message = _netManager.CreateNetMessage<RulesAcceptedMessage>();
_netManager.ClientSendMessage(message);
}
public void UpdateRules()
{
var rules = _sysMan.GetEntitySystem<InfoSystem>().Rules;
rulesSection.SetText(rules.Title, rules.Text, true);
}
public Control RulesSection()
{
rulesSection = new InfoSection("", "", false);
UpdateRules();
return rulesSection;
}
}

View file

@ -1,14 +0,0 @@
using Content.Shared.Kitchen.Components;
using Robust.Shared.Audio;
namespace Content.Client.Kitchen.Components
{
[RegisterComponent]
public sealed class MicrowaveComponent : SharedMicrowaveComponent
{
public IPlayingAudioStream? PlayingStream { get; set; }
[DataField("loopingSound")]
public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg");
}
}

View file

@ -1,24 +0,0 @@
using System;
using Content.Client.Kitchen.Components;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Content.Client.Kitchen.EntitySystems
{
public sealed class MicrowaveSystem : EntitySystem
{
public void StartSoundLoop(MicrowaveComponent microwave)
{
StopSoundLoop(microwave);
microwave.PlayingStream = SoundSystem.Play(microwave.LoopingSound.GetSound(), Filter.Local(),
microwave.Owner, AudioParams.Default.WithMaxDistance(5).WithLoop(true));
}
public void StopSoundLoop(MicrowaveComponent microwave)
{
microwave.PlayingStream?.Stop();
}
}
}

View file

@ -10,7 +10,6 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using static Content.Shared.Kitchen.Components.SharedMicrowaveComponent;
namespace Content.Client.Kitchen.UI
{

View file

@ -1,70 +0,0 @@
using Content.Client.Kitchen.Components;
using Content.Client.Kitchen.EntitySystems;
using Content.Shared.Kitchen.Components;
using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Content.Client.Kitchen.Visualizers
{
[UsedImplicitly]
public sealed class MicrowaveVisualizer : AppearanceVisualizer
{
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var entMan = IoCManager.Resolve<IEntityManager>();
var sprite = entMan.GetComponent<ISpriteComponent>(component.Owner);
var microwaveComponent = entMan.GetComponentOrNull<MicrowaveComponent>(component.Owner);
if (!component.TryGetData(PowerDeviceVisuals.VisualState, out MicrowaveVisualState state))
{
state = MicrowaveVisualState.Idle;
}
// The only reason we get the entity system so late is so that tests don't fail... Amazing, huh?
switch (state)
{
case MicrowaveVisualState.Broken:
sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mwb");
if(microwaveComponent != null)
EntitySystem.Get<MicrowaveSystem>().StopSoundLoop(microwaveComponent);
break;
case MicrowaveVisualState.Idle:
sprite.LayerSetState(MicrowaveVisualizerLayers.Base, "mw");
sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mw_unlit");
if(microwaveComponent != null)
EntitySystem.Get<MicrowaveSystem>().StopSoundLoop(microwaveComponent);
break;
case MicrowaveVisualState.Cooking:
sprite.LayerSetState(MicrowaveVisualizerLayers.Base, "mw");
sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mw_running_unlit");
if(microwaveComponent != null)
EntitySystem.Get<MicrowaveSystem>().StartSoundLoop(microwaveComponent);
break;
default:
Logger.Debug($"Something terrible happened in {this}");
break;
}
var glowingPartsVisible = !(component.TryGetData(PowerDeviceVisuals.Powered, out bool powered) && !powered);
sprite.LayerSetVisible(MicrowaveVisualizerLayers.BaseUnlit, glowingPartsVisible);
}
private enum MicrowaveVisualizerLayers : byte
{
Base,
BaseUnlit
}
}
}

View file

@ -0,0 +1,8 @@
namespace Content.Client.Kitchen.Visualizers;
public enum MicrowaveVisualizerLayers : byte
{
Base,
BaseUnlit
}

View file

@ -73,29 +73,45 @@ public sealed class DamageOverlay : Overlay
{
DeadLevel = 1f;
}
else if (!DeadLevel.Equals(0f))
else if (!MathHelper.CloseTo(0f, DeadLevel, 0.001f))
{
var diff = -DeadLevel;
DeadLevel += GetDiff(diff, lastFrameTime);
}
else
{
DeadLevel = 0f;
}
if (!MathHelper.CloseTo(_oldBruteLevel, BruteLevel, 0.001f))
{
var diff = BruteLevel - _oldBruteLevel;
_oldBruteLevel += GetDiff(diff, lastFrameTime);
}
else
{
_oldBruteLevel = BruteLevel;
}
if (!MathHelper.CloseTo(_oldOxygenLevel, OxygenLevel, 0.001f))
{
var diff = OxygenLevel - _oldOxygenLevel;
_oldOxygenLevel += GetDiff(diff, lastFrameTime);
}
else
{
_oldOxygenLevel = OxygenLevel;
}
if (!MathHelper.CloseTo(_oldCritLevel, CritLevel, 0.001f))
{
var diff = CritLevel - _oldCritLevel;
_oldCritLevel += GetDiff(diff, lastFrameTime);
}
else
{
_oldCritLevel = CritLevel;
}
/*
* darknessAlphaOuter is the maximum alpha for anything outside of the larger circle

View file

@ -3,7 +3,6 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Pulling.Components;
using Robust.Client.Player;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Client.Physics.Controllers
@ -23,7 +22,17 @@ namespace Content.Client.Physics.Controllers
if (TryComp<RelayInputMoverComponent>(player, out var relayMover))
{
if (relayMover.RelayEntity != null)
{
if (TryComp<InputMoverComponent>(player, out var mover) &&
TryComp<InputMoverComponent>(relayMover.RelayEntity, out var relayed))
{
relayed.RelativeEntity = mover.RelativeEntity;
relayed.RelativeRotation = mover.RelativeRotation;
relayed.TargetRelativeRotation = mover.RelativeRotation;
}
HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime);
}
}
HandleClientsideMovement(player, frameTime);
@ -31,8 +40,10 @@ namespace Content.Client.Physics.Controllers
private void HandleClientsideMovement(EntityUid player, float frameTime)
{
var xformQuery = GetEntityQuery<TransformComponent>();
if (!TryComp(player, out InputMoverComponent? mover) ||
!TryComp(player, out TransformComponent? xform))
!xformQuery.TryGetComponent(player, out var xform))
{
return;
}
@ -47,20 +58,12 @@ namespace Content.Client.Physics.Controllers
{
return;
}
if (TryComp<InputMoverComponent>(xform.ParentUid, out var parentMover))
{
mover.LastGridAngle = parentMover.LastGridAngle;
}
}
else if (!TryComp(player, out body))
{
return;
}
if (xform.GridUid != null)
mover.LastGridAngle = GetParentGridAngle(xform, mover);
// Essentially we only want to set our mob to predicted so every other entity we just interpolate
// (i.e. only see what the server has sent us).
// The exception to this is joints.
@ -98,7 +101,7 @@ namespace Content.Client.Physics.Controllers
}
// Server-side should just be handled on its own so we'll just do this shizznit
HandleMobMovement(mover, body, xformMover, frameTime);
HandleMobMovement(mover, body, xformMover, frameTime, xformQuery);
}
protected override bool CanSound()

View file

@ -1,37 +0,0 @@
using Content.Shared.Radiation;
namespace Content.Client.Radiation
{
[RegisterComponent]
[ComponentReference(typeof(SharedRadiationPulseComponent))]
public sealed class RadiationPulseComponent : SharedRadiationPulseComponent
{
private bool _draw;
private bool _decay;
private float _range;
private TimeSpan _startTime;
private TimeSpan _endTime;
public override float Range => _range;
public override TimeSpan StartTime => _startTime;
public override TimeSpan EndTime => _endTime;
public override bool Draw => _draw;
public override bool Decay => _decay;
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not RadiationPulseState state)
{
return;
}
_range = state.Range;
_draw = state.Draw;
_decay = state.Decay;
_startTime = state.StartTime;
_endTime = state.EndTime;
}
}
}

View file

@ -1,4 +1,5 @@
using System.Linq;
using Content.Shared.Radiation.Components;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
@ -18,8 +19,6 @@ namespace Content.Client.Radiation
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
private TimeSpan _lastTick = default;
private readonly ShaderInstance _baseShader;
private readonly Dictionary<EntityUid, (ShaderInstance shd, RadiationShaderInstance instance)> _pulses = new();
@ -52,7 +51,7 @@ namespace Content.Client.Radiation
shd?.SetParameter("renderScale", viewport.RenderScale);
shd?.SetParameter("positionInput", tempCoords);
shd?.SetParameter("range", instance.Range);
var life = (_lastTick - instance.Start) / (instance.End - instance.Start);
var life = (_gameTiming.RealTime - instance.Start).TotalSeconds / instance.Duration;
shd?.SetParameter("life", (float) life);
// There's probably a very good reason not to do this.
@ -75,8 +74,6 @@ namespace Content.Client.Radiation
return;
}
_lastTick = _gameTiming.CurTime;
var currentEyeLoc = currentEye.Position;
var pulses = _entityManager.EntityQuery<RadiationPulseComponent>();
@ -92,9 +89,9 @@ namespace Content.Client.Radiation
_baseShader.Duplicate(),
new RadiationShaderInstance(
_entityManager.GetComponent<TransformComponent>(pulseEntity).MapPosition.Position,
pulse.Range,
pulse.VisualRange,
pulse.StartTime,
pulse.EndTime
pulse.VisualDuration
)
)
);
@ -110,7 +107,7 @@ namespace Content.Client.Radiation
{
var shaderInstance = _pulses[pulseEntity];
shaderInstance.instance.CurrentMapCoords = _entityManager.GetComponent<TransformComponent>(pulseEntity).MapPosition.Position;
shaderInstance.instance.Range = pulse.Range;
shaderInstance.instance.Range = pulse.VisualRange;
} else {
_pulses[pulseEntity].shd.Dispose();
_pulses.Remove(pulseEntity);
@ -124,12 +121,12 @@ namespace Content.Client.Radiation
return _entityManager.GetComponent<TransformComponent>(pulseEntity).MapID == currentEyeLoc.MapId && _entityManager.GetComponent<TransformComponent>(pulseEntity).Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, _entityManager.GetComponent<TransformComponent>(pulseEntity).ParentUid, currentEyeLoc), MaxDist);
}
private sealed record RadiationShaderInstance(Vector2 CurrentMapCoords, float Range, TimeSpan Start, TimeSpan End)
private sealed record RadiationShaderInstance(Vector2 CurrentMapCoords, float Range, TimeSpan Start, float Duration)
{
public Vector2 CurrentMapCoords = CurrentMapCoords;
public float Range = Range;
public TimeSpan Start = Start;
public TimeSpan End = End;
public float Duration = Duration;
};
}
}

View file

@ -1,62 +1,87 @@
using System;
using System.Collections.Generic;
using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
namespace Content.Client.VendingMachines.UI
{
[GenerateTypedNameReferences]
public sealed partial class VendingMachineMenu : DefaultWindow
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private VendingMachineBoundUserInterface Owner { get; }
public event Action<ItemList.ItemListSelectedEventArgs>? OnItemSelected;
private List<VendingMachineInventoryEntry> _cachedInventory = new();
public VendingMachineMenu(VendingMachineBoundUserInterface owner)
public VendingMachineMenu()
{
IoCManager.InjectDependencies(this);
MinSize = SetSize = (250, 150);
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
Owner = owner;
VendingContents.OnItemSelected += ItemSelected;
VendingContents.OnItemSelected += args =>
{
OnItemSelected?.Invoke(args);
};
}
/// <summary>
/// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes
/// </summary>
public void Populate(List<VendingMachineInventoryEntry> inventory)
{
VendingContents.Clear();
_cachedInventory = inventory;
var longestEntry = "";
foreach (VendingMachineInventoryEntry entry in inventory)
if (inventory.Count == 0)
{
var itemName = _prototypeManager.Index<EntityPrototype>(entry.ID).Name;
VendingContents.Clear();
var outOfStockText = Loc.GetString("vending-machine-component-try-eject-out-of-stock");
VendingContents.AddItem(outOfStockText);
SetSizeAfterUpdate(outOfStockText.Length);
return;
}
while (inventory.Count != VendingContents.Count)
{
if (inventory.Count > VendingContents.Count)
VendingContents.AddItem(string.Empty);
else
VendingContents.RemoveAt(VendingContents.Count - 1);
}
var longestEntry = string.Empty;
var spriteSystem = EntitySystem.Get<SpriteSystem>();
for (var i = 0; i < inventory.Count; i++)
{
var entry = inventory[i];
var vendingItem = VendingContents[i];
vendingItem.Text = string.Empty;
vendingItem.Icon = null;
var itemName = entry.ID;
Texture? icon = null;
if (_prototypeManager.TryIndex<EntityPrototype>(entry.ID, out var prototype))
{
itemName = prototype.Name;
icon = spriteSystem.GetPrototypeIcon(prototype).Default;
}
if (itemName.Length > longestEntry.Length)
longestEntry = itemName;
Texture? icon = null;
if(_prototypeManager.TryIndex(entry.ID, out EntityPrototype? prototype))
icon = SpriteComponent.GetPrototypeIcon(prototype, _resourceCache).Default;
VendingContents.AddItem($"{itemName} [{entry.Amount}]", icon);
vendingItem.Text = $"{itemName} [{entry.Amount}]";
vendingItem.Icon = icon;
}
SetSize = (Math.Clamp((longestEntry.Length + 2) * 12, 250, 300),
Math.Clamp(VendingContents.Count * 50, 150, 350));
SetSizeAfterUpdate(longestEntry.Length);
}
public void ItemSelected(ItemList.ItemListSelectedEventArgs args)
private void SetSizeAfterUpdate(int longestEntryLength)
{
Owner.Eject(_cachedInventory[args.ItemIndex].Type, _cachedInventory[args.ItemIndex].ID);
SetSize = (Math.Clamp((longestEntryLength + 2) * 12, 250, 300),
Math.Clamp(VendingContents.Count * 50, 150, 350));
}
}
}

View file

@ -1,241 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Shared.VendingMachines;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
namespace Content.Client.VendingMachines.UI
{
[UsedImplicitly]
public sealed class VendingMachineVisualizer : AppearanceVisualizer, ISerializationHooks
{
// TODO: Should default to off or broken if damaged
//
// TODO: The length of these animations is supposed to be dictated
// by the vending machine's pack prototype's `AnimationDuration`
// but we have no good way of passing that data from the server
// to the client at the moment. Rework Visualizers?
private Dictionary<string, bool> _baseStates = new();
private static readonly Dictionary<string, VendingMachineVisualLayers> LayerMap =
new()
{
{"off", VendingMachineVisualLayers.Unlit},
{"screen", VendingMachineVisualLayers.Screen},
{"normal", VendingMachineVisualLayers.Base},
{"normal-unshaded", VendingMachineVisualLayers.BaseUnshaded},
{"eject", VendingMachineVisualLayers.Base},
{"eject-unshaded", VendingMachineVisualLayers.BaseUnshaded},
{"deny", VendingMachineVisualLayers.Base},
{"deny-unshaded", VendingMachineVisualLayers.BaseUnshaded},
{"broken", VendingMachineVisualLayers.Unlit},
};
[DataField("screen")]
private bool _screen;
[DataField("normal")]
private bool _normal;
[DataField("normalUnshaded")]
private bool _normalUnshaded;
[DataField("eject")]
private bool _eject;
[DataField("ejectUnshaded")]
private bool _ejectUnshaded;
[DataField("deny")]
private bool _deny;
[DataField("denyUnshaded")]
private bool _denyUnshaded;
[DataField("broken")]
private bool _broken;
[DataField("brokenUnshaded")]
private bool _brokenUnshaded;
private readonly Dictionary<string, Animation> _animations = new();
void ISerializationHooks.AfterDeserialization()
{
// Used a dictionary so the yaml can adhere to the style-guide and the texture states can be clear
var states = new Dictionary<string, bool>
{
{"off", true},
{"screen", _screen},
{"normal", _normal},
{"normal-unshaded", _normalUnshaded},
{"eject", _eject},
{"eject-unshaded", _ejectUnshaded},
{"deny", _deny},
{"deny-unshaded", _denyUnshaded},
{"broken", _broken},
{"broken-unshaded", _brokenUnshaded},
};
_baseStates = states;
if (_baseStates["deny"])
{
InitializeAnimation("deny");
}
if (_baseStates["deny-unshaded"])
{
InitializeAnimation("deny-unshaded", true);
}
if (_baseStates["eject"])
{
InitializeAnimation("eject");
}
if (_baseStates["eject-unshaded"])
{
InitializeAnimation("eject-unshaded", true);
}
}
private void InitializeAnimation(string key, bool unshaded = false)
{
_animations.Add(key, new Animation {Length = TimeSpan.FromSeconds(1.2f)});
var flick = new AnimationTrackSpriteFlick();
_animations[key].AnimationTracks.Add(flick);
flick.LayerKey = unshaded ? VendingMachineVisualLayers.BaseUnshaded : VendingMachineVisualLayers.Base;
flick.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame(key, 0f));
}
[Obsolete("Subscribe to your component being initialised instead.")]
public override void InitializeEntity(EntityUid entity)
{
base.InitializeEntity(entity);
IoCManager.Resolve<IEntityManager>().EnsureComponent<AnimationPlayerComponent>(entity);
}
private void HideLayers(ISpriteComponent spriteComponent)
{
foreach (var layer in spriteComponent.AllLayers)
{
layer.Visible = false;
}
spriteComponent.LayerSetVisible(VendingMachineVisualLayers.Unlit, true);
}
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var entMan = IoCManager.Resolve<IEntityManager>();
var sprite = entMan.GetComponent<SpriteComponent>(component.Owner);
// TODO when moving to a system visualizer, re work how this is done
// Currently this only gets called during init, so unless it NEEEDS to be configurable, just make this party of the entity prototype.
if (component.TryGetData(VendingMachineVisuals.Inventory, out string? invId) &&
IoCManager.Resolve<IPrototypeManager>().TryIndex(invId, out VendingMachineInventoryPrototype? prototype) &&
IoCManager.Resolve<IResourceCache>().TryGetResource<RSIResource>(
SharedSpriteComponent.TextureRoot / $"Structures/Machines/VendingMachines/{prototype.SpriteName}.rsi", out var res))
{
sprite.BaseRSI = res.RSI;
}
var animPlayer = entMan.GetComponent<AnimationPlayerComponent>(component.Owner);
if (!component.TryGetData(VendingMachineVisuals.VisualState, out VendingMachineVisualState state))
{
state = VendingMachineVisualState.Normal;
}
// Hide last state
HideLayers(sprite);
ActivateState(sprite, "off");
switch (state)
{
case VendingMachineVisualState.Normal:
ActivateState(sprite, "screen");
ActivateState(sprite, "normal-unshaded");
ActivateState(sprite, "normal");
break;
case VendingMachineVisualState.Off:
break;
case VendingMachineVisualState.Broken:
ActivateState(sprite, "broken-unshaded");
ActivateState(sprite, "broken");
break;
case VendingMachineVisualState.Deny:
ActivateState(sprite, "screen");
ActivateAnimation(sprite, animPlayer, "deny-unshaded");
ActivateAnimation(sprite, animPlayer, "deny");
break;
case VendingMachineVisualState.Eject:
ActivateState(sprite, "screen");
ActivateAnimation(sprite, animPlayer, "eject-unshaded");
ActivateAnimation(sprite, animPlayer, "eject");
break;
default:
throw new ArgumentOutOfRangeException();
}
}
// Helper methods just to avoid all of that hard-to-read-indented code
private void ActivateState(ISpriteComponent spriteComponent, string stateId)
{
// No state for it on the rsi :(
if (!_baseStates[stateId])
{
return;
}
var stateLayer = LayerMap[stateId];
spriteComponent.LayerSetVisible(stateLayer, true);
spriteComponent.LayerSetState(stateLayer, stateId);
}
private void ActivateAnimation(ISpriteComponent spriteComponent, AnimationPlayerComponent animationPlayer, string key)
{
if (!_animations.TryGetValue(key, out var animation))
{
return;
}
if (!animationPlayer.HasRunningAnimation(key))
{
spriteComponent.LayerSetVisible(LayerMap[key], true);
animationPlayer.Play(animation, key);
}
}
public enum VendingMachineVisualLayers : byte
{
// Off / Broken. The other layers will overlay this if the machine is on.
Unlit,
// Normal / Deny / Eject
Base,
BaseUnshaded,
// Screens that are persistent (where the machine is not off or broken)
Screen,
}
}
}

View file

@ -1,22 +1,20 @@
using Content.Client.VendingMachines.UI;
using Content.Shared.VendingMachines;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
using Robust.Client.UserInterface.Controls;
using System.Linq;
namespace Content.Client.VendingMachines
{
public sealed class VendingMachineBoundUserInterface : BoundUserInterface
{
[ViewVariables] private VendingMachineMenu? _menu;
[ViewVariables]
private VendingMachineMenu? _menu;
public SharedVendingMachineComponent? VendingMachine { get; private set; }
private List<VendingMachineInventoryEntry> _cachedInventory = new();
public VendingMachineBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
SendMessage(new InventorySyncRequestMessage());
}
protected override void Open()
@ -24,33 +22,43 @@ namespace Content.Client.VendingMachines
base.Open();
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(Owner.Owner, out SharedVendingMachineComponent? vendingMachine))
{
return;
}
var vendingMachineSys = EntitySystem.Get<VendingMachineSystem>();
VendingMachine = vendingMachine;
_cachedInventory = vendingMachineSys.GetAllInventory(Owner.Owner);
_menu = new VendingMachineMenu(this) {Title = entMan.GetComponent<MetaDataComponent>(Owner.Owner).EntityName};
_menu.Populate(VendingMachine.AllInventory);
_menu = new VendingMachineMenu {Title = entMan.GetComponent<MetaDataComponent>(Owner.Owner).EntityName};
_menu.OnClose += Close;
_menu.OnItemSelected += OnItemSelected;
_menu.Populate(_cachedInventory);
_menu.OpenCentered();
}
public void Eject(InventoryType type, string id)
protected override void UpdateState(BoundUserInterfaceState state)
{
SendMessage(new VendingMachineEjectMessage(type, id));
base.UpdateState(state);
if (state is not VendingMachineInterfaceState newState)
return;
_cachedInventory = newState.Inventory;
_menu?.Populate(_cachedInventory);
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
{
switch (message)
{
case VendingMachineInventoryMessage msg:
_menu?.Populate(msg.Inventory);
break;
}
if (_cachedInventory == null || _cachedInventory.Count == 0)
return;
var selectedItem = _cachedInventory.ElementAtOrDefault(args.ItemIndex);
if (selectedItem == null)
return;
SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
protected override void Dispose(bool disposing)
@ -59,7 +67,12 @@ namespace Content.Client.VendingMachines
if (!disposing)
return;
_menu?.Dispose();
if (_menu == null)
return;
_menu.OnItemSelected -= OnItemSelected;
_menu.OnClose -= Close;
_menu.Dispose();
}
}
}

View file

@ -4,7 +4,57 @@ namespace Content.Client.VendingMachines;
[RegisterComponent]
[ComponentReference(typeof(SharedVendingMachineComponent))]
[Access(typeof(VendingMachineSystem))]
public sealed class VendingMachineComponent : SharedVendingMachineComponent
{
/// <summary>
/// RSI state for when the vending machine is unpowered.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
/// </summary>
[DataField("offState")]
public string? OffState;
/// <summary>
/// RSI state for the screen of the vending machine
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Screen"/>
/// </summary>
[DataField("screenState")]
public string? ScreenState;
/// <summary>
/// RSI state for the vending machine's normal state. Usually a looping animation.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
/// </summary>
[DataField("normalState")]
public string? NormalState;
/// <summary>
/// RSI state for the vending machine's eject animation.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
/// </summary>
[DataField("ejectState")]
public string? EjectState;
/// <summary>
/// RSI state for the vending machine's deny animation. Will either be played once as sprite flick
/// or looped depending on how <see cref="LoopDenyAnimation"/> is set.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.BaseUnshaded"/>
/// </summary>
[DataField("denyState")]
public string? DenyState;
/// <summary>
/// RSI state for when the vending machine is unpowered.
/// Will be displayed on the layer <see cref="VendingMachineVisualLayers.Base"/>
/// </summary>
[DataField("brokenState")]
public string? BrokenState;
/// <summary>
/// If set to <c>true</c> (default) will loop the animation of the <see cref="DenyState"/> for the duration
/// of <see cref="SharedVendingMachineComponent.DenyDelay"/>. If set to <c>false</c> will play a sprite
/// flick animation for the state and then linger on the final frame until the end of the delay.
/// </summary>
[DataField("loopDeny")]
public bool LoopDenyAnimation = true;
}

View file

@ -1,7 +1,148 @@
using Content.Shared.VendingMachines;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
namespace Content.Client.VendingMachines;
public sealed class VendingMachineSystem : SharedVendingMachineSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
}
private void OnAnimationCompleted(EntityUid uid, VendingMachineComponent component, AnimationCompletedEvent args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
UpdateAppearance(uid, VendingMachineVisualState.Normal, component, sprite);
}
private void OnAppearanceChange(EntityUid uid, VendingMachineComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!args.AppearanceData.TryGetValue(VendingMachineVisuals.VisualState, out var visualStateObject) ||
visualStateObject is not VendingMachineVisualState visualState)
{
visualState = VendingMachineVisualState.Normal;
}
UpdateAppearance(uid, visualState, component, args.Sprite);
}
private void UpdateAppearance(EntityUid uid, VendingMachineVisualState visualState, VendingMachineComponent component, SpriteComponent sprite)
{
SetLayerState(VendingMachineVisualLayers.Base, component.OffState, sprite);
switch (visualState)
{
case VendingMachineVisualState.Normal:
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.NormalState, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
case VendingMachineVisualState.Deny:
if (component.LoopDenyAnimation)
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
else
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
case VendingMachineVisualState.Eject:
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
case VendingMachineVisualState.Broken:
HideLayers(sprite);
SetLayerState(VendingMachineVisualLayers.Base, component.BrokenState, sprite);
break;
case VendingMachineVisualState.Off:
HideLayers(sprite);
break;
}
}
private static void SetLayerState(VendingMachineVisualLayers layer, string? state, SpriteComponent sprite)
{
if (string.IsNullOrEmpty(state))
return;
sprite.LayerSetVisible(layer, true);
sprite.LayerSetAutoAnimated(layer, true);
sprite.LayerSetState(layer, state);
}
private void PlayAnimation(EntityUid uid, VendingMachineVisualLayers layer, string? state, float animationTime, SpriteComponent sprite)
{
if (string.IsNullOrEmpty(state))
return;
if (!_animationPlayer.HasRunningAnimation(uid, state))
{
var animation = GetAnimation(layer, state, animationTime);
sprite.LayerSetVisible(layer, true);
_animationPlayer.Play(uid, animation, state);
}
}
private static Animation GetAnimation(VendingMachineVisualLayers layer, string state, float animationTime)
{
return new Animation
{
Length = TimeSpan.FromSeconds(animationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = layer,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(state, 0f)
}
}
}
};
}
private static void HideLayers(SpriteComponent sprite)
{
HideLayer(VendingMachineVisualLayers.BaseUnshaded, sprite);
HideLayer(VendingMachineVisualLayers.Screen, sprite);
}
private static void HideLayer(VendingMachineVisualLayers layer, SpriteComponent sprite)
{
if (!sprite.LayerMapTryGet(layer, out var actualLayer))
return;
sprite.LayerSetVisible(actualLayer, false);
}
}
public enum VendingMachineVisualLayers : byte
{
/// <summary>
/// Off / Broken. The other layers will overlay this if the machine is on.
/// </summary>
Base,
/// <summary>
/// Normal / Deny / Eject
/// </summary>
BaseUnshaded,
/// <summary>
/// Screens that are persistent (where the machine is not off or broken)
/// </summary>
Screen
}

View file

@ -219,68 +219,84 @@ public static class PoolManager
/// </summary>
/// <param name="poolSettings">See <see cref="PoolSettings"/></param>
/// <returns></returns>
public static async Task<PairTracker> GetServerClient(PoolSettings poolSettings = null,
[System.Runtime.CompilerServices.CallerFilePath] string testMethodFilePath = "",
[System.Runtime.CompilerServices.CallerMemberName] string testMethodName = "") =>
await GetServerClientPair(poolSettings ?? new PoolSettings(), $"{testMethodFilePath}, {testMethodName}");
public static async Task<PairTracker> GetServerClient(PoolSettings poolSettings = null) =>
await GetServerClientPair(poolSettings ?? new PoolSettings());
private static async Task<PairTracker> GetServerClientPair(PoolSettings poolSettings, string testMethodName)
private static string GetDefaultTestName()
{
return TestContext.CurrentContext.Test.FullName
.Replace("Content.IntegrationTests.Tests.", "");
}
private static async Task<PairTracker> GetServerClientPair(PoolSettings poolSettings)
{
DieIfPoolFailure();
var currentTestName = poolSettings.TestName ?? GetDefaultTestName();
var poolRetrieveTimeWatch = new Stopwatch();
await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
Pair pair = null;
try
{
var poolRetrieveTimeWatch = new Stopwatch();
poolRetrieveTimeWatch.Start();
await TestContext.Out.WriteLineAsync("Getting server/client");
if (poolSettings.MustBeNew)
{
await TestContext.Out.WriteLineAsync($"Creating, because must be new pair");
await TestContext.Out.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
pair = await CreateServerClientPair(poolSettings);
}
else
{
await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair");
pair = GrabOptimalPair(poolSettings);
if (pair != null)
{
await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
var canSkip = pair.Settings.CanFastRecycle(poolSettings);
if (!canSkip)
if (canSkip)
{
await TestContext.Out.WriteLineAsync($"Cleaning existing pair");
await CleanPooledPair(poolSettings, pair);
await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
}
else
{
await TestContext.Out.WriteLineAsync($"Skip cleanup pair");
await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
await CleanPooledPair(poolSettings, pair);
}
}
else
{
await TestContext.Out.WriteLineAsync($"Creating, because pool empty");
await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool");
pair = await CreateServerClientPair(poolSettings);
}
}
var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
await TestContext.Out.WriteLineAsync(
$"Got server/client (id:{pair.PairId},uses:{pair.TestHistory.Count}) in {poolRetrieveTime.TotalMilliseconds} ms");
pair.Settings = poolSettings;
pair.TestHistory.Add(testMethodName);
var usageWatch = new Stopwatch();
usageWatch.Start();
return new PairTracker()
{
Pair = pair,
UsageWatch = usageWatch
};
}
finally
{
if (pair != null)
if (pair != null && pair.TestHistory.Count > 1)
{
TestContext.Out.WriteLine($"Test History|\n{string.Join('\n', pair.TestHistory)}\n|Test History End");
await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.PairId} Test History Start");
for (int i = 0; i < pair.TestHistory.Count - 1; i++)
{
await TestContext.Out.WriteLineAsync($"- Pair {pair.PairId} Test #{i}: {pair.TestHistory[i]}");
}
await TestContext.Out.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.PairId} Test History End");
}
}
var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
await TestContext.Out.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.PairId} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
await TestContext.Out.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Returning pair {pair.PairId}");
pair.Settings = poolSettings;
pair.TestHistory.Add(currentTestName);
var usageWatch = new Stopwatch();
usageWatch.Start();
return new PairTracker
{
Pair = pair,
UsageWatch = usageWatch
};
}
private static Pair GrabOptimalPair(PoolSettings poolSettings)
@ -409,9 +425,8 @@ public static class PoolManager
await TestContext.Out.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Done recycling");
}
private static async Task<Pair> CreateServerClientPair(PoolSettings poolSettings)
private static void DieIfPoolFailure()
{
Pair pair;
if (PoolFailureReason != null)
{
Assert.Inconclusive(@"
@ -419,7 +434,10 @@ In a different test, the pool manager had an exception when trying to create a s
Instead of risking that the pool manager will fail at creating a server/client pairs for every single test,
we are just going to end this here to save a lot of time. This is the exception that started this:\n {0}", PoolFailureReason);
}
}
private static async Task<Pair> CreateServerClientPair(PoolSettings poolSettings)
{
Pair pair;
try
{
var client = await GenerateClient(poolSettings);
@ -654,6 +672,11 @@ public sealed class PoolSettings
/// </summary>
public bool NoServer { get; init; }
/// <summary>
/// Overrides the test name detection, and uses this in the test history instead
/// </summary>
public string TestName { get; set; }
/// <summary>
/// Tries to guess if we can skip recycling the server/client pair.
/// </summary>
@ -709,19 +732,19 @@ public sealed class PairTracker : IAsyncDisposable
private async Task OnDirtyDispose()
{
var usageTime = UsageWatch.Elapsed;
await TestContext.Out.WriteLineAsync($"Dirty: Test returned in {usageTime.TotalMilliseconds} ms");
await TestContext.Out.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Pair.PairId} in {usageTime.TotalMilliseconds} ms");
var dirtyWatch = new Stopwatch();
dirtyWatch.Start();
Pair.Client.Dispose();
Pair.Server.Dispose();
var disposeTime = dirtyWatch.Elapsed;
await TestContext.Out.WriteLineAsync($"Dirty: Disposed in {disposeTime.TotalMilliseconds} ms");
await TestContext.Out.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Pair.PairId} in {disposeTime.TotalMilliseconds} ms");
}
private async Task OnCleanDispose()
{
var usageTime = UsageWatch.Elapsed;
await TestContext.Out.WriteLineAsync($"Clean: Test returned in {usageTime.TotalMilliseconds} ms");
await TestContext.Out.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Pair.PairId} for {usageTime.TotalMilliseconds} ms");
var cleanWatch = new Stopwatch();
cleanWatch.Start();
// Let any last minute failures the test cause happen.
@ -730,12 +753,12 @@ public sealed class PairTracker : IAsyncDisposable
{
if (Pair.Client.IsAlive == false)
{
throw new Exception("Test killed the client", Pair.Client.UnhandledException);
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Pair.PairId}:", Pair.Client.UnhandledException);
}
if (Pair.Server.IsAlive == false)
{
throw new Exception("Test killed the server", Pair.Server.UnhandledException);
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Pair.PairId}:", Pair.Server.UnhandledException);
}
}
@ -743,18 +766,19 @@ public sealed class PairTracker : IAsyncDisposable
{
Pair.Client.Dispose();
Pair.Server.Dispose();
await PoolManager.ReallyBeIdle(Pair);
var returnTime2 = cleanWatch.Elapsed;
await TestContext.Out.WriteLineAsync($"Clean: Clean disposed in {returnTime2.TotalMilliseconds} ms");
await TestContext.Out.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {returnTime2.TotalMilliseconds} ms");
return;
}
var sRuntimeLog = Pair.Server.ResolveDependency<IRuntimeLog>();
if (sRuntimeLog.ExceptionCount > 0) throw new Exception("Server logged exceptions");
if (sRuntimeLog.ExceptionCount > 0) throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
var cRuntimeLog = Pair.Client.ResolveDependency<IRuntimeLog>();
if (cRuntimeLog.ExceptionCount > 0) throw new Exception("Client logged exceptions");
if (cRuntimeLog.ExceptionCount > 0) throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
PoolManager.NoCheckReturn(Pair);
var returnTime = cleanWatch.Elapsed;
await TestContext.Out.WriteLineAsync($"Clean: Clean returned to pool in {returnTime.TotalMilliseconds} ms");
await TestContext.Out.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Pair.PairId} back into the pool");
}
public Stopwatch UsageWatch { get; set; }
public Pair Pair { get; init; }
@ -765,18 +789,17 @@ public sealed class PairTracker : IAsyncDisposable
switch (disposed)
{
case 0:
await TestContext.Out.WriteLineAsync("Clean Return Start");
await TestContext.Out.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Pair.PairId} started");
break;
case 1:
throw new Exception("Already called clean return before");
throw new Exception($"{nameof(CleanReturnAsync)}: Already clean returned");
case 2:
throw new Exception("Already dirty disposed");
throw new Exception($"{nameof(CleanReturnAsync)}: Already dirty disposed");
default:
throw new Exception("Unexpected disposed value");
throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected disposed value");
}
await OnCleanDispose();
await TestContext.Out.WriteLineAsync($"Clean Return Exiting");
}
public async ValueTask DisposeAsync()
@ -785,18 +808,16 @@ public sealed class PairTracker : IAsyncDisposable
switch (disposed)
{
case 0:
await TestContext.Out.WriteLineAsync("Dirty Return Start");
await TestContext.Out.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Pair.PairId} started");
break;
case 1:
await TestContext.Out.WriteLineAsync("Dirty Return - Already Clean Disposed");
await TestContext.Out.WriteLineAsync($"{nameof(DisposeAsync)}: Pair {Pair.PairId} was properly clean disposed");
return;
case 2:
throw new Exception("Already called dirty return before");
throw new Exception($"{nameof(DisposeAsync)}: Already dirty disposed pair {Pair.PairId}");
default:
throw new Exception("Unexpected disposed value");
throw new Exception($"{nameof(DisposeAsync)}: Unexpected disposed value");
}
await OnDirtyDispose();
await TestContext.Out.WriteLineAsync($"Dirty Return Exiting");
}
}

View file

@ -56,6 +56,10 @@ namespace Content.IntegrationTests.Tests.Buckle
var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords;
var entityManager = server.ResolveDependency<IEntityManager>();
var actionBlocker = entityManager.EntitySysManager.GetEntitySystem<ActionBlockerSystem>();
var standingState = entityManager.EntitySysManager.GetEntitySystem<StandingStateSystem>();
EntityUid human = default;
EntityUid chair = default;
@ -64,12 +68,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var actionBlocker = EntitySystem.Get<ActionBlockerSystem>();
var standingState = EntitySystem.Get<StandingStateSystem>();
human = entityManager.SpawnEntity(BuckleDummyId, coordinates);
chair = entityManager.SpawnEntity(StrapDummyId, coordinates);
@ -119,9 +117,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
var actionBlocker = EntitySystem.Get<ActionBlockerSystem>();
var standingState = EntitySystem.Get<StandingStateSystem>();
// Still buckled
Assert.True(buckle.Buckled);
@ -155,10 +150,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var actionBlocker = EntitySystem.Get<ActionBlockerSystem>();
var standingState = EntitySystem.Get<StandingStateSystem>();
// Still buckled
Assert.True(buckle.Buckled);
@ -228,10 +219,11 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var handsSys = entityManager.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
await server.WaitAssertion(() =>
{
var entityManager = IoCManager.Resolve<IEntityManager>();
human = entityManager.SpawnEntity(BuckleDummyId, coordinates);
var chair = entityManager.SpawnEntity(StrapDummyId, coordinates);
@ -251,7 +243,7 @@ namespace Content.IntegrationTests.Tests.Buckle
{
var akms = entityManager.SpawnEntity(ItemDummyId, coordinates);
Assert.True(EntitySystem.Get<SharedHandsSystem>().TryPickupAnyHand(human, akms));
Assert.True(handsSys.TryPickupAnyHand(human, akms));
}
});
@ -265,7 +257,7 @@ namespace Content.IntegrationTests.Tests.Buckle
// With items in all hands
foreach (var hand in hands.Hands.Values)
{
Assert.NotNull(hands.ActiveHandEntity);
Assert.NotNull(hand.HeldEntity);
}
var legs = body.GetPartsOfType(BodyPartType.Leg);
@ -287,7 +279,7 @@ namespace Content.IntegrationTests.Tests.Buckle
// Now with no item in any hand
foreach (var hand in hands.Hands.Values)
{
Assert.Null(hands.ActiveHandEntity);
Assert.Null(hand.HeldEntity);
}
buckle.TryUnbuckle(human, true);
@ -304,6 +296,7 @@ namespace Content.IntegrationTests.Tests.Buckle
var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords;
var entityManager = server.ResolveDependency<IEntityManager>();
EntityUid human = default;
EntityUid chair = default;
@ -311,9 +304,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
human = entityManager.SpawnEntity(BuckleDummyId, coordinates);
chair = entityManager.SpawnEntity(StrapDummyId, coordinates);
@ -336,8 +326,6 @@ namespace Content.IntegrationTests.Tests.Buckle
await server.WaitAssertion(() =>
{
var entityManager = IoCManager.Resolve<IEntityManager>();
// Move the now unbuckled entity back onto the chair
entityManager.GetComponent<TransformComponent>(human).WorldPosition -= (100, 0);

View file

@ -32,6 +32,7 @@ public sealed class SolutionSystemTests
var server = pairTracker.Pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords;
@ -46,11 +47,11 @@ public sealed class SolutionSystemTests
var originalWater = new Solution("Water", waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(EntitySystem.Get<SolutionContainerSystem>()
Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
solution.AddSolution(originalWater);
Assert.That(EntitySystem.Get<SolutionContainerSystem>()
Assert.That(containerSystem
.TryAddSolution(beaker, solution, oilAdded));
solution.ContainsReagent("Water", out var water);
@ -73,6 +74,7 @@ public sealed class SolutionSystemTests
var testMap = await PoolManager.CreateTestMap(pairTracker);
var entityManager = server.ResolveDependency<IEntityManager>();
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
var coordinates = testMap.GridCoords;
EntityUid beaker;
@ -86,11 +88,11 @@ public sealed class SolutionSystemTests
var originalWater = new Solution("Water", waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(EntitySystem.Get<SolutionContainerSystem>()
Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
solution.AddSolution(originalWater);
Assert.That(EntitySystem.Get<SolutionContainerSystem>()
Assert.That(containerSystem
.TryAddSolution(beaker, solution, oilAdded), Is.False);
solution.ContainsReagent("Water", out var water);
@ -112,6 +114,7 @@ public sealed class SolutionSystemTests
var entityManager = server.ResolveDependency<IEntityManager>();
var testMap = await PoolManager.CreateTestMap(pairTracker);
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
var coordinates = testMap.GridCoords;
EntityUid beaker;
@ -127,11 +130,11 @@ public sealed class SolutionSystemTests
var originalWater = new Solution("Water", waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(EntitySystem.Get<SolutionContainerSystem>()
Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
solution.AddSolution(originalWater);
Assert.That(EntitySystem.Get<SolutionContainerSystem>()
Assert.That(containerSystem
.TryMixAndOverflow(beaker, solution, oilAdded, threshold, out var overflowingSolution));
Assert.That(solution.CurrentVolume, Is.EqualTo(FixedPoint2.New(threshold)));
@ -158,6 +161,7 @@ public sealed class SolutionSystemTests
var server = pairTracker.Pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords;
@ -174,11 +178,11 @@ public sealed class SolutionSystemTests
var originalWater = new Solution("Water", waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(EntitySystem.Get<SolutionContainerSystem>()
Assert.That(containerSystem
.TryGetSolution(beaker, "beaker", out var solution));
solution.AddSolution(originalWater);
Assert.That(EntitySystem.Get<SolutionContainerSystem>()
Assert.That(containerSystem
.TryMixAndOverflow(beaker, solution, oilAdded, threshold, out _),
Is.False);
});

View file

@ -0,0 +1,34 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Administration.UI;
using Content.Server.EUI;
using NUnit.Framework;
using Robust.Server.Player;
using Robust.Shared.IoC;
namespace Content.IntegrationTests.Tests.Cleanup;
public sealed class EuiManagerTest
{
[Test]
public async Task EuiManagerRecycleWithOpenWindowTest()
{
// Even though we are using the server EUI here, we actually want to see if the client EUIManager crashes
for (int i = 0; i < 2; i++)
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{Dirty = true});
var server = pairTracker.Pair.Server;
var sPlayerManager = server.ResolveDependency<IPlayerManager>();
await server.WaitAssertion(async () =>
{
var clientSession = sPlayerManager.ServerSessions.Single();
var eui = IoCManager.Resolve<EuiManager>();
var ui = new AdminAnnounceEui();
eui.OpenEui(ui, clientSession);
});
await pairTracker.CleanReturnAsync();
}
}
}

View file

@ -27,18 +27,21 @@ namespace Content.IntegrationTests.Tests.DoAfter
Task<DoAfterStatus> task = null;
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<DoAfterSystem>();
// That it finishes successfully
await server.WaitPost(() =>
{
var tickTime = 1.0f / IoCManager.Resolve<IGameTiming>().TickRate;
var mapManager = IoCManager.Resolve<IMapManager>();
mapManager.CreateNewMapEntity(MapId.Nullspace);
var entityManager = IoCManager.Resolve<IEntityManager>();
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var cancelToken = new CancellationTokenSource();
var args = new DoAfterEventArgs(mob, tickTime / 2, cancelToken.Token);
task = EntitySystem.Get<DoAfterSystem>().WaitDoAfter(args);
task = doAfterSystem.WaitDoAfter(args);
});
await server.WaitRunTicks(1);
@ -57,17 +60,19 @@ namespace Content.IntegrationTests.Tests.DoAfter
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<DoAfterSystem>();
await server.WaitPost(() =>
{
var tickTime = 1.0f / IoCManager.Resolve<IGameTiming>().TickRate;
var mapManager = IoCManager.Resolve<IMapManager>();
mapManager.CreateNewMapEntity(MapId.Nullspace);
var entityManager = IoCManager.Resolve<IEntityManager>();
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var cancelToken = new CancellationTokenSource();
var args = new DoAfterEventArgs(mob, tickTime * 2, cancelToken.Token);
task = EntitySystem.Get<DoAfterSystem>().WaitDoAfter(args);
task = doAfterSystem.WaitDoAfter(args);
cancelToken.Cancel();
});

View file

@ -53,6 +53,7 @@ namespace Content.IntegrationTests.Tests.Doors
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var doors = entityManager.EntitySysManager.GetEntitySystem<DoorSystem>();
EntityUid airlock = default;
DoorComponent doorComponent = null;
@ -71,7 +72,7 @@ namespace Content.IntegrationTests.Tests.Doors
await server.WaitAssertion(() =>
{
EntitySystem.Get<DoorSystem>().StartOpening(airlock);
doors.StartOpening(airlock);
Assert.That(doorComponent.State, Is.EqualTo(DoorState.Opening));
});
@ -83,7 +84,7 @@ namespace Content.IntegrationTests.Tests.Doors
await server.WaitAssertion(() =>
{
EntitySystem.Get<DoorSystem>().TryClose((EntityUid) airlock);
doors.TryClose(airlock);
Assert.That(doorComponent.State, Is.EqualTo(DoorState.Closing));
});

View file

@ -6,6 +6,7 @@ using Content.Shared.CCVar;
using Content.Shared.Coordinates;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@ -18,79 +19,89 @@ namespace Content.IntegrationTests.Tests
public sealed class EntityTest
{
[Test]
public async Task SpawnTest()
public async Task SpawnAndDeleteAllEntitiesOnDifferentMaps()
{
//TODO: Run this test in a for loop, and figure out why garbage is ending up in the Entities list on cleanup.
//If this gets fixed, see also UninitializedSaveTest.
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, Dirty = true, Destructive = true});
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, Destructive = true});
var server = pairTracker.Pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var entityMan = server.ResolveDependency<IEntityManager>();
var prototypeMan = server.ResolveDependency<IPrototypeManager>();
var tileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
IEntityManager entityMan = null;
var prototypes = new List<EntityPrototype>();
IMapGrid grid = default;
EntityUid testEntity;
//Build up test environment
await server.WaitPost(() =>
{
// Create a one tile grid to stave off the grid 0 monsters
var mapId = mapManager.CreateMap();
mapManager.AddUninitializedMap(mapId);
grid = mapManager.CreateGrid(mapId);
var tileDefinition = tileDefinitionManager["UnderPlating"];
var tile = new Tile(tileDefinition.TileId);
var coordinates = grid.ToCoordinates();
grid.SetTile(coordinates, tile);
mapManager.DoMapInitialize(mapId);
entityMan = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var prototypeMan = IoCManager.Resolve<IPrototypeManager>();
var protoIds = prototypeMan
.EnumeratePrototypes<EntityPrototype>()
.Where(p=>!p.Abstract)
.Select(p => p.ID)
.ToList();
foreach (var protoId in protoIds)
{
var mapId = mapManager.CreateMap();
var grid = mapManager.CreateGrid(mapId);
var coord = new EntityCoordinates(grid.GridEntityId, 0, 0);
entityMan.SpawnEntity(protoId, coord);
}
});
await server.WaitRunTicks(5);
await server.WaitAssertion(() =>
await server.WaitPost(() =>
{
var testLocation = grid.ToCoordinates();
//Generate list of non-abstract prototypes to test
foreach (var prototype in prototypeMan.EnumeratePrototypes<EntityPrototype>())
var entityMetas = entityMan.EntityQuery<MetaDataComponent>().ToList();
foreach (var meta in entityMetas)
{
if (prototype.Abstract)
{
continue;
}
prototypes.Add(prototype);
if(!entityMan.Deleted(meta.Owner))
entityMan.DeleteEntity(meta.Owner);
}
Assert.Multiple(() =>
{
//Iterate list of prototypes to spawn
foreach (var prototype in prototypes)
{
Assert.DoesNotThrow(() =>
{
Logger.LogS(LogLevel.Debug, "EntityTest", $"Testing: {prototype.ID}");
testEntity = entityMan.SpawnEntity(prototype.ID, testLocation);
server.RunTicks(1);
if(!entityMan.Deleted(testEntity))
entityMan.DeleteEntity(testEntity);
}, "Entity '{0}' threw an exception.",
prototype.ID);
}
});
Assert.That(entityMan.EntityCount, Is.Zero);
});
await pairTracker.CleanReturnAsync();
}
[Test]
public async Task SpawnAndDeleteAllEntitiesInTheSameSpot()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, Destructive = true});
var server = pairTracker.Pair.Server;
IEntityManager entityMan = null;
await server.WaitPost(() =>
{
entityMan = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var prototypeMan = IoCManager.Resolve<IPrototypeManager>();
var protoIds = prototypeMan
.EnumeratePrototypes<EntityPrototype>()
.Where(p=>!p.Abstract)
.Select(p => p.ID)
.ToList();
var mapId = mapManager.CreateMap();
var grid = mapManager.CreateGrid(mapId);
var coord = new EntityCoordinates(grid.GridEntityId, 0, 0);
foreach (var protoId in protoIds)
{
entityMan.SpawnEntity(protoId, coord);
}
});
await server.WaitRunTicks(5);
await server.WaitPost(() =>
{
var entityMetas = entityMan.EntityQuery<MetaDataComponent>().ToList();
foreach (var meta in entityMetas)
{
if(!entityMan.Deleted(meta.Owner))
entityMan.DeleteEntity(meta.Owner);
}
Assert.That(entityMan.EntityCount, Is.Zero);
});
await pairTracker.CleanReturnAsync();
}
[Test]
public async Task AllComponentsOneToOneDeleteTest()
{

View file

@ -39,7 +39,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
alertsSystem.ShowAlert(playerUid, AlertType.Debug1);
alertsSystem.ShowAlert(playerUid, AlertType.Debug2);
Assert.AreEqual(alertCount + 2, alerts.Count);
Assert.That(alerts, Has.Count.EqualTo(alertCount + 2));
});
await PoolManager.RunTicksSync(pairTracker.Pair, 5);

View file

@ -8,7 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Content.IntegrationTests.Tests.Lobby;
public class ServerReloginTest
public sealed class ServerReloginTest
{
[Test]
public async Task Relogin()

View file

@ -69,7 +69,13 @@ namespace Content.IntegrationTests.Tests
task = Task.Run(static async () =>
{
await Task.Yield();
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{Disconnected = true});
await using var pairTracker = await PoolManager.GetServerClient(
new PoolSettings
{
Disconnected = true,
TestName = $"{nameof(PostMapInitTest)}.{nameof(GetMapNames)}"
}
);
var server = pairTracker.Pair.Server;
var resourceManager = server.ResolveDependency<IResourceManager>();
var mapFolder = new ResourcePath("/Maps");

View file

@ -129,7 +129,6 @@ public sealed class PrototypeSaveTest
"UniformPrinter",
"OreProcessor",
"MedicalScanner",
"KitchenMicrowave",
"MagazinePistolSubMachineGunTopMounted",
"Recycler",
"EpinephrineChemistryBottle",
@ -235,7 +234,7 @@ public sealed class PrototypeSaveTest
{
uid = entityMan.SpawnEntity(prototype.ID, testLocation);
server.RunTicks(1);
// get default prototype data
Dictionary<string, MappingDataNode> protoData = new();
try

View file

@ -122,7 +122,7 @@ namespace Content.IntegrationTests.Tests
roundEndSystem.DefaultCooldownDuration = TimeSpan.FromSeconds(30);
roundEndSystem.DefaultCountdownDuration = TimeSpan.FromMinutes(4);
roundEndSystem.DefaultRestartRoundDuration = TimeSpan.FromMinutes(1);
EntitySystem.Get<GameTicker>().RestartRound();
ticker.RestartRound();
});
await PoolManager.ReallyBeIdle(pairTracker.Pair, 10);

View file

@ -1,14 +1,42 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.Storage.Components;
using Content.Shared.Item;
using Content.Shared.Storage;
using NUnit.Framework;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
public sealed class StorageFillTest
public sealed class StorageTest
{
/// <summary>
/// Can an item store more than itself weighs.
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
/// </summary>
[Test]
public async Task StorageSizeArbitrageTest()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
var server = pairTracker.Pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<ServerStorageComponent>("Storage", out var storage) ||
storage.Whitelist != null ||
!proto.TryGetComponent<ItemComponent>("Item", out var item)) continue;
Assert.That(storage.StorageCapacityMax, Is.LessThanOrEqualTo(item.Size), $"Found storage arbitrage on {proto.ID}");
}
});
await pairTracker.CleanReturnAsync();
}
[Test]
public async Task TestStorageFillPrototypes()
{

View file

@ -8,7 +8,7 @@ public sealed class SandboxTest
[Test]
public async Task Test()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoServer = true});
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoServer = true, Destructive = true});
var client = pairTracker.Pair.Client;
await client.CheckSandboxed(typeof(Client.Entry.EntryPoint).Assembly);
await pairTracker.CleanReturnAsync();

View file

@ -29,7 +29,7 @@ namespace Content.IntegrationTests.Tests
}
catch (UnknownPrototypeException)
{
throw new UnknownPrototypeException($"Unknown prototype {item} on vending inventory {vendorProto.Name}");
throw new UnknownPrototypeException($"Unknown prototype {item} on vending inventory {vendorProto.ID}");
}
}
}

View file

@ -445,10 +445,11 @@ namespace Content.Server.AI.Pathfinding.Accessible
var parentChunk = node.ParentChunk;
// No guarantee the node even has a region yet (if we're doing neighbor lookups)
if (!_regions[parentChunk.GridId].TryGetValue(parentChunk, out var regions))
{
if (!_regions.TryGetValue(parentChunk.GridId, out var chunk))
return null;
if (!chunk.TryGetValue(parentChunk, out var regions))
return null;
}
foreach (var region in regions)
{

View file

@ -29,9 +29,6 @@ namespace Content.Server.Access.Systems
private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args)
{
// On one hand, these prototypes should default to having the correct name. On the other hand, id cards are
// rarely ever spawned in on their own without an owner, so this is fine.
id.OriginalEntityName ??= EntityManager.GetComponent<MetaDataComponent>(id.Owner).EntityName;
UpdateEntityName(uid, id);
}
@ -80,21 +77,32 @@ namespace Content.Server.Access.Systems
/// <remarks>
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
/// </remarks>
public bool TryChangeJobTitle(EntityUid uid, string jobTitle, IdCardComponent? id = null, EntityUid? player = null)
public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
if (jobTitle.Length > SharedIdCardConsoleComponent.MaxJobTitleLength)
jobTitle = jobTitle[..SharedIdCardConsoleComponent.MaxJobTitleLength];
if (!string.IsNullOrWhiteSpace(jobTitle))
{
jobTitle = jobTitle.Trim();
if (jobTitle.Length > SharedIdCardConsoleComponent.MaxJobTitleLength)
jobTitle = jobTitle[..SharedIdCardConsoleComponent.MaxJobTitleLength];
}
else
{
jobTitle = null;
}
id.JobTitle = jobTitle;
Dirty(id);
UpdateEntityName(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(id.Owner):entity} to {jobTitle} ");
$"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(id.Owner):entity} to {jobTitle} ");
}
return true;
}
@ -105,21 +113,31 @@ namespace Content.Server.Access.Systems
/// <remarks>
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
/// </remarks>
public bool TryChangeFullName(EntityUid uid, string fullName, IdCardComponent? id = null, EntityUid? player = null)
public bool TryChangeFullName(EntityUid uid, string? fullName, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
if (fullName.Length > SharedIdCardConsoleComponent.MaxFullNameLength)
fullName = fullName[..SharedIdCardConsoleComponent.MaxFullNameLength];
if (!string.IsNullOrWhiteSpace(fullName))
{
fullName = fullName.Trim();
if (fullName.Length > SharedIdCardConsoleComponent.MaxFullNameLength)
fullName = fullName[..SharedIdCardConsoleComponent.MaxFullNameLength];
}
else
{
fullName = null;
}
id.FullName = fullName;
Dirty(id);
UpdateEntityName(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(id.Owner):entity} to {fullName} ");
$"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(id.Owner):entity} to {fullName} ");
}
return true;
}
@ -135,17 +153,10 @@ namespace Content.Server.Access.Systems
if (!Resolve(uid, ref id))
return;
if (string.IsNullOrWhiteSpace(id.FullName) && string.IsNullOrWhiteSpace(id.JobTitle))
{
EntityManager.GetComponent<MetaDataComponent>(id.Owner).EntityName = id.OriginalEntityName;
return;
}
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
("originalOwnerName", id.OriginalEntityName),
("jobSuffix", jobSuffix))
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
("fullName", id.FullName),

View file

@ -66,7 +66,6 @@ public sealed class IdExaminableSystem : EntitySystem
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
("originalOwnerName", id.OriginalEntityName),
("jobSuffix", jobSuffix))
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
("fullName", id.FullName),

View file

@ -44,7 +44,7 @@ namespace Content.Server.Administration.Commands
var coordinates = player.AttachedEntity != null
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
: EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
var ghost = _entities.SpawnEntity("AdminObserver", coordinates.ToMap(_entities));
var ghost = _entities.SpawnEntity("AdminObserver", coordinates);
if (canReturn)
{

View file

@ -17,7 +17,6 @@ namespace Content.Server.Atmos.Miasma
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
/// System Variables

View file

@ -74,7 +74,7 @@ public sealed partial class ChatSystem
}
// Re-capitalize message since we removed the prefix.
message = SanitizeMessageCapital(source, message);
message = SanitizeMessageCapital(message);
if (_inventory.TryGetSlotEntity(source, "ears", out var entityUid) &&
TryComp(entityUid, out HeadsetComponent? headset))

View file

@ -127,8 +127,9 @@ public sealed partial class ChatSystem : SharedChatSystem
return;
bool shouldCapitalize = (desiredType != InGameICChatType.Emote);
bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation);
message = SanitizeInGameICMessage(source, message, out var emoteStr, shouldCapitalize);
message = SanitizeInGameICMessage(source, message, out var emoteStr, shouldCapitalize, shouldPunctuate);
// Was there an emote in the message? If so, send it.
if (player != null && emoteStr != message && emoteStr != null)
@ -423,11 +424,13 @@ public sealed partial class ChatSystem : SharedChatSystem
}
// ReSharper disable once InconsistentNaming
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true)
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false)
{
var newMessage = message.Trim();
if (capitalize)
newMessage = SanitizeMessageCapital(source, newMessage);
newMessage = SanitizeMessageCapital(newMessage);
if (punctuate)
newMessage = SanitizeMessagePeriod(newMessage);
newMessage = FormattedMessage.EscapeText(newMessage);
_sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr);
@ -460,7 +463,7 @@ public sealed partial class ChatSystem : SharedChatSystem
.Select(p => p.ConnectedClient);
}
private string SanitizeMessageCapital(EntityUid source, string message)
private string SanitizeMessageCapital(string message)
{
if (string.IsNullOrEmpty(message))
return message;
@ -469,6 +472,16 @@ public sealed partial class ChatSystem : SharedChatSystem
return message;
}
private string SanitizeMessagePeriod(string message)
{
if (string.IsNullOrEmpty(message))
return message;
// Adds a period if the last character is a letter.
if (char.IsLetter(message[^1]))
message += ".";
return message;
}
private void ClientDistanceToList(EntityUid source, int voiceRange, List<ICommonSession> playerSessions)
{
var ghosts = GetEntityQuery<GhostComponent>();

View file

@ -1,10 +1,11 @@
using Content.Server.Fluids.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Slippery;
using Content.Shared.StepTrigger;
using Content.Shared.StepTrigger.Components;
using Content.Shared.StepTrigger.Systems;
using JetBrains.Annotations;
using Robust.Shared.Map;
@ -24,18 +25,21 @@ namespace Content.Server.Chemistry.TileReactions
{
if (reactVolume < 5) return FixedPoint2.Zero;
var entityManager = IoCManager.Resolve<IEntityManager>();
// TODO Make this not puddle smear.
var puddle = EntitySystem.Get<SpillableSystem>()
var puddle = entityManager.EntitySysManager.GetEntitySystem<SpillableSystem>()
.SpillAt(tile, new Solution(reagent.ID, reactVolume), "PuddleSmear", _overflow, false, true);
if (puddle != null)
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var slippery = entityManager.GetComponent<SlipperyComponent>(puddle.Owner);
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddle.Owner);
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
EntitySystem.Get<StepTriggerSystem>().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed);
slippery.ParalyzeTime = _paralyzeTime;
var step = entityManager.EnsureComponent<StepTriggerComponent>(puddle.Owner);
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed, step);
return reactVolume;
}

View file

@ -116,13 +116,13 @@ public sealed class ClimbSystem : SharedClimbSystem
if (TryBonk(component, user))
return;
_doAfterSystem.DoAfter(new DoAfterEventArgs(entityToMove, component.ClimbDelay, default, climbable)
_doAfterSystem.DoAfter(new DoAfterEventArgs(entityToMove, component.ClimbDelay, default, climbable, user)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
UserFinishedEvent = new ClimbFinishedEvent(user, climbable)
UserFinishedEvent = new ClimbFinishedEvent(user, climbable, entityToMove)
});
}
@ -155,10 +155,10 @@ public sealed class ClimbSystem : SharedClimbSystem
private void OnClimbFinished(EntityUid uid, ClimbingComponent climbing, ClimbFinishedEvent args)
{
Climb(uid, args.User, args.Climbable, climbing: climbing);
Climb(uid, args.User, args.Instigator, args.Climbable, climbing: climbing);
}
private void Climb(EntityUid uid, EntityUid user, EntityUid climbable, bool silent = false, ClimbingComponent? climbing = null,
private void Climb(EntityUid uid, EntityUid user, EntityUid instigator, EntityUid climbable, bool silent = false, ClimbingComponent? climbing = null,
PhysicsComponent? physics = null, FixturesComponent? fixtures = null)
{
if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
@ -174,7 +174,7 @@ public sealed class ClimbSystem : SharedClimbSystem
// there's also the cases where the user might collide with the person they are forcing onto the climbable that i haven't accounted for
RaiseLocalEvent(uid, new StartClimbEvent(climbable), false);
RaiseLocalEvent(climbable, new ClimbedOnEvent(uid), false);
RaiseLocalEvent(climbable, new ClimbedOnEvent(uid, user), false);
if (silent)
return;
@ -343,7 +343,7 @@ public sealed class ClimbSystem : SharedClimbSystem
public void ForciblySetClimbing(EntityUid uid, EntityUid climbable, ClimbingComponent? component = null)
{
Climb(uid, uid, climbable, true, component);
Climb(uid, uid, uid, climbable, true, component);
}
private void OnBuckleChange(EntityUid uid, ClimbingComponent component, BuckleChangeEvent args)
@ -436,14 +436,16 @@ public sealed class ClimbSystem : SharedClimbSystem
internal sealed class ClimbFinishedEvent : EntityEventArgs
{
public ClimbFinishedEvent(EntityUid user, EntityUid climbable)
public ClimbFinishedEvent(EntityUid user, EntityUid climbable, EntityUid instigator)
{
User = user;
Climbable = climbable;
Instigator = instigator;
}
public EntityUid User { get; }
public EntityUid Climbable { get; }
public EntityUid Instigator { get; }
}
/// <summary>
@ -452,10 +454,12 @@ internal sealed class ClimbFinishedEvent : EntityEventArgs
public sealed class ClimbedOnEvent : EntityEventArgs
{
public EntityUid Climber;
public EntityUid Instigator;
public ClimbedOnEvent(EntityUid climber)
public ClimbedOnEvent(EntityUid climber, EntityUid instigator)
{
Climber = climber;
Instigator = instigator;
}
}

View file

@ -7,8 +7,8 @@ using Content.Server.Mind.Components;
using Content.Server.MachineLinking.System;
using Content.Server.MachineLinking.Events;
using Content.Server.UserInterface;
using Content.Shared.MobState.Components;
using Content.Server.MobState;
using Content.Shared.MobState.Components;
using Content.Server.Power.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Server.Player;
@ -55,10 +55,6 @@ namespace Content.Server.Cloning.Systems
if (consoleComponent.GeneticScanner != null && consoleComponent.CloningPod != null)
TryClone(uid, consoleComponent.CloningPod.Value, consoleComponent.GeneticScanner.Value, consoleComponent: consoleComponent);
break;
case UiButton.Eject:
if (consoleComponent.CloningPod != null)
TryEject(uid, consoleComponent.CloningPod.Value, consoleComponent: consoleComponent);
break;
}
UpdateUserInterface(consoleComponent);
}
@ -123,14 +119,6 @@ namespace Content.Server.Cloning.Systems
_uiSystem.GetUiOrNull(consoleComponent.Owner, CloningConsoleUiKey.Key)?.SetState(newState);
}
public void TryEject(EntityUid uid, EntityUid clonePodUid, CloningPodComponent? cloningPod = null, CloningConsoleComponent? consoleComponent = null)
{
if (!Resolve(uid, ref consoleComponent) || !Resolve(clonePodUid, ref cloningPod))
return;
_cloningSystem.Eject(clonePodUid, cloningPod);
}
public void TryClone(EntityUid uid, EntityUid cloningPodUid, EntityUid scannerUid, CloningPodComponent? cloningPod = null, MedicalScannerComponent? scannerComp = null, CloningConsoleComponent? consoleComponent = null)
{
if (!Resolve(uid, ref consoleComponent) || !Resolve(cloningPodUid, ref cloningPod) || !Resolve(scannerUid, ref scannerComp))
@ -222,9 +210,10 @@ namespace Content.Server.Cloning.Systems
EntityUid? cloneBody = clonePod.BodyContainer.ContainedEntity;
clonerMindPresent = clonePod.Status == CloningPodStatus.Cloning;
if (cloneBody != null)
if (HasComp<ActiveCloningPodComponent>(consoleComponent.CloningPod))
{
cloneBodyInfo = Identity.Name(cloneBody.Value, EntityManager);
if (cloneBody != null)
cloneBodyInfo = Identity.Name(cloneBody.Value, EntityManager);
clonerStatus = ClonerStatus.ClonerOccupied;
}
}
@ -244,5 +233,6 @@ namespace Content.Server.Cloning.Systems
clonerInRange
);
}
}
}

View file

@ -1,19 +1,35 @@
using Content.Server.Cloning.Components;
using Content.Server.Mind.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.GameTicking;
using Content.Shared.CharacterAppearance.Systems;
using Content.Shared.CharacterAppearance.Components;
using Content.Shared.Species;
using Robust.Server.Player;
using Robust.Shared.Prototypes;
using Content.Server.EUI;
using Robust.Shared.Containers;
using Robust.Server.Containers;
using Content.Shared.Damage;
using Content.Shared.Stacks;
using Content.Shared.Examine;
using Content.Shared.Cloning;
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Server.Cloning.Components;
using Content.Server.Mind.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Atmos.EntitySystems;
using Content.Server.EUI;
using Content.Server.MachineLinking.System;
using Content.Server.MachineLinking.Events;
using Content.Server.MobState;
using Content.Server.Lathe.Components;
using Content.Shared.Chemistry.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Server.Construction.Components;
using Content.Server.Stack;
using Robust.Server.GameObjects;
using Robust.Server.Containers;
using Robust.Server.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
namespace Content.Server.Cloning.Systems
{
@ -28,17 +44,29 @@ namespace Content.Server.Cloning.Systems
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly SharedStackSystem _stackSystem = default!;
[Dependency] private readonly StackSystem _serverStackSystem = default!;
[Dependency] private readonly SpillableSystem _spillableSystem = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
public readonly Dictionary<Mind.Mind, EntityUid> ClonesWaitingForMind = new();
public const float EasyModeCloningCost = 0.7f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CloningPodComponent, MachineDeconstructedEvent>(OnDeconstruct);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
}
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
@ -47,6 +75,14 @@ namespace Content.Server.Cloning.Systems
_signalSystem.EnsureReceiverPorts(uid, CloningPodComponent.PodPort);
}
private void OnDeconstruct(EntityUid uid, CloningPodComponent component, MachineDeconstructedEvent args)
{
if (!TryComp<MaterialStorageComponent>(uid, out var storage))
return;
_serverStackSystem.SpawnMultiple(storage.GetMaterialAmount("Biomass"), 100, "Biomass", Transform(uid).Coordinates);
}
private void UpdateAppearance(CloningPodComponent clonePod)
{
if (TryComp<AppearanceComponent>(clonePod.Owner, out var appearance))
@ -97,11 +133,23 @@ namespace Content.Server.Cloning.Systems
_cloningConsoleSystem.UpdateUserInterface(console);
}
private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
return;
if (TryComp<MaterialStorageComponent>(uid, out var storage))
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", storage.GetMaterialAmount("Biomass"))));
}
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Mind.Mind mind, CloningPodComponent? clonePod)
{
if (!Resolve(uid, ref clonePod) || bodyToClone == null)
return false;
if (HasComp<ActiveCloningPodComponent>(uid))
return false;
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
{
if (EntityManager.EntityExists(clone) &&
@ -120,11 +168,56 @@ namespace Content.Server.Cloning.Systems
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
if (!TryComp<MaterialStorageComponent>(clonePod.Owner, out var podStorage))
return false;
if (!TryComp<HumanoidAppearanceComponent>(bodyToClone, out var humanoid))
return false; // whatever body was to be cloned, was not a humanoid
var speciesProto = _prototype.Index<SpeciesPrototype>(humanoid.Species).Prototype;
var mob = Spawn(speciesProto, Transform(clonePod.Owner).MapPosition);
if (!_prototype.TryIndex<SpeciesPrototype>(humanoid.Species, out var speciesPrototype))
return false;
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
return false;
int cloningCost = (int) physics.FixturesMass;
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
// biomass checks
var biomassAmount = podStorage.GetMaterialAmount("Biomass");
if (biomassAmount < cloningCost)
{
if (clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
return false;
}
podStorage.RemoveMaterial("Biomass", cloningCost);
clonePod.UsedBiomass = cloningCost;
// end of biomass checks
// genetic damage checks
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
{
var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - (chance * 100)))), InGameICChatType.Speak, false);
if (_robustRandom.Prob(chance))
{
UpdateStatus(CloningPodStatus.Gore, clonePod);
clonePod.FailedClone = true;
AddComp<ActiveCloningPodComponent>(uid);
return true;
}
}
// end of genetic damage checks
var mob = Spawn(speciesPrototype.Prototype, Transform(clonePod.Owner).MapPosition);
_appearanceSystem.UpdateAppearance(mob, humanoid.Appearance);
_appearanceSystem.UpdateSexGender(mob, humanoid.Sex, humanoid.Gender);
@ -156,16 +249,17 @@ namespace Content.Server.Cloning.Systems
if (!_powerReceiverSystem.IsPowered(cloning.Owner))
continue;
if (cloning.BodyContainer.ContainedEntity != null)
{
cloning.CloningProgress += frameTime;
cloning.CloningProgress = MathHelper.Clamp(cloning.CloningProgress, 0f, cloning.CloningTime);
}
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
continue;
if (cloning.CapturedMind?.Session?.AttachedEntity == cloning.BodyContainer.ContainedEntity)
{
cloning.CloningProgress += frameTime;
if (cloning.CloningProgress < cloning.CloningTime)
continue;
if (cloning.FailedClone)
EndFailedCloning(cloning.Owner, cloning);
else
Eject(cloning.Owner, cloning);
}
}
}
@ -181,10 +275,40 @@ namespace Content.Server.Cloning.Systems
clonePod.BodyContainer.Remove(entity);
clonePod.CapturedMind = null;
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
{
clonePod.FailedClone = false;
clonePod.CloningProgress = 0f;
UpdateStatus(CloningPodStatus.Idle, clonePod);
var transform = Transform(uid);
var indices = _transformSystem.GetGridOrMapTilePosition(uid);
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
Solution bloodSolution = new();
int i = 0;
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Miasma, 6f);
bloodSolution.AddReagent("Blood", 50);
if (_robustRandom.Prob(0.2f))
i++;
}
_spillableSystem.SpillAt(uid, bloodSolution, "PuddleBlood");
var biomassStack = Spawn("MaterialBiomass", transform.Coordinates);
_stackSystem.SetCount(biomassStack, _robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)));
clonePod.UsedBiomass = 0;
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();

View file

@ -10,6 +10,8 @@ namespace Content.Server.Cloning.Components
[ViewVariables] public ContainerSlot BodyContainer = default!;
[ViewVariables] public Mind.Mind? CapturedMind;
[ViewVariables] public float CloningProgress = 0;
[ViewVariables] public int UsedBiomass = 70;
[ViewVariables] public bool FailedClone = false;
[DataField("cloningTime")]
[ViewVariables] public float CloningTime = 30f;
[ViewVariables] public CloningPodStatus Status;

View file

@ -1,6 +1,7 @@
using Content.Server.Construction.Components;
using Content.Server.Construction.Components;
using Content.Server.Stack;
using Content.Shared.Construction;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Tag;
using Robust.Shared.Containers;
@ -22,6 +23,7 @@ public sealed class MachineFrameSystem : EntitySystem
SubscribeLocalEvent<MachineFrameComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<MachineFrameComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<MachineFrameComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<MachineFrameComponent, ExaminedEvent>(OnMachineFrameExamined);
}
private void OnInit(EntityUid uid, MachineFrameComponent component, ComponentInit args)
@ -295,4 +297,11 @@ public sealed class MachineFrameSystem : EntitySystem
}
}
}
private void OnMachineFrameExamined(EntityUid uid, MachineFrameComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (component.HasBoard)
args.PushMarkup(Loc.GetString("machine-frame-component-on-examine-label", ("board", EntityManager.GetComponent<MetaDataComponent>(component.BoardContainer.ContainedEntities[0]).EntityName)));
}
}

View file

@ -40,8 +40,15 @@ public sealed class CrewManifestEui : BaseEui
switch (msg)
{
case CrewManifestEuiClosed:
_crewManifest.CloseEui(_station, Player, Owner);
Closed();
break;
}
}
public override void Closed()
{
base.Closed();
_crewManifest.CloseEui(_station, Player, Owner);
}
}

View file

@ -186,8 +186,8 @@ public sealed class CrewManifestSystem : EntitySystem
if (eui.Owner == owner)
{
eui.Close();
euis.Remove(session);
eui.Close();
}
if (euis.Count == 0)

View file

@ -1,6 +1,4 @@
using Content.Server.VendingMachines;
using Content.Shared.Throwing;
using Robust.Shared.Random;
namespace Content.Server.Destructible.Thresholds.Behaviors
{
@ -30,17 +28,15 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
!system.EntityManager.TryGetComponent<TransformComponent>(owner, out var xform))
return;
var throwingsys = system.EntityManager.EntitySysManager.GetEntitySystem<ThrowingSystem>();
var totalItems = vendingcomp.AllInventory.Count;
var vendingMachineSystem = EntitySystem.Get<VendingMachineSystem>();
var inventory = vendingMachineSystem.GetAvailableInventory(owner, vendingcomp);
if (inventory.Count <= 0)
return;
var toEject = Math.Min(totalItems * Percent, Max);
var toEject = Math.Min(inventory.Count * Percent, Max);
for (var i = 0; i < toEject; i++)
{
var entity = system.EntityManager.SpawnEntity(system.Random.PickAndTake(vendingcomp.AllInventory).ID, xform.Coordinates);
float range = vendingcomp.NonLimitedEjectRange;
Vector2 direction = new Vector2(system.Random.NextFloat(-range, range), system.Random.NextFloat(-range, range));
throwingsys.TryThrow(entity, direction, vendingcomp.NonLimitedEjectForce);
vendingMachineSystem.EjectRandom(owner, throwItem: true, forceEject: true, vendingcomp);
}
}
}

View file

@ -53,7 +53,6 @@ public sealed partial class EnsnareableSystem
/// <summary>
/// Used where you want to try to ensnare an entity with the <see cref="EnsnareableComponent"/>
/// </summary>
/// <param name="ensnaringEntity">The entity that will be used to ensnare</param>
/// <param name="target">The entity that will be ensnared</param>
/// <param name="component">The ensnaring component</param>
public void TryEnsnare(EntityUid target, EnsnaringComponent component)
@ -68,7 +67,7 @@ public sealed partial class EnsnareableSystem
UpdateAlert(ensnareable);
var ev = new EnsnareEvent(component.WalkSpeed, component.SprintSpeed);
RaiseLocalEvent(target, ev, false);
RaiseLocalEvent(target, ev);
}
/// <summary>
@ -79,7 +78,7 @@ public sealed partial class EnsnareableSystem
public void TryFree(EntityUid target, EnsnaringComponent component, EntityUid? user = null)
{
//Don't do anything if they don't have the ensnareable component.
if (!TryComp<EnsnareableComponent>(target, out var ensnareable))
if (!HasComp<EnsnareableComponent>(target))
return;
if (component.CancelToken != null)
@ -132,7 +131,7 @@ public sealed partial class EnsnareableSystem
UpdateAlert(ensnareable);
var ev = new EnsnareRemoveEvent();
RaiseLocalEvent(component.Owner, ev, false);
RaiseLocalEvent(component.Owner, ev);
}
public void UpdateAlert(EnsnareableComponent component)

View file

@ -44,7 +44,7 @@ public sealed partial class EnsnareableSystem : SharedEnsnareableSystem
UpdateAlert(component);
var ev = new EnsnareRemoveEvent();
RaiseLocalEvent(uid, ev, false);
RaiseLocalEvent(uid, ev);
ensnaring.CancelToken = null;
}

View file

@ -158,7 +158,9 @@ public sealed partial class ExplosionSystem : EntitySystem
damagePerIntensity += value * Math.Max(0, ev.DamageCoefficient);
}
explosionTolerance[index] = (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity);
explosionTolerance[index] = damagePerIntensity > 0
? (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity)
: float.MaxValue;
}
return explosionTolerance;

View file

@ -19,6 +19,8 @@ public sealed partial class TriggerSystem
//Ensures the entity trigger will have an active component
EnsureComp<ActiveTriggerOnTimedCollideComponent>(uid);
var otherUID = args.OtherFixture.Body.Owner;
if (component.Colliding.ContainsKey(otherUID))
return;
component.Colliding.Add(otherUID, 0);
}

View file

@ -126,7 +126,8 @@ namespace Content.Server.Explosion.EntitySystems
{
// Check if entity is bomb/mod. grenade/etc
if (_container.TryGetContainer(uid, "payload", out IContainer? container) &&
TryComp(container.ContainedEntities.First(), out ChemicalPayloadComponent? chemicalPayloadComponent))
container.ContainedEntities.Count > 0 &&
TryComp(container.ContainedEntities[0], out ChemicalPayloadComponent? chemicalPayloadComponent))
{
// If a beaker is missing, the entity won't explode, so no reason to log it
if (!TryComp(chemicalPayloadComponent?.BeakerSlotA.Item, out SolutionContainerManagerComponent? beakerA) ||

View file

@ -165,7 +165,7 @@ namespace Content.Server.Flash
foreach (var entity in flashableEntities)
{
// Check for unobstructed entities while ignoring the mobs with flashable components.
if (!_interactionSystem.InRangeUnobstructed(entity, mapPosition, range, CollisionGroup.Opaque, (e) => flashableEntities.Contains(e)))
if (!_interactionSystem.InRangeUnobstructed(entity, mapPosition, range, CollisionGroup.Opaque, (e) => flashableEntities.Contains(e) || e == source))
continue;
// They shouldn't have flash removed in between right?

View file

@ -121,10 +121,9 @@ namespace Content.Server.GameTicking
var (entities, gridIds) = _mapLoader.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options);
var gridUids = gridIds.Select(g => (EntityUid)g).ToList();
var gridUids = gridIds.Select(g => g).ToList();
RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, entities, gridUids, stationName));
_spawnPoint = _mapManager.GetGrid(gridIds[0]).ToCoordinates();
return (entities, gridUids);
}

View file

@ -25,9 +25,6 @@ namespace Content.Server.GameTicking
{
private const string ObserverPrototypeName = "MobObserver";
[ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")] // See also: MindComponent's OnShutdown shitcode
private EntityCoordinates _spawnPoint;
/// <summary>
/// How many players have joined the round through normal methods.
/// Useful for game rules to look at. Doesn't count observers, people in lobby, etc.
@ -281,24 +278,72 @@ namespace Content.Server.GameTicking
#region Spawn Points
public EntityCoordinates GetObserverSpawnPoint()
{
// TODO rename this to TryGetObserverSpawnPoint to make it clear that the result might be invalid. Or at
// least try try more fallback values, like randomly spawning them in any available map or just creating a
// "we fucked up" map. Its better than dumping them into the void.
var location = _spawnPoint.IsValid(EntityManager) ? _spawnPoint : EntityCoordinates.Invalid;
_possiblePositions.Clear();
foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
{
if (point.SpawnType == SpawnPointType.Observer)
_possiblePositions.Add(transform.Coordinates);
if (point.SpawnType != SpawnPointType.Observer)
continue;
_possiblePositions.Add(transform.Coordinates);
}
var metaQuery = GetEntityQuery<MetaDataComponent>();
// Fallback to a random grid.
if (_possiblePositions.Count == 0)
{
foreach (var grid in _mapManager.GetAllGrids())
{
if (!metaQuery.TryGetComponent(grid.GridEntityId, out var meta) ||
meta.EntityPaused)
{
continue;
}
_possiblePositions.Add(new EntityCoordinates(grid.GridEntityId, Vector2.Zero));
}
}
if (_possiblePositions.Count != 0)
location = _robustRandom.Pick(_possiblePositions);
{
// TODO: This is just here for the eye lerping.
// Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
// update which means we need to add a hack somewhere around it.
var spawn = _robustRandom.Pick(_possiblePositions);
var toMap = spawn.ToMap(EntityManager);
return location;
if (_mapManager.TryFindGridAt(toMap, out var foundGrid))
{
return new EntityCoordinates(foundGrid.GridEntityId,
foundGrid.InvWorldMatrix.Transform(toMap.Position));
}
return spawn;
}
if (_mapManager.MapExists(DefaultMap))
{
return new EntityCoordinates(_mapManager.GetMapEntityId(DefaultMap), Vector2.Zero);
}
// Just pick a point at this point I guess.
foreach (var map in _mapManager.GetAllMapIds())
{
var mapUid = _mapManager.GetMapEntityId(map);
if (!metaQuery.TryGetComponent(mapUid, out var meta) ||
meta.EntityPaused)
{
continue;
}
return new EntityCoordinates(mapUid, Vector2.Zero);
}
// AAAAAAAAAAAAA
_sawmill.Error("Found no observer spawn points!");
return EntityCoordinates.Invalid;
}
#endregion
}

View file

@ -2,8 +2,8 @@ using System.Linq;
using Content.Server.Actions;
using Content.Server.Chat.Managers;
using Content.Server.Disease;
using Content.Server.GameTicking.Rules.Configurations;
using Content.Server.Mind.Components;
using Content.Server.MobState;
using Content.Server.Players;
using Content.Server.Popups;
using Content.Server.Preferences.Managers;
@ -13,7 +13,6 @@ using Content.Server.Zombies;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.CCVar;
using Content.Shared.CharacterAppearance.Components;
using Content.Shared.FixedPoint;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Content.Shared.Preferences;
@ -40,6 +39,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem
[Dependency] private readonly DiseaseSystem _diseaseSystem = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
private Dictionary<string, string> _initialInfectedNames = new();
@ -90,8 +90,8 @@ public sealed class ZombieRuleSystem : GameRuleSystem
("username", player.Value)));
}
///Gets a bunch of the living players and displays them if they're under a threshold.
///InitialInfected is used for the threshold because it scales with the player count well.
//Gets a bunch of the living players and displays them if they're under a threshold.
//InitialInfected is used for the threshold because it scales with the player count well.
if (livingHumans.Count > 0 && livingHumans.Count <= _initialInfectedNames.Count)
{
ev.AddLine("");
@ -203,16 +203,16 @@ public sealed class ZombieRuleSystem : GameRuleSystem
livingHumans = new();
foreach (var ent in allPlayers)
foreach (var (_, mob) in allPlayers)
{
if (ent.Item2.IsAlive())
if (_mobState.IsAlive(mob.Owner, mob))
{
totalPlayers.Add(ent.Item2.Owner);
totalPlayers.Add(mob.Owner);
if (allZombers.HasComponent(ent.Item1.Owner))
livingZombies.Add(ent.Item2.Owner);
if (allZombers.HasComponent(mob.Owner))
livingZombies.Add(mob.Owner);
else
livingHumans.Add(ent.Item2.Owner);
livingHumans.Add(mob.Owner);
}
}
return ((float) livingZombies.Count) / (float) totalPlayers.Count;
@ -300,8 +300,8 @@ public sealed class ZombieRuleSystem : GameRuleSystem
var messageWrapper = Loc.GetString("chat-manager-server-wrap-message");
//gets the names now in case the players leave.
if (inCharacterName != null)
_initialInfectedNames.Add(inCharacterName, mind.Session.Name);
//this gets unhappy if people with the same name get chose. Probably shouldn't happen.
_initialInfectedNames.Add(inCharacterName, mind.Session.Name);
// I went all the way to ChatManager.cs and all i got was this lousy T-shirt
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, Loc.GetString("zombie-patientzero-role-greeting"),

View file

@ -0,0 +1,36 @@
using Content.Shared.CCVar;
using Content.Shared.Info;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Log;
namespace Content.Server.Info;
public class InfoSystem : EntitySystem
{
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<RequestRulesMessage>(OnRequestRules);
}
protected void OnRequestRules(RequestRulesMessage message, EntitySessionEventArgs eventArgs)
{
Logger.DebugS("info", "Client requested rules.");
var title = Loc.GetString(_cfg.GetCVar(CCVars.RulesHeader));
var path = _cfg.GetCVar(CCVars.RulesFile);
var rules = "Server could not read its rules.";
try
{
rules = _res.ContentFileReadAllText($"/Server Info/{path}");
}
catch (System.Exception)
{
Logger.ErrorS("info", "Could not read server rules file.");
}
var response = new RulesMessage(title, rules);
RaiseNetworkEvent(response, eventArgs.SenderSession.ConnectedClient);
}
}

View file

@ -0,0 +1,19 @@
using Content.Shared.Kitchen;
namespace Content.Server.Kitchen.Components;
/// <summary>
/// Attached to a microwave that is currently in the process of cooking
/// </summary>
[RegisterComponent]
public sealed class ActiveMicrowaveComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
public float CookTimeRemaining;
[ViewVariables(VVAccess.ReadWrite)]
public float TotalTime;
[ViewVariables]
public (FoodRecipePrototype?, int) PortionedRecipe;
}

View file

@ -1,49 +1,34 @@
using System.Linq;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Power.Components;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Server.UserInterface;
using Content.Shared.FixedPoint;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Power;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Kitchen.Components
{
[RegisterComponent]
public sealed class MicrowaveComponent : SharedMicrowaveComponent
public sealed class MicrowaveComponent : Component
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly RecipeManager _recipeManager = default!;
#region YAMLSERIALIZE
[DataField("cookTime")] private uint _cookTimeDefault = 5;
[DataField("cookTimeMultiplier")] private int _cookTimeMultiplier = 1000; //For upgrades and stuff I guess?
[DataField("failureResult")] private string _badRecipeName = "FoodBadRecipe";
[DataField("beginCookingSound")] private SoundSpecifier _startCookingSound =
new SoundPathSpecifier("/Audio/Machines/microwave_start_beep.ogg");
[DataField("foodDoneSound")] private SoundSpecifier _cookingCompleteSound =
new SoundPathSpecifier("/Audio/Machines/microwave_done_beep.ogg");
[DataField("cookTimeMultiplier")]
public int CookTimeMultiplier = 1; //For upgrades and stuff I guess? don't ask me.
[DataField("failureResult", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string BadRecipeEntityId = "FoodBadRecipe";
#region audio
[DataField("beginCookingSound")]
public SoundSpecifier StartCookingSound = new SoundPathSpecifier("/Audio/Machines/microwave_start_beep.ogg");
[DataField("foodDoneSound")]
public SoundSpecifier FoodDoneSound = new SoundPathSpecifier("/Audio/Machines/microwave_done_beep.ogg");
[DataField("clickSound")]
private SoundSpecifier _clickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
[DataField("ItemBreakSound")]
public SoundSpecifier ItemBreakSound = new SoundPathSpecifier("/Audio/Effects/clang.ogg");
#endregion YAMLSERIALIZE
public IPlayingAudioStream? PlayingStream { get; set; }
[DataField("loopingSound")]
public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg");
#endregion
[ViewVariables] private bool _busy = false;
[ViewVariables]
public bool Broken;
/// <summary>
@ -51,7 +36,8 @@ namespace Content.Server.Kitchen.Components
/// The cook times for all recipes should be divisible by 5,with a minimum of 1 second.
/// For right now, I don't think any recipe cook time should be greater than 60 seconds.
/// </summary>
[ViewVariables] private uint _currentCookTimerTime = 1;
[DataField("currentCookTimerTime"), ViewVariables(VVAccess.ReadWrite)]
public uint CurrentCookTimerTime = 5;
/// <summary>
/// The max temperature that this microwave can heat objects to.
@ -59,403 +45,9 @@ namespace Content.Server.Kitchen.Components
[DataField("temperatureUpperThreshold")]
public float TemperatureUpperThreshold = 373.15f;
public bool Powered => !_entities.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private bool HasContents => Storage.ContainedEntities.Count > 0;
public bool UIDirty = true;
private bool _lostPower;
private int _currentCookTimeButtonIndex;
public void DirtyUi()
{
UIDirty = true;
}
public int CurrentCookTimeButtonIndex;
public Container Storage = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(MicrowaveUiKey.Key);
protected override void Initialize()
{
base.Initialize();
_currentCookTimerTime = _cookTimeDefault;
Storage = ContainerHelpers.EnsureContainer<Container>(Owner, "microwave_entity_container",
out _);
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
}
}
public void SetCookTime(uint cookTime)
{
_currentCookTimerTime = cookTime;
UIDirty = true;
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
if (!Powered || _busy)
{
return;
}
switch (message.Message)
{
case MicrowaveStartCookMessage:
Wzhzhzh();
break;
case MicrowaveEjectMessage:
if (HasContents)
{
EjectSolids();
ClickSound();
UIDirty = true;
}
break;
case MicrowaveEjectSolidIndexedMessage msg:
if (HasContents)
{
EjectSolid(msg.EntityID);
ClickSound();
UIDirty = true;
}
break;
case MicrowaveSelectCookTimeMessage msg:
_currentCookTimeButtonIndex = msg.ButtonIndex;
_currentCookTimerTime = msg.NewCookTime;
ClickSound();
UIDirty = true;
break;
}
}
public void OnUpdate()
{
if (!Powered)
{
//TODO:If someone cuts power currently, microwave magically keeps going. FIX IT!
SetAppearance(MicrowaveVisualState.Idle);
}
if (_busy && !Powered)
{
//we lost power while we were cooking/busy!
_lostPower = true;
EjectSolids();
_busy = false;
UIDirty = true;
}
if (_busy && Broken)
{
SetAppearance(MicrowaveVisualState.Broken);
//we broke while we were cooking/busy!
_lostPower = true;
EjectSolids();
_busy = false;
UIDirty = true;
}
if (UIDirty)
{
UserInterface?.SetState(new MicrowaveUpdateUserInterfaceState
(
Storage.ContainedEntities.Select(item => item).ToArray(),
_busy,
_currentCookTimeButtonIndex,
_currentCookTimerTime
));
UIDirty = false;
}
}
public void SetAppearance(MicrowaveVisualState state)
{
var finalState = state;
if (Broken)
{
finalState = MicrowaveVisualState.Broken;
}
if (_entities.TryGetComponent(Owner, out AppearanceComponent? appearance))
{
appearance.SetData(PowerDeviceVisuals.VisualState, finalState);
}
}
// ReSharper disable once InconsistentNaming
// ReSharper disable once IdentifierTypo
public void Wzhzhzh()
{
if (!HasContents)
{
return;
}
_busy = true;
// Convert storage into Dictionary of ingredients
var solidsDict = new Dictionary<string, int>();
var reagentDict = new Dictionary<string, FixedPoint2>();
foreach (var item in Storage.ContainedEntities)
{
// special behavior when being microwaved ;)
var ev = new BeingMicrowavedEvent(Owner);
_entities.EventBus.RaiseLocalEvent(item, ev, false);
if (ev.Handled)
{
_busy = false;
UIDirty = true;
return;
}
var tagSys = EntitySystem.Get<TagSystem>();
if (tagSys.HasTag(item, "MicrowaveMachineUnsafe")
|| tagSys.HasTag(item, "Metal"))
{
// destroy microwave
Broken = true;
SetAppearance(MicrowaveVisualState.Broken);
SoundSystem.Play(ItemBreakSound.GetSound(), Filter.Pvs(Owner), Owner);
return;
}
if (tagSys.HasTag(item, "MicrowaveSelfUnsafe")
|| tagSys.HasTag(item, "Plastic"))
{
_entities.SpawnEntity(_badRecipeName,
_entities.GetComponent<TransformComponent>(Owner).Coordinates);
_entities.QueueDeleteEntity(item);
}
var metaData = _entities.GetComponent<MetaDataComponent>(item);
if (metaData.EntityPrototype == null)
{
continue;
}
if (solidsDict.ContainsKey(metaData.EntityPrototype.ID))
{
solidsDict[metaData.EntityPrototype.ID]++;
}
else
{
solidsDict.Add(metaData.EntityPrototype.ID, 1);
}
if (!_entities.TryGetComponent<SolutionContainerManagerComponent>(item, out var solMan))
continue;
foreach (var (_, solution) in solMan.Solutions)
{
foreach (var reagent in solution.Contents)
{
if (reagentDict.ContainsKey(reagent.ReagentId))
reagentDict[reagent.ReagentId] += reagent.Quantity;
else
reagentDict.Add(reagent.ReagentId, reagent.Quantity);
}
}
}
// Check recipes
(FoodRecipePrototype, int) portionedRecipe = _recipeManager.Recipes.Select(
r => CanSatisfyRecipe(r, solidsDict, reagentDict)).Where(r => r.Item2 > 0).FirstOrDefault();
FoodRecipePrototype? recipeToCook = portionedRecipe.Item1;
SetAppearance(MicrowaveVisualState.Cooking);
var time = _currentCookTimerTime * _cookTimeMultiplier;
SoundSystem.Play(_startCookingSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default);
Owner.SpawnTimer((int) (_currentCookTimerTime * _cookTimeMultiplier), () =>
{
if (_lostPower)
{
return;
}
AddTemperature(time);
if (recipeToCook != null)
{
for (int i = 0; i < portionedRecipe.Item2; i++)
{
SubtractContents(recipeToCook);
_entities.SpawnEntity(recipeToCook.Result,
_entities.GetComponent<TransformComponent>(Owner).Coordinates);
}
}
EjectSolids();
SoundSystem.Play(_cookingCompleteSound.GetSound(), Filter.Pvs(Owner),
Owner, AudioParams.Default.WithVolume(-1f));
SetAppearance(MicrowaveVisualState.Idle);
_busy = false;
UIDirty = true;
});
_lostPower = false;
UIDirty = true;
}
/// <summary>
/// Adds temperature to every item in the microwave,
/// based on the time it took to microwave.
/// </summary>
/// <param name="time">The time on the microwave, in seconds.</param>
public void AddTemperature(float time)
{
var solutionContainerSystem = EntitySystem.Get<SolutionContainerSystem>();
foreach (var entity in Storage.ContainedEntities)
{
if (_entities.TryGetComponent(entity, out TemperatureComponent? temp))
{
EntitySystem.Get<TemperatureSystem>().ChangeHeat(entity, time / 10, false, temp);
}
if (_entities.TryGetComponent(entity, out SolutionContainerManagerComponent? solutions))
{
foreach (var (_, solution) in solutions.Solutions)
{
if (solution.Temperature > TemperatureUpperThreshold)
continue;
solutionContainerSystem.AddThermalEnergy(entity, solution, time / 10);
}
}
}
}
private void EjectSolids()
{
for (var i = Storage.ContainedEntities.Count - 1; i >= 0; i--)
{
Storage.Remove(Storage.ContainedEntities.ElementAt(i));
}
}
private void EjectSolid(EntityUid entityId)
{
if (_entities.EntityExists(entityId))
{
Storage.Remove(entityId);
}
}
private void SubtractContents(FoodRecipePrototype recipe)
{
var totalReagentsToRemove = new Dictionary<string, FixedPoint2>(recipe.IngredientsReagents);
var solutionContainerSystem = EntitySystem.Get<SolutionContainerSystem>();
// this is spaghetti ngl
foreach (var item in Storage.ContainedEntities)
{
if (!_entities.TryGetComponent<SolutionContainerManagerComponent>(item, out var solMan))
continue;
// go over every solution
foreach (var (_, solution) in solMan.Solutions)
{
foreach (var (reagent, _) in recipe.IngredientsReagents)
{
// removed everything
if (!totalReagentsToRemove.ContainsKey(reagent))
continue;
if (!solution.ContainsReagent(reagent))
continue;
var quant = solution.GetReagentQuantity(reagent);
if (quant >= totalReagentsToRemove[reagent])
{
quant = totalReagentsToRemove[reagent];
totalReagentsToRemove.Remove(reagent);
}
else
{
totalReagentsToRemove[reagent] -= quant;
}
solutionContainerSystem.TryRemoveReagent(item, solution, reagent, quant);
}
}
}
foreach (var recipeSolid in recipe.IngredientsSolids)
{
for (var i = 0; i < recipeSolid.Value; i++)
{
foreach (var item in Storage.ContainedEntities)
{
var metaData = _entities.GetComponent<MetaDataComponent>(item);
if (metaData.EntityPrototype == null)
{
continue;
}
if (metaData.EntityPrototype.ID == recipeSolid.Key)
{
Storage.Remove(item);
_entities.DeleteEntity(item);
break;
}
}
}
}
}
private (FoodRecipePrototype, int) CanSatisfyRecipe(FoodRecipePrototype recipe, Dictionary<string, int> solids, Dictionary<string, FixedPoint2> reagents)
{
int portions = 0;
if(_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);
else
portions = portions == 0 ? solids[solid.Key] / solid.Value.Int()
: Math.Min(portions, solids[solid.Key] / solid.Value.Int());
}
foreach (var reagent in recipe.IngredientsReagents)
{
if (!reagents.ContainsKey(reagent.Key))
return (recipe, 0);
if (reagents[reagent.Key] < reagent.Value)
return (recipe, 0);
else
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, _currentCookTimerTime / recipe.CookTime));
}
public void ClickSound()
{
SoundSystem.Play(_clickSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default.WithVolume(-2f));
}
}
public sealed class BeingMicrowavedEvent : HandledEntityEventArgs

View file

@ -1,45 +1,187 @@
using System.Linq;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Construction;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Destructible;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Kitchen.Components;
using Robust.Shared.Player;
using JetBrains.Annotations;
using Content.Shared.Interaction.Events;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Popups;
using Content.Server.Hands.Systems;
using Content.Server.Power.Components;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Kitchen;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Tag;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
namespace Content.Server.Kitchen.EntitySystems
{
[UsedImplicitly]
internal sealed class MicrowaveSystem : EntitySystem
public sealed class MicrowaveSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly RecipeManager _recipeManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _sharedContainer = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly TemperatureSystem _temperature = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly HandsSystem _handsSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MicrowaveComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<MicrowaveComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<MicrowaveComponent, InteractUsingEvent>(OnInteractUsing, after: new[]{typeof(AnchorableSystem)});
SubscribeLocalEvent<MicrowaveComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<MicrowaveComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<MicrowaveComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<MicrowaveComponent, MicrowaveStartCookMessage>((u,c,_) => Wzhzhzh(u,c));
SubscribeLocalEvent<MicrowaveComponent, MicrowaveEjectMessage>(OnEjectMessage);
SubscribeLocalEvent<MicrowaveComponent, MicrowaveEjectSolidIndexedMessage>(OnEjectIndex);
SubscribeLocalEvent<MicrowaveComponent, MicrowaveSelectCookTimeMessage>(OnSelectTime);
SubscribeLocalEvent<ActiveMicrowaveComponent, ComponentStartup>(OnCookStart);
SubscribeLocalEvent<ActiveMicrowaveComponent, ComponentShutdown>(OnCookStop);
}
private void OnCookStart(EntityUid uid, ActiveMicrowaveComponent component, ComponentStartup args)
{
if (!TryComp<MicrowaveComponent>(uid, out var microwaveComponent))
return;
SetAppearance(microwaveComponent, MicrowaveVisualState.Cooking);
microwaveComponent.PlayingStream =
_audio.Play(microwaveComponent.LoopingSound, Filter.Pvs(uid), uid, AudioParams.Default.WithLoop(true).WithMaxDistance(5));
}
private void OnCookStop(EntityUid uid, ActiveMicrowaveComponent component, ComponentShutdown args)
{
if (!TryComp<MicrowaveComponent>(uid, out var microwaveComponent))
return;
SetAppearance(microwaveComponent, MicrowaveVisualState.Idle);
microwaveComponent.PlayingStream?.Stop();
}
/// <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)
{
foreach (var entity in component.Storage.ContainedEntities)
{
if (TryComp<TemperatureComponent>(entity, out var tempComp))
_temperature.ChangeHeat(entity, time / 10, false, tempComp);
if (!TryComp<SolutionContainerManagerComponent>(entity, out var solutions))
continue;
foreach (var (_, solution) in solutions.Solutions)
{
if (solution.Temperature > component.TemperatureUpperThreshold)
continue;
_solutionContainer.AddThermalEnergy(entity, solution, time / 10);
}
}
}
private void SubtractContents(MicrowaveComponent component, FoodRecipePrototype recipe)
{
var totalReagentsToRemove = new Dictionary<string, FixedPoint2>(recipe.IngredientsReagents);
// this is spaghetti ngl
foreach (var item in component.Storage.ContainedEntities)
{
if (!TryComp<SolutionContainerManagerComponent>(item, out var solMan))
continue;
// go over every solution
foreach (var (_, solution) in solMan.Solutions)
{
foreach (var (reagent, _) in recipe.IngredientsReagents)
{
// removed everything
if (!totalReagentsToRemove.ContainsKey(reagent))
continue;
if (!solution.ContainsReagent(reagent))
continue;
var quant = solution.GetReagentQuantity(reagent);
if (quant >= totalReagentsToRemove[reagent])
{
quant = totalReagentsToRemove[reagent];
totalReagentsToRemove.Remove(reagent);
}
else
{
totalReagentsToRemove[reagent] -= quant;
}
_solutionContainer.TryRemoveReagent(item, solution, reagent, quant);
}
}
}
foreach (var recipeSolid in recipe.IngredientsSolids)
{
for (var i = 0; i < recipeSolid.Value; i++)
{
foreach (var item in component.Storage.ContainedEntities)
{
var metaData = MetaData(item);
if (metaData.EntityPrototype == null)
{
continue;
}
if (metaData.EntityPrototype.ID == recipeSolid.Key)
{
component.Storage.Remove(item);
EntityManager.DeleteEntity(item);
break;
}
}
}
}
}
private void OnInit(EntityUid uid, MicrowaveComponent component, ComponentInit ags)
{
component.Storage = _container.EnsureContainer<Container>(uid,"microwave_entity_container");
}
private void OnSuicide(EntityUid uid, MicrowaveComponent component, SuicideEvent args)
{
if (args.Handled) return;
if (args.Handled)
return;
args.SetHandled(SuicideKind.Heat);
var victim = args.Victim;
var headCount = 0;
if (TryComp<SharedBodyComponent?>(victim, out var body))
if (TryComp<SharedBodyComponent>(victim, out var body))
{
var headSlots = body.GetSlotsOfType(BodyPartType.Head);
@ -69,36 +211,29 @@ namespace Content.Server.Kitchen.EntitySystems
? Loc.GetString("microwave-component-suicide-multi-head-others-message", ("victim", victim))
: Loc.GetString("microwave-component-suicide-others-message", ("victim", victim));
victim.PopupMessageOtherClients(othersMessage);
var selfMessage = headCount > 1
? Loc.GetString("microwave-component-suicide-multi-head-message")
: Loc.GetString("microwave-component-suicide-message");
victim.PopupMessage(selfMessage);
component.ClickSound();
component.SetCookTime(10);
component.Wzhzhzh();
_popupSystem.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim));
_popupSystem.PopupEntity(selfMessage, victim, Filter.Entities(victim));
_audio.PlayPvs(component.ClickSound, uid, AudioParams.Default.WithVolume(-2));
component.CurrentCookTimerTime = 10;
Wzhzhzh(uid, component);
UpdateUserInterfaceState(uid, component);
}
private void OnSolutionChange(EntityUid uid, MicrowaveComponent component, SolutionChangedEvent args)
{
component.DirtyUi();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in EntityManager.EntityQuery<MicrowaveComponent>())
{
comp.OnUpdate();
}
UpdateUserInterfaceState(uid, component);
}
private void OnInteractUsing(EntityUid uid, MicrowaveComponent component, InteractUsingEvent args)
{
if(args.Handled) return;
if (!component.Powered)
if(args.Handled)
return;
if (!(TryComp<ApcPowerReceiverComponent>(uid, out var apc) && apc.Powered))
{
_popupSystem.PopupEntity(Loc.GetString("microwave-component-interact-using-no-power"), uid, Filter.Entities(args.User));
return;
@ -118,13 +253,236 @@ namespace Content.Server.Kitchen.EntitySystems
args.Handled = true;
_handsSystem.TryDropIntoContainer(args.User, args.Used, component.Storage);
component.DirtyUi();
UpdateUserInterfaceState(uid, component);
}
private void OnBreak(EntityUid uid, MicrowaveComponent component, BreakageEventArgs args)
{
component.Broken = true;
component.SetAppearance(MicrowaveVisualState.Broken);
SetAppearance(component, MicrowaveVisualState.Broken);
RemComp<ActiveMicrowaveComponent>(uid);
_sharedContainer.EmptyContainer(component.Storage);
UpdateUserInterfaceState(uid, component);
}
private void OnPowerChanged(EntityUid uid, MicrowaveComponent component, PowerChangedEvent args)
{
if (!args.Powered)
{
SetAppearance(component, MicrowaveVisualState.Idle);
RemComp<ActiveMicrowaveComponent>(uid);
_sharedContainer.EmptyContainer(component.Storage);
}
UpdateUserInterfaceState(uid, component);
}
public void UpdateUserInterfaceState(EntityUid uid, MicrowaveComponent component)
{
var ui = _userInterface.GetUiOrNull(uid, MicrowaveUiKey.Key);
if (ui == null)
return;
var state = new MicrowaveUpdateUserInterfaceState(
component.Storage.ContainedEntities.ToArray(),
HasComp<ActiveMicrowaveComponent>(uid),
component.CurrentCookTimeButtonIndex,
component.CurrentCookTimerTime
);
_userInterface.SetUiState(ui, state);
}
public void SetAppearance(MicrowaveComponent component, MicrowaveVisualState state)
{
var display = component.Broken ? MicrowaveVisualState.Broken : state;
_appearance.SetData(component.Owner, PowerDeviceVisuals.VisualState, display);
}
public bool HasContents(MicrowaveComponent component)
{
return component.Storage.ContainedEntities.Any();
}
/// <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)
{
if (!HasContents(component) || HasComp<ActiveMicrowaveComponent>(uid))
return;
var solidsDict = new Dictionary<string, int>();
var reagentDict = new Dictionary<string, FixedPoint2>();
foreach (var item in component.Storage.ContainedEntities)
{
// special behavior when being microwaved ;)
var ev = new BeingMicrowavedEvent(uid);
RaiseLocalEvent(item, ev);
if (ev.Handled)
{
UpdateUserInterfaceState(uid, component);
return;
}
// destroy microwave
if (_tag.HasTag(item, "MicrowaveMachineUnsafe") || _tag.HasTag(item, "Metal"))
{
component.Broken = true;
SetAppearance(component, MicrowaveVisualState.Broken);
_audio.PlayPvs(component.ItemBreakSound, uid);
return;
}
if (_tag.HasTag(item, "MicrowaveSelfUnsafe") || _tag.HasTag(item, "Plastic"))
{
var junk = Spawn(component.BadRecipeEntityId, Transform(uid).Coordinates);
component.Storage.Insert(junk);
QueueDel(item);
}
var metaData = MetaData(item); //this simply begs for cooking refactor
if (metaData.EntityPrototype == null)
continue;
if (solidsDict.ContainsKey(metaData.EntityPrototype.ID))
{
solidsDict[metaData.EntityPrototype.ID]++;
}
else
{
solidsDict.Add(metaData.EntityPrototype.ID, 1);
}
if (!TryComp<SolutionContainerManagerComponent>(item, out var solMan))
continue;
foreach (var (_, solution) in solMan.Solutions)
{
foreach (var reagent in solution.Contents)
{
if (reagentDict.ContainsKey(reagent.ReagentId))
reagentDict[reagent.ReagentId] += reagent.Quantity;
else
reagentDict.Add(reagent.ReagentId, reagent.Quantity);
}
}
}
// Check recipes
var portionedRecipe = _recipeManager.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 * component.CookTimeMultiplier;
activeComp.PortionedRecipe = portionedRecipe;
UpdateUserInterfaceState(uid, component);
}
public (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)
{
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);
foreach (var (active, microwave) in EntityManager.EntityQuery<ActiveMicrowaveComponent, MicrowaveComponent>())
{
//check if there's still cook time left
active.CookTimeRemaining -= frameTime;
if (active.CookTimeRemaining > 0)
continue;
//this means the microwave has finished cooking.
AddTemperature(microwave, active.TotalTime);
if (active.PortionedRecipe.Item1 != null)
{
var coords = Transform(microwave.Owner).Coordinates;
for (var i = 0; i < active.PortionedRecipe.Item2; i++)
{
SubtractContents(microwave, active.PortionedRecipe.Item1);
Spawn(active.PortionedRecipe.Item1.Result, coords);
}
}
_sharedContainer.EmptyContainer(microwave.Storage);
UpdateUserInterfaceState(microwave.Owner, microwave);
EntityManager.RemoveComponentDeferred<ActiveMicrowaveComponent>(active.Owner);
_audio.PlayPvs(microwave.FoodDoneSound, microwave.Owner, AudioParams.Default.WithVolume(-1));
}
}
#region ui
private void OnEjectMessage(EntityUid uid, MicrowaveComponent component, MicrowaveEjectMessage args)
{
if (!HasContents(component) || HasComp<ActiveMicrowaveComponent>(uid))
return;
_sharedContainer.EmptyContainer(component.Storage);
_audio.PlayPvs(component.ClickSound, uid, AudioParams.Default.WithVolume(-2));
UpdateUserInterfaceState(uid, component);
}
private void OnEjectIndex(EntityUid uid, MicrowaveComponent component, MicrowaveEjectSolidIndexedMessage args)
{
if (!HasContents(component) || HasComp<ActiveMicrowaveComponent>(uid))
return;
component.Storage.Remove(args.EntityID);
UpdateUserInterfaceState(uid, component);
}
private void OnSelectTime(EntityUid uid, MicrowaveComponent component, MicrowaveSelectCookTimeMessage args)
{
if (!HasContents(component) || HasComp<ActiveMicrowaveComponent>(uid) || !(TryComp<ApcPowerReceiverComponent>(uid, out var apc) && apc.Powered))
return;
component.CurrentCookTimeButtonIndex = args.ButtonIndex;
component.CurrentCookTimerTime = args.NewCookTime;
_audio.PlayPvs(component.ClickSound, uid, AudioParams.Default.WithVolume(-2));
UpdateUserInterfaceState(uid, component);
}
#endregion
}
}

View file

@ -1,10 +1,7 @@
using Content.Shared.Lathe;
using Content.Shared.Research.Prototypes;
using Robust.Server.GameObjects;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Content.Shared.Materials;
using Robust.Shared.Audio;
namespace Content.Server.Lathe.Components
@ -12,20 +9,6 @@ namespace Content.Server.Lathe.Components
[RegisterComponent]
public sealed class LatheComponent : SharedLatheComponent
{
/// <summary>
/// Whitelist for specifying the kind of materials that can be insert into the lathe
/// </summary>
[ViewVariables]
[DataField("whitelist")]
public EntityWhitelist? LatheWhitelist;
/// <summary>
/// Whitelist generated on runtime for what items are specifically used for the lathe's recipes.
/// </summary>
[ViewVariables]
[DataField("materialWhiteList", customTypeSerializer: typeof(PrototypeIdListSerializer<MaterialPrototype>))]
public List<string> MaterialWhiteList = new();
/// <summary>
/// The lathe's construction queue
/// </summary>
@ -49,12 +32,6 @@ namespace Content.Server.Lathe.Components
/// </summary>
[DataField("producingSound")]
public SoundSpecifier? ProducingSound;
/// <summary>
/// The sound that plays when inserting an item into the lathe, if any
/// </summary>
[DataField("insertingSound")]
public SoundSpecifier? InsertingSound;
/// <summmary>
/// The lathe's UI.

View file

@ -1,4 +1,8 @@
using Content.Shared.Lathe;
using Content.Shared.Whitelist;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Content.Shared.Materials;
using Robust.Shared.Audio;
namespace Content.Server.Lathe.Components
{
@ -17,6 +21,26 @@ namespace Content.Server.Lathe.Components
[DataField("StorageLimit")]
private int _storageLimit = -1;
/// <summary>
/// Whitelist for specifying the kind of items that can be insert into this entity.
/// </summary>
[ViewVariables]
[DataField("whitelist")]
public EntityWhitelist? EntityWhitelist;
/// <summary>
/// Whitelist generated on runtime for what specific materials can be inserted into this entity.
/// </summary>
[ViewVariables]
[DataField("materialWhiteList", customTypeSerializer: typeof(PrototypeIdListSerializer<MaterialPrototype>))]
public List<string> MaterialWhiteList = new();
/// <summary>
/// The sound that plays when inserting an item into the storage
/// </summary>
[DataField("insertingSound")]
public SoundSpecifier? InsertingSound;
public override ComponentState GetComponentState()
{
return new MaterialStorageState(Storage);
@ -73,5 +97,14 @@ namespace Content.Server.Lathe.Components
{
return InsertMaterial(id, -amount);
}
// forgive me I needed to write a crumb of e/c code to not go fucking insane i swear i will ecs this entire shitty fucking system one day
public int GetMaterialAmount(string id)
{
if (!Storage.TryGetValue(id, out var amount))
return 0;
return amount;
}
}
}

View file

@ -32,7 +32,7 @@ namespace Content.Server.Lathe
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LatheComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<MaterialStorageComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<LatheComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<LatheComponent, LatheQueueRecipeMessage>(OnLatheQueueRecipeMessage);
SubscribeLocalEvent<LatheComponent, LatheSyncRequestMessage>(OnLatheSyncRequestMessage);
@ -85,35 +85,34 @@ namespace Content.Server.Lathe
if (recipes == null)
return;
if (!TryComp<MaterialStorageComponent>(uid, out var storage))
return;
foreach (var recipe in recipes)
{
foreach (var mat in recipe.RequiredMaterials)
{
if (!component.MaterialWhiteList.Contains(mat.Key))
component.MaterialWhiteList.Add(mat.Key);
if (!storage.MaterialWhiteList.Contains(mat.Key))
storage.MaterialWhiteList.Add(mat.Key);
}
}
}
/// <summary>
/// When someone tries to use an item on the lathe,
/// insert it if it's a stack and fits inside
/// </summary>
private void OnInteractUsing(EntityUid uid, LatheComponent component, InteractUsingEvent args)
private void OnInteractUsing(EntityUid uid, MaterialStorageComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
if (!TryComp<MaterialStorageComponent>(uid, out var storage)
|| !TryComp<MaterialComponent>(args.Used, out var material)
|| component.LatheWhitelist?.IsValid(args.Used) == false)
|| storage.EntityWhitelist?.IsValid(args.Used) == false)
return;
args.Handled = true;
var matUsed = false;
foreach (var mat in material.Materials)
if (component.MaterialWhiteList.Contains(mat.ID))
if (storage.MaterialWhiteList.Contains(mat.ID))
matUsed = true;
if (!matUsed)
@ -148,16 +147,25 @@ namespace Content.Server.Lathe
lastMat = mat;
}
EntityManager.QueueDeleteEntity(args.Used);
// Play a sound when inserting, if any
if (component.InsertingSound != null)
_audioSys.PlayPvs(component.InsertingSound, uid);
_popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid),
("item", args.Used)), uid, Filter.Entities(args.User));
// TODO: You can probably split this part off of lathe component too
if (!TryComp<LatheComponent>(uid, out var lathe))
return;
// We need the prototype to get the color
_prototypeManager.TryIndex(lastMat, out MaterialPrototype? matProto);
EntityManager.QueueDeleteEntity(args.Used);
EnsureComp<LatheInsertingComponent>(uid).TimeRemaining = component.InsertionTime;
EnsureComp<LatheInsertingComponent>(uid).TimeRemaining = lathe.InsertionTime;
_popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid),
("item", args.Used)), uid, Filter.Entities(args.User));

View file

@ -0,0 +1,6 @@
namespace Content.Server.Medical.BiomassReclaimer
{
[RegisterComponent]
public sealed class ActiveBiomassReclaimerComponent : Component
{}
}

View file

@ -0,0 +1,53 @@
using Content.Shared.Storage;
using System.Threading;
namespace Content.Server.Medical.BiomassReclaimer
{
[RegisterComponent]
public sealed class BiomassReclaimerComponent : Component
{
public CancellationTokenSource? CancelToken;
[DataField("accumulator")]
public float Accumulator = 0f;
[DataField("randomMessAccumulator")]
public float RandomMessAccumulator = 0f;
public TimeSpan RandomMessInterval = TimeSpan.FromSeconds(5);
/// <summary>
/// This gets set for each mob it processes.
/// When accumulator hits this, spit out biomass.
/// </summary>
public float CurrentProcessingTime = 70f;
/// <summary>
/// This is calculated from the YieldPerUnitMass
/// and adjusted for genetic damage too.
/// </summary>
public float CurrentExpectedYield = 28f;
public string BloodReagent = "Blood";
public List<EntitySpawnEntry> SpawnedEntities = new();
/// <summary>
/// How many units of biomass it produces for each unit of mass.
/// </summary>
[DataField("yieldPerUnitMass")]
public float YieldPerUnitMass = 0.4f;
/// <summary>
/// Lower number = faster processing.
/// Good for machine upgrading I guess.
/// </summmary>
public float ProcessingSpeedMultiplier = 0.5f;
/// <summary>
/// Will this refuse to gib a living mob?
/// </summary>
[DataField("safetyEnabled")]
public bool SafetyEnabled = true;
}
}

View file

@ -0,0 +1,245 @@
using System.Threading;
using Content.Shared.MobState.Components;
using Content.Shared.Interaction;
using Content.Shared.Audio;
using Content.Shared.Jittering;
using Content.Shared.Chemistry.Components;
using Content.Shared.Throwing;
using Content.Shared.Construction.Components;
using Content.Shared.Nutrition.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.CharacterAppearance.Components;
using Content.Server.MobState;
using Content.Server.Power.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Climbing;
using Content.Server.DoAfter;
using Content.Server.Mind.Components;
using Content.Server.Stack;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Configuration;
using Robust.Server.Player;
namespace Content.Server.Medical.BiomassReclaimer
{
public sealed class BiomassReclaimerSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedJitteringSystem _jitteringSystem = default!;
[Dependency] private readonly SharedAudioSystem _sharedAudioSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly SpillableSystem _spillableSystem = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var (_, reclaimer) in EntityQuery<ActiveBiomassReclaimerComponent, BiomassReclaimerComponent>())
{
reclaimer.Accumulator += frameTime;
reclaimer.RandomMessAccumulator += frameTime;
if (reclaimer.RandomMessAccumulator >= reclaimer.RandomMessInterval.TotalSeconds)
{
if (_robustRandom.Prob(0.3f))
{
if (_robustRandom.Prob(0.7f))
{
Solution blood = new();
blood.AddReagent(reclaimer.BloodReagent, 50);
_spillableSystem.SpillAt(reclaimer.Owner, blood, "PuddleBlood");
}
if (_robustRandom.Prob(0.1f) && reclaimer.SpawnedEntities.Count > 0)
{
var thrown = Spawn(_robustRandom.Pick(reclaimer.SpawnedEntities).PrototypeId, Transform(reclaimer.Owner).Coordinates);
Vector2 direction = (_robustRandom.Next(-30, 30), _robustRandom.Next(-30, 30));
_throwing.TryThrow(thrown, direction, _robustRandom.Next(1, 10));
}
}
reclaimer.RandomMessAccumulator -= (float) reclaimer.RandomMessInterval.TotalSeconds;
}
if (reclaimer.Accumulator < reclaimer.CurrentProcessingTime)
{
continue;
}
reclaimer.Accumulator = 0;
_stackSystem.SpawnMultiple((int) reclaimer.CurrentExpectedYield, 100, "Biomass", Transform(reclaimer.Owner).Coordinates);
reclaimer.SpawnedEntities.Clear();
RemCompDeferred<ActiveBiomassReclaimerComponent>(reclaimer.Owner);
}
}
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActiveBiomassReclaimerComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ActiveBiomassReclaimerComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ActiveBiomassReclaimerComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<BiomassReclaimerComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<BiomassReclaimerComponent, ClimbedOnEvent>(OnClimbedOn);
SubscribeLocalEvent<ReclaimSuccessfulEvent>(OnReclaimSuccessful);
SubscribeLocalEvent<ReclaimCancelledEvent>(OnReclaimCancelled);
}
private void OnInit(EntityUid uid, ActiveBiomassReclaimerComponent component, ComponentInit args)
{
_jitteringSystem.AddJitter(uid, -10, 100);
_sharedAudioSystem.Play("/Audio/Machines/reclaimer_startup.ogg", Filter.Pvs(uid), uid);
_ambientSoundSystem.SetAmbience(uid, true);
}
private void OnShutdown(EntityUid uid, ActiveBiomassReclaimerComponent component, ComponentShutdown args)
{
RemComp<JitteringComponent>(uid);
_ambientSoundSystem.SetAmbience(uid, false);
}
private void OnUnanchorAttempt(EntityUid uid, ActiveBiomassReclaimerComponent component, UnanchorAttemptEvent args)
{
args.Cancel();
}
private void OnAfterInteractUsing(EntityUid uid, BiomassReclaimerComponent component, AfterInteractUsingEvent args)
{
if (!args.CanReach)
return;
if (component.CancelToken != null || args.Target == null)
return;
if (HasComp<MobStateComponent>(args.Used) && CanGib(uid, args.Used, component))
{
component.CancelToken = new CancellationTokenSource();
_doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, 7f, component.CancelToken.Token, target: args.Target)
{
BroadcastFinishedEvent = new ReclaimSuccessfulEvent(args.User, args.Used, uid),
BroadcastCancelledEvent = new ReclaimCancelledEvent(uid),
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnStun = true,
NeedHand = true
});
}
}
private void OnClimbedOn(EntityUid uid, BiomassReclaimerComponent component, ClimbedOnEvent args)
{
if (!CanGib(uid, args.Climber, component))
{
Vector2 direction = (_robustRandom.Next(-2, 2), _robustRandom.Next(-2, 2));
_throwing.TryThrow(args.Climber, direction, 0.5f);
return;
}
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(args.Instigator):player} used a biomass reclaimer to gib {ToPrettyString(args.Climber):target} in {ToPrettyString(uid):reclaimer}");
StartProcessing(args.Climber, component);
}
private void OnReclaimSuccessful(ReclaimSuccessfulEvent args)
{
if (!TryComp<BiomassReclaimerComponent>(args.Reclaimer, out var reclaimer))
return;
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(args.User):player} used a biomass reclaimer to gib {ToPrettyString(args.Target):target} in {ToPrettyString(args.Reclaimer):reclaimer}");
reclaimer.CancelToken = null;
StartProcessing(args.Target, reclaimer);
}
private void OnReclaimCancelled(ReclaimCancelledEvent args)
{
if (!TryComp<BiomassReclaimerComponent>(args.Reclaimer, out var reclaimer))
return;
reclaimer.CancelToken = null;
}
private void StartProcessing(EntityUid toProcess, BiomassReclaimerComponent component)
{
AddComp<ActiveBiomassReclaimerComponent>(component.Owner);
if (TryComp<BloodstreamComponent>(toProcess, out var stream))
{
component.BloodReagent = stream.BloodReagent;
}
if (TryComp<SharedButcherableComponent>(toProcess, out var butcherableComponent))
{
component.SpawnedEntities = butcherableComponent.SpawnedEntities;
}
component.CurrentExpectedYield = CalculateYield(toProcess, component);
component.CurrentProcessingTime = component.CurrentExpectedYield / component.YieldPerUnitMass * component.ProcessingSpeedMultiplier;
EntityManager.QueueDeleteEntity(toProcess);
}
private float CalculateYield(EntityUid uid, BiomassReclaimerComponent component)
{
if (!TryComp<PhysicsComponent>(uid, out var physics))
{
Logger.Error("Somehow tried to extract biomass from " + uid + ", which has no physics component.");
return 0f;
}
return (physics.FixturesMass * component.YieldPerUnitMass);
}
private bool CanGib(EntityUid uid, EntityUid dragged, BiomassReclaimerComponent component)
{
if (HasComp<ActiveBiomassReclaimerComponent>(uid))
return false;
if (!HasComp<MobStateComponent>(dragged))
return false;
if (!Transform(uid).Anchored)
return false;
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
return false;
if (component.SafetyEnabled && !_mobState.IsDead(dragged))
return false;
// Reject souled bodies in easy mode.
if (_configManager.GetCVar(CCVars.BiomassEasyMode) && HasComp<HumanoidAppearanceComponent>(dragged) &&
TryComp<MindComponent>(dragged, out var mindComp))
{
if (mindComp.Mind?.UserId != null && _playerManager.TryGetSessionById(mindComp.Mind.UserId.Value, out var client))
return false;
}
return true;
}
private sealed class ReclaimCancelledEvent : EntityEventArgs
{
public EntityUid Reclaimer;
public ReclaimCancelledEvent(EntityUid reclaimer)
{
Reclaimer = reclaimer;
}
}
private sealed class ReclaimSuccessfulEvent : EntityEventArgs
{
public EntityUid User;
public EntityUid Target;
public EntityUid Reclaimer;
public ReclaimSuccessfulEvent(EntityUid user, EntityUid target, EntityUid reclaimer)
{
User = user;
Target = target;
Reclaimer = reclaimer;
}
}
}
}

View file

@ -75,9 +75,7 @@ namespace Content.Server.Medical.CrewMonitoring
// the monitor. But in the special case where the monitor IS a player (i.e., admin ghost), we base it off
// the players eye rotation. We don't know what that is for sure, but we know their last grid angle, which
// should work well enough?
if (TryComp(uid, out InputMoverComponent? mover))
worldRot = mover.LastGridAngle;
else if (_mapManager.TryGetGrid(xform.GridUid, out var grid))
if (_mapManager.TryGetGrid(xform.GridUid, out var grid))
worldRot = grid.WorldRotation;
// update all sensors info

View file

@ -48,7 +48,7 @@ namespace Content.Server.Medical
private void OnComponentInit(EntityUid uid, MedicalScannerComponent scannerComponent, ComponentInit args)
{
base.Initialize();
scannerComponent.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, $"{scannerComponent.Name}-bodyContainer");
scannerComponent.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, $"scanner-bodyContainer");
_signalSystem.EnsureReceiverPorts(uid, MedicalScannerComponent.ScannerPort);
}
@ -94,6 +94,7 @@ namespace Content.Server.Medical
verb.Act = () => EjectBody(uid, component);
verb.Category = VerbCategory.Eject;
verb.Text = Loc.GetString("medical-scanner-verb-noun-occupant");
verb.Priority = 1; // Promote to top to make ejecting the ALT-click action
args.Verbs.Add(verb);
}

View file

@ -58,15 +58,6 @@ public sealed class RenameCommand : IConsoleCommand
{
if (idCardSystem.TryFindIdCard(entityUid, out var idCard))
idCardSystem.TryChangeFullName(idCard.Owner, name, idCard);
else
{
foreach (var idCardComponent in entMan.EntityQuery<IdCardComponent>())
{
if (idCardComponent.OriginalEntityName != oldName)
continue;
idCardSystem.TryChangeFullName(idCardComponent.Owner, name, idCardComponent);
}
}
}
// PDAs

View file

@ -0,0 +1,31 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Movement.Systems;
using Robust.Shared.Console;
namespace Content.Server.Movement;
[AdminCommand(AdminFlags.Fun)]
public sealed class LockEyesCommand : IConsoleCommand
{
public string Command => $"lockeyes";
public string Description => Loc.GetString("lockeyes-command-description");
public string Help => Loc.GetString("lockeyes-command-help");
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
if (!bool.TryParse(args[0], out var value))
{
shell.WriteError(Loc.GetString("parse-bool-fail", ("args", args[0])));
return;
}
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedMoverController>();
system.CameraRotationLocked = value;
}
}

View file

@ -0,0 +1,44 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Movement.Components;
using Robust.Shared.Console;
namespace Content.Server.Movement;
[AdminCommand(AdminFlags.Fun)]
public sealed class RotateEyesCommand : IConsoleCommand
{
public string Command => "rotateeyes";
public string Description => Loc.GetString("rotateeyes-command-description");
public string Help => Loc.GetString("rotateeyes-command-help");
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var entManager = IoCManager.Resolve<IEntityManager>();
var rotation = Angle.Zero;
if (args.Length == 1)
{
if (!float.TryParse(args[0], out var degrees))
{
shell.WriteError(Loc.GetString("parse-float-fail", ("arg", args[0])));
return;
}
rotation = Angle.FromDegrees(degrees);
}
var count = 0;
foreach (var mover in entManager.EntityQuery<InputMoverComponent>(true))
{
if (mover.TargetRelativeRotation.Equals(rotation))
continue;
mover.TargetRelativeRotation = rotation;
entManager.Dirty(mover);
count++;
}
shell.WriteLine(Loc.GetString("rotateeyes-command-count", ("count", count)));
}
}

View file

@ -28,10 +28,23 @@ namespace Content.Server.Physics.Controllers
var bodyQuery = GetEntityQuery<PhysicsComponent>();
var relayQuery = GetEntityQuery<RelayInputMoverComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var moverQuery = GetEntityQuery<InputMoverComponent>();
foreach (var (mover, xform) in EntityQuery<InputMoverComponent, TransformComponent>(true))
foreach (var mover in EntityQuery<InputMoverComponent>(true))
{
if (relayQuery.TryGetComponent(mover.Owner, out var relayed) && relayed.RelayEntity != null)
{
if (moverQuery.TryGetComponent(relayed.RelayEntity, out var relayMover))
{
relayMover.RelativeEntity = mover.RelativeEntity;
relayMover.RelativeRotation = mover.RelativeRotation;
relayMover.TargetRelativeRotation = mover.TargetRelativeRotation;
continue;
}
}
if (!xformQuery.TryGetComponent(mover.Owner, out var xform))
{
continue;
}
@ -46,18 +59,13 @@ namespace Content.Server.Physics.Controllers
{
continue;
}
if (TryComp<InputMoverComponent>(xform.ParentUid, out var parentMover))
{
mover.LastGridAngle = parentMover.LastGridAngle;
}
}
else if (!bodyQuery.TryGetComponent(mover.Owner, out body))
{
continue;
}
HandleMobMovement(mover, body, xformMover, frameTime);
HandleMobMovement(mover, body, xformMover, frameTime, xformQuery);
}
HandleShuttleMovement(frameTime);

View file

@ -1,103 +0,0 @@
using Content.Shared.Radiation;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Radiation
{
[RegisterComponent]
[ComponentReference(typeof(SharedRadiationPulseComponent))]
public sealed class RadiationPulseComponent : SharedRadiationPulseComponent
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private float _duration;
private float _range = 5f;
private TimeSpan _startTime;
private TimeSpan _endTime;
private bool _draw = true;
private bool _decay = true;
/// <summary>
/// Whether the entity will delete itself after a certain duration defined by
/// <see cref="MinPulseLifespan"/> and <see cref="MaxPulseLifespan"/>
/// </summary>
[DataField("decay")]
public override bool Decay
{
get => _decay;
set
{
_decay = value;
Dirty();
}
}
[DataField("minPulseLifespan")]
public float MinPulseLifespan { get; set; } = 0.8f;
[DataField("maxPulseLifespan")]
public float MaxPulseLifespan { get; set; } = 2.5f;
[DataField("sound")] public SoundSpecifier Sound { get; set; } = new SoundCollectionSpecifier("RadiationPulse");
[DataField("range")]
public override float Range
{
get => _range;
set
{
_range = value;
Dirty();
}
}
[DataField("draw")]
public override bool Draw
{
get => _draw;
set
{
_draw = value;
Dirty();
}
}
public override TimeSpan StartTime => _startTime;
public override TimeSpan EndTime => _endTime;
public void DoPulse()
{
if (Decay)
{
var currentTime = _gameTiming.CurTime;
_startTime = currentTime;
_duration = _random.NextFloat() * (MaxPulseLifespan - MinPulseLifespan) + MinPulseLifespan;
_endTime = currentTime + TimeSpan.FromSeconds(_duration);
}
SoundSystem.Play(Sound.GetSound(), Filter.Pvs(Owner), Owner);
Dirty();
}
public override ComponentState GetComponentState()
{
return new RadiationPulseState(_range, Draw, Decay, _startTime, _endTime);
}
public void Update(float frameTime)
{
if (!Decay || _entMan.Deleted(Owner))
return;
if (_duration <= 0f)
_entMan.QueueDeleteEntity(Owner);
_duration -= frameTime;
}
}
}

View file

@ -1,38 +0,0 @@
using Content.Server.Radiation.Systems;
using JetBrains.Annotations;
namespace Content.Server.Radiation
{
[UsedImplicitly]
public sealed class RadiationPulseSystem : EntitySystem
{
[Dependency] private readonly RadiationSystem _radiation = default!;
private const float RadiationCooldown = 1.0f;
private float _accumulator;
public override void Update(float frameTime)
{
base.Update(frameTime);
_accumulator += frameTime;
while (_accumulator > RadiationCooldown)
{
_accumulator -= RadiationCooldown;
// All code here runs effectively every RadiationCooldown seconds, so use that as the "frame time".
foreach (var comp in EntityManager.EntityQuery<RadiationPulseComponent>())
{
comp.Update(RadiationCooldown);
var ent = comp.Owner;
if (Deleted(ent)) continue;
var cords = Transform(ent).MapPosition;
_radiation.IrradiateRange(cords, comp.Range, comp.RadsPerSecond, RadiationCooldown);
}
}
}
}
}

View file

@ -7,6 +7,31 @@ public sealed class RadiationSystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
private const float RadiationCooldown = 1.0f;
private float _accumulator;
public override void Update(float frameTime)
{
base.Update(frameTime);
_accumulator += frameTime;
while (_accumulator > RadiationCooldown)
{
_accumulator -= RadiationCooldown;
// All code here runs effectively every RadiationCooldown seconds, so use that as the "frame time".
foreach (var comp in EntityManager.EntityQuery<RadiationSourceComponent>())
{
var ent = comp.Owner;
if (Deleted(ent))
continue;
var cords = Transform(ent).MapPosition;
IrradiateRange(cords, comp.Range, comp.RadsPerSecond, RadiationCooldown);
}
}
}
public void IrradiateRange(MapCoordinates coordinates, float range, float radsPerSecond, float time)
{
var lookUp = _lookup.GetEntitiesInRange(coordinates, range);

View file

@ -1,11 +1,13 @@
using Content.Shared.Damage;
using Content.Server.Body.Components;
using Content.Server.MobState;
namespace Content.Server.Salvage;
public sealed class SalvageMobRestrictionsSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
public override void Initialize()
{
base.Initialize();
@ -49,6 +51,7 @@ public sealed class SalvageMobRestrictionsSystem : EntitySystem
foreach (var target in component.MobsToKill)
{
if (Deleted(target, metaQuery)) continue;
if (_mobStateSystem.IsDead(target)) continue; // DONT WASTE BIOMASS
if (bodyQuery.TryGetComponent(target, out var body))
{
// Just because.

View file

@ -12,6 +12,6 @@ namespace Content.Server.Sound.Components
[ViewVariables(VVAccess.ReadWrite)]
[DataField("sound", required: true)]
public SoundSpecifier Sound { get; set; } = default!;
public SoundSpecifier? Sound;
}
}

View file

@ -0,0 +1,9 @@
namespace Content.Server.Sound.Components;
/// <summary>
/// Simple sound emitter that emits sound on entity spawn.
/// </summary>
[RegisterComponent]
public sealed class EmitSoundOnSpawnComponent : BaseEmitSoundComponent
{
}

View file

@ -54,6 +54,7 @@ namespace Content.Server.Sound
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmitSoundOnSpawnComponent, ComponentInit>(HandleEmitSpawnOnInit);
SubscribeLocalEvent<EmitSoundOnLandComponent, LandEvent>(HandleEmitSoundOnLand);
SubscribeLocalEvent<EmitSoundOnUseComponent, UseInHandEvent>(HandleEmitSoundOnUseInHand);
SubscribeLocalEvent<EmitSoundOnThrowComponent, ThrownEvent>(HandleEmitSoundOnThrown);
@ -64,6 +65,11 @@ namespace Content.Server.Sound
SubscribeLocalEvent<EmitSoundOnDropComponent, DroppedEvent>(HandleEmitSoundOnDrop);
}
private void HandleEmitSpawnOnInit(EntityUid uid, EmitSoundOnSpawnComponent component, ComponentInit args)
{
TryEmitSound(component);
}
private void HandleEmitSoundOnTrigger(EntityUid uid, EmitSoundOnTriggerComponent component, TriggerEvent args)
{
TryEmitSound(component);
@ -122,6 +128,8 @@ namespace Content.Server.Sound
private void TryEmitSound(BaseEmitSoundComponent component)
{
if (component.Sound == null)
return;
_audioSystem.PlayPvs(component.Sound, component.Owner, component.Sound.Params.AddVolume(-2f));
}
}

View file

@ -83,6 +83,44 @@ namespace Content.Server.Stack
return entity;
}
/// <summary>
/// Say you want to spawn 97 stacks of something that has a max stack count of 30.
/// This would spawn 3 stacks of 30 and 1 stack of 7.
/// </summary>
public void SpawnMultiple(int amount, int maxCountPerStack, StackPrototype prototype, EntityCoordinates spawnPosition)
{
while (amount > 0)
{
if (amount > maxCountPerStack)
{
var entity = Spawn("MaterialBiomass", spawnPosition);
var stack = Comp<StackComponent>(entity);
SetCount(entity, maxCountPerStack, stack);
amount -= maxCountPerStack;
}
else
{
var entity = Spawn("MaterialBiomass", spawnPosition);
var stack = Comp<StackComponent>(entity);
SetCount(entity, amount, stack);
amount = 0;
}
}
}
public void SpawnMultiple(int amount, int maxCountPerStack, string prototype, EntityCoordinates spawnPosition)
{
if (!_prototypeManager.TryIndex<StackPrototype>(prototype, out var stackType))
{
Logger.Error("Failed to index stack prototype " + prototype);
return;
}
SpawnMultiple(amount, maxCountPerStack, stackType, spawnPosition);
}
private void OnStackAlternativeInteract(EntityUid uid, StackComponent stack, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands == null)

View file

@ -119,7 +119,8 @@ public sealed class StationSystem : EntitySystem
private void OnStationDeleted(EntityUid uid, StationDataComponent component, ComponentShutdown args)
{
if (_stations.Contains(uid) && // Was not deleted via DeleteStation()
_gameTicker.RunLevel == GameRunLevel.InRound) // And not due to a round restart
_gameTicker.RunLevel == GameRunLevel.InRound && // And not due to a round restart
_gameTicker.LobbyEnabled) // If there isn't a lobby, this is probably sandbox, single player, or a test
{
// printing a stack trace, rather than throwing an exception so that entity deletion continues as normal.
Logger.Error($"Station entity {ToPrettyString(uid)} is getting deleted mid-round. Trace: {Environment.StackTrace}");

View file

@ -1,41 +0,0 @@
using Content.Shared.MobState.Components;
using Content.Server.Zombies;
namespace Content.Server.StationEvents.Events
{
/// <summary>
/// Revives several dead entities as zombies
/// </summary>
public sealed class ZombieOutbreak : StationEventSystem
{
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
public override string Prototype => "ZombieOutbreak";
/// <summary>
/// Finds 1-3 random, dead entities across the station
/// and turns them into zombies.
/// </summary>
public override void Started()
{
base.Started();
List<MobStateComponent> deadList = new();
foreach (var mobState in EntityManager.EntityQuery<MobStateComponent>())
{
if (mobState.IsDead() || mobState.IsCritical())
deadList.Add(mobState);
}
RobustRandom.Shuffle(deadList);
var toInfect = RobustRandom.Next(1, 3);
foreach (var target in deadList)
{
if (toInfect-- == 0)
break;
_zombify.ZombifyEntity(target.Owner);
}
}
}
}

View file

@ -48,6 +48,7 @@ namespace Content.Server.Storage.EntitySystems
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
/// <inheritdoc />
public override void Initialize()
@ -67,12 +68,64 @@ namespace Content.Server.Storage.EntitySystems
SubscribeLocalEvent<ServerStorageComponent, BoundUIClosedEvent>(OnBoundUIClosed);
SubscribeLocalEvent<ServerStorageComponent, EntRemovedFromContainerMessage>(OnStorageItemRemoved);
SubscribeLocalEvent<ServerStorageComponent, AreaPickupCompleteEvent>(OnAreaPickupComplete);
SubscribeLocalEvent<ServerStorageComponent, AreaPickupCancelledEvent>(OnAreaPickupCancelled);
SubscribeLocalEvent<EntityStorageComponent, GetVerbsEvent<InteractionVerb>>(AddToggleOpenVerb);
SubscribeLocalEvent<EntityStorageComponent, ContainerRelayMovementEntityEvent>(OnRelayMovement);
SubscribeLocalEvent<StorageFillComponent, MapInitEvent>(OnStorageFillMapInit);
}
private void OnAreaPickupCancelled(EntityUid uid, ServerStorageComponent component, AreaPickupCancelledEvent args)
{
component.CancelToken = null;
}
private void OnAreaPickupComplete(EntityUid uid, ServerStorageComponent component, AreaPickupCompleteEvent args)
{
component.CancelToken = null;
var successfullyInserted = new List<EntityUid>();
var successfullyInsertedPositions = new List<EntityCoordinates>();
var itemQuery = GetEntityQuery<ItemComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
xformQuery.TryGetComponent(uid, out var xform);
foreach (var entity in args.ValidStorables)
{
// Check again, situation may have changed for some entities, but we'll still pick up any that are valid
if (_containerSystem.IsEntityInContainer(entity)
|| entity == args.User
|| !itemQuery.HasComponent(entity))
continue;
if (xform == null ||
!xformQuery.TryGetComponent(entity, out var targetXform) ||
targetXform.MapID != xform.MapID)
{
continue;
}
var position = EntityCoordinates.FromMap(
xform.ParentUid.IsValid() ? xform.ParentUid : uid,
new MapCoordinates(_transform.GetWorldPosition(targetXform, xformQuery),
targetXform.MapID), EntityManager);
if (PlayerInsertEntityInWorld(uid, args.User, entity, component))
{
successfullyInserted.Add(entity);
successfullyInsertedPositions.Add(position);
}
}
// If we picked up atleast one thing, play a sound and do a cool animation!
if (successfullyInserted.Count > 0)
{
_audio.PlayPvs(component.StorageInsertSound, uid);
RaiseNetworkEvent(new AnimateInsertingEntitiesEvent(uid, successfullyInserted, successfullyInsertedPositions));
}
}
private void OnComponentInit(EntityUid uid, ServerStorageComponent storageComp, ComponentInit args)
{
base.Initialize();
@ -130,14 +183,16 @@ namespace Content.Server.Storage.EntitySystems
return;
// Get the session for the user
if (!TryComp<ActorComponent>(args.User, out var actor) || actor?.PlayerSession == null)
if (!TryComp<ActorComponent>(args.User, out var actor))
return;
// Does this player currently have the storage UI open?
bool uiOpen = _uiSystem.SessionHasOpenUi(uid, StorageUiKey.Key, actor.PlayerSession);
ActivationVerb verb = new();
verb.Act = () => OpenStorageUI(uid, args.User, component);
ActivationVerb verb = new()
{
Act = () => OpenStorageUI(uid, args.User, component)
};
if (uiOpen)
{
verb.Text = Loc.GetString("verb-common-close-ui");
@ -178,8 +233,6 @@ namespace Content.Server.Storage.EntitySystems
}
}
/// <summary>
/// Inserts storable entities into this storage container if possible, otherwise return to the hand of the user
/// </summary>
@ -210,9 +263,6 @@ namespace Content.Server.Storage.EntitySystems
/// <returns></returns>
private void OnActivate(EntityUid uid, ServerStorageComponent storageComp, ActivateInWorldEvent args)
{
if (!TryComp<ActorComponent>(args.User, out var actor))
return;
if (TryComp(uid, out LockComponent? lockComponent) && lockComponent.Locked)
return;
@ -224,27 +274,27 @@ namespace Content.Server.Storage.EntitySystems
/// around a click.
/// </summary>
/// <returns></returns>
private async void AfterInteract(EntityUid uid, ServerStorageComponent storageComp, AfterInteractEvent eventArgs)
private async void AfterInteract(EntityUid uid, ServerStorageComponent storageComp, AfterInteractEvent args)
{
if (!eventArgs.CanReach) return;
if (!args.CanReach) return;
if (storageComp.CancelToken != null)
{
storageComp.CancelToken.Cancel();
storageComp.CancelToken = null;
return;
}
// Pick up all entities in a radius around the clicked location.
// The last half of the if is because carpets exist and this is terrible
if (storageComp.AreaInsert && (eventArgs.Target == null || !HasComp<ItemComponent>(eventArgs.Target.Value)))
if (storageComp.AreaInsert && (args.Target == null || !HasComp<ItemComponent>(args.Target.Value)))
{
var validStorables = new List<EntityUid>();
foreach (var entity in _entityLookupSystem.GetEntitiesInRange(eventArgs.ClickLocation, storageComp.AreaInsertRadius, LookupFlags.None))
var itemQuery = GetEntityQuery<ItemComponent>();
foreach (var entity in _entityLookupSystem.GetEntitiesInRange(args.ClickLocation, storageComp.AreaInsertRadius, LookupFlags.None))
{
if (entity == eventArgs.User
|| !HasComp<ItemComponent>(entity)
|| !_interactionSystem.InRangeUnobstructed(eventArgs.User, entity))
if (entity == args.User
|| !itemQuery.HasComponent(entity)
|| !_interactionSystem.InRangeUnobstructed(args.User, entity))
continue;
validStorables.Add(entity);
@ -254,57 +304,30 @@ namespace Content.Server.Storage.EntitySystems
if (validStorables.Count > 1)
{
storageComp.CancelToken = new CancellationTokenSource();
var doAfterArgs = new DoAfterEventArgs(eventArgs.User, 0.2f * validStorables.Count, storageComp.CancelToken.Token, uid)
var doAfterArgs = new DoAfterEventArgs(args.User, 0.2f * validStorables.Count, storageComp.CancelToken.Token, target: uid)
{
BreakOnStun = true,
BreakOnDamage = true,
BreakOnUserMove = true,
NeedHand = true,
TargetCancelledEvent = new AreaPickupCancelledEvent(),
TargetFinishedEvent = new AreaPickupCompleteEvent(args.User, validStorables),
};
await _doAfterSystem.WaitDoAfter(doAfterArgs);
_doAfterSystem.DoAfter(doAfterArgs);
}
// TODO: Make it use the event DoAfter
var successfullyInserted = new List<EntityUid>();
var successfullyInsertedPositions = new List<EntityCoordinates>();
foreach (var entity in validStorables)
{
// Check again, situation may have changed for some entities, but we'll still pick up any that are valid
if (_containerSystem.IsEntityInContainer(entity)
|| entity == eventArgs.User
|| !HasComp<ItemComponent>(entity))
continue;
if (TryComp<TransformComponent>(uid, out var transformOwner) && TryComp<TransformComponent>(entity, out var transformEnt))
{
var position = EntityCoordinates.FromMap(transformOwner.Parent?.Owner ?? uid, transformEnt.MapPosition);
if (PlayerInsertEntityInWorld(uid, eventArgs.User, entity, storageComp))
{
successfullyInserted.Add(entity);
successfullyInsertedPositions.Add(position);
}
}
}
// If we picked up atleast one thing, play a sound and do a cool animation!
if (successfullyInserted.Count > 0)
{
if (storageComp.StorageInsertSound is not null)
SoundSystem.Play(storageComp.StorageInsertSound.GetSound(), Filter.Pvs(uid, entityManager: EntityManager), uid, AudioParams.Default);
RaiseNetworkEvent(new AnimateInsertingEntitiesEvent(uid, successfullyInserted, successfullyInsertedPositions));
}
return;
}
// Pick up the clicked entity
else if (storageComp.QuickInsert)
if (storageComp.QuickInsert)
{
if (eventArgs.Target is not {Valid: true} target)
if (args.Target is not {Valid: true} target)
return;
if (_containerSystem.IsEntityInContainer(target)
|| target == eventArgs.User
|| target == args.User
|| !HasComp<ItemComponent>(target))
return;
@ -313,9 +336,10 @@ namespace Content.Server.Storage.EntitySystems
var parent = transformOwner.ParentUid;
var position = EntityCoordinates.FromMap(
parent.IsValid() ? parent : uid,
transformEnt.MapPosition);
if (PlayerInsertEntityInWorld(uid, eventArgs.User, target, storageComp))
parent.IsValid() ? parent : uid,
transformEnt.MapPosition);
if (PlayerInsertEntityInWorld(uid, args.User, target, storageComp))
{
RaiseNetworkEvent(new AnimateInsertingEntitiesEvent(uid,
new List<EntityUid> { target },
@ -323,7 +347,6 @@ namespace Content.Server.Storage.EntitySystems
}
}
}
return;
}
private void OnDestroy(EntityUid uid, ServerStorageComponent storageComp, DestructionEventArgs args)
@ -679,5 +702,25 @@ namespace Content.Server.Storage.EntitySystems
_popupSystem.PopupEntity(Loc.GetString(message), player, Filter.Entities(player));
}
/// <summary>
/// Raised on storage if it successfully completes area pickup.
/// </summary>
private sealed class AreaPickupCompleteEvent : EntityEventArgs
{
public EntityUid User;
public List<EntityUid> ValidStorables;
public AreaPickupCompleteEvent(EntityUid user, List<EntityUid> validStorables)
{
User = user;
ValidStorables = validStorables;
}
}
private sealed class AreaPickupCancelledEvent : EntityEventArgs
{
}
}
}

View file

@ -0,0 +1,30 @@
using System.Threading;
using Content.Shared.Tools;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Tools.Components;
[RegisterComponent]
public sealed class LatticeCuttingComponent : Component
{
[ViewVariables]
[DataField("toolComponentNeeded")]
public bool ToolComponentNeeded = true;
[ViewVariables]
[DataField("qualityNeeded", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string QualityNeeded = "Cutting";
[ViewVariables]
[DataField("delay")]
public float Delay = 0.25f;
[ViewVariables]
[DataField("vacuumDelay")]
public float VacuumDelay = 1.75f;
/// <summary>
/// Used for do_afters.
/// </summary>
public CancellationTokenSource? CancelTokenSource = null;
}

View file

@ -0,0 +1,101 @@
using System.Threading;
using Content.Server.Tools.Components;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Tools.Components;
using Robust.Shared.Map;
namespace Content.Server.Tools;
public sealed partial class ToolSystem
{
private void InitializeLatticeCutting()
{
SubscribeLocalEvent<LatticeCuttingComponent, AfterInteractEvent>(OnLatticeCuttingAfterInteract);
SubscribeLocalEvent<LatticeCuttingComponent, LatticeCuttingCompleteEvent>(OnLatticeCutComplete);
SubscribeLocalEvent<LatticeCuttingComponent, LatticeCuttingCancelledEvent>(OnLatticeCutCancelled);
}
private void OnLatticeCutCancelled(EntityUid uid, LatticeCuttingComponent component, LatticeCuttingCancelledEvent args)
{
component.CancelTokenSource = null;
}
private void OnLatticeCutComplete(EntityUid uid, LatticeCuttingComponent component, LatticeCuttingCompleteEvent args)
{
component.CancelTokenSource = null;
var gridUid = args.Coordinates.GetGridUid(EntityManager);
if (gridUid == null)
return;
var grid = _mapManager.GetGrid(gridUid.Value);
var tile = grid.GetTileRef(args.Coordinates);
if (_tileDefinitionManager[tile.Tile.TypeId] is not ContentTileDefinition tileDef
|| !tileDef.CanWirecutter
|| tileDef.BaseTurfs.Count == 0
|| tile.IsBlockedTurf(true))
return;
tile.CutTile(_mapManager, _tileDefinitionManager, EntityManager);
}
private void OnLatticeCuttingAfterInteract(EntityUid uid, LatticeCuttingComponent component,
AfterInteractEvent args)
{
if (args.Handled || !args.CanReach || args.Target != null)
return;
if (TryCut(args.User, component, args.ClickLocation))
args.Handled = true;
}
private bool TryCut(EntityUid user, LatticeCuttingComponent component, EntityCoordinates clickLocation)
{
if (component.CancelTokenSource != null)
return true;
ToolComponent? tool = null;
if (component.ToolComponentNeeded && !TryComp<ToolComponent?>(component.Owner, out tool))
return false;
if (!_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid))
return false;
var tile = mapGrid.GetTileRef(clickLocation);
var coordinates = mapGrid.GridTileToLocal(tile.GridIndices);
if (!_interactionSystem.InRangeUnobstructed(user, coordinates, popup: false))
return false;
if (_tileDefinitionManager[tile.Tile.TypeId] is not ContentTileDefinition tileDef
|| !tileDef.CanWirecutter
|| tileDef.BaseTurfs.Count == 0
|| _tileDefinitionManager[tileDef.BaseTurfs[^1]] is not ContentTileDefinition newDef
|| tile.IsBlockedTurf(true))
return false;
var tokenSource = new CancellationTokenSource();
component.CancelTokenSource = tokenSource;
if (!UseTool(component.Owner, user, null, 0f, component.Delay, new[] {component.QualityNeeded},
new LatticeCuttingCompleteEvent
{
Coordinates = clickLocation
}, new LatticeCuttingCancelledEvent(), toolComponent: tool, doAfterEventTarget: component.Owner,
cancelToken: tokenSource.Token))
component.CancelTokenSource = null;
return true;
}
private sealed class LatticeCuttingCompleteEvent : EntityEventArgs
{
public EntityCoordinates Coordinates { get; set; }
}
private sealed class LatticeCuttingCancelledEvent : EntityEventArgs
{
}
}

View file

@ -32,6 +32,7 @@ namespace Content.Server.Tools
base.Initialize();
InitializeTilePrying();
InitializeLatticeCutting();
InitializeWelders();
SubscribeLocalEvent<ToolDoAfterComplete>(OnDoAfterComplete);

View file

@ -1,7 +1,5 @@
using Content.Server.UserInterface;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.VendingMachines;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@ -9,9 +7,14 @@ namespace Content.Server.VendingMachines
{
[RegisterComponent]
[ComponentReference(typeof(SharedVendingMachineComponent))]
[Access(typeof(VendingMachineSystem))]
public sealed class VendingMachineComponent : SharedVendingMachineComponent
{
public bool Ejecting;
public bool Denying;
public bool DispenseOnHitCoolingDown;
public string? NextItemToEject;
public bool Broken;
@ -21,6 +24,8 @@ namespace Content.Server.VendingMachines
[DataField("speedLimiter")]
public bool CanShoot = false;
public bool ThrowNextItem = false;
/// <summary>
/// The chance that a vending machine will randomly dispense an item on hit.
/// Chance is 0 if null.
@ -28,24 +33,48 @@ namespace Content.Server.VendingMachines
[DataField("dispenseOnHitChance")]
public float? DispenseOnHitChance;
/// <summary>
/// The minimum amount of damage that must be done per hit to have a chance
/// of dispensing an item.
/// </summary>
[DataField("dispenseOnHitThreshold")]
public float? DispenseOnHitThreshold;
/// <summary>
/// Amount of time in seconds that need to pass before damage can cause a vending machine to eject again.
/// This value is separate to <see cref="SharedVendingMachineComponent.EjectDelay"/> because that value might be
/// 0 for a vending machine for legitimate reasons (no desired delay/no eject animation)
/// and can be circumvented with forced ejections.
/// </summary>
[DataField("dispenseOnHitCooldown")]
public float? DispenseOnHitCooldown = 1.0f;
/// <summary>
/// Sound that plays when ejecting an item
/// </summary>
[DataField("soundVend")]
// Grabbed from: https://github.com/discordia-space/CEV-Eris/blob/f702afa271136d093ddeb415423240a2ceb212f0/sound/machines/vending_drop.ogg
public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg");
/// <summary>
/// Sound that plays when an item can't be ejected
/// </summary>
[DataField("soundDeny")]
// Yoinked from: https://github.com/discordia-space/CEV-Eris/blob/35bbad6764b14e15c03a816e3e89aa1751660ba9/sound/machines/Custom_deny.ogg
public SoundSpecifier SoundDeny = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
/// <summary>
/// The action available to the player controlling the vending machine
/// </summary>
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string? Action = "VendingThrow";
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(VendingMachineUiKey.Key);
public float NonLimitedEjectForce = 7.5f;
public float NonLimitedEjectRange = 5f;
public float EjectAccumulator = 0f;
public float DenyAccumulator = 0f;
public float DispenseOnHitAccumulator = 0f;
}
}

View file

@ -42,7 +42,7 @@ public sealed class VendingMachineEjectItemWireAction : BaseWireAction
{
if (EntityManager.TryGetComponent(wire.Owner, out VendingMachineComponent? vending))
{
vending.CanShoot = true;
_vendingMachineSystem.SetShooting(wire.Owner, true, vending);
}
return true;
@ -52,7 +52,7 @@ public sealed class VendingMachineEjectItemWireAction : BaseWireAction
{
if (EntityManager.TryGetComponent(wire.Owner, out VendingMachineComponent? vending))
{
vending.CanShoot = false;
_vendingMachineSystem.SetShooting(wire.Owner, false, vending);
}
return true;

View file

@ -1,7 +1,7 @@
using System.Linq;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.UserInterface;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Actions;
@ -16,7 +16,6 @@ using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
namespace Content.Server.VendingMachines
{
@ -28,18 +27,23 @@ namespace Content.Server.VendingMachines
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly SharedActionsSystem _action = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<VendingMachineComponent, InventorySyncRequestMessage>(OnInventoryRequestMessage);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineEjectMessage>(OnInventoryEjectMessage);
SubscribeLocalEvent<VendingMachineComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<VendingMachineComponent, DamageChangedEvent>(OnDamage);
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
SubscribeLocalEvent<VendingMachineComponent, BoundUIOpenedEvent>(OnBoundUIOpened);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineEjectMessage>(OnInventoryEjectMessage);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
}
@ -49,9 +53,9 @@ namespace Content.Server.VendingMachines
var component = (VendingMachineComponent) sharedComponent;
if (TryComp<ApcPowerReceiverComponent>(component.Owner, out var receiver))
if (HasComp<ApcPowerReceiverComponent>(component.Owner))
{
TryUpdateVisualState(uid, null, component);
TryUpdateVisualState(uid, component);
}
if (component.Action != null)
@ -61,17 +65,22 @@ namespace Content.Server.VendingMachines
}
}
private void OnInventoryRequestMessage(EntityUid uid, VendingMachineComponent component, InventorySyncRequestMessage args)
private void OnActivatableUIOpenAttempt(EntityUid uid, VendingMachineComponent component, ActivatableUIOpenAttemptEvent args)
{
if (!this.IsPowered(uid, EntityManager))
return;
if (component.Broken)
args.Cancel();
}
var inventory = new List<VendingMachineInventoryEntry>(component.Inventory);
private void OnBoundUIOpened(EntityUid uid, VendingMachineComponent component, BoundUIOpenedEvent args)
{
UpdateVendingMachineInterfaceState(component);
}
if (component.Emagged) inventory.AddRange(component.EmaggedInventory);
if (component.Contraband) inventory.AddRange(component.ContrabandInventory);
private void UpdateVendingMachineInterfaceState(VendingMachineComponent component)
{
var state = new VendingMachineInterfaceState(GetAllInventory(component.Owner, component));
component.UserInterface?.SendMessage(new VendingMachineInventoryMessage(inventory));
_userInterfaceSystem.TrySetUiState(component.Owner, VendingMachineUiKey.Key, state);
}
private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args)
@ -87,13 +96,13 @@ namespace Content.Server.VendingMachines
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, PowerChangedEvent args)
{
TryUpdateVisualState(uid, null, component);
TryUpdateVisualState(uid, component);
}
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
{
vendComponent.Broken = true;
TryUpdateVisualState(uid, VendingMachineVisualState.Broken, vendComponent);
TryUpdateVisualState(uid, vendComponent);
}
private void OnEmagged(EntityUid uid, VendingMachineComponent component, GotEmaggedEvent args)
@ -107,11 +116,17 @@ namespace Content.Server.VendingMachines
private void OnDamage(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
{
if (component.DispenseOnHitChance == null || args.DamageDelta == null)
if (component.Broken || component.DispenseOnHitCoolingDown ||
component.DispenseOnHitChance == null || args.DamageDelta == null)
return;
if (args.DamageDelta.Total >= component.DispenseOnHitThreshold && _random.Prob(component.DispenseOnHitChance.Value))
EjectRandom(uid, true, component);
if (args.DamageIncreased && args.DamageDelta.Total >= component.DispenseOnHitThreshold &&
_random.Prob(component.DispenseOnHitChance.Value))
{
if (component.DispenseOnHitCooldown > 0f)
component.DispenseOnHitCoolingDown = true;
EjectRandom(uid, throwItem: true, forceEject: true, component);
}
}
private void OnSelfDispense(EntityUid uid, VendingMachineComponent component, VendingMachineSelfDispenseEvent args)
@ -120,7 +135,18 @@ namespace Content.Server.VendingMachines
return;
args.Handled = true;
EjectRandom(uid, true, component);
EjectRandom(uid, throwItem: true, forceEject: false, component);
}
/// <summary>
/// Sets the <see cref="VendingMachineComponent.CanShoot"/> property of the vending machine.
/// </summary>
public void SetShooting(EntityUid uid, bool canShoot, VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.CanShoot = canShoot;
}
public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null)
@ -128,16 +154,18 @@ namespace Content.Server.VendingMachines
if (!Resolve(uid, ref vendComponent))
return;
SoundSystem.Play(vendComponent.SoundDeny.GetSound(), Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f));
// Play the Deny animation
TryUpdateVisualState(uid, VendingMachineVisualState.Deny, vendComponent);
//TODO: This duration should be a distinct value specific to the deny animation
vendComponent.Owner.SpawnTimer(vendComponent.AnimationDuration, () =>
{
TryUpdateVisualState(uid, VendingMachineVisualState.Normal, vendComponent);
});
if (vendComponent.Denying)
return;
vendComponent.Denying = true;
_audioSystem.Play(vendComponent.SoundDeny, Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f));
TryUpdateVisualState(uid, vendComponent);
}
/// <summary>
/// Checks if the user is authorized to use this vending machine
/// </summary>
/// <param name="sender">Entity trying to use the vending machine</param>
public bool IsAuthorized(EntityUid uid, EntityUid? sender, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent) || sender == null)
@ -155,6 +183,13 @@ namespace Content.Server.VendingMachines
return true;
}
/// <summary>
/// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
/// or the item doesn't exist in its inventory.
/// </summary>
/// <param name="type">The type of inventory the item is from</param>
/// <param name="itemId">The prototype ID of the item</param>
/// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
@ -165,13 +200,7 @@ namespace Content.Server.VendingMachines
return;
}
var entry = type switch
{
InventoryType.Regular => vendComponent.Inventory.Find(x => x.ID == itemId),
InventoryType.Emagged when vendComponent.Emagged => vendComponent.EmaggedInventory.Find(x => x.ID == itemId),
InventoryType.Contraband when vendComponent.Contraband => vendComponent.ContrabandInventory.Find(x => x.ID == itemId),
_ => null
};
var entry = GetEntry(itemId, type, vendComponent);
if (entry == null)
{
@ -195,24 +224,20 @@ namespace Content.Server.VendingMachines
// Start Ejecting, and prevent users from ordering while anim playing
vendComponent.Ejecting = true;
vendComponent.NextItemToEject = entry.ID;
vendComponent.ThrowNextItem = throwItem;
entry.Amount--;
vendComponent.UserInterface?.SendMessage(new VendingMachineInventoryMessage(vendComponent.AllInventory));
TryUpdateVisualState(uid, VendingMachineVisualState.Eject, vendComponent);
vendComponent.Owner.SpawnTimer(vendComponent.AnimationDuration, () =>
{
vendComponent.Ejecting = false;
TryUpdateVisualState(uid, VendingMachineVisualState.Normal, vendComponent);
var ent = EntityManager.SpawnEntity(entry.ID, transformComp.Coordinates);
if (throwItem)
{
float range = vendComponent.NonLimitedEjectRange;
Vector2 direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range));
_throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce);
}
});
SoundSystem.Play(vendComponent.SoundVend.GetSound(), Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f));
UpdateVendingMachineInterfaceState(vendComponent);
TryUpdateVisualState(uid, vendComponent);
_audioSystem.Play(vendComponent.SoundVend, Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f));
}
/// <summary>
/// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
/// </summary>
/// <param name="sender">Entity that is trying to use the vending machine</param>
/// <param name="type">The type of inventory the item is from</param>
/// <param name="itemId">The prototype ID of the item</param>
public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
{
if (IsAuthorized(uid, sender, component))
@ -221,12 +246,15 @@ namespace Content.Server.VendingMachines
}
}
public void TryUpdateVisualState(EntityUid uid, VendingMachineVisualState? state = VendingMachineVisualState.Normal, VendingMachineComponent? vendComponent = null)
/// <summary>
/// Tries to update the visuals of the component based on its current state.
/// </summary>
public void TryUpdateVisualState(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
var finalState = state == null ? VendingMachineVisualState.Normal : state;
var finalState = VendingMachineVisualState.Normal;
if (vendComponent.Broken)
{
finalState = VendingMachineVisualState.Broken;
@ -235,6 +263,10 @@ namespace Content.Server.VendingMachines
{
finalState = VendingMachineVisualState.Eject;
}
else if (vendComponent.Denying)
{
finalState = VendingMachineVisualState.Deny;
}
else if (!this.IsPowered(uid, EntityManager))
{
finalState = VendingMachineVisualState.Off;
@ -242,23 +274,121 @@ namespace Content.Server.VendingMachines
if (TryComp<AppearanceComponent>(vendComponent.Owner, out var appearance))
{
appearance.SetData(VendingMachineVisuals.VisualState, finalState);
_appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState, appearance);
}
}
public void EjectRandom(EntityUid uid, bool throwItem, VendingMachineComponent? vendComponent = null)
/// <summary>
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
/// </summary>
/// <param name="throwItem">Whether to throw the item in a random direction after dispensing it.</param>
/// <param name="forceEject">Whether to skip the regular ejection checks and immediately dispense the item without animation.</param>
public void EjectRandom(EntityUid uid, bool throwItem, bool forceEject = false, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
var availableItems = vendComponent.AllInventory.Where(x => x.Amount > 0).ToList();
var availableItems = GetAvailableInventory(uid, vendComponent);
if (availableItems.Count <= 0)
{
return;
}
var item = _random.Pick(availableItems);
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent);
if (forceEject)
{
vendComponent.NextItemToEject = item.ID;
vendComponent.ThrowNextItem = throwItem;
var entry = GetEntry(item.ID, item.Type, vendComponent);
if (entry != null)
entry.Amount--;
EjectItem(vendComponent, forceEject);
}
else
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent);
}
private void EjectItem(VendingMachineComponent vendComponent, bool forceEject = false)
{
// No need to update the visual state because we never changed it during a forced eject
if (!forceEject)
TryUpdateVisualState(vendComponent.Owner, vendComponent);
if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
{
vendComponent.ThrowNextItem = false;
return;
}
var ent = EntityManager.SpawnEntity(vendComponent.NextItemToEject, Transform(vendComponent.Owner).Coordinates);
if (vendComponent.ThrowNextItem)
{
var range = vendComponent.NonLimitedEjectRange;
var direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range));
_throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce);
}
vendComponent.NextItemToEject = null;
vendComponent.ThrowNextItem = false;
}
private void DenyItem(VendingMachineComponent vendComponent)
{
TryUpdateVisualState(vendComponent.Owner, vendComponent);
}
private VendingMachineInventoryEntry? GetEntry(string entryId, InventoryType type, VendingMachineComponent component)
{
if (type == InventoryType.Emagged && component.Emagged)
return component.EmaggedInventory.GetValueOrDefault(entryId);
if (type == InventoryType.Contraband && component.Contraband)
return component.ContrabandInventory.GetValueOrDefault(entryId);
return component.Inventory.GetValueOrDefault(entryId);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in EntityQuery<VendingMachineComponent>())
{
if (comp.Ejecting)
{
comp.EjectAccumulator += frameTime;
if (comp.EjectAccumulator >= comp.EjectDelay)
{
comp.EjectAccumulator = 0f;
comp.Ejecting = false;
EjectItem(comp);
}
}
if (comp.Denying)
{
comp.DenyAccumulator += frameTime;
if (comp.DenyAccumulator >= comp.DenyDelay)
{
comp.DenyAccumulator = 0f;
comp.Denying = false;
DenyItem(comp);
}
}
if (comp.DispenseOnHitCoolingDown)
{
comp.DispenseOnHitAccumulator += frameTime;
if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown)
{
comp.DispenseOnHitAccumulator = 0f;
comp.DispenseOnHitCoolingDown = false;
}
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more