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:
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -2,7 +2,7 @@ name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ stable ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
60
Content.Client/Decals/DecalOverlay.cs
Normal file
60
Content.Client/Decals/DecalOverlay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
Content.Client/Decals/DecalSystem.cs
Normal file
114
Content.Client/Decals/DecalSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Content.Client/Decals/ToggleDecalCommand.cs
Normal file
15
Content.Client/Decals/ToggleDecalCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace Content.Client.Entry
|
||||
"Anchorable",
|
||||
"AmmoBox",
|
||||
"Pickaxe",
|
||||
"IngestionBlocker",
|
||||
"Interactable",
|
||||
"CloningPod",
|
||||
"Destructible",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
135
Content.Server/AI/EntitySystems/NPCSystem.cs
Normal file
135
Content.Server/AI/EntitySystems/NPCSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, () =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
60
Content.Server/Construction/Conditions/Locked.cs
Normal file
60
Content.Server/Construction/Conditions/Locked.cs
Normal 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"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Content.Server/Construction/Conditions/StorageWelded.cs
Normal file
59
Content.Server/Construction/Conditions/StorageWelded.cs
Normal 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",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
118
Content.Server/Decals/Commands/AddDecalCommand.cs
Normal file
118
Content.Server/Decals/Commands/AddDecalCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
Content.Server/Decals/Commands/EditDecalCommand.cs
Normal file
162
Content.Server/Decals/Commands/EditDecalCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Content.Server/Decals/Commands/RemoveDecalCommand.cs
Normal file
46
Content.Server/Decals/Commands/RemoveDecalCommand.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
326
Content.Server/Decals/DecalSystem.cs
Normal file
326
Content.Server/Decals/DecalSystem.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
Content.Server/Nutrition/IngestionEvents.cs
Normal file
79
Content.Server/Nutrition/IngestionEvents.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
Content.Shared/Decals/Decal.cs
Normal file
38
Content.Shared/Decals/Decal.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
15
Content.Shared/Decals/DecalChunkUpdateEvent.cs
Normal file
15
Content.Shared/Decals/DecalChunkUpdateEvent.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Content.Shared/Decals/DecalGridComponent.cs
Normal file
23
Content.Shared/Decals/DecalGridComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Content.Shared/Decals/DecalPrototype.cs
Normal file
15
Content.Shared/Decals/DecalPrototype.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
153
Content.Shared/Decals/SharedDecalSystem.cs
Normal file
153
Content.Shared/Decals/SharedDecalSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
Reference in New Issue
Block a user