Merge branch 'master' into 2021-12-03-remove-IEntity-komm-süsser-todd

# Conflicts:
#	Content.Client/Crayon/CrayonDecalVisualizer.cs
#	Content.Client/Tabletop/TabletopSystem.cs
#	Content.IntegrationTests/Tests/InventoryHelpersTest.cs
#	Content.Server/AI/EntitySystems/AiSystem.cs
#	Content.Server/AI/Utility/AiLogic/UtilityAI.cs
#	Content.Server/AME/AMENodeGroup.cs
#	Content.Server/Administration/AdminVerbSystem.cs
#	Content.Server/Body/Systems/RespiratorSystem.cs
#	Content.Server/Chemistry/Components/InjectorComponent.cs
#	Content.Server/Chemistry/TileReactions/CleanTileReaction.cs
#	Content.Server/Chemistry/TileReactions/SpillTileReaction.cs
#	Content.Server/Crayon/CrayonComponent.cs
#	Content.Server/Doors/Components/ServerDoorComponent.cs
#	Content.Server/Explosion/EntitySystems/TriggerSystem.cs
#	Content.Server/Fluids/Components/MopComponent.cs
#	Content.Server/Fluids/Components/SpillExtensions.cs
#	Content.Server/Fluids/EntitySystems/PuddleSystem.cs
#	Content.Server/Instruments/InstrumentSystem.cs
#	Content.Server/Nutrition/EntitySystems/DrinkSystem.cs
#	Content.Server/Nutrition/EntitySystems/FoodSystem.cs
#	Content.Server/PneumaticCannon/PneumaticCannonSystem.cs
#	Content.Server/Storage/Components/EntityStorageComponent.cs
#	Content.Server/Storage/Components/StorageFillComponent.cs
#	Content.Server/Stunnable/StunbatonSystem.cs
#	Content.Server/Throwing/ThrowHelper.cs
#	Content.Server/Weapon/Ranged/Barrels/BarrelSystem.cs
#	Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs
#	Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs
#	Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
#	Content.Shared/Damage/Components/DamageableComponent.cs
#	Content.Shared/Damage/Systems/DamageableSystem.cs
#	Content.Shared/MobState/Components/MobStateComponent.cs
#	Content.Shared/Slippery/SharedSlipperySystem.cs
This commit is contained in:
Vera Aguilera Puerto
2021-12-07 17:48:49 +01:00
171 changed files with 8579 additions and 6088 deletions

View File

@@ -2,7 +2,7 @@ name: Publish
on:
push:
branches: [ master ]
branches: [ stable ]
jobs:
build:

View File

@@ -395,7 +395,13 @@ namespace Content.Client.Chat.UI
private void WriteChatMessage(StoredChatMessage message)
{
Logger.DebugS("chat", $"{message.Channel}: {message.Message}");
var messageText = FormattedMessage.EscapeText(message.Message);
if (!string.IsNullOrEmpty(message.MessageWrap))
{
messageText = string.Format(message.MessageWrap, messageText);
}
Logger.DebugS("chat", $"{message.Channel}: {messageText}");
if (IsFilteredOut(message.Channel))
return;
@@ -403,12 +409,6 @@ namespace Content.Client.Chat.UI
// TODO: Can make this "smarter" later by only setting it false when the message has been scrolled to
message.Read = true;
var messageText = FormattedMessage.EscapeText(message.Message);
if (!string.IsNullOrEmpty(message.MessageWrap))
{
messageText = string.Format(message.MessageWrap, messageText);
}
var color = message.MessageColorOverride != Color.Transparent
? message.MessageColorOverride
: ChatHelper.ChatColor(message.Channel);

View File

@@ -1,30 +0,0 @@
using Content.Shared.Crayon;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.Crayon
{
[UsedImplicitly]
public class CrayonDecalVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<SpriteComponent>(component.Owner);
if (component.TryGetData(CrayonVisuals.State, out string state))
{
sprite.LayerSetState(0, state);
}
if (component.TryGetData(CrayonVisuals.Color, out string color))
{
sprite.LayerSetColor(0, Color.FromName(color));
}
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Shared.Crayon;
using Content.Shared.Decals;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -22,8 +23,7 @@ namespace Content.Client.Crayon.UI
_menu.OnClose += Close;
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var crayonDecals = prototypeManager.EnumeratePrototypes<CrayonDecalPrototype>().FirstOrDefault();
if (crayonDecals != null)
var crayonDecals = prototypeManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("crayon"));
_menu.Populate(crayonDecals);
_menu.OpenCentered();
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Content.Client.Stylesheets;
using Content.Shared.Crayon;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -89,14 +90,12 @@ namespace Content.Client.Crayon.UI
RefreshList();
}
public void Populate(CrayonDecalPrototype proto)
public void Populate(IEnumerable<DecalPrototype> prototypes)
{
var path = new ResourcePath(proto.SpritePath);
_decals = new Dictionary<string, Texture>();
foreach (var state in proto.Decals)
foreach (var decalPrototype in prototypes)
{
var rsi = new SpriteSpecifier.Rsi(path, state);
_decals.Add(state, rsi.Frame0());
_decals.Add(decalPrototype.ID, decalPrototype.Sprite.Frame0());
}
RefreshList();

View File

@@ -0,0 +1,60 @@
using System.Collections.Generic;
using Content.Shared.Decals;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.Shared.Maths;
namespace Content.Client.Decals
{
public class DecalOverlay : Overlay
{
private readonly DecalSystem _system;
private readonly IMapManager _mapManager;
private readonly IPrototypeManager _prototypeManager;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
public DecalOverlay(DecalSystem system, IMapManager mapManager, IPrototypeManager prototypeManager)
{
_system = system;
_mapManager = mapManager;
_prototypeManager = prototypeManager;
}
protected override void Draw(in OverlayDrawArgs args)
{
var handle = args.WorldHandle;
Dictionary<string, SpriteSpecifier> cachedTextures = new();
SpriteSpecifier GetSpriteSpecifier(string id)
{
if (cachedTextures.TryGetValue(id, out var spriteSpecifier))
return spriteSpecifier;
spriteSpecifier = _prototypeManager.Index<DecalPrototype>(id).Sprite;
cachedTextures.Add(id, spriteSpecifier);
return spriteSpecifier;
}
foreach (var (gridId, zIndexDictionary) in _system.DecalRenderIndex)
{
var grid = _mapManager.GetGrid(gridId);
handle.SetTransform(grid.WorldMatrix);
foreach (var (_, decals) in zIndexDictionary)
{
foreach (var (_, decal) in decals)
{
var spriteSpecifier = GetSpriteSpecifier(decal.Id);
handle.DrawTexture(spriteSpecifier.Frame0(), decal.Coordinates, decal.Angle, decal.Color);
}
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
using System.Collections.Generic;
using Content.Shared.Decals;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Client.Decals
{
public class DecalSystem : SharedDecalSystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
private DecalOverlay _overlay = default!;
public Dictionary<GridId, SortedDictionary<int, SortedDictionary<uint, Decal>>> DecalRenderIndex = new();
private Dictionary<GridId, Dictionary<uint, int>> DecalZIndexIndex = new();
public override void Initialize()
{
base.Initialize();
_overlay = new DecalOverlay(this, MapManager, PrototypeManager);
_overlayManager.AddOverlay(_overlay);
SubscribeNetworkEvent<DecalChunkUpdateEvent>(OnChunkUpdate);
SubscribeLocalEvent<GridInitializeEvent>(OnGridInitialize);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoval);
}
public void ToggleOverlay()
{
if (_overlayManager.HasOverlay<DecalOverlay>())
{
_overlayManager.RemoveOverlay(_overlay);
}
else
{
_overlayManager.AddOverlay(_overlay);
}
}
private void OnGridRemoval(GridRemovalEvent ev)
{
DecalRenderIndex.Remove(ev.GridId);
DecalZIndexIndex.Remove(ev.GridId);
}
private void OnGridInitialize(GridInitializeEvent ev)
{
DecalRenderIndex[ev.GridId] = new();
DecalZIndexIndex[ev.GridId] = new();
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay(_overlay);
}
protected override bool RemoveDecalHook(GridId gridId, uint uid)
{
RemoveDecalFromRenderIndex(gridId, uid);
return base.RemoveDecalHook(gridId, uid);
}
private void RemoveDecalFromRenderIndex(GridId gridId, uint uid)
{
var zIndex = DecalZIndexIndex[gridId][uid];
DecalRenderIndex[gridId][zIndex].Remove(uid);
if (DecalRenderIndex[gridId][zIndex].Count == 0)
DecalRenderIndex[gridId].Remove(zIndex);
DecalZIndexIndex[gridId].Remove(uid);
}
private void OnChunkUpdate(DecalChunkUpdateEvent ev)
{
foreach (var (gridId, gridChunks) in ev.Data)
{
foreach (var (indices, newChunkData) in gridChunks)
{
var chunkCollection = ChunkCollection(gridId);
if (chunkCollection.TryGetValue(indices, out var chunk))
{
var removedUids = new HashSet<uint>(chunk.Keys);
removedUids.ExceptWith(newChunkData.Keys);
foreach (var removedUid in removedUids)
{
RemoveDecalFromRenderIndex(gridId, removedUid);
}
}
foreach (var (uid, decal) in newChunkData)
{
if(!DecalRenderIndex[gridId].ContainsKey(decal.ZIndex))
DecalRenderIndex[gridId][decal.ZIndex] = new();
if (DecalZIndexIndex.TryGetValue(gridId, out var values) && values.TryGetValue(uid, out var zIndex))
{
DecalRenderIndex[gridId][zIndex].Remove(uid);
}
DecalRenderIndex[gridId][decal.ZIndex][uid] = decal;
DecalZIndexIndex[gridId][uid] = decal.ZIndex;
ChunkIndex[gridId][uid] = indices;
}
chunkCollection[indices] = newChunkData;
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
namespace Content.Client.Decals;
public class ToggleDecalCommand : IConsoleCommand
{
public string Command => "toggledecals";
public string Description => "Toggles decaloverlay";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DecalSystem>().ToggleOverlay();
}
}

View File

@@ -8,6 +8,7 @@ namespace Content.Client.Entry
"Anchorable",
"AmmoBox",
"Pickaxe",
"IngestionBlocker",
"Interactable",
"CloningPod",
"Destructible",

View File

@@ -18,6 +18,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
@@ -29,6 +30,7 @@ namespace Content.Client.Tabletop
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManger = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
// Time in seconds to wait until sending the location of a dragged entity to the server again
private const float Delay = 1f / 10; // 10 Hz
@@ -51,6 +53,10 @@ namespace Content.Client.Tabletop
public override void Update(float frameTime)
{
// don't send network messages when doing prediction.
if (!_gameTiming.IsFirstTimePredicted)
return;
// If there is no player entity, return
if (_playerManager.LocalPlayer is not {ControlledEntity: { } playerEntity}) return;

View File

@@ -1,6 +1,7 @@
using System;
using Content.Client.Items.Components;
using Content.Client.Stylesheets;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -8,6 +9,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -21,6 +23,9 @@ namespace Content.Client.Weapons.Ranged.Barrels.Components
private StatusControl? _statusControl;
[DataField("cellSlot", required: true)]
public ItemSlot CellSlot = default!;
/// <summary>
/// Count of bullets in the magazine.
/// </summary>
@@ -30,6 +35,18 @@ namespace Content.Client.Weapons.Ranged.Barrels.Components
[ViewVariables]
public (int count, int max)? MagazineCount { get; private set; }
protected override void Initialize()
{
base.Initialize();
EntitySystem.Get<ItemSlotsSystem>().AddItemSlot(Owner, $"{Name}-powercell-container", CellSlot);
}
protected override void OnRemove()
{
base.OnRemove();
EntitySystem.Get<ItemSlotsSystem>().RemoveItemSlot(Owner, CellSlot);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);

View File

@@ -23,7 +23,7 @@ namespace Content.IntegrationTests.Tests
{
var options = new ServerContentIntegrationOption()
{
CVarOverrides = {{CCVars.AIMaxUpdates.Name, int.MaxValue.ToString()}}
CVarOverrides = {{CCVars.NPCMaxUpdates.Name, int.MaxValue.ToString()}}
};
var server = StartServer(options);

View File

@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Content.Server.Fluids.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Coordinates;
using Content.Shared.FixedPoint;
@@ -24,6 +25,8 @@ namespace Content.IntegrationTests.Tests.Fluids
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
server.Assert(() =>
{
@@ -31,7 +34,7 @@ namespace Content.IntegrationTests.Tests.Fluids
var grid = GetMainGrid(mapManager);
var (x, y) = GetMainTile(grid).GridIndices;
var coordinates = new EntityCoordinates(grid.GridEntityId, x, y);
var puddle = solution.SpillAt(coordinates, "PuddleSmear");
var puddle = spillSystem.SpillAt(solution, coordinates, "PuddleSmear");
Assert.NotNull(puddle);
});
@@ -47,6 +50,8 @@ namespace Content.IntegrationTests.Tests.Fluids
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
IMapGrid grid = null;
@@ -67,7 +72,7 @@ namespace Content.IntegrationTests.Tests.Fluids
{
var coordinates = grid.ToCoordinates();
var solution = new Solution("Water", FixedPoint2.New(20));
var puddle = solution.SpillAt(coordinates, "PuddleSmear");
var puddle = spillSystem.SpillAt(solution, coordinates, "PuddleSmear");
Assert.Null(puddle);
});
@@ -120,13 +125,17 @@ namespace Content.IntegrationTests.Tests.Fluids
float evaporateTime = default;
PuddleComponent puddle = null;
EvaporationComponent evaporation;
var amount = 2;
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
// Spawn a puddle
await server.WaitAssertion(() =>
{
var solution = new Solution("Water", FixedPoint2.New(amount));
puddle = solution.SpillAt(sCoordinates, "PuddleSmear");
puddle = spillSystem.SpillAt(solution, sCoordinates, "PuddleSmear");
// Check that the puddle was created
Assert.NotNull(puddle);

View File

@@ -78,7 +78,7 @@ namespace Content.IntegrationTests.Tests
ID: "InventoryJumpsuitJanitorDummy"
});
EntitySystem.Get<StunSystem>().TryStun(human, TimeSpan.FromSeconds(1f));
EntitySystem.Get<StunSystem>().TryStun(human, TimeSpan.FromSeconds(1f), true);
// Since the mob is stunned, they can't equip this.
Assert.That(inventory.SpawnItemInSlot(Slots.IDCARD, "InventoryIDCardDummy", true), Is.False);

View File

@@ -1,4 +1,5 @@
using Content.Server.GameTicking;
using Content.Server.AI.EntitySystems;
using Content.Server.GameTicking;
using Content.Shared.Movement.Components;
using Content.Shared.Roles;
using Robust.Shared.GameObjects;
@@ -19,6 +20,29 @@ namespace Content.Server.AI.Components
public override string Name => "AiController";
// TODO: Need to ECS a lot more of the AI first before we can ECS this
/// <summary>
/// Whether the AI is actively iterated.
/// </summary>
public bool Awake
{
get => _awake;
set
{
if (_awake == value) return;
_awake = value;
if (_awake)
EntitySystem.Get<NPCSystem>().WakeNPC(this);
else
EntitySystem.Get<NPCSystem>().SleepNPC(this);
}
}
[DataField("awake")]
private bool _awake = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("startingGear")]
public string? StartingGearPrototype { get; set; }

View File

@@ -1,133 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Server.AI.Components;
using Content.Server.AI.Utility.AiLogic;
using Content.Shared;
using Content.Shared.CCVar;
using Content.Shared.MobState;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Content.Server.AI.EntitySystems
{
/// <summary>
/// Handles NPCs running every tick.
/// </summary>
[UsedImplicitly]
internal class AiSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
/// <summary>
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
/// </summary>
private readonly HashSet<AiControllerComponent> _awakeAi = new();
// To avoid modifying awakeAi while iterating over it.
private readonly List<SleepAiMessage> _queuedSleepMessages = new();
private readonly List<MobStateChangedMessage> _queuedMobStateMessages = new();
public bool IsAwake(AiControllerComponent npc) => _awakeAi.Contains(npc);
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SleepAiMessage>(HandleAiSleep);
SubscribeLocalEvent<MobStateChangedMessage>(MobStateChanged);
}
/// <inheritdoc />
public override void Update(float frameTime)
{
var cvarMaxUpdates = _configurationManager.GetCVar(CCVars.AIMaxUpdates);
if (cvarMaxUpdates <= 0)
return;
foreach (var message in _queuedMobStateMessages)
{
// TODO: Need to generecise this but that will be part of a larger cleanup later anyway.
if ((!IoCManager.Resolve<IEntityManager>().EntityExists(message.Entity) ? EntityLifeStage.Deleted : IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(message.Entity).EntityLifeStage) >= EntityLifeStage.Deleted ||
!IoCManager.Resolve<IEntityManager>().TryGetComponent(message.Entity, out UtilityAi? controller))
{
continue;
}
controller.MobStateChanged(message);
}
_queuedMobStateMessages.Clear();
foreach (var message in _queuedSleepMessages)
{
switch (message.Sleep)
{
case true:
if (_awakeAi.Count == cvarMaxUpdates && _awakeAi.Contains(message.Component))
{
Logger.Warning($"Under AI limit again: {_awakeAi.Count - 1} / {cvarMaxUpdates}");
}
_awakeAi.Remove(message.Component);
break;
case false:
_awakeAi.Add(message.Component);
if (_awakeAi.Count > cvarMaxUpdates)
{
Logger.Warning($"AI limit exceeded: {_awakeAi.Count} / {cvarMaxUpdates}");
}
break;
}
}
_queuedSleepMessages.Clear();
var toRemove = new List<AiControllerComponent>();
var maxUpdates = Math.Min(_awakeAi.Count, cvarMaxUpdates);
var count = 0;
foreach (var npc in _awakeAi)
{
if (npc.Deleted)
{
toRemove.Add(npc);
continue;
}
if (npc.Paused)
continue;
if (count >= maxUpdates)
{
break;
}
npc.Update(frameTime);
count++;
}
foreach (var processor in toRemove)
{
_awakeAi.Remove(processor);
}
}
private void HandleAiSleep(SleepAiMessage message)
{
_queuedSleepMessages.Add(message);
}
private void MobStateChanged(MobStateChangedMessage message)
{
if (!IoCManager.Resolve<IEntityManager>().HasComponent<AiControllerComponent>(message.Entity))
{
return;
}
_queuedMobStateMessages.Add(message);
}
}
}

View File

@@ -0,0 +1,135 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.AI.Components;
using Content.Server.MobState.States;
using Content.Shared.CCVar;
using Content.Shared.MobState;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Random;
namespace Content.Server.AI.EntitySystems
{
/// <summary>
/// Handles NPCs running every tick.
/// </summary>
[UsedImplicitly]
internal class NPCSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
/// <summary>
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
/// </summary>
private readonly HashSet<AiControllerComponent> _awakeNPCs = new();
/// <summary>
/// Whether any NPCs are allowed to run at all.
/// </summary>
public bool Enabled { get; set; } = true;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AiControllerComponent, MobStateChangedEvent>(OnMobStateChange);
SubscribeLocalEvent<AiControllerComponent, ComponentInit>(OnNPCInit);
SubscribeLocalEvent<AiControllerComponent, ComponentShutdown>(OnNPCShutdown);
_configurationManager.OnValueChanged(CCVars.NPCEnabled, SetEnabled, true);
var maxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates);
if (maxUpdates < 1024)
_awakeNPCs.EnsureCapacity(maxUpdates);
}
private void SetEnabled(bool value) => Enabled = value;
public override void Shutdown()
{
base.Shutdown();
_configurationManager.UnsubValueChanged(CCVars.NPCEnabled, SetEnabled);
}
private void OnNPCInit(EntityUid uid, AiControllerComponent component, ComponentInit args)
{
if (!component.Awake) return;
_awakeNPCs.Add(component);
}
private void OnNPCShutdown(EntityUid uid, AiControllerComponent component, ComponentShutdown args)
{
_awakeNPCs.Remove(component);
}
/// <summary>
/// Allows the NPC to actively be updated.
/// </summary>
/// <param name="component"></param>
public void WakeNPC(AiControllerComponent component)
{
_awakeNPCs.Add(component);
}
/// <summary>
/// Stops the NPC from actively being updated.
/// </summary>
/// <param name="component"></param>
public void SleepNPC(AiControllerComponent component)
{
_awakeNPCs.Remove(component);
}
/// <inheritdoc />
public override void Update(float frameTime)
{
if (!Enabled) return;
var cvarMaxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates);
if (cvarMaxUpdates <= 0) return;
var npcs = _awakeNPCs.ToArray();
var startIndex = 0;
// If we're overcap we'll just update randomly so they all still at least do something
// Didn't randomise the array (even though it'd probably be better) because god damn that'd be expensive.
if (npcs.Length > cvarMaxUpdates)
{
startIndex = _robustRandom.Next(npcs.Length);
}
for (var i = 0; i < npcs.Length; i++)
{
var index = (i + startIndex) % npcs.Length;
var npc = npcs[index];
if (npc.Deleted)
continue;
if (npc.Paused)
continue;
npc.Update(frameTime);
}
}
private void OnMobStateChange(EntityUid uid, AiControllerComponent component, MobStateChangedEvent args)
{
switch (args.CurrentMobState)
{
case NormalMobState:
component.Awake = true;
break;
case CriticalMobState:
case DeadMobState:
component.Awake = false;
break;
}
}
}
}

View File

@@ -1,24 +0,0 @@
using Content.Server.AI.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.AI
{
/// <summary>
/// Indicates whether an AI should be updated by the AiSystem or not.
/// Useful to sleep AI when they die or otherwise should be inactive.
/// </summary>
internal sealed class SleepAiMessage : EntityEventArgs
{
/// <summary>
/// Sleep or awake.
/// </summary>
public bool Sleep { get; }
public AiControllerComponent Component { get; }
public SleepAiMessage(AiControllerComponent component, bool sleep)
{
Component = component;
Sleep = sleep;
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Threading;
using Content.Server.AI.Components;
using Content.Server.AI.EntitySystems;
using Content.Server.AI.LoadBalancer;
using Content.Server.AI.Operators;
using Content.Server.AI.Utility.Actions;
@@ -59,33 +60,13 @@ namespace Content.Server.AI.Utility.AiLogic
private CancellationTokenSource? _actionCancellation;
/// <summary>
/// If we can't do anything then stop thinking; should probably use ActionBlocker instead
/// </summary>
private bool _isDead;
/*public void AfterDeserialization()
{
if (BehaviorSets.Count > 0)
{
var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
foreach (var bSet in BehaviorSets)
{
behaviorManager.AddBehaviorSet(this, bSet, false);
}
behaviorManager.RebuildActions(this);
}
}*/
protected override void Initialize()
{
if (BehaviorSets.Count > 0)
{
var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
behaviorManager.RebuildActions(this);
IoCManager.Resolve<IEntityManager>().EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false));
EntitySystem.Get<NPCSystem>().WakeNPC(this);
}
base.Initialize();
@@ -103,27 +84,6 @@ namespace Content.Server.AI.Utility.AiLogic
CurrentAction = null;
}
public void MobStateChanged(MobStateChangedMessage message)
{
var oldDeadState = _isDead;
_isDead = message.Component.IsIncapacitated();
if (oldDeadState != _isDead)
{
var entityManager = IoCManager.Resolve<IEntityManager>();
switch (_isDead)
{
case true:
entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, true));
break;
case false:
entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false));
break;
}
}
}
private void ReceivedAction()
{
if (_actionRequest == null)

View File

@@ -84,9 +84,9 @@ namespace Content.Server.AI.Utility
if (rebuild)
RebuildActions(npc);
if (npc.BehaviorSets.Count == 1 && !EntitySystem.Get<AiSystem>().IsAwake(npc))
if (npc.BehaviorSets.Count == 1 && !npc.Awake)
{
_entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, false));
EntitySystem.Get<NPCSystem>().WakeNPC(npc);
}
}
@@ -113,9 +113,9 @@ namespace Content.Server.AI.Utility
if (rebuild)
RebuildActions(npc);
if (npc.BehaviorSets.Count == 0 && EntitySystem.Get<AiSystem>().IsAwake(npc))
if (npc.BehaviorSets.Count == 0 && npc.Awake)
{
_entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, true));
EntitySystem.Get<NPCSystem>().SleepNPC(npc);
}
}

View File

@@ -25,6 +25,13 @@ namespace Content.Server.Access.Components
{
public override string Name => "AccessReader";
/// <summary>
/// Whether this reader is enabled or not. If disabled, all access
/// checks will pass.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Enabled = true;
/// <summary>
/// The set of tags that will automatically deny an allowed check, if any of them are present.
/// </summary>

View File

@@ -60,7 +60,7 @@ namespace Content.Server.Access.Systems
return false;
}
return reader.AccessLists.Count == 0 || reader.AccessLists.Any(a => a.IsSubsetOf(accessTags));
return !reader.Enabled || reader.AccessLists.Count == 0 || reader.AccessLists.Any(a => a.IsSubsetOf(accessTags));
}
public ICollection<string> FindAccessTags(EntityUid uid)

View File

@@ -125,7 +125,7 @@ namespace Content.Server.Administration
verb.Act = () =>
{
var coords = EntityManager.GetComponent<TransformComponent>(args.Target).Coordinates;
Timer.Spawn(_gameTiming.TickPeriod, () => _explosions.SpawnExplosion(coords, 0, 1, 2, 1), CancellationToken.None);
Timer.Spawn(_gameTiming.TickPeriod, () => _explosions.SpawnExplosion(coords, 0, 1, 2, 1, args.Target), CancellationToken.None);
if (EntityManager.TryGetComponent(args.Target, out SharedBodyComponent? body))
{
body.Gib();

View File

@@ -179,7 +179,7 @@ namespace Content.Server.Atmos.EntitySystems
flammable.Resisting = true;
flammable.Owner.PopupMessage(Loc.GetString("flammable-component-resist-message"));
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(2f), alerts: alerts);
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(2f), true, alerts: alerts);
// TODO FLAMMABLE: Make this not use TimerComponent...
flammable.Owner.SpawnTimer(2000, () =>

View File

@@ -1,10 +1,12 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Atmos.Reactions
@@ -36,7 +38,8 @@ namespace Content.Server.Atmos.Reactions
mixture.AdjustMoles(GasId, -MolesPerUnit);
var tileRef = tile.GridIndices.GetTileRef(tile.GridIndex);
tileRef.SpillAt(new Solution(Reagent, FixedPoint2.New(MolesPerUnit)), PuddlePrototype, sound: false);
EntitySystem.Get<SpillableSystem>()
.SpillAt(tileRef, new Solution(Reagent, FixedPoint2.New(MolesPerUnit)), PuddlePrototype, sound: false);
return ReactionResult.Reacting;
}

View File

@@ -50,7 +50,7 @@ public class LungSystem : EntitySystem
Inhale(uid, lung.CycleDelay);
}
public void UpdateLung(EntityUid uid, float frameTime,
public void UpdateLung(EntityUid uid,
LungComponent? lung=null,
SharedMechanismComponent? mech=null)
{
@@ -69,8 +69,8 @@ public class LungSystem : EntitySystem
lung.AccumulatedFrametime += lung.Status switch
{
LungStatus.Inhaling => frameTime,
LungStatus.Exhaling => -frameTime,
LungStatus.Inhaling => 1,
LungStatus.Exhaling => -1,
_ => throw new ArgumentOutOfRangeException()
};

View File

@@ -32,41 +32,40 @@ namespace Content.Server.Body.Systems
foreach (var (respirator, blood, body) in
EntityManager.EntityQuery<RespiratorComponent, BloodstreamComponent, SharedBodyComponent>())
{
var uid = (respirator).Owner;
var uid = respirator.Owner;
if (!EntityManager.TryGetComponent<MobStateComponent>(uid, out var state) ||
state.IsDead())
{
return;
continue;
}
respirator.AccumulatedFrametime += frameTime;
if (respirator.AccumulatedFrametime < 1)
{
return;
continue;
}
ProcessGases(uid, respirator, frameTime, blood, body);
ProcessGases(uid, respirator, blood, body);
respirator.AccumulatedFrametime -= 1;
if (SuffocatingPercentage(respirator) > 0)
{
TakeSuffocationDamage(uid, respirator);
return;
continue;
}
StopSuffocation(uid, respirator);
}
}
private Dictionary<Gas, float> NeedsAndDeficit(RespiratorComponent respirator, float frameTime)
private Dictionary<Gas, float> NeedsAndDeficit(RespiratorComponent respirator)
{
var needs = new Dictionary<Gas, float>(respirator.NeedsGases);
foreach (var (gas, amount) in respirator.DeficitGases)
{
var newAmount = (needs.GetValueOrDefault(gas) + amount) * frameTime;
var newAmount = (needs.GetValueOrDefault(gas) + amount);
needs[gas] = newAmount;
}
@@ -130,7 +129,7 @@ namespace Content.Server.Body.Systems
return respirator.ProducesGases.ToDictionary(pair => pair.Key, pair => GasProducedMultiplier(respirator, pair.Key, usedAverage));
}
private void ProcessGases(EntityUid uid, RespiratorComponent respirator, float frameTime,
private void ProcessGases(EntityUid uid, RespiratorComponent respirator,
BloodstreamComponent? bloodstream,
SharedBodyComponent? body)
{
@@ -139,12 +138,12 @@ namespace Content.Server.Body.Systems
var lungs = _bodySystem.GetComponentsOnMechanisms<LungComponent>(uid, body).ToArray();
var needs = NeedsAndDeficit(respirator, frameTime);
var needs = NeedsAndDeficit(respirator);
var used = 0f;
foreach (var (lung, mech) in lungs)
{
_lungSystem.UpdateLung((lung).Owner, frameTime, lung, mech);
_lungSystem.UpdateLung(lung.Owner, lung, mech);
}
foreach (var (gas, amountNeeded) in needs)

View File

@@ -1,18 +1,28 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.CombatMode;
using Content.Server.DoAfter;
using Content.Shared.ActionBlocker;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.MobState.Components;
using Content.Shared.Popups;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@@ -42,16 +52,26 @@ namespace Content.Server.Chemistry.Components
/// Amount to inject or draw on each usage. If the injector is inject only, it will
/// attempt to inject it's entire contents upon use.
/// </summary>
[ViewVariables]
[ViewVariables(VVAccess.ReadWrite)]
[DataField("transferAmount")]
private FixedPoint2 _transferAmount = FixedPoint2.New(5);
/// <summary>
/// Initial storage volume of the injector
/// Injection delay (seconds) when the target is a mob.
/// </summary>
[ViewVariables]
[DataField("initialMaxVolume")]
private FixedPoint2 _initialMaxVolume = FixedPoint2.New(15);
/// <remarks>
/// The base delay has a minimum of 1 second, but this will still be modified if the target is incapacitated or
/// in combat mode.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("delay")]
public float Delay = 5;
/// <summary>
/// Token for interrupting a do-after action (e.g., injection another player). If not null, implies
/// component is currently "in use".
/// </summary>
public CancellationTokenSource? CancelToken;
private InjectorToggleMode _toggleState;
@@ -112,9 +132,18 @@ namespace Content.Server.Chemistry.Components
/// <param name="eventArgs"></param>
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (CancelToken != null)
{
CancelToken.Cancel();
return true;
}
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return false;
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(eventArgs.User))
return false;
var solutionsSys = EntitySystem.Get<SolutionContainerSystem>();
//Make sure we have the attacking entity
if (eventArgs.Target is not {Valid: true} target ||
@@ -123,6 +152,14 @@ namespace Content.Server.Chemistry.Components
return false;
}
// Is the target a mob? If yes, use a do-after to give them time to respond.
if (_entities.HasComponent<MobStateComponent>(target) ||
_entities.HasComponent<BloodstreamComponent>(target))
{
if (!await TryInjectDoAfter(eventArgs.User, target))
return true;
}
// Handle injecting/drawing for solutions
if (ToggleState == InjectorToggleMode.Inject)
{
@@ -162,6 +199,75 @@ namespace Content.Server.Chemistry.Components
return true;
}
/// <summary>
/// Send informative pop-up messages and wait for a do-after to complete.
/// </summary>
public async Task<bool> TryInjectDoAfter(EntityUid user, EntityUid target)
{
var popupSys = EntitySystem.Get<SharedPopupSystem>();
// Create a pop-up for the user
popupSys.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user));
// Get entity for logging. Log with EntityUids when?
var logSys = EntitySystem.Get<AdminLogSystem>();
var actualDelay = MathF.Max(Delay, 1f);
if (user != target)
{
// Create a pop-up for the target
var userName = _entities.GetComponent<MetaDataComponent>(user).EntityName;
popupSys.PopupEntity(Loc.GetString("injector-component-injecting-target",
("user", userName)), user, Filter.Entities(target));
// Check if the target is incapacitated or in combat mode and modify time accordingly.
if (_entities.TryGetComponent<MobStateComponent>(target, out var mobState) &&
mobState.IsIncapacitated())
{
actualDelay /= 2;
}
else if (_entities.TryGetComponent<CombatModeComponent>(target, out var combat) &&
combat.IsInCombatMode)
{
// Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
// combat with fast syringes & lag.
actualDelay += 1;
}
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
if (ToggleState == InjectorToggleMode.Inject)
{
logSys.Add(LogType.ForceFeed,
$"{_entities.ToPrettyString(user)} is attempting to inject a solution into {_entities.ToPrettyString(target)}");
// TODO solution pretty string.
}
}
else
{
// Self-injections take half as long.
actualDelay /= 2;
if (ToggleState == InjectorToggleMode.Inject)
logSys.Add(LogType.Ingestion,
$"{_entities.ToPrettyString(user)} is attempting to inject themselves with a solution.");
//TODO solution pretty string.
}
CancelToken = new();
var status = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(
new DoAfterEventArgs(user, actualDelay, CancelToken.Token, target)
{
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
MovementThreshold = 1.0f
});
CancelToken = null;
return status == DoAfterStatus.Finished;
}
/// <summary>
/// Called when use key is pressed when held in active hand
/// </summary>

View File

@@ -1,6 +1,8 @@
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Components;
using Content.Shared.Hands;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using System;
namespace Content.Server.Chemistry.EntitySystems
{
@@ -12,6 +14,16 @@ namespace Content.Server.Chemistry.EntitySystems
base.Initialize();
SubscribeLocalEvent<InjectorComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<InjectorComponent, HandDeselectedEvent>(OnInjectorDeselected);
}
private void OnInjectorDeselected(EntityUid uid, InjectorComponent component, HandDeselectedEvent args)
{
if (component.CancelToken != null)
{
component.CancelToken.Cancel();
component.CancelToken = null;
}
}
private void OnSolutionChange(EntityUid uid, InjectorComponent component, SolutionChangedEvent args)

View File

@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Database;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
@@ -128,5 +130,27 @@ namespace Content.Server.Chemistry.EntitySystems
return true;
}
public static string ToPrettyString(Solution solution)
{
var sb = new StringBuilder();
sb.Append("Solution content: [");
var first = true;
foreach (var (id, quantity) in solution.Contents)
{
if (first)
{
first = false;
}
else
{
sb.Append(", ");
}
sb.AppendFormat("{0}: {1}u", id, quantity);
}
sb.Append(']');
return sb.ToString();
}
}
}

View File

@@ -15,12 +15,17 @@ public class Electrocute : ReagentEffect
[DataField("electrocuteDamageScale")] public int ElectrocuteDamageScale = 5;
/// <remarks>
/// true - refresh electrocute time, false - accumulate electrocute time
/// </remarks>
[DataField("refresh")] public bool Refresh = true;
public override bool ShouldLog => true;
public override void Effect(ReagentEffectArgs args)
{
EntitySystem.Get<ElectrocutionSystem>().TryDoElectrocution(args.SolutionEntity, null,
Math.Max((args.Quantity * ElectrocuteDamageScale).Int(), 1), TimeSpan.FromSeconds(ElectrocuteTime));
Math.Max((args.Quantity * ElectrocuteDamageScale).Int(), 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh);
args.Source?.RemoveReagent(args.Reagent.ID, args.Quantity);
}

View File

@@ -30,6 +30,12 @@ namespace Content.Server.Chemistry.ReagentEffects.StatusEffects
[DataField("time")]
public float Time = 2.0f;
/// <remarks>
/// true - refresh status effect time, false - accumulate status effect time
/// </remarks>
[DataField("refresh")]
public bool Refresh = true;
/// <summary>
/// Should this effect add the status effect, remove time from it, or set its cooldown?
/// </summary>
@@ -41,7 +47,7 @@ namespace Content.Server.Chemistry.ReagentEffects.StatusEffects
var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem<StatusEffectsSystem>();
if (Type == StatusEffectMetabolismType.Add && Component != String.Empty)
{
statusSys.TryAddStatusEffect(args.SolutionEntity, Key, TimeSpan.FromSeconds(Time), Component);
statusSys.TryAddStatusEffect(args.SolutionEntity, Key, TimeSpan.FromSeconds(Time), Refresh, Component);
}
else if (Type == StatusEffectMetabolismType.Remove)
{

View File

@@ -23,10 +23,16 @@ namespace Content.Server.Chemistry.ReagentEffects.StatusEffects
[DataField("time")]
public float Time = 2.0f;
/// <remarks>
/// true - refresh jitter time, false - accumulate jitter time
/// </remarks>
[DataField("refresh")]
public bool Refresh = true;
public override void Effect(ReagentEffectArgs args)
{
args.EntityManager.EntitySysManager.GetEntitySystem<SharedJitteringSystem>()
.DoJitter(args.SolutionEntity, TimeSpan.FromSeconds(Time), Amplitude, Frequency);
.DoJitter(args.SolutionEntity, TimeSpan.FromSeconds(Time), Refresh, Amplitude, Frequency);
}
}
}

View File

@@ -1,12 +1,14 @@
using System.Linq;
using Content.Server.Cleanable;
using Content.Server.Coordinates.Helpers;
using Content.Server.Decals;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Chemistry.TileReactions
@@ -36,6 +38,12 @@ namespace Content.Server.Chemistry.TileReactions
}
}
var decalSystem = EntitySystem.Get<DecalSystem>();
foreach (var uid in decalSystem.GetDecalsInRange(tile.GridIndex, tile.GridIndices+new Vector2(0.5f, 0.5f), validDelegate: x => x.Cleanable))
{
decalSystem.RemoveDecal(tile.GridIndex, uid);
}
return amount;
}
}

View File

@@ -1,9 +1,11 @@
using Content.Server.Fluids.Components;
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 JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -15,9 +17,12 @@ namespace Content.Server.Chemistry.TileReactions
{
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
{
if (reactVolume < 5 || !tile.TryGetPuddle(null, out _)) return FixedPoint2.Zero;
var spillSystem = EntitySystem.Get<SpillableSystem>();
if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _)) return FixedPoint2.Zero;
return tile.SpillAt(new Solution(reagent.ID, reactVolume), "PuddleSmear", true, false, true) != null ? reactVolume : FixedPoint2.Zero;
return spillSystem.SpillAt(tile,new Solution(reagent.ID, reactVolume), "PuddleSmear", true, false, true) != null
? reactVolume
: FixedPoint2.Zero;
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Fluids.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
@@ -26,7 +27,8 @@ namespace Content.Server.Chemistry.TileReactions
if (reactVolume < 5) return FixedPoint2.Zero;
// TODO Make this not puddle smear.
var puddle = tile.SpillAt(new Solution(reagent.ID, reactVolume), "PuddleSmear", _overflow, false, true);
var puddle = EntitySystem.Get<SpillableSystem>()
.SpillAt(tile, new Solution(reagent.ID, reactVolume), "PuddleSmear", _overflow, false, true);
if (puddle != null)
{

View File

@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Storage.Components;
using Content.Shared.Construction;
using Content.Shared.Examine;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Content.Server.Construction.Conditions
{
[UsedImplicitly]
[DataDefinition]
public class Locked : IGraphCondition
{
[DataField("locked")]
public bool IsLocked { get; private set; } = true;
public bool Condition(EntityUid uid, IEntityManager entityManager)
{
if (!entityManager.TryGetComponent(uid, out LockComponent? lockcomp))
return false;
return lockcomp.Locked == IsLocked;
}
public bool DoExamine(ExaminedEvent args)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var entity = args.Examined;
if (!entMan.TryGetComponent(entity, out LockComponent? lockcomp)) return false;
switch (IsLocked)
{
case true when !lockcomp.Locked:
args.PushMarkup(Loc.GetString("construction-examine-condition-lock"));
return true;
case false when lockcomp.Locked:
args.PushMarkup(Loc.GetString("construction-examine-condition-unlock"));
return true;
}
return false;
}
public IEnumerable<ConstructionGuideEntry> GenerateGuideEntry()
{
yield return new ConstructionGuideEntry()
{
Localization = IsLocked
? "construction-step-condition-wire-panel-lock"
: "construction-step-condition-wire-panel-unlock"
};
}
}
}

View File

@@ -0,0 +1,59 @@
using System.Collections.Generic;
using Content.Server.Storage.Components;
using Content.Shared.Construction;
using Content.Shared.Examine;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Construction.Conditions
{
[UsedImplicitly]
[DataDefinition]
public class StorageWelded : IGraphCondition
{
[DataField("welded")]
public bool Welded { get; private set; } = true;
public bool Condition(EntityUid uid, IEntityManager entityManager)
{
if (!entityManager.TryGetComponent(uid, out EntityStorageComponent? entityStorageComponent))
return false;
return entityStorageComponent.IsWeldedShut == Welded;
}
public bool DoExamine(ExaminedEvent args)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var entity = args.Examined;
if (!entMan.TryGetComponent(entity, out EntityStorageComponent? entityStorage)) return false;
var metaData = entMan.GetComponent<MetaDataComponent>(entity);
if (entityStorage.IsWeldedShut != Welded)
{
if (Welded == true)
args.PushMarkup(Loc.GetString("construction-examine-condition-door-weld", ("entityName", metaData.EntityName)) + "\n");
else
args.PushMarkup(Loc.GetString("construction-examine-condition-door-unweld", ("entityName", metaData.EntityName)) + "\n");
return true;
}
return false;
}
public IEnumerable<ConstructionGuideEntry> GenerateGuideEntry()
{
yield return new ConstructionGuideEntry()
{
Localization = Welded
? "construction-guide-condition-door-weld"
: "construction-guide-condition-door-unweld",
};
}
}
}

View File

@@ -59,7 +59,7 @@ namespace Content.Server.Conveyor
signal != TwoWayLeverSignal.Middle)
{
args.Cancel();
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(2f));
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(2f), true);
component.Owner.PopupMessage(args.Attemptee, Loc.GetString("conveyor-component-failed-link"));
}
}

View File

@@ -2,9 +2,10 @@ using System.Linq;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.UserInterface;
using Content.Shared.Administration.Logs;
using Content.Shared.Audio;
using Content.Shared.Crayon;
using Content.Server.Decals;
using Content.Shared.Decals;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
@@ -60,11 +61,8 @@ namespace Content.Server.Crayon
Charges = Capacity;
// Get the first one from the catalog and set it as default
var decals = _prototypeManager.EnumeratePrototypes<CrayonDecalPrototype>().FirstOrDefault();
if (decals != null)
{
SelectedState = decals.Decals.First();
}
var decal = _prototypeManager.EnumeratePrototypes<DecalPrototype>().FirstOrDefault(x => x.Tags.Contains("crayon"));
SelectedState = decal?.ID ?? string.Empty;
Dirty();
}
@@ -74,15 +72,11 @@ namespace Content.Server.Crayon
{
case CrayonSelectMessage msg:
// Check if the selected state is valid
var crayonDecals = _prototypeManager.EnumeratePrototypes<CrayonDecalPrototype>().FirstOrDefault();
if (crayonDecals != null)
{
if (crayonDecals.Decals.Contains(msg.State))
if (_prototypeManager.TryIndex<DecalPrototype>(msg.State, out var prototype) && prototype.Tags.Contains("crayon"))
{
SelectedState = msg.State;
Dirty();
}
}
break;
default:
break;
@@ -131,12 +125,8 @@ namespace Content.Server.Crayon
return true;
}
var entity = IoCManager.Resolve<IEntityManager>().SpawnEntity("CrayonDecal", eventArgs.ClickLocation);
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out AppearanceComponent? appearance))
{
appearance.SetData(CrayonVisuals.State, SelectedState);
appearance.SetData(CrayonVisuals.Color, _color);
}
if(!EntitySystem.Get<DecalSystem>().TryAddDecal(SelectedState, eventArgs.ClickLocation.Offset(new Vector2(-0.5f,-0.5f)), out _, Color.FromName(_color), cleanable: true))
return false;
if (_useSound != null)
SoundSystem.Play(Filter.Pvs(Owner), _useSound.GetSound(), Owner, AudioHelpers.WithVariation(0.125f));

View File

@@ -51,7 +51,7 @@ namespace Content.Server.Damage.Systems
component.LastHit = _gameTiming.CurTime;
if (_robustRandom.Prob(component.StunChance))
_stunSystem.TryStun(uid, TimeSpan.FromSeconds(component.StunSeconds));
_stunSystem.TryStun(uid, TimeSpan.FromSeconds(component.StunSeconds), true);
var damageScale = (speed / component.MinimumSpeed) * component.Factor;

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Decals;
using Content.Shared.Maps;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Server.Decals.Commands
{
[AdminCommand(AdminFlags.Mapping)]
public sealed class AddDecalCommand : IConsoleCommand
{
public string Command => "adddecal";
public string Description => "Creates a decal on the map";
public string Help => $"{Command} <id> <x position> <y position> <gridId> [angle=<angle> zIndex=<zIndex> color=<color>]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 4 || args.Length > 7)
{
shell.WriteError($"Received invalid amount of arguments arguments. Expected 4 to 7, got {args.Length}.\nUsage: {Help}");
return;
}
if (!IoCManager.Resolve<IPrototypeManager>().HasIndex<DecalPrototype>(args[0]))
{
shell.WriteError($"Cannot find decalprototype '{args[0]}'.");
}
if (!float.TryParse(args[1], out var x))
{
shell.WriteError($"Failed parsing x-coordinate '{args[1]}'.");
return;
}
if (!float.TryParse(args[2], out var y))
{
shell.WriteError($"Failed parsing y-coordinate'{args[2]}'.");
return;
}
var mapManager = IoCManager.Resolve<IMapManager>();
if (!int.TryParse(args[3], out var gridIdRaw) || !mapManager.TryGetGrid(new GridId(gridIdRaw), out var grid))
{
shell.WriteError($"Failed parsing gridId '{args[3]}'.");
return;
}
var coordinates = new EntityCoordinates(grid.GridEntityId, new Vector2(x, y));
if (grid.GetTileRef(coordinates).IsSpace())
{
shell.WriteError($"Cannot create decal on space tile at {coordinates}.");
return;
}
Color? color = null;
var zIndex = 0;
Angle? rotation = null;
if (args.Length > 4)
{
for (int i = 4; i < args.Length; i++)
{
var rawValue = args[i].Split('=');
if (rawValue.Length != 2)
{
shell.WriteError($"Failed parsing parameter: '{args[i]}'");
return;
}
switch (rawValue[0])
{
case "angle":
if (!double.TryParse(rawValue[1], out var degrees))
{
shell.WriteError($"Failed parsing angle '{rawValue[1]}'.");
return;
}
rotation = Angle.FromDegrees(degrees);
break;
case "zIndex":
if (!int.TryParse(rawValue[1], out zIndex))
{
shell.WriteError($"Failed parsing zIndex '{rawValue[1]}'.");
return;
}
break;
case "color":
if (!Color.TryFromName(rawValue[1], out var colorRaw))
{
shell.WriteError($"Failed parsing color '{rawValue[1]}'.");
return;
}
color = colorRaw;
break;
default:
shell.WriteError($"Unknown parameter key '{rawValue[0]}'.");
return;
}
}
}
if(EntitySystem.Get<DecalSystem>().TryAddDecal(args[0], coordinates, out var uid, color, rotation, zIndex))
{
shell.WriteLine($"Successfully created decal {uid}.");
}
else
{
shell.WriteError($"Failed adding decal.");
}
}
}
}

View File

@@ -0,0 +1,162 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.Decals;
[AdminCommand(AdminFlags.Mapping)]
public class EditDecalCommand : IConsoleCommand
{
public string Command => "editdecal";
public string Description => "Edits a decal.";
public string Help => $@"{Command} <gridId> <uid> <mode>\n
Possible modes are:\n
- position <x position> <y position>\n
- color <color>\n
- id <id>\n
- rotation <degrees>\n
- zindex <zIndex>\n
- clean <cleanable>
";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 4)
{
shell.WriteError("Expected at least 5 arguments.");
return;
}
if (!int.TryParse(args[0], out var gridIdRaw))
{
shell.WriteError($"Failed parsing gridId '{args[3]}'.");
return;
}
if (!uint.TryParse(args[1], out var uid))
{
shell.WriteError($"Failed parsing uid '{args[1]}'.");
return;
}
var gridId = new GridId(gridIdRaw);
if (!IoCManager.Resolve<IMapManager>().GridExists(gridId))
{
shell.WriteError($"No grid with gridId {gridId} exists.");
return;
}
var decalSystem = EntitySystem.Get<DecalSystem>();
switch (args[2].ToLower())
{
case "position":
if(args.Length != 5)
{
shell.WriteError("Expected 6 arguments.");
return;
}
if (!float.TryParse(args[3], out var x) || !float.TryParse(args[4], out var y))
{
shell.WriteError("Failed parsing position.");
return;
}
if (!decalSystem.SetDecalPosition(gridId, uid, gridId, new Vector2(x, y)))
{
shell.WriteError("Failed changing decalposition.");
}
break;
case "color":
if(args.Length != 4)
{
shell.WriteError("Expected 5 arguments.");
return;
}
if (!Color.TryFromName(args[3], out var color))
{
shell.WriteError("Failed parsing color.");
return;
}
if (!decalSystem.SetDecalColor(gridId, uid, color))
{
shell.WriteError("Failed changing decal color.");
}
break;
case "id":
if(args.Length != 4)
{
shell.WriteError("Expected 5 arguments.");
return;
}
if (!decalSystem.SetDecalId(gridId, uid, args[3]))
{
shell.WriteError("Failed changing decal id.");
}
break;
case "rotation":
if(args.Length != 4)
{
shell.WriteError("Expected 5 arguments.");
return;
}
if (!double.TryParse(args[3], out var degrees))
{
shell.WriteError("Failed parsing degrees.");
return;
}
if (!decalSystem.SetDecalRotation(gridId, uid, Angle.FromDegrees(degrees)))
{
shell.WriteError("Failed changing decal rotation.");
}
break;
case "zindex":
if(args.Length != 4)
{
shell.WriteError("Expected 5 arguments.");
return;
}
if (!int.TryParse(args[3], out var zIndex))
{
shell.WriteError("Failed parsing zIndex.");
return;
}
if (!decalSystem.SetDecalZIndex(gridId, uid, zIndex))
{
shell.WriteError("Failed changing decal zIndex.");
}
break;
case "clean":
if(args.Length != 4)
{
shell.WriteError("Expected 5 arguments.");
return;
}
if (!bool.TryParse(args[3], out var cleanable))
{
shell.WriteError("Failed parsing cleanable.");
return;
}
if (!decalSystem.SetDecalCleanable(gridId, uid, cleanable))
{
shell.WriteError("Failed changing decal cleanable flag.");
}
break;
default:
shell.WriteError("Invalid mode.");
return;
}
}
}

View File

@@ -0,0 +1,46 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Server.Decals.Commands
{
[AdminCommand(AdminFlags.Mapping)]
public class RemoveDecalCommand : IConsoleCommand
{
public string Command => "rmdecal";
public string Description => "removes a decal";
public string Help => $"{Command} <uid> <gridId>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError($"Unexpected number of arguments.\nExpected two: {Help}");
return;
}
if (!uint.TryParse(args[0], out var uid))
{
shell.WriteError($"Failed parsing uid.");
return;
}
if (!int.TryParse(args[1], out var rawGridId) ||
!IoCManager.Resolve<IMapManager>().GridExists(new GridId(rawGridId)))
{
shell.WriteError("Failed parsing gridId.");
}
var decalSystem = EntitySystem.Get<DecalSystem>();
if (decalSystem.RemoveDecal(new GridId(rawGridId), uid))
{
shell.WriteLine($"Successfully removed decal {uid}.");
return;
}
shell.WriteError($"Failed trying to remove decal {uid}.");
}
}
}

View File

@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Decals;
using Content.Shared.Maps;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Player;
namespace Content.Server.Decals
{
public class DecalSystem : SharedDecalSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
private readonly Dictionary<GridId, HashSet<Vector2i>> _dirtyChunks = new();
private readonly Dictionary<IPlayerSession, Dictionary<GridId, HashSet<Vector2i>>> _previousSentChunks = new();
public override void Initialize()
{
base.Initialize();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
MapManager.TileChanged += OnTileChanged;
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
MapManager.TileChanged -= OnTileChanged;
}
private void OnTileChanged(object? sender, TileChangedEventArgs e)
{
if (!e.NewTile.IsSpace())
return;
var chunkCollection = ChunkCollection(e.NewTile.GridIndex);
var indices = GetChunkIndices(e.NewTile.GridIndices);
var toDelete = new HashSet<uint>();
if (chunkCollection.TryGetValue(indices, out var chunk))
{
foreach (var (uid, decal) in chunk)
{
if (new Vector2((int) Math.Floor(decal.Coordinates.X), (int) Math.Floor(decal.Coordinates.Y)) ==
e.NewTile.GridIndices)
{
toDelete.Add(uid);
}
}
}
if (toDelete.Count == 0) return;
foreach (var uid in toDelete)
{
RemoveDecalInternal(e.NewTile.GridIndex, uid);
}
DirtyChunk(e.NewTile.GridIndex, indices);
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
switch (e.NewStatus)
{
case SessionStatus.InGame:
_previousSentChunks[e.Session] = new();
break;
case SessionStatus.Disconnected:
_previousSentChunks.Remove(e.Session);
break;
}
}
protected override void DirtyChunk(GridId id, Vector2i chunkIndices)
{
if(!_dirtyChunks.ContainsKey(id))
_dirtyChunks[id] = new HashSet<Vector2i>();
_dirtyChunks[id].Add(chunkIndices);
}
public bool TryAddDecal(string id, EntityCoordinates coordinates, [NotNullWhen(true)] out uint? uid, Color? color = null, Angle? rotation = null, int zIndex = 0, bool cleanable = false)
{
uid = 0;
if (!PrototypeManager.HasIndex<DecalPrototype>(id))
return false;
var gridId = coordinates.GetGridId(EntityManager);
if (MapManager.GetGrid(gridId).GetTileRef(coordinates).IsSpace())
return false;
rotation ??= Angle.Zero;
var decal = new Decal(coordinates.Position, id, color, rotation.Value, zIndex, cleanable);
var chunkCollection = DecalGridChunkCollection(gridId);
uid = chunkCollection.NextUid++;
var chunkIndices = GetChunkIndices(decal.Coordinates);
if(!chunkCollection.ChunkCollection.ContainsKey(chunkIndices))
chunkCollection.ChunkCollection[chunkIndices] = new();
chunkCollection.ChunkCollection[chunkIndices][uid.Value] = decal;
ChunkIndex[gridId][uid.Value] = chunkIndices;
DirtyChunk(gridId, chunkIndices);
return true;
}
public bool RemoveDecal(GridId gridId, uint uid) => RemoveDecalInternal(gridId, uid);
public HashSet<uint> GetDecalsInRange(GridId gridId, Vector2 position, float distance = 0.75f, Func<Decal, bool>? validDelegate = null)
{
var uids = new HashSet<uint>();
var chunkCollection = ChunkCollection(gridId);
var chunkIndices = GetChunkIndices(position);
if (!chunkCollection.TryGetValue(chunkIndices, out var chunk))
return uids;
foreach (var (uid, decal) in chunk)
{
if ((position - decal.Coordinates-new Vector2(0.5f, 0.5f)).Length > distance)
continue;
if (validDelegate == null || validDelegate(decal))
{
uids.Add(uid);
}
}
return uids;
}
public bool SetDecalPosition(GridId gridId, uint uid, EntityCoordinates coordinates)
{
return SetDecalPosition(gridId, uid, coordinates.GetGridId(EntityManager), coordinates.Position);
}
public bool SetDecalPosition(GridId gridId, uint uid, GridId newGridId, Vector2 position)
{
if (!ChunkIndex.TryGetValue(gridId, out var values) || !values.TryGetValue(uid, out var indices))
{
return false;
}
DirtyChunk(gridId, indices);
var chunkCollection = ChunkCollection(gridId);
var decal = chunkCollection[indices][uid];
if (newGridId == gridId)
{
chunkCollection[indices][uid] = decal.WithCoordinates(position);
return true;
}
RemoveDecalInternal(gridId, uid);
var newChunkCollection = ChunkCollection(newGridId);
var chunkIndices = GetChunkIndices(position);
if(!newChunkCollection.ContainsKey(chunkIndices))
newChunkCollection[chunkIndices] = new();
newChunkCollection[chunkIndices][uid] = decal.WithCoordinates(position);
ChunkIndex[newGridId][uid] = chunkIndices;
DirtyChunk(newGridId, chunkIndices);
return true;
}
public bool SetDecalColor(GridId gridId, uint uid, Color? color)
{
if (!ChunkIndex.TryGetValue(gridId, out var values) || !values.TryGetValue(uid, out var indices))
{
return false;
}
var chunkCollection = ChunkCollection(gridId);
var decal = chunkCollection[indices][uid];
chunkCollection[indices][uid] = decal.WithColor(color);
DirtyChunk(gridId, indices);
return true;
}
public bool SetDecalId(GridId gridId, uint uid, string id)
{
if (!ChunkIndex.TryGetValue(gridId, out var values) || !values.TryGetValue(uid, out var indices))
{
return false;
}
if (!PrototypeManager.HasIndex<DecalPrototype>(id))
throw new ArgumentOutOfRangeException($"Tried to set decal id to invalid prototypeid: {id}");
var chunkCollection = ChunkCollection(gridId);
var decal = chunkCollection[indices][uid];
chunkCollection[indices][uid] = decal.WithId(id);
DirtyChunk(gridId, indices);
return true;
}
public bool SetDecalRotation(GridId gridId, uint uid, Angle angle)
{
if (!ChunkIndex.TryGetValue(gridId, out var values) || !values.TryGetValue(uid, out var indices))
{
return false;
}
var chunkCollection = ChunkCollection(gridId);
var decal = chunkCollection[indices][uid];
chunkCollection[indices][uid] = decal.WithRotation(angle);
DirtyChunk(gridId, indices);
return true;
}
public bool SetDecalZIndex(GridId gridId, uint uid, int zIndex)
{
if (!ChunkIndex.TryGetValue(gridId, out var values) || !values.TryGetValue(uid, out var indices))
{
return false;
}
var chunkCollection = ChunkCollection(gridId);
var decal = chunkCollection[indices][uid];
chunkCollection[indices][uid] = decal.WithZIndex(zIndex);
DirtyChunk(gridId, indices);
return true;
}
public bool SetDecalCleanable(GridId gridId, uint uid, bool cleanable)
{
if (!ChunkIndex.TryGetValue(gridId, out var values) || !values.TryGetValue(uid, out var indices))
{
return false;
}
var chunkCollection = ChunkCollection(gridId);
var decal = chunkCollection[indices][uid];
chunkCollection[indices][uid] = decal.WithCleanable(cleanable);
DirtyChunk(gridId, indices);
return true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var session in Filter.Broadcast().Recipients)
{
if(session is not IPlayerSession playerSession || playerSession.Status != SessionStatus.InGame)
continue;
var chunks = GetChunksForSession(playerSession);
var updatedChunks = new Dictionary<GridId, HashSet<Vector2i>>();
foreach (var (gridId, gridChunks) in chunks)
{
var newChunks = new HashSet<Vector2i>(gridChunks);
if (_previousSentChunks[playerSession].TryGetValue(gridId, out var previousChunks))
{
newChunks.ExceptWith(previousChunks);
}
if (_dirtyChunks.TryGetValue(gridId, out var dirtyChunks))
{
gridChunks.IntersectWith(dirtyChunks);
newChunks.UnionWith(gridChunks);
}
if (newChunks.Count == 0)
continue;
updatedChunks[gridId] = newChunks;
}
if(updatedChunks.Count == 0)
continue;
_previousSentChunks[playerSession] = chunks;
//send all gridChunks to client
SendChunkUpdates(playerSession, updatedChunks);
}
_dirtyChunks.Clear();
}
private void SendChunkUpdates(IPlayerSession session, Dictionary<GridId, HashSet<Vector2i>> updatedChunks)
{
var updatedDecals = new Dictionary<GridId, Dictionary<Vector2i, Dictionary<uint, Decal>>>();
foreach (var (gridId, chunks) in updatedChunks)
{
var gridChunks = new Dictionary<Vector2i, Dictionary<uint, Decal>>();
foreach (var indices in chunks)
{
gridChunks.Add(indices,
ChunkCollection(gridId).TryGetValue(indices, out var chunk)
? chunk
: new Dictionary<uint, Decal>());
}
updatedDecals[gridId] = gridChunks;
}
RaiseNetworkEvent(new DecalChunkUpdateEvent{Data = updatedDecals}, Filter.SinglePlayer(session));
}
private HashSet<EntityUid> GetSessionViewers(IPlayerSession session)
{
var viewers = new HashSet<EntityUid>();
if (session.Status != SessionStatus.InGame || session.AttachedEntity is null)
return viewers;
viewers.Add(session.AttachedEntity.Value);
foreach (var uid in session.ViewSubscriptions)
{
viewers.Add(uid);
}
return viewers;
}
private Dictionary<GridId, HashSet<Vector2i>> GetChunksForSession(IPlayerSession session)
{
return GetChunksForViewers(GetSessionViewers(session));
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Server.Fluids.EntitySystems;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -20,10 +21,10 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
/// </summary>
/// <param name="owner">Entity on which behavior is executed</param>
/// <param name="system">system calling the behavior</param>
/// <param name="entityManager"></param>
public void Execute(EntityUid owner, DestructibleSystem system)
{
var solutionContainerSystem = EntitySystem.Get<SolutionContainerSystem>();
var spillableSystem = EntitySystem.Get<SpillableSystem>();
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
@@ -31,12 +32,12 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
solutionContainerSystem.TryGetSolution(owner, spillableComponent.SolutionName,
out var compSolution))
{
compSolution.SpillAt(coordinates, "PuddleSmear", false);
spillableSystem.SpillAt(compSolution, coordinates, "PuddleSmear", false);
}
else if (Solution != null &&
solutionContainerSystem.TryGetSolution(owner, Solution, out var behaviorSolution))
{
behaviorSolution.SpillAt(coordinates, "PuddleSmear", false);
spillableSystem.SpillAt(behaviorSolution, coordinates, "PuddleSmear", false);
}
}
}

View File

@@ -12,8 +12,8 @@ namespace Content.Server.DoAfter
public sealed class DoAfterSystem : EntitySystem
{
// We cache these lists as to not allocate them every update tick...
private readonly List<DoAfter> _cancelled = new();
private readonly List<DoAfter> _finished = new();
private readonly Queue<DoAfter> _cancelled = new();
private readonly Queue<DoAfter> _finished = new();
public override void Initialize()
{
@@ -52,17 +52,17 @@ namespace Content.Server.DoAfter
case DoAfterStatus.Running:
break;
case DoAfterStatus.Cancelled:
_cancelled.Add(doAfter);
_cancelled.Enqueue(doAfter);
break;
case DoAfterStatus.Finished:
_finished.Add(doAfter);
_finished.Enqueue(doAfter);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
foreach (var doAfter in _cancelled)
while (_cancelled.TryDequeue(out var doAfter))
{
comp.Cancelled(doAfter);
@@ -76,7 +76,7 @@ namespace Content.Server.DoAfter
RaiseLocalEvent(doAfter.EventArgs.BroadcastCancelledEvent);
}
foreach (var doAfter in _finished)
while (_finished.TryDequeue(out var doAfter))
{
comp.Finished(doAfter);
@@ -89,10 +89,6 @@ namespace Content.Server.DoAfter
if(doAfter.EventArgs.BroadcastFinishedEvent != null)
RaiseLocalEvent(doAfter.EventArgs.BroadcastFinishedEvent);
}
// Clean the shared lists at the end, ensuring they'll be clean for the next time we need them.
_cancelled.Clear();
_finished.Clear();
}
}

View File

@@ -584,7 +584,7 @@ namespace Content.Server.Doors.Components
if (IoCManager.Resolve<IEntityManager>().HasComponent<DamageableComponent>(e.Owner))
EntitySystem.Get<DamageableSystem>().TryChangeDamage(e.Owner, CrushDamage);
EntitySystem.Get<StunSystem>().TryParalyze(e.Owner, TimeSpan.FromSeconds(DoorStunTime));
EntitySystem.Get<StunSystem>().TryParalyze(e.Owner, TimeSpan.FromSeconds(DoorStunTime), true);
}
// If we hit someone, open up after stun (opens right when stun ends)

View File

@@ -26,6 +26,9 @@ namespace Content.Server.Electrocution
[DataField("onHandInteract")]
public bool OnHandInteract { get; set; } = true;
[DataField("onInteractUsing")]
public bool OnInteractUsing { get; set; } = true;
[DataField("requirePower")]
public bool RequirePower { get; } = true;

View File

@@ -52,7 +52,7 @@ namespace Content.Server.Electrocution
}
entityManager.EntitySysManager.GetEntitySystem<ElectrocutionSystem>()
.TryDoElectrocution(uid, null, damage, TimeSpan.FromSeconds(seconds));
.TryDoElectrocution(uid, null, damage, TimeSpan.FromSeconds(seconds), true);
}
}
}

View File

@@ -73,6 +73,7 @@ namespace Content.Server.Electrocution
SubscribeLocalEvent<ElectrifiedComponent, StartCollideEvent>(OnElectrifiedStartCollide);
SubscribeLocalEvent<ElectrifiedComponent, AttackedEvent>(OnElectrifiedAttacked);
SubscribeLocalEvent<ElectrifiedComponent, InteractHandEvent>(OnElectrifiedHandInteract);
SubscribeLocalEvent<ElectrifiedComponent, InteractUsingEvent>(OnElectrifiedInteractUsing);
SubscribeLocalEvent<RandomInsulationComponent, MapInitEvent>(OnRandomInsulationMapInit);
UpdatesAfter.Add(typeof(PowerNetSystem));
@@ -140,6 +141,14 @@ namespace Content.Server.Electrocution
TryDoElectrifiedAct(uid, args.User, electrified);
}
private void OnElectrifiedInteractUsing(EntityUid uid, ElectrifiedComponent electrified, InteractUsingEvent args)
{
if (!electrified.OnInteractUsing)
return;
TryDoElectrifiedAct(uid, args.User, electrified);
}
public bool TryDoElectrifiedAct(EntityUid uid, EntityUid targetUid,
ElectrifiedComponent? electrified = null,
NodeContainerComponent? nodeContainer = null,
@@ -177,7 +186,7 @@ namespace Content.Server.Electrocution
entity,
uid,
(int) (electrified.ShockDamage * MathF.Pow(RecursiveDamageMultiplier, depth)),
TimeSpan.FromSeconds(electrified.ShockTime * MathF.Pow(RecursiveTimeMultiplier, depth)),
TimeSpan.FromSeconds(electrified.ShockTime * MathF.Pow(RecursiveTimeMultiplier, depth)), true,
electrified.SiemensCoefficient);
}
@@ -213,7 +222,7 @@ namespace Content.Server.Electrocution
node,
(int) (electrified.ShockDamage * MathF.Pow(RecursiveDamageMultiplier, depth) * damageMult),
TimeSpan.FromSeconds(electrified.ShockTime * MathF.Pow(RecursiveTimeMultiplier, depth) *
timeMult),
timeMult), true,
electrified.SiemensCoefficient);
}
@@ -235,12 +244,12 @@ namespace Content.Server.Electrocution
/// <returns>Whether the entity <see cref="uid"/> was stunned by the shock.</returns>
public bool TryDoElectrocution(
EntityUid uid, EntityUid? sourceUid, int shockDamage, TimeSpan time, float siemensCoefficient = 1f,
EntityUid uid, EntityUid? sourceUid, int shockDamage, TimeSpan time, bool refresh, float siemensCoefficient = 1f,
StatusEffectsComponent? statusEffects = null,
SharedAlertsComponent? alerts = null)
{
if (!DoCommonElectrocutionAttempt(uid, sourceUid, ref siemensCoefficient)
|| !DoCommonElectrocution(uid, sourceUid, shockDamage, time, siemensCoefficient, statusEffects, alerts))
|| !DoCommonElectrocution(uid, sourceUid, shockDamage, time, refresh, siemensCoefficient, statusEffects, alerts))
return false;
RaiseLocalEvent(uid, new ElectrocutedEvent(uid, sourceUid, siemensCoefficient));
@@ -254,6 +263,7 @@ namespace Content.Server.Electrocution
Node node,
int shockDamage,
TimeSpan time,
bool refresh,
float siemensCoefficient = 1f,
StatusEffectsComponent? statusEffects = null,
SharedAlertsComponent? alerts = null,
@@ -264,9 +274,9 @@ namespace Content.Server.Electrocution
// Coefficient needs to be higher than this to do a powered electrocution!
if(siemensCoefficient <= 0.5f)
return DoCommonElectrocution(uid, sourceUid, shockDamage, time, siemensCoefficient, statusEffects, alerts);
return DoCommonElectrocution(uid, sourceUid, shockDamage, time, refresh, siemensCoefficient, statusEffects, alerts);
if (!DoCommonElectrocution(uid, sourceUid, null, time, siemensCoefficient, statusEffects, alerts))
if (!DoCommonElectrocution(uid, sourceUid, null, time, refresh, siemensCoefficient, statusEffects, alerts))
return false;
if (!Resolve(sourceUid, ref sourceTransform)) // This shouldn't really happen, but just in case...
@@ -307,7 +317,7 @@ namespace Content.Server.Electrocution
}
private bool DoCommonElectrocution(EntityUid uid, EntityUid? sourceUid,
int? shockDamage, TimeSpan time, float siemensCoefficient = 1f,
int? shockDamage, TimeSpan time, bool refresh, float siemensCoefficient = 1f,
StatusEffectsComponent? statusEffects = null,
SharedAlertsComponent? alerts = null)
{
@@ -329,14 +339,14 @@ namespace Content.Server.Electrocution
!_statusEffectsSystem.CanApplyEffect(uid, StatusEffectKey, statusEffects))
return false;
if (!_statusEffectsSystem.TryAddStatusEffect<ElectrocutedComponent>(uid, StatusEffectKey, time,
if (!_statusEffectsSystem.TryAddStatusEffect<ElectrocutedComponent>(uid, StatusEffectKey, time, refresh,
statusEffects, alerts))
return false;
var shouldStun = siemensCoefficient > 0.5f;
if (shouldStun)
_stunSystem.TryParalyze(uid, time * ParalyzeTimeMultiplier, statusEffects, alerts);
_stunSystem.TryParalyze(uid, time * ParalyzeTimeMultiplier, refresh, statusEffects, alerts);
// TODO: Sparks here.
@@ -350,8 +360,8 @@ namespace Content.Server.Electrocution
$"{statusEffects.Owner} took {actual.Total} powered electrocution damage");
}
_stutteringSystem.DoStutter(uid, time * StutteringTimeMultiplier, statusEffects, alerts);
_jitteringSystem.DoJitter(uid, time * JitterTimeMultiplier, JitterAmplitude, JitterFrequency, true,
_stutteringSystem.DoStutter(uid, time * StutteringTimeMultiplier, refresh, statusEffects, alerts);
_jitteringSystem.DoJitter(uid, time * JitterTimeMultiplier, refresh, JitterAmplitude, JitterFrequency, true,
statusEffects, alerts);
_popupSystem.PopupEntity(Loc.GetString("electrocuted-component-mob-shocked-popup-player"), uid,

View File

@@ -294,6 +294,7 @@ namespace Content.Server.Explosion.EntitySystems
int heavyImpactRange = 0,
int lightImpactRange = 0,
int flashRange = 0,
EntityUid? user = null,
ExplosiveComponent? explosive = null,
TransformComponent? transform = null)
{
@@ -306,7 +307,7 @@ namespace Content.Server.Explosion.EntitySystems
if (explosive is { Exploding: false })
{
_triggers.Explode(entity, explosive);
_triggers.Explode(entity, explosive, user);
}
else
{
@@ -322,7 +323,7 @@ namespace Content.Server.Explosion.EntitySystems
var epicenter = transform.Coordinates;
SpawnExplosion(epicenter, devastationRange, heavyImpactRange, lightImpactRange, flashRange);
SpawnExplosion(epicenter, devastationRange, heavyImpactRange, lightImpactRange, flashRange, entity, user);
}
}
@@ -331,7 +332,9 @@ namespace Content.Server.Explosion.EntitySystems
int devastationRange = 0,
int heavyImpactRange = 0,
int lightImpactRange = 0,
int flashRange = 0)
int flashRange = 0,
EntityUid? entity = null,
EntityUid? user = null)
{
var mapId = epicenter.GetMapId(EntityManager);
if (mapId == MapId.Nullspace)
@@ -339,8 +342,22 @@ namespace Content.Server.Explosion.EntitySystems
return;
}
_logSystem.Add(LogType.Damaged, LogImpact.High ,
$"Spawned explosion at {epicenter} with range {devastationRange}/{heavyImpactRange}/{lightImpactRange}/{flashRange}");
// logging
var text = $"{epicenter} with range {devastationRange}/{heavyImpactRange}/{lightImpactRange}/{flashRange}";
if (entity == null)
{
_logSystem.Add(LogType.Explosion, LogImpact.High, $"Explosion spawned at {text}");
}
else if (user == null)
{
_logSystem.Add(LogType.Explosion, LogImpact.High,
$"{entity.Value} exploded at {text}");
}
else
{
_logSystem.Add(LogType.Explosion, LogImpact.High,
$"{entity.Value} caused {entity.Value} to explode at {text}");
}
var maxRange = MathHelper.Max(devastationRange, heavyImpactRange, lightImpactRange, 0);
var epicenterMapPos = epicenter.ToMapPos(EntityManager);

View File

@@ -1,10 +1,14 @@
using System;
using Content.Server.Administration.Logs;
using Content.Server.Doors.Components;
using Content.Server.Explosion.Components;
using Content.Server.Flash;
using Content.Server.Flash.Components;
using Content.Server.Projectiles.Components;
using Content.Shared.Audio;
using Content.Shared.Database;
using Content.Shared.Doors;
using Content.Shared.Throwing;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
@@ -21,9 +25,9 @@ namespace Content.Server.Explosion.EntitySystems
public class TriggerEvent : HandledEntityEventArgs
{
public EntityUid Triggered { get; }
public EntityUid User { get; }
public EntityUid? User { get; }
public TriggerEvent(EntityUid triggered, EntityUid user = default)
public TriggerEvent(EntityUid triggered, EntityUid? user = null)
{
Triggered = triggered;
User = user;
@@ -35,6 +39,7 @@ namespace Content.Server.Explosion.EntitySystems
{
[Dependency] private readonly ExplosionSystem _explosions = default!;
[Dependency] private readonly FlashSystem _flashSystem = default!;
[Dependency] private readonly AdminLogSystem _logSystem = default!;
public override void Initialize()
{
@@ -53,11 +58,11 @@ namespace Content.Server.Explosion.EntitySystems
{
if (!EntityManager.TryGetComponent(uid, out ExplosiveComponent? explosiveComponent)) return;
Explode(uid, explosiveComponent);
Explode(uid, explosiveComponent, args.User);
}
// You really shouldn't call this directly (TODO Change that when ExplosionHelper gets changed).
public void Explode(EntityUid uid, ExplosiveComponent component)
public void Explode(EntityUid uid, ExplosiveComponent component, EntityUid? user = null)
{
if (component.Exploding)
{
@@ -65,7 +70,12 @@ namespace Content.Server.Explosion.EntitySystems
}
component.Exploding = true;
_explosions.SpawnExplosion(uid, component.DevastationRange, component.HeavyImpactRange, component.LightImpactRange, component.FlashRange);
_explosions.SpawnExplosion(uid,
component.DevastationRange,
component.HeavyImpactRange,
component.LightImpactRange,
component.FlashRange,
user);
EntityManager.QueueDeleteEntity(uid);
}
#endregion
@@ -113,16 +123,23 @@ namespace Content.Server.Explosion.EntitySystems
private void HandleCollide(EntityUid uid, TriggerOnCollideComponent component, StartCollideEvent args)
{
Trigger(component.Owner);
EntityUid? user = null;
if (EntityManager.TryGetComponent(uid, out ProjectileComponent projectile))
user = projectile.Shooter;
else if (EntityManager.TryGetComponent(uid, out ThrownItemComponent thrown))
user = thrown.Thrower;
Trigger(component.Owner, user);
}
public void Trigger(EntityUid trigger, EntityUid user = default)
public void Trigger(EntityUid trigger, EntityUid? user = null)
{
var triggerEvent = new TriggerEvent(trigger, user);
EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent);
}
public void HandleTimerTrigger(TimeSpan delay, EntityUid triggered, EntityUid user = default)
public void HandleTimerTrigger(TimeSpan delay, EntityUid triggered, EntityUid? user = null)
{
if (delay.TotalSeconds <= 0)
{

View File

@@ -129,7 +129,7 @@ namespace Content.Server.Flash
flashable.Dirty();
}
_stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration/1000f),
_stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration/1000f), true,
slowTo, slowTo);
if (displayPopup && user != null && target != user)

View File

@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
@@ -89,6 +90,7 @@ namespace Content.Server.Fluids.Components
* will spill some of the mop's solution onto the puddle which will evaporate eventually.
*/
var solutionSystem = EntitySystem.Get<SolutionContainerSystem>();
var spillableSystem = EntitySystem.Get<SpillableSystem>();
if (!solutionSystem.TryGetSolution(Owner, SolutionName, out var contents ) ||
Mopping ||
@@ -106,8 +108,8 @@ namespace Content.Server.Fluids.Components
if (eventArgs.Target is not {Valid: true} target)
{
// Drop the liquid on the mop on to the ground
solutionSystem.SplitSolution(Owner, contents, FixedPoint2.Min(ResidueAmount, CurrentVolume))
.SpillAt(eventArgs.ClickLocation, "PuddleSmear");
var solution = solutionSystem.SplitSolution(Owner, contents, FixedPoint2.Min(ResidueAmount, CurrentVolume));
spillableSystem.SpillAt(solution, eventArgs.ClickLocation, "PuddleSmear");
return true;
}
@@ -147,9 +149,9 @@ namespace Content.Server.Fluids.Components
// After cleaning the puddle, make a new puddle with solution from the mop as a "wet floor". Then evaporate it slowly.
// we do this WITHOUT adding to the existing puddle. Otherwise we have might have water puddles with the vomit sprite.
solutionSystem.SplitSolution(Owner, contents, transferAmount)
.SplitSolution(ResidueAmount)
.SpillAt(eventArgs.ClickLocation, "PuddleSmear", combine: false);
var splitSolution = solutionSystem.SplitSolution(Owner, contents, transferAmount)
.SplitSolution(ResidueAmount);
spillableSystem.SpillAt(splitSolution, eventArgs.ClickLocation, "PuddleSmear", combine: false);
}
else
{

View File

@@ -1,184 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Coordinates.Helpers;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Server.Fluids.Components
{
// TODO: Kill these with fire
public static class SpillExtensions
{
/// <summary>
/// Spills the specified solution at the entity's location if possible.
/// </summary>
/// <param name="entity">
/// The entity to use as a location to spill the solution at.
/// </param>
/// <param name="solution">Initial solution for the prototype.</param>
/// <param name="prototype">The prototype to use.</param>
/// <param name="sound">Play the spill sound.</param>
/// <returns>The puddle if one was created, null otherwise.</returns>
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
public static PuddleComponent? SpillAt(this Solution solution, EntityUid entity, string prototype,
bool sound = true, bool combine = true)
{
return solution.SpillAt(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(entity).Coordinates, prototype, sound, combine: combine);
}
/// <summary>
/// Spills the specified solution at the entity's location if possible.
/// </summary>
/// <param name="entity">
/// The entity to use as a location to spill the solution at.
/// </param>
/// <param name="solution">Initial solution for the prototype.</param>
/// <param name="prototype">The prototype to use.</param>
/// <param name="puddle">The puddle if one was created, null otherwise.</param>
/// <param name="sound">Play the spill sound.</param>
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
/// <returns>True if a puddle was created, false otherwise.</returns>
public static bool TrySpillAt(this Solution solution, EntityUid entity, string prototype,
[NotNullWhen(true)] out PuddleComponent? puddle, bool sound = true, bool combine = true)
{
puddle = solution.SpillAt(entity, prototype, sound, combine: combine);
return puddle != null;
}
/// <summary>
/// Spills solution at the specified grid coordinates.
/// </summary>
/// <param name="solution">Initial solution for the prototype.</param>
/// <param name="coordinates">The coordinates to spill the solution at.</param>
/// <param name="prototype">The prototype to use.</param>
/// <param name="sound">Whether or not to play the spill sound.</param>
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
/// <returns>The puddle if one was created, null otherwise.</returns>
public static PuddleComponent? SpillAt(this Solution solution, EntityCoordinates coordinates, string prototype,
bool overflow = true, bool sound = true, bool combine = true)
{
if (solution.TotalVolume == 0) return null;
var mapManager = IoCManager.Resolve<IMapManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!mapManager.TryGetGrid(coordinates.GetGridId(entityManager), out var mapGrid))
return null; // Let's not spill to space.
return SpillAt(mapGrid.GetTileRef(coordinates), solution, prototype, overflow, sound, combine: combine);
}
/// <summary>
/// Spills the specified solution at the entity's location if possible.
/// </summary>
/// <param name="coordinates">The coordinates to spill the solution at.</param>
/// <param name="solution">Initial solution for the prototype.</param>
/// <param name="prototype">The prototype to use.</param>
/// <param name="puddle">The puddle if one was created, null otherwise.</param>
/// <param name="sound">Play the spill sound.</param>
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
/// <returns>True if a puddle was created, false otherwise.</returns>
public static bool TrySpillAt(this Solution solution, EntityCoordinates coordinates, string prototype,
[NotNullWhen(true)] out PuddleComponent? puddle, bool sound = true, bool combine = true)
{
puddle = solution.SpillAt(coordinates, prototype, sound, combine: combine);
return puddle != null;
}
public static bool TryGetPuddle(this TileRef tileRef, GridTileLookupSystem? gridTileLookupSystem,
[NotNullWhen(true)] out PuddleComponent? puddle)
{
foreach (var entity in tileRef.GetEntitiesInTileFast(gridTileLookupSystem))
{
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out PuddleComponent? p))
{
puddle = p;
return true;
}
}
puddle = null;
return false;
}
public static PuddleComponent? SpillAt(this TileRef tileRef, Solution solution, string prototype,
bool overflow = true, bool sound = true, bool noTileReact = false, bool combine = true)
{
if (solution.TotalVolume <= 0) return null;
// If space return early, let that spill go out into the void
if (tileRef.Tile.IsEmpty) return null;
var mapManager = IoCManager.Resolve<IMapManager>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var serverEntityManager = IoCManager.Resolve<IServerEntityManager>();
var gridId = tileRef.GridIndex;
if (!mapManager.TryGetGrid(gridId, out var mapGrid)) return null; // Let's not spill to invalid grids.
if (!noTileReact)
{
// First, do all tile reactions
foreach (var reagent in solution.Contents.ToArray())
{
var proto = prototypeManager.Index<ReagentPrototype>(reagent.ReagentId);
proto.ReactionTile(tileRef, reagent.Quantity);
}
}
// Tile reactions used up everything.
if (solution.CurrentVolume == FixedPoint2.Zero)
return null;
// Get normalized co-ordinate for spill location and spill it in the centre
// TODO: Does SnapGrid or something else already do this?
var spillGridCoords = mapGrid.GridTileToWorld(tileRef.GridIndices);
var spillEntities = IoCManager.Resolve<IEntityLookup>()
.GetEntitiesIntersecting(mapGrid.ParentMapId, spillGridCoords.Position).ToArray();
foreach (var spillEntity in spillEntities)
{
if (EntitySystem.Get<SolutionContainerSystem>()
.TryGetRefillableSolution(spillEntity, out var solutionContainerComponent))
{
EntitySystem.Get<SolutionContainerSystem>().Refill(spillEntity, solutionContainerComponent,
solution.SplitSolution(FixedPoint2.Min(
solutionContainerComponent.AvailableVolume,
solutionContainerComponent.MaxSpillRefill))
);
}
}
var puddleSystem = EntitySystem.Get<PuddleSystem>();
if (combine)
{
foreach (var spillEntity in spillEntities)
{
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(spillEntity, out PuddleComponent? puddleComponent)) continue;
if (!overflow && puddleSystem.WouldOverflow(puddleComponent.Owner, solution, puddleComponent)) return null;
if (!puddleSystem.TryAddSolution(puddleComponent.Owner, solution, sound)) continue;
return puddleComponent;
}
}
var puddleEnt = serverEntityManager.SpawnEntity(prototype, spillGridCoords);
var newPuddleComponent = IoCManager.Resolve<IEntityManager>().GetComponent<PuddleComponent>(puddleEnt);
puddleSystem.TryAddSolution(newPuddleComponent.Owner, solution, sound);
return newPuddleComponent;
}
}
}

View File

@@ -38,7 +38,6 @@ namespace Content.Server.Fluids.EntitySystems
base.Initialize();
SubscribeLocalEvent<PuddleComponent, UnanchoredEvent>(OnUnanchored);
SubscribeLocalEvent<SpillableComponent, GetOtherVerbsEvent>(AddSpillVerb);
SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
SubscribeLocalEvent<PuddleComponent, SolutionChangedEvent>(OnUpdate);
SubscribeLocalEvent<PuddleComponent, ComponentInit>(OnInit);
@@ -88,26 +87,6 @@ namespace Content.Server.Fluids.EntitySystems
}
}
private void AddSpillVerb(EntityUid uid, SpillableComponent component, GetOtherVerbsEvent args)
{
if (!args.CanAccess || !args.CanInteract)
return;
if (!_solutionContainerSystem.TryGetDrainableSolution(args.Target, out var solution))
return;
if (solution.DrainAvailable == FixedPoint2.Zero)
return;
Verb verb = new();
verb.Text = Loc.GetString("spill-target-verb-get-data-text");
// TODO VERB ICONS spill icon? pouring out a glass/beaker?
verb.Act = () => _solutionContainerSystem.SplitSolution(args.Target,
solution, solution.DrainAvailable).SpillAt(EntityManager.GetComponent<TransformComponent>(args.Target).Coordinates, "PuddleSmear");
verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
args.Verbs.Add(verb);
}
private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args)
{
if (EntityManager.TryGetComponent<SlipperyComponent>(uid, out var slippery) && slippery.Slippery)

View File

@@ -1,12 +1,22 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Construction.Components;
using Content.Server.Coordinates.Helpers;
using Content.Server.Fluids.Components;
using Content.Shared.Examine;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Server.Fluids.EntitySystems;
@@ -14,20 +24,178 @@ namespace Content.Server.Fluids.EntitySystems;
public class SpillableSystem : EntitySystem
{
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityLookup _entityLookup = default!;
[Dependency] private readonly GridTileLookupSystem _gridTileLookupSystem = default!;
[Dependency] private readonly AdminLogSystem _logSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
SubscribeLocalEvent<SpillableComponent, GetOtherVerbsEvent>(AddSpillVerb);
}
void SpillOnLand(EntityUid uid, SpillableComponent component, LandEvent args) {
if (args.User != null && _solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solutionComponent))
/// <summary>
/// Spills the specified solution at the entity's location if possible.
/// </summary>
/// <param name="uid">
/// The entity to use as a location to spill the solution at.
/// </param>
/// <param name="solution">Initial solution for the prototype.</param>
/// <param name="prototype">The prototype to use.</param>
/// <param name="sound">Play the spill sound.</param>
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
/// <param name="transformComponent">Optional Transform component</param>
/// <returns>The puddle if one was created, null otherwise.</returns>
public PuddleComponent? SpillAt(EntityUid uid, Solution solution, string prototype,
bool sound = true, bool combine = true, TransformComponent? transformComponent = null)
{
_solutionContainerSystem
.Drain(uid, solutionComponent, solutionComponent.DrainAvailable)
.SpillAt(EntityManager.GetComponent<TransformComponent>(uid).Coordinates, "PuddleSmear");
return !Resolve(uid, ref transformComponent, false)
? null
: SpillAt(solution, transformComponent.Coordinates, prototype, sound: sound, combine: combine);
}
private void SpillOnLand(EntityUid uid, SpillableComponent component, LandEvent args)
{
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution)) return;
if (args.User != null)
{
_logSystem.Add(LogType.Landed,
$"{EntityManager.ToPrettyString(uid)} spilled {SolutionContainerSystem.ToPrettyString(solution)} on landing");
}
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.DrainAvailable);
SpillAt(drainedSolution, EntityManager.GetComponent<TransformComponent>(uid).Coordinates, "PuddleSmear");
}
private void AddSpillVerb(EntityUid uid, SpillableComponent component, GetOtherVerbsEvent args)
{
if (!args.CanAccess || !args.CanInteract)
return;
if (!_solutionContainerSystem.TryGetDrainableSolution(args.Target, out var solution))
return;
if (solution.DrainAvailable == FixedPoint2.Zero)
return;
Verb verb = new();
verb.Text = Loc.GetString("spill-target-verb-get-data-text");
// TODO VERB ICONS spill icon? pouring out a glass/beaker?
verb.Act = () =>
{
var puddleSolution = _solutionContainerSystem.SplitSolution(args.Target,
solution, solution.DrainAvailable);
SpillAt(puddleSolution, Transform(args.Target).Coordinates, "PuddleSmear");
};
verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
args.Verbs.Add(verb);
}
/// <summary>
/// Spills solution at the specified grid coordinates.
/// </summary>
/// <param name="solution">Initial solution for the prototype.</param>
/// <param name="coordinates">The coordinates to spill the solution at.</param>
/// <param name="prototype">The prototype to use.</param>
/// <param name="overflow">If the puddle overflow will be calculated. Defaults to true.</param>
/// <param name="sound">Whether or not to play the spill sound.</param>
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
/// <returns>The puddle if one was created, null otherwise.</returns>
public PuddleComponent? SpillAt(Solution solution, EntityCoordinates coordinates, string prototype,
bool overflow = true, bool sound = true, bool combine = true)
{
if (solution.TotalVolume == 0) return null;
if (!_mapManager.TryGetGrid(coordinates.GetGridId(EntityManager), out var mapGrid))
return null; // Let's not spill to space.
return SpillAt(mapGrid.GetTileRef(coordinates), solution, prototype, overflow, sound,
combine: combine);
}
public bool TryGetPuddle(TileRef tileRef, [NotNullWhen(true)] out PuddleComponent? puddle)
{
foreach (var entity in tileRef.GetEntitiesInTileFast(_gridTileLookupSystem))
{
if (EntityManager.TryGetComponent(entity, out PuddleComponent? p))
{
puddle = p;
return true;
}
}
puddle = null;
return false;
}
public PuddleComponent? SpillAt(TileRef tileRef, Solution solution, string prototype,
bool overflow = true, bool sound = true, bool noTileReact = false, bool combine = true)
{
if (solution.TotalVolume <= 0) return null;
// If space return early, let that spill go out into the void
if (tileRef.Tile.IsEmpty) return null;
var gridId = tileRef.GridIndex;
if (!_mapManager.TryGetGrid(gridId, out var mapGrid)) return null; // Let's not spill to invalid grids.
if (!noTileReact)
{
// First, do all tile reactions
foreach (var (reagentId, quantity) in solution.Contents)
{
var proto = _prototypeManager.Index<ReagentPrototype>(reagentId);
proto.ReactionTile(tileRef, quantity);
}
}
// Tile reactions used up everything.
if (solution.CurrentVolume == FixedPoint2.Zero)
return null;
// Get normalized co-ordinate for spill location and spill it in the centre
// TODO: Does SnapGrid or something else already do this?
var spillGridCoords = mapGrid.GridTileToWorld(tileRef.GridIndices);
var spillEntities = _entityLookup.GetEntitiesIntersecting(mapGrid.ParentMapId, spillGridCoords.Position).ToArray();
foreach (var spillEntity in spillEntities)
{
if (_solutionContainerSystem.TryGetRefillableSolution(spillEntity, out var solutionContainerComponent))
{
_solutionContainerSystem.Refill(spillEntity, solutionContainerComponent,
solution.SplitSolution(FixedPoint2.Min(
solutionContainerComponent.AvailableVolume,
solutionContainerComponent.MaxSpillRefill))
);
}
}
if (combine)
{
foreach (var spillEntity in spillEntities)
{
if (!EntityManager.TryGetComponent(spillEntity, out PuddleComponent? puddleComponent)) continue;
if (!overflow && _puddleSystem.WouldOverflow(puddleComponent.Owner, solution, puddleComponent))
return null;
if (!_puddleSystem.TryAddSolution(puddleComponent.Owner, solution, sound)) continue;
return puddleComponent;
}
}
var puddleEnt = EntityManager.SpawnEntity(prototype, spillGridCoords);
var newPuddleComponent = EntityManager.GetComponent<PuddleComponent>(puddleEnt);
_puddleSystem.TryAddSolution(newPuddleComponent.Owner, solution, sound);
return newPuddleComponent;
}
}

View File

@@ -159,7 +159,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
if (instrument.InstrumentPlayer.AttachedEntity is {Valid: true} mob)
{
_stunSystem.TryParalyze(mob, TimeSpan.FromSeconds(1));
_stunSystem.TryParalyze(mob, TimeSpan.FromSeconds(1), true);
instrument.Owner.PopupMessage(mob, "instrument-component-finger-cramps-max-message");
}

View File

@@ -1,4 +1,3 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Sound;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
@@ -7,6 +6,7 @@ using Robust.Shared.ViewVariables;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Shared.Analyzers;
using System.Threading;
namespace Content.Server.Nutrition.Components
{
@@ -50,8 +50,9 @@ namespace Content.Server.Nutrition.Components
public float ForceFeedDelay = 3;
/// <summary>
/// If true, this drink has some DoAfter active (someone is being force fed).
/// Token for interrupting a do-after action (e.g., force feeding). If not null, implies component is
/// currently "in use".
/// </summary>
public bool InUse = false;
public CancellationTokenSource? CancelToken;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.FixedPoint;
@@ -55,9 +56,10 @@ namespace Content.Server.Nutrition.Components
public float ForceFeedDelay = 3;
/// <summary>
/// If true, this food has some DoAfter active (someone is being force fed).
/// Token for interrupting a do-after action (e.g., force feeding). If not null, implies component is
/// currently "in use".
/// </summary>
public bool InUse = false;
public CancellationTokenSource? CancelToken;
[ViewVariables]
public int UsesRemaining

View File

@@ -0,0 +1,26 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Analyzers;
using Robust.Shared.ViewVariables;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Nutrition.EntitySystems;
/// <summary>
/// Component that denotes a piece of clothing that blocks the mouth or otherwise prevents eating & drinking.
/// </summary>
/// <remarks>
/// In the event that more head-wear & mask functionality is added (like identity systems, or raising/lowering of
/// masks), then this component might become redundant.
/// </remarks>
[RegisterComponent, Friend(typeof(FoodSystem), typeof(DrinkSystem))]
public class IngestionBlockerComponent : Component
{
public override string Name => "IngestionBlocker";
/// <summary>
/// Is this component currently blocking consumption.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("enabled")]
public bool Enabled { get; set; } = true;
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Audio;
@@ -20,6 +21,7 @@ namespace Content.Server.Nutrition.EntitySystems
public class CreamPieSystem : SharedCreamPieSystem
{
[Dependency] private readonly SolutionContainerSystem _solutionsSystem = default!;
[Dependency] private readonly SpillableSystem _spillableSystem = default!;
protected override void SplattedCreamPie(EntityUid uid, CreamPieComponent creamPie)
{
@@ -27,7 +29,7 @@ namespace Content.Server.Nutrition.EntitySystems
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<FoodComponent?>(creamPie.Owner, out var foodComp) && _solutionsSystem.TryGetSolution(creamPie.Owner, foodComp.SolutionName, out var solution))
{
solution.SpillAt(creamPie.Owner, "PuddleSmear", false);
_spillableSystem.SpillAt(creamPie.Owner, solution, "PuddleSmear", false);
}
}

View File

@@ -4,6 +4,7 @@ using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Fluids.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.ActionBlocker;
@@ -14,6 +15,7 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Nutrition.Components;
@@ -42,6 +44,7 @@ namespace Content.Server.Nutrition.EntitySystems
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedAdminLogSystem _logSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SpillableSystem _spillableSystem = default!;
public override void Initialize()
{
@@ -51,12 +54,26 @@ namespace Content.Server.Nutrition.EntitySystems
SubscribeLocalEvent<DrinkComponent, ComponentInit>(OnDrinkInit);
SubscribeLocalEvent<DrinkComponent, LandEvent>(HandleLand);
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<DrinkComponent, HandDeselectedEvent>(OnDrinkDeselected);
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<SharedBodyComponent, ForceDrinkEvent>(OnForceDrink);
SubscribeLocalEvent<ForceDrinkCancelledEvent>(OnForceDrinkCancelled);
}
/// <summary>
/// If the user is currently forcing someone do drink, this cancels the attempt if they swap hands or
/// otherwise loose the item. Prevents force-feeding dual-wielding.
/// </summary>
private void OnDrinkDeselected(EntityUid uid, DrinkComponent component, HandDeselectedEvent args)
{
if (component.CancelToken != null)
{
component.CancelToken.Cancel();
component.CancelToken = null;
}
}
public bool IsEmpty(EntityUid uid, DrinkComponent? component = null)
{
if(!Resolve(uid, ref component))
@@ -185,7 +202,7 @@ namespace Content.Server.Nutrition.EntitySystems
UpdateAppearance(component);
var solution = _solutionContainerSystem.Drain(uid, interactions, interactions.DrainAvailable);
solution.SpillAt(uid, "PuddleSmear");
_spillableSystem.SpillAt(uid, solution, "PuddleSmear");
SoundSystem.Play(Filter.Pvs(uid), component.BurstSound.GetSound(), uid, AudioParams.Default.WithVolume(-4));
}
@@ -235,6 +252,14 @@ namespace Content.Server.Nutrition.EntitySystems
if (!Resolve(uid, ref drink))
return false;
// if currently being used to force-feed, cancel that action.
if (drink.CancelToken != null)
{
drink.CancelToken.Cancel();
drink.CancelToken = null;
return true;
}
if (!drink.Opened)
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open",
@@ -260,13 +285,8 @@ namespace Content.Server.Nutrition.EntitySystems
return true;
}
if (_foodSystem.IsMouthBlocked(userUid, out var blocker))
{
var name = EntityManager.GetComponent<MetaDataComponent>(blocker.Value).EntityName;
_popupSystem.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", name)),
userUid, Filter.Entities(userUid));
if (_foodSystem.IsMouthBlocked(userUid, userUid))
return true;
}
var transferAmount = FixedPoint2.Min(drink.TransferAmount, drinkSolution.DrainAvailable);
var drain = _solutionContainerSystem.Drain(uid, drinkSolution, transferAmount);
@@ -281,7 +301,7 @@ namespace Content.Server.Nutrition.EntitySystems
if (EntityManager.HasComponent<RefillableSolutionComponent>(uid))
{
drain.SpillAt(userUid, "PuddleSmear");
_spillableSystem.SpillAt(userUid, drain, "PuddleSmear");
return true;
}
@@ -312,8 +332,12 @@ namespace Content.Server.Nutrition.EntitySystems
return false;
// cannot stack do-afters
if (drink.InUse)
return false;
if (drink.CancelToken != null)
{
drink.CancelToken.Cancel();
drink.CancelToken = null;
return true;
}
if (!EntityManager.HasComponent<SharedBodyComponent>(targetUid))
return false;
@@ -333,13 +357,8 @@ namespace Content.Server.Nutrition.EntitySystems
return true;
}
if (_foodSystem.IsMouthBlocked(targetUid, out var blocker))
{
var name = EntityManager.GetComponent<MetaDataComponent>(blocker.Value).EntityName;
_popupSystem.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", name)),
userUid, Filter.Entities(userUid));
if (_foodSystem.IsMouthBlocked(targetUid, userUid))
return true;
}
EntityManager.TryGetComponent(userUid, out MetaDataComponent? meta);
var userName = meta?.EntityName ?? string.Empty;
@@ -347,7 +366,8 @@ namespace Content.Server.Nutrition.EntitySystems
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)),
userUid, Filter.Entities(targetUid));
_doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, drink.ForceFeedDelay, target: targetUid)
drink.CancelToken = new();
_doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, drink.ForceFeedDelay, drink.CancelToken.Token, targetUid)
{
BreakOnUserMove = true,
BreakOnDamage = true,
@@ -355,13 +375,12 @@ namespace Content.Server.Nutrition.EntitySystems
BreakOnTargetMove = true,
MovementThreshold = 1.0f,
TargetFinishedEvent = new ForceDrinkEvent(userUid, drink, drinkSolution),
BroadcastCancelledEvent = new ForceDrinkCancelledEvent(drink)
BroadcastCancelledEvent = new ForceDrinkCancelledEvent(drink),
});
// logging
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{userUid} is forcing {targetUid} to drink {uid}");
drink.InUse = true;
return true;
}
@@ -370,7 +389,10 @@ namespace Content.Server.Nutrition.EntitySystems
/// </summary>
private void OnForceDrink(EntityUid uid, SharedBodyComponent body, ForceDrinkEvent args)
{
args.Drink.InUse = false;
if (args.Drink.Deleted)
return;
args.Drink.CancelToken = null;
var transferAmount = FixedPoint2.Min(args.Drink.TransferAmount, args.DrinkSolution.DrainAvailable);
var drained = _solutionContainerSystem.Drain((args.Drink).Owner, args.DrinkSolution, transferAmount);
@@ -379,7 +401,7 @@ namespace Content.Server.Nutrition.EntitySystems
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-cannot-drink-other"),
uid, Filter.Entities(args.User));
drained.SpillAt(uid, "PuddleSmear");
_spillableSystem.SpillAt(uid, drained, "PuddleSmear");
return;
}
@@ -392,7 +414,7 @@ namespace Content.Server.Nutrition.EntitySystems
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"),
uid, Filter.Entities(args.User));
drained.SpillAt(uid, "PuddleSmear");
_spillableSystem.SpillAt(uid, drained, "PuddleSmear");
return;
}
@@ -416,31 +438,7 @@ namespace Content.Server.Nutrition.EntitySystems
private void OnForceDrinkCancelled(ForceDrinkCancelledEvent args)
{
args.Drink.InUse = false;
}
}
public sealed class ForceDrinkEvent : EntityEventArgs
{
public readonly EntityUid User;
public readonly DrinkComponent Drink;
public readonly Solution DrinkSolution;
public ForceDrinkEvent(EntityUid user, DrinkComponent drink, Solution drinkSolution)
{
User = user;
Drink = drink;
DrinkSolution = drinkSolution;
}
}
public sealed class ForceDrinkCancelledEvent : EntityEventArgs
{
public readonly DrinkComponent Drink;
public ForceDrinkCancelledEvent( DrinkComponent drink)
{
Drink = drink;
args.Drink.CancelToken = null;
}
}
}

View File

@@ -12,7 +12,6 @@ using Content.Server.Popups;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
@@ -27,7 +26,11 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using System.Collections.Generic;
using Robust.Shared.Utility;
using Content.Server.Inventory.Components;
using Content.Shared.Inventory;
using Content.Shared.Hands;
namespace Content.Server.Nutrition.EntitySystems
{
@@ -51,9 +54,24 @@ namespace Content.Server.Nutrition.EntitySystems
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand);
SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
SubscribeLocalEvent<FoodComponent, HandDeselectedEvent>(OnFoodDeselected);
SubscribeLocalEvent<FoodComponent, GetInteractionVerbsEvent>(AddEatVerb);
SubscribeLocalEvent<SharedBodyComponent, ForceFeedEvent>(OnForceFeed);
SubscribeLocalEvent<ForceFeedCancelledEvent>(OnForceFeedCancelled);
SubscribeLocalEvent<InventoryComponent, IngestionAttemptEvent>(OnInventoryIngestAttempt);
}
/// <summary>
/// If the user is currently force feeding someone, this cancels the attempt if they swap hands or otherwise
/// loose the item. Prevents force-feeding dual-wielding.
/// </summary>
private void OnFoodDeselected(EntityUid uid, FoodComponent component, HandDeselectedEvent args)
{
if (component.CancelToken != null)
{
component.CancelToken.Cancel();
component.CancelToken = null;
}
}
/// <summary>
@@ -119,6 +137,14 @@ namespace Content.Server.Nutrition.EntitySystems
if (!Resolve(uid, ref food))
return false;
// if currently being used to force-feed, cancel that action.
if (food.CancelToken != null)
{
food.CancelToken.Cancel();
food.CancelToken = null;
return true;
}
if (uid == user || //Suppresses self-eating
EntityManager.TryGetComponent<MobStateComponent>(uid, out var mobState) && mobState.IsAlive()) // Suppresses eating alive mobs
return false;
@@ -137,11 +163,8 @@ namespace Content.Server.Nutrition.EntitySystems
!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(user, out var stomachs, body))
return false;
if (IsMouthBlocked(user, out var blocker))
if (IsMouthBlocked(user, user))
{
var name = EntityManager.GetComponent<MetaDataComponent>(blocker.Value).EntityName;
_popupSystem.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", name)),
user, Filter.Entities(user));
return true;
}
@@ -218,6 +241,9 @@ namespace Content.Server.Nutrition.EntitySystems
private void AddEatVerb(EntityUid uid, FoodComponent component, GetInteractionVerbsEvent ev)
{
if (component.CancelToken != null)
return;
if (uid == ev.User ||
!ev.CanInteract ||
!ev.CanAccess ||
@@ -250,6 +276,14 @@ namespace Content.Server.Nutrition.EntitySystems
if (!Resolve(uid, ref food))
return false;
// if currently being used to force-feed, cancel that action.
if (food.CancelToken != null)
{
food.CancelToken.Cancel();
food.CancelToken = null;
return true;
}
if (!EntityManager.HasComponent<SharedBodyComponent>(target))
return false;
@@ -264,11 +298,8 @@ namespace Content.Server.Nutrition.EntitySystems
return true;
}
if (IsMouthBlocked(target, out var blocker))
if (IsMouthBlocked(target, user))
{
var name = EntityManager.GetComponent<MetaDataComponent>(blocker.Value).EntityName;
_popupSystem.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", name)),
user, Filter.Entities(user));
return true;
}
@@ -281,7 +312,8 @@ namespace Content.Server.Nutrition.EntitySystems
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)),
user, Filter.Entities(target));
_doAfterSystem.DoAfter(new DoAfterEventArgs(user, food.ForceFeedDelay, target: target)
food.CancelToken = new();
_doAfterSystem.DoAfter(new DoAfterEventArgs(user, food.ForceFeedDelay, food.CancelToken.Token, target)
{
BreakOnUserMove = true,
BreakOnDamage = true,
@@ -295,13 +327,15 @@ namespace Content.Server.Nutrition.EntitySystems
// logging
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{user} is forcing {target} to eat {uid}");
food.InUse = true;
return true;
}
private void OnForceFeed(EntityUid uid, SharedBodyComponent body, ForceFeedEvent args)
{
args.Food.InUse = false;
if (args.Food.Deleted)
return;
args.Food.CancelToken = null;
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(uid, out var stomachs, body))
return;
@@ -361,7 +395,7 @@ namespace Content.Server.Nutrition.EntitySystems
if (!Resolve(uid, ref food) || !Resolve(target, ref body, false))
return;
if (IsMouthBlocked(target, out _))
if (IsMouthBlocked(target))
return;
if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var foodSolution))
@@ -438,66 +472,57 @@ namespace Content.Server.Nutrition.EntitySystems
private void OnForceFeedCancelled(ForceFeedCancelledEvent args)
{
args.Food.InUse = false;
args.Food.CancelToken = null;
}
/// <summary>
/// Is an entity's mouth accessible, or is it blocked by something like a mask? Does not actually check if
/// the user has a mouth. Body system when?
/// Block ingestion attempts based on the equipped mask or head-wear
/// </summary>
public bool IsMouthBlocked(EntityUid uid, [NotNullWhen(true)] out EntityUid? blockingEntity,
InventoryComponent? inventory = null)
private void OnInventoryIngestAttempt(EntityUid uid, InventoryComponent component, IngestionAttemptEvent args)
{
blockingEntity = null;
if (args.Cancelled)
return;
if (!Resolve(uid, ref inventory, false))
return false;
IngestionBlockerComponent blocker;
// check masks
if (inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.MASK, out ItemComponent? mask))
if (component.TryGetSlotItem(EquipmentSlotDefines.Slots.MASK, out ItemComponent? mask) &&
EntityManager.TryGetComponent(mask.Owner, out blocker) &&
blocker.Enabled)
{
// For now, lets just assume that any masks always covers the mouth
// TODO MASKS if the ability is added to raise/lower masks, this needs to be updated.
blockingEntity = mask.Owner;
return true;
args.Blocker = mask.Owner;
args.Cancel();
return;
}
// check helmets. Note that not all helmets cover the face.
if (inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.HEAD, out ItemComponent? head) &&
EntityManager.TryGetComponent(((IComponent) head).Owner, out TagComponent tag) &&
tag.HasTag("ConcealsFace"))
if (component.TryGetSlotItem(EquipmentSlotDefines.Slots.HEAD, out ItemComponent? head) &&
EntityManager.TryGetComponent(head.Owner, out blocker) &&
blocker.Enabled)
{
blockingEntity = head.Owner;
return true;
}
return false;
args.Blocker = head.Owner;
args.Cancel();
}
}
public sealed class ForceFeedEvent : EntityEventArgs
/// <summary>
/// Check whether the target's mouth is blocked by equipment (masks or head-wear).
/// </summary>
/// <param name="uid">The target whose equipment is checked</param>
/// <param name="popupUid">Optional entity that will receive an informative pop-up identifying the blocking
/// piece of equipment.</param>
/// <returns></returns>
public bool IsMouthBlocked(EntityUid uid, EntityUid? popupUid = null)
{
public readonly EntityUid User;
public readonly FoodComponent Food;
public readonly Solution FoodSolution;
public readonly List<UtensilComponent> Utensils;
public ForceFeedEvent(EntityUid user, FoodComponent food, Solution foodSolution, List<UtensilComponent> utensils)
var attempt = new IngestionAttemptEvent();
RaiseLocalEvent(uid, attempt, false);
if (attempt.Cancelled && attempt.Blocker != null && popupUid != null)
{
User = user;
Food = food;
FoodSolution = foodSolution;
Utensils = utensils;
}
var name = EntityManager.GetComponent<MetaDataComponent>(attempt.Blocker.Value).EntityName;
_popupSystem.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", name)),
uid, Filter.Entities(popupUid.Value));
}
public sealed class ForceFeedCancelledEvent : EntityEventArgs
{
public readonly FoodComponent Food;
public ForceFeedCancelledEvent(FoodComponent food)
{
Food = food;
return attempt.Cancelled;
}
}
}

View File

@@ -0,0 +1,79 @@
using Content.Server.Nutrition.Components;
using Content.Shared.Chemistry.Components;
using Robust.Shared.GameObjects;
using System.Collections.Generic;
namespace Content.Server.Nutrition;
/// <summary>
/// Raised directed at the consumer when attempting to ingest something.
/// </summary>
public sealed class IngestionAttemptEvent : CancellableEntityEventArgs
{
/// <summary>
/// The equipment that is blocking consumption. Should only be non-null if the event was canceled.
/// </summary>
public EntityUid? Blocker = null;
}
/// <summary>
/// Raised directed at the food after a successful force-feed do-after.
/// </summary>
public sealed class ForceFeedEvent : EntityEventArgs
{
public readonly EntityUid User;
public readonly FoodComponent Food;
public readonly Solution FoodSolution;
public readonly List<UtensilComponent> Utensils;
public ForceFeedEvent(EntityUid user, FoodComponent food, Solution foodSolution, List<UtensilComponent> utensils)
{
User = user;
Food = food;
FoodSolution = foodSolution;
Utensils = utensils;
}
}
/// <summary>
/// Raised directed at the food after a failed force-feed do-after.
/// </summary>
public sealed class ForceFeedCancelledEvent : EntityEventArgs
{
public readonly FoodComponent Food;
public ForceFeedCancelledEvent(FoodComponent food)
{
Food = food;
}
}
/// <summary>
/// Raised directed at the drink after a successful force-drink do-after.
/// </summary>
public sealed class ForceDrinkEvent : EntityEventArgs
{
public readonly EntityUid User;
public readonly DrinkComponent Drink;
public readonly Solution DrinkSolution;
public ForceDrinkEvent(EntityUid user, DrinkComponent drink, Solution drinkSolution)
{
User = user;
Drink = drink;
DrinkSolution = drinkSolution;
}
}
/// <summary>
/// Raised directed at the food after a failed force-dink do-after.
/// </summary>
public sealed class ForceDrinkCancelledEvent : EntityEventArgs
{
public readonly DrinkComponent Drink;
public ForceDrinkCancelledEvent(DrinkComponent drink)
{
Drink = drink;
}
}

View File

@@ -242,7 +242,8 @@ namespace Content.Server.PneumaticCannon
if(EntityManager.TryGetComponent<StatusEffectsComponent?>(data.User, out var status)
&& comp.Power == PneumaticCannonPower.High)
{
_stun.TryParalyze(data.User, TimeSpan.FromSeconds(comp.HighPowerStunTime), status);
_stun.TryParalyze(data.User, TimeSpan.FromSeconds(comp.HighPowerStunTime), true, status);
data.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-power-stun",
("cannon", comp.Owner)));
}

View File

@@ -28,13 +28,13 @@ namespace Content.Server.Speech.EntitySystems
SubscribeLocalEvent<StutteringAccentComponent, AccentGetEvent>(OnAccent);
}
public override void DoStutter(EntityUid uid, TimeSpan time, StatusEffectsComponent? status = null, SharedAlertsComponent? alerts = null)
public override void DoStutter(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null, SharedAlertsComponent? alerts = null)
{
if (!Resolve(uid, ref status, false))
return;
if (!_statusEffectsSystem.HasStatusEffect(uid, StutterKey, status))
_statusEffectsSystem.TryAddStatusEffect<StutteringAccentComponent>(uid, StutterKey, time, status, alerts);
_statusEffectsSystem.TryAddStatusEffect<StutteringAccentComponent>(uid, StutterKey, time, refresh, status, alerts);
else
_statusEffectsSystem.TryAddTime(uid, StutterKey, time, status);
}

View File

@@ -365,18 +365,12 @@ namespace Content.Server.Storage.Components
// Trying to add while open just dumps it on the ground below us.
if (Open)
{
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(entity).WorldPosition = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).WorldPosition;
var entMan = IoCManager.Resolve<IEntityManager>();
entMan.GetComponent<TransformComponent>(entity).WorldPosition = entMan.GetComponent<TransformComponent>(Owner).WorldPosition;
return true;
}
if (!Contents.Insert(entity)) return false;
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(entity).LocalPosition = Vector2.Zero;
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out IPhysBody? body))
{
body.CanCollide = false;
}
return true;
return Contents.Insert(entity);
}
/// <inheritdoc />

View File

@@ -2,7 +2,7 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Storage.Components
{
public interface IStorageComponent
public interface IStorageComponent : IComponent
{
bool Remove(EntityUid entity);
bool Insert(EntityUid entity);

View File

@@ -48,10 +48,18 @@ namespace Content.Server.Storage.Components
continue;
}
var entMan = IoCManager.Resolve<IEntityManager>();
var transform = entMan.GetComponent<TransformComponent>(Owner);
for (var i = 0; i < storageItem.Amount; i++)
{
storage.Insert(
IoCManager.Resolve<IEntityManager>().SpawnEntity(storageItem.PrototypeId, IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates));
var ent = entMan.SpawnEntity(storageItem.PrototypeId, transform.Coordinates);
if (storage.Insert(ent)) continue;
Logger.ErrorS("storage", $"Tried to StorageFill {storageItem.PrototypeId} inside {Owner} but can't.");
entMan.DeleteEntity(ent);
}
if (!string.IsNullOrEmpty(storageItem.GroupId)) alreadySpawnedGroups.Add(storageItem.GroupId);

View File

@@ -34,12 +34,12 @@ namespace Content.Server.Stunnable
// Let the actual methods log errors for these.
Resolve(otherUid, ref alerts, ref standingState, ref appearance, false);
_stunSystem.TryStun(otherUid, TimeSpan.FromSeconds(component.StunAmount), status, alerts);
_stunSystem.TryStun(otherUid, TimeSpan.FromSeconds(component.StunAmount), true, status, alerts);
_stunSystem.TryKnockdown(otherUid, TimeSpan.FromSeconds(component.KnockdownAmount),
_stunSystem.TryKnockdown(otherUid, TimeSpan.FromSeconds(component.KnockdownAmount), true,
status, alerts);
_stunSystem.TrySlowdown(otherUid, TimeSpan.FromSeconds(component.SlowdownAmount),
_stunSystem.TrySlowdown(otherUid, TimeSpan.FromSeconds(component.SlowdownAmount), true,
component.WalkSpeedMultiplier, component.RunSpeedMultiplier, status, alerts);
}
}

View File

@@ -34,7 +34,7 @@ namespace Content.Server.Stunnable
if (args.Handled || !_random.Prob(args.PushProbability))
return;
if (!TryParalyze(uid, TimeSpan.FromSeconds(4f), status))
if (!TryParalyze(uid, TimeSpan.FromSeconds(4f), true, status))
return;
var source = args.Source;

View File

@@ -120,7 +120,7 @@ namespace Content.Server.Stunnable
private void StunEntity(EntityUid entity, StunbatonComponent comp)
{
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out StatusEffectsComponent? status) || !comp.Activated) return;
if (!EntityManager.TryGetComponent(entity, out StatusEffectsComponent? status) || !comp.Activated) return;
// TODO: Make slowdown inflicted customizable.
@@ -128,23 +128,23 @@ namespace Content.Server.Stunnable
if (!EntityManager.HasComponent<SlowedDownComponent>(entity))
{
if (_robustRandom.Prob(comp.ParalyzeChanceNoSlowdown))
_stunSystem.TryParalyze(entity, TimeSpan.FromSeconds(comp.ParalyzeTime), status);
_stunSystem.TryParalyze(entity, TimeSpan.FromSeconds(comp.ParalyzeTime), true, status);
else
_stunSystem.TrySlowdown(entity, TimeSpan.FromSeconds(comp.SlowdownTime), 0.5f, 0.5f, status);
_stunSystem.TrySlowdown(entity, TimeSpan.FromSeconds(comp.SlowdownTime), true, 0.5f, 0.5f, status);
}
else
{
if (_robustRandom.Prob(comp.ParalyzeChanceWithSlowdown))
_stunSystem.TryParalyze(entity, TimeSpan.FromSeconds(comp.ParalyzeTime), status);
_stunSystem.TryParalyze(entity, TimeSpan.FromSeconds(comp.ParalyzeTime), true, status);
else
_stunSystem.TrySlowdown(entity, TimeSpan.FromSeconds(comp.SlowdownTime), 0.5f, 0.5f, status);
_stunSystem.TrySlowdown(entity, TimeSpan.FromSeconds(comp.SlowdownTime), true, 0.5f, 0.5f, status);
}
var slowdownTime = TimeSpan.FromSeconds(comp.SlowdownTime);
_jitterSystem.DoJitter(entity, slowdownTime, status:status);
_stutteringSystem.DoStutter(entity, slowdownTime, status);
_jitterSystem.DoJitter(entity, slowdownTime, true, status:status);
_stutteringSystem.DoStutter(entity, slowdownTime, true, status);
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<PowerCellSlotComponent?>(comp.Owner, out var slot) || slot.Cell == null || !(slot.Cell.CurrentCharge < comp.EnergyPerUse))
if (!EntityManager.TryGetComponent<PowerCellSlotComponent?>(comp.Owner, out var slot) || slot.Cell == null || !(slot.Cell.CurrentCharge < comp.EnergyPerUse))
return;
SoundSystem.Play(Filter.Pvs(comp.Owner), comp.SparksSound.GetSound(), comp.Owner, AudioHelpers.WithVariation(0.25f));
@@ -158,8 +158,8 @@ namespace Content.Server.Stunnable
return;
}
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<SpriteComponent?>(comp.Owner, out var sprite) ||
!IoCManager.Resolve<IEntityManager>().TryGetComponent<ItemComponent?>(comp.Owner, out var item)) return;
if (!EntityManager.TryGetComponent<SpriteComponent?>(comp.Owner, out var sprite) ||
!EntityManager.TryGetComponent<ItemComponent?>(comp.Owner, out var item)) return;
SoundSystem.Play(Filter.Pvs(comp.Owner), comp.SparksSound.GetSound(), comp.Owner, AudioHelpers.WithVariation(0.25f));
item.EquippedPrefix = "off";

View File

@@ -30,8 +30,8 @@ namespace Content.Server.Throwing
/// <param name="direction">A vector pointing from the entity to its destination.</param>
/// <param name="strength">How much the direction vector should be multiplied for velocity.</param>
/// <param name="user"></param>
/// <param name="pushbackRatio">The ratio of impulse applied to the thrower</param>
internal static void TryThrow(this EntityUid entity, Vector2 direction, float strength = 1.0f, EntityUid? user = null, float pushbackRatio = 1.0f)
/// <param name="pushbackRatio">The ratio of impulse applied to the thrower - defaults to 10 because otherwise it's not enough to properly recover from getting spaced</param>
internal static void TryThrow(this EntityUid entity, Vector2 direction, float strength = 1.0f, EntityUid? user = null, float pushbackRatio = 10.0f)
{
var entities = IoCManager.Resolve<IEntityManager>();
if (entities.GetComponent<MetaDataComponent>(entity).EntityDeleted ||

View File

@@ -3,9 +3,11 @@ using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using System;
namespace Content.Server.Weapon.Ranged.Barrels
{
@@ -19,8 +21,8 @@ namespace Content.Server.Weapon.Ranged.Barrels
SubscribeLocalEvent<RevolverBarrelComponent, GetAlternativeVerbsEvent>(AddSpinVerb);
SubscribeLocalEvent<ServerBatteryBarrelComponent, GetAlternativeVerbsEvent>(AddEjectCellVerb);
SubscribeLocalEvent<ServerBatteryBarrelComponent, GetInteractionVerbsEvent>(AddInsertCellVerb);
SubscribeLocalEvent<ServerBatteryBarrelComponent, EntInsertedIntoContainerMessage>(OnCellSlotUpdated);
SubscribeLocalEvent<ServerBatteryBarrelComponent, EntRemovedFromContainerMessage>(OnCellSlotUpdated);
SubscribeLocalEvent<BoltActionBarrelComponent, GetInteractionVerbsEvent>(AddToggleBoltVerb);
@@ -28,6 +30,12 @@ namespace Content.Server.Weapon.Ranged.Barrels
SubscribeLocalEvent<ServerMagazineBarrelComponent, GetAlternativeVerbsEvent>(AddEjectMagazineVerb);
}
private void OnCellSlotUpdated(EntityUid uid, ServerBatteryBarrelComponent component, ContainerModifiedMessage args)
{
if (args.Container.ID == component.CellSlot.ID)
component.UpdateAppearance();
}
private void AddSpinVerb(EntityUid uid, RevolverBarrelComponent component, GetAlternativeVerbsEvent args)
{
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
@@ -62,44 +70,6 @@ namespace Content.Server.Weapon.Ranged.Barrels
args.Verbs.Add(verb);
}
// TODO VERBS EJECTABLES Standardize eject/insert verbs into a single system?
// Really, why isn't this just PowerCellSlotComponent?
private void AddEjectCellVerb(EntityUid uid, ServerBatteryBarrelComponent component, GetAlternativeVerbsEvent args)
{
if (args.Hands == null ||
!args.CanAccess ||
!args.CanInteract ||
!component.PowerCellRemovable ||
component.PowerCell == null ||
!_actionBlockerSystem.CanPickup(args.User))
return;
Verb verb = new()
{
Text = EntityManager.GetComponent<MetaDataComponent>(component.PowerCell.Owner).EntityName,
Category = VerbCategory.Eject,
Act = () => component.TryEjectCell(args.User)
};
args.Verbs.Add(verb);
}
private void AddInsertCellVerb(EntityUid uid, ServerBatteryBarrelComponent component, GetInteractionVerbsEvent args)
{
if (args.Using is not {Valid: true} @using ||
!args.CanAccess ||
!args.CanInteract ||
component.PowerCell != null ||
!EntityManager.HasComponent<BatteryComponent>(@using) ||
!_actionBlockerSystem.CanDrop(args.User))
return;
Verb verb = new();
verb.Text = EntityManager.GetComponent<MetaDataComponent>(@using).EntityName;
verb.Category = VerbCategory.Insert;
verb.Act = () => component.TryInsertPowerCell(@using);
args.Verbs.Add(verb);
}
private void AddEjectMagazineVerb(EntityUid uid, ServerMagazineBarrelComponent component, GetAlternativeVerbsEvent args)
{
if (args.Hands == null ||

View File

@@ -26,7 +26,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
[RegisterComponent]
[NetworkedComponent()]
#pragma warning disable 618
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent, IMapInit, IExamine
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, IMapInit, IExamine
#pragma warning restore 618
{
// Originally I had this logic shared with PumpBarrel and used a couple of variables to control things
@@ -270,7 +270,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
return false;
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
public bool UseEntity(UseEntityEventArgs eventArgs)
{
if (BoltOpen)
{
@@ -284,7 +284,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
return true;
}
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryInsertBullet(eventArgs.User, eventArgs.Using);
}

View File

@@ -25,7 +25,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
/// </summary>
[RegisterComponent]
[NetworkedComponent()]
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, IMapInit, ISerializationHooks
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, IMapInit, ISerializationHooks
{
public override string Name => "PumpBarrel";
@@ -224,13 +224,13 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
return false;
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
public bool UseEntity(UseEntityEventArgs eventArgs)
{
Cycle(true);
return true;
}
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryInsertBullet(eventArgs);
}

View File

@@ -22,7 +22,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
{
[RegisterComponent]
[NetworkedComponent()]
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, ISerializationHooks
{
[Dependency] private readonly IRobustRandom _random = default!;
@@ -251,7 +251,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
/// <param name="eventArgs"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public override bool UseEntity(UseEntityEventArgs eventArgs)
public bool UseEntity(UseEntityEventArgs eventArgs)
{
EjectAllSlots();
Dirty();
@@ -259,7 +259,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
return true;
}
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryInsertBullet(eventArgs.User, eventArgs.Using);
}

View File

@@ -1,19 +1,15 @@
using System;
using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Power.Components;
using Content.Server.Projectiles.Components;
using Content.Shared.Interaction;
using Content.Shared.Sound;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@@ -27,6 +23,9 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
public override string Name => "BatteryBarrel";
[DataField("cellSlot", required: true)]
public ItemSlot CellSlot = new();
// The minimum change we need before we can fire
[DataField("lowerChargeLimit")]
[ViewVariables] private float _lowerChargeLimit = 10;
@@ -36,23 +35,14 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
[DataField("ammoPrototype")]
[ViewVariables] private string? _ammoPrototype;
[ViewVariables] public EntityUid? PowerCellEntity => _powerCellContainer.ContainedEntity;
public BatteryComponent? PowerCell => _powerCellContainer.ContainedEntity == null
? null
: _entities.GetComponentOrNull<BatteryComponent>(_powerCellContainer.ContainedEntity.Value);
private ContainerSlot _powerCellContainer = default!;
public BatteryComponent? PowerCell => _entities.GetComponentOrNull<BatteryComponent>(CellSlot.Item);
private ContainerSlot _ammoContainer = default!;
[DataField("powerCellPrototype")]
private string? _powerCellPrototype;
[DataField("powerCellRemovable")]
[ViewVariables] public bool PowerCellRemovable;
public override int ShotsLeft
{
get
{
if (_powerCellContainer.ContainedEntity is not {Valid: true} powerCell)
if (CellSlot.Item is not {} powerCell)
{
return 0;
}
@@ -65,7 +55,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
{
get
{
if (_powerCellContainer.ContainedEntity is not {Valid: true} powerCell)
if (CellSlot.Item is not {} powerCell)
{
return 0;
}
@@ -76,12 +66,6 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
private AppearanceComponent? _appearanceComponent;
// Sounds
[DataField("soundPowerCellInsert", required: true)]
private SoundSpecifier _soundPowerCellInsert = default!;
[DataField("soundPowerCellEject", required: true)]
private SoundSpecifier _soundPowerCellEject = default!;
public override ComponentState GetComponentState()
{
(int, int)? count = (ShotsLeft, Capacity);
@@ -94,12 +78,8 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
protected override void Initialize()
{
base.Initialize();
_powerCellContainer = Owner.EnsureContainer<ContainerSlot>($"{Name}-powercell-container", out var existing);
if (!existing && _powerCellPrototype != null)
{
var powerCellEntity = _entities.SpawnEntity(_powerCellPrototype, _entities.GetComponent<TransformComponent>(Owner).Coordinates);
_powerCellContainer.Insert(powerCellEntity);
}
EntitySystem.Get<ItemSlotsSystem>().AddItemSlot(Owner, $"{Name}-powercell-container", CellSlot);
if (_ammoPrototype != null)
{
@@ -113,6 +93,12 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
Dirty();
}
protected override void OnRemove()
{
base.OnRemove();
EntitySystem.Get<ItemSlotsSystem>().RemoveItemSlot(Owner, CellSlot);
}
protected override void Startup()
{
base.Startup();
@@ -121,7 +107,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
public void UpdateAppearance()
{
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, _powerCellContainer.ContainedEntity != null);
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, CellSlot.HasItem);
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
Dirty();
@@ -143,7 +129,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
public override EntityUid? TakeProjectile(EntityCoordinates spawnAt)
{
var powerCellEntity = _powerCellContainer.ContainedEntity;
var powerCellEntity = CellSlot.Item;
if (powerCellEntity == null)
{
@@ -198,76 +184,5 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
UpdateAppearance();
return entity.Value;
}
public bool TryInsertPowerCell(EntityUid entity)
{
if (_powerCellContainer.ContainedEntity != null)
{
return false;
}
if (!_entities.HasComponent<BatteryComponent>(entity))
{
return false;
}
SoundSystem.Play(Filter.Pvs(Owner), _soundPowerCellInsert.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
_powerCellContainer.Insert(entity);
Dirty();
UpdateAppearance();
return true;
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
{
if (!PowerCellRemovable)
{
return false;
}
return PowerCellEntity != default && TryEjectCell(eventArgs.User);
}
public bool TryEjectCell(EntityUid user)
{
if (PowerCell == null || !PowerCellRemovable)
{
return false;
}
if (!_entities.TryGetComponent(user, out HandsComponent? hands))
{
return false;
}
var cell = PowerCell;
if (!_powerCellContainer.Remove(cell.Owner))
{
return false;
}
Dirty();
UpdateAppearance();
if (!hands.PutInHand(_entities.GetComponent<ItemComponent>(cell.Owner)))
{
_entities.GetComponent<TransformComponent>(cell.Owner).Coordinates = _entities.GetComponent<TransformComponent>(user).Coordinates;
}
SoundSystem.Play(Filter.Pvs(Owner), _soundPowerCellEject.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
return true;
}
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!_entities.HasComponent<BatteryComponent>(eventArgs.Using))
{
return false;
}
return TryInsertPowerCell(eventArgs.Using);
}
}
}

View File

@@ -27,7 +27,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
[RegisterComponent]
[NetworkedComponent()]
#pragma warning disable 618
public sealed class ServerMagazineBarrelComponent : ServerRangedBarrelComponent, IExamine
public sealed class ServerMagazineBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, IExamine
#pragma warning restore 618
{
[Dependency] private readonly IEntityManager _entities = default!;
@@ -248,7 +248,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
public bool UseEntity(UseEntityEventArgs eventArgs)
{
// Behavior:
// If bolt open just close it
@@ -391,7 +391,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
UpdateAppearance();
}
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (CanInsertMagazine(eventArgs.User, eventArgs.Using, quiet: false))
{

View File

@@ -35,7 +35,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
/// Only difference between them is how they retrieve a projectile to shoot (battery, magazine, etc.)
/// </summary>
#pragma warning disable 618
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IUse, IInteractUsing, IExamine, ISerializationHooks
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IExamine, ISerializationHooks
#pragma warning restore 618
{
// There's still some of py01 and PJB's work left over, especially in underlying shooting logic,
@@ -133,9 +133,9 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
}
}
protected override void OnAdd()
protected override void Initialize()
{
base.OnAdd();
base.Initialize();
Owner.EnsureComponentWarn(out ServerRangedWeaponComponent rangedWeaponComponent);
@@ -167,10 +167,6 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
return angle;
}
public abstract bool UseEntity(UseEntityEventArgs eventArgs);
public abstract Task<bool> InteractUsing(InteractUsingEventArgs eventArgs);
public void ChangeFireSelector(FireRateSelector rateSelector)
{
if ((rateSelector & AllRateSelectors) != 0)

View File

@@ -171,7 +171,7 @@ namespace Content.Server.Weapon.Ranged
{
//Wound them
EntitySystem.Get<DamageableSystem>().TryChangeDamage(user, ClumsyDamage);
EntitySystem.Get<StunSystem>().TryParalyze(user, TimeSpan.FromSeconds(3f));
EntitySystem.Get<StunSystem>().TryParalyze(user, TimeSpan.FromSeconds(3f), true);
// Apply salt to the wound ("Honk!")
SoundSystem.Play(

View File

@@ -1,4 +1,4 @@
namespace Content.Shared.Database;
namespace Content.Shared.Database;
// DO NOT CHANGE THE NUMERIC VALUES OF THESE
public enum LogType
@@ -40,12 +40,13 @@ public enum LogType
Pickup = 36,
Drop = 37,
BulletHit = 38,
ForceFeed = 40,
ForceFeed = 40, // involuntary
Ingestion = 53, // voluntary
MeleeHit = 41,
HitScanHit = 42,
Suicide = 43,
Explosion = 44,
Radiation = 45,
Radiation = 45, // Unused
Barotrauma = 46,
Flammable = 47,
Asphyxiation = 48,

View File

@@ -300,12 +300,13 @@ namespace Content.Shared.CCVar
CVarDef.Create("hud.fps_counter_visible", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* AI
* NPCs
*/
public static readonly CVarDef<int> AIMaxUpdates =
CVarDef.Create("ai.maxupdates", 64);
public static readonly CVarDef<int> NPCMaxUpdates =
CVarDef.Create("npc.max_updates", 64);
public static readonly CVarDef<bool> NPCEnabled = CVarDef.Create("npc.enabled", true);
/*
* Net

View File

@@ -1,6 +1,7 @@
using Content.Shared.Sound;
using Content.Shared.Whitelist;
using Robust.Shared.Analyzers;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
@@ -81,6 +82,12 @@ namespace Content.Shared.Containers.ItemSlots
public SoundSpecifier? EjectSound;
// maybe default to /Audio/Machines/id_swipe.ogg?
/// <summary>
/// Options used for playing the insert/eject sounds.
/// </summary>
[DataField("soundOptions")]
public AudioParams SoundOptions = AudioParams.Default;
/// <summary>
/// The name of this item slot. This will be shown to the user in the verb menu.
/// </summary>
@@ -116,6 +123,18 @@ namespace Content.Shared.Containers.ItemSlots
[DataField("ejectOnInteract")]
public bool EjectOnInteract = false;
/// <summary>
/// If true, and if this slot is attached to an item, then it will attempt to eject slot when to the slot is
/// used in the user's hands.
/// </summary>
/// <remarks>
/// Desirable for things like ranged weapons ('Z' to eject), but not desirable for others (e.g., PDA uses
/// 'Z' to open UI). Unlike <see cref="EjectOnInteract"/>, this will not make any changes to the context
/// menu, nor will it disable alt-click interactions.
/// </remarks>
[DataField("ejectOnUse")]
public bool EjectOnUse = false;
/// <summary>
/// Override the insert verb text. Defaults to [insert category] -> [item-name]. If not null, the verb will
/// not be given a category.

View File

@@ -34,6 +34,7 @@ namespace Content.Shared.Containers.ItemSlots
SubscribeLocalEvent<ItemSlotsComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ItemSlotsComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<ItemSlotsComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<ItemSlotsComponent, GetAlternativeVerbsEvent>(AddEjectVerbs);
SubscribeLocalEvent<ItemSlotsComponent, GetInteractionVerbsEvent>(AddInteractionVerbsVerbs);
@@ -125,6 +126,25 @@ namespace Content.Shared.Containers.ItemSlots
}
}
/// <summary>
/// Attempt to eject an item from the first valid item slot.
/// </summary>
private void OnUseInHand(EntityUid uid, ItemSlotsComponent itemSlots, UseInHandEvent args)
{
if (args.Handled)
return;
foreach (var slot in itemSlots.Slots.Values)
{
if (slot.Locked || !slot.EjectOnUse || slot.Item == null)
continue;
args.Handled = true;
TryEjectToHands(uid, slot, args.User);
break;
}
}
/// <summary>
/// Tries to insert a held item in any fitting item slot. If a valid slot already contains an item, it will
/// swap it out and place the old one in the user's hand.
@@ -172,7 +192,7 @@ namespace Content.Shared.Containers.ItemSlots
// ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage
if (slot.InsertSound != null)
SoundSystem.Play(Filter.Pvs(uid), slot.InsertSound.GetSound(), uid);
SoundSystem.Play(Filter.Pvs(uid), slot.InsertSound.GetSound(), uid, slot.SoundOptions);
}
/// <summary>
@@ -267,7 +287,7 @@ namespace Content.Shared.Containers.ItemSlots
// ContainerSlot automatically raises a directed EntRemovedFromContainerMessage
if (slot.EjectSound != null)
SoundSystem.Play(Filter.Pvs(uid), slot.EjectSound.GetSound(), uid);
SoundSystem.Play(Filter.Pvs(uid), slot.EjectSound.GetSound(), uid, slot.SoundOptions);
}
/// <summary>
@@ -317,7 +337,7 @@ namespace Content.Shared.Containers.ItemSlots
return false;
if (user != null && EntityManager.TryGetComponent(user.Value, out SharedHandsComponent? hands))
hands.TryPutInAnyHand(item.Value);
hands.TryPutInActiveHandOrAny(item.Value);
return true;
}

View File

@@ -72,16 +72,4 @@ namespace Content.Shared.Crayon
Color = color;
}
}
[Serializable, NetSerializable, Prototype("crayonDecal")]
public class CrayonDecalPrototype : IPrototype
{
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
[DataField("spritePath")] public string SpritePath { get; } = string.Empty;
[DataField("decals")] public List<string> Decals { get; } = new();
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using Content.Shared.Acts;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Radiation;
using Robust.Shared.Analyzers;
@@ -13,8 +12,6 @@ using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.ViewVariables;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
namespace Content.Shared.Damage
{
@@ -95,11 +92,7 @@ namespace Content.Shared.Damage
damage.DamageDict.Add(typeID, damageValue);
}
var actual = EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner, damage);
// should logging be disabled during rad storms? a lot of entities are going to be damaged.
if (actual != null && !actual.Empty)
EntitySystem.Get<SharedAdminLogSystem>().Add(LogType.Radiation, $"{Owner} took {actual.Total} radiation damage");
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner, damage);
}
// TODO EXPLOSION Remove this.
@@ -120,11 +113,7 @@ namespace Content.Shared.Damage
damage.DamageDict.Add(typeID, damageValue);
}
var actual = EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner, damage);
// will logging handle nukes?
if (actual != null && !actual.Empty)
EntitySystem.Get<SharedAdminLogSystem>().Add(LogType.Explosion, $"{Owner} took {actual.Total} explosion damage");
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner, damage);
}
}

View File

@@ -1,7 +1,5 @@
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -15,8 +13,6 @@ namespace Content.Shared.Damage
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedAdminLogSystem _logs = default!;
public override void Initialize()
{
SubscribeLocalEvent<DamageableComponent, ComponentInit>(DamageableInit);
@@ -24,45 +20,6 @@ namespace Content.Shared.Damage
SubscribeLocalEvent<DamageableComponent, ComponentGetState>(DamageableGetState);
}
/// <summary>
/// Update the total damage value and optionally add to admin logs
/// </summary>
protected virtual void SetTotalDamage(DamageableComponent damageable, FixedPoint2 @new, bool logChange)
{
var owner = damageable.Owner;
var old = damageable.TotalDamage;
if (@new == old)
{
return;
}
damageable.TotalDamage = @new;
if (!logChange)
return;
LogType logType;
string type;
FixedPoint2 change;
if (@new > old)
{
logType = LogType.Damaged;
type = "received";
change = @new - old;
}
else
{
logType = LogType.Healed;
type = "healed";
change = old - @new;
}
_logs.Add(logType, $"{owner} {type} {change} damage. Old: {old} | New: {@new}");
}
/// <summary>
/// Initialize a damageable component
/// </summary>
@@ -111,7 +68,7 @@ namespace Content.Shared.Damage
public void SetDamage(DamageableComponent damageable, DamageSpecifier damage)
{
damageable.Damage = damage;
DamageChanged(damageable, false);
DamageChanged(damageable);
}
/// <summary>
@@ -121,11 +78,11 @@ namespace Content.Shared.Damage
/// This updates cached damage information, flags the component as dirty, and raises a damage changed event.
/// The damage changed event is used by other systems, such as damage thresholds.
/// </remarks>
public void DamageChanged(DamageableComponent component, bool logChange, DamageSpecifier? damageDelta = null,
public void DamageChanged(DamageableComponent component, DamageSpecifier? damageDelta = null,
bool interruptsDoAfters = true)
{
component.DamagePerGroup = component.Damage.GetDamagePerGroup();
SetTotalDamage(component, component.Damage.Total, logChange);
component.TotalDamage = component.Damage.Total;
component.Dirty();
if (EntityManager.TryGetComponent<AppearanceComponent>(component.Owner, out var appearance) && damageDelta != null)
@@ -146,7 +103,7 @@ namespace Content.Shared.Damage
/// null if the user had no applicable components that can take damage.
/// </returns>
public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false,
bool interruptsDoAfters = true, bool logChange = false)
bool interruptsDoAfters = true)
{
if (!EntityManager.TryGetComponent<DamageableComponent>(uid, out var damageable))
{
@@ -195,7 +152,7 @@ namespace Content.Shared.Damage
if (!delta.Empty)
{
DamageChanged(damageable, logChange, delta, interruptsDoAfters);
DamageChanged(damageable, delta, interruptsDoAfters);
}
return delta;
@@ -222,7 +179,7 @@ namespace Content.Shared.Damage
// Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an
// empty damage delta.
DamageChanged(component, false, new DamageSpecifier());
DamageChanged(component, new DamageSpecifier());
}
private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
@@ -247,7 +204,7 @@ namespace Content.Shared.Damage
if (!delta.Empty)
{
component.Damage = newDamage;
DamageChanged(component, false, delta);
DamageChanged(component, delta);
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Decals
{
[Serializable, NetSerializable]
[DataDefinition]
public class Decal
{
[DataField("coordinates")] public readonly Vector2 Coordinates = Vector2.Zero;
[DataField("id")] public readonly string Id = string.Empty;
[DataField("color")] public readonly Color? Color;
[DataField("angle")] public readonly Angle Angle = Angle.Zero;
[DataField("zIndex")] public readonly int ZIndex;
[DataField("cleanable")] public bool Cleanable;
public Decal() {}
public Decal(Vector2 coordinates, string id, Color? color, Angle angle, int zIndex, bool cleanable)
{
Coordinates = coordinates;
Id = id;
Color = color;
Angle = angle;
ZIndex = zIndex;
Cleanable = cleanable;
}
public Decal WithCoordinates(Vector2 coordinates) => new(coordinates, Id, Color, Angle, ZIndex, Cleanable);
public Decal WithId(string id) => new(Coordinates, id, Color, Angle, ZIndex, Cleanable);
public Decal WithColor(Color? color) => new(Coordinates, Id, color, Angle, ZIndex, Cleanable);
public Decal WithRotation(Angle angle) => new(Coordinates, Id, Color, angle, ZIndex, Cleanable);
public Decal WithZIndex(int zIndex) => new(Coordinates, Id, Color, Angle, zIndex, Cleanable);
public Decal WithCleanable(bool cleanable) => new(Coordinates, Id, Color, Angle, ZIndex, cleanable);
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Content.Shared.Decals
{
[Serializable, NetSerializable]
public class DecalChunkUpdateEvent : EntityEventArgs
{
public Dictionary<GridId, Dictionary<Vector2i, Dictionary<uint, Decal>>> Data = new();
}
}

View File

@@ -0,0 +1,74 @@
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Content.Shared.Decals
{
[TypeSerializer]
public class DecalGridChunkCollectionTypeSerializer : ITypeSerializer<DecalGridComponent.DecalGridChunkCollection, MappingDataNode>
{
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
return serializationManager.ValidateNode<Dictionary<Vector2i, Dictionary<uint, Decal>>>(node, context);
}
public DeserializationResult Read(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null)
{
//todo this read method does not support pushing inheritance
var dictionary =
serializationManager.ReadValueOrThrow<Dictionary<Vector2i, Dictionary<uint, Decal>>>(node, context, skipHook);
var uids = new SortedSet<uint>();
var uidChunkMap = new Dictionary<uint, Vector2i>();
foreach (var (indices, decals) in dictionary)
{
foreach (var (uid, _) in decals)
{
uids.Add(uid);
uidChunkMap[uid] = indices;
}
}
var uidMap = new Dictionary<uint, uint>();
uint nextIndex = 0;
foreach (var uid in uids)
{
uidMap[uid] = nextIndex++;
}
var newDict = new Dictionary<Vector2i, Dictionary<uint, Decal>>();
foreach (var (oldUid, newUid) in uidMap)
{
var indices = uidChunkMap[oldUid];
if(!newDict.ContainsKey(indices))
newDict[indices] = new();
newDict[indices][newUid] = dictionary[indices][oldUid];
}
return new DeserializedValue<DecalGridComponent.DecalGridChunkCollection>(
new DecalGridComponent.DecalGridChunkCollection(newDict){NextUid = nextIndex});
}
public DataNode Write(ISerializationManager serializationManager, DecalGridComponent.DecalGridChunkCollection value, bool alwaysWrite = false,
ISerializationContext? context = null)
{
return serializationManager.WriteValue(value.ChunkCollection, alwaysWrite, context);
}
public DecalGridComponent.DecalGridChunkCollection Copy(ISerializationManager serializationManager, DecalGridComponent.DecalGridChunkCollection source,
DecalGridComponent.DecalGridChunkCollection target, bool skipHook, ISerializationContext? context = null)
{
var dict = serializationManager.Copy(source.ChunkCollection, target.ChunkCollection, context, skipHook)!;
return new DecalGridComponent.DecalGridChunkCollection(dict) {NextUid = source.NextUid};
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Decals
{
[RegisterComponent]
[Friend(typeof(SharedDecalSystem))]
public class DecalGridComponent : Component
{
public override string Name => "DecalGrid";
[DataField("chunkCollection", serverOnly: true)]
public DecalGridChunkCollection ChunkCollection = new(new ());
public record DecalGridChunkCollection(Dictionary<Vector2i, Dictionary<uint, Decal>> ChunkCollection)
{
public uint NextUid;
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Content.Shared.Decals
{
[Prototype("decal")]
public class DecalPrototype : IPrototype
{
[DataField("id")] public string ID { get; } = null!;
[DataField("sprite")] public SpriteSpecifier Sprite { get; } = SpriteSpecifier.Invalid;
[DataField("tags")] public List<string> Tags = new();
}
}

View File

@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Shared.Decals
{
public abstract class SharedDecalSystem : EntitySystem
{
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] protected readonly IMapManager MapManager = default!;
protected readonly Dictionary<GridId, Dictionary<uint, Vector2i>> ChunkIndex = new();
public const int ChunkSize = 32;
public static Vector2i GetChunkIndices(Vector2 coordinates) => new ((int) Math.Floor(coordinates.X / ChunkSize), (int) Math.Floor(coordinates.Y / ChunkSize));
private float _viewSize;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GridInitializeEvent>(OnGridInitialize);
_configurationManager.OnValueChanged(CVars.NetMaxUpdateRange, OnPvsRangeChanged, true);
}
public override void Shutdown()
{
base.Shutdown();
_configurationManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnPvsRangeChanged);
}
private void OnPvsRangeChanged(float obj)
{
_viewSize = obj * 2f;
}
private void OnGridInitialize(GridInitializeEvent msg)
{
var comp = EntityManager.EnsureComponent<DecalGridComponent>(MapManager.GetGrid(msg.GridId).GridEntityId);
ChunkIndex[msg.GridId] = new();
foreach (var (indices, decals) in comp.ChunkCollection.ChunkCollection)
{
foreach (var uid in decals.Keys)
{
ChunkIndex[msg.GridId][uid] = indices;
}
}
}
protected DecalGridComponent.DecalGridChunkCollection DecalGridChunkCollection(GridId gridId) => EntityManager
.GetComponent<DecalGridComponent>(MapManager.GetGrid(gridId).GridEntityId).ChunkCollection;
protected Dictionary<Vector2i, Dictionary<uint, Decal>> ChunkCollection(GridId gridId) => DecalGridChunkCollection(gridId).ChunkCollection;
protected virtual void DirtyChunk(GridId id, Vector2i chunkIndices) {}
protected bool RemoveDecalInternal(GridId gridId, uint uid)
{
if (!RemoveDecalHook(gridId, uid)) return false;
if (!ChunkIndex.TryGetValue(gridId, out var values) || !values.TryGetValue(uid, out var indices))
{
return false;
}
var chunkCollection = ChunkCollection(gridId);
if (!chunkCollection.TryGetValue(indices, out var chunk) || !chunk.Remove(uid))
{
return false;
}
if (chunkCollection[indices].Count == 0)
chunkCollection.Remove(indices);
ChunkIndex[gridId]?.Remove(uid);
DirtyChunk(gridId, indices);
return true;
}
protected virtual bool RemoveDecalHook(GridId gridId, uint uid) => true;
private (Box2 view, MapId mapId) CalcViewBounds(in EntityUid euid)
{
var xform = EntityManager.GetComponent<TransformComponent>(euid);
var view = Box2.UnitCentered.Scale(_viewSize).Translated(xform.WorldPosition);
var map = xform.MapID;
return (view, map);
}
protected Dictionary<GridId, HashSet<Vector2i>> GetChunksForViewers(HashSet<EntityUid> viewers)
{
var chunks = new Dictionary<GridId, HashSet<Vector2i>>();
foreach (var viewerUid in viewers)
{
var (bounds, mapId) = CalcViewBounds(viewerUid);
MapManager.FindGridsIntersectingEnumerator(mapId, bounds, out var gridsEnumerator, true);
while(gridsEnumerator.MoveNext(out var grid))
{
if(!chunks.ContainsKey(grid.Index))
chunks[grid.Index] = new();
var enumerator = new ChunkIndicesEnumerator(grid.InvWorldMatrix.TransformBox(bounds), ChunkSize);
while (enumerator.MoveNext(out var indices))
{
chunks[grid.Index].Add(indices.Value);
}
}
}
return chunks;
}
}
internal struct ChunkIndicesEnumerator
{
private Vector2i _chunkLB;
private Vector2i _chunkRT;
private int _xIndex;
private int _yIndex;
internal ChunkIndicesEnumerator(Box2 localAABB, int chunkSize)
{
_chunkLB = new Vector2i((int)Math.Floor(localAABB.Left / chunkSize), (int)Math.Floor(localAABB.Bottom / chunkSize));
_chunkRT = new Vector2i((int)Math.Floor(localAABB.Right / chunkSize), (int)Math.Floor(localAABB.Top / chunkSize));
_xIndex = _chunkLB.X;
_yIndex = _chunkLB.Y;
}
public bool MoveNext([NotNullWhen(true)] out Vector2i? indices)
{
if (_yIndex > _chunkRT.Y)
{
_yIndex = _chunkLB.Y;
_xIndex += 1;
}
indices = new Vector2i(_xIndex, _yIndex);
_yIndex += 1;
return _xIndex <= _chunkRT.X;
}
}
}

View File

@@ -53,12 +53,13 @@ namespace Content.Shared.Jittering
/// </remarks>
/// <param name="uid">Entity in question.</param>
/// <param name="time">For how much time to apply the effect.</param>
/// <param name="refresh">The status effect cooldown should be refreshed (true) or accumulated (false).</param>
/// <param name="amplitude">Jitteriness of the animation. See <see cref="MaxAmplitude"/> and <see cref="MinAmplitude"/>.</param>
/// <param name="frequency">Frequency for jittering. See <see cref="MaxFrequency"/> and <see cref="MinFrequency"/>.</param>
/// <param name="forceValueChange">Whether to change any existing jitter value even if they're greater than the ones we're setting.</param>
/// <param name="status">The status effects component to modify.</param>
/// <param name="alerts">The alerts component.</param>
public void DoJitter(EntityUid uid, TimeSpan time, float amplitude = 10f, float frequency = 4f, bool forceValueChange = false,
public void DoJitter(EntityUid uid, TimeSpan time, bool refresh, float amplitude = 10f, float frequency = 4f, bool forceValueChange = false,
StatusEffectsComponent? status = null,
SharedAlertsComponent? alerts = null)
{
@@ -68,7 +69,7 @@ namespace Content.Shared.Jittering
amplitude = Math.Clamp(amplitude, MinAmplitude, MaxAmplitude);
frequency = Math.Clamp(frequency, MinFrequency, MaxFrequency);
if (StatusEffects.TryAddStatusEffect<JitteringComponent>(uid, "Jitter", time, status, alerts))
if (StatusEffects.TryAddStatusEffect<JitteringComponent>(uid, "Jitter", time, refresh, status, alerts))
{
var jittering = EntityManager.GetComponent<JitteringComponent>(uid);

View File

@@ -289,9 +289,11 @@ namespace Content.Shared.MobState.Components
/// </summary>
private void SetMobState(IMobState? old, (IMobState state, FixedPoint2 threshold)? current)
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!current.HasValue)
{
old?.ExitState(Owner, IoCManager.Resolve<IEntityManager>());
old?.ExitState(Owner, entMan);
return;
}
@@ -301,22 +303,19 @@ namespace Content.Shared.MobState.Components
if (state == old)
{
state.UpdateState(Owner, threshold, IoCManager.Resolve<IEntityManager>());
state.UpdateState(Owner, threshold, entMan);
return;
}
old?.ExitState(Owner, IoCManager.Resolve<IEntityManager>());
old?.ExitState(Owner, entMan);
CurrentState = state;
state.EnterState(Owner, IoCManager.Resolve<IEntityManager>());
state.UpdateState(Owner, threshold, IoCManager.Resolve<IEntityManager>());
state.EnterState(Owner, entMan);
state.UpdateState(Owner, threshold, entMan);
var message = new MobStateChangedMessage(this, old, state);
#pragma warning disable 618
SendMessage(message);
#pragma warning restore 618
IoCManager.Resolve<IEntityManager>().EventBus.RaiseEvent(EventSource.Local, message);
var message = new MobStateChangedEvent(this, old, state);
entMan.EventBus.RaiseLocalEvent(Owner, message);
Dirty();
}

View File

@@ -4,11 +4,9 @@ using Robust.Shared.GameObjects;
namespace Content.Shared.MobState
{
#pragma warning disable 618
public class MobStateChangedMessage : ComponentMessage
#pragma warning restore 618
public class MobStateChangedEvent : EntityEventArgs
{
public MobStateChangedMessage(
public MobStateChangedEvent(
MobStateComponent component,
IMobState? oldMobState,
IMobState currentMobState)

View File

@@ -68,7 +68,7 @@ namespace Content.Shared.Nutrition.EntitySystems
CreamedEntity(uid, creamPied, args);
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(creamPie.ParalyzeTime));
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(creamPie.ParalyzeTime), true);
}
protected virtual void CreamedEntity(EntityUid uid, CreamPiedComponent creamPied, ThrowHitByEvent args) {}

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