mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 18:31:17 +03:00
Merge remote-tracking branch 'upstream/master' into upstream-sync
# Conflicts: # .github/PULL_REQUEST_TEMPLATE.md # Content.Server/GameTicking/GameTicker.RoundFlow.cs # Resources/Prototypes/Entities/Clothing/Neck/cloaks.yml # Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml
This commit is contained in:
commit
b877259791
742 changed files with 243785 additions and 6598 deletions
|
|
@ -1,15 +1,15 @@
|
|||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
Title="{Loc 'admin-announce-title'}"
|
||||
Title="{Loc admin-announce-title}"
|
||||
MinWidth="500">
|
||||
<GridContainer Columns="1">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<LineEdit Name="Announcer" Access="Public" PlaceHolder="{Loc 'announcer-placeholder'}" Text="{Loc 'Central Command'}" HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
|
||||
<LineEdit Name="Announcer" Access="Public" PlaceHolder="{Loc admin-announce-announcer-placeholder}" Text="{Loc admin-announce-announcer-default}" HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="1" />
|
||||
<OptionButton Name="AnnounceMethod" Access="Public" HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
|
||||
</BoxContainer>
|
||||
<LineEdit Name="Announcement" Access="Public" PlaceHolder="{Loc 'announcement-placeholder'}"/>
|
||||
<LineEdit Name="Announcement" Access="Public" PlaceHolder="{Loc admin-announce-announcement-placeholder}"/>
|
||||
|
||||
<Button Name="AnnounceButton" Access="Public" Disabled="True" Text="{Loc 'Announce'}" HorizontalAlignment="Center"/>
|
||||
<Button Name="AnnounceButton" Access="Public" Disabled="True" Text="{Loc admin-announce-button}" HorizontalAlignment="Center"/>
|
||||
</GridContainer>
|
||||
</DefaultWindow>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ namespace Content.Client.Administration.UI
|
|||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
AnnounceMethod.AddItem(_localization.GetString("announce-type-station"));
|
||||
AnnounceMethod.AddItem(_localization.GetString("admin-announce-type-station"));
|
||||
AnnounceMethod.SetItemMetadata(0, AdminAnnounceType.Station);
|
||||
AnnounceMethod.AddItem(_localization.GetString("announce-type-server"));
|
||||
AnnounceMethod.AddItem(_localization.GetString("admin-announce-type-server"));
|
||||
AnnounceMethod.SetItemMetadata(1, AdminAnnounceType.Server);
|
||||
AnnounceMethod.OnItemSelected += AnnounceMethodOnOnItemSelected;
|
||||
Announcement.OnTextChanged += AnnouncementOnOnTextChanged;
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@
|
|||
<BoxContainer Orientation="Vertical">
|
||||
<GridContainer Columns="3">
|
||||
<cc:UICommandButton Command="kick" Text="{Loc admin-player-actions-window-title}" WindowType="{x:Type at:PlayerActionsWindow}" />
|
||||
<cc:UICommandButton Command="ban" Text="{Loc Ban}" WindowType="{x:Type at:BanWindow}" />
|
||||
<cc:CommandButton Command="aghost" Text="{Loc Admin Ghost}" />
|
||||
<cc:UICommandButton Command="tpto" Text="{Loc Teleport}" WindowType="{x:Type at:TeleportWindow}" />
|
||||
<cc:CommandButton Command="permissions" Text="{Loc Permissions Panel}" />
|
||||
<cc:CommandButton Command="announceui" Text="{Loc Announce}"/>
|
||||
<cc:UICommandButton Command="callshuttle" Text="{Loc (Re)call Shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
|
||||
<cc:CommandButton Command="adminlogs" Text="{Loc Admin Logs}"/>
|
||||
<cc:CommandButton Command="adminnotes" Text="{Loc Admin Notes}"/>
|
||||
<cc:UICommandButton Command="ban" Text="{Loc admin-player-actions-window-ban}" WindowType="{x:Type at:BanWindow}" />
|
||||
<cc:CommandButton Command="aghost" Text="{Loc admin-player-actions-window-admin-ghost}" />
|
||||
<cc:UICommandButton Command="tpto" Text="{Loc admin-player-actions-window-teleport}" WindowType="{x:Type at:TeleportWindow}" />
|
||||
<cc:CommandButton Command="permissions" Text="{Loc admin-player-actions-window-permissions}" />
|
||||
<cc:CommandButton Command="announceui" Text="{Loc admin-player-actions-window-announce}"/>
|
||||
<cc:UICommandButton Command="callshuttle" Text="{Loc admin-player-actions-window-shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
|
||||
<cc:CommandButton Command="adminlogs" Text="{Loc admin-player-actions-window-admin-logs}"/>
|
||||
<cc:CommandButton Command="adminnotes" Text="{Loc admin-player-actions-window-admin-notes}"/>
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
|
|
|||
|
|
@ -1,264 +1,70 @@
|
|||
using Content.Client.Atmos.Overlays;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.GameTicking;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
|
||||
public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private readonly SpriteSystem _spriteSys = default!;
|
||||
|
||||
// Gas overlays
|
||||
public readonly float[] Timer = new float[Atmospherics.TotalNumberOfGases];
|
||||
public readonly float[][] FrameDelays = new float[Atmospherics.TotalNumberOfGases][];
|
||||
public readonly int[] FrameCounter = new int[Atmospherics.TotalNumberOfGases];
|
||||
public readonly Texture[][] Frames = new Texture[Atmospherics.TotalNumberOfGases][];
|
||||
|
||||
// Fire overlays
|
||||
public const int FireStates = 3;
|
||||
public const string FireRsiPath = "/Textures/Effects/fire.rsi";
|
||||
|
||||
public readonly float[] FireTimer = new float[FireStates];
|
||||
public readonly float[][] FireFrameDelays = new float[FireStates][];
|
||||
public readonly int[] FireFrameCounter = new int[FireStates];
|
||||
public readonly Texture[][] FireFrames = new Texture[FireStates][];
|
||||
|
||||
private readonly Dictionary<EntityUid, Dictionary<Vector2i, GasOverlayChunk>> _tileData =
|
||||
new();
|
||||
|
||||
public const int GasOverlayZIndex = 1;
|
||||
private GasTileOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<GasOverlayMessage>(HandleGasOverlayMessage);
|
||||
SubscribeNetworkEvent<GasOverlayUpdateEvent>(HandleGasOverlayUpdate);
|
||||
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||
|
||||
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
|
||||
{
|
||||
var overlay = _atmosphereSystem.GetOverlay(i);
|
||||
switch (overlay)
|
||||
{
|
||||
case SpriteSpecifier.Rsi animated:
|
||||
var rsi = _resourceCache.GetResource<RSIResource>(animated.RsiPath).RSI;
|
||||
var stateId = animated.RsiState;
|
||||
|
||||
if(!rsi.TryGetState(stateId, out var state)) continue;
|
||||
|
||||
Frames[i] = state.GetFrames(RSI.State.Direction.South);
|
||||
FrameDelays[i] = state.GetDelays();
|
||||
FrameCounter[i] = 0;
|
||||
break;
|
||||
case SpriteSpecifier.Texture texture:
|
||||
Frames[i] = new[] {texture.Frame0()};
|
||||
FrameDelays[i] = Array.Empty<float>();
|
||||
break;
|
||||
case null:
|
||||
Frames[i] = Array.Empty<Texture>();
|
||||
FrameDelays[i] = Array.Empty<float>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var fire = _resourceCache.GetResource<RSIResource>(FireRsiPath).RSI;
|
||||
|
||||
for (var i = 0; i < FireStates; i++)
|
||||
{
|
||||
if (!fire.TryGetState((i+1).ToString(), out var state))
|
||||
throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
|
||||
|
||||
FireFrames[i] = state.GetFrames(RSI.State.Direction.South);
|
||||
FireFrameDelays[i] = state.GetDelays();
|
||||
FireFrameCounter[i] = 0;
|
||||
}
|
||||
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
overlayManager.AddOverlay(new GasTileOverlay());
|
||||
overlayManager.AddOverlay(new FireTileOverlay());
|
||||
_overlay = new GasTileOverlay(this, _resourceCache, ProtoMan, _spriteSys);
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void HandleGasOverlayMessage(GasOverlayMessage message)
|
||||
public override void Reset(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
foreach (var (indices, data) in message.OverlayData)
|
||||
{
|
||||
var chunk = GetOrCreateChunk(message.GridId, indices);
|
||||
chunk.Update(data, indices);
|
||||
}
|
||||
}
|
||||
|
||||
// Slightly different to the server-side system version
|
||||
private GasOverlayChunk GetOrCreateChunk(EntityUid gridId, Vector2i indices)
|
||||
{
|
||||
if (!_tileData.TryGetValue(gridId, out var chunks))
|
||||
{
|
||||
chunks = new Dictionary<Vector2i, GasOverlayChunk>();
|
||||
_tileData[gridId] = chunks;
|
||||
}
|
||||
|
||||
var chunkIndices = GetGasChunkIndices(indices);
|
||||
|
||||
if (!chunks.TryGetValue(chunkIndices, out var chunk))
|
||||
{
|
||||
chunk = new GasOverlayChunk(gridId, chunkIndices);
|
||||
chunks[chunkIndices] = chunk;
|
||||
}
|
||||
|
||||
return chunk;
|
||||
_overlay.TileData.Clear();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
overlayManager.RemoveOverlay<GasTileOverlay>();
|
||||
overlayManager.RemoveOverlay<FireTileOverlay>();
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev)
|
||||
{
|
||||
foreach (var (grid, removedIndicies) in ev.RemovedChunks)
|
||||
{
|
||||
if (!_overlay.TileData.TryGetValue(grid, out var chunks))
|
||||
continue;
|
||||
|
||||
foreach (var index in removedIndicies)
|
||||
{
|
||||
chunks.Remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (grid, gridData) in ev.UpdatedChunks)
|
||||
{
|
||||
var chunks = _overlay.TileData.GetOrNew(grid);
|
||||
foreach (var chunkData in gridData)
|
||||
{
|
||||
chunks[chunkData.Index] = chunkData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGridRemoved(GridRemovalEvent ev)
|
||||
{
|
||||
_tileData.Remove(ev.EntityUid);
|
||||
}
|
||||
|
||||
public bool HasData(EntityUid gridId)
|
||||
{
|
||||
return _tileData.ContainsKey(gridId);
|
||||
}
|
||||
|
||||
public GasOverlayEnumerator GetOverlays(EntityUid gridIndex, Vector2i indices)
|
||||
{
|
||||
if (!_tileData.TryGetValue(gridIndex, out var chunks))
|
||||
return default;
|
||||
|
||||
var chunkIndex = GetGasChunkIndices(indices);
|
||||
if (!chunks.TryGetValue(chunkIndex, out var chunk))
|
||||
return default;
|
||||
|
||||
var overlays = chunk.GetData(indices);
|
||||
|
||||
return new GasOverlayEnumerator(overlays, this);
|
||||
}
|
||||
|
||||
public FireOverlayEnumerator GetFireOverlays(EntityUid gridIndex, Vector2i indices)
|
||||
{
|
||||
if (!_tileData.TryGetValue(gridIndex, out var chunks))
|
||||
return default;
|
||||
|
||||
var chunkIndex = GetGasChunkIndices(indices);
|
||||
if (!chunks.TryGetValue(chunkIndex, out var chunk))
|
||||
return default;
|
||||
|
||||
var overlays = chunk.GetData(indices);
|
||||
|
||||
return new FireOverlayEnumerator(overlays, this);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
|
||||
{
|
||||
var delays = FrameDelays[i];
|
||||
if (delays.Length == 0) continue;
|
||||
|
||||
var frameCount = FrameCounter[i];
|
||||
Timer[i] += frameTime;
|
||||
var time = delays[frameCount];
|
||||
|
||||
if (Timer[i] < time)
|
||||
continue;
|
||||
|
||||
Timer[i] -= time;
|
||||
FrameCounter[i] = (frameCount + 1) % Frames[i].Length;
|
||||
}
|
||||
|
||||
for (var i = 0; i < FireStates; i++)
|
||||
{
|
||||
var delays = FireFrameDelays[i];
|
||||
if (delays.Length == 0) continue;
|
||||
|
||||
var frameCount = FireFrameCounter[i];
|
||||
FireTimer[i] += frameTime;
|
||||
var time = delays[frameCount];
|
||||
|
||||
if (FireTimer[i] < time) continue;
|
||||
FireTimer[i] -= time;
|
||||
FireFrameCounter[i] = (frameCount + 1) % FireFrames[i].Length;
|
||||
}
|
||||
}
|
||||
|
||||
public struct GasOverlayEnumerator
|
||||
{
|
||||
private readonly GasTileOverlaySystem _system;
|
||||
private readonly GasData[]? _data;
|
||||
// TODO: Take Fire Temperature into account, when we code fire color
|
||||
|
||||
private readonly int _length; // We cache the length so we can avoid a pointer dereference, for speed. Brrr.
|
||||
private int _current;
|
||||
|
||||
public GasOverlayEnumerator(in GasOverlayData data, GasTileOverlaySystem system)
|
||||
{
|
||||
// Gas can't be null, as the caller to this constructor already ensured it wasn't.
|
||||
_data = data.Gas;
|
||||
|
||||
_system = system;
|
||||
|
||||
_length = _data?.Length ?? 0;
|
||||
_current = 0;
|
||||
}
|
||||
|
||||
public bool MoveNext(out (Texture Texture, Color Color) overlay)
|
||||
{
|
||||
if (_current < _length)
|
||||
{
|
||||
// Data can't be null here unless length/current are incorrect
|
||||
var gas = _data![_current++];
|
||||
var frames = _system.Frames[gas.Index];
|
||||
overlay = (frames[_system.FrameCounter[gas.Index]], Color.White.WithAlpha(gas.Opacity));
|
||||
return true;
|
||||
}
|
||||
|
||||
overlay = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public struct FireOverlayEnumerator
|
||||
{
|
||||
private readonly GasTileOverlaySystem _system;
|
||||
private byte _fireState;
|
||||
// TODO: Take Fire Temperature into account, when we code fire color
|
||||
|
||||
public FireOverlayEnumerator(in GasOverlayData data, GasTileOverlaySystem system)
|
||||
{
|
||||
_fireState = data.FireState;
|
||||
_system = system;
|
||||
}
|
||||
public bool MoveNext(out (Texture Texture, Color Color) overlay)
|
||||
{
|
||||
|
||||
if (_fireState != 0)
|
||||
{
|
||||
var state = _fireState - 1;
|
||||
var frames = _system.FireFrames[state];
|
||||
// TODO ATMOS Set color depending on temperature
|
||||
overlay = (frames[_system.FireFrameCounter[state]], Color.White);
|
||||
|
||||
// Setting this to zero so we don't get stuck in an infinite loop.
|
||||
_fireState = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
overlay = default;
|
||||
return false;
|
||||
}
|
||||
_overlay.TileData.Remove(ev.EntityUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
using Content.Client.Atmos.EntitySystems;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Atmos.Overlays
|
||||
{
|
||||
public sealed class FireTileOverlay : Overlay
|
||||
{
|
||||
private readonly GasTileOverlaySystem _gasTileOverlaySystem;
|
||||
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
private readonly ShaderInstance _shader;
|
||||
|
||||
public FireTileOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_gasTileOverlaySystem = EntitySystem.Get<GasTileOverlaySystem>();
|
||||
_shader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance().Duplicate();
|
||||
ZIndex = GasTileOverlaySystem.GasOverlayZIndex + 1;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var drawHandle = args.WorldHandle;
|
||||
|
||||
var mapId = args.Viewport.Eye!.Position.MapId;
|
||||
var worldBounds = args.WorldBounds;
|
||||
|
||||
drawHandle.UseShader(_shader);
|
||||
|
||||
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
|
||||
{
|
||||
if (!_gasTileOverlaySystem.HasData(mapGrid.GridEntityId))
|
||||
continue;
|
||||
|
||||
drawHandle.SetTransform(mapGrid.WorldMatrix);
|
||||
|
||||
foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds))
|
||||
{
|
||||
var enumerator = _gasTileOverlaySystem.GetFireOverlays(mapGrid.GridEntityId, tile.GridIndices);
|
||||
while (enumerator.MoveNext(out var tuple))
|
||||
{
|
||||
drawHandle.DrawTexture(tuple.Texture, new Vector2(tile.X, tile.Y), tuple.Color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawHandle.SetTransform(Matrix3.Identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +1,208 @@
|
|||
using Content.Client.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Atmos.Overlays
|
||||
{
|
||||
public sealed class GasTileOverlay : Overlay
|
||||
{
|
||||
private readonly GasTileOverlaySystem _gasTileOverlaySystem;
|
||||
private readonly GasTileOverlaySystem _system;
|
||||
private readonly IMapManager _mapManager;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;
|
||||
private readonly ShaderInstance _shader;
|
||||
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
public readonly Dictionary<EntityUid, Dictionary<Vector2i, GasOverlayChunk>> TileData = new();
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
// Gas overlays
|
||||
private readonly float[] _timer;
|
||||
private readonly float[][] _frameDelays;
|
||||
private readonly int[] _frameCounter;
|
||||
|
||||
public GasTileOverlay()
|
||||
// TODO combine textures into a single texture atlas.
|
||||
private readonly Texture[][] _frames;
|
||||
|
||||
// Fire overlays
|
||||
private const int FireStates = 3;
|
||||
private const string FireRsiPath = "/Textures/Effects/fire.rsi";
|
||||
|
||||
private readonly float[] _fireTimer = new float[FireStates];
|
||||
private readonly float[][] _fireFrameDelays = new float[FireStates][];
|
||||
private readonly int[] _fireFrameCounter = new int[FireStates];
|
||||
private readonly Texture[][] _fireFrames = new Texture[FireStates][];
|
||||
|
||||
private int _gasCount;
|
||||
|
||||
public const int GasOverlayZIndex = (int) Content.Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else
|
||||
|
||||
public GasTileOverlay(GasTileOverlaySystem system, IResourceCache resourceCache, IPrototypeManager protoMan, SpriteSystem spriteSys)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_system = system;
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
_shader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
|
||||
ZIndex = GasOverlayZIndex;
|
||||
|
||||
_gasTileOverlaySystem = EntitySystem.Get<GasTileOverlaySystem>();
|
||||
ZIndex = GasTileOverlaySystem.GasOverlayZIndex;
|
||||
_gasCount = _system.VisibleGasId.Length;
|
||||
_timer = new float[_gasCount];
|
||||
_frameDelays = new float[_gasCount][];
|
||||
_frameCounter = new int[_gasCount];
|
||||
_frames = new Texture[_gasCount][];
|
||||
|
||||
for (var i = 0; i < _gasCount; i++)
|
||||
{
|
||||
var gasPrototype = protoMan.Index<GasPrototype>(_system.VisibleGasId[i].ToString());
|
||||
|
||||
SpriteSpecifier overlay;
|
||||
|
||||
if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState))
|
||||
overlay = new SpriteSpecifier.Rsi(new ResourcePath(gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState);
|
||||
else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture))
|
||||
overlay = new SpriteSpecifier.Texture(new ResourcePath(gasPrototype.GasOverlayTexture));
|
||||
else
|
||||
continue;
|
||||
|
||||
switch (overlay)
|
||||
{
|
||||
case SpriteSpecifier.Rsi animated:
|
||||
var rsi = resourceCache.GetResource<RSIResource>(animated.RsiPath).RSI;
|
||||
var stateId = animated.RsiState;
|
||||
|
||||
if (!rsi.TryGetState(stateId, out var state)) continue;
|
||||
|
||||
_frames[i] = state.GetFrames(RSI.State.Direction.South);
|
||||
_frameDelays[i] = state.GetDelays();
|
||||
_frameCounter[i] = 0;
|
||||
break;
|
||||
case SpriteSpecifier.Texture texture:
|
||||
_frames[i] = new[] { spriteSys.Frame0(texture) };
|
||||
_frameDelays[i] = Array.Empty<float>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var fire = resourceCache.GetResource<RSIResource>(FireRsiPath).RSI;
|
||||
|
||||
for (var i = 0; i < FireStates; i++)
|
||||
{
|
||||
if (!fire.TryGetState((i + 1).ToString(), out var state))
|
||||
throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
|
||||
|
||||
_fireFrames[i] = state.GetFrames(RSI.State.Direction.South);
|
||||
_fireFrameDelays[i] = state.GetDelays();
|
||||
_fireFrameCounter[i] = 0;
|
||||
}
|
||||
}
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
for (var i = 0; i < _gasCount; i++)
|
||||
{
|
||||
var delays = _frameDelays[i];
|
||||
if (delays.Length == 0) continue;
|
||||
|
||||
var frameCount = _frameCounter[i];
|
||||
_timer[i] += args.DeltaSeconds;
|
||||
var time = delays[frameCount];
|
||||
|
||||
if (_timer[i] < time)
|
||||
continue;
|
||||
|
||||
_timer[i] -= time;
|
||||
_frameCounter[i] = (frameCount + 1) % _frames[i].Length;
|
||||
}
|
||||
|
||||
for (var i = 0; i < FireStates; i++)
|
||||
{
|
||||
var delays = _fireFrameDelays[i];
|
||||
if (delays.Length == 0) continue;
|
||||
|
||||
var frameCount = _fireFrameCounter[i];
|
||||
_fireTimer[i] += args.DeltaSeconds;
|
||||
var time = delays[frameCount];
|
||||
|
||||
if (_fireTimer[i] < time) continue;
|
||||
_fireTimer[i] -= time;
|
||||
_fireFrameCounter[i] = (frameCount + 1) % _fireFrames[i].Length;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var drawHandle = args.WorldHandle;
|
||||
|
||||
var mapId = args.Viewport.Eye!.Position.MapId;
|
||||
var worldBounds = args.WorldBounds;
|
||||
|
||||
drawHandle.UseShader(null);
|
||||
|
||||
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
|
||||
foreach (var mapGrid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
if (!_gasTileOverlaySystem.HasData(mapGrid.GridEntityId))
|
||||
if (!TileData.TryGetValue(mapGrid.GridEntityId, out var gridData))
|
||||
continue;
|
||||
|
||||
drawHandle.SetTransform(mapGrid.WorldMatrix);
|
||||
var floatBounds = mapGrid.InvWorldMatrix.TransformBox(in args.WorldBounds);
|
||||
var localBounds = new Box2i(
|
||||
(int) MathF.Floor(floatBounds.Left),
|
||||
(int) MathF.Floor(floatBounds.Bottom),
|
||||
(int) MathF.Ceiling(floatBounds.Right),
|
||||
(int) MathF.Ceiling(floatBounds.Top));
|
||||
|
||||
foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds))
|
||||
// Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are
|
||||
// ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls
|
||||
// by chunk, even though its currently slower.
|
||||
|
||||
drawHandle.UseShader(null);
|
||||
foreach (var chunk in gridData.Values)
|
||||
{
|
||||
var enumerator = _gasTileOverlaySystem.GetOverlays(mapGrid.GridEntityId, tile.GridIndices);
|
||||
while (enumerator.MoveNext(out var tuple))
|
||||
var enumerator = new GasChunkEnumerator(chunk);
|
||||
|
||||
while (enumerator.MoveNext(out var gas))
|
||||
{
|
||||
drawHandle.DrawTexture(tuple.Texture, new Vector2(tile.X, tile.Y), tuple.Color);
|
||||
if (gas.Value.Opacity == null)
|
||||
continue;
|
||||
|
||||
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
|
||||
if (!localBounds.Contains(tilePosition))
|
||||
continue;
|
||||
|
||||
for (var i = 0; i < _gasCount; i++)
|
||||
{
|
||||
var opacity = gas.Value.Opacity[i];
|
||||
if (opacity > 0)
|
||||
drawHandle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And again for fire, with the unshaded shader
|
||||
drawHandle.UseShader(_shader);
|
||||
foreach (var chunk in gridData.Values)
|
||||
{
|
||||
var enumerator = new GasChunkEnumerator(chunk);
|
||||
|
||||
while (enumerator.MoveNext(out var gas))
|
||||
{
|
||||
if (gas.Value.FireState == 0)
|
||||
continue;
|
||||
|
||||
var index = chunk.Origin + (enumerator.X, enumerator.Y);
|
||||
if (!localBounds.Contains(index))
|
||||
continue;
|
||||
|
||||
var state = gas.Value.FireState - 1;
|
||||
var texture = _fireFrames[state][_fireFrameCounter[state]];
|
||||
drawHandle.DrawTexture(texture, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawHandle.UseShader(null);
|
||||
drawHandle.SetTransform(Matrix3.Identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
using System.Threading;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.Lobby;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Maps;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Threading;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Content.Client.Audio
|
||||
|
|
|
|||
|
|
@ -46,14 +46,18 @@ namespace Content.Client.Buckle
|
|||
return;
|
||||
}
|
||||
|
||||
if (_buckled && buckle.DrawDepth.HasValue)
|
||||
// TODO when ECSing, make this a visualizer
|
||||
if (_buckled &&
|
||||
LastEntityBuckledTo != null &&
|
||||
EntMan.GetComponent<TransformComponent>(LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North &&
|
||||
EntMan.TryGetComponent<SpriteComponent>(LastEntityBuckledTo, out var buckledSprite))
|
||||
{
|
||||
_originalDrawDepth ??= ownerSprite.DrawDepth;
|
||||
ownerSprite.DrawDepth = buckle.DrawDepth.Value;
|
||||
ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_originalDrawDepth.HasValue && !buckle.DrawDepth.HasValue)
|
||||
if (_originalDrawDepth.HasValue && !_buckled)
|
||||
{
|
||||
ownerSprite.DrawDepth = _originalDrawDepth.Value;
|
||||
_originalDrawDepth = null;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
using Content.Client.Items.Components;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Clothing
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedItemComponent))]
|
||||
[ComponentReference(typeof(ItemComponent))]
|
||||
[NetworkedComponent()]
|
||||
public sealed class ClothingComponent : ItemComponent
|
||||
[ComponentReference(typeof(SharedClothingComponent))]
|
||||
public sealed class ClothingComponent : SharedClothingComponent
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("femaleMask")]
|
||||
public FemaleClothingMask FemaleMask { get; } = FemaleClothingMask.UniformFull;
|
||||
public FemaleClothingMask FemaleMask = FemaleClothingMask.UniformFull;
|
||||
|
||||
public string? InSlot;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using Content.Client.Inventory;
|
||||
using Content.Shared.CharacterAppearance;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Item;
|
||||
|
|
@ -16,7 +17,7 @@ using static Robust.Shared.GameObjects.SharedSpriteComponent;
|
|||
|
||||
namespace Content.Client.Clothing;
|
||||
|
||||
public sealed class ClothingSystem : EntitySystem
|
||||
public sealed class ClothingVisualsSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a shitty hotfix written by me (Paul) to save me from renaming all files.
|
||||
|
|
@ -52,13 +53,13 @@ public sealed class ClothingSystem : EntitySystem
|
|||
SubscribeLocalEvent<ClothingComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<ClothingComponent, GotUnequippedEvent>(OnGotUnequipped);
|
||||
|
||||
SubscribeLocalEvent<SharedItemComponent, GetEquipmentVisualsEvent>(OnGetVisuals);
|
||||
SubscribeLocalEvent<ClothingComponent, GetEquipmentVisualsEvent>(OnGetVisuals);
|
||||
|
||||
SubscribeLocalEvent<ClientInventoryComponent, VisualsChangedEvent>(OnVisualsChanged);
|
||||
SubscribeLocalEvent<SpriteComponent, DidUnequipEvent>(OnDidUnequip);
|
||||
}
|
||||
|
||||
private void OnGetVisuals(EntityUid uid, SharedItemComponent item, GetEquipmentVisualsEvent args)
|
||||
private void OnGetVisuals(EntityUid uid, ClothingComponent item, GetEquipmentVisualsEvent args)
|
||||
{
|
||||
if (!TryComp(args.Equipee, out ClientInventoryComponent? inventory))
|
||||
return;
|
||||
|
|
@ -99,15 +100,15 @@ public sealed class ClothingSystem : EntitySystem
|
|||
/// <remarks>
|
||||
/// Useful for lazily adding clothing sprites without modifying yaml. And for backwards compatibility.
|
||||
/// </remarks>
|
||||
private bool TryGetDefaultVisuals(EntityUid uid, SharedItemComponent item, string slot, string? speciesId,
|
||||
private bool TryGetDefaultVisuals(EntityUid uid, ClothingComponent clothing, string slot, string? speciesId,
|
||||
[NotNullWhen(true)] out List<PrototypeLayerData>? layers)
|
||||
{
|
||||
layers = null;
|
||||
|
||||
RSI? rsi = null;
|
||||
|
||||
if (item.RsiPath != null)
|
||||
rsi = _cache.GetResource<RSIResource>(TextureRoot / item.RsiPath).RSI;
|
||||
if (clothing.RsiPath != null)
|
||||
rsi = _cache.GetResource<RSIResource>(TextureRoot / clothing.RsiPath).RSI;
|
||||
else if (TryComp(uid, out SpriteComponent? sprite))
|
||||
rsi = sprite.BaseRSI;
|
||||
|
||||
|
|
@ -117,9 +118,9 @@ public sealed class ClothingSystem : EntitySystem
|
|||
var correctedSlot = slot;
|
||||
TemporarySlotMap.TryGetValue(correctedSlot, out correctedSlot);
|
||||
|
||||
var state = (item.EquippedPrefix == null)
|
||||
var state = (clothing.EquippedPrefix == null)
|
||||
? $"equipped-{correctedSlot}"
|
||||
: $"{item.EquippedPrefix}-equipped-{correctedSlot}";
|
||||
: $"{clothing.EquippedPrefix}-equipped-{correctedSlot}";
|
||||
|
||||
// species specific
|
||||
if (speciesId != null && rsi.TryGetState($"{state}-{speciesId}", out _))
|
||||
|
|
@ -35,7 +35,7 @@ namespace Content.Client.Communications.UI
|
|||
SizeFlagsStretchRatio = 1
|
||||
};
|
||||
AnnounceButton = new Button();
|
||||
AnnounceButton.Text = "Announce";
|
||||
AnnounceButton.Text = Loc.GetString("comms-console-menu-announcement-button");
|
||||
AnnounceButton.OnPressed += (_) => Owner.AnnounceButtonPressed(_messageInput.Text.Trim());
|
||||
AnnounceButton.Disabled = !owner.CanAnnounce;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
using System;
|
||||
using Content.Shared.Sound;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
|
||||
|
||||
namespace Content.Client.Disposal.Visualizers
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using JetBrains.Annotations;
|
|||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ namespace Content.Client.DoAfter.UI
|
|||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
|
||||
VerticalAlignment = VAlignment.Center;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
|
|
|
|||
45
Content.Client/DoAfter/UI/DoAfterControl.cs
Normal file
45
Content.Client/DoAfter/UI/DoAfterControl.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using Content.Client.Resources;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.DoAfter.UI;
|
||||
|
||||
public sealed class DoAfterControl : PanelContainer
|
||||
{
|
||||
public float Ratio
|
||||
{
|
||||
get => _bar.Ratio;
|
||||
set => _bar.Ratio = value;
|
||||
}
|
||||
|
||||
public bool Cancelled
|
||||
{
|
||||
get => _bar.Cancelled;
|
||||
set => _bar.Cancelled = value;
|
||||
}
|
||||
|
||||
private DoAfterBar _bar;
|
||||
|
||||
public DoAfterControl()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
var cache = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
AddChild(new TextureRect
|
||||
{
|
||||
Texture = cache.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"),
|
||||
TextureScale = Vector2.One * DoAfterBar.DoAfterBarScale,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
});
|
||||
|
||||
_bar = new DoAfterBar();
|
||||
AddChild(_bar);
|
||||
VerticalAlignment = VAlignment.Bottom;
|
||||
_bar.Measure(Vector2.Infinity);
|
||||
Measure(Vector2.Infinity);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.IoC;
|
||||
using Content.Client.Resources;
|
||||
using Content.Shared.DoAfter;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.DoAfter.UI
|
||||
{
|
||||
|
|
@ -21,14 +12,12 @@ namespace Content.Client.DoAfter.UI
|
|||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
private readonly Dictionary<byte, PanelContainer> _doAfterControls = new();
|
||||
private readonly Dictionary<byte, DoAfterBar> _doAfterBars = new();
|
||||
private readonly Dictionary<byte, DoAfterControl> _doAfterControls = new();
|
||||
|
||||
// We'll store cancellations for a little bit just so we can flash the graphic to indicate it's cancelled
|
||||
private readonly Dictionary<byte, TimeSpan> _cancelledDoAfters = new();
|
||||
|
||||
public EntityUid AttachedEntity { get; set; }
|
||||
private ScreenCoordinates _playerPosition;
|
||||
public EntityUid? AttachedEntity { get; set; }
|
||||
|
||||
public DoAfterGui()
|
||||
{
|
||||
|
|
@ -37,8 +26,6 @@ namespace Content.Client.DoAfter.UI
|
|||
IoCManager.InjectDependencies(this);
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(this);
|
||||
SeparationOverride = 0;
|
||||
|
||||
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
|
@ -53,43 +40,21 @@ namespace Content.Client.DoAfter.UI
|
|||
}
|
||||
|
||||
_doAfterControls.Clear();
|
||||
_doAfterBars.Clear();
|
||||
_cancelledDoAfters.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the necessary control for a DoAfter progress bar.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public void AddDoAfter(ClientDoAfter message)
|
||||
{
|
||||
if (_doAfterControls.ContainsKey(message.ID))
|
||||
return;
|
||||
|
||||
var doAfterBar = new DoAfterBar
|
||||
{
|
||||
VerticalAlignment = VAlignment.Center
|
||||
};
|
||||
|
||||
_doAfterBars[message.ID] = doAfterBar;
|
||||
|
||||
var control = new PanelContainer
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextureRect
|
||||
{
|
||||
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"),
|
||||
TextureScale = Vector2.One * DoAfterBar.DoAfterBarScale,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
},
|
||||
|
||||
doAfterBar
|
||||
}
|
||||
};
|
||||
|
||||
AddChild(control);
|
||||
_doAfterControls.Add(message.ID, control);
|
||||
var doAfterBar = new DoAfterControl();
|
||||
AddChild(doAfterBar);
|
||||
_doAfterControls.Add(message.ID, doAfterBar);
|
||||
Measure(Vector2.Infinity);
|
||||
}
|
||||
|
||||
// NOTE THAT THE BELOW ONLY HANDLES THE UI SIDE
|
||||
|
|
@ -107,8 +72,6 @@ namespace Content.Client.DoAfter.UI
|
|||
RemoveChild(control);
|
||||
control.DisposeAllChildren();
|
||||
_doAfterControls.Remove(id);
|
||||
_doAfterBars.Remove(id);
|
||||
|
||||
_cancelledDoAfters.Remove(id);
|
||||
}
|
||||
|
||||
|
|
@ -122,23 +85,13 @@ namespace Content.Client.DoAfter.UI
|
|||
if (_cancelledDoAfters.ContainsKey(id))
|
||||
return;
|
||||
|
||||
DoAfterBar doAfterBar;
|
||||
|
||||
if (!_doAfterControls.TryGetValue(id, out var doAfterControl))
|
||||
{
|
||||
doAfterControl = new PanelContainer();
|
||||
doAfterControl = new DoAfterControl();
|
||||
AddChild(doAfterControl);
|
||||
DebugTools.Assert(!_doAfterBars.ContainsKey(id));
|
||||
doAfterBar = new DoAfterBar();
|
||||
doAfterControl.AddChild(doAfterBar);
|
||||
_doAfterBars[id] = doAfterBar;
|
||||
}
|
||||
else
|
||||
{
|
||||
doAfterBar = _doAfterBars[id];
|
||||
}
|
||||
|
||||
doAfterBar.Cancelled = true;
|
||||
doAfterControl.Cancelled = true;
|
||||
_cancelledDoAfters.Add(id, _gameTiming.CurTime);
|
||||
}
|
||||
|
||||
|
|
@ -146,32 +99,29 @@ namespace Content.Client.DoAfter.UI
|
|||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!AttachedEntity.IsValid() ||
|
||||
!_entityManager.TryGetComponent(AttachedEntity, out DoAfterComponent? doAfterComponent))
|
||||
if (!_entityManager.TryGetComponent(AttachedEntity, out DoAfterComponent? doAfterComponent))
|
||||
{
|
||||
Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var doAfters = doAfterComponent.DoAfters;
|
||||
|
||||
if (doAfters.Count == 0)
|
||||
{
|
||||
Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(AttachedEntity);
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(AttachedEntity.Value);
|
||||
|
||||
if (_eyeManager.CurrentMap != transform.MapID || !transform.Coordinates.IsValid(_entityManager))
|
||||
{
|
||||
Visible = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Visible = true;
|
||||
}
|
||||
|
||||
Visible = true;
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
var toRemove = new List<byte>();
|
||||
|
||||
|
|
@ -195,15 +145,16 @@ namespace Content.Client.DoAfter.UI
|
|||
if (_cancelledDoAfters.ContainsKey(id) || !_doAfterControls.ContainsKey(id))
|
||||
continue;
|
||||
|
||||
var doAfterBar = _doAfterBars[id];
|
||||
var control = _doAfterControls[id];
|
||||
var ratio = (currentTime - message.StartTime).TotalSeconds;
|
||||
doAfterBar.Ratio = MathF.Min(1.0f,
|
||||
control.Ratio = MathF.Min(1.0f,
|
||||
(float) ratio / message.Delay);
|
||||
|
||||
// Just in case it doesn't get cleaned up by the system for whatever reason.
|
||||
if (ratio > message.Delay + DoAfterSystem.ExcessTime)
|
||||
{
|
||||
toRemove.Add(id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,9 +163,14 @@ namespace Content.Client.DoAfter.UI
|
|||
RemoveDoAfter(id);
|
||||
}
|
||||
|
||||
var screenCoordinates = _eyeManager.CoordinatesToScreen(transform.Coordinates);
|
||||
_playerPosition = new ScreenCoordinates(screenCoordinates.Position / UIScale, screenCoordinates.Window);
|
||||
LayoutContainer.SetPosition(this, new Vector2(_playerPosition.X - Width / 2, _playerPosition.Y - Height - 30.0f));
|
||||
UpdatePosition(transform);
|
||||
}
|
||||
|
||||
public void UpdatePosition(TransformComponent xform)
|
||||
{
|
||||
var screenCoordinates = _eyeManager.CoordinatesToScreen(xform.Coordinates);
|
||||
var position = screenCoordinates.Position / UIScale - DesiredSize / 2f;
|
||||
LayoutContainer.SetPosition(this, position - new Vector2(0, 40f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ namespace Content.Client.Entry
|
|||
// Do not add to these, they are legacy.
|
||||
factory.RegisterClass<SharedLatheComponent>();
|
||||
factory.RegisterClass<SharedSpawnPointComponent>();
|
||||
factory.RegisterClass<SharedVendingMachineComponent>();
|
||||
factory.RegisterClass<SharedReagentDispenserComponent>();
|
||||
factory.RegisterClass<SharedChemMasterComponent>();
|
||||
factory.RegisterClass<SharedGravityGeneratorComponent>();
|
||||
|
|
@ -183,10 +182,10 @@ namespace Content.Client.Entry
|
|||
ContentContexts.SetupContexts(inputMan.Contexts);
|
||||
|
||||
IoCManager.Resolve<IGameHud>().Initialize();
|
||||
IoCManager.Resolve<IParallaxManager>().LoadParallax(); // Have to do this later because prototypes are needed.
|
||||
IoCManager.Resolve<IParallaxManager>().LoadDefaultParallax(); // Have to do this later because prototypes are needed.
|
||||
|
||||
var overlayMgr = IoCManager.Resolve<IOverlayManager>();
|
||||
overlayMgr.AddOverlay(new ParallaxOverlay());
|
||||
|
||||
overlayMgr.AddOverlay(new SingularityOverlay());
|
||||
overlayMgr.AddOverlay(new FlashOverlay());
|
||||
overlayMgr.AddOverlay(new RadiationPulseOverlay());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.IdentityManagement;
|
||||
|
|
@ -14,6 +12,8 @@ using Robust.Client.UserInterface.Controls;
|
|||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using static Content.Shared.Interaction.SharedInteractionSystem;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
using Content.Shared.Extinguisher;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Client.Extinguisher
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class FireExtinguisherVisualizer : AppearanceVisualizer
|
||||
{
|
||||
[DataField("safety_on_state")]
|
||||
private string? _safetyOnState;
|
||||
[DataField("safety_off_state")]
|
||||
private string? _safetyOffState;
|
||||
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
|
||||
if (component.TryGetData<bool>(FireExtinguisherVisuals.Safety, out var safety))
|
||||
{
|
||||
SetSafety(component, safety);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSafety(AppearanceComponent component, bool safety)
|
||||
{
|
||||
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
|
||||
|
||||
sprite.LayerSetState(FireExtinguisherVisualLayers.Base, safety ? _safetyOnState : _safetyOffState);
|
||||
}
|
||||
}
|
||||
|
||||
public enum FireExtinguisherVisualLayers : byte
|
||||
{
|
||||
Base
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ using Content.Shared.Gravity;
|
|||
|
||||
namespace Content.Client.Gravity
|
||||
{
|
||||
internal sealed class GravitySystem : SharedGravitySystem
|
||||
public sealed class GravitySystem : SharedGravitySystem
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,14 @@ namespace Content.Client.Hands
|
|||
}
|
||||
#endregion
|
||||
|
||||
public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, SharedHandsComponent? hands = null)
|
||||
{
|
||||
base.DoDrop(uid, hand, doDropInteraction, hands);
|
||||
|
||||
if (TryComp(hand.HeldEntity, out SpriteComponent? sprite))
|
||||
sprite.RenderOrder = EntityManager.CurrentTick.Value;
|
||||
}
|
||||
|
||||
public EntityUid? GetActiveHandEntity()
|
||||
{
|
||||
return TryGetPlayerHands(out var hands) ? hands.ActiveHandEntity : null;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
using System.Text;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Disease.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Text;
|
||||
using static Content.Shared.MedicalScanner.SharedHealthAnalyzerComponent;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.IdentityManagement;
|
||||
|
||||
namespace Content.Client.IdentityManagement;
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ namespace Content.Client.Inventory
|
|||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
[Dependency] private readonly ClothingSystem _clothingSystem = default!;
|
||||
[Dependency] private readonly ClothingVisualsSystem _clothingVisualsSystem = default!;
|
||||
|
||||
public const int ButtonSize = 64;
|
||||
private const int ButtonSeparation = 4;
|
||||
|
|
@ -165,7 +165,7 @@ namespace Content.Client.Inventory
|
|||
|
||||
private void OnInit(EntityUid uid, ClientInventoryComponent component, ComponentInit args)
|
||||
{
|
||||
_clothingSystem.InitClothing(uid, component);
|
||||
_clothingVisualsSystem.InitClothing(uid, component);
|
||||
|
||||
if (!TryGetUIElements(uid, out var window, out var bottomLeft, out var bottomRight, out var topQuick,
|
||||
component))
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Client.Strip;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Strip.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.Inventory
|
||||
{
|
||||
|
|
@ -33,7 +28,7 @@ namespace Content.Client.Inventory
|
|||
_strippingMenu = new StrippingMenu($"{Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner.Owner, entMan)))}");
|
||||
|
||||
_strippingMenu.OnClose += Close;
|
||||
_strippingMenu.OpenCentered();
|
||||
_strippingMenu.OpenCenteredLeft();
|
||||
UpdateMenu();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
using Content.Shared.Item;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Client.Items.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedItemComponent))]
|
||||
[Virtual]
|
||||
public class ItemComponent : SharedItemComponent { }
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Item;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Containers;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using static Robust.Shared.GameObjects.SharedSpriteComponent;
|
||||
|
||||
namespace Content.Client.Items.Systems;
|
||||
|
|
@ -19,18 +19,16 @@ public sealed class ItemSystem : SharedItemSystem
|
|||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SharedItemComponent, GetInhandVisualsEvent>(OnGetVisuals);
|
||||
SubscribeLocalEvent<ItemComponent, GetInhandVisualsEvent>(OnGetVisuals);
|
||||
}
|
||||
|
||||
#region InhandVisuals
|
||||
|
||||
/// <summary>
|
||||
/// When an items visual state changes, notify and entities that are holding this item that their sprite may need updating.
|
||||
/// </summary>
|
||||
public override void VisualsChanged(EntityUid uid, SharedItemComponent? item = null)
|
||||
public override void VisualsChanged(EntityUid uid)
|
||||
{
|
||||
if (!Resolve(uid, ref item, false))
|
||||
return;
|
||||
|
||||
// if the item is in a container, it might be equipped to hands or inventory slots --> update visuals.
|
||||
if (_containerSystem.TryGetContainingContainer(uid, out var container))
|
||||
RaiseLocalEvent(container.Owner, new VisualsChangedEvent(uid, container.ID), true);
|
||||
|
|
@ -39,12 +37,12 @@ public sealed class ItemSystem : SharedItemSystem
|
|||
/// <summary>
|
||||
/// An entity holding this item is requesting visual information for in-hand sprites.
|
||||
/// </summary>
|
||||
private void OnGetVisuals(EntityUid uid, SharedItemComponent item, GetInhandVisualsEvent args)
|
||||
private void OnGetVisuals(EntityUid uid, ItemComponent item, GetInhandVisualsEvent args)
|
||||
{
|
||||
var defaultKey = $"inhand-{args.Location.ToString().ToLowerInvariant()}";
|
||||
|
||||
// try get explicit visuals
|
||||
if (item.InhandVisuals == null || !item.InhandVisuals.TryGetValue(args.Location, out var layers))
|
||||
if (!item.InhandVisuals.TryGetValue(args.Location, out var layers))
|
||||
{
|
||||
// get defaults
|
||||
if (!TryGetDefaultVisuals(uid, item, defaultKey, out layers))
|
||||
|
|
@ -71,7 +69,7 @@ public sealed class ItemSystem : SharedItemSystem
|
|||
/// <remarks>
|
||||
/// Useful for lazily adding in-hand sprites without modifying yaml. And backwards compatibility.
|
||||
/// </remarks>
|
||||
private bool TryGetDefaultVisuals(EntityUid uid, SharedItemComponent item, string defaultKey, [NotNullWhen(true)] out List<PrototypeLayerData>? result)
|
||||
private bool TryGetDefaultVisuals(EntityUid uid, ItemComponent item, string defaultKey, [NotNullWhen(true)] out List<PrototypeLayerData>? result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
|
|
@ -85,9 +83,9 @@ public sealed class ItemSystem : SharedItemSystem
|
|||
if (rsi == null || rsi.Path == null)
|
||||
return false;
|
||||
|
||||
var state = (item.EquippedPrefix == null)
|
||||
var state = (item.HeldPrefix == null)
|
||||
? defaultKey
|
||||
: $"{item.EquippedPrefix}-{defaultKey}";
|
||||
: $"{item.HeldPrefix}-{defaultKey}";
|
||||
|
||||
if (!rsi.TryGetState(state, out var _))
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.Items.Components;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
|
|
@ -8,12 +6,8 @@ using Content.Shared.IdentityManagement;
|
|||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using static Content.Client.IoC.StaticIoC;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
using Content.Shared.Kitchen.Components;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Client.Kitchen.Components
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Content.Client.Items.Components;
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.Light.Component;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
|
|
@ -6,103 +6,79 @@ using Robust.Client.UserInterface.Controls;
|
|||
using Robust.Shared.Timing;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Light.Components
|
||||
namespace Content.Client.Light.Components;
|
||||
|
||||
public sealed class HandheldLightStatus : Control
|
||||
{
|
||||
[RegisterComponent]
|
||||
[Access(typeof(HandheldLightSystem))]
|
||||
public sealed class HandheldLightComponent : SharedHandheldLightComponent, IItemStatus
|
||||
private const float TimerCycle = 1;
|
||||
|
||||
private readonly HandheldLightComponent _parent;
|
||||
private readonly PanelContainer[] _sections = new PanelContainer[HandheldLightComponent.StatusLevels - 1];
|
||||
|
||||
private float _timer;
|
||||
|
||||
private static readonly StyleBoxFlat StyleBoxLit = new()
|
||||
{
|
||||
public byte? Level;
|
||||
public bool Activated;
|
||||
BackgroundColor = Color.LimeGreen
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically set item-prefixes when toggling the flashlight.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Flashlights should probably be using explicit unshaded sprite, in-hand and clothing layers, this is
|
||||
/// mostly here for backwards compatibility.
|
||||
/// </remarks>
|
||||
[DataField("addPrefix")]
|
||||
public bool AddPrefix = false;
|
||||
private static readonly StyleBoxFlat StyleBoxUnlit = new()
|
||||
{
|
||||
BackgroundColor = Color.Black
|
||||
};
|
||||
|
||||
public Control MakeControl()
|
||||
public HandheldLightStatus(HandheldLightComponent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
var wrapper = new BoxContainer
|
||||
{
|
||||
return new StatusControl(this);
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 4,
|
||||
HorizontalAlignment = HAlignment.Center
|
||||
};
|
||||
|
||||
AddChild(wrapper);
|
||||
|
||||
for (var i = 0; i < _sections.Length; i++)
|
||||
{
|
||||
var panel = new PanelContainer {MinSize = (20, 20)};
|
||||
wrapper.AddChild(panel);
|
||||
_sections[i] = panel;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StatusControl : Control
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
_timer += args.DeltaSeconds;
|
||||
_timer %= TimerCycle;
|
||||
|
||||
var level = _parent.Level;
|
||||
|
||||
for (var i = 0; i < _sections.Length; i++)
|
||||
{
|
||||
private const float TimerCycle = 1;
|
||||
|
||||
private readonly HandheldLightComponent _parent;
|
||||
private readonly PanelContainer[] _sections = new PanelContainer[StatusLevels - 1];
|
||||
|
||||
private float _timer;
|
||||
|
||||
private static readonly StyleBoxFlat StyleBoxLit = new()
|
||||
if (i == 0)
|
||||
{
|
||||
BackgroundColor = Color.LimeGreen
|
||||
};
|
||||
|
||||
private static readonly StyleBoxFlat StyleBoxUnlit = new()
|
||||
{
|
||||
BackgroundColor = Color.Black
|
||||
};
|
||||
|
||||
public StatusControl(HandheldLightComponent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
var wrapper = new BoxContainer
|
||||
if (level == 0 || level == null)
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 4,
|
||||
HorizontalAlignment = HAlignment.Center
|
||||
};
|
||||
|
||||
AddChild(wrapper);
|
||||
|
||||
for (var i = 0; i < _sections.Length; i++)
|
||||
{
|
||||
var panel = new PanelContainer {MinSize = (20, 20)};
|
||||
wrapper.AddChild(panel);
|
||||
_sections[i] = panel;
|
||||
_sections[0].PanelOverride = StyleBoxUnlit;
|
||||
}
|
||||
else if (level == 1)
|
||||
{
|
||||
// Flash the last light.
|
||||
_sections[0].PanelOverride = _timer > TimerCycle / 2 ? StyleBoxLit : StyleBoxUnlit;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sections[0].PanelOverride = StyleBoxLit;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
_timer += args.DeltaSeconds;
|
||||
_timer %= TimerCycle;
|
||||
|
||||
var level = _parent.Level;
|
||||
|
||||
for (var i = 0; i < _sections.Length; i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
if (level == 0 || level == null)
|
||||
{
|
||||
_sections[0].PanelOverride = StyleBoxUnlit;
|
||||
}
|
||||
else if (level == 1)
|
||||
{
|
||||
// Flash the last light.
|
||||
_sections[0].PanelOverride = _timer > TimerCycle / 2 ? StyleBoxLit : StyleBoxUnlit;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sections[0].PanelOverride = StyleBoxLit;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
_sections[i].PanelOverride = level >= i + 2 ? StyleBoxLit : StyleBoxUnlit;
|
||||
}
|
||||
}
|
||||
_sections[i].PanelOverride = level >= i + 2 ? StyleBoxLit : StyleBoxUnlit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,21 @@
|
|||
using Content.Client.Items.Systems;
|
||||
using Content.Client.Items;
|
||||
using Content.Client.Light.Components;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.Light.Component;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Light;
|
||||
|
||||
public sealed class HandheldLightSystem : EntitySystem
|
||||
public sealed class HandheldLightSystem : SharedHandheldLightSystem
|
||||
{
|
||||
[Dependency] private readonly ItemSystem _itemSys = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<HandheldLightComponent, ComponentHandleState>(OnHandleState);
|
||||
|
||||
SubscribeLocalEvent<HandheldLightComponent, ItemStatusCollectMessage>(OnGetStatusControl);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, HandheldLightComponent component, ref ComponentHandleState args)
|
||||
|
||||
private static void OnGetStatusControl(EntityUid uid, HandheldLightComponent component, ItemStatusCollectMessage args)
|
||||
{
|
||||
if (args.Current is not SharedHandheldLightComponent.HandheldLightComponentState state)
|
||||
return;
|
||||
|
||||
component.Level = state.Charge;
|
||||
|
||||
if (state.Activated == component.Activated)
|
||||
return;
|
||||
|
||||
component.Activated = state.Activated;
|
||||
|
||||
// really hand-held lights should be using a separate unshaded layer. (see FlashlightVisualizer)
|
||||
// this prefix stuff is largely for backwards compatibility with RSIs/yamls that have not been updated.
|
||||
if (component.AddPrefix && TryComp(uid, out SharedItemComponent? item))
|
||||
{
|
||||
item.EquippedPrefix = state.Activated ? "on" : "off";
|
||||
_itemSys.VisualsChanged(uid);
|
||||
}
|
||||
args.Controls.Add(new HandheldLightStatus(component));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.Light.Component;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Animations;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
using System;
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.Sound;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Client.Light.Visualizers
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io" Title="Port Selector" MinSize="400 200">
|
||||
<DefaultWindow xmlns="https://spacestation14.io" Title="{Loc signal-port-selector-menu-title}" MinSize="400 200">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.3">
|
||||
|
|
@ -12,8 +12,8 @@
|
|||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Button Name="ButtonClear" Text="Clear"/>
|
||||
<Button Name="ButtonLinkDefault" Text="Link Defaults"/>
|
||||
<Button Name="ButtonClear" Text="{Loc signal-port-selector-menu-clear}"/>
|
||||
<Button Name="ButtonLinkDefault" Text="{Loc signal-port-selector-menu-link-defaults}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Client.Clothing;
|
||||
using Content.Shared.Clothing.EntitySystems;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
|
|
@ -14,6 +15,7 @@ public sealed class JetpackSystem : SharedJetpackSystem
|
|||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly ClothingSystem _clothing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -35,7 +37,7 @@ public sealed class JetpackSystem : SharedJetpackSystem
|
|||
args.Sprite?.LayerSetState(0, state);
|
||||
|
||||
if (TryComp<ClothingComponent>(uid, out var clothing))
|
||||
clothing.EquippedPrefix = enabled ? "on" : null;
|
||||
_clothing.SetEquippedPrefix(uid, enabled ? "on" : null, clothing);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Content.Client.PDA
|
|||
base.Open();
|
||||
SendMessage(new PDARequestUpdateInterfaceMessage());
|
||||
_menu = new PDAMenu();
|
||||
_menu.OpenToLeft();
|
||||
_menu.OpenCenteredLeft();
|
||||
_menu.OnClose += Close;
|
||||
_menu.FlashLightToggleButton.OnToggled += _ =>
|
||||
{
|
||||
|
|
@ -79,14 +79,16 @@ namespace Content.Client.PDA
|
|||
if (msg.PDAOwnerInfo.IdOwner != null || msg.PDAOwnerInfo.JobTitle != null)
|
||||
{
|
||||
_menu.IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui",
|
||||
("Owner",msg.PDAOwnerInfo.IdOwner ?? "Unknown"),
|
||||
("JobTitle",msg.PDAOwnerInfo.JobTitle ?? "Unassigned")));
|
||||
("Owner",msg.PDAOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown")),
|
||||
("JobTitle",msg.PDAOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned"))));
|
||||
}
|
||||
else
|
||||
{
|
||||
_menu.IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui-blank"));
|
||||
}
|
||||
|
||||
_menu.StationNameLabel.SetMarkup(Loc.GetString("comp-pda-ui-station", ("Station",msg.StationName ?? Loc.GetString("comp-pda-ui-unknown"))));
|
||||
|
||||
_menu.EjectIdButton.Visible = msg.PDAOwnerInfo.IdOwner != null || msg.PDAOwnerInfo.JobTitle != null;
|
||||
_menu.EjectPenButton.Visible = msg.HasPen;
|
||||
_menu.ActivateUplinkButton.Visible = msg.HasUplink;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@
|
|||
HorizontalExpand="True"
|
||||
MinSize="50 50">
|
||||
<RichTextLabel Name="PdaOwnerLabel" Access="Public" />
|
||||
<RichTextLabel Name="IdInfoLabel"
|
||||
Access="Public"
|
||||
HorizontalExpand="True" />
|
||||
<PanelContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="IdInfoLabel"
|
||||
<RichTextLabel Name="StationNameLabel"
|
||||
Access="Public"
|
||||
HorizontalExpand="True" />
|
||||
<Button Name="EjectIdButton"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client.Parallax.Data
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.IoC;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Parallax.Data;
|
||||
|
|
|
|||
|
|
@ -62,5 +62,12 @@ public sealed class ParallaxLayerConfig
|
|||
/// </summary>
|
||||
[DataField("slowness")]
|
||||
public float Slowness { get; set; } = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Should the parallax scroll at a specific rate per second.
|
||||
/// </summary>
|
||||
[DataField("scrolling")] public Vector2 Scrolling = Vector2.Zero;
|
||||
|
||||
[DataField("shader")] public string? Shader = "unshaded";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +1,30 @@
|
|||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Content.Client.Parallax;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.Parallax.Managers;
|
||||
|
||||
public interface IParallaxManager
|
||||
{
|
||||
/// <summary>
|
||||
/// The current parallax.
|
||||
/// Changing this causes a new parallax to be loaded (eventually).
|
||||
/// Do not alter until prototype manager is available.
|
||||
/// Useful "csi" input for testing new parallaxes:
|
||||
/// using Content.Client.Parallax.Managers; IoCManager.Resolve<IParallaxManager>().ParallaxName = "test";
|
||||
/// </summary>
|
||||
string ParallaxName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All WorldHomePosition values are offset by this.
|
||||
/// </summary>
|
||||
Vector2 ParallaxAnchor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The layers of the currently loaded parallax.
|
||||
/// This will change on a whim without notification.
|
||||
/// </summary>
|
||||
ParallaxLayerPrepared[] ParallaxLayers { get; }
|
||||
bool IsLoaded(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Used to initialize the manager.
|
||||
/// The layers of the selected parallax.
|
||||
/// </summary>
|
||||
ParallaxLayerPrepared[] GetParallaxLayers(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Loads in the default parallax to use.
|
||||
/// Do not call until prototype manager is available.
|
||||
/// </summary>
|
||||
void LoadParallax();
|
||||
void LoadDefaultParallax();
|
||||
|
||||
Task LoadParallaxByName(string name);
|
||||
|
||||
void UnloadParallax(string name);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,111 +1,103 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Client.Parallax.Data;
|
||||
using Content.Shared;
|
||||
using Content.Shared.CCVar;
|
||||
using Nett;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Content.Client.Parallax.Managers;
|
||||
|
||||
internal sealed class ParallaxManager : IParallaxManager
|
||||
public sealed class ParallaxManager : IParallaxManager
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
private string _parallaxName = "";
|
||||
public string ParallaxName
|
||||
{
|
||||
get => _parallaxName;
|
||||
set
|
||||
{
|
||||
LoadParallaxByName(value);
|
||||
}
|
||||
}
|
||||
private ISawmill _sawmill = Logger.GetSawmill("parallax");
|
||||
|
||||
public Vector2 ParallaxAnchor { get; set; }
|
||||
|
||||
private CancellationTokenSource? _presentParallaxLoadCancel;
|
||||
private readonly ConcurrentDictionary<string, ParallaxLayerPrepared[]> _parallaxesLQ = new();
|
||||
private readonly ConcurrentDictionary<string, ParallaxLayerPrepared[]> _parallaxesHQ = new();
|
||||
|
||||
private ParallaxLayerPrepared[] _parallaxLayersHQ = {};
|
||||
private ParallaxLayerPrepared[] _parallaxLayersLQ = {};
|
||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> _loadingParallaxes = new();
|
||||
|
||||
public ParallaxLayerPrepared[] ParallaxLayers => _configurationManager.GetCVar(CCVars.ParallaxLowQuality) ? _parallaxLayersLQ : _parallaxLayersHQ;
|
||||
public bool IsLoaded(string name) => _parallaxesLQ.ContainsKey(name);
|
||||
|
||||
public async void LoadParallax()
|
||||
public ParallaxLayerPrepared[] GetParallaxLayers(string name)
|
||||
{
|
||||
await LoadParallaxByName("default");
|
||||
if (_configurationManager.GetCVar(CCVars.ParallaxLowQuality))
|
||||
{
|
||||
return !_parallaxesLQ.TryGetValue(name, out var lq) ? Array.Empty<ParallaxLayerPrepared>() : lq;
|
||||
}
|
||||
|
||||
return !_parallaxesLQ.TryGetValue(name, out var hq) ? Array.Empty<ParallaxLayerPrepared>() : hq;
|
||||
}
|
||||
|
||||
private async Task LoadParallaxByName(string name)
|
||||
public void UnloadParallax(string name)
|
||||
{
|
||||
// Update _parallaxName
|
||||
if (_parallaxName == name)
|
||||
if (_loadingParallaxes.TryGetValue(name, out var loading))
|
||||
{
|
||||
loading.Cancel();
|
||||
_loadingParallaxes.Remove(name, out _);
|
||||
return;
|
||||
}
|
||||
_parallaxName = name;
|
||||
|
||||
if (!_parallaxesLQ.ContainsKey(name)) return;
|
||||
_parallaxesLQ.Remove(name, out _);
|
||||
_parallaxesHQ.Remove(name, out _);
|
||||
}
|
||||
|
||||
public async void LoadDefaultParallax()
|
||||
{
|
||||
await LoadParallaxByName("Default");
|
||||
}
|
||||
|
||||
public async Task LoadParallaxByName(string name)
|
||||
{
|
||||
if (_parallaxesLQ.ContainsKey(name) || _loadingParallaxes.ContainsKey(name)) return;
|
||||
|
||||
// Cancel any existing load and setup the new cancellation token
|
||||
_presentParallaxLoadCancel?.Cancel();
|
||||
_presentParallaxLoadCancel = new CancellationTokenSource();
|
||||
var cancel = _presentParallaxLoadCancel.Token;
|
||||
|
||||
// Empty parallax name = no layers (this is so that the initial "" parallax name is consistent)
|
||||
if (_parallaxName == "")
|
||||
{
|
||||
_parallaxLayersHQ = _parallaxLayersLQ = new ParallaxLayerPrepared[] {};
|
||||
return;
|
||||
}
|
||||
var token = new CancellationTokenSource();
|
||||
_loadingParallaxes[name] = token;
|
||||
var cancel = token.Token;
|
||||
|
||||
// Begin (for real)
|
||||
Logger.InfoS("parallax", $"Loading parallax {name}");
|
||||
_sawmill.Info($"Loading parallax {name}");
|
||||
|
||||
try
|
||||
{
|
||||
var parallaxPrototype = _prototypeManager.Index<ParallaxPrototype>(name);
|
||||
|
||||
ParallaxLayerPrepared[] hq;
|
||||
ParallaxLayerPrepared[] lq;
|
||||
ParallaxLayerPrepared[][] layers;
|
||||
|
||||
if (parallaxPrototype.LayersLQUseHQ)
|
||||
{
|
||||
lq = hq = await LoadParallaxLayers(parallaxPrototype.Layers, cancel);
|
||||
layers = new ParallaxLayerPrepared[2][];
|
||||
layers[0] = layers[1] = await LoadParallaxLayers(parallaxPrototype.Layers, cancel);
|
||||
}
|
||||
else
|
||||
{
|
||||
var results = await Task.WhenAll(
|
||||
layers = await Task.WhenAll(
|
||||
LoadParallaxLayers(parallaxPrototype.Layers, cancel),
|
||||
LoadParallaxLayers(parallaxPrototype.LayersLQ, cancel)
|
||||
);
|
||||
hq = results[0];
|
||||
lq = results[1];
|
||||
}
|
||||
|
||||
// Still keeping this check just in case.
|
||||
if (_parallaxName == name)
|
||||
{
|
||||
_parallaxLayersHQ = hq;
|
||||
_parallaxLayersLQ = lq;
|
||||
Logger.InfoS("parallax", $"Loaded parallax {name}");
|
||||
}
|
||||
_loadingParallaxes.Remove(name, out _);
|
||||
|
||||
if (token.Token.IsCancellationRequested) return;
|
||||
|
||||
_parallaxesLQ[name] = layers[0];
|
||||
_parallaxesHQ[name] = layers[1];
|
||||
|
||||
_sawmill.Info($"Loaded parallax {name}");
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorS("parallax", $"Failed to loaded parallax {name}: {ex}");
|
||||
_sawmill.Error($"Failed to loaded parallax {name}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
using Content.Client.Parallax.Managers;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.Parallax;
|
||||
|
|
@ -13,10 +12,11 @@ namespace Content.Client.Parallax;
|
|||
/// </summary>
|
||||
public sealed class ParallaxControl : Control
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IParallaxManager _parallaxManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)] public Vector2i Offset { get; set; }
|
||||
[ViewVariables(VVAccess.ReadWrite)] public Vector2 Offset { get; set; }
|
||||
|
||||
public ParallaxControl()
|
||||
{
|
||||
|
|
@ -24,20 +24,24 @@ public sealed class ParallaxControl : Control
|
|||
|
||||
Offset = (_random.Next(0, 1000), _random.Next(0, 1000));
|
||||
RectClipContent = true;
|
||||
_parallaxManager.LoadParallaxByName("FastSpace");
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
foreach (var layer in _parallaxManager.ParallaxLayers)
|
||||
foreach (var layer in _parallaxManager.GetParallaxLayers("FastSpace"))
|
||||
{
|
||||
var tex = layer.Texture;
|
||||
var texSize = tex.Size * layer.Config.Scale.Floored();
|
||||
var texSize = (tex.Size.X * (int) Size.X, tex.Size.Y * (int) Size.X) * layer.Config.Scale.Floored() / 540;
|
||||
var ourSize = PixelSize;
|
||||
|
||||
var currentTime = (float) _timing.RealTime.TotalSeconds;
|
||||
var offset = Offset + new Vector2(currentTime * 100f, currentTime * 0f);
|
||||
|
||||
if (layer.Config.Tiled)
|
||||
{
|
||||
// Multiply offset by slowness to match normal parallax
|
||||
var scaledOffset = (Offset * layer.Config.Slowness).Floored();
|
||||
var scaledOffset = (offset * layer.Config.Slowness).Floored();
|
||||
|
||||
// Then modulo the scaled offset by the size to prevent drawing a bunch of offscreen tiles for really small images.
|
||||
scaledOffset.X %= texSize.X;
|
||||
|
|
|
|||
|
|
@ -1,47 +1,55 @@
|
|||
using System;
|
||||
using Content.Client.Parallax.Managers;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Parallax;
|
||||
|
||||
public sealed class ParallaxOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IParallaxManager _parallaxManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly IParallaxManager _manager = default!;
|
||||
private readonly ParallaxSystem _parallax;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld;
|
||||
private readonly ShaderInstance _shader;
|
||||
|
||||
public ParallaxOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_shader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
|
||||
_parallax = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ParallaxSystem>();
|
||||
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (args.Viewport.Eye == null)
|
||||
{
|
||||
if (args.MapId == MapId.Nullspace)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_configurationManager.GetCVar(CCVars.ParallaxEnabled))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero;
|
||||
var screenHandle = args.WorldHandle;
|
||||
screenHandle.UseShader(_shader);
|
||||
|
||||
foreach (var layer in _parallaxManager.ParallaxLayers)
|
||||
var layers = _parallax.GetParallaxLayers(args.MapId);
|
||||
var realTime = (float) _timing.RealTime.TotalSeconds;
|
||||
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
ShaderInstance? shader;
|
||||
|
||||
if (!string.IsNullOrEmpty(layer.Config.Shader))
|
||||
shader = _prototypeManager.Index<ShaderPrototype>(layer.Config.Shader).Instance();
|
||||
else
|
||||
shader = null;
|
||||
|
||||
screenHandle.UseShader(shader);
|
||||
var tex = layer.Texture;
|
||||
|
||||
// Size of the texture in world units.
|
||||
|
|
@ -52,10 +60,11 @@ public sealed class ParallaxOverlay : Overlay
|
|||
// The effects of this are such that a slowness of 1 anchors the layer to the centre of the screen, while a slowness of 0 anchors the layer to the world.
|
||||
// (For values 0.0 to 1.0 this is in effect a lerp, but it's deliberately unclamped.)
|
||||
// The ParallaxAnchor adapts the parallax for station positioning and possibly map-specific tweaks.
|
||||
var home = layer.Config.WorldHomePosition + _parallaxManager.ParallaxAnchor;
|
||||
var home = layer.Config.WorldHomePosition + _manager.ParallaxAnchor;
|
||||
var scrolled = layer.Config.Scrolling * realTime;
|
||||
|
||||
// Origin - start with the parallax shift itself.
|
||||
var originBL = (args.Viewport.Eye.Position.Position - home) * layer.Config.Slowness;
|
||||
var originBL = (position - home) * layer.Config.Slowness + scrolled;
|
||||
|
||||
// Place at the home.
|
||||
originBL += home;
|
||||
|
|
@ -90,6 +99,8 @@ public sealed class ParallaxOverlay : Overlay
|
|||
screenHandle.DrawTextureRect(tex, Box2.FromDimensions(originBL, size));
|
||||
}
|
||||
}
|
||||
|
||||
screenHandle.UseShader(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
71
Content.Client/Parallax/ParallaxSystem.cs
Normal file
71
Content.Client/Parallax/ParallaxSystem.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using Content.Client.Parallax.Managers;
|
||||
using Content.Shared.Parallax;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Parallax;
|
||||
|
||||
public sealed class ParallaxSystem : SharedParallaxSystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlay = default!;
|
||||
[Dependency] private readonly IParallaxManager _parallax = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
|
||||
private const string Fallback = "Default";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_overlay.AddOverlay(new ParallaxOverlay());
|
||||
SubscribeLocalEvent<ParallaxComponent, ComponentHandleState>(OnParallaxHandleState);
|
||||
_protoManager.PrototypesReloaded += OnReload;
|
||||
}
|
||||
|
||||
private void OnReload(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
_parallax.UnloadParallax(Fallback);
|
||||
_parallax.LoadDefaultParallax();
|
||||
|
||||
foreach (var comp in EntityQuery<ParallaxComponent>(true))
|
||||
{
|
||||
_parallax.UnloadParallax(comp.Parallax);
|
||||
_parallax.LoadParallaxByName(comp.Parallax);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlay.RemoveOverlay<ParallaxOverlay>();
|
||||
_protoManager.PrototypesReloaded -= OnReload;
|
||||
}
|
||||
|
||||
private void OnParallaxHandleState(EntityUid uid, ParallaxComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not ParallaxComponentState state) return;
|
||||
component.Parallax = state.Parallax;
|
||||
|
||||
if (!_parallax.IsLoaded(component.Parallax))
|
||||
{
|
||||
_parallax.LoadParallaxByName(component.Parallax);
|
||||
}
|
||||
}
|
||||
|
||||
public ParallaxLayerPrepared[] GetParallaxLayers(MapId mapId)
|
||||
{
|
||||
return _parallax.GetParallaxLayers(GetParallax(_map.GetMapEntityId(mapId)));
|
||||
}
|
||||
|
||||
public string GetParallax(MapId mapId)
|
||||
{
|
||||
return GetParallax(_map.GetMapEntityId(mapId));
|
||||
}
|
||||
|
||||
public string GetParallax(EntityUid mapUid)
|
||||
{
|
||||
return TryComp<ParallaxComponent>(mapUid, out var parallax) ? parallax.Parallax : Fallback;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,8 +24,6 @@ namespace Content.Client.Physics.Controllers
|
|||
{
|
||||
if (relayMover.RelayEntity != null)
|
||||
HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HandleClientsideMovement(player, frameTime);
|
||||
|
|
@ -34,13 +32,26 @@ namespace Content.Client.Physics.Controllers
|
|||
private void HandleClientsideMovement(EntityUid player, float frameTime)
|
||||
{
|
||||
if (!TryComp(player, out InputMoverComponent? mover) ||
|
||||
!TryComp(player, out TransformComponent? xform)) return;
|
||||
!TryComp(player, out TransformComponent? xform))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PhysicsComponent? body = null;
|
||||
TransformComponent? xformMover = xform;
|
||||
|
||||
if (mover.ToParent && HasComp<RelayInputMoverComponent>(xform.ParentUid))
|
||||
{
|
||||
if (!TryComp(xform.ParentUid, out body)) return;
|
||||
if (!TryComp(xform.ParentUid, out body) ||
|
||||
!TryComp(xform.ParentUid, out xformMover))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<InputMoverComponent>(xform.ParentUid, out var parentMover))
|
||||
{
|
||||
mover.LastGridAngle = parentMover.LastGridAngle;
|
||||
}
|
||||
}
|
||||
else if (!TryComp(player, out body))
|
||||
{
|
||||
|
|
@ -60,10 +71,13 @@ namespace Content.Client.Physics.Controllers
|
|||
|
||||
if (TryComp(player, out JointComponent? jointComponent))
|
||||
{
|
||||
foreach (var joint in jointComponent.GetJoints)
|
||||
foreach (var joint in jointComponent.GetJoints.Values)
|
||||
{
|
||||
joint.BodyA.Predict = true;
|
||||
joint.BodyB.Predict = true;
|
||||
if (TryComp(joint.BodyAUid, out PhysicsComponent? physics))
|
||||
physics.Predict = true;
|
||||
|
||||
if (TryComp(joint.BodyBUid, out physics))
|
||||
physics.Predict = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,12 +98,7 @@ namespace Content.Client.Physics.Controllers
|
|||
}
|
||||
|
||||
// Server-side should just be handled on its own so we'll just do this shizznit
|
||||
HandleMobMovement(mover, body, xform, frameTime);
|
||||
}
|
||||
|
||||
protected override Filter GetSoundPlayers(EntityUid mover)
|
||||
{
|
||||
return Filter.Local();
|
||||
HandleMobMovement(mover, body, xformMover, frameTime);
|
||||
}
|
||||
|
||||
protected override bool CanSound()
|
||||
|
|
|
|||
|
|
@ -157,7 +157,8 @@ namespace Content.Client.Popups
|
|||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
if (_aliveWorldLabels.Count == 0) return;
|
||||
if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
|
||||
return;
|
||||
|
||||
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
var playerPos = player != null ? Transform(player.Value).MapPosition : MapCoordinates.Nullspace;
|
||||
|
|
@ -234,13 +235,13 @@ namespace Content.Client.Popups
|
|||
|
||||
public CursorPopupLabel(ScreenCoordinates screenCoords) : base()
|
||||
{
|
||||
InitialPos = screenCoords.Position / UIScale - DesiredSize / 2;
|
||||
InitialPos = screenCoords.Position - DesiredSize / 2;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs eventArgs)
|
||||
{
|
||||
base.FrameUpdate(eventArgs);
|
||||
LayoutContainer.SetPosition(this, InitialPos - (0, 20 * (TotalTime * TotalTime + TotalTime)));
|
||||
LayoutContainer.SetPosition(this, InitialPos / UIScale - (0, 20 * (TotalTime * TotalTime + TotalTime)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ public sealed class RadarControl : Control
|
|||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
MinSize = (SizeFull, SizeFull);
|
||||
RectClipContent = true;
|
||||
}
|
||||
|
||||
public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ namespace Content.Client.Storage
|
|||
_window.StorageContainerButton.OnPressed += TouchedContainerButton;
|
||||
|
||||
_window.OnClose += Close;
|
||||
_window.OpenCentered();
|
||||
_window.OpenCenteredLeft();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Robust.Client.UserInterface.Controls;
|
|||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Content.Client.Items.Components;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Item;
|
||||
using Robust.Client.UserInterface;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
using static Content.Shared.Storage.SharedStorageComponent;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,6 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
|||
|
||||
public void RemoveTimer(EntityUid uid)
|
||||
{
|
||||
EntityManager.RemoveComponent<ActiveSurveillanceCameraMonitorVisualsComponent>(uid);
|
||||
EntityManager.RemoveComponentDeferred<ActiveSurveillanceCameraMonitorVisualsComponent>(uid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
|
|||
public event Action? CameraDisconnect;
|
||||
|
||||
private string _currentAddress = string.Empty;
|
||||
private bool _isSwitching;
|
||||
private readonly FixedEye _defaultEye = new();
|
||||
private readonly Dictionary<string, int> _subnetMap = new();
|
||||
|
||||
|
|
@ -135,27 +136,37 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
|
|||
|
||||
private void SetCameraView(IEye? eye)
|
||||
{
|
||||
var eyeChanged = eye != CameraView.Eye || CameraView.Eye == null;
|
||||
CameraView.Eye = eye ?? _defaultEye;
|
||||
CameraView.Visible = eye != null;
|
||||
CameraViewBackground.Visible = true;
|
||||
CameraView.Visible = !eyeChanged && !_isSwitching;
|
||||
CameraDisconnectButton.Disabled = eye == null;
|
||||
|
||||
if (eye != null)
|
||||
{
|
||||
if (!eyeChanged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isSwitching = true;
|
||||
CameraViewBackground.Visible = true;
|
||||
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status",
|
||||
("status", Loc.GetString("surveillance-camera-monitor-ui-status-connecting")),
|
||||
("address", _currentAddress));
|
||||
("status", Loc.GetString("surveillance-camera-monitor-ui-status-connecting")),
|
||||
("address", _currentAddress));
|
||||
CameraSwitchTimer!();
|
||||
}
|
||||
else
|
||||
{
|
||||
CameraViewBackground.Visible = true;
|
||||
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status-disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSwitchTimerComplete()
|
||||
{
|
||||
CameraViewBackground.Visible = false;
|
||||
_isSwitching = false;
|
||||
CameraView.Visible = CameraView.Eye != _defaultEye;
|
||||
CameraViewBackground.Visible = CameraView.Eye == _defaultEye;
|
||||
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status",
|
||||
("status", Loc.GetString("surveillance-camera-monitor-ui-status-connected")),
|
||||
("address", _currentAddress));
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public sealed class ToggleableLightVisualsSystem : VisualizerSystem<ToggleableLi
|
|||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<ToggleableLightVisualsComponent, GetInhandVisualsEvent>(OnGetHeldVisuals, after: new[] { typeof(ItemSystem) });
|
||||
SubscribeLocalEvent<ToggleableLightVisualsComponent, GetEquipmentVisualsEvent>(OnGetEquipmentVisuals, after: new[] { typeof(ClothingSystem) });
|
||||
SubscribeLocalEvent<ToggleableLightVisualsComponent, GetEquipmentVisualsEvent>(OnGetEquipmentVisuals, after: new[] { typeof(ClothingVisualsSystem) });
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, ToggleableLightVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
using System;
|
||||
using Content.Shared.Sound;
|
||||
using Content.Shared.Trigger;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Client.Trigger
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.VendingMachines;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using static Content.Shared.VendingMachines.SharedVendingMachineComponent;
|
||||
|
|
@ -139,7 +142,18 @@ namespace Content.Client.VendingMachines.UI
|
|||
base.OnChangeData(component);
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var sprite = entMan.GetComponent<ISpriteComponent>(component.Owner);
|
||||
var sprite = entMan.GetComponent<SpriteComponent>(component.Owner);
|
||||
|
||||
// TODO when moving to a system visualizer, re work how this is done
|
||||
// Currently this only gets called during init, so unless it NEEEDS to be configurable, just make this party of the entity prototype.
|
||||
if (component.TryGetData(VendingMachineVisuals.Inventory, out string? invId) &&
|
||||
IoCManager.Resolve<IPrototypeManager>().TryIndex(invId, out VendingMachineInventoryPrototype? prototype) &&
|
||||
IoCManager.Resolve<IResourceCache>().TryGetResource<RSIResource>(
|
||||
SharedSpriteComponent.TextureRoot / $"Structures/Machines/VendingMachines/{prototype.SpriteName}.rsi", out var res))
|
||||
{
|
||||
sprite.BaseRSI = res.RSI;
|
||||
}
|
||||
|
||||
var animPlayer = entMan.GetComponent<AnimationPlayerComponent>(component.Owner);
|
||||
if (!component.TryGetData(VendingMachineVisuals.VisualState, out VendingMachineVisualState state))
|
||||
{
|
||||
|
|
|
|||
10
Content.Client/VendingMachines/VendingMachineComponent.cs
Normal file
10
Content.Client/VendingMachines/VendingMachineComponent.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using Content.Shared.VendingMachines;
|
||||
|
||||
namespace Content.Client.VendingMachines;
|
||||
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedVendingMachineComponent))]
|
||||
public sealed class VendingMachineComponent : SharedVendingMachineComponent
|
||||
{
|
||||
|
||||
}
|
||||
7
Content.Client/VendingMachines/VendingMachineSystem.cs
Normal file
7
Content.Client/VendingMachines/VendingMachineSystem.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
using Content.Shared.VendingMachines;
|
||||
|
||||
namespace Content.Client.VendingMachines;
|
||||
|
||||
public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
||||
{
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Content.Shared.Wires;
|
||||
using Content.Shared.Wires;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ namespace Content.Client.Wires.UI
|
|||
|
||||
_menu = new WiresMenu(this);
|
||||
_menu.OnClose += Close;
|
||||
_menu.OpenCentered();
|
||||
_menu.OpenCenteredLeft();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
|
|
|
|||
|
|
@ -267,16 +267,8 @@ namespace Content.Client.Wires.UI
|
|||
|
||||
|
||||
_statusContainer.RemoveAllChildren();
|
||||
var originalStatuses = new List<StatusEntry>(state.Statuses); // TODO: maybe not this way?
|
||||
var shuffledStatuses = new List<StatusEntry>();
|
||||
for (var i = originalStatuses.Count; i > 0; i--)
|
||||
{
|
||||
var index = random.Next(originalStatuses.Count);
|
||||
shuffledStatuses.Add(originalStatuses[index]);
|
||||
originalStatuses.RemoveAt(index);
|
||||
}
|
||||
|
||||
foreach (var status in shuffledStatuses)
|
||||
foreach (var status in state.Statuses)
|
||||
{
|
||||
if (status.Value is StatusLightData statusLightData)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Client.Parallax.Managers;
|
||||
using Content.Client.Parallax;
|
||||
using Robust.Shared.Maths;
|
||||
|
|
@ -6,13 +8,30 @@ namespace Content.IntegrationTests
|
|||
{
|
||||
public sealed class DummyParallaxManager : IParallaxManager
|
||||
{
|
||||
public string ParallaxName { get; set; } = "";
|
||||
public Vector2 ParallaxAnchor { get; set; }
|
||||
public ParallaxLayerPrepared[] ParallaxLayers { get; } = {};
|
||||
|
||||
public void LoadParallax()
|
||||
public bool IsLoaded(string name)
|
||||
{
|
||||
ParallaxName = "default";
|
||||
return true;
|
||||
}
|
||||
|
||||
public ParallaxLayerPrepared[] GetParallaxLayers(string name)
|
||||
{
|
||||
return Array.Empty<ParallaxLayerPrepared>();
|
||||
}
|
||||
|
||||
public void LoadDefaultParallax()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public Task LoadParallaxByName(string name)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void UnloadParallax(string name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace Content.IntegrationTests.Tests.AI
|
|||
{
|
||||
foreach (var entity in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!entity.TryGetComponent<UtilityAi>("UtilityAI", out var npcNode)) continue;
|
||||
if (!entity.TryGetComponent<UtilityNPCComponent>("UtilityAI", out var npcNode)) continue;
|
||||
|
||||
foreach (var entry in npcNode.BehaviorSets)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Client.Clickable;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
|
|
@ -60,7 +60,6 @@ namespace Content.IntegrationTests.Tests
|
|||
{
|
||||
var ent = serverEntManager.SpawnEntity(prototype, testMap.GridCoords);
|
||||
serverEntManager.GetComponent<TransformComponent>(ent).WorldRotation = angle;
|
||||
serverEntManager.GetComponent<SpriteComponent>(ent).Scale = (scale, scale);
|
||||
entity = ent;
|
||||
});
|
||||
|
||||
|
|
@ -71,6 +70,8 @@ namespace Content.IntegrationTests.Tests
|
|||
|
||||
await client.WaitPost(() =>
|
||||
{
|
||||
clientEntManager.GetComponent<SpriteComponent>(entity).Scale = (scale, scale);
|
||||
|
||||
// these tests currently all assume player eye is 0
|
||||
eyeManager.CurrentEye.Rotation = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Construction
|
||||
|
|
@ -10,6 +14,42 @@ namespace Content.IntegrationTests.Tests.Construction
|
|||
{
|
||||
// discount linter for construction graphs
|
||||
// TODO: Create serialization validators for these?
|
||||
// Top test definitely can be but writing a serializer takes ages.
|
||||
|
||||
/// <summary>
|
||||
/// Checks every entity prototype with a construction component has a valid start node.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestStartNodeValid()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
var map = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.Components.ContainsKey("Construction"))
|
||||
continue;
|
||||
|
||||
var ent = entMan.SpawnEntity(proto.ID, new MapCoordinates(Vector2.Zero, map.MapId));
|
||||
var construction = entMan.GetComponent<ConstructionComponent>(ent);
|
||||
|
||||
var graph = protoMan.Index<ConstructionGraphPrototype>(construction.Graph);
|
||||
entMan.DeleteEntity(ent);
|
||||
|
||||
Assert.That(graph.Nodes.ContainsKey(construction.Node),
|
||||
$"Found no startNode \"{construction.Node}\" on graph \"{graph.ID}\" for entity \"{proto.ID}\"!");
|
||||
}
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestStartIsValid()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Clothing.Components;
|
||||
using Content.Server.Inventory;
|
||||
using Content.Shared.Clothing.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Item;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
|
@ -35,8 +37,9 @@ namespace Content.IntegrationTests.Tests
|
|||
entMgr.AddComponent<ContainerManagerComponent>(container);
|
||||
|
||||
var child = entMgr.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var item = entMgr.AddComponent<ItemComponent>(child);
|
||||
item.SlotFlags = SlotFlags.HEAD;
|
||||
var item = entMgr.AddComponent<ClothingComponent>(child);
|
||||
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ClothingSystem>().SetSlots(item.Owner, SlotFlags.HEAD, item);
|
||||
|
||||
// Equip item.
|
||||
Assert.That(invSystem.TryEquip(container, child, "head"), Is.True);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
|||
});
|
||||
|
||||
await server.WaitRunTicks(1);
|
||||
Assert.That(task.Status, Is.EqualTo(TaskStatus.RanToCompletion));
|
||||
#pragma warning disable RA0004
|
||||
Assert.That(task.Result == DoAfterStatus.Finished);
|
||||
#pragma warning restore RA0004
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
|
@ -69,7 +72,10 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
|||
});
|
||||
|
||||
await server.WaitRunTicks(3);
|
||||
Assert.That(task.Result == DoAfterStatus.Cancelled, $"Result was {task.Result}");
|
||||
Assert.That(task.Status, Is.EqualTo(TaskStatus.RanToCompletion));
|
||||
#pragma warning disable RA0004
|
||||
Assert.That(task.Result, Is.EqualTo(DoAfterStatus.Cancelled), $"Result was {task.Result}");
|
||||
#pragma warning restore RA0004
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ using Robust.Shared.Map;
|
|||
namespace Content.IntegrationTests.Tests.Gravity
|
||||
{
|
||||
[TestFixture]
|
||||
[TestOf(typeof(WeightlessSystem))]
|
||||
[TestOf(typeof(GravitySystem))]
|
||||
[TestOf(typeof(GravityGeneratorComponent))]
|
||||
public sealed class WeightlessStatusTests
|
||||
{
|
||||
|
|
@ -21,6 +21,9 @@ namespace Content.IntegrationTests.Tests.Gravity
|
|||
id: HumanDummy
|
||||
components:
|
||||
- type: Alerts
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
|
||||
- type: entity
|
||||
name: GravityGeneratorDummy
|
||||
id: GravityGeneratorDummy
|
||||
|
|
@ -38,7 +41,6 @@ namespace Content.IntegrationTests.Tests.Gravity
|
|||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var alertsSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<AlertsSystem>();
|
||||
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ namespace Content.IntegrationTests.Tests
|
|||
var grid1Entity = grid1.GridEntityId;
|
||||
var grid2Entity = grid2.GridEntityId;
|
||||
|
||||
Assert.That(!entityMan.GetComponent<GravityComponent>(grid1Entity).Enabled);
|
||||
Assert.That(entityMan.GetComponent<GravityComponent>(grid2Entity).Enabled);
|
||||
Assert.That(!entityMan.GetComponent<GravityComponent>(grid1Entity).EnabledVV);
|
||||
Assert.That(entityMan.GetComponent<GravityComponent>(grid2Entity).EnabledVV);
|
||||
|
||||
// Re-enable needs power so it turns off again.
|
||||
// Charge rate is ridiculously high so it finishes in one tick.
|
||||
|
|
@ -88,7 +88,7 @@ namespace Content.IntegrationTests.Tests
|
|||
|
||||
var grid2Entity = grid2.GridEntityId;
|
||||
|
||||
Assert.That(entityMan.GetComponent<GravityComponent>(grid2Entity).Enabled, Is.False);
|
||||
Assert.That(entityMan.GetComponent<GravityComponent>(grid2Entity).EnabledVV, Is.False);
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ namespace Content.IntegrationTests.Tests
|
|||
id: UniformDummy
|
||||
components:
|
||||
- type: Clothing
|
||||
Slots: [innerclothing]
|
||||
slots: [innerclothing]
|
||||
- type: Item
|
||||
size: 5
|
||||
|
||||
- type: entity
|
||||
|
|
@ -34,8 +35,9 @@ namespace Content.IntegrationTests.Tests
|
|||
id: IDCardDummy
|
||||
components:
|
||||
- type: Clothing
|
||||
Slots:
|
||||
slots:
|
||||
- idcard
|
||||
- type: Item
|
||||
size: 5
|
||||
- type: IdCard
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Content.Server.Interaction;
|
|||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Containers;
|
||||
|
|
@ -13,7 +14,6 @@ using Robust.Shared.IoC;
|
|||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Reflection;
|
||||
using ItemComponent = Content.Server.Clothing.Components.ItemComponent;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Interaction.Click
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Content.IntegrationTests.Tests.Interaction
|
|||
[TestOf(typeof(SharedInteractionSystem))]
|
||||
public sealed class InRangeUnobstructed
|
||||
{
|
||||
private const string HumanId = "MobHumanBase";
|
||||
private const string HumanId = "MobHuman";
|
||||
|
||||
private const float InteractionRange = SharedInteractionSystem.InteractionRange;
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Content.IntegrationTests.Tests
|
|||
id: InventoryJumpsuitJanitorDummy
|
||||
components:
|
||||
- type: Clothing
|
||||
Slots: [innerclothing]
|
||||
slots: [innerclothing]
|
||||
|
||||
- type: entity
|
||||
name: InventoryIDCardDummy
|
||||
|
|
@ -36,7 +36,7 @@ namespace Content.IntegrationTests.Tests
|
|||
components:
|
||||
- type: Clothing
|
||||
QuickEquip: false
|
||||
Slots:
|
||||
slots:
|
||||
- idcard
|
||||
- type: PDA
|
||||
";
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
#nullable enable
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.IntegrationTests;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SpriteComponent = Robust.Server.GameObjects.SpriteComponent;
|
||||
using SpriteComponent = Robust.Client.GameObjects.SpriteComponent;
|
||||
|
||||
namespace Content.MapRenderer.Painters
|
||||
{
|
||||
|
|
@ -40,7 +37,7 @@ namespace Content.MapRenderer.Painters
|
|||
|
||||
await client.WaitPost(() =>
|
||||
{
|
||||
if (cEntityManager.TryGetComponent(cPlayerManager.LocalPlayer!.ControlledEntity!, out Robust.Client.GameObjects.SpriteComponent? sprite))
|
||||
if (cEntityManager.TryGetComponent(cPlayerManager.LocalPlayer!.ControlledEntity!, out SpriteComponent? sprite))
|
||||
{
|
||||
sprite.Visible = false;
|
||||
}
|
||||
|
|
@ -49,14 +46,6 @@ namespace Content.MapRenderer.Painters
|
|||
var sEntityManager = server.ResolveDependency<IServerEntityManager>();
|
||||
var sPlayerManager = server.ResolveDependency<IPlayerManager>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
if (sEntityManager.TryGetComponent(sPlayerManager.ServerSessions.Single().AttachedEntity!, out SpriteComponent? sprite))
|
||||
{
|
||||
sprite.Visible = false;
|
||||
}
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
|
||||
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Content.Server.Administration;
|
||||
using Content.Server.AI.Components;
|
||||
using Content.Server.AI.EntitySystems;
|
||||
using Content.Server.AI.Utility;
|
||||
using Content.Server.AI.Utility.AiLogic;
|
||||
using Content.Shared.Administration;
|
||||
|
|
@ -35,22 +36,22 @@ namespace Content.Server.AI.Commands
|
|||
return;
|
||||
}
|
||||
|
||||
if (_entities.HasComponent<AiControllerComponent>(entId))
|
||||
if (_entities.HasComponent<NPCComponent>(entId))
|
||||
{
|
||||
shell.WriteLine("Entity already has an AI component.");
|
||||
return;
|
||||
}
|
||||
|
||||
var comp = _entities.AddComponent<UtilityAi>(entId);
|
||||
var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
|
||||
var comp = _entities.AddComponent<UtilityNPCComponent>(entId);
|
||||
var npcSystem = IoCManager.Resolve<IEntityManager>().EntitySysManager.GetEntitySystem<NPCSystem>();
|
||||
|
||||
for (var i = 1; i < args.Length; i++)
|
||||
{
|
||||
var bSet = args[i];
|
||||
behaviorManager.AddBehaviorSet(comp, bSet, false);
|
||||
npcSystem.AddBehaviorSet(comp, bSet, false);
|
||||
}
|
||||
|
||||
behaviorManager.RebuildActions(comp);
|
||||
npcSystem.RebuildActions(comp);
|
||||
shell.WriteLine("AI component added.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
Content.Server/AI/Components/ActiveNPCComponent.cs
Normal file
7
Content.Server/AI/Components/ActiveNPCComponent.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Content.Server.AI.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to NPCs that are actively being updated.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class ActiveNPCComponent : Component {}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
using Content.Server.AI.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.AI.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[Virtual]
|
||||
public class AiControllerComponent : Component
|
||||
{
|
||||
[DataField("logic")] private float _visionRadius = 8.0f;
|
||||
|
||||
// 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)]
|
||||
public float VisionRadius
|
||||
{
|
||||
get => _visionRadius;
|
||||
set => _visionRadius = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the entity Sprinting (running)?
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool Sprinting { get; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Calculated linear velocity direction of the entity.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Vector2 VelocityDir { get; set; }
|
||||
|
||||
public virtual void Update(float frameTime) {}
|
||||
}
|
||||
}
|
||||
17
Content.Server/AI/Components/NPCComponent.cs
Normal file
17
Content.Server/AI/Components/NPCComponent.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Content.Server.AI.EntitySystems;
|
||||
|
||||
namespace Content.Server.AI.Components
|
||||
{
|
||||
[Access(typeof(NPCSystem))]
|
||||
public abstract class NPCComponent : Component
|
||||
{
|
||||
// TODO: Soon. I didn't realise how much effort it was to deprecate the old one.
|
||||
/// <summary>
|
||||
/// Contains all of the world data for a particular NPC in terms of how it sees the world.
|
||||
/// </summary>
|
||||
//[ViewVariables, DataField("blackboardA")]
|
||||
//public Dictionary<string, object> BlackboardA = new();
|
||||
|
||||
public float VisionRadius => 7f;
|
||||
}
|
||||
}
|
||||
38
Content.Server/AI/Considerations/Bots/CanInjectCon.cs
Normal file
38
Content.Server/AI/Considerations/Bots/CanInjectCon.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.Tracking;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Server.Silicons.Bots;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Bot
|
||||
{
|
||||
public sealed class CanInjectCon : Consideration
|
||||
{
|
||||
protected override float GetScore(Blackboard context)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var target = context.GetState<TargetEntityState>().GetValue();
|
||||
|
||||
if (target == null || !entMan.TryGetComponent(target, out DamageableComponent? damageableComponent))
|
||||
return 0;
|
||||
|
||||
if (entMan.TryGetComponent(target, out RecentlyInjectedComponent? recently))
|
||||
return 0f;
|
||||
|
||||
if (!entMan.TryGetComponent(target, out MobStateComponent? mobState) || mobState.IsDead())
|
||||
return 0f;
|
||||
|
||||
if (damageableComponent.TotalDamage == 0)
|
||||
return 0f;
|
||||
|
||||
if (damageableComponent.TotalDamage <= MedibotComponent.StandardMedDamageThreshold)
|
||||
return 1f;
|
||||
|
||||
if (damageableComponent.TotalDamage >= MedibotComponent.EmergencyMedDamageThreshold)
|
||||
return 1f;
|
||||
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Content.Server/AI/EntitySystems/InjectNearbySystem.cs
Normal file
74
Content.Server/AI/EntitySystems/InjectNearbySystem.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.AI.Tracking;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Silicons.Bots;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.AI.EntitySystems
|
||||
{
|
||||
public sealed class InjectNearbySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
public EntityUid GetNearbyInjectable(EntityUid medibot, float range = 4)
|
||||
{
|
||||
foreach (var entity in _lookup.GetEntitiesInRange(medibot, range))
|
||||
{
|
||||
if (HasComp<InjectableSolutionComponent>(entity) && HasComp<MobStateComponent>(entity))
|
||||
return entity;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public bool Inject(EntityUid medibot, EntityUid target)
|
||||
{
|
||||
if (!TryComp<MedibotComponent>(medibot, out var botComp))
|
||||
return false;
|
||||
|
||||
if (!TryComp<DamageableComponent>(target, out var damage))
|
||||
return false;
|
||||
|
||||
if (!_solutionSystem.TryGetInjectableSolution(target, out var injectable))
|
||||
return false;
|
||||
|
||||
if (!_interactionSystem.InRangeUnobstructed(medibot, target))
|
||||
return true; // return true lets the bot reattempt the action on the same target
|
||||
|
||||
if (damage.TotalDamage == 0)
|
||||
return false;
|
||||
|
||||
if (damage.TotalDamage <= MedibotComponent.StandardMedDamageThreshold)
|
||||
{
|
||||
_solutionSystem.TryAddReagent(target, injectable, botComp.StandardMed, botComp.StandardMedInjectAmount, out var accepted);
|
||||
EnsureComp<RecentlyInjectedComponent>(target);
|
||||
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
|
||||
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
|
||||
_chat.TrySendInGameICMessage(medibot, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (damage.TotalDamage >= MedibotComponent.EmergencyMedDamageThreshold)
|
||||
{
|
||||
_solutionSystem.TryAddReagent(target, injectable, botComp.EmergencyMed, botComp.EmergencyMedInjectAmount, out var accepted);
|
||||
EnsureComp<RecentlyInjectedComponent>(target);
|
||||
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
|
||||
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
|
||||
_chat.TrySendInGameICMessage(medibot, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs
Normal file
42
Content.Server/AI/EntitySystems/NPCSystem.Blackboard.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.AI.Components;
|
||||
|
||||
namespace Content.Server.AI.EntitySystems;
|
||||
|
||||
public sealed partial class NPCSystem
|
||||
{
|
||||
/*
|
||||
/// <summary>
|
||||
/// Tries to get the blackboard data for a particular key. Returns default if not found
|
||||
/// </summary>
|
||||
public T? GetValueOrDefault<T>(NPCComponent component, string key)
|
||||
{
|
||||
if (component.BlackboardA.TryGetValue(key, out var value))
|
||||
{
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the blackboard data for a particular key.
|
||||
/// </summary>
|
||||
public bool TryGetValue<T>(NPCComponent component, string key, [NotNullWhen(true)] out T? value)
|
||||
{
|
||||
if (component.BlackboardA.TryGetValue(key, out var data))
|
||||
{
|
||||
value = (T) data;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Constants to make development easier
|
||||
*/
|
||||
|
||||
public const string VisionRadius = "VisionRadius";
|
||||
}
|
||||
284
Content.Server/AI/EntitySystems/NPCSystem.Utility.cs
Normal file
284
Content.Server/AI/EntitySystems/NPCSystem.Utility.cs
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using Content.Server.AI.Components;
|
||||
using Content.Server.AI.LoadBalancer;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Utility;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.AiLogic;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Utility;
|
||||
using Content.Server.CPUJob.JobQueues;
|
||||
using Content.Server.CPUJob.JobQueues.Queues;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
|
||||
namespace Content.Server.AI.EntitySystems;
|
||||
|
||||
public sealed partial class NPCSystem
|
||||
{
|
||||
/*
|
||||
* Handles Utility AI, implemented via IAUS
|
||||
*/
|
||||
|
||||
private readonly NpcActionComparer _comparer = new();
|
||||
|
||||
private Dictionary<string, List<Type>> _behaviorSets = new();
|
||||
|
||||
private readonly AiActionJobQueue _aiRequestQueue = new();
|
||||
|
||||
private void InitializeUtility()
|
||||
{
|
||||
SubscribeLocalEvent<UtilityNPCComponent, ComponentStartup>(OnUtilityStartup);
|
||||
|
||||
foreach (var bSet in _prototypeManager.EnumeratePrototypes<BehaviorSetPrototype>())
|
||||
{
|
||||
var actions = new List<Type>();
|
||||
|
||||
foreach (var act in bSet.Actions)
|
||||
{
|
||||
if (!_reflectionManager.TryLooseGetType(act, out var parsedType) ||
|
||||
!typeof(IAiUtility).IsAssignableFrom(parsedType))
|
||||
{
|
||||
_sawmill.Error($"Unable to parse AI action for {act}");
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.Add(parsedType);
|
||||
}
|
||||
}
|
||||
|
||||
_behaviorSets[bSet.ID] = actions;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUtilityStartup(EntityUid uid, UtilityNPCComponent component, ComponentStartup args)
|
||||
{
|
||||
if (component.BehaviorSets.Count > 0)
|
||||
{
|
||||
RebuildActions(component);
|
||||
}
|
||||
|
||||
component._planCooldownRemaining = component.PlanCooldown;
|
||||
component._blackboard = new Blackboard(component.Owner);
|
||||
}
|
||||
|
||||
public AiActionRequestJob RequestAction(UtilityNPCComponent component, AiActionRequest request, CancellationTokenSource cancellationToken)
|
||||
{
|
||||
var job = new AiActionRequestJob(0.002, request, cancellationToken.Token);
|
||||
// AI should already know if it shouldn't request again
|
||||
_aiRequestQueue.EnqueueJob(job);
|
||||
return job;
|
||||
}
|
||||
|
||||
private void UpdateUtility(float frameTime)
|
||||
{
|
||||
foreach (var (_, comp) in EntityQuery<ActiveNPCComponent, UtilityNPCComponent>())
|
||||
{
|
||||
if (_count >= _maxUpdates) break;
|
||||
|
||||
Update(comp, frameTime);
|
||||
_count++;
|
||||
}
|
||||
|
||||
_aiRequestQueue.Process();
|
||||
}
|
||||
|
||||
private void ReceivedAction(UtilityNPCComponent component)
|
||||
{
|
||||
if (component._actionRequest == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (component._actionRequest.Exception)
|
||||
{
|
||||
case null:
|
||||
break;
|
||||
default:
|
||||
_sawmill.Fatal(component._actionRequest.Exception.ToString());
|
||||
ExceptionDispatchInfo.Capture(component._actionRequest.Exception).Throw();
|
||||
// The code never actually reaches here, because the above throws.
|
||||
// This is to tell the compiler that the flow never leaves here.
|
||||
throw component._actionRequest.Exception;
|
||||
}
|
||||
var action = component._actionRequest.Result;
|
||||
component._actionRequest = null;
|
||||
// Actions with lower scores should be implicitly dumped by GetAction
|
||||
// If we're not allowed to replace the action with an action of the same type then dump.
|
||||
if (action == null || !action.CanOverride && component.CurrentAction?.GetType() == action.GetType())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentOp = component.CurrentAction?.ActionOperators.Peek();
|
||||
if (currentOp != null && currentOp.HasStartup)
|
||||
{
|
||||
currentOp.Shutdown(Outcome.Failed);
|
||||
}
|
||||
|
||||
component.CurrentAction = action;
|
||||
action.SetupOperators(component._blackboard);
|
||||
}
|
||||
|
||||
private void Update(UtilityNPCComponent component, float frameTime)
|
||||
{
|
||||
// If we asked for a new action we don't want to dump the existing one.
|
||||
if (component._actionRequest != null)
|
||||
{
|
||||
if (component._actionRequest.Status != JobStatus.Finished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReceivedAction(component);
|
||||
// Do something next tick
|
||||
return;
|
||||
}
|
||||
|
||||
component._planCooldownRemaining -= frameTime;
|
||||
|
||||
// Might find a better action while we're doing one already
|
||||
if (component._planCooldownRemaining <= 0.0f)
|
||||
{
|
||||
component._planCooldownRemaining = component.PlanCooldown;
|
||||
component._actionCancellation = new CancellationTokenSource();
|
||||
component._actionRequest = RequestAction(component, new AiActionRequest(component.Owner, component._blackboard, component.AvailableActions), component._actionCancellation);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// When we spawn in we won't get an action for a bit
|
||||
if (component.CurrentAction == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var outcome = component.CurrentAction.Execute(frameTime);
|
||||
|
||||
switch (outcome)
|
||||
{
|
||||
case Outcome.Success:
|
||||
if (component.CurrentAction.ActionOperators.Count == 0)
|
||||
{
|
||||
component.CurrentAction.Shutdown();
|
||||
component.CurrentAction = null;
|
||||
// Nothing to compare new action to
|
||||
component._blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
|
||||
}
|
||||
break;
|
||||
case Outcome.Continuing:
|
||||
break;
|
||||
case Outcome.Failed:
|
||||
component.CurrentAction.Shutdown();
|
||||
component.CurrentAction = null;
|
||||
component._blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the BehaviorSet to the NPC.
|
||||
/// </summary>
|
||||
/// <param name="npc"></param>
|
||||
/// <param name="behaviorSet"></param>
|
||||
/// <param name="rebuild">Set to false if you want to manually rebuild it after bulk updates.</param>
|
||||
public void AddBehaviorSet(UtilityNPCComponent npc, string behaviorSet, bool rebuild = true)
|
||||
{
|
||||
if (!_behaviorSets.ContainsKey(behaviorSet))
|
||||
{
|
||||
_sawmill.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} but no such BehaviorSet found!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!npc.BehaviorSets.Add(behaviorSet))
|
||||
{
|
||||
_sawmill.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} which already has the BehaviorSet!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (rebuild)
|
||||
RebuildActions(npc);
|
||||
|
||||
if (npc.BehaviorSets.Count == 1 && !IsAwake(npc))
|
||||
WakeNPC(npc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the BehaviorSet from the NPC.
|
||||
/// </summary>
|
||||
/// <param name="npc"></param>
|
||||
/// <param name="behaviorSet"></param>
|
||||
/// <param name="rebuild">Set to false if yo uwant to manually rebuild it after bulk updates.</param>
|
||||
public void RemoveBehaviorSet(UtilityNPCComponent npc, string behaviorSet, bool rebuild = true)
|
||||
{
|
||||
if (!_behaviorSets.TryGetValue(behaviorSet, out var actions))
|
||||
{
|
||||
Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but no such BehaviorSet found!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!npc.BehaviorSets.Remove(behaviorSet))
|
||||
{
|
||||
Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but it doesn't have that BehaviorSet!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (rebuild)
|
||||
RebuildActions(npc);
|
||||
|
||||
if (npc.BehaviorSets.Count == 0)
|
||||
SleepNPC(npc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear our actions and re-instantiate them from our BehaviorSets.
|
||||
/// Will ensure each action is unique.
|
||||
/// </summary>
|
||||
/// <param name="npc"></param>
|
||||
public void RebuildActions(UtilityNPCComponent npc)
|
||||
{
|
||||
npc.AvailableActions.Clear();
|
||||
foreach (var bSet in npc.BehaviorSets)
|
||||
{
|
||||
foreach (var action in GetActions(bSet))
|
||||
{
|
||||
if (npc.AvailableActions.Contains(action)) continue;
|
||||
// Setup
|
||||
action.Owner = npc.Owner;
|
||||
|
||||
// Ad to actions.
|
||||
npc.AvailableActions.Add(action);
|
||||
}
|
||||
}
|
||||
|
||||
SortActions(npc);
|
||||
}
|
||||
|
||||
private IEnumerable<IAiUtility> GetActions(string behaviorSet)
|
||||
{
|
||||
foreach (var action in _behaviorSets[behaviorSet])
|
||||
{
|
||||
yield return (IAiUtility) _typeFactory.CreateInstance(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whenever the behavior sets are changed we'll re-sort the actions by bonus
|
||||
/// </summary>
|
||||
private void SortActions(UtilityNPCComponent npc)
|
||||
{
|
||||
npc.AvailableActions.Sort(_comparer);
|
||||
}
|
||||
|
||||
private sealed class NpcActionComparer : Comparer<IAiUtility>
|
||||
{
|
||||
public override int Compare(IAiUtility? x, IAiUtility? y)
|
||||
{
|
||||
if (x == null || y == null) return 0;
|
||||
return y.Bonus.CompareTo(x.Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using System.Linq;
|
||||
using Content.Server.AI.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.MobState;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
|
||||
namespace Content.Server.AI.EntitySystems
|
||||
{
|
||||
|
|
@ -12,120 +12,103 @@ namespace Content.Server.AI.EntitySystems
|
|||
/// Handles NPCs running every tick.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal sealed class NPCSystem : EntitySystem
|
||||
public sealed partial class NPCSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
|
||||
/// </summary>
|
||||
private readonly HashSet<AiControllerComponent> _awakeNPCs = new();
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether any NPCs are allowed to run at all.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
private int _maxUpdates;
|
||||
|
||||
private int _count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AiControllerComponent, MobStateChangedEvent>(OnMobStateChange);
|
||||
SubscribeLocalEvent<AiControllerComponent, ComponentInit>(OnNPCInit);
|
||||
SubscribeLocalEvent<AiControllerComponent, ComponentShutdown>(OnNPCShutdown);
|
||||
_sawmill = Logger.GetSawmill("npc");
|
||||
InitializeUtility();
|
||||
SubscribeLocalEvent<NPCComponent, MobStateChangedEvent>(OnMobStateChange);
|
||||
SubscribeLocalEvent<NPCComponent, ComponentInit>(OnNPCInit);
|
||||
SubscribeLocalEvent<NPCComponent, ComponentShutdown>(OnNPCShutdown);
|
||||
_configurationManager.OnValueChanged(CCVars.NPCEnabled, SetEnabled, true);
|
||||
|
||||
var maxUpdates = _configurationManager.GetCVar(CCVars.NPCMaxUpdates);
|
||||
|
||||
if (maxUpdates < 1024)
|
||||
_awakeNPCs.EnsureCapacity(maxUpdates);
|
||||
_configurationManager.OnValueChanged(CCVars.NPCMaxUpdates, SetMaxUpdates, true);
|
||||
}
|
||||
|
||||
private void SetMaxUpdates(int obj) => _maxUpdates = obj;
|
||||
private void SetEnabled(bool value) => Enabled = value;
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_configurationManager.UnsubValueChanged(CCVars.NPCEnabled, SetEnabled);
|
||||
_configurationManager.UnsubValueChanged(CCVars.NPCMaxUpdates, SetMaxUpdates);
|
||||
}
|
||||
|
||||
private void OnNPCInit(EntityUid uid, AiControllerComponent component, ComponentInit args)
|
||||
private void OnNPCInit(EntityUid uid, NPCComponent component, ComponentInit args)
|
||||
{
|
||||
if (!component.Awake) return;
|
||||
|
||||
_awakeNPCs.Add(component);
|
||||
WakeNPC(component);
|
||||
}
|
||||
|
||||
private void OnNPCShutdown(EntityUid uid, AiControllerComponent component, ComponentShutdown args)
|
||||
private void OnNPCShutdown(EntityUid uid, NPCComponent component, ComponentShutdown args)
|
||||
{
|
||||
_awakeNPCs.Remove(component);
|
||||
SleepNPC(component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the NPC awake and updating?
|
||||
/// </summary>
|
||||
public bool IsAwake(NPCComponent component, ActiveNPCComponent? active = null)
|
||||
{
|
||||
return Resolve(component.Owner, ref active, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the NPC to actively be updated.
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
public void WakeNPC(AiControllerComponent component)
|
||||
public void WakeNPC(NPCComponent component)
|
||||
{
|
||||
_awakeNPCs.Add(component);
|
||||
_sawmill.Debug($"Waking {ToPrettyString(component.Owner)}");
|
||||
EnsureComp<ActiveNPCComponent>(component.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the NPC from actively being updated.
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
public void SleepNPC(AiControllerComponent component)
|
||||
public void SleepNPC(NPCComponent component)
|
||||
{
|
||||
_awakeNPCs.Remove(component);
|
||||
_sawmill.Debug($"Sleeping {ToPrettyString(component.Owner)}");
|
||||
RemComp<ActiveNPCComponent>(component.Owner);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(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++)
|
||||
{
|
||||
MetaDataComponent? metadata = null;
|
||||
var index = (i + startIndex) % npcs.Length;
|
||||
var npc = npcs[index];
|
||||
|
||||
if (Deleted(npc.Owner, metadata))
|
||||
continue;
|
||||
|
||||
// Probably gets resolved in deleted for us already
|
||||
if (Paused(npc.Owner, metadata))
|
||||
continue;
|
||||
|
||||
npc.Update(frameTime);
|
||||
}
|
||||
_count = 0;
|
||||
UpdateUtility(frameTime);
|
||||
}
|
||||
|
||||
private void OnMobStateChange(EntityUid uid, AiControllerComponent component, MobStateChangedEvent args)
|
||||
private void OnMobStateChange(EntityUid uid, NPCComponent component, MobStateChangedEvent args)
|
||||
{
|
||||
switch (args.CurrentMobState)
|
||||
{
|
||||
case DamageState.Alive:
|
||||
component.Awake = true;
|
||||
WakeNPC(component);
|
||||
break;
|
||||
case DamageState.Critical:
|
||||
case DamageState.Dead:
|
||||
component.Awake = false;
|
||||
SleepNPC(component);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ namespace Content.Server.AI.LoadBalancer
|
|||
|
||||
var entity = _request.Context.GetState<SelfState>().GetValue();
|
||||
|
||||
if (!IoCManager.Resolve<IEntityManager>().HasComponent<AiControllerComponent>(entity))
|
||||
if (!IoCManager.Resolve<IEntityManager>().HasComponent<NPCComponent>(entity))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
using System.Threading;
|
||||
using Content.Server.CPUJob.JobQueues.Queues;
|
||||
|
||||
namespace Content.Server.AI.LoadBalancer
|
||||
{
|
||||
/// <summary>
|
||||
/// This will queue up an AI's request for an action and give it one when possible
|
||||
/// </summary>
|
||||
public sealed class AiActionSystem : EntitySystem
|
||||
{
|
||||
private readonly AiActionJobQueue _aiRequestQueue = new();
|
||||
|
||||
public AiActionRequestJob RequestAction(AiActionRequest request, CancellationTokenSource cancellationToken)
|
||||
{
|
||||
var job = new AiActionRequestJob(0.002, request, cancellationToken.Token);
|
||||
// AI should already know if it shouldn't request again
|
||||
_aiRequestQueue.EnqueueJob(job);
|
||||
return job;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
_aiRequestQueue.Process();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Content.Server/AI/Operators/Bots/InjectOperator.cs
Normal file
24
Content.Server/AI/Operators/Bots/InjectOperator.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using Content.Server.AI.EntitySystems;
|
||||
|
||||
namespace Content.Server.AI.Operators.Bots
|
||||
{
|
||||
public sealed class InjectOperator : AiOperator
|
||||
{
|
||||
private EntityUid _medibot;
|
||||
private EntityUid _target;
|
||||
public InjectOperator(EntityUid medibot, EntityUid target)
|
||||
{
|
||||
_medibot = medibot;
|
||||
_target = target;
|
||||
}
|
||||
|
||||
public override Outcome Execute(float frameTime)
|
||||
{
|
||||
var injectSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InjectNearbySystem>();
|
||||
if (injectSystem.Inject(_medibot, _target))
|
||||
return Outcome.Success;
|
||||
|
||||
return Outcome.Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ namespace Content.Server.AI.Operators.Inventory
|
|||
var handsSys = sysMan.GetEntitySystem<SharedHandsSystem>();
|
||||
|
||||
if (entMan.Deleted(_target)
|
||||
|| !entMan.HasComponent<SharedItemComponent>(_target)
|
||||
|| !entMan.HasComponent<ItemComponent>(_target)
|
||||
|| _target.IsInContainer()
|
||||
|| !interactionSystem.InRangeUnobstructed(_owner, _target, popup: true))
|
||||
{
|
||||
|
|
@ -35,7 +35,7 @@ namespace Content.Server.AI.Operators.Inventory
|
|||
// select empty hand
|
||||
if (!handsSys.TrySelectEmptyHand(_owner))
|
||||
return Outcome.Failed;
|
||||
|
||||
|
||||
interactionSystem.InteractHand(_owner, _target);
|
||||
return Outcome.Success;
|
||||
}
|
||||
|
|
|
|||
22
Content.Server/AI/Operators/Speech/SpeechOperator.cs
Normal file
22
Content.Server/AI/Operators/Speech/SpeechOperator.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Content.Server.Chat.Systems;
|
||||
|
||||
namespace Content.Server.AI.Operators.Speech
|
||||
{
|
||||
public sealed class SpeakOperator : AiOperator
|
||||
{
|
||||
private EntityUid _speaker;
|
||||
private string _speechString;
|
||||
public SpeakOperator(EntityUid speaker, string speechString)
|
||||
{
|
||||
_speaker = speaker;
|
||||
_speechString = speechString;
|
||||
}
|
||||
|
||||
public override Outcome Execute(float frameTime)
|
||||
{
|
||||
var chatSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
|
||||
chatSystem.TrySendInGameICMessage(_speaker, _speechString, InGameICChatType.Speak, false);
|
||||
return Outcome.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Server.AI.Components;
|
||||
using Content.Server.AI.EntitySystems;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
|
|||
|
||||
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
|
||||
var access = accessSystem.FindAccessTags(entity);
|
||||
var visionRadius = entMan.GetComponent<AiControllerComponent>(entity).VisionRadius;
|
||||
var visionRadius = entMan.GetComponent<NPCComponent>(entity).VisionRadius;
|
||||
|
||||
return new ReachableArgs(visionRadius, access, collisionMask);
|
||||
}
|
||||
|
|
|
|||
12
Content.Server/AI/Tracking/RecentlyInjectedComponent.cs
Normal file
12
Content.Server/AI/Tracking/RecentlyInjectedComponent.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Content.Server.AI.Tracking
|
||||
{
|
||||
/// Added when a medibot injects someone
|
||||
/// So they don't get injected again for at least a minute.
|
||||
[RegisterComponent]
|
||||
public sealed class RecentlyInjectedComponent : Component
|
||||
{
|
||||
public float Accumulator = 0f;
|
||||
|
||||
public TimeSpan RemoveTime = TimeSpan.FromMinutes(1);
|
||||
}
|
||||
}
|
||||
25
Content.Server/AI/Tracking/RecentlyInjectedSystem.cs
Normal file
25
Content.Server/AI/Tracking/RecentlyInjectedSystem.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
namespace Content.Server.AI.Tracking
|
||||
{
|
||||
public sealed class RecentlyInjectedSystem : EntitySystem
|
||||
{
|
||||
|
||||
Queue<EntityUid> RemQueue = new();
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var toRemove in RemQueue)
|
||||
{
|
||||
RemComp<RecentlyInjectedComponent>(toRemove);
|
||||
}
|
||||
RemQueue.Clear();
|
||||
foreach (var entity in EntityQuery<RecentlyInjectedComponent>())
|
||||
{
|
||||
entity.Accumulator += frameTime;
|
||||
if (entity.Accumulator < entity.RemoveTime.TotalSeconds)
|
||||
continue;
|
||||
entity.Accumulator = 0;
|
||||
RemQueue.Enqueue(entity.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
Content.Server/AI/Utility/Actions/Bots/InjectNearby.cs
Normal file
57
Content.Server/AI/Utility/Actions/Bots/InjectNearby.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Generic;
|
||||
using Content.Server.AI.Operators.Movement;
|
||||
using Content.Server.AI.Operators.Bots;
|
||||
using Content.Server.AI.Operators.Speech;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.Utility.Considerations.Containers;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.ActionBlocker;
|
||||
using Content.Server.AI.WorldState.States.Movement;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.Utility.Considerations.Bot;
|
||||
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Bots
|
||||
{
|
||||
public sealed class InjectNearby : UtilityAction
|
||||
{
|
||||
public EntityUid Target { get; set; } = default!;
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
MoveToEntityOperator moveOperator = new MoveToEntityOperator(Owner, Target);
|
||||
float waitTime = 3f;
|
||||
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
moveOperator,
|
||||
new SpeakOperator(Owner, Loc.GetString("medibot-start-inject")),
|
||||
new WaitOperator(waitTime),
|
||||
new InjectOperator(Owner, Target),
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<TargetEntityState>().SetValue(Target);
|
||||
context.GetState<MoveTargetState>().SetValue(Target);
|
||||
}
|
||||
|
||||
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
|
||||
{
|
||||
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
|
||||
|
||||
return new[]
|
||||
{
|
||||
considerationsManager.Get<CanMoveCon>()
|
||||
.BoolCurve(context),
|
||||
considerationsManager.Get<TargetAccessibleCon>()
|
||||
.BoolCurve(context),
|
||||
considerationsManager.Get<CanInjectCon>()
|
||||
.BoolCurve(context),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
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;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Utility;
|
||||
using Content.Server.CPUJob.JobQueues;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Content.Server.AI.Utility.AiLogic
|
||||
{
|
||||
// TODO: Need to split out the IMover stuff for NPC to a generic one that can be used for hoomans as well.
|
||||
[RegisterComponent]
|
||||
[ComponentProtoName("UtilityAI")]
|
||||
[ComponentReference(typeof(AiControllerComponent))]
|
||||
public sealed class UtilityAi : AiControllerComponent
|
||||
{
|
||||
// TODO: Look at having ParallelOperators (probably no more than that as then you'd have a full-blown BT)
|
||||
// Also RepeatOperators (e.g. if we're following an entity keep repeating MoveToEntity)
|
||||
private AiActionSystem _planner = default!;
|
||||
public Blackboard Blackboard => _blackboard;
|
||||
private Blackboard _blackboard = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The sum of all BehaviorSets gives us what actions the AI can take
|
||||
/// </summary>
|
||||
[DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<BehaviorSetPrototype>))]
|
||||
public HashSet<string> BehaviorSets { get; } = new();
|
||||
|
||||
public List<IAiUtility> AvailableActions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The currently running action; most importantly are the operators.
|
||||
/// </summary>
|
||||
public UtilityAction? CurrentAction { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown,
|
||||
/// or if there's no players nearby increase it.
|
||||
/// </summary>
|
||||
public float PlanCooldown { get; } = 0.5f;
|
||||
private float _planCooldownRemaining;
|
||||
|
||||
/// <summary>
|
||||
/// If we've requested a plan then wait patiently for the action
|
||||
/// </summary>
|
||||
private AiActionRequestJob? _actionRequest;
|
||||
|
||||
private CancellationTokenSource? _actionCancellation;
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
if (BehaviorSets.Count > 0)
|
||||
{
|
||||
var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
|
||||
behaviorManager.RebuildActions(this);
|
||||
EntitySystem.Get<NPCSystem>().WakeNPC(this);
|
||||
}
|
||||
|
||||
base.Initialize();
|
||||
_planCooldownRemaining = PlanCooldown;
|
||||
_blackboard = new Blackboard(Owner);
|
||||
_planner = EntitySystem.Get<AiActionSystem>();
|
||||
}
|
||||
|
||||
protected override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
var currentOp = CurrentAction?.ActionOperators.Peek();
|
||||
currentOp?.Shutdown(Outcome.Failed);
|
||||
CurrentAction?.Shutdown();
|
||||
CurrentAction = null;
|
||||
}
|
||||
|
||||
private void ReceivedAction()
|
||||
{
|
||||
if (_actionRequest == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (_actionRequest.Exception)
|
||||
{
|
||||
case null:
|
||||
break;
|
||||
default:
|
||||
Logger.FatalS("ai", _actionRequest.Exception.ToString());
|
||||
ExceptionDispatchInfo.Capture(_actionRequest.Exception).Throw();
|
||||
// The code never actually reaches here, because the above throws.
|
||||
// This is to tell the compiler that the flow never leaves here.
|
||||
throw _actionRequest.Exception;
|
||||
}
|
||||
var action = _actionRequest.Result;
|
||||
_actionRequest = null;
|
||||
// Actions with lower scores should be implicitly dumped by GetAction
|
||||
// If we're not allowed to replace the action with an action of the same type then dump.
|
||||
if (action == null || !action.CanOverride && CurrentAction?.GetType() == action.GetType())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentOp = CurrentAction?.ActionOperators.Peek();
|
||||
if (currentOp != null && currentOp.HasStartup)
|
||||
{
|
||||
currentOp.Shutdown(Outcome.Failed);
|
||||
}
|
||||
|
||||
CurrentAction = action;
|
||||
action.SetupOperators(_blackboard);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// If we asked for a new action we don't want to dump the existing one.
|
||||
if (_actionRequest != null)
|
||||
{
|
||||
if (_actionRequest.Status != JobStatus.Finished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReceivedAction();
|
||||
// Do something next tick
|
||||
return;
|
||||
}
|
||||
|
||||
_planCooldownRemaining -= frameTime;
|
||||
|
||||
// Might find a better action while we're doing one already
|
||||
if (_planCooldownRemaining <= 0.0f)
|
||||
{
|
||||
_planCooldownRemaining = PlanCooldown;
|
||||
_actionCancellation = new CancellationTokenSource();
|
||||
_actionRequest = _planner.RequestAction(new AiActionRequest(Owner, _blackboard, AvailableActions), _actionCancellation);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// When we spawn in we won't get an action for a bit
|
||||
if (CurrentAction == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var outcome = CurrentAction.Execute(frameTime);
|
||||
|
||||
switch (outcome)
|
||||
{
|
||||
case Outcome.Success:
|
||||
if (CurrentAction.ActionOperators.Count == 0)
|
||||
{
|
||||
CurrentAction.Shutdown();
|
||||
CurrentAction = null;
|
||||
// Nothing to compare new action to
|
||||
_blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
|
||||
}
|
||||
break;
|
||||
case Outcome.Continuing:
|
||||
break;
|
||||
case Outcome.Failed:
|
||||
CurrentAction.Shutdown();
|
||||
CurrentAction = null;
|
||||
_blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs
Normal file
46
Content.Server/AI/Utility/AiLogic/UtilityNPCComponent.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using Content.Server.AI.Components;
|
||||
using Content.Server.AI.LoadBalancer;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
using System.Threading;
|
||||
using Content.Server.AI.EntitySystems;
|
||||
|
||||
namespace Content.Server.AI.Utility.AiLogic
|
||||
{
|
||||
[RegisterComponent, Access(typeof(NPCSystem))]
|
||||
[ComponentReference(typeof(NPCComponent))]
|
||||
public sealed class UtilityNPCComponent : NPCComponent
|
||||
{
|
||||
public Blackboard Blackboard => _blackboard;
|
||||
public Blackboard _blackboard = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The sum of all BehaviorSets gives us what actions the AI can take
|
||||
/// </summary>
|
||||
[DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<BehaviorSetPrototype>))]
|
||||
public HashSet<string> BehaviorSets { get; } = new();
|
||||
|
||||
public List<IAiUtility> AvailableActions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The currently running action; most importantly are the operators.
|
||||
/// </summary>
|
||||
public UtilityAction? CurrentAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown,
|
||||
/// or if there's no players nearby increase it.
|
||||
/// </summary>
|
||||
public float PlanCooldown { get; } = 0.5f;
|
||||
|
||||
public float _planCooldownRemaining;
|
||||
|
||||
/// <summary>
|
||||
/// If we've requested a plan then wait patiently for the action
|
||||
/// </summary>
|
||||
public AiActionRequestJob? _actionRequest;
|
||||
|
||||
public CancellationTokenSource? _actionCancellation;
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ namespace Content.Server.AI.Utility.Considerations.Clothing
|
|||
continue;
|
||||
}
|
||||
|
||||
if ((clothingComponent.SlotFlags & slotDef.SlotFlags) != 0)
|
||||
if ((clothingComponent.Slots & slotDef.SlotFlags) != 0)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ namespace Content.Server.AI.Utility.Considerations.Inventory
|
|||
// If not then check if we have a free hand
|
||||
var target = context.GetState<TargetEntityState>().GetValue();
|
||||
|
||||
if (target == null || !IoCManager.Resolve<IEntityManager>().HasComponent<SharedItemComponent>(target))
|
||||
if (target == null || !IoCManager.Resolve<IEntityManager>().HasComponent<ItemComponent>(target))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Content.Server.AI.Utility.Considerations.Inventory
|
|||
{
|
||||
var target = context.GetState<TargetEntityState>().GetValue();
|
||||
|
||||
if (target == null || !IoCManager.Resolve<IEntityManager>().HasComponent<SharedItemComponent>(target))
|
||||
if (target == null || !IoCManager.Resolve<IEntityManager>().HasComponent<ItemComponent>(target))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves
|
|||
foreach (var entity in context.GetState<EnumerableInventoryState>().GetValue())
|
||||
{
|
||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out ClothingComponent? clothing) &&
|
||||
(clothing.SlotFlags & SlotFlags.GLOVES) != 0)
|
||||
(clothing.Slots & SlotFlags.GLOVES) != 0)
|
||||
{
|
||||
yield return new EquipGloves {Owner = owner, Target = entity, Bonus = Bonus};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves
|
|||
foreach (var entity in context.GetState<NearbyClothingState>().GetValue())
|
||||
{
|
||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out ClothingComponent? clothing) &&
|
||||
(clothing.SlotFlags & SlotFlags.GLOVES) != 0)
|
||||
(clothing.Slots & SlotFlags.GLOVES) != 0)
|
||||
{
|
||||
yield return new PickUpGloves {Owner = owner, Target = entity, Bonus = Bonus};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head
|
|||
foreach (var entity in context.GetState<EnumerableInventoryState>().GetValue())
|
||||
{
|
||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out ClothingComponent? clothing) &&
|
||||
(clothing.SlotFlags & SlotFlags.HEAD) != 0)
|
||||
(clothing.Slots & SlotFlags.HEAD) != 0)
|
||||
{
|
||||
yield return new EquipHead {Owner = owner, Target = entity, Bonus = Bonus};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head
|
|||
foreach (var entity in context.GetState<NearbyClothingState>().GetValue())
|
||||
{
|
||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out ClothingComponent? clothing) &&
|
||||
(clothing.SlotFlags & SlotFlags.HEAD) != 0)
|
||||
(clothing.Slots & SlotFlags.HEAD) != 0)
|
||||
{
|
||||
yield return new PickUpHead {Owner = owner, Target = entity, Bonus = Bonus};
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue