Merge branch 'master' into mathmerge

This commit is contained in:
Pieter-Jan Briers
2020-08-20 20:33:43 +02:00
808 changed files with 18173 additions and 5666 deletions

View File

@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Movement;
@@ -19,23 +20,31 @@ namespace Content.Server.GameObjects.EntitySystems.AI
[UsedImplicitly]
internal class AiSystem : EntitySystem
{
#pragma warning disable 649
[Dependency] private readonly IPauseManager _pauseManager;
[Dependency] private readonly IDynamicTypeFactory _typeFactory;
[Dependency] private readonly IReflectionManager _reflectionManager;
#pragma warning restore 649
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
private readonly Dictionary<string, Type> _processorTypes = new Dictionary<string, Type>();
/// <summary>
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
/// </summary>
private readonly HashSet<AiLogicProcessor> _awakeAi = new HashSet<AiLogicProcessor>();
// To avoid modifying awakeAi while iterating over it.
private readonly List<SleepAiMessage> _queuedSleepMessages = new List<SleepAiMessage>();
public bool IsAwake(AiLogicProcessor processor) => _awakeAi.Contains(processor);
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SleepAiMessage>(HandleAiSleep);
var processors = _reflectionManager.GetAllChildren<AiLogicProcessor>();
foreach (var processor in processors)
{
var att = (AiLogicProcessorAttribute)Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute));
var att = (AiLogicProcessorAttribute) Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute))!;
// Tests should pick this up
DebugTools.AssertNotNull(att);
_processorTypes.Add(att.SerializeName, processor);
@@ -45,23 +54,35 @@ namespace Content.Server.GameObjects.EntitySystems.AI
/// <inheritdoc />
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<AiControllerComponent>())
foreach (var message in _queuedSleepMessages)
{
if (_pauseManager.IsEntityPaused(comp.Owner))
switch (message.Sleep)
{
continue;
case true:
_awakeAi.Remove(message.Processor);
break;
case false:
_awakeAi.Add(message.Processor);
break;
}
ProcessorInitialize(comp);
var processor = comp.Processor;
}
_queuedSleepMessages.Clear();
foreach (var processor in _awakeAi)
{
processor.Update(frameTime);
}
}
private void HandleAiSleep(SleepAiMessage message)
{
_queuedSleepMessages.Add(message);
}
/// <summary>
/// Will start up the controller's processor if not already done so
/// Will start up the controller's processor if not already done so.
/// Also add them to the awakeAi for updates.
/// </summary>
/// <param name="controller"></param>
public void ProcessorInitialize(AiControllerComponent controller)
@@ -70,6 +91,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI
controller.Processor = CreateProcessor(controller.LogicName);
controller.Processor.SelfEntity = controller.Owner;
controller.Processor.Setup();
_awakeAi.Add(controller.Processor);
}
private AiLogicProcessor CreateProcessor(string name)
@@ -94,7 +116,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI
+ "\n processorId: Class that inherits AiLogicProcessor and has an AiLogicProcessor attribute."
+ "\n entityID: Uid of entity to add the AiControllerComponent to. Open its VV menu to find this.";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if(args.Length != 2)
{

View File

@@ -0,0 +1,24 @@
using Robust.Server.AI;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.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 : EntitySystemMessage
{
/// <summary>
/// Sleep or awake.
/// </summary>
public bool Sleep { get; }
public AiLogicProcessor Processor { get; }
public SleepAiMessage(AiLogicProcessor processor, bool sleep)
{
Processor = processor;
Sleep = sleep;
}
}
}

View File

@@ -1,109 +0,0 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// This interface gives components behavior on getting destoyed.
/// </summary>
public interface IDestroyAct
{
/// <summary>
/// Called when object is destroyed
/// </summary>
void OnDestroy(DestructionEventArgs eventArgs);
}
public class DestructionEventArgs : EventArgs
{
public IEntity Owner { get; set; }
public bool IsSpawnWreck { get; set; }
}
public class BreakageEventArgs : EventArgs
{
public IEntity Owner { get; set; }
}
public interface IBreakAct
{
/// <summary>
/// Called when object is broken
/// </summary>
void OnBreak(BreakageEventArgs eventArgs);
}
public interface IExAct
{
/// <summary>
/// Called when explosion reaches the entity
/// </summary>
void OnExplosion(ExplosionEventArgs eventArgs);
}
public class ExplosionEventArgs : EventArgs
{
public GridCoordinates Source { get; set; }
public IEntity Target { get; set; }
public ExplosionSeverity Severity { get; set; }
}
[UsedImplicitly]
public sealed class ActSystem : EntitySystem
{
public void HandleDestruction(IEntity owner, bool isWreck)
{
var eventArgs = new DestructionEventArgs
{
Owner = owner,
IsSpawnWreck = isWreck
};
var destroyActs = owner.GetAllComponents<IDestroyAct>().ToList();
foreach (var destroyAct in destroyActs)
{
destroyAct.OnDestroy(eventArgs);
}
owner.Delete();
}
public void HandleExplosion(GridCoordinates source, IEntity target, ExplosionSeverity severity)
{
var eventArgs = new ExplosionEventArgs
{
Source = source,
Target = target,
Severity = severity
};
var exActs = target.GetAllComponents<IExAct>().ToList();
foreach (var exAct in exActs)
{
exAct.OnExplosion(eventArgs);
}
}
public void HandleBreakage(IEntity owner)
{
var eventArgs = new BreakageEventArgs
{
Owner = owner,
};
var breakActs = owner.GetAllComponents<IBreakAct>().ToList();
foreach (var breakAct in breakActs)
{
breakAct.OnBreak(eventArgs);
}
}
}
public enum ExplosionSeverity
{
Light,
Heavy,
Destruction,
}
}

View File

@@ -0,0 +1,475 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.Interfaces.GameTicking;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.EntitySystems.Atmos;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems.Atmos
{
[UsedImplicitly]
internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
[Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
[Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
[Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!;
/// <summary>
/// The tiles that have had their atmos data updated since last tick
/// </summary>
private Dictionary<GridId, HashSet<MapIndices>> _invalidTiles = new Dictionary<GridId, HashSet<MapIndices>>();
private Dictionary<IPlayerSession, PlayerGasOverlay> _knownPlayerChunks =
new Dictionary<IPlayerSession, PlayerGasOverlay>();
/// <summary>
/// Gas data stored in chunks to make PVS / bubbling easier.
/// </summary>
private Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>> _overlay =
new Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>>();
/// <summary>
/// How far away do we update gas overlays (minimum; due to chunking further away tiles may also be updated).
/// </summary>
private float _updateRange;
// Because the gas overlay updates aren't run every tick we need to avoid the pop-in that might occur with
// the regular PVS range.
private const float RangeOffset = 6.0f;
/// <summary>
/// Overlay update ticks per second.
/// </summary>
private float _updateCooldown;
public override void Initialize()
{
base.Initialize();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_mapManager.OnGridRemoved += OnGridRemoved;
_configManager.RegisterCVar("net.gasoverlaytickrate", 3.0f);
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_mapManager.OnGridRemoved -= OnGridRemoved;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invalidate(GridId gridIndex, MapIndices indices)
{
if (!_invalidTiles.TryGetValue(gridIndex, out var existing))
{
existing = new HashSet<MapIndices>();
_invalidTiles[gridIndex] = existing;
}
existing.Add(indices);
}
private GasOverlayChunk GetOrCreateChunk(GridId gridIndex, MapIndices indices)
{
if (!_overlay.TryGetValue(gridIndex, out var chunks))
{
chunks = new Dictionary<MapIndices, GasOverlayChunk>();
_overlay[gridIndex] = chunks;
}
var chunkIndices = GetGasChunkIndices(indices);
if (!chunks.TryGetValue(chunkIndices, out var chunk))
{
chunk = new GasOverlayChunk(gridIndex, chunkIndices);
chunks[chunkIndices] = chunk;
}
return chunk;
}
private void OnGridRemoved(GridId gridId)
{
if (_overlay.ContainsKey(gridId))
{
_overlay.Remove(gridId);
}
}
public void ResettingCleanup()
{
_invalidTiles.Clear();
_overlay.Clear();
foreach (var (_, data) in _knownPlayerChunks)
{
data.Reset();
}
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.InGame)
{
if (_knownPlayerChunks.ContainsKey(e.Session))
{
_knownPlayerChunks.Remove(e.Session);
}
return;
}
if (!_knownPlayerChunks.ContainsKey(e.Session))
{
_knownPlayerChunks[e.Session] = new PlayerGasOverlay();
}
}
/// <summary>
/// Checks whether the overlay-relevant data for a gas tile has been updated.
/// </summary>
/// <param name="gam"></param>
/// <param name="oldTile"></param>
/// <param name="indices"></param>
/// <param name="overlayData"></param>
/// <returns>true if updated</returns>
private bool TryRefreshTile(GridAtmosphereComponent gam, GasOverlayData oldTile, MapIndices indices, out GasOverlayData overlayData)
{
var tile = gam.GetTile(indices);
var tileData = new List<GasData>();
for (byte i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var gas = Atmospherics.GetGas(i);
var overlay = Atmospherics.GetOverlay(i);
if (overlay == null || tile.Air == null) continue;
var moles = tile.Air.Gases[i];
if (moles < gas.GasMolesVisible) continue;
var data = new GasData(i, (byte) (FloatMath.Clamp01(moles / gas.GasMolesVisibleMax) * 255));
tileData.Add(data);
}
overlayData = new GasOverlayData(tile.Hotspot.State, tile.Hotspot.Temperature, tileData.Count == 0 ? null : tileData.ToArray());
if (overlayData.Equals(oldTile))
{
return false;
}
return true;
}
/// <summary>
/// Get every chunk in range of our entity that exists, including on other grids.
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
private List<GasOverlayChunk> GetChunksInRange(IEntity entity)
{
var inRange = new List<GasOverlayChunk>();
// This is the max in any direction that we can get a chunk (e.g. max 2 chunks away of data).
var (maxXDiff, maxYDiff) = ((int) (_updateRange / ChunkSize) + 1, (int) (_updateRange / ChunkSize) + 1);
var worldBounds = Box2.CenteredAround(entity.Transform.WorldPosition,
new Vector2(_updateRange, _updateRange));
foreach (var grid in _mapManager.FindGridsIntersecting(entity.Transform.MapID, worldBounds))
{
if (!_overlay.TryGetValue(grid.Index, out var chunks))
{
continue;
}
var entityTile = grid.GetTileRef(entity.Transform.GridPosition).GridIndices;
for (var x = -maxXDiff; x <= maxXDiff; x++)
{
for (var y = -maxYDiff; y <= maxYDiff; y++)
{
var chunkIndices = GetGasChunkIndices(new MapIndices(entityTile.X + x * ChunkSize, entityTile.Y + y * ChunkSize));
if (!chunks.TryGetValue(chunkIndices, out var chunk)) continue;
// Now we'll check if it's in range and relevant for us
// (e.g. if we're on the very edge of a chunk we may need more chunks).
var (xDiff, yDiff) = (chunkIndices.X - entityTile.X, chunkIndices.Y - entityTile.Y);
if (xDiff > 0 && xDiff > _updateRange ||
yDiff > 0 && yDiff > _updateRange ||
xDiff < 0 && Math.Abs(xDiff + ChunkSize) > _updateRange ||
yDiff < 0 && Math.Abs(yDiff + ChunkSize) > _updateRange) continue;
inRange.Add(chunk);
}
}
}
return inRange;
}
public override void Update(float frameTime)
{
AccumulatedFrameTime += frameTime;
_updateCooldown = 1 / _configManager.GetCVar<float>("net.gasoverlaytickrate");
if (AccumulatedFrameTime < _updateCooldown)
{
return;
}
_updateRange = _configManager.GetCVar<float>("net.maxupdaterange") + RangeOffset;
// TODO: So in the worst case scenario we still have to send a LOT of tile data per tick if there's a fire.
// If we go with say 15 tile radius then we have up to 900 tiles to update per tick.
// In a saltern fire the worst you'll normally see is around 650 at the moment.
// Need a way to fake this more because sending almost 2,000 tile updates per second to even 50 players is... yikes
// I mean that's as big as it gets so larger maps will have the same but still, that's a lot of data.
// Some ways to do this are potentially: splitting fire and gas update data so they don't update at the same time
// (gives the illusion of more updates happening), e.g. if gas updates are 3 times a second and fires are 1.6 times a second or something.
// Could also look at updating tiles close to us more frequently (e.g. within 1 chunk every tick).
// Stuff just out of our viewport we need so when we move it doesn't pop in but it doesn't mean we need to update it every tick.
AccumulatedFrameTime -= _updateCooldown;
var gridAtmosComponents = new Dictionary<GridId, GridAtmosphereComponent>();
var updatedTiles = new Dictionary<GasOverlayChunk, HashSet<MapIndices>>();
// So up to this point we've been caching the updated tiles for multiple ticks.
// Now we'll go through and check whether the update actually matters for the overlay or not,
// and if not then we won't bother sending the data.
foreach (var (gridId, indices) in _invalidTiles)
{
var gridEntityId = _mapManager.GetGrid(gridId).GridEntityId;
if (!EntityManager.GetEntity(gridEntityId).TryGetComponent(out GridAtmosphereComponent? gam))
{
continue;
}
// If it's being invalidated it should have this right?
// At any rate we'll cache it for here + the AddChunk
if (!gridAtmosComponents.ContainsKey(gridId))
{
gridAtmosComponents[gridId] = gam;
}
foreach (var invalid in indices)
{
var chunk = GetOrCreateChunk(gridId, invalid);
if (!TryRefreshTile(gam, chunk.GetData(invalid), invalid, out var data)) continue;
if (!updatedTiles.TryGetValue(chunk, out var tiles))
{
tiles = new HashSet<MapIndices>();
updatedTiles[chunk] = tiles;
}
updatedTiles[chunk].Add(invalid);
chunk.Update(data, invalid);
}
}
var currentTick = _gameTiming.CurTick;
// Set the LastUpdate for chunks.
foreach (var (chunk, _) in updatedTiles)
{
chunk.Dirty(currentTick);
}
// Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
// If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
// Afterwards we reset all the chunk data for the next time we tick.
foreach (var (session, overlay) in _knownPlayerChunks)
{
if (session.AttachedEntity == null) continue;
// Get chunks in range and update if we've moved around or the chunks have new overlay data
var chunksInRange = GetChunksInRange(session.AttachedEntity);
var knownChunks = overlay.GetKnownChunks();
var chunksToRemove = new List<GasOverlayChunk>();
var chunksToAdd = new List<GasOverlayChunk>();
foreach (var chunk in chunksInRange)
{
if (!knownChunks.Contains(chunk))
{
chunksToAdd.Add(chunk);
}
}
foreach (var chunk in knownChunks)
{
if (!chunksInRange.Contains(chunk))
{
chunksToRemove.Add(chunk);
}
}
foreach (var chunk in chunksToAdd)
{
var message = overlay.AddChunk(currentTick, chunk);
if (message != null)
{
RaiseNetworkEvent(message, session.ConnectedClient);
}
}
foreach (var chunk in chunksToRemove)
{
overlay.RemoveChunk(chunk);
}
var clientInvalids = new Dictionary<GridId, List<(MapIndices, GasOverlayData)>>();
// Check for any dirty chunks in range and bundle the data to send to the client.
foreach (var chunk in chunksInRange)
{
if (!updatedTiles.TryGetValue(chunk, out var invalids)) continue;
if (!clientInvalids.TryGetValue(chunk.GridIndices, out var existingData))
{
existingData = new List<(MapIndices, GasOverlayData)>();
clientInvalids[chunk.GridIndices] = existingData;
}
chunk.GetData(existingData, invalids);
}
foreach (var (grid, data) in clientInvalids)
{
RaiseNetworkEvent(overlay.UpdateClient(grid, data), session.ConnectedClient);
}
}
// Cleanup
_invalidTiles.Clear();
}
private sealed class PlayerGasOverlay
{
private readonly Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>> _data =
new Dictionary<GridId, Dictionary<MapIndices, GasOverlayChunk>>();
private readonly Dictionary<GasOverlayChunk, GameTick> _lastSent =
new Dictionary<GasOverlayChunk, GameTick>();
public GasOverlayMessage UpdateClient(GridId grid, List<(MapIndices, GasOverlayData)> data)
{
return new GasOverlayMessage(grid, data);
}
public void Reset()
{
_data.Clear();
_lastSent.Clear();
}
public List<GasOverlayChunk> GetKnownChunks()
{
var known = new List<GasOverlayChunk>();
foreach (var (_, chunks) in _data)
{
foreach (var (_, chunk) in chunks)
{
known.Add(chunk);
}
}
return known;
}
public GasOverlayMessage? AddChunk(GameTick currentTick, GasOverlayChunk chunk)
{
if (!_data.TryGetValue(chunk.GridIndices, out var chunks))
{
chunks = new Dictionary<MapIndices, GasOverlayChunk>();
_data[chunk.GridIndices] = chunks;
}
if (_lastSent.TryGetValue(chunk, out var last) && last >= chunk.LastUpdate)
{
return null;
}
_lastSent[chunk] = currentTick;
var message = ChunkToMessage(chunk);
return message;
}
public void RemoveChunk(GasOverlayChunk chunk)
{
// Don't need to sync to client as they can manage it themself.
if (!_data.TryGetValue(chunk.GridIndices, out var chunks))
{
return;
}
if (chunks.ContainsKey(chunk.MapIndices))
{
chunks.Remove(chunk.MapIndices);
}
}
/// <summary>
/// Retrieve a whole chunk as a message, only getting the relevant tiles for the gas overlay.
/// </summary>
/// <param name="chunk"></param>
/// <returns></returns>
private GasOverlayMessage? ChunkToMessage(GasOverlayChunk chunk)
{
// Chunk data should already be up to date.
// Only send relevant tiles to client.
var tileData = new List<(MapIndices, GasOverlayData)>();
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
// TODO: Check could be more robust I think.
var data = chunk.TileData[x, y];
if ((data.Gas == null || data.Gas.Length == 0) && data.FireState == 0 && data.FireTemperature == 0.0f)
{
continue;
}
var indices = new MapIndices(chunk.MapIndices.X + x, chunk.MapIndices.Y + y);
tileData.Add((indices, data));
}
}
if (tileData.Count == 0)
{
return null;
}
return new GasOverlayMessage(chunk.GridIndices, tileData);
}
}
}
}

View File

@@ -34,7 +34,7 @@ namespace Content.Server.GameObjects.EntitySystems
if (!EntityManager.TryGetEntity(grid.GridEntityId, out var gridEnt)) return null;
return gridEnt.TryGetComponent(out IGridAtmosphereComponent atmos) ? atmos : null;
return gridEnt.TryGetComponent(out IGridAtmosphereComponent? atmos) ? atmos : null;
}
public override void Update(float frameTime)

View File

@@ -1,29 +0,0 @@
using Content.Server.GameObjects.Components.Metabolism;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// Triggers metabolism updates for <see cref="BloodstreamComponent"/>
/// </summary>
[UsedImplicitly]
internal sealed class BloodstreamSystem : EntitySystem
{
private float _accumulatedFrameTime;
public override void Update(float frameTime)
{
//Trigger metabolism updates at most once per second
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime > 1.0f)
{
foreach (var component in ComponentManager.EntityQuery<BloodstreamComponent>())
{
component.OnUpdate(_accumulatedFrameTime);
}
_accumulatedFrameTime -= 1.0f;
}
}
}
}

View File

@@ -0,0 +1,29 @@
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Metabolism;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class BodySystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var body in ComponentManager.EntityQuery<BodyManagerComponent>())
{
body.PreMetabolism(frameTime);
}
foreach (var metabolism in ComponentManager.EntityQuery<MetabolismComponent>())
{
metabolism.Update(frameTime);
}
foreach (var body in ComponentManager.EntityQuery<BodyManagerComponent>())
{
body.PostMetabolism(frameTime);
}
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
@@ -12,6 +13,7 @@ using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Input;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Physics;
using Content.Shared.Physics.Pull;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Interfaces.Player;
@@ -437,7 +439,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
/// Uses a weapon/object on an entity
/// Finds components with the InteractUsing interface and calls their function
/// </summary>
public void Interaction(IEntity user, IEntity weapon, IEntity attacked, GridCoordinates clickLocation)
public async Task Interaction(IEntity user, IEntity weapon, IEntity attacked, GridCoordinates clickLocation)
{
var attackMsg = new InteractUsingMessage(user, weapon, attacked, clickLocation);
RaiseLocalEvent(attackMsg);
@@ -446,7 +448,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
return;
}
var attackBys = attacked.GetAllComponents<IInteractUsing>().ToList();
var attackBys = attacked.GetAllComponents<IInteractUsing>().OrderByDescending(x => x.Priority);
var attackByEventArgs = new InteractUsingEventArgs
{
User = user, ClickLocation = clickLocation, Using = weapon, Target = attacked
@@ -457,7 +459,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
{
foreach (var attackBy in attackBys)
{
if (attackBy.InteractUsing(attackByEventArgs))
if (await attackBy.InteractUsing(attackByEventArgs))
{
// If an InteractUsing returns a status completion we finish our attack
return;

View File

@@ -0,0 +1,18 @@
using Content.Server.GameObjects.Components.Movement;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class ClimbSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<ClimbingComponent>())
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
internal sealed class CloningSystem : EntitySystem
{
public static List<EntityUid> scannedUids = new List<EntityUid>();
public static void AddToScannedUids(EntityUid uid)
{
if (!scannedUids.Contains(uid))
{
scannedUids.Add(uid);
}
}
public static bool HasUid(EntityUid uid)
{
return scannedUids.Contains(uid);
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Construction;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Interactable;
@@ -76,7 +77,7 @@ namespace Content.Server.GameObjects.EntitySystems
TryStartItemConstruction(placingEnt, msg.PrototypeName);
}
private void HandleToolInteraction(AfterInteractMessage msg)
private async void HandleToolInteraction(AfterInteractMessage msg)
{
if(msg.Handled)
return;
@@ -104,7 +105,7 @@ namespace Content.Server.GameObjects.EntitySystems
// the target entity is in the process of being constructed/deconstructed
if (msg.Attacked.TryGetComponent<ConstructionComponent>(out var constructComp))
{
var result = TryConstructEntity(constructComp, handEnt, msg.User);
var result = await TryConstructEntity(constructComp, handEnt, msg.User);
// TryConstructEntity may delete the existing entity
@@ -367,7 +368,7 @@ namespace Content.Server.GameObjects.EntitySystems
}
}
private bool TryConstructEntity(ConstructionComponent constructionComponent, IEntity handTool, IEntity user)
private async Task<bool> TryConstructEntity(ConstructionComponent constructionComponent, IEntity handTool, IEntity user)
{
var constructEntity = constructionComponent.Owner;
var spriteComponent = constructEntity.GetComponent<SpriteComponent>();
@@ -384,7 +385,7 @@ namespace Content.Server.GameObjects.EntitySystems
var stage = constructPrototype.Stages[constructionComponent.Stage];
if (TryProcessStep(constructEntity, stage.Forward, handTool, user, transformComponent.GridPosition))
if (await TryProcessStep(constructEntity, stage.Forward, handTool, user, transformComponent.GridPosition))
{
constructionComponent.Stage++;
if (constructionComponent.Stage == constructPrototype.Stages.Count - 1)
@@ -406,7 +407,7 @@ namespace Content.Server.GameObjects.EntitySystems
}
}
else if (TryProcessStep(constructEntity, stage.Backward, handTool, user, transformComponent.GridPosition))
else if (await TryProcessStep(constructEntity, stage.Backward, handTool, user, transformComponent.GridPosition))
{
constructionComponent.Stage--;
stage = constructPrototype.Stages[constructionComponent.Stage];
@@ -443,7 +444,7 @@ namespace Content.Server.GameObjects.EntitySystems
}
}
private bool TryProcessStep(IEntity constructEntity, ConstructionStep step, IEntity slapped, IEntity user, GridCoordinates gridCoords)
private async Task<bool> TryProcessStep(IEntity constructEntity, ConstructionStep step, IEntity slapped, IEntity user, GridCoordinates gridCoords)
{
if (step == null)
{
@@ -473,9 +474,9 @@ namespace Content.Server.GameObjects.EntitySystems
// Handle welder manually since tool steps specify fuel amount needed, for some reason.
if (toolStep.ToolQuality.HasFlag(ToolQuality.Welding))
return slapped.TryGetComponent<WelderComponent>(out var welder)
&& welder.UseTool(user, constructEntity, toolStep.ToolQuality, toolStep.Amount);
&& await welder.UseTool(user, constructEntity, toolStep.DoAfterDelay, toolStep.ToolQuality, toolStep.Amount);
return tool.UseTool(user, constructEntity, toolStep.ToolQuality);
return await tool.UseTool(user, constructEntity, toolStep.DoAfterDelay, toolStep.ToolQuality);
default:
throw new NotImplementedException();

View File

@@ -5,6 +5,7 @@ using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -53,7 +54,7 @@ namespace Content.Server.GameObjects.EntitySystems.DoAfter
// For this we need to stay on the same hand slot and need the same item in that hand slot
// (or if there is no item there we need to keep it free).
if (eventArgs.NeedHand && eventArgs.User.TryGetComponent(out HandsComponent handsComponent))
if (eventArgs.NeedHand && eventArgs.User.TryGetComponent(out HandsComponent? handsComponent))
{
_activeHand = handsComponent.ActiveHand;
_activeItem = handsComponent.GetActiveHand;
@@ -63,7 +64,7 @@ namespace Content.Server.GameObjects.EntitySystems.DoAfter
AsTask = Tcs.Task;
}
public void HandleDamage(object? sender, DamageEventArgs eventArgs)
public void HandleDamage(HealthChangedEventArgs args)
{
_tookDamage = true;
}
@@ -125,7 +126,7 @@ namespace Content.Server.GameObjects.EntitySystems.DoAfter
}
if (EventArgs.BreakOnStun &&
EventArgs.User.TryGetComponent(out StunnableComponent stunnableComponent) &&
EventArgs.User.TryGetComponent(out StunnableComponent? stunnableComponent) &&
stunnableComponent.Stunned)
{
return true;
@@ -133,7 +134,7 @@ namespace Content.Server.GameObjects.EntitySystems.DoAfter
if (EventArgs.NeedHand)
{
if (!EventArgs.User.TryGetComponent(out HandsComponent handsComponent))
if (!EventArgs.User.TryGetComponent(out HandsComponent? handsComponent))
{
// If we had a hand but no longer have it that's still a paddlin'
if (_activeHand != null)

View File

@@ -3,7 +3,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
using Content.Server.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Damage;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Timing;
using Robust.Shared.GameObjects.Systems;
@@ -73,19 +73,19 @@ namespace Content.Server.GameObjects.EntitySystems.DoAfter
// Caller's gonna be responsible for this I guess
var doAfterComponent = eventArgs.User.GetComponent<DoAfterComponent>();
doAfterComponent.Add(doAfter);
DamageableComponent? damageableComponent = null;
IDamageableComponent? damageableComponent = null;
// TODO: If the component's deleted this may not get unsubscribed?
if (eventArgs.BreakOnDamage && eventArgs.User.TryGetComponent(out damageableComponent))
{
damageableComponent.Damaged += doAfter.HandleDamage;
damageableComponent.HealthChangedEvent += doAfter.HandleDamage;
}
await doAfter.AsTask;
if (damageableComponent != null)
{
damageableComponent.Damaged -= doAfter.HandleDamage;
damageableComponent.HealthChangedEvent -= doAfter.HandleDamage;
}
return doAfter.Status;

View File

@@ -1,145 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Content.Server.GameObjects.Components.Atmos;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
private int _tickTimer = 0;
private HashSet<GasTileOverlayData> _queue = new HashSet<GasTileOverlayData>();
private Dictionary<GridId, HashSet<MapIndices>> _invalid = new Dictionary<GridId, HashSet<MapIndices>>();
private Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>> _overlay =
new Dictionary<GridId, Dictionary<MapIndices, GasOverlayData>>();
[Robust.Shared.IoC.Dependency] private IPlayerManager _playerManager = default!;
public override void Initialize()
{
base.Initialize();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invalidate(GridId gridIndex, MapIndices indices)
{
if (!_invalid.TryGetValue(gridIndex, out var set) || set == null)
{
set = new HashSet<MapIndices>();
_invalid.Add(gridIndex, set);
}
set.Add(indices);
}
public void SetTileOverlay(GridId gridIndex, MapIndices indices, GasData[] gasData, int fireState = 0, float fireTemperature = 0f)
{
if(!_overlay.TryGetValue(gridIndex, out var _))
_overlay[gridIndex] = new Dictionary<MapIndices, GasOverlayData>();
_overlay[gridIndex][indices] = new GasOverlayData(fireState, fireTemperature, gasData);
_queue.Add(GetData(gridIndex, indices));
}
private void OnPlayerStatusChanged(object sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.InGame) return;
RaiseNetworkEvent(new GasTileOverlayMessage(GetData(), true), e.Session.ConnectedClient);
}
private GasTileOverlayData[] GetData()
{
var list = new List<GasTileOverlayData>();
foreach (var (gridId, tiles) in _overlay)
{
foreach (var (indices, _) in tiles)
{
var data = GetData(gridId, indices);
if(data.Data.Gas.Length > 0)
list.Add(data);
}
}
return list.ToArray();
}
private GasTileOverlayData GetData(GridId gridIndex, MapIndices indices)
{
return new GasTileOverlayData(gridIndex, indices, _overlay[gridIndex][indices]);
}
private void Revalidate()
{
var mapMan = IoCManager.Resolve<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
var list = new List<GasData>();
foreach (var (gridId, indices) in _invalid)
{
if (!mapMan.GridExists(gridId))
{
_invalid.Remove(gridId);
return;
}
var grid = entityMan.GetEntity(mapMan.GetGrid(gridId).GridEntityId);
if (!grid.TryGetComponent(out GridAtmosphereComponent gam)) continue;
foreach (var index in indices)
{
var tile = gam.GetTile(index);
if (tile?.Air == null) continue;
list.Clear();
for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var gas = Atmospherics.GetGas(i);
var overlay = gas.GasOverlay;
if (overlay == null) continue;
var moles = tile.Air.Gases[i];
if(moles == 0f || moles < gas.GasMolesVisible) continue;
list.Add(new GasData(i, MathF.Max(MathF.Min(1, moles / gas.GasMolesVisibleMax), 0f)));
}
if (list.Count == 0) continue;
SetTileOverlay(gridId, index, list.ToArray(), tile.Hotspot.State, tile.Hotspot.Temperature);
}
indices.Clear();
}
}
public override void Update(float frameTime)
{
_tickTimer++;
Revalidate();
if (_tickTimer < 10) return;
_tickTimer = 0;
if(_queue.Count > 0)
RaiseNetworkEvent(new GasTileOverlayMessage(_queue.ToArray()));
_queue.Clear();
}
}
}

View File

@@ -0,0 +1,20 @@
using Content.Server.Atmos;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
public class GasVaporSystem : EntitySystem
{
/// <inheritdoc />
public override void Update(float frameTime)
{
foreach (var GasVapor in ComponentManager.EntityQuery<GasVaporComponent>())
{
if (GasVapor.Initialized)
{
GasVapor.Update(frameTime);
}
}
}
}
}

View File

@@ -5,20 +5,21 @@ using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class HungerSystem : EntitySystem
public class HungerSystem : EntitySystem
{
private float _accumulatedFrameTime;
public override void Update(float frameTime)
{
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime > 1.0f)
if (_accumulatedFrameTime > 1)
{
foreach (var comp in ComponentManager.EntityQuery<HungerComponent>())
{
comp.OnUpdate(_accumulatedFrameTime);
}
_accumulatedFrameTime -= 1.0f;
_accumulatedFrameTime -= 1;
}
}
}

View File

@@ -7,6 +7,7 @@ namespace Content.Server.GameObjects.EntitySystems
[UsedImplicitly]
internal sealed class MedicalScannerSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<MedicalScannerComponent>())

View File

@@ -83,7 +83,7 @@ namespace Content.Server.GameObjects.EntitySystems
ev.Entity.RemoveComponent<PlayerInputMoverComponent>();
}
if (ev.Entity.TryGetComponent(out ICollidableComponent physics) &&
if (ev.Entity.TryGetComponent(out ICollidableComponent? physics) &&
physics.TryGetController(out MoverController controller))
{
controller.StopMoving();

View File

@@ -6,6 +6,7 @@ using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Input;
using Content.Shared.Interfaces;
using JetBrains.Annotations;
using Robust.Server.GameObjects.Components;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
@@ -113,7 +114,13 @@ namespace Content.Server.GameObjects.EntitySystems
var viewers = _playerManager.GetPlayersInRange(player.Transform.GridPosition, 15);
EntityManager.SpawnEntity("pointingarrow", coords);
var arrow = EntityManager.SpawnEntity("pointingarrow", coords);
if (player.TryGetComponent(out VisibilityComponent? playerVisibility))
{
var arrowVisibility = arrow.EnsureComponent<VisibilityComponent>();
arrowVisibility.Layer = playerVisibility.Layer;
}
string selfMessage;
string viewerMessage;

View File

@@ -2,7 +2,9 @@ using System.Collections.Generic;
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.StationEvents;
using Content.Shared.Damage;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
@@ -28,13 +30,13 @@ namespace Content.Server.GameObjects.EntitySystems.StationEvents
private const int DamageThreshold = 10;
private Dictionary<IEntity, float> _accumulatedDamage = new Dictionary<IEntity, float>();
public override void Initialize()
{
base.Initialize();
_speciesQuery = new TypeEntityQuery(typeof(SpeciesComponent));
_speciesQuery = new TypeEntityQuery(typeof(IBodyManagerComponent));
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -72,7 +74,7 @@ namespace Content.Server.GameObjects.EntitySystems.StationEvents
var damageMultiple = (int) (totalDamage / DamageThreshold);
_accumulatedDamage[species] = totalDamage % DamageThreshold;
damageableComponent.TakeDamage(DamageType.Heat, damageMultiple * DamageThreshold, comp.Owner, comp.Owner);
damageableComponent.ChangeDamage(DamageType.Heat, damageMultiple * DamageThreshold, false, comp.Owner);
}
}
@@ -80,10 +82,10 @@ namespace Content.Server.GameObjects.EntitySystems.StationEvents
{
return;
}
// probably don't need to worry about clearing this at roundreset unless you have a radiation pulse at roundstart
// (which is currently not possible)
_accumulatedDamage.Clear();
}
}
}
}

View File

@@ -2,25 +2,34 @@ using System;
using System.Collections.Generic;
using System.Text;
using Content.Server.StationEvents;
using Content.Server.Interfaces.GameTicking;
using JetBrains.Annotations;
using Robust.Server.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using static Content.Shared.StationEvents.SharedStationEvent;
namespace Content.Server.GameObjects.EntitySystems.StationEvents
{
[UsedImplicitly]
// Somewhat based off of TG's implementation of events
public sealed class StationEventSystem : EntitySystem
{
// Somewhat based off of TG's implementation of events
public StationEvent CurrentEvent { get; private set; }
#pragma warning disable 649
[Dependency] private readonly IServerNetManager _netManager;
[Dependency] private readonly IPlayerManager _playerManager;
[Dependency] private readonly IGameTicker _gameTicker;
#pragma warning restore 649
public StationEvent CurrentEvent { get; private set; }
public IReadOnlyCollection<StationEvent> StationEvents => _stationEvents;
private List<StationEvent> _stationEvents = new List<StationEvent>();
private const float MinimumTimeUntilFirstEvent = 600;
@@ -154,8 +163,31 @@ namespace Content.Server.GameObjects.EntitySystems.StationEvents
var stationEvent = (StationEvent) typeFactory.CreateInstance(type);
_stationEvents.Add(stationEvent);
}
_netManager.RegisterNetMessage<MsgGetStationEvents>(nameof(MsgGetStationEvents), GetEventReceived);
}
private void GetEventReceived(MsgGetStationEvents msg)
{
var player = _playerManager.GetSessionByChannel(msg.MsgChannel);
SendEvents(player);
}
private void SendEvents(IPlayerSession player)
{
if (!IoCManager.Resolve<IConGroupController>().CanCommand(player, "events"))
return;
var newMsg = _netManager.CreateNetMessage<MsgGetStationEvents>();
newMsg.Events = new List<string>();
foreach (var e in StationEvents)
{
newMsg.Events.Add(e.Name);
}
_netManager.ServerSendMessage(newMsg, player.ConnectedClient);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -164,7 +196,18 @@ namespace Content.Server.GameObjects.EntitySystems.StationEvents
{
return;
}
// Stop events from happening in lobby and force active event to end if the round ends
if (_gameTicker.RunLevel != GameTicking.GameRunLevel.InRound)
{
if (CurrentEvent != null)
{
Enabled = false;
}
return;
}
// Keep running the current event
if (CurrentEvent != null)
{
@@ -318,4 +361,4 @@ namespace Content.Server.GameObjects.EntitySystems.StationEvents
CurrentEvent?.Shutdown();
}
}
}
}

View File

@@ -1,29 +0,0 @@
using Content.Server.GameObjects.Components.Nutrition;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// Triggers digestion updates on <see cref="StomachComponent"/>
/// </summary>
[UsedImplicitly]
internal sealed class StomachSystem : EntitySystem
{
private float _accumulatedFrameTime;
public override void Update(float frameTime)
{
//Update at most once per second
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime > 1.0f)
{
foreach (var component in ComponentManager.EntityQuery<StomachComponent>())
{
component.OnUpdate(_accumulatedFrameTime);
}
_accumulatedFrameTime -= 1.0f;
}
}
}
}

View File

@@ -5,20 +5,21 @@ using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class ThirstSystem : EntitySystem
public class ThirstSystem : EntitySystem
{
private float _accumulatedFrameTime;
public override void Update(float frameTime)
{
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime > 1.0f)
if (_accumulatedFrameTime > 1)
{
foreach (var component in ComponentManager.EntityQuery<ThirstComponent>())
{
component.OnUpdate(_accumulatedFrameTime);
}
_accumulatedFrameTime -= 1.0f;
_accumulatedFrameTime -= 1;
}
}
}

View File

@@ -113,8 +113,17 @@ namespace Content.Server.GameObjects.EntitySystems
if (verb.RequireInteractionRange && !VerbUtility.InVerbUseRange(userEntity, entity))
continue;
if (verb.BlockedByContainers && !userEntity.IsInSameOrNoContainer(entity))
continue;
if (verb.BlockedByContainers)
{
if (!userEntity.IsInSameOrNoContainer(entity))
{
if (!ContainerHelpers.TryGetContainer(entity, out var container) ||
container.Owner != userEntity)
{
continue;
}
}
}
var verbData = verb.GetData(userEntity, component);
if (verbData.IsInvisible)