mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-15 13:21:11 +03:00
Merge remote-tracking branch 'upstream/master' into upstream-sync
# Conflicts: # Resources/Prototypes/Maps/game.yml
This commit is contained in:
commit
7d12bbd1a1
322 changed files with 17471 additions and 208692 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
25
Content.Client/Info/InfoSystem.cs
Normal file
25
Content.Client/Info/InfoSystem.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
8
Content.Client/Kitchen/Visualizers/MicrowaveVisuals.cs
Normal file
8
Content.Client/Kitchen/Visualizers/MicrowaveVisuals.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace Content.Client.Kitchen.Visualizers;
|
||||
|
||||
public enum MicrowaveVisualizerLayers : byte
|
||||
{
|
||||
Base,
|
||||
BaseUnlit
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
34
Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs
Normal file
34
Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,8 +186,8 @@ public sealed class CrewManifestSystem : EntitySystem
|
|||
|
||||
if (eui.Owner == owner)
|
||||
{
|
||||
eui.Close();
|
||||
euis.Remove(session);
|
||||
eui.Close();
|
||||
}
|
||||
|
||||
if (euis.Count == 0)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) ||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
36
Content.Server/Info/InfoSystem.cs
Normal file
36
Content.Server/Info/InfoSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Content.Server.Medical.BiomassReclaimer
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ActiveBiomassReclaimerComponent : Component
|
||||
{}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
31
Content.Server/Movement/LockEyesCommand.cs
Normal file
31
Content.Server/Movement/LockEyesCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
44
Content.Server/Movement/RotateEyesCommand.cs
Normal file
44
Content.Server/Movement/RotateEyesCommand.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
Content.Server/Tools/Components/LatticeCuttingComponent.cs
Normal file
30
Content.Server/Tools/Components/LatticeCuttingComponent.cs
Normal 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;
|
||||
}
|
||||
101
Content.Server/Tools/ToolSystem.LatticeCutting.cs
Normal file
101
Content.Server/Tools/ToolSystem.LatticeCutting.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ namespace Content.Server.Tools
|
|||
base.Initialize();
|
||||
|
||||
InitializeTilePrying();
|
||||
InitializeLatticeCutting();
|
||||
InitializeWelders();
|
||||
|
||||
SubscribeLocalEvent<ToolDoAfterComplete>(OnDoAfterComplete);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue