mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-14 23:14:45 +01:00
remote merge upstream
This commit is contained in:
3
Content.Benchmarks/GlobalUsings.cs
Normal file
3
Content.Benchmarks/GlobalUsings.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Global usings for Content.Benchmarks
|
||||
|
||||
global using Robust.UnitTesting.Pool;
|
||||
@@ -24,7 +24,7 @@ namespace Content.Client.Administration.UI.BanPanel;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BanPanel : DefaultWindow
|
||||
{
|
||||
public event Action<string?, (IPAddress, int)?, bool, ImmutableTypedHwid?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
|
||||
public event Action<Ban>? BanSubmitted;
|
||||
public event Action<string>? PlayerChanged;
|
||||
private string? PlayerUsername { get; set; }
|
||||
private (IPAddress, int)? IpAddress { get; set; }
|
||||
@@ -37,8 +37,8 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
// This is less efficient than just holding a reference to the root control and enumerating children, but you
|
||||
// have to know how the controls are nested, which makes the code more complicated.
|
||||
// Role group name -> the role buttons themselves.
|
||||
private readonly Dictionary<string, List<Button>> _roleCheckboxes = new();
|
||||
private readonly ISawmill _banpanelSawmill;
|
||||
private readonly Dictionary<string, List<(Button, IPrototype)>> _roleCheckboxes = new();
|
||||
private readonly ISawmill _banPanelSawmill;
|
||||
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
@@ -79,7 +79,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_banpanelSawmill = _logManager.GetSawmill("admin.banpanel");
|
||||
_banPanelSawmill = _logManager.GetSawmill("admin.banpanel");
|
||||
PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
|
||||
PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
|
||||
PlayerCheckbox.OnPressed += _ =>
|
||||
@@ -110,7 +110,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
TypeOption.SelectId(args.Id);
|
||||
OnTypeChanged();
|
||||
};
|
||||
LastConnCheckbox.OnPressed += args =>
|
||||
LastConnCheckbox.OnPressed += _ =>
|
||||
{
|
||||
IpLine.ModulateSelfOverride = null;
|
||||
HwidLine.ModulateSelfOverride = null;
|
||||
@@ -164,7 +164,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
|
||||
var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>()
|
||||
.OrderBy(x => x.ID);
|
||||
CreateRoleGroup("Antagonist", Color.Red, antagRoles);
|
||||
CreateRoleGroup(AntagPrototype.GroupName, AntagPrototype.GroupColor, antagRoles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,14 +236,14 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
{
|
||||
foreach (var role in _roleCheckboxes[groupName])
|
||||
{
|
||||
role.Pressed = args.Pressed;
|
||||
role.Item1.Pressed = args.Pressed;
|
||||
}
|
||||
|
||||
if (args.Pressed)
|
||||
{
|
||||
if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity))
|
||||
{
|
||||
_banpanelSawmill
|
||||
_banPanelSawmill
|
||||
.Warning("Departmental role ban severity could not be parsed from config!");
|
||||
return;
|
||||
}
|
||||
@@ -255,14 +255,14 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
{
|
||||
foreach (var button in roleButtons)
|
||||
{
|
||||
if (button.Pressed)
|
||||
if (button.Item1.Pressed)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity))
|
||||
{
|
||||
_banpanelSawmill
|
||||
_banPanelSawmill
|
||||
.Warning("Role ban severity could not be parsed from config!");
|
||||
return;
|
||||
}
|
||||
@@ -294,7 +294,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a checkbutton specifically for one "role" in a "group"
|
||||
/// Adds a check button specifically for one "role" in a "group"
|
||||
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
|
||||
/// </summary>
|
||||
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
|
||||
@@ -302,23 +302,36 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
var roleCheckboxContainer = new BoxContainer();
|
||||
var roleCheckButton = new Button
|
||||
{
|
||||
Name = $"{role}RoleCheckbox",
|
||||
Name = role,
|
||||
Text = role,
|
||||
ToggleMode = true,
|
||||
};
|
||||
roleCheckButton.OnToggled += args =>
|
||||
{
|
||||
// Checks the role group checkbox if all the children are pressed
|
||||
if (args.Pressed && _roleCheckboxes[group].All(e => e.Pressed))
|
||||
if (args.Pressed && _roleCheckboxes[group].All(e => e.Item1.Pressed))
|
||||
roleGroupCheckbox.Pressed = args.Pressed;
|
||||
else
|
||||
roleGroupCheckbox.Pressed = false;
|
||||
};
|
||||
|
||||
IPrototype rolePrototype;
|
||||
|
||||
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype))
|
||||
rolePrototype = jobPrototype;
|
||||
else if (_protoMan.TryIndex<AntagPrototype>(role, out var antagPrototype))
|
||||
rolePrototype = antagPrototype;
|
||||
else
|
||||
{
|
||||
_banPanelSawmill.Error($"Adding a role checkbox for role {role}: role is not a JobPrototype or AntagPrototype.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This is adding the icon before the role name
|
||||
// TODO: This should not be using raw strings for prototypes as it means it won't be validated at all.
|
||||
// I know the ban manager is doing the same thing, but that should not leak into UI code.
|
||||
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype) && _protoMan.Resolve(jobPrototype.Icon, out var iconProto))
|
||||
// // I know the ban manager is doing the same thing, but that should not leak into UI code.
|
||||
if (jobPrototype is not null && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
|
||||
{
|
||||
var jobIconTexture = new TextureRect
|
||||
{
|
||||
@@ -335,7 +348,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
|
||||
|
||||
_roleCheckboxes.TryAdd(group, []);
|
||||
_roleCheckboxes[group].Add(roleCheckButton);
|
||||
_roleCheckboxes[group].Add((roleCheckButton, rolePrototype));
|
||||
}
|
||||
|
||||
public void UpdateBanFlag(bool newFlag)
|
||||
@@ -488,7 +501,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
newSeverity = serverSeverity;
|
||||
else
|
||||
{
|
||||
_banpanelSawmill
|
||||
_banPanelSawmill
|
||||
.Warning("Server ban severity could not be parsed from config!");
|
||||
}
|
||||
|
||||
@@ -501,7 +514,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
}
|
||||
else
|
||||
{
|
||||
_banpanelSawmill
|
||||
_banPanelSawmill
|
||||
.Warning("Role ban severity could not be parsed from config!");
|
||||
}
|
||||
break;
|
||||
@@ -546,34 +559,51 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
|
||||
private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
string[]? roles = null;
|
||||
ProtoId<JobPrototype>[]? jobs = null;
|
||||
ProtoId<AntagPrototype>[]? antags = null;
|
||||
|
||||
if (TypeOption.SelectedId == (int) Types.Role)
|
||||
{
|
||||
var rolesList = new List<string>();
|
||||
var jobList = new List<ProtoId<JobPrototype>>();
|
||||
var antagList = new List<ProtoId<AntagPrototype>>();
|
||||
|
||||
if (_roleCheckboxes.Count == 0)
|
||||
throw new DebugAssertException("RoleCheckboxes was empty");
|
||||
|
||||
foreach (var button in _roleCheckboxes.Values.SelectMany(departmentButtons => departmentButtons))
|
||||
{
|
||||
if (button is { Pressed: true, Text: not null })
|
||||
if (button.Item1 is { Pressed: true, Name: not null })
|
||||
{
|
||||
rolesList.Add(button.Text);
|
||||
switch (button.Item2)
|
||||
{
|
||||
case JobPrototype:
|
||||
jobList.Add(button.Item2.ID);
|
||||
|
||||
break;
|
||||
case AntagPrototype:
|
||||
antagList.Add(button.Item2.ID);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rolesList.Count == 0)
|
||||
if (jobList.Count + antagList.Count == 0)
|
||||
{
|
||||
Tabs.CurrentTab = (int) TabNumbers.Roles;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
roles = rolesList.ToArray();
|
||||
jobs = jobList.ToArray();
|
||||
antags = antagList.ToArray();
|
||||
}
|
||||
|
||||
if (TypeOption.SelectedId == (int) Types.None)
|
||||
{
|
||||
TypeOption.ModulateSelfOverride = Color.Red;
|
||||
Tabs.CurrentTab = (int) TabNumbers.BasicInfo;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -585,6 +615,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
ReasonTextEdit.GrabKeyboardFocus();
|
||||
ReasonTextEdit.ModulateSelfOverride = Color.Red;
|
||||
ReasonTextEdit.OnKeyBindDown += ResetTextEditor;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -593,6 +624,7 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
|
||||
SubmitButton.ModulateSelfOverride = Color.Red;
|
||||
SubmitButton.Text = Loc.GetString("ban-panel-confirm");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -601,7 +633,22 @@ public sealed partial class BanPanel : DefaultWindow
|
||||
var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null;
|
||||
var severity = (NoteSeverity) SeverityOption.SelectedId;
|
||||
var erase = EraseCheckbox.Pressed;
|
||||
BanSubmitted?.Invoke(player, IpAddress, useLastIp, Hwid, useLastHwid, (uint) (TimeEntered * Multiplier), reason, severity, roles, erase);
|
||||
|
||||
var ban = new Ban(
|
||||
player,
|
||||
IpAddress,
|
||||
useLastIp,
|
||||
Hwid,
|
||||
useLastHwid,
|
||||
(uint)(TimeEntered * Multiplier),
|
||||
reason,
|
||||
severity,
|
||||
jobs,
|
||||
antags,
|
||||
erase
|
||||
);
|
||||
|
||||
BanSubmitted?.Invoke(ban);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
|
||||
@@ -14,8 +14,7 @@ public sealed class BanPanelEui : BaseEui
|
||||
{
|
||||
BanPanel = new BanPanel();
|
||||
BanPanel.OnClose += () => SendMessage(new CloseEuiMessage());
|
||||
BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase)
|
||||
=> SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase));
|
||||
BanPanel.BanSubmitted += ban => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(ban));
|
||||
BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player));
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,11 @@ public sealed partial class AdminNotesLine : BoxContainer
|
||||
|
||||
if (Note.UnbannedTime is not null)
|
||||
{
|
||||
ExtraLabel.Text = Loc.GetString("admin-notes-unbanned", ("admin", Note.UnbannedByName ?? "[error]"), ("date", Note.UnbannedTime));
|
||||
ExtraLabel.Text = Loc.GetString(
|
||||
"admin-notes-unbanned",
|
||||
("admin", Note.UnbannedByName ?? "[error]"),
|
||||
("date", Note.UnbannedTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))
|
||||
);
|
||||
ExtraLabel.Visible = true;
|
||||
}
|
||||
else if (Note.ExpiryTime is not null)
|
||||
@@ -139,7 +143,7 @@ public sealed partial class AdminNotesLine : BoxContainer
|
||||
|
||||
private string FormatRoleBanMessage()
|
||||
{
|
||||
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new []{"unknown"})} ");
|
||||
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} ");
|
||||
return FormatBanMessageCommon(banMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ public sealed partial class ThresholdBoundControl : BoxContainer
|
||||
public void SetValue(float value)
|
||||
{
|
||||
_value = value;
|
||||
CSpinner.Value = ScaledValue;
|
||||
if (!CSpinner.HasKeyboardFocus())
|
||||
{
|
||||
CSpinner.Value = ScaledValue;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetEnabled(bool enabled)
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
|
||||
comp.OpeningAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(comp.OpeningAnimationTime),
|
||||
Length = comp.OpeningAnimationTime,
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick
|
||||
@@ -47,7 +47,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
|
||||
comp.ClosingAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(comp.ClosingAnimationTime),
|
||||
Length = comp.ClosingAnimationTime,
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick
|
||||
@@ -63,7 +63,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
|
||||
comp.EmaggingAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(comp.EmaggingAnimationTime),
|
||||
Length = comp.EmaggingAnimationTime,
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick
|
||||
@@ -116,14 +116,14 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
|
||||
return;
|
||||
case DoorState.Opening:
|
||||
if (entity.Comp.OpeningAnimationTime == 0.0)
|
||||
if (entity.Comp.OpeningAnimationTime == TimeSpan.Zero)
|
||||
return;
|
||||
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.AnimationKey);
|
||||
|
||||
return;
|
||||
case DoorState.Closing:
|
||||
if (entity.Comp.ClosingAnimationTime == 0.0 || entity.Comp.CurrentlyCrushing.Count != 0)
|
||||
if (entity.Comp.ClosingAnimationTime == TimeSpan.Zero || entity.Comp.CurrentlyCrushing.Count != 0)
|
||||
return;
|
||||
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.AnimationKey);
|
||||
|
||||
90
Content.Client/Graphics/OverlayResourceCache.cs
Normal file
90
Content.Client/Graphics/OverlayResourceCache.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// A cache for <see cref="Overlay"/>s to store per-viewport render resources, such as render targets.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of data stored in the cache.</typeparam>
|
||||
public sealed class OverlayResourceCache<T> : IDisposable where T : class, IDisposable
|
||||
{
|
||||
private readonly Dictionary<long, CacheEntry> _cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Get the data for a specific viewport, creating a new entry if necessary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The cached data may be cleared at any time if <see cref="IClydeViewport.ClearCachedResources"/> gets invoked.
|
||||
/// </remarks>
|
||||
/// <param name="viewport">The viewport for which to retrieve cached data.</param>
|
||||
/// <param name="factory">A delegate used to create the cached data, if necessary.</param>
|
||||
public T GetForViewport(IClydeViewport viewport, Func<IClydeViewport, T> factory)
|
||||
{
|
||||
return GetForViewport(viewport, out _, factory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the data for a specific viewport, creating a new entry if necessary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The cached data may be cleared at any time if <see cref="IClydeViewport.ClearCachedResources"/> gets invoked.
|
||||
/// </remarks>
|
||||
/// <param name="viewport">The viewport for which to retrieve cached data.</param>
|
||||
/// <param name="wasCached">True if the data was pulled from cache, false if it was created anew.</param>
|
||||
/// <param name="factory">A delegate used to create the cached data, if necessary.</param>
|
||||
public T GetForViewport(IClydeViewport viewport, out bool wasCached, Func<IClydeViewport, T> factory)
|
||||
{
|
||||
if (_cache.TryGetValue(viewport.Id, out var entry))
|
||||
{
|
||||
wasCached = true;
|
||||
return entry.Data;
|
||||
}
|
||||
|
||||
wasCached = false;
|
||||
|
||||
entry = new CacheEntry
|
||||
{
|
||||
Data = factory(viewport),
|
||||
Viewport = new WeakReference<IClydeViewport>(viewport),
|
||||
};
|
||||
_cache.Add(viewport.Id, entry);
|
||||
|
||||
viewport.ClearCachedResources += ViewportOnClearCachedResources;
|
||||
|
||||
return entry.Data;
|
||||
}
|
||||
|
||||
private void ViewportOnClearCachedResources(ClearCachedViewportResourcesEvent ev)
|
||||
{
|
||||
if (!_cache.Remove(ev.ViewportId, out var entry))
|
||||
{
|
||||
// I think this could theoretically happen if you manually dispose the cache *after* a leaked viewport got
|
||||
// GC'd, but before its ClearCachedResources got invoked.
|
||||
return;
|
||||
}
|
||||
|
||||
entry.Data.Dispose();
|
||||
|
||||
if (ev.Viewport != null)
|
||||
ev.Viewport.ClearCachedResources -= ViewportOnClearCachedResources;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var entry in _cache)
|
||||
{
|
||||
if (entry.Value.Viewport.TryGetTarget(out var viewport))
|
||||
viewport.ClearCachedResources -= ViewportOnClearCachedResources;
|
||||
|
||||
entry.Value.Data.Dispose();
|
||||
}
|
||||
|
||||
_cache.Clear();
|
||||
}
|
||||
|
||||
private struct CacheEntry
|
||||
{
|
||||
public T Data;
|
||||
public WeakReference<IClydeViewport> Viewport;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ public sealed class AfterLightTargetOverlay : Overlay
|
||||
return;
|
||||
|
||||
var lightOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
|
||||
var lightRes = lightOverlay.GetCachedForViewport(args.Viewport);
|
||||
var bounds = args.WorldBounds;
|
||||
|
||||
// at 1-1 render scale it's mostly fine but at 4x4 it's way too fkn big
|
||||
@@ -38,7 +39,7 @@ public sealed class AfterLightTargetOverlay : Overlay
|
||||
|
||||
var localMatrix =
|
||||
viewport.LightRenderTarget.GetWorldToLocalMatrix(viewport.Eye, newScale);
|
||||
var diff = (lightOverlay.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
|
||||
var diff = (lightRes.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
|
||||
var halfDiff = diff / 2;
|
||||
|
||||
// Pixels -> Metres -> Half distance.
|
||||
@@ -53,7 +54,7 @@ public sealed class AfterLightTargetOverlay : Overlay
|
||||
viewport.LightRenderTarget.Size.Y + halfDiff.Y);
|
||||
|
||||
worldHandle.SetTransform(localMatrix);
|
||||
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
|
||||
worldHandle.DrawTextureRectRegion(lightRes.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
|
||||
}, Color.Transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Graphics;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -27,11 +28,7 @@ public sealed class AmbientOcclusionOverlay : Overlay
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
|
||||
|
||||
private IRenderTexture? _aoTarget;
|
||||
private IRenderTexture? _aoBlurBuffer;
|
||||
|
||||
// Couldn't figure out a way to avoid this so if you can then please do.
|
||||
private IRenderTexture? _aoStencilTarget;
|
||||
private readonly OverlayResourceCache<CachedResources> _resources = new ();
|
||||
|
||||
public AmbientOcclusionOverlay()
|
||||
{
|
||||
@@ -69,30 +66,32 @@ public sealed class AmbientOcclusionOverlay : Overlay
|
||||
var turfSystem = _entManager.System<TurfSystem>();
|
||||
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
|
||||
|
||||
if (_aoTarget?.Texture.Size != target.Size)
|
||||
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
||||
|
||||
if (res.AOTarget?.Texture.Size != target.Size)
|
||||
{
|
||||
_aoTarget?.Dispose();
|
||||
_aoTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
|
||||
res.AOTarget?.Dispose();
|
||||
res.AOTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
|
||||
}
|
||||
|
||||
if (_aoBlurBuffer?.Texture.Size != target.Size)
|
||||
if (res.AOBlurBuffer?.Texture.Size != target.Size)
|
||||
{
|
||||
_aoBlurBuffer?.Dispose();
|
||||
_aoBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
|
||||
res.AOBlurBuffer?.Dispose();
|
||||
res.AOBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
|
||||
}
|
||||
|
||||
if (_aoStencilTarget?.Texture.Size != target.Size)
|
||||
if (res.AOStencilTarget?.Texture.Size != target.Size)
|
||||
{
|
||||
_aoStencilTarget?.Dispose();
|
||||
_aoStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
|
||||
res.AOStencilTarget?.Dispose();
|
||||
res.AOStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
|
||||
}
|
||||
|
||||
// Draw the texture data to the texture.
|
||||
args.WorldHandle.RenderInRenderTarget(_aoTarget,
|
||||
args.WorldHandle.RenderInRenderTarget(res.AOTarget,
|
||||
() =>
|
||||
{
|
||||
worldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
|
||||
var invMatrix = _aoTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
|
||||
var invMatrix = res.AOTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
|
||||
|
||||
foreach (var entry in query.QueryAabb(mapId, worldBounds))
|
||||
{
|
||||
@@ -106,11 +105,11 @@ public sealed class AmbientOcclusionOverlay : Overlay
|
||||
}
|
||||
}, Color.Transparent);
|
||||
|
||||
_clyde.BlurRenderTarget(viewport, _aoTarget, _aoBlurBuffer, viewport.Eye!, 14f);
|
||||
_clyde.BlurRenderTarget(viewport, res.AOTarget, res.AOBlurBuffer, viewport.Eye!, 14f);
|
||||
|
||||
// Need to do stencilling after blur as it will nuke it.
|
||||
// Draw stencil for the grid so we don't draw in space.
|
||||
args.WorldHandle.RenderInRenderTarget(_aoStencilTarget,
|
||||
args.WorldHandle.RenderInRenderTarget(res.AOStencilTarget,
|
||||
() =>
|
||||
{
|
||||
// Don't want lighting affecting it.
|
||||
@@ -136,13 +135,36 @@ public sealed class AmbientOcclusionOverlay : Overlay
|
||||
|
||||
// Draw the stencil texture to depth buffer.
|
||||
worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
|
||||
worldHandle.DrawTextureRect(_aoStencilTarget!.Texture, worldBounds);
|
||||
worldHandle.DrawTextureRect(res.AOStencilTarget!.Texture, worldBounds);
|
||||
|
||||
// Draw the Blurred AO texture finally.
|
||||
worldHandle.UseShader(_proto.Index(StencilEqualDrawShader).Instance());
|
||||
worldHandle.DrawTextureRect(_aoTarget!.Texture, worldBounds, color);
|
||||
worldHandle.DrawTextureRect(res.AOTarget!.Texture, worldBounds, color);
|
||||
|
||||
args.WorldHandle.SetTransform(Matrix3x2.Identity);
|
||||
args.WorldHandle.UseShader(null);
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_resources.Dispose();
|
||||
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
private sealed class CachedResources : IDisposable
|
||||
{
|
||||
public IRenderTexture? AOTarget;
|
||||
public IRenderTexture? AOBlurBuffer;
|
||||
|
||||
// Couldn't figure out a way to avoid this so if you can then please do.
|
||||
public IRenderTexture? AOStencilTarget;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
AOTarget?.Dispose();
|
||||
AOBlurBuffer?.Dispose();
|
||||
AOStencilTarget?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Graphics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
|
||||
@@ -13,7 +13,8 @@ public sealed class BeforeLightTargetOverlay : Overlay
|
||||
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
public IRenderTexture EnlargedLightTarget = default!;
|
||||
private readonly OverlayResourceCache<CachedResources> _resources = new();
|
||||
|
||||
public Box2Rotated EnlargedBounds;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,16 +37,42 @@ public sealed class BeforeLightTargetOverlay : Overlay
|
||||
var size = args.Viewport.LightRenderTarget.Size + (int) (_skirting * EyeManager.PixelsPerMeter);
|
||||
EnlargedBounds = args.WorldBounds.Enlarged(_skirting / 2f);
|
||||
|
||||
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
||||
|
||||
// This just exists to copy the lightrendertarget and write back to it.
|
||||
if (EnlargedLightTarget?.Size != size)
|
||||
if (res.EnlargedLightTarget?.Size != size)
|
||||
{
|
||||
EnlargedLightTarget = _clyde
|
||||
res.EnlargedLightTarget = _clyde
|
||||
.CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-copy");
|
||||
}
|
||||
|
||||
args.WorldHandle.RenderInRenderTarget(EnlargedLightTarget,
|
||||
args.WorldHandle.RenderInRenderTarget(res.EnlargedLightTarget,
|
||||
() =>
|
||||
{
|
||||
}, _clyde.GetClearColor(args.MapUid));
|
||||
}
|
||||
|
||||
internal CachedResources GetCachedForViewport(IClydeViewport viewport)
|
||||
{
|
||||
return _resources.GetForViewport(viewport,
|
||||
static _ => throw new InvalidOperationException(
|
||||
"Expected BeforeLightTargetOverlay to have created its resources"));
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_resources.Dispose();
|
||||
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
internal sealed class CachedResources : IDisposable
|
||||
{
|
||||
public IRenderTexture EnlargedLightTarget = default!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
EnlargedLightTarget?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Client.Graphics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
|
||||
@@ -15,7 +16,7 @@ public sealed class LightBlurOverlay : Overlay
|
||||
|
||||
public const int ContentZIndex = TileEmissionOverlay.ContentZIndex + 1;
|
||||
|
||||
private IRenderTarget? _blurTarget;
|
||||
private readonly OverlayResourceCache<CachedResources> _resources = new();
|
||||
|
||||
public LightBlurOverlay()
|
||||
{
|
||||
@@ -29,16 +30,36 @@ public sealed class LightBlurOverlay : Overlay
|
||||
return;
|
||||
|
||||
var beforeOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
|
||||
var size = beforeOverlay.EnlargedLightTarget.Size;
|
||||
var beforeLightRes = beforeOverlay.GetCachedForViewport(args.Viewport);
|
||||
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
||||
|
||||
if (_blurTarget?.Size != size)
|
||||
var size = beforeLightRes.EnlargedLightTarget.Size;
|
||||
|
||||
if (res.BlurTarget?.Size != size)
|
||||
{
|
||||
_blurTarget = _clyde
|
||||
res.BlurTarget = _clyde
|
||||
.CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-blur");
|
||||
}
|
||||
|
||||
var target = beforeOverlay.EnlargedLightTarget;
|
||||
var target = beforeLightRes.EnlargedLightTarget;
|
||||
// Yeah that's all this does keep walkin.
|
||||
_clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 5f);
|
||||
_clyde.BlurRenderTarget(args.Viewport, target, res.BlurTarget, args.Viewport.Eye, 14f * 5f);
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_resources.Dispose();
|
||||
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
private sealed class CachedResources : IDisposable
|
||||
{
|
||||
public IRenderTarget? BlurTarget;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BlurTarget?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,9 @@ public sealed class RoofOverlay : Overlay
|
||||
|
||||
var worldHandle = args.WorldHandle;
|
||||
var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
|
||||
var lightRes = lightoverlay.GetCachedForViewport(args.Viewport);
|
||||
var bounds = lightoverlay.EnlargedBounds;
|
||||
var target = lightoverlay.EnlargedLightTarget;
|
||||
var target = lightRes.EnlargedLightTarget;
|
||||
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(args.MapId, bounds, ref _grids, approx: true, includeMap: true);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Graphics;
|
||||
using Content.Shared.Light.Components;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
@@ -24,8 +25,7 @@ public sealed class SunShadowOverlay : Overlay
|
||||
|
||||
private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
|
||||
|
||||
private IRenderTexture? _blurTarget;
|
||||
private IRenderTexture? _target;
|
||||
private readonly OverlayResourceCache<CachedResources> _resources = new();
|
||||
|
||||
public SunShadowOverlay()
|
||||
{
|
||||
@@ -55,16 +55,18 @@ public sealed class SunShadowOverlay : Overlay
|
||||
var worldBounds = args.WorldBounds;
|
||||
var targetSize = viewport.LightRenderTarget.Size;
|
||||
|
||||
if (_target?.Size != targetSize)
|
||||
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
||||
|
||||
if (res.Target?.Size != targetSize)
|
||||
{
|
||||
_target = _clyde
|
||||
res.Target = _clyde
|
||||
.CreateRenderTarget(targetSize,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||
name: "sun-shadow-target");
|
||||
|
||||
if (_blurTarget?.Size != targetSize)
|
||||
if (res.BlurTarget?.Size != targetSize)
|
||||
{
|
||||
_blurTarget = _clyde
|
||||
res.BlurTarget = _clyde
|
||||
.CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
|
||||
}
|
||||
}
|
||||
@@ -93,11 +95,11 @@ public sealed class SunShadowOverlay : Overlay
|
||||
_shadows.Clear();
|
||||
|
||||
// Draw shadow polys to stencil
|
||||
args.WorldHandle.RenderInRenderTarget(_target,
|
||||
args.WorldHandle.RenderInRenderTarget(res.Target,
|
||||
() =>
|
||||
{
|
||||
var invMatrix =
|
||||
_target.GetWorldToLocalMatrix(eye, scale);
|
||||
res.Target.GetWorldToLocalMatrix(eye, scale);
|
||||
var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
|
||||
|
||||
// Go through shadows in range.
|
||||
@@ -142,7 +144,7 @@ public sealed class SunShadowOverlay : Overlay
|
||||
Color.Transparent);
|
||||
|
||||
// Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
|
||||
_clyde.BlurRenderTarget(viewport, _target, _blurTarget!, eye, 1f);
|
||||
_clyde.BlurRenderTarget(viewport, res.Target, res.BlurTarget!, eye, 1f);
|
||||
|
||||
// Draw stencil (see roofoverlay).
|
||||
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
|
||||
@@ -155,8 +157,27 @@ public sealed class SunShadowOverlay : Overlay
|
||||
var maskShader = _protoManager.Index(MixShader).Instance();
|
||||
worldHandle.UseShader(maskShader);
|
||||
|
||||
worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
|
||||
worldHandle.DrawTextureRect(res.Target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_resources.Dispose();
|
||||
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
private sealed class CachedResources : IDisposable
|
||||
{
|
||||
public IRenderTexture? BlurTarget;
|
||||
public IRenderTexture? Target;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BlurTarget?.Dispose();
|
||||
Target?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class TileEmissionOverlay : Overlay
|
||||
var worldHandle = args.WorldHandle;
|
||||
var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
|
||||
var bounds = lightoverlay.EnlargedBounds;
|
||||
var target = lightoverlay.EnlargedLightTarget;
|
||||
var target = lightoverlay.GetCachedForViewport(args.Viewport).EnlargedLightTarget;
|
||||
var viewport = args.Viewport;
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);
|
||||
|
||||
@@ -186,10 +186,10 @@ namespace Content.Client.Lobby
|
||||
else
|
||||
{
|
||||
Lobby!.StartTime.Text = string.Empty;
|
||||
Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
|
||||
Lobby!.ReadyButton.Text = Loc.GetString(Lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready");
|
||||
Lobby!.ReadyButton.ToggleMode = true;
|
||||
Lobby!.ReadyButton.Disabled = false;
|
||||
Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
|
||||
Lobby!.ObserveButton.Disabled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -678,8 +678,10 @@ namespace Content.Client.Lobby.UI
|
||||
selector.Setup(items, title, 250, description, guides: antag.Guides);
|
||||
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
|
||||
|
||||
var requirements = _entManager.System<SharedRoleSystem>().GetAntagRequirement(antag);
|
||||
if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
|
||||
if (!_requirements.IsAllowed(
|
||||
antag,
|
||||
(HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter,
|
||||
out var reason))
|
||||
{
|
||||
selector.LockRequirements(reason);
|
||||
Profile = Profile?.WithAntagPreference(antag.ID, false);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Client.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class DrinkSystem : SharedDrinkSystem
|
||||
{
|
||||
}
|
||||
@@ -7,7 +7,11 @@ namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class StencilOverlay
|
||||
{
|
||||
private void DrawRestrictedRange(in OverlayDrawArgs args, RestrictedRangeComponent rangeComp, Matrix3x2 invMatrix)
|
||||
private void DrawRestrictedRange(
|
||||
in OverlayDrawArgs args,
|
||||
CachedResources res,
|
||||
RestrictedRangeComponent rangeComp,
|
||||
Matrix3x2 invMatrix)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var renderScale = args.Viewport.RenderScale.X;
|
||||
@@ -38,7 +42,7 @@ public sealed partial class StencilOverlay
|
||||
// Cut out the irrelevant bits via stencil
|
||||
// This is why we don't just use parallax; we might want specific tiles to get drawn over
|
||||
// particularly for planet maps or stations.
|
||||
worldHandle.RenderInRenderTarget(_blep!, () =>
|
||||
worldHandle.RenderInRenderTarget(res.Blep!, () =>
|
||||
{
|
||||
worldHandle.UseShader(_shader);
|
||||
worldHandle.DrawRect(localAABB, Color.White);
|
||||
@@ -46,7 +50,7 @@ public sealed partial class StencilOverlay
|
||||
|
||||
worldHandle.SetTransform(Matrix3x2.Identity);
|
||||
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
|
||||
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
|
||||
worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
|
||||
var curTime = _timing.RealTime;
|
||||
var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(new ResPath("/Textures/Parallaxes/noise.png")), curTime);
|
||||
|
||||
|
||||
@@ -11,7 +11,12 @@ public sealed partial class StencilOverlay
|
||||
{
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
|
||||
private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha, Matrix3x2 invMatrix)
|
||||
private void DrawWeather(
|
||||
in OverlayDrawArgs args,
|
||||
CachedResources res,
|
||||
WeatherPrototype weatherProto,
|
||||
float alpha,
|
||||
Matrix3x2 invMatrix)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var mapId = args.MapId;
|
||||
@@ -22,7 +27,7 @@ public sealed partial class StencilOverlay
|
||||
// Cut out the irrelevant bits via stencil
|
||||
// This is why we don't just use parallax; we might want specific tiles to get drawn over
|
||||
// particularly for planet maps or stations.
|
||||
worldHandle.RenderInRenderTarget(_blep!, () =>
|
||||
worldHandle.RenderInRenderTarget(res.Blep!, () =>
|
||||
{
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
_grids.Clear();
|
||||
@@ -56,7 +61,7 @@ public sealed partial class StencilOverlay
|
||||
|
||||
worldHandle.SetTransform(Matrix3x2.Identity);
|
||||
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
|
||||
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
|
||||
worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
|
||||
var curTime = _timing.RealTime;
|
||||
var sprite = _sprite.GetFrame(weatherProto.Sprite, curTime);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Graphics;
|
||||
using Content.Client.Parallax;
|
||||
using Content.Client.Weather;
|
||||
using Content.Shared.Salvage;
|
||||
@@ -34,7 +35,7 @@ public sealed partial class StencilOverlay : Overlay
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
|
||||
private IRenderTexture? _blep;
|
||||
private readonly OverlayResourceCache<CachedResources> _resources = new();
|
||||
|
||||
private readonly ShaderInstance _shader;
|
||||
|
||||
@@ -55,10 +56,12 @@ public sealed partial class StencilOverlay : Overlay
|
||||
var mapUid = _map.GetMapOrInvalid(args.MapId);
|
||||
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
|
||||
|
||||
if (_blep?.Texture.Size != args.Viewport.Size)
|
||||
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
||||
|
||||
if (res.Blep?.Texture.Size != args.Viewport.Size)
|
||||
{
|
||||
_blep?.Dispose();
|
||||
_blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
|
||||
res.Blep?.Dispose();
|
||||
res.Blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
|
||||
}
|
||||
|
||||
if (_entManager.TryGetComponent<WeatherComponent>(mapUid, out var comp))
|
||||
@@ -69,16 +72,33 @@ public sealed partial class StencilOverlay : Overlay
|
||||
continue;
|
||||
|
||||
var alpha = _weather.GetPercent(weather, mapUid);
|
||||
DrawWeather(args, weatherProto, alpha, invMatrix);
|
||||
DrawWeather(args, res, weatherProto, alpha, invMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
if (_entManager.TryGetComponent<RestrictedRangeComponent>(mapUid, out var restrictedRangeComponent))
|
||||
{
|
||||
DrawRestrictedRange(args, restrictedRangeComponent, invMatrix);
|
||||
DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix);
|
||||
}
|
||||
|
||||
args.WorldHandle.UseShader(null);
|
||||
args.WorldHandle.SetTransform(Matrix3x2.Identity);
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_resources.Dispose();
|
||||
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
private sealed class CachedResources : IDisposable
|
||||
{
|
||||
public IRenderTexture? Blep;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Blep?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Lobby;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Players.JobWhitelist;
|
||||
@@ -26,7 +25,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
[Dependency] private readonly IPrototypeManager _prototypes = default!;
|
||||
|
||||
private readonly Dictionary<string, TimeSpan> _roles = new();
|
||||
private readonly List<string> _roleBans = new();
|
||||
private readonly List<string> _jobBans = new();
|
||||
private readonly List<string> _antagBans = new();
|
||||
private readonly List<string> _jobWhitelists = new();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
@@ -52,16 +52,19 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
// Reset on disconnect, just in case.
|
||||
_roles.Clear();
|
||||
_jobWhitelists.Clear();
|
||||
_roleBans.Clear();
|
||||
_jobBans.Clear();
|
||||
_antagBans.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void RxRoleBans(MsgRoleBans message)
|
||||
{
|
||||
_sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries.");
|
||||
_sawmill.Debug($"Received role ban info: {message.JobBans.Count} job ban entries and {message.AntagBans.Count} antag ban entries.");
|
||||
|
||||
_roleBans.Clear();
|
||||
_roleBans.AddRange(message.Bans);
|
||||
_jobBans.Clear();
|
||||
_jobBans.AddRange(message.JobBans);
|
||||
_antagBans.Clear();
|
||||
_antagBans.AddRange(message.AntagBans);
|
||||
Updated?.Invoke();
|
||||
}
|
||||
|
||||
@@ -90,33 +93,97 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
Updated?.Invoke();
|
||||
}
|
||||
|
||||
public bool IsAllowed(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
/// <summary>
|
||||
/// Check a list of job- and antag prototypes against the current player, for requirements and bans.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// False if any of the prototypes are banned or have unmet requirements.
|
||||
/// </returns>>
|
||||
public bool IsAllowed(
|
||||
List<ProtoId<JobPrototype>>? jobs,
|
||||
List<ProtoId<AntagPrototype>>? antags,
|
||||
HumanoidCharacterProfile? profile,
|
||||
[NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
reason = null;
|
||||
|
||||
if (_roleBans.Contains($"Job:{job.ID}"))
|
||||
if (antags is not null)
|
||||
{
|
||||
foreach (var proto in antags)
|
||||
{
|
||||
if (!IsAllowed(_prototypes.Index(proto), profile, out reason))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (jobs is not null)
|
||||
{
|
||||
foreach (var proto in jobs)
|
||||
{
|
||||
if (!IsAllowed(_prototypes.Index(proto), profile, out reason))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check the job prototype against the current player, for requirements and bans
|
||||
/// </summary>
|
||||
public bool IsAllowed(
|
||||
JobPrototype job,
|
||||
HumanoidCharacterProfile? profile,
|
||||
[NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
// Check the player's bans
|
||||
if (_jobBans.Contains(job.ID))
|
||||
{
|
||||
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check whitelist requirements
|
||||
if (!CheckWhitelist(job, out reason))
|
||||
return false;
|
||||
|
||||
var player = _playerManager.LocalSession;
|
||||
if (player == null)
|
||||
return true;
|
||||
// Check other role requirements
|
||||
var reqs = _entManager.System<SharedRoleSystem>().GetRoleRequirements(job);
|
||||
if (!CheckRoleRequirements(reqs, profile, out reason))
|
||||
return false;
|
||||
|
||||
return CheckRoleRequirements(job, profile, out reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CheckRoleRequirements(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
/// <summary>
|
||||
/// Check the antag prototype against the current player, for requirements and bans
|
||||
/// </summary>
|
||||
public bool IsAllowed(
|
||||
AntagPrototype antag,
|
||||
HumanoidCharacterProfile? profile,
|
||||
[NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
var reqs = _entManager.System<SharedRoleSystem>().GetJobRequirement(job);
|
||||
return CheckRoleRequirements(reqs, profile, out reason);
|
||||
// Check the player's bans
|
||||
if (_antagBans.Contains(antag.ID))
|
||||
{
|
||||
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check whitelist requirements
|
||||
if (!CheckWhitelist(antag, out reason))
|
||||
return false;
|
||||
|
||||
// Check other role requirements
|
||||
var reqs = _entManager.System<SharedRoleSystem>().GetRoleRequirements(antag);
|
||||
if (!CheckRoleRequirements(reqs, profile, out reason))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CheckRoleRequirements(HashSet<JobRequirement>? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
// This must be private so code paths can't accidentally skip requirement overrides. Call this through IsAllowed()
|
||||
private bool CheckRoleRequirements(HashSet<JobRequirement>? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
reason = null;
|
||||
|
||||
@@ -151,6 +218,15 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CheckWhitelist(AntagPrototype antag, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
reason = default;
|
||||
|
||||
// TODO: Implement antag whitelisting.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public TimeSpan FetchOverallPlaytime()
|
||||
{
|
||||
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
|
||||
|
||||
@@ -125,7 +125,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
|
||||
var name = Loc.GetString(proto.SetName);
|
||||
|
||||
if (proto.Prototype != null &&
|
||||
_prototypeManager.Resolve(proto.Prototype, out var entProto))
|
||||
_prototypeManager.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile
|
||||
{
|
||||
name = entProto.Name;
|
||||
}
|
||||
@@ -144,7 +144,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
|
||||
|
||||
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject
|
||||
&& proto.Prototype != null
|
||||
&& _prototypeManager.Resolve(proto.Prototype, out var entProto))
|
||||
&& _prototypeManager.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile
|
||||
{
|
||||
tooltip = Loc.GetString(entProto.Name);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Silicons.StationAi;
|
||||
|
||||
public sealed class StationAiFixerConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
private StationAiFixerConsoleWindow? _window;
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindow<StationAiFixerConsoleWindow>();
|
||||
_window.SetOwner(Owner);
|
||||
|
||||
_window.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage;
|
||||
_window.OpenConfirmationDialogAction += OpenConfirmationDialog;
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
base.Update();
|
||||
_window?.UpdateState();
|
||||
}
|
||||
|
||||
private void OpenConfirmationDialog()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
_window.ConfirmationDialog?.Close();
|
||||
_window.ConfirmationDialog = new StationAiFixerConsoleConfirmationDialog();
|
||||
_window.ConfirmationDialog.OpenCentered();
|
||||
_window.ConfirmationDialog.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage;
|
||||
}
|
||||
|
||||
private void SendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
|
||||
{
|
||||
SendPredictedMessage(new StationAiFixerConsoleMessage(action));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'station-ai-fixer-console-window-purge-warning-title'}"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="400">
|
||||
<RichTextLabel Name="PurgeWarningLabel1" Margin="20 10 20 0"/>
|
||||
<RichTextLabel Name="PurgeWarningLabel2" Margin="20 10 20 0"/>
|
||||
<RichTextLabel Name="PurgeWarningLabel3" Margin="20 10 20 10"/>
|
||||
<BoxContainer HorizontalExpand="True">
|
||||
<Button Name="CancelPurge"
|
||||
Text="{Loc 'station-ai-fixer-console-window-cancel-action'}"
|
||||
SetWidth="150"
|
||||
Margin="20 10 0 10"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Button Name="ContinuePurge"
|
||||
Text="{Loc 'station-ai-fixer-console-window-continue-action'}"
|
||||
SetWidth="150"
|
||||
Margin="0 10 20 10"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,30 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Silicons.StationAi;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StationAiFixerConsoleConfirmationDialog : FancyWindow
|
||||
{
|
||||
public event Action<StationAiFixerConsoleAction>? SendStationAiFixerConsoleMessageAction;
|
||||
|
||||
public StationAiFixerConsoleConfirmationDialog()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
PurgeWarningLabel1.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-1"));
|
||||
PurgeWarningLabel2.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-2"));
|
||||
PurgeWarningLabel3.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-3"));
|
||||
|
||||
CancelPurge.OnButtonDown += _ => Close();
|
||||
ContinuePurge.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Purge);
|
||||
}
|
||||
|
||||
public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
|
||||
{
|
||||
SendStationAiFixerConsoleMessageAction?.Invoke(action);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Silicons.StationAi;
|
||||
|
||||
public sealed partial class StationAiFixerConsoleSystem : SharedStationAiFixerConsoleSystem
|
||||
{
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StationAiFixerConsoleComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
||||
}
|
||||
|
||||
private void OnAppearanceChange(Entity<StationAiFixerConsoleComponent> ent, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (_userInterface.TryGetOpenUi(ent.Owner, StationAiFixerConsoleUiKey.Key, out var bui))
|
||||
{
|
||||
bui?.Update<StationAiFixerConsoleBoundUserInterfaceState>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'station-ai-fixer-console-window'}"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Horizontal">
|
||||
|
||||
<!-- Left side - AI display -->
|
||||
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical" MinWidth="225" Margin="20 15 20 20">
|
||||
|
||||
<!-- AI panel -->
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- AI name -->
|
||||
<Label Name="StationAiNameLabel"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 5 0 0"
|
||||
Text="{Loc 'station-ai-fixer-console-window-no-station-ai'}"/>
|
||||
|
||||
<!-- AI portrait -->
|
||||
<AnimatedTextureRect Name="StationAiPortraitTexture" VerticalAlignment="Center" SetSize="128 128" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- AI status panel-->
|
||||
<PanelContainer Name="StationAiStatus">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#757575" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<!-- AI name -->
|
||||
<Label Name="StationAiStatusLabel"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Loc 'station-ai-fixer-console-window-no-station-ai-status'}"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Central divider -->
|
||||
<PanelContainer StyleClasses="LowDivider" VerticalExpand="True" Margin="0 0 0 0" SetWidth="2"/>
|
||||
|
||||
<!-- Right side - control panel -->
|
||||
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical" MinWidth="225" Margin="10 10 10 10">
|
||||
|
||||
<!-- Locked controls -->
|
||||
<BoxContainer Name="LockScreen"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Orientation="Vertical"
|
||||
ReservesSpace="False">
|
||||
|
||||
<controls:StripeBack VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 5">
|
||||
<PanelContainer VerticalExpand="True" HorizontalExpand="True">
|
||||
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical">
|
||||
<Control VerticalExpand="True"/>
|
||||
<TextureRect VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
SetSize="64 64"
|
||||
Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/VerbIcons/lock.svg.192dpi.png">
|
||||
</TextureRect>
|
||||
<Label Text="{Loc 'station-ai-fixer-console-window-controls-locked'}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 5 0 0"/>
|
||||
<Control VerticalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:StripeBack>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Action progress screen -->
|
||||
<BoxContainer Name="ActionProgressScreen"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Orientation="Vertical"
|
||||
ReservesSpace="False"
|
||||
Visible="False">
|
||||
|
||||
<Control VerticalExpand="True" Margin="0 0 0 0"/>
|
||||
<Label Name="ActionInProgressLabel" Text="???" HorizontalAlignment="Center"/>
|
||||
<ProgressBar Name="ActionProgressBar"
|
||||
MinValue="0"
|
||||
MaxValue="1"
|
||||
SetHeight="20"
|
||||
Margin="5 10 5 10">
|
||||
</ProgressBar>
|
||||
<Label Name="ActionProgressEtaLabel" Text="???" HorizontalAlignment="Center"/>
|
||||
|
||||
<!-- Cancel button -->
|
||||
<Button Name="CancelButton" HorizontalExpand="True" Margin="0 20 0 10" SetHeight="40"
|
||||
Text="{Loc 'station-ai-fixer-console-window-cancel-action'}">
|
||||
<TextureRect HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SetSize="24 24"
|
||||
Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/Nano/cross.svg.png">
|
||||
</TextureRect>
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Visible controls -->
|
||||
<BoxContainer Name="MainControls"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Orientation="Vertical"
|
||||
ReservesSpace="False"
|
||||
Visible="False">
|
||||
|
||||
<controls:StripeBack>
|
||||
<PanelContainer>
|
||||
<Label Text="{Loc 'Controls'}"
|
||||
HorizontalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</PanelContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<!-- Eject button -->
|
||||
<Button Name="EjectButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
|
||||
Text="{Loc 'station-ai-fixer-console-window-station-ai-eject'}">
|
||||
<TextureRect HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SetSize="32 32"
|
||||
Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/VerbIcons/eject.svg.192dpi.png">
|
||||
</TextureRect>
|
||||
</Button>
|
||||
|
||||
<!-- Repair button -->
|
||||
<Button Name="RepairButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
|
||||
Text="{Loc 'station-ai-fixer-console-window-station-ai-repair'}">
|
||||
<TextureRect HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SetSize="32 32"
|
||||
Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/hammer_scaled.svg.192dpi.png">
|
||||
</TextureRect>
|
||||
</Button>
|
||||
|
||||
<!-- Purge button -->
|
||||
<Button Name="PurgeButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
|
||||
Text="{Loc 'station-ai-fixer-console-window-station-ai-purge'}">
|
||||
<TextureRect HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SetSize="32 32"
|
||||
Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png">
|
||||
</TextureRect>
|
||||
</Button>
|
||||
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Text="{Loc 'station-ai-fixer-console-window-flavor-left'}" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'station-ai-fixer-console-window-flavor-right'}" StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,198 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Silicons.StationAi;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StationAiFixerConsoleWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private readonly StationAiFixerConsoleSystem _stationAiFixerConsole;
|
||||
private readonly SharedStationAiSystem _stationAi;
|
||||
|
||||
private EntityUid? _owner;
|
||||
|
||||
private readonly SpriteSpecifier.Rsi _emptyPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_empty");
|
||||
private readonly SpriteSpecifier.Rsi _rebootingPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_fuzz");
|
||||
private SpriteSpecifier? _currentPortrait;
|
||||
|
||||
public event Action<StationAiFixerConsoleAction>? SendStationAiFixerConsoleMessageAction;
|
||||
public event Action? OpenConfirmationDialogAction;
|
||||
|
||||
public StationAiFixerConsoleConfirmationDialog? ConfirmationDialog;
|
||||
|
||||
private readonly Dictionary<StationAiState, Color> _statusColors = new()
|
||||
{
|
||||
[StationAiState.Empty] = Color.FromHex("#464966"),
|
||||
[StationAiState.Occupied] = Color.FromHex("#3E6C45"),
|
||||
[StationAiState.Rebooting] = Color.FromHex("#A5762F"),
|
||||
[StationAiState.Dead] = Color.FromHex("#BB3232"),
|
||||
};
|
||||
|
||||
public StationAiFixerConsoleWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_stationAiFixerConsole = _entManager.System<StationAiFixerConsoleSystem>();
|
||||
_stationAi = _entManager.System<StationAiSystem>();
|
||||
|
||||
StationAiPortraitTexture.DisplayRect.TextureScale = new Vector2(4f, 4f);
|
||||
|
||||
CancelButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Cancel);
|
||||
EjectButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Eject);
|
||||
RepairButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Repair);
|
||||
PurgeButton.OnButtonDown += _ => OnOpenConfirmationDialog();
|
||||
|
||||
CancelButton.Label.HorizontalAlignment = HAlignment.Left;
|
||||
EjectButton.Label.HorizontalAlignment = HAlignment.Left;
|
||||
RepairButton.Label.HorizontalAlignment = HAlignment.Left;
|
||||
PurgeButton.Label.HorizontalAlignment = HAlignment.Left;
|
||||
|
||||
CancelButton.Label.Margin = new Thickness(40, 0, 0, 0);
|
||||
EjectButton.Label.Margin = new Thickness(40, 0, 0, 0);
|
||||
RepairButton.Label.Margin = new Thickness(40, 0, 0, 0);
|
||||
PurgeButton.Label.Margin = new Thickness(40, 0, 0, 0);
|
||||
}
|
||||
|
||||
public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
|
||||
{
|
||||
SendStationAiFixerConsoleMessageAction?.Invoke(action);
|
||||
}
|
||||
|
||||
public void OnOpenConfirmationDialog()
|
||||
{
|
||||
OpenConfirmationDialogAction?.Invoke();
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
base.Close();
|
||||
ConfirmationDialog?.Close();
|
||||
}
|
||||
|
||||
public void SetOwner(EntityUid owner)
|
||||
{
|
||||
_owner = owner;
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
public void UpdateState()
|
||||
{
|
||||
if (!_entManager.TryGetComponent<StationAiFixerConsoleComponent>(_owner, out var stationAiFixerConsole))
|
||||
return;
|
||||
|
||||
var ent = (_owner.Value, stationAiFixerConsole);
|
||||
var isLocked = _entManager.TryGetComponent<LockComponent>(_owner, out var lockable) && lockable.Locked;
|
||||
|
||||
var stationAiHolderInserted = _stationAiFixerConsole.IsStationAiHolderInserted((_owner.Value, stationAiFixerConsole));
|
||||
var stationAi = stationAiFixerConsole.ActionTarget;
|
||||
var stationAiState = StationAiState.Empty;
|
||||
|
||||
if (_entManager.TryGetComponent<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization))
|
||||
{
|
||||
stationAiState = stationAiCustomization.State;
|
||||
}
|
||||
|
||||
// Set subscreen visibility
|
||||
LockScreen.Visible = isLocked;
|
||||
MainControls.Visible = !isLocked && !_stationAiFixerConsole.IsActionInProgress(ent);
|
||||
ActionProgressScreen.Visible = !isLocked && _stationAiFixerConsole.IsActionInProgress(ent);
|
||||
|
||||
// Update station AI name
|
||||
StationAiNameLabel.Text = GetStationAiName(stationAi);
|
||||
StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-no-station-ai-status");
|
||||
|
||||
// Update station AI portrait
|
||||
var portrait = _emptyPortrait;
|
||||
var statusColor = _statusColors[StationAiState.Empty];
|
||||
|
||||
if (stationAiState == StationAiState.Rebooting)
|
||||
{
|
||||
portrait = _rebootingPortrait;
|
||||
StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-station-ai-rebooting");
|
||||
_statusColors.TryGetValue(StationAiState.Rebooting, out statusColor);
|
||||
}
|
||||
else if (stationAi != null &&
|
||||
stationAiCustomization != null &&
|
||||
_stationAi.TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData))
|
||||
{
|
||||
StationAiStatusLabel.Text = stationAiState == StationAiState.Occupied ?
|
||||
Loc.GetString("station-ai-fixer-console-window-station-ai-online") :
|
||||
Loc.GetString("station-ai-fixer-console-window-station-ai-offline");
|
||||
|
||||
if (layerData.TryGetValue(stationAiState.ToString(), out var stateData) && stateData is { RsiPath: not null, State: not null })
|
||||
{
|
||||
portrait = new SpriteSpecifier.Rsi(new ResPath(stateData.RsiPath), stateData.State);
|
||||
}
|
||||
|
||||
_statusColors.TryGetValue(stationAiState, out statusColor);
|
||||
}
|
||||
|
||||
if (_currentPortrait == null || !_currentPortrait.Equals(portrait))
|
||||
{
|
||||
StationAiPortraitTexture.SetFromSpriteSpecifier(portrait);
|
||||
_currentPortrait = portrait;
|
||||
}
|
||||
|
||||
StationAiStatus.PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = statusColor,
|
||||
};
|
||||
|
||||
// Update buttons
|
||||
EjectButton.Disabled = !stationAiHolderInserted;
|
||||
RepairButton.Disabled = !stationAiHolderInserted || stationAiState != StationAiState.Dead;
|
||||
PurgeButton.Disabled = !stationAiHolderInserted || stationAiState == StationAiState.Empty;
|
||||
|
||||
// Update progress bar
|
||||
if (ActionProgressScreen.Visible)
|
||||
UpdateProgressBar(ent);
|
||||
}
|
||||
|
||||
public void UpdateProgressBar(Entity<StationAiFixerConsoleComponent> ent)
|
||||
{
|
||||
ActionInProgressLabel.Text = ent.Comp.ActionType == StationAiFixerConsoleAction.Repair ?
|
||||
Loc.GetString("station-ai-fixer-console-window-action-progress-repair") :
|
||||
Loc.GetString("station-ai-fixer-console-window-action-progress-purge");
|
||||
|
||||
var fullTimeSpan = ent.Comp.ActionEndTime - ent.Comp.ActionStartTime;
|
||||
var remainingTimeSpan = ent.Comp.ActionEndTime - _timing.CurTime;
|
||||
|
||||
var time = remainingTimeSpan.TotalSeconds > 60 ? remainingTimeSpan.TotalMinutes : remainingTimeSpan.TotalSeconds;
|
||||
var units = remainingTimeSpan.TotalSeconds > 60 ? Loc.GetString("generic-minutes") : Loc.GetString("generic-seconds");
|
||||
ActionProgressEtaLabel.Text = Loc.GetString("station-ai-fixer-console-window-action-progress-eta", ("time", (int)time), ("units", units));
|
||||
|
||||
ActionProgressBar.Value = 1f - (float)remainingTimeSpan.Divide(fullTimeSpan);
|
||||
}
|
||||
|
||||
private string GetStationAiName(EntityUid? uid)
|
||||
{
|
||||
if (_entManager.TryGetComponent<MetaDataComponent>(uid, out var metadata))
|
||||
{
|
||||
return metadata.EntityName;
|
||||
}
|
||||
|
||||
return Loc.GetString("station-ai-fixer-console-window-no-station-ai");
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
if (!ActionProgressScreen.Visible)
|
||||
return;
|
||||
|
||||
if (!_entManager.TryGetComponent<StationAiFixerConsoleComponent>(_owner, out var stationAiFixerConsole))
|
||||
return;
|
||||
|
||||
UpdateProgressBar((_owner.Value, stationAiFixerConsole));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Graphics;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
@@ -26,8 +27,7 @@ public sealed class StationAiOverlay : Overlay
|
||||
|
||||
private readonly HashSet<Vector2i> _visibleTiles = new();
|
||||
|
||||
private IRenderTexture? _staticTexture;
|
||||
private IRenderTexture? _stencilTexture;
|
||||
private readonly OverlayResourceCache<CachedResources> _resources = new();
|
||||
|
||||
private float _updateRate = 1f / 30f;
|
||||
private float _accumulator;
|
||||
@@ -39,12 +39,14 @@ public sealed class StationAiOverlay : Overlay
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (_stencilTexture?.Texture.Size != args.Viewport.Size)
|
||||
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
||||
|
||||
if (res.StencilTexture?.Texture.Size != args.Viewport.Size)
|
||||
{
|
||||
_staticTexture?.Dispose();
|
||||
_stencilTexture?.Dispose();
|
||||
_stencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil");
|
||||
_staticTexture = _clyde.CreateRenderTarget(args.Viewport.Size,
|
||||
res.StaticTexture?.Dispose();
|
||||
res.StencilTexture?.Dispose();
|
||||
res.StencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil");
|
||||
res.StaticTexture = _clyde.CreateRenderTarget(args.Viewport.Size,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||
name: "station-ai-static");
|
||||
}
|
||||
@@ -78,7 +80,7 @@ public sealed class StationAiOverlay : Overlay
|
||||
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
|
||||
|
||||
// Draw visible tiles to stencil
|
||||
worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
|
||||
worldHandle.RenderInRenderTarget(res.StencilTexture!, () =>
|
||||
{
|
||||
worldHandle.SetTransform(matty);
|
||||
|
||||
@@ -91,7 +93,7 @@ public sealed class StationAiOverlay : Overlay
|
||||
Color.Transparent);
|
||||
|
||||
// Once this is gucci optimise rendering.
|
||||
worldHandle.RenderInRenderTarget(_staticTexture!,
|
||||
worldHandle.RenderInRenderTarget(res.StaticTexture!,
|
||||
() =>
|
||||
{
|
||||
worldHandle.SetTransform(invMatrix);
|
||||
@@ -104,12 +106,12 @@ public sealed class StationAiOverlay : Overlay
|
||||
// Not on a grid
|
||||
else
|
||||
{
|
||||
worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
|
||||
worldHandle.RenderInRenderTarget(res.StencilTexture!, () =>
|
||||
{
|
||||
},
|
||||
Color.Transparent);
|
||||
|
||||
worldHandle.RenderInRenderTarget(_staticTexture!,
|
||||
worldHandle.RenderInRenderTarget(res.StaticTexture!,
|
||||
() =>
|
||||
{
|
||||
worldHandle.SetTransform(Matrix3x2.Identity);
|
||||
@@ -119,14 +121,33 @@ public sealed class StationAiOverlay : Overlay
|
||||
|
||||
// Use the lighting as a mask
|
||||
worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
|
||||
worldHandle.DrawTextureRect(_stencilTexture!.Texture, worldBounds);
|
||||
worldHandle.DrawTextureRect(res.StencilTexture!.Texture, worldBounds);
|
||||
|
||||
// Draw the static
|
||||
worldHandle.UseShader(_proto.Index(StencilDrawShader).Instance());
|
||||
worldHandle.DrawTextureRect(_staticTexture!.Texture, worldBounds);
|
||||
worldHandle.DrawTextureRect(res.StaticTexture!.Texture, worldBounds);
|
||||
|
||||
worldHandle.SetTransform(Matrix3x2.Identity);
|
||||
worldHandle.UseShader(null);
|
||||
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_resources.Dispose();
|
||||
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
private sealed class CachedResources : IDisposable
|
||||
{
|
||||
public IRenderTexture? StaticTexture;
|
||||
public IRenderTexture? StencilTexture;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StaticTexture?.Dispose();
|
||||
StencilTexture?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +81,10 @@ public sealed partial class StationAiSystem : SharedStationAiSystem
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualState.Key, out var layerData, args.Component))
|
||||
_sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData);
|
||||
if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualLayers.Icon, out var layerData, args.Component))
|
||||
_sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData);
|
||||
|
||||
_sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData != null);
|
||||
_sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData != null);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
|
||||
@@ -90,23 +90,25 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>();
|
||||
var requirementsManager = IoCManager.Resolve<JobRequirementsManager>();
|
||||
|
||||
// TODO: role.Requirements value doesn't work at all as an equality key, this must be fixed
|
||||
// Grouping roles
|
||||
var groupedRoles = ghostState.GhostRoles.GroupBy(
|
||||
role => (role.Name, role.Description, role.Requirements));
|
||||
role => (
|
||||
role.Name,
|
||||
role.Description,
|
||||
// Check the prototypes for role requirements and bans
|
||||
requirementsManager.IsAllowed(role.RolePrototypes.Item1, role.RolePrototypes.Item2, null, out var reason),
|
||||
reason));
|
||||
|
||||
// Add a new entry for each role group
|
||||
foreach (var group in groupedRoles)
|
||||
{
|
||||
var reason = group.Key.reason;
|
||||
var name = group.Key.Name;
|
||||
var description = group.Key.Description;
|
||||
var hasAccess = requirementsManager.CheckRoleRequirements(
|
||||
group.Key.Requirements,
|
||||
null,
|
||||
out var reason);
|
||||
var prototypesAllowed = group.Key.Item3;
|
||||
|
||||
// Adding a new role
|
||||
_window.AddEntry(name, description, hasAccess, reason, group, spriteSystem);
|
||||
_window.AddEntry(name, description, prototypesAllowed, reason, group, spriteSystem);
|
||||
}
|
||||
|
||||
// Restore the Collapsible box state if it is saved
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Generic implementation of <see cref="ITestContextLike"/> for usage outside of actual tests.
|
||||
/// </summary>
|
||||
public sealed class ExternalTestContext(string name, TextWriter writer) : ITestContextLike
|
||||
{
|
||||
public string FullName => name;
|
||||
public TextWriter Out => writer;
|
||||
}
|
||||
@@ -3,3 +3,4 @@
|
||||
global using NUnit.Framework;
|
||||
global using System;
|
||||
global using System.Threading.Tasks;
|
||||
global using Robust.UnitTesting.Pool;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Something that looks like a <see cref="TestContext"/>, for passing to integration tests.
|
||||
/// </summary>
|
||||
public interface ITestContextLike
|
||||
{
|
||||
string FullName { get; }
|
||||
TextWriter Out { get; }
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical implementation of <see cref="ITestContextLike"/> for usage in actual NUnit tests.
|
||||
/// </summary>
|
||||
public sealed class NUnitTestContextWrap(TestContext context, TextWriter writer) : ITestContextLike
|
||||
{
|
||||
public string FullName => context.Test.FullName;
|
||||
public TextWriter Out => writer;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
/// <summary>
|
||||
/// Simple data class that stored information about a map being used by a test.
|
||||
/// </summary>
|
||||
public sealed class TestMapData
|
||||
{
|
||||
public EntityUid MapUid { get; set; }
|
||||
public Entity<MapGridComponent> Grid;
|
||||
public MapId MapId;
|
||||
public EntityCoordinates GridCoords { get; set; }
|
||||
public MapCoordinates MapCoords { get; set; }
|
||||
public TileRef Tile { get; set; }
|
||||
|
||||
// Client-side uids
|
||||
public EntityUid CMapUid { get; set; }
|
||||
public EntityUid CGridUid { get; set; }
|
||||
public EntityCoordinates CGridCoords { get; set; }
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
private readonly Dictionary<string, object> _modifiedClientCvars = new();
|
||||
private readonly Dictionary<string, object> _modifiedServerCvars = new();
|
||||
|
||||
private void OnServerCvarChanged(CVarChangeInfo args)
|
||||
{
|
||||
_modifiedServerCvars.TryAdd(args.Name, args.OldValue);
|
||||
}
|
||||
|
||||
private void OnClientCvarChanged(CVarChangeInfo args)
|
||||
{
|
||||
_modifiedClientCvars.TryAdd(args.Name, args.OldValue);
|
||||
}
|
||||
|
||||
internal void ClearModifiedCvars()
|
||||
{
|
||||
_modifiedClientCvars.Clear();
|
||||
_modifiedServerCvars.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts any cvars that were modified during a test back to their original values.
|
||||
/// </summary>
|
||||
public async Task RevertModifiedCvars()
|
||||
{
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
foreach (var (name, value) in _modifiedServerCvars)
|
||||
{
|
||||
if (Server.CfgMan.GetCVar(name).Equals(value))
|
||||
continue;
|
||||
Server.Log.Info($"Resetting cvar {name} to {value}");
|
||||
Server.CfgMan.SetCVar(name, value);
|
||||
}
|
||||
|
||||
// I just love order dependent cvars
|
||||
if (_modifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik))
|
||||
Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik);
|
||||
|
||||
});
|
||||
|
||||
await Client.WaitPost(() =>
|
||||
{
|
||||
foreach (var (name, value) in _modifiedClientCvars)
|
||||
{
|
||||
if (Client.CfgMan.GetCVar(name).Equals(value))
|
||||
continue;
|
||||
|
||||
var flags = Client.CfgMan.GetCVarFlags(name);
|
||||
if (flags.HasFlag(CVar.REPLICATED) && flags.HasFlag(CVar.SERVER))
|
||||
continue;
|
||||
|
||||
Client.Log.Info($"Resetting cvar {name} to {value}");
|
||||
Client.CfgMan.SetCVar(name, value);
|
||||
}
|
||||
});
|
||||
|
||||
ClearModifiedCvars();
|
||||
}
|
||||
}
|
||||
@@ -1,172 +1,19 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
// Contains misc helper functions to make writing tests easier.
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a map, a grid, and a tile, and gives back references to them.
|
||||
/// </summary>
|
||||
[MemberNotNull(nameof(TestMap))]
|
||||
public async Task<TestMapData> CreateTestMap(bool initialized = true, string tile = "Plating")
|
||||
{
|
||||
var mapData = new TestMapData();
|
||||
TestMap = mapData;
|
||||
await Server.WaitIdleAsync();
|
||||
var tileDefinitionManager = Server.ResolveDependency<ITileDefinitionManager>();
|
||||
|
||||
TestMap = mapData;
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
mapData.MapUid = Server.System<SharedMapSystem>().CreateMap(out mapData.MapId, runMapInit: initialized);
|
||||
mapData.Grid = Server.MapMan.CreateGridEntity(mapData.MapId);
|
||||
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
|
||||
var plating = tileDefinitionManager[tile];
|
||||
var platingTile = new Tile(plating.TileId);
|
||||
Server.System<SharedMapSystem>().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
|
||||
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
|
||||
mapData.Tile = Server.System<SharedMapSystem>().GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
|
||||
});
|
||||
|
||||
TestMap = mapData;
|
||||
if (!Settings.Connected)
|
||||
return mapData;
|
||||
|
||||
await RunTicksSync(10);
|
||||
mapData.CMapUid = ToClientUid(mapData.MapUid);
|
||||
mapData.CGridUid = ToClientUid(mapData.Grid);
|
||||
mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
|
||||
|
||||
TestMap = mapData;
|
||||
return mapData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a client-side uid into a server-side uid
|
||||
/// </summary>
|
||||
public EntityUid ToServerUid(EntityUid uid) => ConvertUid(uid, Client, Server);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a server-side uid into a client-side uid
|
||||
/// </summary>
|
||||
public EntityUid ToClientUid(EntityUid uid) => ConvertUid(uid, Server, Client);
|
||||
|
||||
private static EntityUid ConvertUid(
|
||||
EntityUid uid,
|
||||
RobustIntegrationTest.IntegrationInstance source,
|
||||
RobustIntegrationTest.IntegrationInstance destination)
|
||||
{
|
||||
if (!uid.IsValid())
|
||||
return EntityUid.Invalid;
|
||||
|
||||
if (!source.EntMan.TryGetComponent<MetaDataComponent>(uid, out var meta))
|
||||
{
|
||||
Assert.Fail($"Failed to resolve MetaData while converting the EntityUid for entity {uid}");
|
||||
return EntityUid.Invalid;
|
||||
}
|
||||
|
||||
if (!destination.EntMan.TryGetEntity(meta.NetEntity, out var otherUid))
|
||||
{
|
||||
Assert.Fail($"Failed to resolve net ID while converting the EntityUid entity {source.EntMan.ToPrettyString(uid)}");
|
||||
return EntityUid.Invalid;
|
||||
}
|
||||
|
||||
return otherUid.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a command on the server and wait some number of ticks.
|
||||
/// </summary>
|
||||
public async Task WaitCommand(string cmd, int numTicks = 10)
|
||||
{
|
||||
await Server.ExecuteCommand(cmd);
|
||||
await RunTicksSync(numTicks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a command on the client and wait some number of ticks.
|
||||
/// </summary>
|
||||
public async Task WaitClientCommand(string cmd, int numTicks = 10)
|
||||
{
|
||||
await Client.ExecuteCommand(cmd);
|
||||
await RunTicksSync(numTicks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all entity prototypes that have some component.
|
||||
/// </summary>
|
||||
public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
|
||||
HashSet<string>? ignored = null,
|
||||
bool ignoreAbstract = true,
|
||||
bool ignoreTestPrototypes = true)
|
||||
where T : IComponent, new()
|
||||
{
|
||||
if (!Server.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out var reg)
|
||||
&& !Client.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out reg))
|
||||
{
|
||||
Assert.Fail($"Unknown component: {typeof(T).Name}");
|
||||
return new();
|
||||
}
|
||||
|
||||
var id = reg.Name;
|
||||
var list = new List<(EntityPrototype, T)>();
|
||||
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (ignored != null && ignored.Contains(proto.ID))
|
||||
continue;
|
||||
|
||||
if (ignoreAbstract && proto.Abstract)
|
||||
continue;
|
||||
|
||||
if (ignoreTestPrototypes && IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (proto.Components.TryGetComponent(id, out var cmp))
|
||||
list.Add((proto, (T)cmp));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all entity prototypes that have some component.
|
||||
/// </summary>
|
||||
public List<EntityPrototype> GetPrototypesWithComponent(Type type,
|
||||
HashSet<string>? ignored = null,
|
||||
bool ignoreAbstract = true,
|
||||
bool ignoreTestPrototypes = true)
|
||||
{
|
||||
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
|
||||
var list = new List<EntityPrototype>();
|
||||
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (ignored != null && ignored.Contains(proto.ID))
|
||||
continue;
|
||||
|
||||
if (ignoreAbstract && proto.Abstract)
|
||||
continue;
|
||||
|
||||
if (ignoreTestPrototypes && IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (proto.Components.ContainsKey(id))
|
||||
list.Add((proto));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
public Task<TestMapData> CreateTestMap(bool initialized = true)
|
||||
=> CreateTestMap(initialized, "Plating");
|
||||
|
||||
/// <summary>
|
||||
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
// This partial class contains helper methods to deal with yaml prototypes.
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
private Dictionary<Type, HashSet<string>> _loadedPrototypes = new();
|
||||
private HashSet<string> _loadedEntityPrototypes = new();
|
||||
|
||||
public async Task LoadPrototypes(List<string> prototypes)
|
||||
{
|
||||
await LoadPrototypes(Server, prototypes);
|
||||
await LoadPrototypes(Client, prototypes);
|
||||
}
|
||||
|
||||
private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List<string> prototypes)
|
||||
{
|
||||
var changed = new Dictionary<Type, HashSet<string>>();
|
||||
foreach (var file in prototypes)
|
||||
{
|
||||
instance.ProtoMan.LoadString(file, changed: changed);
|
||||
}
|
||||
|
||||
await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
|
||||
|
||||
foreach (var (kind, ids) in changed)
|
||||
{
|
||||
_loadedPrototypes.GetOrNew(kind).UnionWith(ids);
|
||||
}
|
||||
|
||||
if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds))
|
||||
_loadedEntityPrototypes.UnionWith(entIds);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype(EntityPrototype proto)
|
||||
{
|
||||
return _loadedEntityPrototypes.Contains(proto.ID);
|
||||
}
|
||||
|
||||
public bool IsTestEntityPrototype(string id)
|
||||
{
|
||||
return _loadedEntityPrototypes.Contains(id);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype<TPrototype>(string id) where TPrototype : IPrototype
|
||||
{
|
||||
return IsTestPrototype(typeof(TPrototype), id);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype<TPrototype>(TPrototype proto) where TPrototype : IPrototype
|
||||
{
|
||||
return IsTestPrototype(typeof(TPrototype), proto.ID);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype(Type kind, string id)
|
||||
{
|
||||
return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
|
||||
}
|
||||
}
|
||||
@@ -8,84 +8,17 @@ using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Client;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
// This partial class contains logic related to recycling & disposing test pairs.
|
||||
public sealed partial class TestPair : IAsyncDisposable
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
public PairState State { get; private set; } = PairState.Ready;
|
||||
|
||||
private async Task OnDirtyDispose()
|
||||
protected override async Task Cleanup()
|
||||
{
|
||||
var usageTime = Watch.Elapsed;
|
||||
Watch.Restart();
|
||||
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Id} in {usageTime.TotalMilliseconds} ms");
|
||||
Kill();
|
||||
var disposeTime = Watch.Elapsed;
|
||||
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
|
||||
// Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
|
||||
// because someone forgot to clean-return the pair.
|
||||
Assert.Warn("Test was dirty-disposed.");
|
||||
}
|
||||
|
||||
private async Task OnCleanDispose()
|
||||
{
|
||||
await Server.WaitIdleAsync();
|
||||
await Client.WaitIdleAsync();
|
||||
await base.Cleanup();
|
||||
await ResetModifiedPreferences();
|
||||
await Server.RemoveAllDummySessions();
|
||||
|
||||
if (TestMap != null)
|
||||
{
|
||||
await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
|
||||
TestMap = null;
|
||||
}
|
||||
|
||||
await RevertModifiedCvars();
|
||||
|
||||
var usageTime = Watch.Elapsed;
|
||||
Watch.Restart();
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
|
||||
// Let any last minute failures the test cause happen.
|
||||
await ReallyBeIdle();
|
||||
if (!Settings.Destructive)
|
||||
{
|
||||
if (Client.IsAlive == false)
|
||||
{
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Id}:", Client.UnhandledException);
|
||||
}
|
||||
|
||||
if (Server.IsAlive == false)
|
||||
{
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Id}:", Server.UnhandledException);
|
||||
}
|
||||
}
|
||||
|
||||
if (Settings.MustNotBeReused)
|
||||
{
|
||||
Kill();
|
||||
await ReallyBeIdle();
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {Watch.Elapsed.TotalMilliseconds} ms");
|
||||
return;
|
||||
}
|
||||
|
||||
var sRuntimeLog = Server.ResolveDependency<IRuntimeLog>();
|
||||
if (sRuntimeLog.ExceptionCount > 0)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
|
||||
var cRuntimeLog = Client.ResolveDependency<IRuntimeLog>();
|
||||
if (cRuntimeLog.ExceptionCount > 0)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
|
||||
|
||||
var returnTime = Watch.Elapsed;
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
|
||||
State = PairState.Ready;
|
||||
}
|
||||
|
||||
private async Task ResetModifiedPreferences()
|
||||
@@ -95,61 +28,14 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
{
|
||||
await Server.WaitPost(() => prefMan.SetProfile(user, 0, new HumanoidCharacterProfile()).Wait());
|
||||
}
|
||||
|
||||
_modifiedProfiles.Clear();
|
||||
}
|
||||
|
||||
public async ValueTask CleanReturnAsync()
|
||||
protected override async Task Recycle(PairSettings next, TextWriter testOut)
|
||||
{
|
||||
if (State != PairState.InUse)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
|
||||
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
|
||||
State = PairState.CleanDisposed;
|
||||
await OnCleanDispose();
|
||||
DebugTools.Assert(State is PairState.Dead or PairState.Ready);
|
||||
PoolManager.NoCheckReturn(this);
|
||||
ClearContext();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case PairState.Dead:
|
||||
case PairState.Ready:
|
||||
break;
|
||||
case PairState.InUse:
|
||||
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
|
||||
await OnDirtyDispose();
|
||||
PoolManager.NoCheckReturn(this);
|
||||
ClearContext();
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
|
||||
{
|
||||
Settings = default!;
|
||||
Watch.Restart();
|
||||
await testOut.WriteLineAsync($"Recycling...");
|
||||
|
||||
var gameTicker = Server.System<GameTicker>();
|
||||
var cNetMgr = Client.ResolveDependency<IClientNetManager>();
|
||||
|
||||
await RunTicksSync(1);
|
||||
|
||||
// Disconnect the client if they are connected.
|
||||
if (cNetMgr.IsConnected)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
|
||||
await Client.WaitPost(() => cNetMgr.ClientDisconnect("Test pooling cleanup disconnect"));
|
||||
await RunTicksSync(1);
|
||||
}
|
||||
Assert.That(cNetMgr.IsConnected, Is.False);
|
||||
|
||||
// Move to pre-round lobby. Required to toggle dummy ticker on and off
|
||||
var gameTicker = Server.System<GameTicker>();
|
||||
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
|
||||
@@ -162,8 +48,7 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
|
||||
//Apply Cvars
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
|
||||
await PoolManager.SetupCVars(Client, settings);
|
||||
await PoolManager.SetupCVars(Server, settings);
|
||||
await ApplySettings(next);
|
||||
await RunTicksSync(1);
|
||||
|
||||
// Restart server.
|
||||
@@ -171,52 +56,30 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
await Server.WaitPost(() => Server.EntMan.FlushEntities());
|
||||
await Server.WaitPost(() => gameTicker.RestartRound());
|
||||
await RunTicksSync(1);
|
||||
|
||||
// Connect client
|
||||
if (settings.ShouldBeConnected)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Connecting client");
|
||||
Client.SetConnectTarget(Server);
|
||||
await Client.WaitPost(() => cNetMgr.ClientConnect(null!, 0, null!));
|
||||
}
|
||||
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Idling");
|
||||
await ReallyBeIdle();
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Done recycling");
|
||||
}
|
||||
|
||||
public void ValidateSettings(PoolSettings settings)
|
||||
public override void ValidateSettings(PairSettings s)
|
||||
{
|
||||
base.ValidateSettings(s);
|
||||
var settings = (PoolSettings) s;
|
||||
|
||||
var cfg = Server.CfgMan;
|
||||
Assert.That(cfg.GetCVar(CCVars.AdminLogsEnabled), Is.EqualTo(settings.AdminLogsEnabled));
|
||||
Assert.That(cfg.GetCVar(CCVars.GameLobbyEnabled), Is.EqualTo(settings.InLobby));
|
||||
Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.UseDummyTicker));
|
||||
Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.DummyTicker));
|
||||
|
||||
var entMan = Server.ResolveDependency<EntityManager>();
|
||||
var ticker = entMan.System<GameTicker>();
|
||||
Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
|
||||
var ticker = Server.System<GameTicker>();
|
||||
Assert.That(ticker.DummyTicker, Is.EqualTo(settings.DummyTicker));
|
||||
|
||||
var expectPreRound = settings.InLobby | settings.DummyTicker;
|
||||
var expectedLevel = expectPreRound ? GameRunLevel.PreRoundLobby : GameRunLevel.InRound;
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(expectedLevel));
|
||||
|
||||
var baseClient = Client.ResolveDependency<IBaseClient>();
|
||||
var netMan = Client.ResolveDependency<INetManager>();
|
||||
Assert.That(netMan.IsConnected, Is.Not.EqualTo(!settings.ShouldBeConnected));
|
||||
|
||||
if (!settings.ShouldBeConnected)
|
||||
if (ticker.DummyTicker || !settings.Connected)
|
||||
return;
|
||||
|
||||
Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
|
||||
var cPlayer = Client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
|
||||
var sPlayer = Server.ResolveDependency<IPlayerManager>();
|
||||
Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
|
||||
var sPlayer = Server.ResolveDependency<ISharedPlayerManager>();
|
||||
var session = sPlayer.Sessions.Single();
|
||||
Assert.That(cPlayer.LocalSession?.UserId, Is.EqualTo(session.UserId));
|
||||
|
||||
if (ticker.DummyTicker)
|
||||
return;
|
||||
|
||||
var status = ticker.PlayerGameStatuses[session.UserId];
|
||||
var expected = settings.InLobby
|
||||
? PlayerGameStatus.NotReadyToPlay
|
||||
@@ -231,11 +94,11 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
}
|
||||
|
||||
Assert.That(session.AttachedEntity, Is.Not.Null);
|
||||
Assert.That(entMan.EntityExists(session.AttachedEntity));
|
||||
Assert.That(entMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
|
||||
var mindCont = entMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
|
||||
Assert.That(Server.EntMan.EntityExists(session.AttachedEntity));
|
||||
Assert.That(Server.EntMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
|
||||
var mindCont = Server.EntMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
|
||||
Assert.That(mindCont.Mind, Is.Not.Null);
|
||||
Assert.That(entMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
|
||||
Assert.That(Server.EntMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
|
||||
Assert.That(mind!.VisitingEntity, Is.Null);
|
||||
Assert.That(mind.OwnedEntity, Is.EqualTo(session.AttachedEntity!.Value));
|
||||
Assert.That(mind.UserId, Is.EqualTo(session.UserId));
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
// This partial class contains methods for running the server/client pairs for some number of ticks
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the server-client pair in sync
|
||||
/// </summary>
|
||||
/// <param name="ticks">How many ticks to run them for</param>
|
||||
public async Task RunTicksSync(int ticks)
|
||||
{
|
||||
for (var i = 0; i < ticks; i++)
|
||||
{
|
||||
await Server.WaitRunTicks(1);
|
||||
await Client.WaitRunTicks(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a time interval to some number of ticks.
|
||||
/// </summary>
|
||||
public int SecondsToTicks(float seconds)
|
||||
{
|
||||
return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the server & client in sync for some amount of time
|
||||
/// </summary>
|
||||
public async Task RunSeconds(float seconds)
|
||||
{
|
||||
await RunTicksSync(SecondsToTicks(seconds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
|
||||
/// </summary>
|
||||
/// <param name="runTicks">How many ticks to run</param>
|
||||
public async Task ReallyBeIdle(int runTicks = 25)
|
||||
{
|
||||
for (var i = 0; i < runTicks; i++)
|
||||
{
|
||||
await Client.WaitRunTicks(1);
|
||||
await Server.WaitRunTicks(1);
|
||||
for (var idleCycles = 0; idleCycles < 4; idleCycles++)
|
||||
{
|
||||
await Client.WaitIdleAsync();
|
||||
await Server.WaitIdleAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the server/clients until the ticks are synchronized.
|
||||
/// By default the client will be one tick ahead of the server.
|
||||
/// </summary>
|
||||
public async Task SyncTicks(int targetDelta = 1)
|
||||
{
|
||||
var sTick = (int)Server.Timing.CurTick.Value;
|
||||
var cTick = (int)Client.Timing.CurTick.Value;
|
||||
var delta = cTick - sTick;
|
||||
|
||||
if (delta == targetDelta)
|
||||
return;
|
||||
if (delta > targetDelta)
|
||||
await Server.WaitRunTicks(delta - targetDelta);
|
||||
else
|
||||
await Client.WaitRunTicks(targetDelta - delta);
|
||||
|
||||
sTick = (int)Server.Timing.CurTick.Value;
|
||||
cTick = (int)Client.Timing.CurTick.Value;
|
||||
delta = cTick - sTick;
|
||||
Assert.That(delta, Is.EqualTo(targetDelta));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Client.IoC;
|
||||
using Content.Client.Parallax.Managers;
|
||||
using Content.IntegrationTests.Tests.Destructible;
|
||||
using Content.IntegrationTests.Tests.DeviceNetwork;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Players;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
@@ -18,156 +19,99 @@ namespace Content.IntegrationTests.Pair;
|
||||
/// <summary>
|
||||
/// This object wraps a pooled server+client pair.
|
||||
/// </summary>
|
||||
public sealed partial class TestPair
|
||||
public sealed partial class TestPair : RobustIntegrationTest.TestPair
|
||||
{
|
||||
public readonly int Id;
|
||||
private bool _initialized;
|
||||
private TextWriter _testOut = default!;
|
||||
public readonly Stopwatch Watch = new();
|
||||
public readonly List<string> TestHistory = new();
|
||||
public PoolSettings Settings = default!;
|
||||
public TestMapData? TestMap;
|
||||
private List<NetUserId> _modifiedProfiles = new();
|
||||
|
||||
private int _nextServerSeed;
|
||||
private int _nextClientSeed;
|
||||
|
||||
public int ServerSeed;
|
||||
public int ClientSeed;
|
||||
|
||||
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
|
||||
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
|
||||
|
||||
public void Deconstruct(
|
||||
out RobustIntegrationTest.ServerIntegrationInstance server,
|
||||
out RobustIntegrationTest.ClientIntegrationInstance client)
|
||||
{
|
||||
server = Server;
|
||||
client = Client;
|
||||
}
|
||||
|
||||
public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
|
||||
|
||||
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
|
||||
|
||||
public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
|
||||
public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;
|
||||
|
||||
public TestPair(int id)
|
||||
protected override async Task Initialize()
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public async Task Initialize(PoolSettings settings, TextWriter testOut, List<string> testPrototypes)
|
||||
{
|
||||
if (_initialized)
|
||||
throw new InvalidOperationException("Already initialized");
|
||||
|
||||
_initialized = true;
|
||||
Settings = settings;
|
||||
(Client, ClientLogHandler) = await PoolManager.GenerateClient(settings, testOut);
|
||||
(Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
|
||||
ActivateContext(testOut);
|
||||
|
||||
Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
|
||||
Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
|
||||
|
||||
if (!settings.NoLoadTestPrototypes)
|
||||
await LoadPrototypes(testPrototypes!);
|
||||
|
||||
if (!settings.UseDummyTicker)
|
||||
var settings = (PoolSettings)Settings;
|
||||
if (!settings.DummyTicker)
|
||||
{
|
||||
var gameTicker = Server.ResolveDependency<IEntityManager>().System<GameTicker>();
|
||||
var gameTicker = Server.System<GameTicker>();
|
||||
await Server.WaitPost(() => gameTicker.RestartRound());
|
||||
}
|
||||
}
|
||||
|
||||
// Always initially connect clients to generate an initial random set of preferences/profiles.
|
||||
// This is to try and prevent issues where if the first test that connects the client is consistently some test
|
||||
// that uses a fixed seed, it would effectively prevent it from beingrandomized.
|
||||
public override async Task RevertModifiedCvars()
|
||||
{
|
||||
// I just love order dependent cvars
|
||||
// I.e., cvars that when changed automatically cause others to also change.
|
||||
var modified = ModifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik);
|
||||
|
||||
Client.SetConnectTarget(Server);
|
||||
await Client.WaitIdleAsync();
|
||||
var netMgr = Client.ResolveDependency<IClientNetManager>();
|
||||
await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!));
|
||||
await ReallyBeIdle(10);
|
||||
await Client.WaitRunTicks(1);
|
||||
await base.RevertModifiedCvars();
|
||||
|
||||
if (!settings.ShouldBeConnected)
|
||||
if (!modified)
|
||||
return;
|
||||
|
||||
await Server.WaitPost(() => Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik!));
|
||||
ClearModifiedCvars();
|
||||
}
|
||||
|
||||
protected override async Task ApplySettings(IIntegrationInstance instance, PairSettings n)
|
||||
{
|
||||
var next = (PoolSettings)n;
|
||||
await base.ApplySettings(instance, next);
|
||||
var cfg = instance.CfgMan;
|
||||
await instance.WaitPost(() =>
|
||||
{
|
||||
await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
|
||||
await ReallyBeIdle(10);
|
||||
}
|
||||
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
|
||||
cfg.SetCVar(CCVars.GameDummyTicker, next.DummyTicker);
|
||||
|
||||
var cRand = Client.ResolveDependency<IRobustRandom>();
|
||||
var sRand = Server.ResolveDependency<IRobustRandom>();
|
||||
_nextClientSeed = cRand.Next();
|
||||
_nextServerSeed = sRand.Next();
|
||||
if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
|
||||
cfg.SetCVar(CCVars.GameLobbyEnabled, next.InLobby);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
|
||||
cfg.SetCVar(CCVars.GameMap, next.Map);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
|
||||
cfg.SetCVar(CCVars.AdminLogsEnabled, next.AdminLogsEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
public void Kill()
|
||||
protected override RobustIntegrationTest.ClientIntegrationOptions ClientOptions()
|
||||
{
|
||||
State = PairState.Dead;
|
||||
ServerLogHandler.ShuttingDown = true;
|
||||
ClientLogHandler.ShuttingDown = true;
|
||||
Server.Dispose();
|
||||
Client.Dispose();
|
||||
}
|
||||
var opts = base.ClientOptions();
|
||||
|
||||
private void ClearContext()
|
||||
{
|
||||
_testOut = default!;
|
||||
ServerLogHandler.ClearContext();
|
||||
ClientLogHandler.ClearContext();
|
||||
}
|
||||
|
||||
public void ActivateContext(TextWriter testOut)
|
||||
{
|
||||
_testOut = testOut;
|
||||
ServerLogHandler.ActivateContext(testOut);
|
||||
ClientLogHandler.ActivateContext(testOut);
|
||||
}
|
||||
|
||||
public void Use()
|
||||
{
|
||||
if (State != PairState.Ready)
|
||||
throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
|
||||
State = PairState.InUse;
|
||||
}
|
||||
|
||||
public enum PairState : byte
|
||||
{
|
||||
Ready = 0,
|
||||
InUse = 1,
|
||||
CleanDisposed = 2,
|
||||
Dead = 3,
|
||||
}
|
||||
|
||||
public void SetupSeed()
|
||||
{
|
||||
var sRand = Server.ResolveDependency<IRobustRandom>();
|
||||
if (Settings.ServerSeed is { } severSeed)
|
||||
opts.LoadTestAssembly = false;
|
||||
opts.ContentStart = true;
|
||||
opts.FailureLogLevel = LogLevel.Warning;
|
||||
opts.Options = new()
|
||||
{
|
||||
ServerSeed = severSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerSeed = _nextServerSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
_nextServerSeed = sRand.Next();
|
||||
}
|
||||
LoadConfigAndUserData = false,
|
||||
};
|
||||
|
||||
var cRand = Client.ResolveDependency<IRobustRandom>();
|
||||
if (Settings.ClientSeed is { } clientSeed)
|
||||
opts.BeforeStart += () =>
|
||||
{
|
||||
ClientSeed = clientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
}
|
||||
else
|
||||
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
|
||||
{
|
||||
ClientBeforeIoC = () => IoCManager.Register<IParallaxManager, DummyParallaxManager>(true)
|
||||
});
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
|
||||
protected override RobustIntegrationTest.ServerIntegrationOptions ServerOptions()
|
||||
{
|
||||
var opts = base.ServerOptions();
|
||||
|
||||
opts.LoadTestAssembly = false;
|
||||
opts.ContentStart = true;
|
||||
opts.Options = new()
|
||||
{
|
||||
ClientSeed = _nextClientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
_nextClientSeed = cRand.Next();
|
||||
}
|
||||
LoadConfigAndUserData = false,
|
||||
};
|
||||
|
||||
opts.BeforeStart += () =>
|
||||
{
|
||||
// Server-only systems (i.e., systems that subscribe to events with server-only components)
|
||||
// There's probably a better way to do this.
|
||||
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
|
||||
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#nullable enable
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
// Partial class containing cvar logic
|
||||
// Partial class containing test cvars
|
||||
// This could probably be merged into the main file, but I'm keeping it separate to reduce
|
||||
// conflicts for forks.
|
||||
public static partial class PoolManager
|
||||
{
|
||||
private static readonly (string cvar, string value)[] TestCvars =
|
||||
public static readonly (string cvar, string value)[] TestCvars =
|
||||
{
|
||||
// @formatter:off
|
||||
(CCVars.DatabaseSynchronous.Name, "true"),
|
||||
@@ -17,9 +16,7 @@ public static partial class PoolManager
|
||||
(CCVars.HolidaysEnabled.Name, "false"),
|
||||
(CCVars.GameMap.Name, TestMap),
|
||||
(CCVars.AdminLogsQueueSendDelay.Name, "0"),
|
||||
(CVars.NetPVS.Name, "false"),
|
||||
(CCVars.NPCMaxUpdates.Name, "999999"),
|
||||
(CVars.ThreadParallelCount.Name, "1"),
|
||||
(CCVars.GameRoleTimers.Name, "false"),
|
||||
(CCVars.GameRoleLoadoutTimers.Name, "false"),
|
||||
(CCVars.GameRoleWhitelist.Name, "false"),
|
||||
@@ -30,49 +27,13 @@ public static partial class PoolManager
|
||||
(CCVars.ProcgenPreload.Name, "false"),
|
||||
(CCVars.WorldgenEnabled.Name, "false"),
|
||||
(CCVars.GatewayGeneratorEnabled.Name, "false"),
|
||||
(CVars.ReplayClientRecordingEnabled.Name, "false"),
|
||||
(CVars.ReplayServerRecordingEnabled.Name, "false"),
|
||||
(CCVars.GameDummyTicker.Name, "true"),
|
||||
(CCVars.GameLobbyEnabled.Name, "false"),
|
||||
(CCVars.ConfigPresetDevelopment.Name, "false"),
|
||||
(CCVars.AdminLogsEnabled.Name, "false"),
|
||||
(CCVars.AutosaveEnabled.Name, "false"),
|
||||
(CVars.NetBufferSize.Name, "0"),
|
||||
(CCVars.InteractionRateLimitCount.Name, "9999999"),
|
||||
(CCVars.InteractionRateLimitPeriod.Name, "0.1"),
|
||||
(CCVars.MovementMobPushing.Name, "false"),
|
||||
};
|
||||
|
||||
public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
|
||||
{
|
||||
var cfg = instance.ResolveDependency<IConfigurationManager>();
|
||||
await instance.WaitPost(() =>
|
||||
{
|
||||
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
|
||||
cfg.SetCVar(CCVars.GameDummyTicker, settings.UseDummyTicker);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
|
||||
cfg.SetCVar(CCVars.GameLobbyEnabled, settings.InLobby);
|
||||
|
||||
if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
|
||||
cfg.SetCVar(CVars.NetInterp, settings.DisableInterpolate);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
|
||||
cfg.SetCVar(CCVars.GameMap, settings.Map);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
|
||||
cfg.SetCVar(CCVars.AdminLogsEnabled, settings.AdminLogsEnabled);
|
||||
|
||||
if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
|
||||
cfg.SetCVar(CVars.NetInterp, !settings.DisableInterpolate);
|
||||
});
|
||||
}
|
||||
|
||||
private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
|
||||
{
|
||||
foreach (var (cvar, value) in TestCvars)
|
||||
{
|
||||
options.CVarOverrides[cvar] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
// Partial class for handling the discovering and storing test prototypes.
|
||||
public static partial class PoolManager
|
||||
{
|
||||
private static List<string> _testPrototypes = new();
|
||||
|
||||
private const BindingFlags Flags = BindingFlags.Static
|
||||
| BindingFlags.NonPublic
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.DeclaredOnly;
|
||||
|
||||
private static void DiscoverTestPrototypes(Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
foreach (var field in type.GetFields(Flags))
|
||||
{
|
||||
if (!field.HasCustomAttribute<TestPrototypesAttribute>())
|
||||
continue;
|
||||
|
||||
var val = field.GetValue(null);
|
||||
if (val is not string str)
|
||||
throw new Exception($"TestPrototypeAttribute is only valid on non-null string fields");
|
||||
|
||||
_testPrototypes.Add(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,373 +1,17 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Content.Client.IoC;
|
||||
using Content.Client.Parallax.Managers;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.IntegrationTests.Tests;
|
||||
using Content.IntegrationTests.Tests.Destructible;
|
||||
using Content.IntegrationTests.Tests.DeviceNetwork;
|
||||
using Content.IntegrationTests.Tests.Interaction.Click;
|
||||
using Robust.Client;
|
||||
using Robust.Server;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
|
||||
/// </summary>
|
||||
// The static class exist to avoid breaking changes
|
||||
public static partial class PoolManager
|
||||
{
|
||||
public static readonly ContentPoolManager Instance = new();
|
||||
public const string TestMap = "Empty";
|
||||
private static int _pairId;
|
||||
private static readonly object PairLock = new();
|
||||
private static bool _initialized;
|
||||
|
||||
// Pair, IsBorrowed
|
||||
private static readonly Dictionary<TestPair, bool> Pairs = new();
|
||||
private static bool _dead;
|
||||
private static Exception? _poolFailureReason;
|
||||
|
||||
private static HashSet<Assembly> _contentAssemblies = default!;
|
||||
|
||||
public static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
|
||||
PoolSettings poolSettings,
|
||||
TextWriter testOut)
|
||||
{
|
||||
var options = new RobustIntegrationTest.ServerIntegrationOptions
|
||||
{
|
||||
ContentStart = true,
|
||||
Options = new ServerOptions()
|
||||
{
|
||||
LoadConfigAndUserData = false,
|
||||
LoadContentResources = !poolSettings.NoLoadContent,
|
||||
},
|
||||
ContentAssemblies = _contentAssemblies.ToArray()
|
||||
};
|
||||
|
||||
var logHandler = new PoolTestLogHandler("SERVER");
|
||||
logHandler.ActivateContext(testOut);
|
||||
options.OverrideLogHandler = () => logHandler;
|
||||
|
||||
options.BeforeStart += () =>
|
||||
{
|
||||
// Server-only systems (i.e., systems that subscribe to events with server-only components)
|
||||
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
|
||||
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
|
||||
|
||||
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
|
||||
IoCManager.Resolve<IConfigurationManager>()
|
||||
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
|
||||
};
|
||||
|
||||
SetDefaultCVars(options);
|
||||
var server = new RobustIntegrationTest.ServerIntegrationInstance(options);
|
||||
await server.WaitIdleAsync();
|
||||
await SetupCVars(server, poolSettings);
|
||||
return (server, logHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This shuts down the pool, and disposes all the server/client pairs.
|
||||
/// This is a one time operation to be used when the testing program is exiting.
|
||||
/// </summary>
|
||||
public static void Shutdown()
|
||||
{
|
||||
List<TestPair> localPairs;
|
||||
lock (PairLock)
|
||||
{
|
||||
if (_dead)
|
||||
return;
|
||||
_dead = true;
|
||||
localPairs = Pairs.Keys.ToList();
|
||||
}
|
||||
|
||||
foreach (var pair in localPairs)
|
||||
{
|
||||
pair.Kill();
|
||||
}
|
||||
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
public static string DeathReport()
|
||||
{
|
||||
lock (PairLock)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var pairs = Pairs.Keys.OrderBy(pair => pair.Id);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
var borrowed = Pairs[pair];
|
||||
builder.AppendLine($"Pair {pair.Id}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}");
|
||||
for (var i = 0; i < pair.TestHistory.Count; i++)
|
||||
{
|
||||
builder.AppendLine($"#{i}: {pair.TestHistory[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<(RobustIntegrationTest.ClientIntegrationInstance, PoolTestLogHandler)> GenerateClient(
|
||||
PoolSettings poolSettings,
|
||||
TextWriter testOut)
|
||||
{
|
||||
var options = new RobustIntegrationTest.ClientIntegrationOptions
|
||||
{
|
||||
FailureLogLevel = LogLevel.Warning,
|
||||
ContentStart = true,
|
||||
ContentAssemblies = new[]
|
||||
{
|
||||
typeof(Shared.Entry.EntryPoint).Assembly,
|
||||
typeof(Client.Entry.EntryPoint).Assembly,
|
||||
typeof(PoolManager).Assembly,
|
||||
}
|
||||
};
|
||||
|
||||
if (poolSettings.NoLoadContent)
|
||||
{
|
||||
Assert.Warn("NoLoadContent does not work on the client, ignoring");
|
||||
}
|
||||
|
||||
options.Options = new GameControllerOptions()
|
||||
{
|
||||
LoadConfigAndUserData = false,
|
||||
// LoadContentResources = !poolSettings.NoLoadContent
|
||||
};
|
||||
|
||||
var logHandler = new PoolTestLogHandler("CLIENT");
|
||||
logHandler.ActivateContext(testOut);
|
||||
options.OverrideLogHandler = () => logHandler;
|
||||
|
||||
options.BeforeStart += () =>
|
||||
{
|
||||
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
|
||||
{
|
||||
ClientBeforeIoC = () =>
|
||||
{
|
||||
// do not register extra systems or components here -- they will get cleared when the client is
|
||||
// disconnected. just use reflection.
|
||||
IoCManager.Register<IParallaxManager, DummyParallaxManager>(true);
|
||||
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
|
||||
IoCManager.Resolve<IConfigurationManager>()
|
||||
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SetDefaultCVars(options);
|
||||
var client = new RobustIntegrationTest.ClientIntegrationInstance(options);
|
||||
await client.WaitIdleAsync();
|
||||
await SetupCVars(client, poolSettings);
|
||||
return (client, logHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Pair.TestPair"/>, which can be used to get access to a server, and client <see cref="Pair.TestPair"/>
|
||||
/// </summary>
|
||||
/// <param name="poolSettings">See <see cref="PoolSettings"/></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<TestPair> GetServerClient(
|
||||
PoolSettings? poolSettings = null,
|
||||
ITestContextLike? testContext = null)
|
||||
{
|
||||
return await GetServerClientPair(
|
||||
poolSettings ?? new PoolSettings(),
|
||||
testContext ?? new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out));
|
||||
}
|
||||
|
||||
private static string GetDefaultTestName(ITestContextLike testContext)
|
||||
{
|
||||
return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
|
||||
}
|
||||
|
||||
private static async Task<TestPair> GetServerClientPair(
|
||||
PoolSettings poolSettings,
|
||||
ITestContextLike testContext)
|
||||
{
|
||||
if (!_initialized)
|
||||
throw new InvalidOperationException($"Pool manager has not been initialized");
|
||||
|
||||
// Trust issues with the AsyncLocal that backs this.
|
||||
var testOut = testContext.Out;
|
||||
|
||||
DieIfPoolFailure();
|
||||
var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext);
|
||||
var poolRetrieveTimeWatch = new Stopwatch();
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
|
||||
TestPair? pair = null;
|
||||
try
|
||||
{
|
||||
poolRetrieveTimeWatch.Start();
|
||||
if (poolSettings.MustBeNew)
|
||||
{
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
|
||||
pair = await CreateServerClientPair(poolSettings, testOut);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair");
|
||||
pair = GrabOptimalPair(poolSettings);
|
||||
if (pair != null)
|
||||
{
|
||||
pair.ActivateContext(testOut);
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
|
||||
var canSkip = pair.Settings.CanFastRecycle(poolSettings);
|
||||
|
||||
if (canSkip)
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
|
||||
await SetupCVars(pair.Client, poolSettings);
|
||||
await SetupCVars(pair.Server, poolSettings);
|
||||
await pair.RunTicksSync(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
|
||||
await pair.CleanPooledPair(poolSettings, testOut);
|
||||
}
|
||||
|
||||
await pair.RunTicksSync(5);
|
||||
await pair.SyncTicks(targetDelta: 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool");
|
||||
pair = await CreateServerClientPair(poolSettings, testOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pair != null && pair.TestHistory.Count > 0)
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History Start");
|
||||
for (var i = 0; i < pair.TestHistory.Count; i++)
|
||||
{
|
||||
await testOut.WriteLineAsync($"- Pair {pair.Id} Test #{i}: {pair.TestHistory[i]}");
|
||||
}
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History End");
|
||||
}
|
||||
}
|
||||
|
||||
pair.ValidateSettings(poolSettings);
|
||||
|
||||
var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
|
||||
|
||||
pair.ClearModifiedCvars();
|
||||
pair.Settings = poolSettings;
|
||||
pair.TestHistory.Add(currentTestName);
|
||||
pair.SetupSeed();
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
|
||||
|
||||
pair.Watch.Restart();
|
||||
return pair;
|
||||
}
|
||||
|
||||
private static TestPair? GrabOptimalPair(PoolSettings poolSettings)
|
||||
{
|
||||
lock (PairLock)
|
||||
{
|
||||
TestPair? fallback = null;
|
||||
foreach (var pair in Pairs.Keys)
|
||||
{
|
||||
if (Pairs[pair])
|
||||
continue;
|
||||
|
||||
if (!pair.Settings.CanFastRecycle(poolSettings))
|
||||
{
|
||||
fallback = pair;
|
||||
continue;
|
||||
}
|
||||
|
||||
pair.Use();
|
||||
Pairs[pair] = true;
|
||||
return pair;
|
||||
}
|
||||
|
||||
if (fallback != null)
|
||||
{
|
||||
fallback.Use();
|
||||
Pairs[fallback!] = true;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by TestPair after checking the server/client pair, Don't use this.
|
||||
/// </summary>
|
||||
public static void NoCheckReturn(TestPair pair)
|
||||
{
|
||||
lock (PairLock)
|
||||
{
|
||||
if (pair.State == TestPair.PairState.Dead)
|
||||
Pairs.Remove(pair);
|
||||
else if (pair.State == TestPair.PairState.Ready)
|
||||
Pairs[pair] = false;
|
||||
else
|
||||
throw new InvalidOperationException($"Attempted to return a pair in an invalid state. Pair: {pair.Id}. State: {pair.State}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void DieIfPoolFailure()
|
||||
{
|
||||
if (_poolFailureReason != null)
|
||||
{
|
||||
// If the _poolFailureReason is not null, we can assume at least one test failed.
|
||||
// So we say inconclusive so we don't add more failed tests to search through.
|
||||
Assert.Inconclusive(@$"
|
||||
In a different test, the pool manager had an exception when trying to create a server/client pair.
|
||||
Instead of risking that the pool manager will fail at creating a server/client pairs for every single test,
|
||||
we are just going to end this here to save a lot of time. This is the exception that started this:\n {_poolFailureReason}");
|
||||
}
|
||||
|
||||
if (_dead)
|
||||
{
|
||||
// If Pairs is null, we ran out of time, we can't assume a test failed.
|
||||
// So we are going to tell it all future tests are a failure.
|
||||
Assert.Fail("The pool was shut down");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<TestPair> CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = Interlocked.Increment(ref _pairId);
|
||||
var pair = new TestPair(id);
|
||||
await pair.Initialize(poolSettings, testOut, _testPrototypes);
|
||||
pair.Use();
|
||||
await pair.RunTicksSync(5);
|
||||
await pair.SyncTicks(targetDelta: 1);
|
||||
return pair;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_poolFailureReason = ex;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a server, or a client until a condition is true
|
||||
@@ -423,29 +67,42 @@ we are just going to end this here to save a lot of time. This is the exception
|
||||
Assert.That(passed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the pool manager.
|
||||
/// </summary>
|
||||
/// <param name="extraAssemblies">Assemblies to search for to discover extra prototypes and systems.</param>
|
||||
public static void Startup(params Assembly[] extraAssemblies)
|
||||
public static async Task<TestPair> GetServerClient(
|
||||
PoolSettings? settings = null,
|
||||
ITestContextLike? testContext = null)
|
||||
{
|
||||
if (_initialized)
|
||||
throw new InvalidOperationException("Already initialized");
|
||||
return await Instance.GetPair(settings, testContext);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
_contentAssemblies =
|
||||
[
|
||||
typeof(Shared.Entry.EntryPoint).Assembly,
|
||||
typeof(Server.Entry.EntryPoint).Assembly,
|
||||
typeof(PoolManager).Assembly
|
||||
];
|
||||
_contentAssemblies.UnionWith(extraAssemblies);
|
||||
public static void Startup(params Assembly[] extra)
|
||||
=> Instance.Startup(extra);
|
||||
|
||||
_testPrototypes.Clear();
|
||||
DiscoverTestPrototypes(typeof(PoolManager).Assembly);
|
||||
foreach (var assembly in extraAssemblies)
|
||||
{
|
||||
DiscoverTestPrototypes(assembly);
|
||||
}
|
||||
public static void Shutdown() => Instance.Shutdown();
|
||||
public static string DeathReport() => Instance.DeathReport();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
|
||||
/// </summary>
|
||||
public sealed class ContentPoolManager : PoolManager<TestPair>
|
||||
{
|
||||
public override PairSettings DefaultSettings => new PoolSettings();
|
||||
protected override string GetDefaultTestName(ITestContextLike testContext)
|
||||
{
|
||||
return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
|
||||
}
|
||||
|
||||
public override void Startup(params Assembly[] extraAssemblies)
|
||||
{
|
||||
DefaultCvars.AddRange(PoolManager.TestCvars);
|
||||
|
||||
var shared = extraAssemblies
|
||||
.Append(typeof(Shared.Entry.EntryPoint).Assembly)
|
||||
.Append(typeof(PoolManager).Assembly)
|
||||
.ToArray();
|
||||
|
||||
Startup([typeof(Client.Entry.EntryPoint).Assembly],
|
||||
[typeof(Server.Entry.EntryPoint).Assembly],
|
||||
shared);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,31 @@
|
||||
#nullable enable
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for the pooled server, and client pair.
|
||||
/// Some options are for changing the pair, and others are
|
||||
/// so the pool can properly clean up what you borrowed.
|
||||
/// </summary>
|
||||
public sealed class PoolSettings
|
||||
/// <inheritdoc/>
|
||||
public sealed class PoolSettings : PairSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Set to true if the test will ruin the server/client pair.
|
||||
/// </summary>
|
||||
public bool Destructive { get; init; }
|
||||
public override bool Connected
|
||||
{
|
||||
get => _connected || InLobby;
|
||||
init => _connected = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be created fresh.
|
||||
/// </summary>
|
||||
public bool Fresh { get; init; }
|
||||
private readonly bool _dummyTicker = true;
|
||||
private readonly bool _connected;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server should be using a dummy ticker. Ignored if <see cref="InLobby"/> is true.
|
||||
/// </summary>
|
||||
public bool DummyTicker { get; init; } = true;
|
||||
public bool DummyTicker
|
||||
{
|
||||
get => _dummyTicker && !InLobby;
|
||||
init => _dummyTicker = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, this enables the creation of admin logs during the test.
|
||||
/// </summary>
|
||||
public bool AdminLogsEnabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be connected from each other.
|
||||
/// Defaults to disconnected as it makes dirty recycling slightly faster.
|
||||
/// If <see cref="InLobby"/> is true, this option is ignored.
|
||||
/// </summary>
|
||||
public bool Connected { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be in the lobby.
|
||||
/// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
|
||||
@@ -53,81 +41,22 @@ public sealed class PoolSettings
|
||||
/// </summary>
|
||||
public bool NoLoadContent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// This will return a server-client pair that has not loaded test prototypes.
|
||||
/// Try avoiding this whenever possible, as this will always create & destroy a new pair.
|
||||
/// Use <see cref="Pair.TestPair.IsTestPrototype(Robust.Shared.Prototypes.EntityPrototype)"/> if you need to exclude test prototypees.
|
||||
/// </summary>
|
||||
public bool NoLoadTestPrototypes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true to disable the NetInterp CVar on the given server/client pair
|
||||
/// </summary>
|
||||
public bool DisableInterpolate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true to always clean up the server/client pair before giving it to another borrower
|
||||
/// </summary>
|
||||
public bool Dirty { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to the path of a map to have the given server/client pair load the map.
|
||||
/// </summary>
|
||||
public string Map { get; init; } = PoolManager.TestMap;
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the test name detection, and uses this in the test history instead
|
||||
/// </summary>
|
||||
public string? TestName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
|
||||
/// </summary>
|
||||
public int? ServerSeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
|
||||
/// </summary>
|
||||
public int? ClientSeed { get; set; }
|
||||
|
||||
#region Inferred Properties
|
||||
|
||||
/// <summary>
|
||||
/// If the returned pair must not be reused
|
||||
/// </summary>
|
||||
public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// If the given pair must be brand new
|
||||
/// </summary>
|
||||
public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
|
||||
|
||||
public bool UseDummyTicker => !InLobby && DummyTicker;
|
||||
|
||||
public bool ShouldBeConnected => InLobby || Connected;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Tries to guess if we can skip recycling the server/client pair.
|
||||
/// </summary>
|
||||
/// <param name="nextSettings">The next set of settings the old pair will be set to</param>
|
||||
/// <returns>If we can skip cleaning it up</returns>
|
||||
public bool CanFastRecycle(PoolSettings nextSettings)
|
||||
public override bool CanFastRecycle(PairSettings nextSettings)
|
||||
{
|
||||
if (MustNotBeReused)
|
||||
throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
|
||||
if (!base.CanFastRecycle(nextSettings))
|
||||
return false;
|
||||
|
||||
if (nextSettings.MustBeNew)
|
||||
throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
|
||||
|
||||
if (Dirty)
|
||||
if (nextSettings is not PoolSettings next)
|
||||
return false;
|
||||
|
||||
// Check that certain settings match.
|
||||
return !ShouldBeConnected == !nextSettings.ShouldBeConnected
|
||||
&& UseDummyTicker == nextSettings.UseDummyTicker
|
||||
&& Map == nextSettings.Map
|
||||
&& InLobby == nextSettings.InLobby;
|
||||
return DummyTicker == next.DummyTicker
|
||||
&& Map == next.Map
|
||||
&& InLobby == next.InLobby;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
using System.IO;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Log handler intended for pooled integration tests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class logs to two places: an NUnit <see cref="TestContext"/>
|
||||
/// (so it nicely gets attributed to a test in your IDE),
|
||||
/// and an in-memory ring buffer for diagnostic purposes.
|
||||
/// If test pooling breaks, the ring buffer can be used to see what the broken instance has gone through.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class PoolTestLogHandler : ILogHandler
|
||||
{
|
||||
private readonly string? _prefix;
|
||||
|
||||
private RStopwatch _stopwatch;
|
||||
|
||||
public TextWriter? ActiveContext { get; private set; }
|
||||
|
||||
public LogLevel? FailureLevel { get; set; }
|
||||
|
||||
public PoolTestLogHandler(string? prefix)
|
||||
{
|
||||
_prefix = prefix != null ? $"{prefix}: " : "";
|
||||
}
|
||||
|
||||
public bool ShuttingDown;
|
||||
|
||||
public void Log(string sawmillName, LogEvent message)
|
||||
{
|
||||
var level = message.Level.ToRobust();
|
||||
|
||||
if (ShuttingDown && (FailureLevel == null || level < FailureLevel))
|
||||
return;
|
||||
|
||||
if (ActiveContext is not { } testContext)
|
||||
{
|
||||
// If this gets hit it means something is logging to this instance while it's "between" tests.
|
||||
// This is a bug in either the game or the testing system, and must always be investigated.
|
||||
throw new InvalidOperationException("Log to pool test log handler without active test context");
|
||||
}
|
||||
|
||||
var name = LogMessage.LogLevelToName(level);
|
||||
var seconds = _stopwatch.Elapsed.TotalSeconds;
|
||||
var rendered = message.RenderMessage();
|
||||
var line = $"{_prefix}{seconds:F3}s [{name}] {sawmillName}: {rendered}";
|
||||
|
||||
testContext.WriteLine(line);
|
||||
|
||||
if (FailureLevel == null || level < FailureLevel)
|
||||
return;
|
||||
|
||||
testContext.Flush();
|
||||
Assert.Fail($"{line} Exception: {message.Exception}");
|
||||
}
|
||||
|
||||
public void ClearContext()
|
||||
{
|
||||
ActiveContext = null;
|
||||
}
|
||||
|
||||
public void ActivateContext(TextWriter context)
|
||||
{
|
||||
_stopwatch.Restart();
|
||||
ActiveContext = context;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute that indicates that a string contains yaml prototype data that should be loaded by integration tests.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
[MeansImplicitUse]
|
||||
public sealed class TestPrototypesAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -54,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Access
|
||||
system.ClearDenyTags(reader);
|
||||
|
||||
// test one list
|
||||
system.AddAccess(reader, "A");
|
||||
system.TryAddAccess(reader, "A");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
|
||||
@@ -62,10 +62,10 @@ namespace Content.IntegrationTests.Tests.Access
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
|
||||
});
|
||||
system.ClearAccesses(reader);
|
||||
system.TryClearAccesses(reader);
|
||||
|
||||
// test one list - two items
|
||||
system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
|
||||
system.TryAddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
|
||||
@@ -73,14 +73,14 @@ namespace Content.IntegrationTests.Tests.Access
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
|
||||
});
|
||||
system.ClearAccesses(reader);
|
||||
system.TryClearAccesses(reader);
|
||||
|
||||
// test two list
|
||||
var accesses = new List<HashSet<ProtoId<AccessLevelPrototype>>>() {
|
||||
new HashSet<ProtoId<AccessLevelPrototype>> () { "A" },
|
||||
new HashSet<ProtoId<AccessLevelPrototype>> () { "B", "C" }
|
||||
};
|
||||
system.AddAccesses(reader, accesses);
|
||||
system.TryAddAccesses(reader, accesses);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
|
||||
@@ -90,10 +90,10 @@ namespace Content.IntegrationTests.Tests.Access
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B", "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
|
||||
});
|
||||
system.ClearAccesses(reader);
|
||||
system.TryClearAccesses(reader);
|
||||
|
||||
// test deny list
|
||||
system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
|
||||
system.TryAddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
|
||||
system.AddDenyTag(reader, "B");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
@@ -102,7 +102,7 @@ namespace Content.IntegrationTests.Tests.Access
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
|
||||
});
|
||||
system.ClearAccesses(reader);
|
||||
system.TryClearAccesses(reader);
|
||||
system.ClearDenyTags(reader);
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
|
||||
135
Content.IntegrationTests/Tests/Chasm/ChasmTest.cs
Normal file
135
Content.IntegrationTests/Tests/Chasm/ChasmTest.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using Content.IntegrationTests.Tests.Movement;
|
||||
using Content.Shared.Chasm;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Weapons.Misc;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Chasm;
|
||||
|
||||
/// <summary>
|
||||
/// A test for chasms, which delete entities when a player walks over them.
|
||||
/// </summary>
|
||||
[TestOf(typeof(ChasmComponent))]
|
||||
public sealed class ChasmTest : MovementTest
|
||||
{
|
||||
private readonly EntProtoId _chasmProto = "FloorChasmEntity";
|
||||
private readonly EntProtoId _catWalkProto = "Catwalk";
|
||||
private readonly EntProtoId _grapplingGunProto = "WeaponGrapplingGun";
|
||||
|
||||
/// <summary>
|
||||
/// Test that a player falls into the chasm when walking over it.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChasmFallTest()
|
||||
{
|
||||
// Spawn a chasm.
|
||||
await SpawnTarget(_chasmProto);
|
||||
Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
|
||||
|
||||
// Attempt (and fail) to walk past the chasm.
|
||||
// If you are modifying the default value of ChasmFallingComponent.DeletionTime this time might need to be adjusted.
|
||||
await Move(DirectionFlag.East, 0.5f);
|
||||
|
||||
// We should be falling right now.
|
||||
Assert.That(TryComp<ChasmFallingComponent>(Player, out var falling), "Player is not falling after walking over a chasm.");
|
||||
|
||||
var fallTime = (float)falling.DeletionTime.TotalSeconds;
|
||||
|
||||
// Wait until we get deleted.
|
||||
await Pair.RunSeconds(fallTime);
|
||||
|
||||
// Check that the player was deleted.
|
||||
AssertDeleted(Player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that a catwalk placed over a chasm will protect a player from falling.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChasmCatwalkTest()
|
||||
{
|
||||
// Spawn a chasm.
|
||||
await SpawnTarget(_chasmProto);
|
||||
Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
|
||||
|
||||
// Spawn a catwalk over the chasm.
|
||||
var catwalk = await Spawn(_catWalkProto);
|
||||
|
||||
// Attempt to walk past the chasm.
|
||||
await Move(DirectionFlag.East, 1f);
|
||||
|
||||
// We should be on the other side.
|
||||
Assert.That(Delta(), Is.LessThan(-0.5), "Player was unable to walk over a chasm with a catwalk.");
|
||||
|
||||
// Check that the player is not deleted.
|
||||
AssertExists(Player);
|
||||
|
||||
// Make sure the player is not falling right now.
|
||||
Assert.That(HasComp<ChasmFallingComponent>(Player), Is.False, "Player has ChasmFallingComponent after walking over a catwalk.");
|
||||
|
||||
// Delete the catwalk.
|
||||
await Delete(catwalk);
|
||||
|
||||
// Attempt (and fail) to walk past the chasm.
|
||||
await Move(DirectionFlag.West, 1f);
|
||||
|
||||
// Wait until we get deleted.
|
||||
await Pair.RunSeconds(5f);
|
||||
|
||||
// Check that the player was deleted
|
||||
AssertDeleted(Player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a player is able to cross a chasm by using a grappling gun.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChasmGrappleTest()
|
||||
{
|
||||
// Spawn a chasm.
|
||||
await SpawnTarget(_chasmProto);
|
||||
Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
|
||||
|
||||
// Give the player a grappling gun.
|
||||
var grapplingGun = await PlaceInHands(_grapplingGunProto);
|
||||
await Pair.RunSeconds(2f); // guns have a cooldown when picking them up
|
||||
|
||||
// Shoot at the wall to the right.
|
||||
Assert.That(WallRight, Is.Not.Null, "No wall to shoot at!");
|
||||
await AttemptShoot(WallRight);
|
||||
await Pair.RunSeconds(2f);
|
||||
|
||||
// Check that the grappling hook is embedded into the wall.
|
||||
Assert.That(TryComp<GrapplingGunComponent>(grapplingGun, out var grapplingGunComp), "Grappling gun did not have GrapplingGunComponent.");
|
||||
Assert.That(grapplingGunComp.Projectile, Is.Not.Null, "Grappling gun projectile does not exist.");
|
||||
Assert.That(SEntMan.TryGetComponent<EmbeddableProjectileComponent>(grapplingGunComp.Projectile, out var embeddable), "Grappling hook was not embeddable.");
|
||||
Assert.That(embeddable.EmbeddedIntoUid, Is.EqualTo(ToServer(WallRight)), "Grappling hook was not embedded into the wall.");
|
||||
|
||||
// Check that the player is hooked.
|
||||
var grapplingSystem = SEntMan.System<SharedGrapplingGunSystem>();
|
||||
Assert.That(grapplingSystem.IsEntityHooked(SPlayer), "Player is not hooked to the wall.");
|
||||
Assert.That(HasComp<JointRelayTargetComponent>(Player), "Player does not have the JointRelayTargetComponent after using a grappling gun.");
|
||||
|
||||
// Attempt to walk past the chasm.
|
||||
await Move(DirectionFlag.East, 1f);
|
||||
|
||||
// We should be on the other side.
|
||||
Assert.That(Delta(), Is.LessThan(-0.5), "Player was unable to walk over a chasm with a grappling gun.");
|
||||
|
||||
// Check that the player is not deleted.
|
||||
AssertExists(Player);
|
||||
|
||||
// Make sure the player is not falling right now.
|
||||
Assert.That(HasComp<ChasmFallingComponent>(Player), Is.False, "Player has ChasmFallingComponent after moving over a chasm with a grappling gun.");
|
||||
|
||||
// Drop the grappling gun.
|
||||
await Drop();
|
||||
|
||||
// Check that the player no longer hooked.
|
||||
Assert.That(grapplingSystem.IsEntityHooked(SPlayer), Is.False, "Player still hooked after dropping the grappling gun.");
|
||||
Assert.That(HasComp<JointRelayTargetComponent>(Player), Is.False, "Player still has the JointRelayTargetComponent after dropping the grappling gun.");
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Content.Server.Construction.Components;
|
||||
using Content.Server.Gravity;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Item;
|
||||
@@ -85,7 +86,7 @@ public abstract partial class InteractionTest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn an entity entity and set it as the target.
|
||||
/// Spawn an entity at the target coordinates and set it as the target.
|
||||
/// </summary>
|
||||
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
|
||||
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
|
||||
@@ -103,6 +104,22 @@ public abstract partial class InteractionTest
|
||||
}
|
||||
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
|
||||
|
||||
/// <summary>
|
||||
/// Spawn an entity entity at the target coordinates without setting it as the target.
|
||||
/// </summary>
|
||||
protected async Task<NetEntity> Spawn(string prototype)
|
||||
{
|
||||
var entity = NetEntity.Invalid;
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
|
||||
});
|
||||
|
||||
await RunTicks(5);
|
||||
AssertPrototype(prototype, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn an entity in preparation for deconstruction
|
||||
/// </summary>
|
||||
@@ -386,6 +403,119 @@ public abstract partial class InteractionTest
|
||||
|
||||
#endregion
|
||||
|
||||
# region Combat
|
||||
/// <summary>
|
||||
/// Returns if the player is currently in combat mode.
|
||||
/// </summary>
|
||||
protected bool IsInCombatMode()
|
||||
{
|
||||
if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
|
||||
{
|
||||
Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
|
||||
return false;
|
||||
}
|
||||
|
||||
return combat.IsInCombatMode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the combat mode for the player.
|
||||
/// </summary>
|
||||
protected async Task SetCombatMode(bool enabled)
|
||||
{
|
||||
if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
|
||||
{
|
||||
Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
|
||||
return;
|
||||
}
|
||||
|
||||
await Server.WaitPost(() => SCombatMode.SetInCombatMode(SPlayer, enabled, combat));
|
||||
await RunTicks(1);
|
||||
|
||||
Assert.That(combat.IsInCombatMode, Is.EqualTo(enabled), $"Player could not set combate mode to {enabled}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make the player shoot with their currently held gun.
|
||||
/// The player needs to be able to enter combat mode for this.
|
||||
/// This does not pass a target entity into the GunSystem, meaning that targets that
|
||||
/// need to be aimed at directly won't be hit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Guns have a cooldown when picking them up.
|
||||
/// So make sure to wait a little after spawning a gun in the player's hand or this will fail.
|
||||
/// </remarks>
|
||||
/// <param name="target">The target coordinates to shoot at. Defaults to the current <see cref="TargetCoords"/>.</param>
|
||||
/// <param name="assert">If true this method will assert that the gun was successfully fired.</param>
|
||||
protected async Task AttemptShoot(NetCoordinates? target = null, bool assert = true)
|
||||
{
|
||||
var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
|
||||
|
||||
if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
|
||||
{
|
||||
Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter combat mode before shooting.
|
||||
var wasInCombatMode = IsInCombatMode();
|
||||
await SetCombatMode(true);
|
||||
|
||||
Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, actualTarget);
|
||||
if (assert)
|
||||
Assert.That(success, "Gun failed to shoot.");
|
||||
});
|
||||
await RunTicks(1);
|
||||
|
||||
// If the player was not in combat mode before then disable it again.
|
||||
await SetCombatMode(wasInCombatMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make the player shoot with their currently held gun.
|
||||
/// The player needs to be able to enter combat mode for this.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Guns have a cooldown when picking them up.
|
||||
/// So make sure to wait a little after spawning a gun in the player's hand or this will fail.
|
||||
/// </remarks>
|
||||
/// <param name="target">The target entity to shoot at. Defaults to the current <see cref="Target"/> entity.</param>
|
||||
/// <param name="assert">If true this method will assert that the gun was successfully fired.</param>
|
||||
protected async Task AttemptShoot(NetEntity? target = null, bool assert = true)
|
||||
{
|
||||
var actualTarget = target ?? Target;
|
||||
Assert.That(actualTarget, Is.Not.Null, "No target to shoot at!");
|
||||
|
||||
if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
|
||||
{
|
||||
Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter combat mode before shooting.
|
||||
var wasInCombatMode = IsInCombatMode();
|
||||
await SetCombatMode(true);
|
||||
|
||||
Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, Position(actualTarget!.Value), ToServer(actualTarget));
|
||||
if (assert)
|
||||
Assert.That(success, "Gun failed to shoot.");
|
||||
});
|
||||
await RunTicks(1);
|
||||
|
||||
// If the player was not in combat mode before then disable it again.
|
||||
await SetCombatMode(wasInCombatMode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Wait for any currently active DoAfters to finish.
|
||||
/// </summary>
|
||||
@@ -746,6 +876,18 @@ public abstract partial class InteractionTest
|
||||
return SEntMan.GetComponent<T>(ToServer(target!.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method to check if the target has a component on the server.
|
||||
/// </summary>
|
||||
protected bool HasComp<T>(NetEntity? target = null) where T : IComponent
|
||||
{
|
||||
target ??= Target;
|
||||
if (target == null)
|
||||
Assert.Fail("No target specified");
|
||||
|
||||
return SEntMan.HasComponent<T>(ToServer(target));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Comp{T}"/>
|
||||
protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
|
||||
{
|
||||
@@ -1013,7 +1155,7 @@ public abstract partial class InteractionTest
|
||||
}
|
||||
|
||||
Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
|
||||
return (TControl) control;
|
||||
return (TControl)control;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1177,8 +1319,8 @@ public abstract partial class InteractionTest
|
||||
{
|
||||
var atmosSystem = SEntMan.System<AtmosphereSystem>();
|
||||
var moles = new float[Atmospherics.AdjustedNumberOfGases];
|
||||
moles[(int) Gas.Oxygen] = 21.824779f;
|
||||
moles[(int) Gas.Nitrogen] = 82.10312f;
|
||||
moles[(int)Gas.Oxygen] = 21.824779f;
|
||||
moles[(int)Gas.Nitrogen] = 82.10312f;
|
||||
atmosSystem.SetMapAtmosphere(target, false, new GasMixture(moles, Atmospherics.T20C));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,12 +7,16 @@ using Content.IntegrationTests.Pair;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Server.Stack;
|
||||
using Content.Server.Tools;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Item.ItemToggle;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
@@ -21,8 +25,6 @@ using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.UnitTesting;
|
||||
using Content.Shared.Item.ItemToggle;
|
||||
using Robust.Client.State;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Interaction;
|
||||
|
||||
@@ -107,6 +109,8 @@ public abstract partial class InteractionTest
|
||||
protected SharedMapSystem MapSystem = default!;
|
||||
protected ISawmill SLogger = default!;
|
||||
protected SharedUserInterfaceSystem SUiSys = default!;
|
||||
protected SharedCombatModeSystem SCombatMode = default!;
|
||||
protected SharedGunSystem SGun = default!;
|
||||
|
||||
// CLIENT dependencies
|
||||
protected IEntityManager CEntMan = default!;
|
||||
@@ -124,7 +128,7 @@ public abstract partial class InteractionTest
|
||||
protected HandsComponent Hands = default!;
|
||||
protected DoAfterComponent DoAfters = default!;
|
||||
|
||||
public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds;
|
||||
public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds;
|
||||
|
||||
// Simple mob that has one hand and can perform misc interactions.
|
||||
[TestPrototypes]
|
||||
@@ -149,6 +153,7 @@ public abstract partial class InteractionTest
|
||||
tags:
|
||||
- CanPilot
|
||||
- type: UserInterface
|
||||
- type: CombatMode
|
||||
";
|
||||
|
||||
[SetUp]
|
||||
@@ -163,6 +168,7 @@ public abstract partial class InteractionTest
|
||||
ProtoMan = Server.ResolveDependency<IPrototypeManager>();
|
||||
Factory = Server.ResolveDependency<IComponentFactory>();
|
||||
STiming = Server.ResolveDependency<IGameTiming>();
|
||||
SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
|
||||
HandSys = SEntMan.System<HandsSystem>();
|
||||
InteractSys = SEntMan.System<SharedInteractionSystem>();
|
||||
ToolSys = SEntMan.System<ToolSystem>();
|
||||
@@ -173,20 +179,21 @@ public abstract partial class InteractionTest
|
||||
SConstruction = SEntMan.System<Server.Construction.ConstructionSystem>();
|
||||
STestSystem = SEntMan.System<InteractionTestSystem>();
|
||||
Stack = SEntMan.System<StackSystem>();
|
||||
SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
|
||||
SUiSys = Client.System<SharedUserInterfaceSystem>();
|
||||
SUiSys = SEntMan.System<SharedUserInterfaceSystem>();
|
||||
SCombatMode = SEntMan.System<SharedCombatModeSystem>();
|
||||
SGun = SEntMan.System<SharedGunSystem>();
|
||||
|
||||
// client dependencies
|
||||
CEntMan = Client.ResolveDependency<IEntityManager>();
|
||||
UiMan = Client.ResolveDependency<IUserInterfaceManager>();
|
||||
CTiming = Client.ResolveDependency<IGameTiming>();
|
||||
InputManager = Client.ResolveDependency<IInputManager>();
|
||||
CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
|
||||
InputSystem = CEntMan.System<Robust.Client.GameObjects.InputSystem>();
|
||||
CTestSystem = CEntMan.System<InteractionTestSystem>();
|
||||
CConSys = CEntMan.System<ConstructionSystem>();
|
||||
ExamineSys = CEntMan.System<ExamineSystem>();
|
||||
CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
|
||||
CUiSys = Client.System<SharedUserInterfaceSystem>();
|
||||
CUiSys = CEntMan.System<SharedUserInterfaceSystem>();
|
||||
|
||||
// Setup map.
|
||||
await Pair.CreateTestMap();
|
||||
|
||||
@@ -145,7 +145,7 @@ public sealed class MaterialArbitrageTest
|
||||
|
||||
Dictionary<string, double> priceCache = new();
|
||||
|
||||
Dictionary<string, (Dictionary<string, int> Ents, Dictionary<string, int> Mats)> spawnedOnDestroy = new();
|
||||
Dictionary<string, (Dictionary<string, float> Ents, Dictionary<string, float> Mats)> spawnedOnDestroy = new();
|
||||
|
||||
// cache the compositions of entities
|
||||
// If the entity is refineable (i.e. glass shared can be turned into glass, we take the greater of the two compositions.
|
||||
@@ -217,8 +217,8 @@ public sealed class MaterialArbitrageTest
|
||||
|
||||
var comp = (DestructibleComponent) destructible.Component;
|
||||
|
||||
var spawnedEnts = new Dictionary<string, int>();
|
||||
var spawnedMats = new Dictionary<string, int>();
|
||||
var spawnedEnts = new Dictionary<string, float>();
|
||||
var spawnedMats = new Dictionary<string, float>();
|
||||
|
||||
// This test just blindly assumes that ALL spawn entity behaviors get triggered. In reality, some entities
|
||||
// might only trigger a subset. If that starts being a problem, this test either needs fixing or needs to
|
||||
@@ -233,14 +233,14 @@ public sealed class MaterialArbitrageTest
|
||||
|
||||
foreach (var (key, value) in spawn.Spawn)
|
||||
{
|
||||
spawnedEnts[key] = spawnedEnts.GetValueOrDefault(key) + value.Max;
|
||||
spawnedEnts[key] = spawnedEnts.GetValueOrDefault(key) + (float)(value.Min + value.Max) / 2;
|
||||
|
||||
if (!compositions.TryGetValue(key, out var composition))
|
||||
continue;
|
||||
|
||||
foreach (var (matId, amount) in composition)
|
||||
{
|
||||
spawnedMats[matId] = value.Max * amount + spawnedMats.GetValueOrDefault(matId);
|
||||
spawnedMats[matId] = (float)(value.Min + value.Max) / 2 * amount + spawnedMats.GetValueOrDefault(matId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,7 +451,7 @@ public sealed class MaterialArbitrageTest
|
||||
await server.WaitPost(() => mapSystem.DeleteMap(testMap.MapId));
|
||||
await pair.CleanReturnAsync();
|
||||
|
||||
async Task<double> GetSpawnedPrice(Dictionary<string, int> ents)
|
||||
async Task<double> GetSpawnedPrice(Dictionary<string, float> ents)
|
||||
{
|
||||
double price = 0;
|
||||
foreach (var (id, num) in ents)
|
||||
|
||||
@@ -24,6 +24,15 @@ public abstract class MovementTest : InteractionTest
|
||||
/// </summary>
|
||||
protected virtual bool AddWalls => true;
|
||||
|
||||
/// <summary>
|
||||
/// The wall entity on the left side.
|
||||
/// </summary>
|
||||
protected NetEntity? WallLeft;
|
||||
/// <summary>
|
||||
/// The wall entity on the right side.
|
||||
/// </summary>
|
||||
protected NetEntity? WallRight;
|
||||
|
||||
[SetUp]
|
||||
public override async Task Setup()
|
||||
{
|
||||
@@ -38,8 +47,11 @@ public abstract class MovementTest : InteractionTest
|
||||
|
||||
if (AddWalls)
|
||||
{
|
||||
await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
|
||||
await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0)));
|
||||
var sWallLeft = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
|
||||
var sWallRight = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0)));
|
||||
|
||||
WallLeft = SEntMan.GetNetEntity(sWallLeft);
|
||||
WallRight = SEntMan.GetNetEntity(sWallRight);
|
||||
}
|
||||
|
||||
await AddGravity();
|
||||
|
||||
@@ -20,6 +20,7 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.UnitTesting.Pool;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
@@ -9,6 +9,7 @@ using Content.IntegrationTests;
|
||||
using Content.MapRenderer.Painters;
|
||||
using Content.Server.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.UnitTesting.Pool;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Webp;
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
_adminLogger.Add(LogType.Action, LogImpact.High,
|
||||
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
|
||||
|
||||
_accessReader.SetAccesses(accessReaderEnt.Value, newAccessList);
|
||||
_accessReader.TrySetAccesses(accessReaderEnt.Value, newAccessList);
|
||||
|
||||
var ev = new OnAccessOverriderAccessUpdatedEvent(player);
|
||||
RaiseLocalEvent(component.TargetAccessReaderId, ref ev);
|
||||
|
||||
@@ -7,9 +7,7 @@ using Content.Server.EUI;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Administration;
|
||||
|
||||
@@ -21,7 +19,6 @@ public sealed class BanPanelEui : BaseEui
|
||||
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly IAdminManager _admins = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
@@ -52,7 +49,7 @@ public sealed class BanPanelEui : BaseEui
|
||||
switch (msg)
|
||||
{
|
||||
case BanPanelEuiStateMsg.CreateBanRequest r:
|
||||
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid, r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
|
||||
BanPlayer(r.Ban);
|
||||
break;
|
||||
case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
|
||||
ChangePlayer(r.PlayerUsername);
|
||||
@@ -60,29 +57,26 @@ public sealed class BanPanelEui : BaseEui
|
||||
}
|
||||
}
|
||||
|
||||
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
|
||||
private async void BanPlayer(Ban ban)
|
||||
{
|
||||
if (!_admins.HasAdminFlag(Player, AdminFlags.Ban))
|
||||
{
|
||||
_sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to create a ban with no ban flag");
|
||||
|
||||
return;
|
||||
}
|
||||
if (target == null && string.IsNullOrWhiteSpace(ipAddressString) && hwid == null)
|
||||
|
||||
if (ban.Target == null && string.IsNullOrWhiteSpace(ban.IpAddress) && ban.Hwid == null)
|
||||
{
|
||||
_chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-no-data"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
(IPAddress, int)? addressRange = null;
|
||||
if (ipAddressString is not null)
|
||||
if (ban.IpAddress is not null)
|
||||
{
|
||||
var hid = "0";
|
||||
var split = ipAddressString.Split('/', 2);
|
||||
ipAddressString = split[0];
|
||||
if (split.Length > 1)
|
||||
hid = split[1];
|
||||
|
||||
if (!IPAddress.TryParse(ipAddressString, out var ipAddress) || !uint.TryParse(hid, out var hidInt) || hidInt > Ipv6_CIDR || hidInt > Ipv4_CIDR && ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
||||
if (!IPAddress.TryParse(ban.IpAddress, out var ipAddress) || !uint.TryParse(ban.IpAddressHid, out var hidInt) || hidInt > Ipv6_CIDR || hidInt > Ipv4_CIDR && ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
_chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-invalid-ip"));
|
||||
return;
|
||||
@@ -94,12 +88,12 @@ public sealed class BanPanelEui : BaseEui
|
||||
addressRange = (ipAddress, (int) hidInt);
|
||||
}
|
||||
|
||||
var targetUid = target is not null ? PlayerId : null;
|
||||
addressRange = useLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange;
|
||||
var targetHWid = useLastHwid ? LastHwid : hwid;
|
||||
if (target != null && target != PlayerName || Guid.TryParse(target, out var parsed) && parsed != PlayerId)
|
||||
var targetUid = ban.Target is not null ? PlayerId : null;
|
||||
addressRange = ban.UseLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange;
|
||||
var targetHWid = ban.UseLastHwid ? LastHwid : ban.Hwid;
|
||||
if (ban.Target != null && ban.Target != PlayerName || Guid.TryParse(ban.Target, out var parsed) && parsed != PlayerId)
|
||||
{
|
||||
var located = await _playerLocator.LookupIdByNameOrIdAsync(target);
|
||||
var located = await _playerLocator.LookupIdByNameOrIdAsync(ban.Target);
|
||||
if (located == null)
|
||||
{
|
||||
_chat.DispatchServerMessage(Player, Loc.GetString("cmd-ban-player"));
|
||||
@@ -107,7 +101,7 @@ public sealed class BanPanelEui : BaseEui
|
||||
}
|
||||
targetUid = located.UserId;
|
||||
var targetAddress = located.LastAddress;
|
||||
if (useLastIp && targetAddress != null)
|
||||
if (ban.UseLastIp && targetAddress != null)
|
||||
{
|
||||
if (targetAddress.IsIPv4MappedToIPv6)
|
||||
targetAddress = targetAddress.MapToIPv4();
|
||||
@@ -116,30 +110,50 @@ public sealed class BanPanelEui : BaseEui
|
||||
var hid = targetAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR;
|
||||
addressRange = (targetAddress, hid);
|
||||
}
|
||||
targetHWid = useLastHwid ? located.LastHWId : hwid;
|
||||
targetHWid = ban.UseLastHwid ? located.LastHWId : ban.Hwid;
|
||||
}
|
||||
|
||||
if (roles?.Count > 0)
|
||||
if (ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
foreach (var role in roles)
|
||||
foreach (var role in ban.BannedJobs ?? [])
|
||||
{
|
||||
if (_prototypeManager.HasIndex<JobPrototype>(role))
|
||||
{
|
||||
_banManager.CreateRoleBan(targetUid, target, Player.UserId, addressRange, targetHWid, role, minutes, severity, reason, now);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to issue a job ban with an invalid job: {role}");
|
||||
}
|
||||
_banManager.CreateRoleBan(
|
||||
targetUid,
|
||||
ban.Target,
|
||||
Player.UserId,
|
||||
addressRange,
|
||||
targetHWid,
|
||||
role,
|
||||
ban.BanDurationMinutes,
|
||||
ban.Severity,
|
||||
ban.Reason,
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var role in ban.BannedAntags ?? [])
|
||||
{
|
||||
_banManager.CreateRoleBan(
|
||||
targetUid,
|
||||
ban.Target,
|
||||
Player.UserId,
|
||||
addressRange,
|
||||
targetHWid,
|
||||
role,
|
||||
ban.BanDurationMinutes,
|
||||
ban.Severity,
|
||||
ban.Reason,
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
Close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (erase &&
|
||||
targetUid != null)
|
||||
if (ban.Erase && targetUid is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -152,7 +166,16 @@ public sealed class BanPanelEui : BaseEui
|
||||
}
|
||||
}
|
||||
|
||||
_banManager.CreateServerBan(targetUid, target, Player.UserId, addressRange, targetHWid, minutes, severity, reason);
|
||||
_banManager.CreateServerBan(
|
||||
targetUid,
|
||||
ban.Target,
|
||||
Player.UserId,
|
||||
addressRange,
|
||||
targetHWid,
|
||||
ban.BanDurationMinutes,
|
||||
ban.Severity,
|
||||
ban.Reason
|
||||
);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Round)]
|
||||
public sealed class ReadyAll : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _e = default!;
|
||||
|
||||
public string Command => "readyall";
|
||||
public string Description => "Readies up all players in the lobby, except for observers.";
|
||||
public string Help => $"{Command} | ̣{Command} <ready>";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var ready = true;
|
||||
|
||||
if (args.Length > 0)
|
||||
{
|
||||
ready = bool.Parse(args[0]);
|
||||
}
|
||||
|
||||
var gameTicker = _e.System<GameTicker>();
|
||||
|
||||
|
||||
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
|
||||
{
|
||||
shell.WriteLine("This command can only be ran while in the lobby!");
|
||||
return;
|
||||
}
|
||||
|
||||
gameTicker.ToggleReadyAll(ready);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Content.Server/Administration/Commands/ReadyAllCommand.cs
Normal file
32
Content.Server/Administration/Commands/ReadyAllCommand.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Round)]
|
||||
public sealed class ReadyAllCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
|
||||
public override string Command => "readyall";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var ready = true;
|
||||
|
||||
if (_gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-can-only-run-from-pre-round-lobby"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && !bool.TryParse(args[0], out ready))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
|
||||
return;
|
||||
}
|
||||
|
||||
_gameTicker.ToggleReadyAll(ready);
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,10 @@ public sealed class RoleBanCommand : IConsoleCommand
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
string target;
|
||||
string job;
|
||||
string role;
|
||||
string reason;
|
||||
uint minutes;
|
||||
|
||||
if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), out NoteSeverity severity))
|
||||
{
|
||||
_sawmill ??= _log.GetSawmill("admin.role_ban");
|
||||
@@ -43,30 +44,33 @@ public sealed class RoleBanCommand : IConsoleCommand
|
||||
{
|
||||
case 3:
|
||||
target = args[0];
|
||||
job = args[1];
|
||||
role = args[1];
|
||||
reason = args[2];
|
||||
minutes = 0;
|
||||
|
||||
break;
|
||||
case 4:
|
||||
target = args[0];
|
||||
job = args[1];
|
||||
role = args[1];
|
||||
reason = args[2];
|
||||
|
||||
if (!uint.TryParse(args[3], out minutes))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-roleban-minutes-parse", ("time", args[3]), ("help", Help)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case 5:
|
||||
target = args[0];
|
||||
job = args[1];
|
||||
role = args[1];
|
||||
reason = args[2];
|
||||
|
||||
if (!uint.TryParse(args[3], out minutes))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-roleban-minutes-parse", ("time", args[3]), ("help", Help)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,26 +84,27 @@ public sealed class RoleBanCommand : IConsoleCommand
|
||||
default:
|
||||
shell.WriteError(Loc.GetString("cmd-roleban-arg-count"));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_proto.HasIndex<JobPrototype>(job))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", job)));
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
var located = await _locator.LookupIdByNameOrIdAsync(target);
|
||||
if (located == null)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-roleban-name-parse"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var targetUid = located.UserId;
|
||||
var targetHWid = located.LastHWId;
|
||||
|
||||
_bans.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, job, minutes, severity, reason, DateTimeOffset.UtcNow);
|
||||
if (_proto.HasIndex<JobPrototype>(role))
|
||||
_bans.CreateRoleBan<JobPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow);
|
||||
else if (_proto.HasIndex<AntagPrototype>(role))
|
||||
_bans.CreateRoleBan<AntagPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow);
|
||||
else
|
||||
shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", role)));
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
|
||||
@@ -26,24 +26,25 @@ namespace Content.Server.Administration.Managers;
|
||||
|
||||
public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
[Dependency] private readonly ServerDbEntryManager _entryManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
|
||||
[Dependency] private readonly ServerDbEntryManager _entryManager = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = default!;
|
||||
[Dependency] private readonly UserDbDataManager _userDbData = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public const string SawmillId = "admin.bans";
|
||||
public const string JobPrefix = "Job:";
|
||||
public const string PrefixAntag = "Antag:";
|
||||
public const string PrefixJob = "Job:";
|
||||
|
||||
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
|
||||
// Cached ban exemption flags are used to handle
|
||||
@@ -91,30 +92,6 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
_cachedBanExemptions.Remove(player);
|
||||
}
|
||||
|
||||
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
|
||||
{
|
||||
banDef = await _db.AddServerRoleBanAsync(banDef);
|
||||
|
||||
if (banDef.UserId != null
|
||||
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
|
||||
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
|
||||
{
|
||||
cachedBans.Add(banDef);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
return _cachedRoleBans.TryGetValue(session, out var roleBans)
|
||||
? roleBans.Select(banDef => banDef.Role).ToHashSet()
|
||||
: null;
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
// Clear out players that have disconnected.
|
||||
@@ -232,23 +209,54 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
#endregion
|
||||
|
||||
#region Job Bans
|
||||
#region Role Bans
|
||||
|
||||
// If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin.
|
||||
// Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset.
|
||||
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
|
||||
public async void CreateRoleBan<T>(
|
||||
NetUserId? target,
|
||||
string? targetUsername,
|
||||
NetUserId? banningAdmin,
|
||||
(IPAddress, int)? addressRange,
|
||||
ImmutableTypedHwid? hwid,
|
||||
ProtoId<T> role,
|
||||
uint? minutes,
|
||||
NoteSeverity severity,
|
||||
string reason,
|
||||
DateTimeOffset timeOfBan
|
||||
) where T : class, IPrototype
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(role, out JobPrototype? _))
|
||||
string encodedRole;
|
||||
|
||||
// TODO: Note that it's possible to clash IDs here between a job and an antag. The refactor that introduced
|
||||
// this check has consciously avoided refactoring Job and Antag prototype.
|
||||
// Refactor Job- and Antag- Prototype to introduce a common RolePrototype, which will fix this possible clash.
|
||||
|
||||
//TODO remove this check as part of the above refactor
|
||||
if (_prototypeManager.HasIndex<JobPrototype>(role) && _prototypeManager.HasIndex<AntagPrototype>(role))
|
||||
{
|
||||
throw new ArgumentException($"Invalid role '{role}'", nameof(role));
|
||||
_sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is both JobPrototype and AntagPrototype.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
role = string.Concat(JobPrefix, role);
|
||||
DateTimeOffset? expires = null;
|
||||
if (minutes > 0)
|
||||
// Don't trust the input: make sure the job or antag actually exists.
|
||||
if (_prototypeManager.HasIndex<JobPrototype>(role))
|
||||
encodedRole = PrefixJob + role;
|
||||
else if (_prototypeManager.HasIndex<AntagPrototype>(role))
|
||||
encodedRole = PrefixAntag + role;
|
||||
else
|
||||
{
|
||||
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value);
|
||||
_sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is not a JobPrototype or an AntagPrototype.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DateTimeOffset? expires = null;
|
||||
|
||||
if (minutes > 0)
|
||||
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value);
|
||||
|
||||
_systems.TryGetEntitySystem(out GameTicker? ticker);
|
||||
int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
|
||||
var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero;
|
||||
@@ -266,21 +274,34 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
severity,
|
||||
banningAdmin,
|
||||
null,
|
||||
role);
|
||||
encodedRole);
|
||||
|
||||
if (!await AddRoleBan(banDef))
|
||||
{
|
||||
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires));
|
||||
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length)));
|
||||
|
||||
if (target != null && _playerManager.TryGetSessionById(target.Value, out var session))
|
||||
{
|
||||
if (target is not null && _playerManager.TryGetSessionById(target.Value, out var session))
|
||||
SendRoleBans(session);
|
||||
}
|
||||
|
||||
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
|
||||
{
|
||||
banDef = await _db.AddServerRoleBanAsync(banDef);
|
||||
|
||||
if (banDef.UserId != null
|
||||
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
|
||||
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
|
||||
{
|
||||
cachedBans.Add(banDef);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime)
|
||||
@@ -319,32 +340,109 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
}
|
||||
|
||||
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
|
||||
{
|
||||
return GetRoleBans<JobPrototype>(playerUserId, PrefixJob);
|
||||
}
|
||||
|
||||
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId)
|
||||
{
|
||||
return GetRoleBans<AntagPrototype>(playerUserId, PrefixAntag);
|
||||
}
|
||||
|
||||
private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId, string prefix) where T : class, IPrototype
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
if (!_cachedRoleBans.TryGetValue(session, out var roleBans))
|
||||
return GetRoleBans<T>(session, prefix);
|
||||
}
|
||||
|
||||
private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession, string prefix) where T : class, IPrototype
|
||||
{
|
||||
if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans))
|
||||
return null;
|
||||
|
||||
return roleBans
|
||||
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
|
||||
.Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
|
||||
.Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal))
|
||||
.Select(ban => new ProtoId<T>(ban.Role[prefix.Length..]))
|
||||
.ToHashSet();
|
||||
}
|
||||
#endregion
|
||||
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
return _cachedRoleBans.TryGetValue(session, out var roleBans)
|
||||
? roleBans.Select(banDef => banDef.Role).ToHashSet()
|
||||
: null;
|
||||
}
|
||||
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs)
|
||||
{
|
||||
return IsRoleBanned(player, jobs, PrefixJob);
|
||||
}
|
||||
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags)
|
||||
{
|
||||
return IsRoleBanned(player, antags, PrefixAntag);
|
||||
}
|
||||
|
||||
private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles, string prefix) where T : class, IPrototype
|
||||
{
|
||||
var bans = GetRoleBans(player.UserId);
|
||||
|
||||
if (bans is null || bans.Count == 0)
|
||||
return false;
|
||||
|
||||
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var role in roles)
|
||||
{
|
||||
if (bans.Contains(prefix + role))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SendRoleBans(ICommonSession pSession)
|
||||
{
|
||||
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List<ServerRoleBanDef>();
|
||||
var jobBans = GetRoleBans<JobPrototype>(pSession, PrefixJob);
|
||||
var jobBansList = new List<string>(jobBans?.Count ?? 0);
|
||||
|
||||
if (jobBans is not null)
|
||||
{
|
||||
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var encodedId in jobBans)
|
||||
{
|
||||
jobBansList.Add(encodedId.ToString().Replace(PrefixJob, ""));
|
||||
}
|
||||
}
|
||||
|
||||
var antagBans = GetRoleBans<AntagPrototype>(pSession, PrefixAntag);
|
||||
var antagBansList = new List<string>(antagBans?.Count ?? 0);
|
||||
|
||||
if (antagBans is not null)
|
||||
{
|
||||
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var encodedId in antagBans)
|
||||
{
|
||||
antagBansList.Add(encodedId.ToString().Replace(PrefixAntag, ""));
|
||||
}
|
||||
}
|
||||
|
||||
var bans = new MsgRoleBans()
|
||||
{
|
||||
Bans = roleBans.Select(o => o.Role).ToList()
|
||||
JobBans = jobBansList,
|
||||
AntagBans = antagBansList,
|
||||
};
|
||||
|
||||
_sawmill.Debug($"Sent rolebans to {pSession.Name}");
|
||||
_sawmill.Debug($"Sent role bans to {pSession.Name}");
|
||||
_netManager.ServerSendMessage(bans, pSession.Channel);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill(SawmillId);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Database;
|
||||
@@ -25,19 +24,63 @@ public interface IBanManager
|
||||
/// <param name="severity">Severity of the resulting ban note</param>
|
||||
/// <param name="reason">Reason for the ban</param>
|
||||
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of prefixed prototype IDs with the player's role bans.
|
||||
/// </summary>
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player is currently banned from any of the listed roles.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="antags">A list of valid antag prototype IDs.</param>
|
||||
/// <returns>Returns True if an active role ban is found for this player for any of the listed roles.</returns>
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player is currently banned from any of the listed roles.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="jobs">A list of valid job prototype IDs.</param>
|
||||
/// <returns>Returns True if an active role ban is found for this player for any of the listed roles.</returns>
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of prototype IDs with the player's job bans.
|
||||
/// </summary>
|
||||
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of prototype IDs with the player's antag bans.
|
||||
/// </summary>
|
||||
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a job ban for the specified target, username or GUID
|
||||
/// </summary>
|
||||
/// <param name="target">Target user, username or GUID, null for none</param>
|
||||
/// <param name="role">Role to be banned from</param>
|
||||
/// <param name="targetUsername">The username of the target, if known</param>
|
||||
/// <param name="banningAdmin">The responsible admin for the ban</param>
|
||||
/// <param name="addressRange">The range of IPs that are to be banned, if known</param>
|
||||
/// <param name="hwid">The HWID to be banned, if known</param>
|
||||
/// <param name="role">The role ID to be banned from. Either an AntagPrototype or a JobPrototype</param>
|
||||
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
|
||||
/// <param name="severity">Severity of the resulting ban note</param>
|
||||
/// <param name="reason">Reason for the ban</param>
|
||||
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
|
||||
/// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
|
||||
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
|
||||
public void CreateRoleBan<T>(
|
||||
NetUserId? target,
|
||||
string? targetUsername,
|
||||
NetUserId? banningAdmin,
|
||||
(IPAddress, int)? addressRange,
|
||||
ImmutableTypedHwid? hwid,
|
||||
ProtoId<T> role,
|
||||
uint? minutes,
|
||||
NoteSeverity severity,
|
||||
string reason,
|
||||
DateTimeOffset timeOfBan
|
||||
) where T : class, IPrototype;
|
||||
|
||||
/// <summary>
|
||||
/// Pardons a role ban for the specified target, username or GUID
|
||||
|
||||
@@ -11,10 +11,12 @@ using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Server.Pointing.Components;
|
||||
using Content.Server.Polymorph.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Server.Tabletop;
|
||||
using Content.Server.Tabletop.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Components;
|
||||
using Content.Shared.Atmos.Components;
|
||||
@@ -36,7 +38,10 @@ using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Silicons.Laws;
|
||||
using Content.Shared.Silicons.Laws.Components;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Tabletop.Components;
|
||||
@@ -47,6 +52,7 @@ using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
@@ -55,6 +61,10 @@ namespace Content.Server.Administration.Systems;
|
||||
|
||||
public sealed partial class AdminVerbSystem
|
||||
{
|
||||
private readonly ProtoId<PolymorphPrototype> LizardSmite = "AdminLizardSmite";
|
||||
private readonly ProtoId<PolymorphPrototype> VulpkaninSmite = "AdminVulpSmite";
|
||||
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
|
||||
[Dependency] private readonly BodySystem _bodySystem = default!;
|
||||
@@ -72,6 +82,7 @@ public sealed partial class AdminVerbSystem
|
||||
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly RoleSystem _role = default!;
|
||||
[Dependency] private readonly TabletopSystem _tabletopSystem = default!;
|
||||
[Dependency] private readonly VomitSystem _vomitSystem = default!;
|
||||
[Dependency] private readonly WeldableSystem _weldableSystem = default!;
|
||||
@@ -80,6 +91,12 @@ public sealed partial class AdminVerbSystem
|
||||
[Dependency] private readonly SuperBonkSystem _superBonkSystem = default!;
|
||||
[Dependency] private readonly SlipperySystem _slipperySystem = default!;
|
||||
|
||||
private readonly EntProtoId _actionViewLawsProtoId = "ActionViewLaws";
|
||||
private readonly ProtoId<SiliconLawsetPrototype> _crewsimovLawset = "Crewsimov";
|
||||
|
||||
private readonly EntProtoId _siliconMindRole = "MindRoleSiliconBrain";
|
||||
private const string SiliconLawBoundUserInterface = "SiliconLawBoundUserInterface";
|
||||
|
||||
// All smite verbs have names so invokeverb works.
|
||||
private void AddSmiteVerbs(GetVerbsEvent<Verb> args)
|
||||
{
|
||||
@@ -100,7 +117,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = explodeName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var coords = _transformSystem.GetMapCoordinates(args.Target);
|
||||
@@ -121,7 +138,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = chessName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Tabletop/chessboard.rsi"), "chessboard"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/Tabletop/chessboard.rsi"), "chessboard"),
|
||||
Act = () =>
|
||||
{
|
||||
_sharedGodmodeSystem.EnableGodmode(args.Target); // So they don't suffocate.
|
||||
@@ -150,7 +167,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = flamesName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Fire/fire.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Alerts/Fire/fire.png")),
|
||||
Act = () =>
|
||||
{
|
||||
// Fuck you. Burn Forever.
|
||||
@@ -173,7 +190,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = monkeyName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/monkey.rsi"), "monkey"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Animals/monkey.rsi"), "monkey"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminMonkeySmite");
|
||||
@@ -188,7 +205,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = disposalBinName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Piping/disposal.rsi"), "disposal"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Piping/disposal.rsi"), "disposal"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminDisposalsSmite");
|
||||
@@ -206,20 +223,21 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = hardElectrocuteName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
int damageToDeal;
|
||||
if (!_mobThresholdSystem.TryGetThresholdForState(args.Target, MobState.Critical, out var criticalThreshold)) {
|
||||
if (!_mobThresholdSystem.TryGetThresholdForState(args.Target, MobState.Critical, out var criticalThreshold))
|
||||
{
|
||||
// We can't crit them so try killing them.
|
||||
if (!_mobThresholdSystem.TryGetThresholdForState(args.Target, MobState.Dead,
|
||||
out var deadThreshold))
|
||||
return;// whelp.
|
||||
damageToDeal = deadThreshold.Value.Int() - (int) damageable.TotalDamage;
|
||||
damageToDeal = deadThreshold.Value.Int() - (int)damageable.TotalDamage;
|
||||
}
|
||||
else
|
||||
{
|
||||
damageToDeal = criticalThreshold.Value.Int() - (int) damageable.TotalDamage;
|
||||
damageToDeal = criticalThreshold.Value.Int() - (int)damageable.TotalDamage;
|
||||
}
|
||||
|
||||
if (damageToDeal <= 0)
|
||||
@@ -252,7 +270,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = creamPieName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"),
|
||||
Act = () =>
|
||||
{
|
||||
_creamPieSystem.SetCreamPied(args.Target, creamPied, true);
|
||||
@@ -270,7 +288,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = bloodRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
|
||||
Act = () =>
|
||||
{
|
||||
_bloodstreamSystem.SpillAllSolutions((args.Target, bloodstream));
|
||||
@@ -323,7 +341,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = handsRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hands.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/remove-hands.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var baseXform = Transform(args.Target);
|
||||
@@ -346,7 +364,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = handRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hand.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/remove-hand.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var baseXform = Transform(args.Target);
|
||||
@@ -370,7 +388,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = stomachRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"),
|
||||
Act = () =>
|
||||
{
|
||||
foreach (var entity in _bodySystem.GetBodyOrganEntityComps<StomachComponent>((args.Target, body)))
|
||||
@@ -391,7 +409,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = lungRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"),
|
||||
Act = () =>
|
||||
{
|
||||
foreach (var entity in _bodySystem.GetBodyOrganEntityComps<LungComponent>((args.Target, body)))
|
||||
@@ -415,7 +433,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = pinballName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Balls/basketball.rsi"), "icon"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/Balls/basketball.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
var xform = Transform(args.Target);
|
||||
@@ -450,7 +468,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = yeetName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var xform = Transform(args.Target);
|
||||
@@ -482,7 +500,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = breadName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/bread.rsi"), "plain"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Consumable/Food/Baked/bread.rsi"), "plain"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminBreadSmite");
|
||||
@@ -497,7 +515,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = mouseName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/mouse.rsi"), "icon-0"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Animals/mouse.rsi"), "icon-0"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminMouseSmite");
|
||||
@@ -514,7 +532,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = ghostKickName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/gavel.svg.192dpi.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/gavel.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
{
|
||||
_ghostKickManager.DoDisconnect(actorComponent.PlayerSession.Channel, "Smitten.");
|
||||
@@ -533,7 +551,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = nyanifyName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
var ears = Spawn("ClothingHeadHatCatEars", Transform(args.Target).Coordinates);
|
||||
@@ -551,7 +569,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = killSignName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Misc/killsign.rsi"), "icon"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/killsign.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<KillSignComponent>(args.Target);
|
||||
@@ -567,7 +585,7 @@ public sealed partial class AdminVerbSystem
|
||||
Text = cluwneName,
|
||||
Category = VerbCategory.Smite,
|
||||
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Mask/cluwne.rsi"), "icon"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Mask/cluwne.rsi"), "icon"),
|
||||
|
||||
Act = () =>
|
||||
{
|
||||
@@ -583,7 +601,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = maidenName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
_outfit.SetOutfit(args.Target, "JanitorMaidGear", (_, clothing) =>
|
||||
@@ -604,7 +622,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = angerPointingArrowsName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/pointing.rsi"), "pointing"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/pointing.rsi"), "pointing"),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<PointingArrowAngeringComponent>(args.Target);
|
||||
@@ -619,7 +637,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = dustName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Materials/materials.rsi"), "ash"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Materials/materials.rsi"), "ash"),
|
||||
Act = () =>
|
||||
{
|
||||
QueueDel(args.Target);
|
||||
@@ -636,7 +654,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = youtubeVideoSimulationName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Misc/buffering_smite_icon.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Misc/buffering_smite_icon.png")),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<BufferingComponent>(args.Target);
|
||||
@@ -651,7 +669,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = instrumentationName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "supersynth"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "supersynth"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminInstrumentSmite");
|
||||
@@ -689,22 +707,37 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = reptilianName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Plushies/lizard.rsi"), "icon"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/Plushies/lizard.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");
|
||||
_polymorphSystem.PolymorphEntity(args.Target, LizardSmite);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", reptilianName, Loc.GetString("admin-smite-reptilian-species-swap-description"))
|
||||
};
|
||||
args.Verbs.Add(reptilian);
|
||||
|
||||
var vulpName = Loc.GetString("admin-smite-vulpkanin-species-swap-name").ToLowerInvariant();
|
||||
Verb vulp = new()
|
||||
{
|
||||
Text = vulpName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Balls/tennisball.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, VulpkaninSmite);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", vulpName, Loc.GetString("admin-smite-vulpkanin-species-swap-description"))
|
||||
};
|
||||
args.Verbs.Add(vulp);
|
||||
|
||||
var lockerName = Loc.GetString("admin-smite-locker-stuff-name").ToLowerInvariant();
|
||||
Verb locker = new()
|
||||
{
|
||||
Text = lockerName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Storage/closet.rsi"), "generic"),
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Storage/closet.rsi"), "generic"),
|
||||
Act = () =>
|
||||
{
|
||||
var xform = Transform(args.Target);
|
||||
@@ -727,7 +760,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = headstandName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<HeadstandComponent>(args.Target);
|
||||
@@ -742,7 +775,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = zoomInName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/zoom.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/zoom.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var eye = EnsureComp<ContentEyeComponent>(args.Target);
|
||||
@@ -758,7 +791,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = flipEyeName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/flip.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/flip.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var eye = EnsureComp<ContentEyeComponent>(args.Target);
|
||||
@@ -774,7 +807,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = runWalkSwapName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/run-walk-swap.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/run-walk-swap.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var movementSpeed = EnsureComp<MovementSpeedModifierComponent>(args.Target);
|
||||
@@ -795,7 +828,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = backwardsAccentName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/help-backwards.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/help-backwards.png")),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<BackwardsAccentComponent>(args.Target);
|
||||
@@ -810,7 +843,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = disarmProneName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Actions/disarm.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/disarm.png")),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<DisarmProneComponent>(args.Target);
|
||||
@@ -825,7 +858,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = superSpeedName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/super_speed.png")),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/super_speed.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var movementSpeed = EnsureComp<MovementSpeedModifierComponent>(args.Target);
|
||||
@@ -942,5 +975,36 @@ public sealed partial class AdminVerbSystem
|
||||
Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description"))
|
||||
};
|
||||
args.Verbs.Add(crawler);
|
||||
|
||||
var siliconName = Loc.GetString("admin-smite-silicon-laws-bound-name").ToLowerInvariant();
|
||||
Verb silicon = new()
|
||||
{
|
||||
Text = siliconName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Interface/Actions/actions_borg.rsi"), "state-laws"),
|
||||
Act = () =>
|
||||
{
|
||||
var userInterfaceComp = EnsureComp<UserInterfaceComponent>(args.Target);
|
||||
_uiSystem.SetUi((args.Target, userInterfaceComp), SiliconLawsUiKey.Key, new InterfaceData(SiliconLawBoundUserInterface));
|
||||
|
||||
if (!HasComp<SiliconLawBoundComponent>(args.Target))
|
||||
{
|
||||
EnsureComp<SiliconLawBoundComponent>(args.Target);
|
||||
_actions.AddAction(args.Target, _actionViewLawsProtoId);
|
||||
}
|
||||
|
||||
EnsureComp<SiliconLawProviderComponent>(args.Target);
|
||||
_siliconLawSystem.SetLaws(_siliconLawSystem.GetLawset(_crewsimovLawset).Laws, args.Target);
|
||||
|
||||
if (_mindSystem.TryGetMind(args.Target, out var mindId, out _))
|
||||
_role.MindAddRole(mindId, _siliconMindRole);
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("admin-smite-silicon-laws-bound-self"), args.Target,
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", siliconName, Loc.GetString("admin-smite-silicon-laws-bound-description"))
|
||||
};
|
||||
args.Verbs.Add(silicon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,17 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
|
||||
@@ -161,33 +162,35 @@ public sealed partial class AntagSelectionSystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given session has the primary antag preferences for a given definition
|
||||
/// Checks if a given session has enabled the antag preferences for a given definition,
|
||||
/// and if it is blocked by any requirements or bans.
|
||||
/// </summary>
|
||||
public bool HasPrimaryAntagPreference(ICommonSession? session, AntagSelectionDefinition def)
|
||||
/// <returns>Returns true if at least one role from the provided list passes every condition</returns>>
|
||||
public bool ValidAntagPreference(ICommonSession? session, List<ProtoId<AntagPrototype>> roles)
|
||||
{
|
||||
if (session == null)
|
||||
return true;
|
||||
|
||||
if (def.PrefRoles.Count == 0)
|
||||
if (roles.Count == 0)
|
||||
return false;
|
||||
|
||||
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
|
||||
return pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given session has the fallback antag preferences for a given definition
|
||||
/// </summary>
|
||||
public bool HasFallbackAntagPreference(ICommonSession? session, AntagSelectionDefinition def)
|
||||
{
|
||||
if (session == null)
|
||||
return true;
|
||||
var valid = false;
|
||||
|
||||
if (def.FallbackRoles.Count == 0)
|
||||
return false;
|
||||
// Check each individual antag role
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var list = new List<ProtoId<AntagPrototype>>{role};
|
||||
|
||||
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
|
||||
return pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p));
|
||||
|
||||
if (pref.AntagPreferences.Contains(role)
|
||||
&& !_ban.IsRoleBanned(session, list)
|
||||
&& _playTime.IsAllowed(session, list))
|
||||
valid = true;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
@@ -8,11 +9,11 @@ using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Station.Events;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Clothing;
|
||||
@@ -40,12 +41,14 @@ namespace Content.Server.Antag;
|
||||
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
|
||||
{
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly IBanManager _ban = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly GhostRoleSystem _ghostRole = default!;
|
||||
[Dependency] private readonly JobSystem _jobs = default!;
|
||||
[Dependency] private readonly LoadoutSystem _loadout = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingSystem _playTime = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _pref = default!;
|
||||
[Dependency] private readonly RoleSystem _role = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
@@ -344,7 +347,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
{
|
||||
_adminLogger.Add(LogType.AntagSelection, $"Start trying to make {session} become the antagonist: {ToPrettyString(ent)}");
|
||||
|
||||
if (checkPref && !HasPrimaryAntagPreference(session, def))
|
||||
if (checkPref && !ValidAntagPreference(session, def.PrefRoles))
|
||||
return false;
|
||||
|
||||
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
|
||||
@@ -497,11 +500,12 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (ent.Comp.PreSelectedSessions.TryGetValue(def, out var preSelected) && preSelected.Contains(session))
|
||||
continue;
|
||||
|
||||
if (HasPrimaryAntagPreference(session, def))
|
||||
// Add player to the appropriate antag pool
|
||||
if (ValidAntagPreference(session, def.PrefRoles))
|
||||
{
|
||||
preferredList.Add(session);
|
||||
}
|
||||
else if (HasFallbackAntagPreference(session, def))
|
||||
else if (ValidAntagPreference(session, def.FallbackRoles))
|
||||
{
|
||||
fallbackList.Add(session);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ namespace Content.Server.Database
|
||||
public abstract class ServerDbBase
|
||||
{
|
||||
private readonly ISawmill _opsLog;
|
||||
|
||||
public event Action<DatabaseNotification>? OnNotificationReceived;
|
||||
|
||||
/// <param name="opsLog">Sawmill to trace log database operations to.</param>
|
||||
@@ -1394,7 +1393,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
ban.LastEditedAt,
|
||||
ban.ExpirationTime,
|
||||
ban.Hidden,
|
||||
new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) },
|
||||
new [] { ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null) },
|
||||
MakePlayerRecord(unbanningAdmin),
|
||||
ban.Unban?.UnbanTime);
|
||||
}
|
||||
@@ -1694,7 +1693,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
NormalizeDatabaseTime(firstBan.LastEditedAt),
|
||||
NormalizeDatabaseTime(firstBan.ExpirationTime),
|
||||
firstBan.Hidden,
|
||||
banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(),
|
||||
banGroup.Select(ban => ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null)).ToArray(),
|
||||
MakePlayerRecord(unbanningAdmin),
|
||||
NormalizeDatabaseTime(firstBan.Unban?.UnbanTime)));
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class DoorElectronicsSystem : EntitySystem
|
||||
DoorElectronicsUpdateConfigurationMessage args)
|
||||
{
|
||||
var accessReader = EnsureComp<AccessReaderComponent>(uid);
|
||||
_accessReader.SetAccesses((uid, accessReader), args.AccessList);
|
||||
_accessReader.TrySetAccesses((uid, accessReader), args.AccessList);
|
||||
}
|
||||
|
||||
private void OnAccessReaderChanged(
|
||||
|
||||
@@ -18,7 +18,6 @@ using Content.Server.Speech.Components;
|
||||
using Content.Server.Spreader;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Server.Temperature.Systems;
|
||||
using Content.Server.Traits.Assorted;
|
||||
using Content.Server.Zombies;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
@@ -33,6 +32,7 @@ using Content.Shared.Maps;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using Content.Shared.Zombies;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
@@ -275,7 +275,7 @@ public sealed partial class ExplosionSystem
|
||||
radius = Math.Min(radius, MaxIterations / 4);
|
||||
|
||||
EntityUid? referenceGrid = null;
|
||||
float mass = 0;
|
||||
var mass = float.MinValue;
|
||||
|
||||
// First attempt to find a grid that is relatively close to the explosion's center. Instead of looking in a
|
||||
// diameter x diameter sized box, use a smaller box with radius sized sides:
|
||||
@@ -285,7 +285,7 @@ public sealed partial class ExplosionSystem
|
||||
_mapManager.FindGridsIntersecting(epicenter.MapId, box, ref _grids);
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.Mass > mass)
|
||||
if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.FixturesMass > mass)
|
||||
{
|
||||
mass = physics.Mass;
|
||||
referenceGrid = grid.Owner;
|
||||
@@ -315,7 +315,7 @@ public sealed partial class ExplosionSystem
|
||||
{
|
||||
if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.Mass > mass)
|
||||
{
|
||||
mass = physics.Mass;
|
||||
mass = physics.FixturesMass;
|
||||
referenceGrid = grid.Owner;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.GameTicking.Commands
|
||||
namespace Content.Server.GameTicking.Commands;
|
||||
|
||||
[AnyCommand]
|
||||
public sealed class ToggleReadyCommand : LocalizedEntityCommands
|
||||
{
|
||||
[AnyCommand]
|
||||
sealed class ToggleReadyCommand : IConsoleCommand
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
|
||||
public override string Command => "toggleready";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _e = default!;
|
||||
|
||||
public string Command => "toggleready";
|
||||
public string Description => "";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
if (args.Length != 1)
|
||||
{
|
||||
var player = shell.Player;
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
if (player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ticker = _e.System<GameTicker>();
|
||||
ticker.ToggleReady(player, bool.Parse(args[0]));
|
||||
shell.WriteError(Loc.GetString("shell-need-exactly-one-argument"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-can-only-run-from-pre-round-lobby"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bool.TryParse(args[0], out var ready))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
|
||||
return;
|
||||
}
|
||||
|
||||
_gameTicker.ToggleReady(player, ready);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking.Events;
|
||||
|
||||
[ByRefEvent]
|
||||
public struct IsJobAllowedEvent(ICommonSession player, ProtoId<JobPrototype> jobId, bool cancelled = false)
|
||||
{
|
||||
public readonly ICommonSession Player = player;
|
||||
public readonly ProtoId<JobPrototype> JobId = jobId;
|
||||
public bool Cancelled = cancelled;
|
||||
}
|
||||
24
Content.Server/GameTicking/Events/IsRoleAllowedEvent.cs
Normal file
24
Content.Server/GameTicking/Events/IsRoleAllowedEvent.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised to check if a player is allowed/able to assume a role.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="jobs">Optional list of job prototype IDs</param>
|
||||
/// <param name="antags">Optional list of antag prototype IDs</param>
|
||||
[ByRefEvent]
|
||||
public struct IsRoleAllowedEvent(
|
||||
ICommonSession player,
|
||||
List<ProtoId<JobPrototype>>? jobs,
|
||||
List<ProtoId<AntagPrototype>>? antags,
|
||||
bool cancelled = false)
|
||||
{
|
||||
public readonly ICommonSession Player = player;
|
||||
public readonly List<ProtoId<JobPrototype>>? Jobs = jobs;
|
||||
public readonly List<ProtoId<AntagPrototype>>? Antags = antags;
|
||||
public bool Cancelled = cancelled;
|
||||
}
|
||||
@@ -173,7 +173,6 @@ namespace Content.Server.GameTicking
|
||||
return;
|
||||
}
|
||||
|
||||
var status = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay;
|
||||
_playerGameStatuses[player.UserId] = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay;
|
||||
RaiseNetworkEvent(GetStatusMsg(player), player.Channel);
|
||||
// update server info to reflect new ready count
|
||||
|
||||
@@ -141,12 +141,13 @@ namespace Content.Server.GameTicking
|
||||
var character = GetPlayerProfile(player);
|
||||
|
||||
var jobBans = _banManager.GetJobBans(player.UserId);
|
||||
if (jobBans == null || jobId != null && jobBans.Contains(jobId))
|
||||
if (jobBans == null || jobId != null && jobBans.Contains(jobId)) //TODO: use IsRoleBanned directly?
|
||||
return;
|
||||
|
||||
if (jobId != null)
|
||||
{
|
||||
var ev = new IsJobAllowedEvent(player, new ProtoId<JobPrototype>(jobId));
|
||||
var jobs = new List<ProtoId<JobPrototype>> {jobId};
|
||||
var ev = new IsRoleAllowedEvent(player, jobs, null);
|
||||
RaiseLocalEvent(ref ev);
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Content.Server.GameTicking
|
||||
jObject["panic_bunker"] = _cfg.GetCVar(CCVars.PanicBunkerEnabled);
|
||||
jObject["run_level"] = (int) _runLevel;
|
||||
if (preset != null)
|
||||
jObject["preset"] = Loc.GetString(preset.ModeTitle);
|
||||
jObject["preset"] = (Decoy == null) ? Loc.GetString(preset.ModeTitle) : Loc.GetString(Decoy.ModeTitle);
|
||||
if (_runLevel >= GameRunLevel.InRound)
|
||||
{
|
||||
jObject["round_start_time"] = _roundStartDateTime.ToString("o");
|
||||
|
||||
@@ -15,12 +15,6 @@ public sealed partial class GhostRoleComponent : Component
|
||||
|
||||
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
|
||||
|
||||
// Actually make use of / enforce this requirement?
|
||||
// Why is this even here.
|
||||
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
|
||||
[DataField("requirements")]
|
||||
public HashSet<JobRequirement>? Requirements;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Ghost.Roles.Events;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
@@ -32,13 +34,16 @@ using Content.Server.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Collections;
|
||||
using Content.Shared.Ghost.Roles.Components;
|
||||
using Content.Shared.Roles.Components;
|
||||
|
||||
namespace Content.Server.Ghost.Roles;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class GhostRoleSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IBanManager _ban = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IEntityManager _ent = default!;
|
||||
[Dependency] private readonly EuiManager _euiManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
@@ -459,6 +464,23 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
|
||||
return;
|
||||
|
||||
TryPrototypes(roleEnt, out var antags, out var jobs);
|
||||
|
||||
// Check role bans
|
||||
if (_ban.IsRoleBanned(player, antags) || _ban.IsRoleBanned(player, jobs))
|
||||
{
|
||||
Log.Warning($"Server rejected ghost role request '{roleEnt.Comp.RoleName}' for '{player.Name}' - client missed ban?");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check role requirements
|
||||
if (!IsRoleAllowed(player, jobs, antags))
|
||||
{
|
||||
Log.Warning($"Server rejected ghost role request '{roleEnt.Comp.RoleName}' for '{player.Name}' - client missed requirement check?");
|
||||
return;
|
||||
}
|
||||
|
||||
// Decide to do a raffle or not
|
||||
if (roleEnt.Comp.RaffleConfig is not null)
|
||||
{
|
||||
JoinRaffle(player, identifier);
|
||||
@@ -469,6 +491,78 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect all role prototypes on the Ghostrole.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns true if at least on role prototype could be found.
|
||||
/// </returns>
|
||||
private bool TryPrototypes(
|
||||
Entity<GhostRoleComponent> roleEnt,
|
||||
out List<ProtoId<AntagPrototype>> antags,
|
||||
out List<ProtoId<JobPrototype>> jobs)
|
||||
{
|
||||
antags = [];
|
||||
jobs = [];
|
||||
|
||||
// If there is a mind already, check its mind roles.
|
||||
// Not sure if this can ever actually happen.
|
||||
if (TryComp<MindContainerComponent>(roleEnt, out var mindCont)
|
||||
&& TryComp<MindComponent>(mindCont.Mind, out var mind))
|
||||
{
|
||||
foreach (var role in mind.MindRoleContainer.ContainedEntities)
|
||||
{
|
||||
if(!TryComp<MindRoleComponent>(role, out var comp))
|
||||
continue;
|
||||
|
||||
if (comp.JobPrototype is not null)
|
||||
jobs.Add(comp.JobPrototype.Value);
|
||||
|
||||
else if (comp.AntagPrototype is not null)
|
||||
antags.Add(comp.AntagPrototype.Value);
|
||||
}
|
||||
|
||||
return antags.Count > 0 || jobs.Count > 0;
|
||||
}
|
||||
|
||||
if (roleEnt.Comp.JobProto is not null)
|
||||
jobs.Add(roleEnt.Comp.JobProto.Value);
|
||||
|
||||
|
||||
// If there is no mind, check the mindRole prototypes
|
||||
foreach (var proto in roleEnt.Comp.MindRoles)
|
||||
{
|
||||
if (!_prototype.TryIndex(proto, out var indexed)
|
||||
|| !indexed.TryGetComponent<MindRoleComponent>(out var comp, _ent.ComponentFactory))
|
||||
continue;
|
||||
var roleComp = (MindRoleComponent)comp;
|
||||
|
||||
if (roleComp.JobPrototype is not null)
|
||||
jobs.Add(roleComp.JobPrototype.Value);
|
||||
else if (roleComp.AntagPrototype is not null)
|
||||
antags.Add(roleComp.AntagPrototype.Value);
|
||||
else
|
||||
Log.Debug($"Mind role '{proto}' of '{roleEnt.Comp.RoleName}' has neither a job or antag prototype specified");
|
||||
}
|
||||
|
||||
return antags.Count > 0 || jobs.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player passes the requirements for the supplied roles.
|
||||
/// Returns false if any role fails the check.
|
||||
/// </summary>
|
||||
private bool IsRoleAllowed(
|
||||
ICommonSession player,
|
||||
List<ProtoId<JobPrototype>>? jobIds,
|
||||
List<ProtoId<AntagPrototype>>? antagIds)
|
||||
{
|
||||
var ev = new IsRoleAllowedEvent(player, jobIds, antagIds);
|
||||
RaiseLocalEvent(ref ev);
|
||||
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle.
|
||||
/// </summary>
|
||||
@@ -571,13 +665,15 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
? _timing.CurTime.Add(raffle.Countdown)
|
||||
: TimeSpan.MinValue;
|
||||
|
||||
TryPrototypes((uid, role), out var antags, out var jobs);
|
||||
|
||||
roles.Add(new GhostRoleInfo
|
||||
{
|
||||
Identifier = id,
|
||||
Name = role.RoleName,
|
||||
Description = role.RoleDescription,
|
||||
Rules = role.RoleRules,
|
||||
Requirements = role.Requirements,
|
||||
RolePrototypes = (jobs, antags),
|
||||
Kind = kind,
|
||||
RafflePlayerCount = rafflePlayerCount,
|
||||
RaffleEndTime = raffleEndTime
|
||||
|
||||
@@ -8,6 +8,8 @@ using Content.Shared.Chat.TypingIndicator;
|
||||
using Content.Shared.Holopad;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Labels.Components;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Content.Shared.Speech;
|
||||
@@ -38,6 +40,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
|
||||
private float _updateTimer = 1.0f;
|
||||
private const float UpdateTime = 1.0f;
|
||||
@@ -77,6 +80,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove);
|
||||
SubscribeLocalEvent<HolopadComponent, EntParentChangedMessage>(OnParentChanged);
|
||||
SubscribeLocalEvent<HolopadComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<HolopadUserComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
|
||||
}
|
||||
|
||||
#region: Holopad UI bound user interface messages
|
||||
@@ -226,7 +231,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi))
|
||||
continue;
|
||||
|
||||
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi))
|
||||
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value))
|
||||
LinkHolopadToUser(entity, args.Actor);
|
||||
}
|
||||
|
||||
@@ -446,6 +451,17 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
UpdateHolopadControlLockoutStartTime(entity);
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(Entity<HolopadUserComponent> ent, ref MobStateChangedEvent args)
|
||||
{
|
||||
if (!HasComp<StationAiHeldComponent>(ent))
|
||||
return;
|
||||
|
||||
foreach (var holopad in ent.Comp.LinkedHolopads)
|
||||
{
|
||||
ShutDownHolopad(holopad);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -605,25 +621,23 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
if (entity.Comp.Hologram != null)
|
||||
DeleteHologram(entity.Comp.Hologram.Value, entity);
|
||||
|
||||
if (entity.Comp.User != null)
|
||||
// Check if the associated holopad user is an AI
|
||||
if (HasComp<StationAiHeldComponent>(entity.Comp.User) &&
|
||||
_stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore))
|
||||
{
|
||||
// Check if the associated holopad user is an AI
|
||||
if (TryComp<StationAiHeldComponent>(entity.Comp.User, out var stationAiHeld) &&
|
||||
_stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore))
|
||||
// Return the AI eye to free roaming
|
||||
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
|
||||
|
||||
// If the AI core is still broadcasting, end its calls
|
||||
if (TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
|
||||
_telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone)))
|
||||
{
|
||||
// Return the AI eye to free roaming
|
||||
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
|
||||
|
||||
// If the AI core is still broadcasting, end its calls
|
||||
if (entity.Owner != stationAiCore.Owner &&
|
||||
TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
|
||||
_telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone)))
|
||||
{
|
||||
_telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
|
||||
}
|
||||
_telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
|
||||
}
|
||||
|
||||
UnlinkHolopadFromUser(entity, entity.Comp.User.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnlinkHolopadFromUser(entity, entity.Comp.User);
|
||||
}
|
||||
|
||||
Dirty(entity);
|
||||
|
||||
@@ -6,7 +6,5 @@ namespace Content.Server.Nutrition.Components;
|
||||
/// This component prevents NPC mobs like mice or cows from wanting to drink something that shouldn't be drank from.
|
||||
/// Including but not limited to: puddles
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(DrinkSystem))]
|
||||
public sealed partial class BadDrinkComponent : Component
|
||||
{
|
||||
}
|
||||
[RegisterComponent]
|
||||
public sealed partial class BadDrinkComponent : Component;
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
|
||||
namespace Content.Server.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class DrinkSystem : SharedDrinkSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// TODO add InteractNoHandEvent for entities like mice.
|
||||
SubscribeLocalEvent<DrinkComponent, SolutionContainerChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<DrinkComponent, ComponentInit>(OnDrinkInit);
|
||||
// run before inventory so for bucket it always tries to drink before equipping (when empty)
|
||||
// run after openable so its always open -> drink
|
||||
}
|
||||
|
||||
private void OnDrinkInit(Entity<DrinkComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (TryComp<DrainableSolutionComponent>(entity, out var existingDrainable))
|
||||
{
|
||||
// Beakers have Drink component but they should use the existing Drainable
|
||||
entity.Comp.Solution = existingDrainable.Solution;
|
||||
}
|
||||
else
|
||||
{
|
||||
_solutionContainer.EnsureSolution(entity.Owner, entity.Comp.Solution, out _);
|
||||
}
|
||||
|
||||
UpdateAppearance(entity, entity.Comp);
|
||||
|
||||
if (TryComp(entity, out RefillableSolutionComponent? refillComp))
|
||||
refillComp.Solution = entity.Comp.Solution;
|
||||
|
||||
if (TryComp(entity, out DrainableSolutionComponent? drainComp))
|
||||
drainComp.Solution = entity.Comp.Solution;
|
||||
}
|
||||
|
||||
private void OnSolutionChange(Entity<DrinkComponent> entity, ref SolutionContainerChangedEvent args)
|
||||
{
|
||||
UpdateAppearance(entity, entity.Comp);
|
||||
}
|
||||
|
||||
public void UpdateAppearance(EntityUid uid, DrinkComponent component)
|
||||
{
|
||||
if (!TryComp<AppearanceComponent>(uid, out var appearance) ||
|
||||
!HasComp<SolutionContainerManagerComponent>(uid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var drainAvailable = DrinkVolume(uid, component);
|
||||
_appearance.SetData(uid, FoodVisuals.Visual, drainAvailable.Float(), appearance);
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,9 @@ public sealed class JobWhitelistManager : IPostInjectInit
|
||||
SendJobWhitelist(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns false if role whitelist is required but the player does not have it.
|
||||
/// </summary>
|
||||
public bool IsAllowed(ICommonSession session, ProtoId<JobPrototype> job)
|
||||
{
|
||||
if (!_config.GetCVar(CCVars.GameRoleWhitelist))
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class JobWhitelistSystem : EntitySystem
|
||||
{
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
|
||||
SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
|
||||
SubscribeLocalEvent<IsRoleAllowedEvent>(OnIsRoleAllowed);
|
||||
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
|
||||
|
||||
CacheJobs();
|
||||
@@ -51,11 +51,18 @@ public sealed class JobWhitelistSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIsJobAllowed(ref IsJobAllowedEvent ev)
|
||||
private void OnIsRoleAllowed(ref IsRoleAllowedEvent ev)
|
||||
{
|
||||
if (!_manager.IsAllowed(ev.Player, ev.JobId))
|
||||
ev.Cancelled = true;
|
||||
if (ev.Jobs is null)
|
||||
return;
|
||||
|
||||
foreach (var proto in ev.Jobs)
|
||||
{
|
||||
if (!_manager.IsAllowed(ev.Player, proto))
|
||||
ev.Cancelled = true;
|
||||
}
|
||||
}
|
||||
//TODO: Antagonist role whitelists?
|
||||
|
||||
private void OnGetDisallowedJobs(ref GetDisallowedJobsEvent ev)
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
|
||||
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
|
||||
SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
|
||||
SubscribeLocalEvent<IsRoleAllowedEvent>(OnIsRoleAllowed);
|
||||
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
|
||||
_adminManager.OnPermsChanged += AdminPermsChanged;
|
||||
}
|
||||
@@ -86,6 +86,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
trackers.UnionWith(GetTimedRoles(player));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the player has an attached mob and it is alive (even if in critical).
|
||||
/// </summary>
|
||||
private bool IsPlayerAlive(ICommonSession session)
|
||||
{
|
||||
var attached = session.AttachedEntity;
|
||||
@@ -176,9 +179,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
RemoveDisallowedJobs(ev.Player, ev.Jobs);
|
||||
}
|
||||
|
||||
private void OnIsJobAllowed(ref IsJobAllowedEvent ev)
|
||||
private void OnIsRoleAllowed(ref IsRoleAllowedEvent ev)
|
||||
{
|
||||
if (!IsAllowed(ev.Player, ev.JobId))
|
||||
if (!IsAllowed(ev.Player, ev.Jobs) || !IsAllowed(ev.Player, ev.Antags))
|
||||
ev.Cancelled = true;
|
||||
}
|
||||
|
||||
@@ -187,10 +190,55 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
ev.Jobs.UnionWith(GetDisallowedJobs(ev.Player));
|
||||
}
|
||||
|
||||
public bool IsAllowed(ICommonSession player, string role)
|
||||
/// <summary>
|
||||
/// Checks if the player meets role requirements.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="jobs">A list of role prototype IDs</param>
|
||||
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
|
||||
public bool IsAllowed(ICommonSession player, List<ProtoId<JobPrototype>>? jobs)
|
||||
{
|
||||
if (!_prototypes.TryIndex<JobPrototype>(role, out var job) ||
|
||||
!_cfg.GetCVar(CCVars.GameRoleTimers))
|
||||
if (jobs is null)
|
||||
return true;
|
||||
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
if (!IsAllowed(player, job))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player meets role requirements.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="antags">A list of role prototype IDs</param>
|
||||
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
|
||||
public bool IsAllowed(ICommonSession player, List<ProtoId<AntagPrototype>>? antags)
|
||||
{
|
||||
if (antags is null)
|
||||
return true;
|
||||
|
||||
foreach (var antag in antags)
|
||||
{
|
||||
if (!IsAllowed(player, antag))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player meets role requirements.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="job">A list of role prototype IDs</param>
|
||||
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
|
||||
public bool IsAllowed(ICommonSession player, ProtoId<JobPrototype> job)
|
||||
{
|
||||
if (!_cfg.GetCVar(CCVars.GameRoleTimers))
|
||||
return true;
|
||||
|
||||
if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
|
||||
@@ -199,7 +247,43 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
playTimes = new Dictionary<string, TimeSpan>();
|
||||
}
|
||||
|
||||
return JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
|
||||
var requirements = _roles.GetRoleRequirements(job);
|
||||
return JobRequirements.TryRequirementsMet(
|
||||
requirements,
|
||||
playTimes,
|
||||
out _,
|
||||
EntityManager,
|
||||
_prototypes,
|
||||
(HumanoidCharacterProfile?)
|
||||
_preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player meets role requirements.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="antag">A list of role prototype IDs</param>
|
||||
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
|
||||
public bool IsAllowed(ICommonSession player, ProtoId<AntagPrototype> antag)
|
||||
{
|
||||
if (!_cfg.GetCVar(CCVars.GameRoleTimers))
|
||||
return true;
|
||||
|
||||
if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
|
||||
{
|
||||
Log.Error($"Unable to check playtimes {Environment.StackTrace}");
|
||||
playTimes = new Dictionary<string, TimeSpan>();
|
||||
}
|
||||
|
||||
var requirements = _roles.GetRoleRequirements(antag);
|
||||
return JobRequirements.TryRequirementsMet(
|
||||
requirements,
|
||||
playTimes,
|
||||
out _,
|
||||
EntityManager,
|
||||
_prototypes,
|
||||
(HumanoidCharacterProfile?)
|
||||
_preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
|
||||
}
|
||||
|
||||
public HashSet<ProtoId<JobPrototype>> GetDisallowedJobs(ICommonSession player)
|
||||
|
||||
@@ -261,7 +261,7 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
|
||||
if (configuration.TransferHumanoidAppearance)
|
||||
{
|
||||
_humanoid.CloneAppearance(child, uid);
|
||||
_humanoid.CloneAppearance(uid, child);
|
||||
}
|
||||
|
||||
if (_mindSystem.TryGetMind(uid, out var mindId, out var mind))
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, ComponentShutdown>(OnConsoleShutdown);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, PowerChangedEvent>(OnConsolePowerChange);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, AnchorStateChangedEvent>(OnConsoleAnchorChange);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, ActivatableUIOpenAttemptEvent>(OnConsoleUIOpenAttempt);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, AfterActivatableUIOpenEvent>(OnConsoleUIOpenAttempt);
|
||||
Subs.BuiEvents<ShuttleConsoleComponent>(ShuttleConsoleUiKey.Key, subs =>
|
||||
{
|
||||
subs.Event<ShuttleConsoleFTLBeaconMessage>(OnBeaconFTLMessage);
|
||||
@@ -150,10 +150,9 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
|
||||
}
|
||||
|
||||
private void OnConsoleUIOpenAttempt(EntityUid uid, ShuttleConsoleComponent component,
|
||||
ActivatableUIOpenAttemptEvent args)
|
||||
AfterActivatableUIOpenEvent args)
|
||||
{
|
||||
if (!TryPilot(args.User, uid))
|
||||
args.Cancel();
|
||||
TryPilot(args.User, uid);
|
||||
}
|
||||
|
||||
private void OnConsoleAnchorChange(EntityUid uid, ShuttleConsoleComponent component,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Mind;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Server.Player;
|
||||
using Content.Shared.Popups;
|
||||
|
||||
namespace Content.Server.Silicons.StationAi;
|
||||
|
||||
public sealed partial class StationAiFixerConsoleSystem : SharedStationAiFixerConsoleSystem
|
||||
{
|
||||
[Dependency] private readonly EuiManager _eui = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
protected override void FinalizeAction(Entity<StationAiFixerConsoleComponent> ent)
|
||||
{
|
||||
if (IsActionInProgress(ent) && ent.Comp.ActionTarget != null)
|
||||
{
|
||||
switch (ent.Comp.ActionType)
|
||||
{
|
||||
case StationAiFixerConsoleAction.Repair:
|
||||
|
||||
// Send message to disembodied player that they are being revived
|
||||
if (_mind.TryGetMind(ent.Comp.ActionTarget.Value, out _, out var mind) &&
|
||||
mind.IsVisitingEntity &&
|
||||
_player.TryGetSessionById(mind.UserId, out var session))
|
||||
{
|
||||
_eui.OpenEui(new ReturnToBodyEui(mind, _mind, _player), session);
|
||||
_popup.PopupEntity(Loc.GetString("station-ai-fixer-console-repair-finished"), ent);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("station-ai-fixer-console-repair-successful"), ent);
|
||||
}
|
||||
|
||||
// TODO: make predicted once a user is not required
|
||||
if (ent.Comp.RepairFinishedSound != null)
|
||||
{
|
||||
_audio.PlayPvs(ent.Comp.RepairFinishedSound, ent);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case StationAiFixerConsoleAction.Purge:
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("station-ai-fixer-console-purge-successful"), ent);
|
||||
|
||||
// TODO: make predicted once a user is not required
|
||||
if (ent.Comp.PurgeFinishedSound != null)
|
||||
{
|
||||
_audio.PlayPvs(ent.Comp.PurgeFinishedSound, ent);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
base.FinalizeAction(ent);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,34 @@
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.Destructible;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Spawners.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Rejuvenate;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.StationAi;
|
||||
using Content.Shared.Turrets;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -16,19 +40,300 @@ public sealed class StationAiSystem : SharedStationAiSystem
|
||||
{
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly RoleSystem _roles = default!;
|
||||
[Dependency] private readonly ItemSlotsSystem _slots = default!;
|
||||
[Dependency] private readonly GhostSystem _ghost = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly DestructibleSystem _destructible = default!;
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popups = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly StationJobsSystem _stationJobs = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
private readonly HashSet<Entity<StationAiCoreComponent>> _stationAiCores = new();
|
||||
|
||||
private readonly ProtoId<ChatNotificationPrototype> _turretIsAttackingChatNotificationPrototype = "TurretIsAttacking";
|
||||
private readonly ProtoId<ChatNotificationPrototype> _aiWireSnippedChatNotificationPrototype = "AiWireSnipped";
|
||||
private readonly ProtoId<ChatNotificationPrototype> _aiLosingPowerChatNotificationPrototype = "AiLosingPower";
|
||||
private readonly ProtoId<ChatNotificationPrototype> _aiCriticalPowerChatNotificationPrototype = "AiCriticalPower";
|
||||
|
||||
private readonly ProtoId<JobPrototype> _stationAiJob = "StationAi";
|
||||
private readonly EntProtoId _stationAiBrain = "StationAiBrain";
|
||||
|
||||
private readonly ProtoId<AlertPrototype> _batteryAlert = "BorgBattery";
|
||||
private readonly ProtoId<AlertPrototype> _damageAlert = "BorgHealth";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StationAiCoreComponent, AfterConstructionChangeEntityEvent>(AfterConstructionChangeEntity);
|
||||
SubscribeLocalEvent<StationAiCoreComponent, ContainerSpawnEvent>(OnContainerSpawn);
|
||||
SubscribeLocalEvent<StationAiCoreComponent, ApcPowerReceiverBatteryChangedEvent>(OnApcBatteryChanged);
|
||||
SubscribeLocalEvent<StationAiCoreComponent, ChargeChangedEvent>(OnChargeChanged);
|
||||
SubscribeLocalEvent<StationAiCoreComponent, DamageChangedEvent>(OnDamageChanged);
|
||||
SubscribeLocalEvent<StationAiCoreComponent, DestructionEventArgs>(OnDestruction);
|
||||
SubscribeLocalEvent<StationAiCoreComponent, DoAfterAttemptEvent<IntellicardDoAfterEvent>>(OnDoAfterAttempt);
|
||||
SubscribeLocalEvent<StationAiCoreComponent, RejuvenateEvent>(OnRejuvenate);
|
||||
|
||||
SubscribeLocalEvent<ExpandICChatRecipientsEvent>(OnExpandICChatRecipients);
|
||||
SubscribeLocalEvent<StationAiTurretComponent, AmmoShotEvent>(OnAmmoShot);
|
||||
}
|
||||
|
||||
private void AfterConstructionChangeEntity(Entity<StationAiCoreComponent> ent, ref AfterConstructionChangeEntityEvent args)
|
||||
{
|
||||
if (!_container.TryGetContainer(ent, StationAiCoreComponent.BrainContainer, out var container) ||
|
||||
container.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var brain = container.ContainedEntities[0];
|
||||
|
||||
if (_mind.TryGetMind(brain, out var mindId, out var mind))
|
||||
{
|
||||
// Found an existing mind to transfer into the AI core
|
||||
var aiBrain = Spawn(_stationAiBrain, Transform(ent.Owner).Coordinates);
|
||||
_roles.MindAddJobRole(mindId, mind, false, _stationAiJob);
|
||||
_mind.TransferTo(mindId, aiBrain);
|
||||
|
||||
if (!TryComp<StationAiHolderComponent>(ent, out var targetHolder) ||
|
||||
!_slots.TryInsert(ent, targetHolder.Slot, aiBrain, null))
|
||||
{
|
||||
QueueDel(aiBrain);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We should consider keeping the borg brain inside the AI core.
|
||||
// When the core is destroyed, the station AI can be transferred into the brain,
|
||||
// then dropped on the ground. The deceased AI can then be revived later,
|
||||
// instead of being lost forever.
|
||||
QueueDel(brain);
|
||||
}
|
||||
|
||||
private void OnContainerSpawn(Entity<StationAiCoreComponent> ent, ref ContainerSpawnEvent args)
|
||||
{
|
||||
// Ensure that players that recently joined the round will spawn
|
||||
// into an AI core that has a full battery and full integrity.
|
||||
if (TryComp<BatteryComponent>(ent, out var battery))
|
||||
{
|
||||
_battery.SetCharge(ent, battery.MaxCharge);
|
||||
}
|
||||
|
||||
if (TryComp<DamageableComponent>(ent, out var damageable))
|
||||
{
|
||||
_damageable.SetAllDamage(ent, damageable, 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
base.OnAiInsert(ent, ref args);
|
||||
|
||||
UpdateBatteryAlert(ent);
|
||||
UpdateCoreIntegrityAlert(ent);
|
||||
UpdateDamagedAccent(ent);
|
||||
}
|
||||
|
||||
protected override void OnAiRemove(Entity<StationAiCoreComponent> ent, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
base.OnAiRemove(ent, ref args);
|
||||
|
||||
_alerts.ClearAlert(args.Entity, _batteryAlert);
|
||||
_alerts.ClearAlert(args.Entity, _damageAlert);
|
||||
|
||||
if (TryComp<DamagedSiliconAccentComponent>(args.Entity, out var accent))
|
||||
{
|
||||
accent.OverrideChargeLevel = null;
|
||||
accent.OverrideTotalDamage = null;
|
||||
accent.DamageAtMaxCorruption = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMobStateChanged(Entity<StationAiCustomizationComponent> ent, ref MobStateChangedEvent args)
|
||||
{
|
||||
if (args.NewMobState != MobState.Alive)
|
||||
{
|
||||
SetStationAiState(ent, StationAiState.Dead);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = StationAiState.Rebooting;
|
||||
|
||||
if (_mind.TryGetMind(ent, out var _, out var mind) && !mind.IsVisitingEntity)
|
||||
{
|
||||
state = StationAiState.Occupied;
|
||||
}
|
||||
|
||||
if (TryGetCore(ent, out var aiCore) && aiCore.Comp != null)
|
||||
{
|
||||
var aiCoreEnt = (aiCore.Owner, aiCore.Comp);
|
||||
|
||||
if (SetupEye(aiCoreEnt))
|
||||
AttachEye(aiCoreEnt);
|
||||
}
|
||||
|
||||
SetStationAiState(ent, state);
|
||||
}
|
||||
|
||||
private void OnDestruction(Entity<StationAiCoreComponent> ent, ref DestructionEventArgs args)
|
||||
{
|
||||
var station = _station.GetOwningStation(ent);
|
||||
|
||||
if (station == null)
|
||||
return;
|
||||
|
||||
if (!HasComp<ContainerSpawnPointComponent>(ent))
|
||||
return;
|
||||
|
||||
// If the destroyed core could act as a player spawn point,
|
||||
// reduce the number of available AI jobs by one
|
||||
_stationJobs.TryAdjustJobSlot(station.Value, _stationAiJob, -1, false, true);
|
||||
}
|
||||
|
||||
private void OnApcBatteryChanged(Entity<StationAiCoreComponent> ent, ref ApcPowerReceiverBatteryChangedEvent args)
|
||||
{
|
||||
if (!args.Enabled)
|
||||
return;
|
||||
|
||||
if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
|
||||
return;
|
||||
|
||||
var ev = new ChatNotificationEvent(_aiLosingPowerChatNotificationPrototype, ent);
|
||||
RaiseLocalEvent(held.Value, ref ev);
|
||||
}
|
||||
|
||||
private void OnChargeChanged(Entity<StationAiCoreComponent> entity, ref ChargeChangedEvent args)
|
||||
{
|
||||
UpdateBatteryAlert(entity);
|
||||
UpdateDamagedAccent(entity);
|
||||
}
|
||||
|
||||
private void OnDamageChanged(Entity<StationAiCoreComponent> entity, ref DamageChangedEvent args)
|
||||
{
|
||||
UpdateCoreIntegrityAlert(entity);
|
||||
UpdateDamagedAccent(entity);
|
||||
}
|
||||
|
||||
private void UpdateDamagedAccent(Entity<StationAiCoreComponent> ent)
|
||||
{
|
||||
if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
|
||||
return;
|
||||
|
||||
if (!TryComp<DamagedSiliconAccentComponent>(held, out var accent))
|
||||
return;
|
||||
|
||||
if (TryComp<BatteryComponent>(ent, out var battery))
|
||||
accent.OverrideChargeLevel = battery.CurrentCharge / battery.MaxCharge;
|
||||
|
||||
if (TryComp<DamageableComponent>(ent, out var damageable))
|
||||
accent.OverrideTotalDamage = damageable.TotalDamage;
|
||||
|
||||
if (TryComp<DestructibleComponent>(ent, out var destructible))
|
||||
accent.DamageAtMaxCorruption = _destructible.DestroyedAt(ent, destructible);
|
||||
|
||||
Dirty(held.Value, accent);
|
||||
}
|
||||
|
||||
private void UpdateBatteryAlert(Entity<StationAiCoreComponent> ent)
|
||||
{
|
||||
if (!TryComp<BatteryComponent>(ent, out var battery))
|
||||
return;
|
||||
|
||||
if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
|
||||
return;
|
||||
|
||||
if (!_proto.TryIndex(_batteryAlert, out var proto))
|
||||
return;
|
||||
|
||||
var chargePercent = battery.CurrentCharge / battery.MaxCharge;
|
||||
var chargeLevel = Math.Round(chargePercent * proto.MaxSeverity);
|
||||
|
||||
_alerts.ShowAlert(held.Value, _batteryAlert, (short)Math.Clamp(chargeLevel, 0, proto.MaxSeverity));
|
||||
|
||||
if (TryComp<ApcPowerReceiverBatteryComponent>(ent, out var apcBattery) &&
|
||||
apcBattery.Enabled &&
|
||||
chargePercent < 0.2)
|
||||
{
|
||||
var ev = new ChatNotificationEvent(_aiCriticalPowerChatNotificationPrototype, ent);
|
||||
RaiseLocalEvent(held.Value, ref ev);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCoreIntegrityAlert(Entity<StationAiCoreComponent> ent)
|
||||
{
|
||||
if (!TryComp<DamageableComponent>(ent, out var damageable))
|
||||
return;
|
||||
|
||||
if (!TryComp<DestructibleComponent>(ent, out var destructible))
|
||||
return;
|
||||
|
||||
if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
|
||||
return;
|
||||
|
||||
if (!_proto.TryIndex(_damageAlert, out var proto))
|
||||
return;
|
||||
|
||||
var damagePercent = damageable.TotalDamage / _destructible.DestroyedAt(ent, destructible);
|
||||
var damageLevel = Math.Round(damagePercent.Float() * proto.MaxSeverity);
|
||||
|
||||
_alerts.ShowAlert(held.Value, _damageAlert, (short)Math.Clamp(damageLevel, 0, proto.MaxSeverity));
|
||||
}
|
||||
|
||||
private void OnDoAfterAttempt(Entity<StationAiCoreComponent> ent, ref DoAfterAttemptEvent<IntellicardDoAfterEvent> args)
|
||||
{
|
||||
if (TryGetHeld((ent.Owner, ent.Comp), out _))
|
||||
return;
|
||||
|
||||
// Prevent AIs from being uploaded into an unpowered or broken AI core.
|
||||
|
||||
if (TryComp<ApcPowerReceiverComponent>(ent, out var apcPower) && !apcPower.Powered)
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("station-ai-has-no-power-for-upload"), ent, args.Event.User);
|
||||
args.Cancel();
|
||||
}
|
||||
else if (TryComp<DestructibleComponent>(ent, out var destructible) && destructible.IsBroken)
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("station-ai-is-too-damaged-for-upload"), ent, args.Event.User);
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public override void KillHeldAi(Entity<StationAiCoreComponent> ent)
|
||||
{
|
||||
base.KillHeldAi(ent);
|
||||
|
||||
if (TryGetHeld((ent.Owner, ent.Comp), out var held) &&
|
||||
_mind.TryGetMind(held.Value, out var mindId, out var mind))
|
||||
{
|
||||
_ghost.OnGhostAttempt(mindId, canReturnGlobal: true, mind: mind);
|
||||
RemComp<StationAiOverlayComponent>(held.Value);
|
||||
}
|
||||
|
||||
ClearEye(ent);
|
||||
}
|
||||
|
||||
private void OnRejuvenate(Entity<StationAiCoreComponent> ent, ref RejuvenateEvent args)
|
||||
{
|
||||
if (TryGetHeld((ent.Owner, ent.Comp), out var held))
|
||||
{
|
||||
_mobState.ChangeMobState(held.Value, MobState.Alive);
|
||||
EnsureComp<StationAiOverlayComponent>(held.Value);
|
||||
}
|
||||
|
||||
if (TryComp<StationAiHolderComponent>(ent, out var holder))
|
||||
{
|
||||
_appearance.SetData(ent, StationAiVisuals.Broken, false);
|
||||
UpdateAppearance((ent, holder));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
@@ -147,7 +452,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
|
||||
if (!TryGetHeld((stationAiCore, stationAiCore.Comp), out var insertedAi))
|
||||
continue;
|
||||
|
||||
hashSet.Add(insertedAi);
|
||||
hashSet.Add(insertedAi.Value);
|
||||
}
|
||||
|
||||
return hashSet;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -87,6 +86,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
|
||||
if (!_container.Insert(args.SpawnResult.Value, container, containerXform: xform))
|
||||
continue;
|
||||
|
||||
var ev = new ContainerSpawnEvent(args.SpawnResult.Value);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,3 +96,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
|
||||
args.SpawnResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on a container when a player is spawned into it.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct ContainerSpawnEvent(EntityUid Player);
|
||||
|
||||
@@ -371,7 +371,7 @@ public sealed partial class StationJobsSystem
|
||||
if (weight is not null && job.Weight != weight.Value)
|
||||
continue;
|
||||
|
||||
if (!(roleBans == null || !roleBans.Contains(jobId)))
|
||||
if (!(roleBans == null || !roleBans.Contains(jobId))) //TODO: Replace with IsRoleBanned
|
||||
continue;
|
||||
|
||||
availableJobs ??= new List<string>(profile.JobPriorities.Count);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Server.Traits.Assorted;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for the narcolepsy trait.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(NarcolepsySystem))]
|
||||
public sealed partial class NarcolepsyComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The random time between incidents, (min, max).
|
||||
/// </summary>
|
||||
[DataField("timeBetweenIncidents", required: true)]
|
||||
public Vector2 TimeBetweenIncidents { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The duration of incidents, (min, max).
|
||||
/// </summary>
|
||||
[DataField("durationOfIncident", required: true)]
|
||||
public Vector2 DurationOfIncident { get; private set; }
|
||||
|
||||
public float NextIncidentTime;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Traits.Assorted;
|
||||
|
||||
/// <summary>
|
||||
/// This handles narcolepsy, causing the affected to fall asleep uncontrollably at a random interval.
|
||||
/// </summary>
|
||||
public sealed class NarcolepsySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<NarcolepsyComponent, ComponentStartup>(SetupNarcolepsy);
|
||||
}
|
||||
|
||||
private void SetupNarcolepsy(EntityUid uid, NarcolepsyComponent component, ComponentStartup args)
|
||||
{
|
||||
component.NextIncidentTime =
|
||||
_random.NextFloat(component.TimeBetweenIncidents.X, component.TimeBetweenIncidents.Y);
|
||||
}
|
||||
|
||||
public void AdjustNarcolepsyTimer(EntityUid uid, int TimerReset, NarcolepsyComponent? narcolepsy = null)
|
||||
{
|
||||
if (!Resolve(uid, ref narcolepsy, false))
|
||||
return;
|
||||
|
||||
narcolepsy.NextIncidentTime = TimerReset;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<NarcolepsyComponent>();
|
||||
while (query.MoveNext(out var uid, out var narcolepsy))
|
||||
{
|
||||
narcolepsy.NextIncidentTime -= frameTime;
|
||||
|
||||
if (narcolepsy.NextIncidentTime >= 0)
|
||||
continue;
|
||||
|
||||
// Set the new time.
|
||||
narcolepsy.NextIncidentTime +=
|
||||
_random.NextFloat(narcolepsy.TimeBetweenIncidents.X, narcolepsy.TimeBetweenIncidents.Y);
|
||||
|
||||
var duration = _random.NextFloat(narcolepsy.DurationOfIncident.X, narcolepsy.DurationOfIncident.Y);
|
||||
|
||||
// Make sure the sleep time doesn't cut into the time to next incident.
|
||||
narcolepsy.NextIncidentTime += duration;
|
||||
|
||||
_statusEffects.TryAddStatusEffectDuration(uid, SleepingSystem.StatusEffectForcedSleeping, TimeSpan.FromSeconds(duration));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Chat;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Server.IdentityManagement;
|
||||
@@ -14,6 +16,7 @@ using Content.Server.StationEvents.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.CombatMode.Pacification;
|
||||
using Content.Shared.Damage;
|
||||
@@ -41,6 +44,7 @@ using Content.Shared.Tag;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.Roles;
|
||||
|
||||
namespace Content.Server.Zombies;
|
||||
|
||||
@@ -53,23 +57,27 @@ namespace Content.Server.Zombies;
|
||||
public sealed partial class ZombieSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly IBanManager _ban = default!;
|
||||
[Dependency] private readonly IChatManager _chatMan = default!;
|
||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _faction = default!;
|
||||
[Dependency] private readonly GhostSystem _ghost = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||
[Dependency] private readonly IdentitySystem _identity = default!;
|
||||
[Dependency] private readonly ServerInventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _player = default!;
|
||||
|
||||
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
|
||||
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
|
||||
private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie";
|
||||
private static readonly string MindRoleZombie = "MindRoleZombie";
|
||||
private static readonly List<ProtoId<AntagPrototype>> BannableZombiePrototypes = ["Zombie"];
|
||||
|
||||
/// <summary>
|
||||
/// Handles an entity turning into a zombie when they die or go into crit
|
||||
@@ -104,6 +112,24 @@ public sealed partial class ZombieSystem
|
||||
if (!Resolve(target, ref mobState, logMissing: false))
|
||||
return;
|
||||
|
||||
// Detach role-banned players before zombification
|
||||
if (TryComp<ActorComponent>(target, out var actor) && _ban.IsRoleBanned(actor.PlayerSession, BannableZombiePrototypes))
|
||||
{
|
||||
var sess = actor.PlayerSession;
|
||||
var message = Loc.GetString("zombie-roleban-ghosted");
|
||||
|
||||
if (_mind.TryGetMind(sess, out var playerMindEnt, out var playerMind))
|
||||
{
|
||||
// Detach
|
||||
_ghost.SpawnGhost((playerMindEnt, playerMind), target);
|
||||
|
||||
// Notify
|
||||
_chatMan.DispatchServerMessage(sess, message);
|
||||
}
|
||||
else
|
||||
Log.Error($"Mind for session '{sess}' could not be found");
|
||||
}
|
||||
|
||||
//you're a real zombie now, son.
|
||||
var zombiecomp = AddComp<ZombieComponent>(target);
|
||||
|
||||
@@ -246,7 +272,7 @@ public sealed partial class ZombieSystem
|
||||
if (hasMind && mind != null && _player.TryGetSessionById(mind.UserId, out var session))
|
||||
{
|
||||
//Zombie role for player manifest
|
||||
_role.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
|
||||
_role.MindAddRole(mindId, MindRoleZombie, mind: null, silent: true);
|
||||
|
||||
//Greeting message for new bebe zombers
|
||||
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
||||
@@ -267,6 +293,7 @@ public sealed partial class ZombieSystem
|
||||
ghostRole.RoleName = Loc.GetString("zombie-generic");
|
||||
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
|
||||
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
|
||||
ghostRole.MindRoles.Add(MindRoleZombie);
|
||||
}
|
||||
|
||||
if (TryComp<HandsComponent>(target, out var handsComp))
|
||||
|
||||
@@ -34,6 +34,15 @@ public sealed partial class AccessReaderComponent : Component
|
||||
[DataField("access")]
|
||||
public List<HashSet<ProtoId<AccessLevelPrototype>>> AccessLists = new();
|
||||
|
||||
/// <summary>
|
||||
/// An unmodified copy of the original list of the access groups that grant access to this reader.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If null, the access lists of this entity have not been modified yet.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public List<HashSet<ProtoId<AccessLevelPrototype>>>? AccessListsOriginal = null;
|
||||
|
||||
/// <summary>
|
||||
/// A list of <see cref="StationRecordKey"/>s that grant access. Only a single matching key is required to gain access.
|
||||
/// </summary>
|
||||
@@ -76,6 +85,16 @@ public sealed partial class AccessReaderComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool BreakOnAccessBreaker = true;
|
||||
|
||||
/// <summary>
|
||||
/// The examination text associated with this component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The text can be supplied with the 'access' variable to populate it
|
||||
/// with a comma separated list of the access levels contained in <see cref="AccessLists"/>.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public LocId ExaminationText = "access-reader-examination";
|
||||
}
|
||||
|
||||
[DataDefinition, Serializable, NetSerializable]
|
||||
@@ -96,19 +115,36 @@ public sealed class AccessReaderComponentState : ComponentState
|
||||
public bool Enabled;
|
||||
public HashSet<ProtoId<AccessLevelPrototype>> DenyTags;
|
||||
public List<HashSet<ProtoId<AccessLevelPrototype>>> AccessLists;
|
||||
public List<HashSet<ProtoId<AccessLevelPrototype>>>? AccessListsOriginal;
|
||||
public List<(NetEntity, uint)> AccessKeys;
|
||||
public Queue<AccessRecord> AccessLog;
|
||||
public int AccessLogLimit;
|
||||
|
||||
public AccessReaderComponentState(bool enabled, HashSet<ProtoId<AccessLevelPrototype>> denyTags, List<HashSet<ProtoId<AccessLevelPrototype>>> accessLists, List<(NetEntity, uint)> accessKeys, Queue<AccessRecord> accessLog, int accessLogLimit)
|
||||
public AccessReaderComponentState(
|
||||
bool enabled,
|
||||
HashSet<ProtoId<AccessLevelPrototype>> denyTags,
|
||||
List<HashSet<ProtoId<AccessLevelPrototype>>> accessLists,
|
||||
List<HashSet<ProtoId<AccessLevelPrototype>>>? accessListsOriginal,
|
||||
List<(NetEntity, uint)> accessKeys,
|
||||
Queue<AccessRecord> accessLog,
|
||||
int accessLogLimit)
|
||||
{
|
||||
Enabled = enabled;
|
||||
DenyTags = denyTags;
|
||||
AccessLists = accessLists;
|
||||
AccessListsOriginal = accessListsOriginal;
|
||||
AccessKeys = accessKeys;
|
||||
AccessLog = accessLog;
|
||||
AccessLogLimit = accessLogLimit;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised after the settings on the access reader are changed.
|
||||
/// </summary>
|
||||
public sealed class AccessReaderConfigurationChangedEvent : EntityEventArgs;
|
||||
|
||||
/// <summary>
|
||||
/// Raised before the settings on the access reader are changed. Can be cancelled.
|
||||
/// </summary>
|
||||
public sealed class AccessReaderConfigurationAttemptEvent : CancellableEntityEventArgs;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Access.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This component allows you to see whether an access reader's settings have been modified.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class ShowAccessReaderSettingsComponent : Component, IClothingSlots
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines from which equipment slots this entity can provide its benefits.
|
||||
/// </summary>
|
||||
public SlotFlags Slots { get; set; } = ~SlotFlags.POCKET;
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.DeviceLinking.Events;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.NameIdentifier;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.StationRecords;
|
||||
@@ -37,17 +40,74 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AccessReaderComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<AccessReaderComponent, GotEmaggedEvent>(OnEmagged);
|
||||
SubscribeLocalEvent<AccessReaderComponent, LinkAttemptEvent>(OnLinkAttempt);
|
||||
SubscribeLocalEvent<AccessReaderComponent, AccessReaderConfigurationAttemptEvent>(OnConfigurationAttempt);
|
||||
|
||||
SubscribeLocalEvent<AccessReaderComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<AccessReaderComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<AccessReaderComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!GetMainAccessReader(ent, out var mainAccessReader))
|
||||
return;
|
||||
|
||||
mainAccessReader.Value.Comp.AccessListsOriginal ??= new(mainAccessReader.Value.Comp.AccessLists);
|
||||
|
||||
var accessHasBeenModified = mainAccessReader.Value.Comp.AccessLists.Count != mainAccessReader.Value.Comp.AccessListsOriginal.Count;
|
||||
|
||||
if (!accessHasBeenModified)
|
||||
{
|
||||
foreach (var accessSubgroup in mainAccessReader.Value.Comp.AccessLists)
|
||||
{
|
||||
if (!mainAccessReader.Value.Comp.AccessListsOriginal.Any(y => y.SetEquals(accessSubgroup)))
|
||||
{
|
||||
accessHasBeenModified = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var canSeeAccessModification = accessHasBeenModified &&
|
||||
(HasComp<ShowAccessReaderSettingsComponent>(ent) ||
|
||||
_inventorySystem.TryGetInventoryEntity<ShowAccessReaderSettingsComponent>(args.Examiner, out _));
|
||||
|
||||
if (canSeeAccessModification)
|
||||
{
|
||||
var localizedCurrentNames = GetLocalizedAccessNames(mainAccessReader.Value.Comp.AccessLists);
|
||||
var accessesFormatted = ContentLocalizationManager.FormatListToOr(localizedCurrentNames);
|
||||
var currentSettingsMessage = localizedCurrentNames.Count > 0
|
||||
? Loc.GetString("access-reader-access-settings-modified-message", ("access", accessesFormatted))
|
||||
: Loc.GetString("access-reader-access-settings-removed-message");
|
||||
|
||||
args.PushMarkup(currentSettingsMessage);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var localizedOriginalNames = GetLocalizedAccessNames(mainAccessReader.Value.Comp.AccessListsOriginal);
|
||||
|
||||
// If the string list is empty either there were no access restrictions or the localized names were invalid
|
||||
if (localizedOriginalNames.Count == 0)
|
||||
return;
|
||||
|
||||
var originalAccessesFormatted = ContentLocalizationManager.FormatListToOr(localizedOriginalNames);
|
||||
var originalSettingsMessage = Loc.GetString(mainAccessReader.Value.Comp.ExaminationText, ("access", originalAccessesFormatted));
|
||||
args.PushMarkup(originalSettingsMessage);
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, AccessReaderComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new AccessReaderComponentState(component.Enabled, component.DenyTags, component.AccessLists,
|
||||
_recordsSystem.Convert(component.AccessKeys), component.AccessLog, component.AccessLogLimit);
|
||||
args.State = new AccessReaderComponentState(
|
||||
component.Enabled,
|
||||
component.DenyTags,
|
||||
component.AccessLists,
|
||||
component.AccessListsOriginal,
|
||||
_recordsSystem.Convert(component.AccessKeys),
|
||||
component.AccessLog,
|
||||
component.AccessLogLimit);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, AccessReaderComponent component, ref ComponentHandleState args)
|
||||
@@ -66,6 +126,7 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
}
|
||||
|
||||
component.AccessLists = new(state.AccessLists);
|
||||
component.AccessListsOriginal = state.AccessListsOriginal == null ? null : new(state.AccessListsOriginal);
|
||||
component.DenyTags = new(state.DenyTags);
|
||||
component.AccessLog = new(state.AccessLog);
|
||||
component.AccessLogLimit = state.AccessLogLimit;
|
||||
@@ -100,6 +161,13 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
Dirty(uid, reader);
|
||||
}
|
||||
|
||||
private void OnConfigurationAttempt(Entity<AccessReaderComponent> ent, ref AccessReaderConfigurationAttemptEvent args)
|
||||
{
|
||||
// The first time that the access list of the reader is modified,
|
||||
// make a copy of the original settings
|
||||
ent.Comp.AccessListsOriginal ??= new(ent.Comp.AccessLists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the source for access tags
|
||||
/// then compares it with the all targets accesses to see if it is allowed.
|
||||
@@ -348,11 +416,23 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
|
||||
#region: AccessLists API
|
||||
|
||||
/// <summary>
|
||||
/// Tries to clear the entity's <see cref="AccessReaderComponent.AccessLists"/>.
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity which is having its access permissions cleared.</param>
|
||||
public void TryClearAccesses(Entity<AccessReaderComponent> ent)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
ClearAccesses(ent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the entity's <see cref="AccessReaderComponent.AccessLists"/>.
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity which is having its access permissions cleared.</param>
|
||||
public void ClearAccesses(Entity<AccessReaderComponent> ent)
|
||||
private void ClearAccesses(Entity<AccessReaderComponent> ent)
|
||||
{
|
||||
ent.Comp.AccessLists.Clear();
|
||||
|
||||
@@ -360,32 +440,65 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to replace the access permissions in an entity's <see cref="AccessReaderComponent.AccessLists"/> with a supplied list.
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity which is having its list of access permissions replaced.</param>
|
||||
/// <param name="accesses">The list of access permissions replacing the original one.</param>
|
||||
public void TrySetAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
SetAccesses(ent, accesses);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the access permissions in an entity's <see cref="AccessReaderComponent.AccessLists"/> with a supplied list.
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity which is having its list of access permissions replaced.</param>
|
||||
/// <param name="accesses">The list of access permissions replacing the original one.</param>
|
||||
public void SetAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
private void SetAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
{
|
||||
ent.Comp.AccessLists.Clear();
|
||||
|
||||
AddAccesses(ent, accesses);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "TrySetAccesses"/>
|
||||
public void TrySetAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
SetAccesses(ent, accesses);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "SetAccesses"/>
|
||||
public void SetAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
private void SetAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
{
|
||||
ent.Comp.AccessLists.Clear();
|
||||
|
||||
AddAccesses(ent, accesses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a collection of access permissions to an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity to which the new access permissions are being added.</param>
|
||||
/// <param name="accesses">The list of access permissions being added.</param>
|
||||
public void TryAddAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
AddAccesses(ent, accesses);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a collection of access permissions to an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity to which the new access permissions are being added.</param>
|
||||
/// <param name="accesses">The list of access permissions being added.</param>
|
||||
public void AddAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
private void AddAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
{
|
||||
foreach (var access in accesses)
|
||||
{
|
||||
@@ -396,8 +509,17 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "TryAddAccesses"/>
|
||||
public void TryAddAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
AddAccesses(ent, accesses);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "AddAccesses"/>
|
||||
public void AddAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
private void AddAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
{
|
||||
foreach (var access in accesses)
|
||||
{
|
||||
@@ -408,13 +530,27 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add an access permission to an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity to which the access permission is being added.</param>
|
||||
/// <param name="access">The access permission being added.</param>
|
||||
/// <param name="dirty">If true, the component will be marked as changed afterward.</param>
|
||||
public void TryAddAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
AddAccess(ent, access);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an access permission to an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity to which the access permission is being added.</param>
|
||||
/// <param name="access">The access permission being added.</param>
|
||||
/// <param name="dirty">If true, the component will be marked as changed afterward.</param>
|
||||
public void AddAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access, bool dirty = true)
|
||||
private void AddAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access, bool dirty = true)
|
||||
{
|
||||
ent.Comp.AccessLists.Add(access);
|
||||
|
||||
@@ -425,18 +561,40 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "TryAddAccess"/>
|
||||
public void TryAddAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
AddAccess(ent, access);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "AddAccess"/>
|
||||
public void AddAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
|
||||
private void AddAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
|
||||
{
|
||||
AddAccess(ent, new HashSet<ProtoId<AccessLevelPrototype>>() { access }, dirty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to remove a collection of access permissions from an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity from which the access permissions are being removed.</param>
|
||||
/// <param name="accesses">The list of access permissions being removed.</param>
|
||||
public void TryRemoveAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
RemoveAccesses(ent, accesses);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a collection of access permissions from an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity from which the access permissions are being removed.</param>
|
||||
/// <param name="accesses">The list of access permissions being removed.</param>
|
||||
public void RemoveAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
private void RemoveAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
|
||||
{
|
||||
foreach (var access in accesses)
|
||||
{
|
||||
@@ -447,8 +605,17 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "TryRemoveAccesses"/>
|
||||
public void TryRemoveAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
RemoveAccesses(ent, accesses);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "RemoveAccesses"/>
|
||||
public void RemoveAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
private void RemoveAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
|
||||
{
|
||||
foreach (var access in accesses)
|
||||
{
|
||||
@@ -459,13 +626,27 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to removes an access permission from an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity from which the access permission is being removed.</param>
|
||||
/// <param name="access">The access permission being removed.</param>
|
||||
/// <param name="dirty">If true, the component will be marked as changed afterward.</param>
|
||||
public void TryRemoveAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
RemoveAccess(ent, access);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an access permission from an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
|
||||
/// </summary>
|
||||
/// <param name="ent">The access reader entity from which the access permission is being removed.</param>
|
||||
/// <param name="access">The access permission being removed.</param>
|
||||
/// <param name="dirty">If true, the component will be marked as changed afterward.</param>
|
||||
public void RemoveAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access, bool dirty = true)
|
||||
private void RemoveAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access, bool dirty = true)
|
||||
{
|
||||
for (int i = ent.Comp.AccessLists.Count - 1; i >= 0; i--)
|
||||
{
|
||||
@@ -482,12 +663,29 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "TryRemoveAccess"/>
|
||||
public void TryRemoveAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access)
|
||||
{
|
||||
if (CanConfigureAccessReader(ent))
|
||||
{
|
||||
RemoveAccess(ent, new HashSet<ProtoId<AccessLevelPrototype>>() { access });
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref = "RemoveAccess"/>
|
||||
public void RemoveAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
|
||||
private void RemoveAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
|
||||
{
|
||||
RemoveAccess(ent, new HashSet<ProtoId<AccessLevelPrototype>>() { access }, dirty);
|
||||
}
|
||||
|
||||
private bool CanConfigureAccessReader(Entity<AccessReaderComponent> ent)
|
||||
{
|
||||
var ev = new AccessReaderConfigurationAttemptEvent();
|
||||
RaiseLocalEvent(ent, ev);
|
||||
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: AccessKeys API
|
||||
@@ -727,4 +925,38 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private List<string> GetLocalizedAccessNames(List<HashSet<ProtoId<AccessLevelPrototype>>> accessLists)
|
||||
{
|
||||
var localizedNames = new List<string>();
|
||||
string? andSeparator = null;
|
||||
|
||||
foreach (var accessHashSet in accessLists)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var accessSubset = accessHashSet.ToList();
|
||||
|
||||
// Combine the names of all access levels in the subset into a single string
|
||||
foreach (var access in accessSubset)
|
||||
{
|
||||
var accessName = Loc.GetString("access-reader-unknown-id");
|
||||
|
||||
if (_prototype.Resolve(access, out var accessProto) && !string.IsNullOrWhiteSpace(accessProto.Name))
|
||||
accessName = Loc.GetString(accessProto.Name);
|
||||
|
||||
sb.Append(Loc.GetString("access-reader-access-label", ("access", accessName)));
|
||||
|
||||
if (accessSubset.IndexOf(access) < (accessSubset.Count - 1))
|
||||
{
|
||||
andSeparator ??= " " + Loc.GetString("generic-and") + " ";
|
||||
sb.Append(andSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
// Add this string to the list
|
||||
localizedNames.Add(sb.ToString());
|
||||
}
|
||||
|
||||
return localizedNames;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Net;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Administration;
|
||||
@@ -21,32 +23,9 @@ public sealed class BanPanelEuiState : EuiStateBase
|
||||
public static class BanPanelEuiStateMsg
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CreateBanRequest : EuiMessageBase
|
||||
public sealed class CreateBanRequest(Ban ban) : EuiMessageBase
|
||||
{
|
||||
public string? Player { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
public ImmutableTypedHwid? Hwid { get; set; }
|
||||
public uint Minutes { get; set; }
|
||||
public string Reason { get; set; }
|
||||
public NoteSeverity Severity { get; set; }
|
||||
public string[]? Roles { get; set; }
|
||||
public bool UseLastIp { get; set; }
|
||||
public bool UseLastHwid { get; set; }
|
||||
public bool Erase { get; set; }
|
||||
|
||||
public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase)
|
||||
{
|
||||
Player = player;
|
||||
IpAddress = ipAddress == null ? null : $"{ipAddress.Value.Item1}/{ipAddress.Value.Item2}";
|
||||
UseLastIp = useLastIp;
|
||||
Hwid = hwid;
|
||||
UseLastHwid = useLastHwid;
|
||||
Minutes = minutes;
|
||||
Reason = reason;
|
||||
Severity = severity;
|
||||
Roles = roles;
|
||||
Erase = erase;
|
||||
}
|
||||
public Ban Ban { get; } = ban;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
@@ -60,3 +39,50 @@ public static class BanPanelEuiStateMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains all the data related to a particular ban action created by the BanPanel window.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed record Ban
|
||||
{
|
||||
public Ban(
|
||||
string? target,
|
||||
(IPAddress, int)? ipAddressTuple,
|
||||
bool useLastIp,
|
||||
ImmutableTypedHwid? hwid,
|
||||
bool useLastHwid,
|
||||
uint banDurationMinutes,
|
||||
string reason,
|
||||
NoteSeverity severity,
|
||||
ProtoId<JobPrototype>[]? bannedJobs,
|
||||
ProtoId<AntagPrototype>[]? bannedAntags,
|
||||
bool erase)
|
||||
{
|
||||
Target = target;
|
||||
IpAddress = ipAddressTuple?.Item1.ToString();
|
||||
IpAddressHid = ipAddressTuple?.Item2.ToString() ?? "0";
|
||||
UseLastIp = useLastIp;
|
||||
Hwid = hwid;
|
||||
UseLastHwid = useLastHwid;
|
||||
BanDurationMinutes = banDurationMinutes;
|
||||
Reason = reason;
|
||||
Severity = severity;
|
||||
BannedJobs = bannedJobs;
|
||||
BannedAntags = bannedAntags;
|
||||
Erase = erase;
|
||||
}
|
||||
|
||||
public readonly string? Target;
|
||||
public readonly string? IpAddress;
|
||||
public readonly string? IpAddressHid;
|
||||
public readonly bool UseLastIp;
|
||||
public readonly ImmutableTypedHwid? Hwid;
|
||||
public readonly bool UseLastHwid;
|
||||
public readonly uint BanDurationMinutes;
|
||||
public readonly string Reason;
|
||||
public readonly NoteSeverity Severity;
|
||||
public readonly ProtoId<JobPrototype>[]? BannedJobs;
|
||||
public readonly ProtoId<AntagPrototype>[]? BannedAntags;
|
||||
public readonly bool Erase;
|
||||
}
|
||||
|
||||
@@ -187,22 +187,22 @@ public sealed partial class DoorComponent : Component
|
||||
public string EmaggingSpriteState = "sparks";
|
||||
|
||||
/// <summary>
|
||||
/// The sprite state used for the door when it's open.
|
||||
/// The length of the door's opening animation.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float OpeningAnimationTime = 0.8f;
|
||||
public TimeSpan OpeningAnimationTime = TimeSpan.FromSeconds(0.8);
|
||||
|
||||
/// <summary>
|
||||
/// The sprite state used for the door when it's open.
|
||||
/// The length of the door's closing animation.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ClosingAnimationTime = 0.8f;
|
||||
public TimeSpan ClosingAnimationTime = TimeSpan.FromSeconds(0.8);
|
||||
|
||||
/// <summary>
|
||||
/// The sprite state used for the door when it's open.
|
||||
/// The length of the door's emagging animation.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float EmaggingAnimationTime = 1.5f;
|
||||
public TimeSpan EmaggingAnimationTime = TimeSpan.FromSeconds(1.5);
|
||||
|
||||
/// <summary>
|
||||
/// The animation used when the door opens.
|
||||
@@ -264,8 +264,8 @@ public sealed partial class DoorComponent : Component
|
||||
/// <summary>
|
||||
/// Default time that the door should take to pry open.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float PryTime = 1.5f;
|
||||
[DataField]
|
||||
public TimeSpan PryTime = TimeSpan.FromSeconds(1.5f);
|
||||
|
||||
[DataField]
|
||||
public bool ChangeAirtight = true;
|
||||
|
||||
@@ -12,7 +12,7 @@ public sealed partial class ResetNarcolepsy : EventEntityEffect<ResetNarcolepsy>
|
||||
/// The # of seconds the effect resets the narcolepsy timer to
|
||||
/// </summary>
|
||||
[DataField("TimerReset")]
|
||||
public int TimerReset = 600;
|
||||
public TimeSpan TimerReset = TimeSpan.FromSeconds(600);
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
=> Loc.GetString("reagent-effect-guidebook-reset-narcolepsy", ("chance", Probability));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Ghost.Roles
|
||||
@@ -12,11 +13,10 @@ namespace Content.Shared.Ghost.Roles
|
||||
public string Description { get; set; }
|
||||
public string Rules { get; set; }
|
||||
|
||||
// TODO ROLE TIMERS
|
||||
// Actually make use of / enforce this requirement?
|
||||
// Why is this even here.
|
||||
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
|
||||
public HashSet<JobRequirement>? Requirements { get; set; }
|
||||
/// <summary>
|
||||
/// A list of all antag and job prototype IDs of the ghost role and its mind role(s).
|
||||
/// </summary>
|
||||
public (List<ProtoId<JobPrototype>>?,List<ProtoId<AntagPrototype>>?) RolePrototypes;
|
||||
|
||||
/// <inheritdoc cref="GhostRoleKind"/>
|
||||
public GhostRoleKind Kind { get; set; }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user