Compare commits
185 Commits
master
...
ed-29-09-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d73fcd53ff | ||
|
|
d25f979691 | ||
|
|
824cddaa72 | ||
|
|
97ac343c6c | ||
|
|
58b6f3d64e | ||
|
|
d6a8c169b2 | ||
|
|
b8ed3f9664 | ||
|
|
b2d09ba457 | ||
|
|
d699a4e985 | ||
|
|
42786240ec | ||
|
|
705e4d3aa1 | ||
|
|
ea3c44686c | ||
|
|
4555b72608 | ||
|
|
0663576c46 | ||
|
|
0678e3b468 | ||
|
|
f6cd8673d3 | ||
|
|
aa828b96ab | ||
|
|
7c39b4595f | ||
|
|
c55b41dff8 | ||
|
|
1e219aaf49 | ||
|
|
fabef941c2 | ||
|
|
7102da139b | ||
|
|
005683d074 | ||
|
|
320e67a411 | ||
|
|
dddb6163f5 | ||
|
|
329908df92 | ||
|
|
2f7b73e830 | ||
|
|
eee5751a22 | ||
|
|
3ee7d81944 | ||
|
|
04d71da982 | ||
|
|
3f575a64f3 | ||
|
|
8e9aa1dbb6 | ||
|
|
add531a434 | ||
|
|
a3ddba6f42 | ||
|
|
fd40888b0e | ||
|
|
c1a21693fa | ||
|
|
d79fb62d8d | ||
|
|
95d91283a3 | ||
|
|
f5cad5f12f | ||
|
|
a26bafacb1 | ||
|
|
c0b1eae162 | ||
|
|
c7f5545a46 | ||
|
|
c70d2cfb9f | ||
|
|
b58bf396bc | ||
|
|
2824334a1e | ||
|
|
83fe027964 | ||
|
|
29e1f6cddf | ||
|
|
08c1b2c9be | ||
|
|
92f246058c | ||
|
|
0ac83937c9 | ||
|
|
eabb00a1e2 | ||
|
|
c7b239bcbb | ||
|
|
2245235db1 | ||
|
|
d5face573d | ||
|
|
818a715822 | ||
|
|
7678251ad5 | ||
|
|
b6797afe52 | ||
|
|
f9243dfdd7 | ||
|
|
8c16b4580b | ||
|
|
cc4cab5677 | ||
|
|
9893aca467 | ||
|
|
b85fed759a | ||
|
|
ae22c7c3d0 | ||
|
|
85f3cc7583 | ||
|
|
a746c3cc0f | ||
|
|
1c74e1e100 | ||
|
|
a5129c141c | ||
|
|
886b365099 | ||
|
|
e6e47b599d | ||
|
|
365d12a4e9 | ||
|
|
63c468d963 | ||
|
|
4796c92609 | ||
|
|
5b255d13c6 | ||
|
|
7c650da7d7 | ||
|
|
11e965cd99 | ||
|
|
0c7b1e9163 | ||
|
|
512f28458c | ||
|
|
c2fb4a126f | ||
|
|
2b411b244e | ||
|
|
c075c89cd0 | ||
|
|
4f311d6c44 | ||
|
|
5c54d199a8 | ||
|
|
ed89c0e061 | ||
|
|
867d0f5130 | ||
|
|
e1da7ec9c5 | ||
|
|
393e6cbc07 | ||
|
|
5a67e3c26a | ||
|
|
c19cdad787 | ||
|
|
d95b5da7d2 | ||
|
|
b2c8565a2d | ||
|
|
9d0a7b7729 | ||
|
|
76b680b03b | ||
|
|
fbb9c9c524 | ||
|
|
8cf9da90d3 | ||
|
|
6d576fc8ce | ||
|
|
5cb0917d5f | ||
|
|
128d06518e | ||
|
|
0e0f015422 | ||
|
|
e09ea850f5 | ||
|
|
d9d968a479 | ||
|
|
f13f7830d6 | ||
|
|
8cf5c3f6bc | ||
|
|
c4a42e556f | ||
|
|
940eaa4674 | ||
|
|
eb1bd0a565 | ||
|
|
2349898dcc | ||
|
|
3844f1e7a5 | ||
|
|
b41ce9cce6 | ||
|
|
4a815c006f | ||
|
|
27b86bcca8 | ||
|
|
e59bc06c25 | ||
|
|
0a61f2a583 | ||
|
|
1a92ada5bd | ||
|
|
9c98f5f9f4 | ||
|
|
857ae2a088 | ||
|
|
ffb5bd7325 | ||
|
|
b692b6e33e | ||
|
|
e1ba33814b | ||
|
|
933da32da5 | ||
|
|
684a4a382d | ||
|
|
1dd977effd | ||
|
|
ca47e59e43 | ||
|
|
0dd1733998 | ||
|
|
599b962234 | ||
|
|
09eee5074d | ||
|
|
f21c6f2030 | ||
|
|
ce05248428 | ||
|
|
a4368264f0 | ||
|
|
fc89f231a5 | ||
|
|
7ff98dd94f | ||
|
|
bf18b5e26b | ||
|
|
dfc7d183ad | ||
|
|
972adcee21 | ||
|
|
138ea68076 | ||
|
|
377dd6b36c | ||
|
|
bc0691822a | ||
|
|
d80f53bb48 | ||
|
|
d8ab007c50 | ||
|
|
731d6ff53c | ||
|
|
09a197eb91 | ||
|
|
584f0aaa7b | ||
|
|
fb71020889 | ||
|
|
7444c8ea4a | ||
|
|
e0fd44da66 | ||
|
|
9b5f9c3fd6 | ||
|
|
e27576929f | ||
|
|
02061592dd | ||
|
|
ff94d3e7ad | ||
|
|
ea89711029 | ||
|
|
fd20cc2a00 | ||
|
|
31d30f24f9 | ||
|
|
9313c07924 | ||
|
|
e05d9e944b | ||
|
|
f1d52e0c13 | ||
|
|
97d4153d84 | ||
|
|
b4f4d6e295 | ||
|
|
2ffe0db61f | ||
|
|
11d434818e | ||
|
|
cc6aa626da | ||
|
|
9c3af67cd1 | ||
|
|
8c67c5b5a2 | ||
|
|
3732885713 | ||
|
|
7616b9aa1c | ||
|
|
1908317e3c | ||
|
|
fdd4789d32 | ||
|
|
29da03b4e4 | ||
|
|
c317fa9840 | ||
|
|
e7dc6ae990 | ||
|
|
3aece8d46c | ||
|
|
52a4e95651 | ||
|
|
f1ae8ecdfe | ||
|
|
6768ff1e91 | ||
|
|
d17182c162 | ||
|
|
a4b7cd73c5 | ||
|
|
659648b03d | ||
|
|
bcc30813e9 | ||
|
|
0ba1a7c4dd | ||
|
|
ab40b1ab73 | ||
|
|
79a34556e5 | ||
|
|
71bcda1fec | ||
|
|
82e7cb020c | ||
|
|
928e6c8079 | ||
|
|
c6fc95e32b | ||
|
|
5e84fae772 | ||
|
|
2f3db89ca2 |
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;
|
||||
@@ -25,7 +25,7 @@ namespace Content.Client.Access.UI
|
||||
public void SetAccessLevels(IPrototypeManager protoManager, List<ProtoId<AccessLevelPrototype>> accessLevels)
|
||||
{
|
||||
_accessButtons.Clear();
|
||||
AccessLevelGrid.DisposeAllChildren();
|
||||
AccessLevelGrid.RemoveAllChildren();
|
||||
|
||||
foreach (var access in accessLevels)
|
||||
{
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Content.Client.Access.UI
|
||||
|
||||
public void SetAllowedIcons(string currentJobIconId)
|
||||
{
|
||||
IconGrid.DisposeAllChildren();
|
||||
IconGrid.RemoveAllChildren();
|
||||
|
||||
var jobIconButtonGroup = new ButtonGroup();
|
||||
var i = 0;
|
||||
|
||||
@@ -99,8 +99,8 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
|
||||
|
||||
private bool TryRebuildAccessGroupControls()
|
||||
{
|
||||
AccessGroupList.DisposeAllChildren();
|
||||
AccessLevelChecklist.DisposeAllChildren();
|
||||
AccessGroupList.RemoveAllChildren();
|
||||
AccessLevelChecklist.RemoveAllChildren();
|
||||
|
||||
// No access level prototypes were assigned to any of the access level groups.
|
||||
// Either the turret controller has no assigned access levels or their names were invalid.
|
||||
@@ -165,7 +165,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
|
||||
/// </summary>
|
||||
public void RebuildAccessLevelsControls()
|
||||
{
|
||||
AccessLevelChecklist.DisposeAllChildren();
|
||||
AccessLevelChecklist.RemoveAllChildren();
|
||||
_accessLevelEntries.Clear();
|
||||
|
||||
// No access level prototypes were assigned to any of the access level groups
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Content.Client.Actions
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IResourceManager _resources = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization = default!;
|
||||
|
||||
public event Action<EntityUid>? OnActionAdded;
|
||||
public event Action<EntityUid>? OnActionRemoved;
|
||||
@@ -286,8 +287,27 @@ namespace Content.Client.Actions
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assignmentNode is SequenceDataNode sequenceAssignments)
|
||||
{
|
||||
try
|
||||
{
|
||||
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(sequenceAssignments, notNullableOverride: true);
|
||||
|
||||
foreach (var index in nodeAssignments)
|
||||
{
|
||||
assignments.Add(new SlotAssignment(index.Hotbar, index.Slot, actionId));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"Failed to parse action assignments: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
AddActionDirect((user, actions), actionId);
|
||||
}
|
||||
|
||||
AssignSlot?.Invoke(assignments);
|
||||
}
|
||||
|
||||
private void OnWorldTargetAttempt(Entity<WorldTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
|
||||
@@ -309,10 +329,10 @@ namespace Content.Client.Actions
|
||||
// this is the actual entity-world targeting magic
|
||||
EntityUid? targetEnt = null;
|
||||
if (TryComp<EntityTargetActionComponent>(ent, out var entity) &&
|
||||
args.Input.EntityUid != null &&
|
||||
ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
|
||||
args.Input.EntityUid is { Valid: true } entityUid &&
|
||||
ValidateEntityTarget(user, entityUid, (uid, entity)))
|
||||
{
|
||||
targetEnt = args.Input.EntityUid;
|
||||
targetEnt = entityUid;
|
||||
}
|
||||
|
||||
if (action.ClientExclusive)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc admin-camera-window-title-placeholder}"
|
||||
SetSize="425 550"
|
||||
MinSize="200 225"
|
||||
Name="Window">
|
||||
MinSize="200 225">
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
|
||||
/// </summary>
|
||||
public void UpdateReagents()
|
||||
{
|
||||
ReagentList.DisposeAllChildren();
|
||||
ReagentList.RemoveAllChildren();
|
||||
|
||||
if (_selectedSolution == null || _solutions == null)
|
||||
return;
|
||||
@@ -92,7 +92,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
|
||||
/// <param name="solution">The selected solution.</param>
|
||||
private void UpdateVolumeBox(Solution solution)
|
||||
{
|
||||
VolumeBox.DisposeAllChildren();
|
||||
VolumeBox.RemoveAllChildren();
|
||||
|
||||
var volumeLabel = new Label();
|
||||
volumeLabel.HorizontalExpand = true;
|
||||
@@ -131,7 +131,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
|
||||
/// <param name="solution">The selected solution.</param>
|
||||
private void UpdateThermalBox(Solution solution)
|
||||
{
|
||||
ThermalBox.DisposeAllChildren();
|
||||
ThermalBox.RemoveAllChildren();
|
||||
var heatCap = solution.GetHeatCapacity(null);
|
||||
var specificHeatLabel = new Label();
|
||||
specificHeatLabel.HorizontalExpand = true;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -206,7 +206,7 @@ namespace Content.Client.Cargo.UI
|
||||
if (!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
|
||||
return;
|
||||
|
||||
Requests.DisposeAllChildren();
|
||||
Requests.RemoveAllChildren();
|
||||
|
||||
foreach (var order in orders)
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Content.Client.Cargo.UI
|
||||
|
||||
public void SetOrders(SpriteSystem sprites, IPrototypeManager protoManager, List<CargoOrderData> orders)
|
||||
{
|
||||
Orders.DisposeAllChildren();
|
||||
Orders.RemoveAllChildren();
|
||||
|
||||
foreach (var order in orders)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Client.CrewManifest.UI;
|
||||
using Content.Client.CrewManifest.UI;
|
||||
using Content.Shared.CrewManifest;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -21,7 +21,6 @@ public sealed partial class CrewManifestUiFragment : BoxContainer
|
||||
|
||||
public void UpdateState(string stationName, CrewManifestEntries? entries)
|
||||
{
|
||||
CrewManifestListing.DisposeAllChildren();
|
||||
CrewManifestListing.RemoveAllChildren();
|
||||
|
||||
StationNameContainer.Visible = entries != null;
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Content.Client.Changelog
|
||||
// Changelog is not kept in memory so load it again.
|
||||
var changelogs = await _changelog.LoadChangelog();
|
||||
|
||||
Tabs.DisposeAllChildren();
|
||||
Tabs.RemoveAllChildren();
|
||||
|
||||
var i = 0;
|
||||
foreach (var changelog in changelogs)
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace Content.Client.ContextMenu.UI
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
RootMenu.MenuBody.DisposeAllChildren();
|
||||
RootMenu.MenuBody.RemoveAllChildren();
|
||||
CancelOpen?.Cancel();
|
||||
CancelClose?.Cancel();
|
||||
OnContextClosed?.Invoke();
|
||||
|
||||
@@ -293,7 +293,7 @@ namespace Content.Client.ContextMenu.UI
|
||||
var element = new EntityMenuElement(entity);
|
||||
element.SubMenu = new ContextMenuPopup(_context, element);
|
||||
element.SubMenu.OnPopupOpen += () => _verb.OpenVerbMenu(entity, popup: element.SubMenu);
|
||||
element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.DisposeAllChildren;
|
||||
element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.RemoveAllChildren;
|
||||
_context.AddElement(menu, element);
|
||||
Elements.TryAdd(entity, element);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Content.Client.Crayon.UI
|
||||
private void RefreshList()
|
||||
{
|
||||
// Clear
|
||||
Grids.DisposeAllChildren();
|
||||
Grids.RemoveAllChildren();
|
||||
|
||||
if (_decals == null || _allDecals == null)
|
||||
return;
|
||||
|
||||
@@ -18,7 +18,6 @@ public sealed partial class CrewManifestUi : DefaultWindow
|
||||
|
||||
public void Populate(string name, CrewManifestEntries? entries)
|
||||
{
|
||||
CrewManifestListing.DisposeAllChildren();
|
||||
CrewManifestListing.RemoveAllChildren();
|
||||
|
||||
StationNameContainer.Visible = entries != null;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -72,7 +72,7 @@ public sealed partial class GatewayWindow : FancyWindow,
|
||||
_isUnlockPending = _nextUnlock >= _timing.CurTime;
|
||||
_isCooldownPending = _nextReady >= _timing.CurTime;
|
||||
|
||||
Container.DisposeAllChildren();
|
||||
Container.RemoveAllChildren();
|
||||
|
||||
if (_destinations.Count == 0)
|
||||
{
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
AlertsContainer.Visible = showAlerts;
|
||||
|
||||
if (showAlerts)
|
||||
AlertsContainer.DisposeAllChildren();
|
||||
AlertsContainer.RemoveAllChildren();
|
||||
|
||||
if (msg.Unrevivable == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
|
||||
@@ -404,7 +404,7 @@ public sealed partial class MarkingPicker : Control
|
||||
|
||||
var stateNames = GetMarkingStateNames(prototype);
|
||||
_currentMarkingColors.Clear();
|
||||
CMarkingColors.DisposeAllChildren();
|
||||
CMarkingColors.RemoveAllChildren();
|
||||
List<ColorSelectorSliders> colorSliders = new();
|
||||
for (int i = 0; i < prototype.Sprites.Count; i++)
|
||||
{
|
||||
|
||||
@@ -216,7 +216,6 @@ public sealed partial class SingleMarkingPicker : BoxContainer
|
||||
|
||||
var marking = _markings[Slot];
|
||||
|
||||
ColorSelectorContainer.DisposeAllChildren();
|
||||
ColorSelectorContainer.RemoveAllChildren();
|
||||
|
||||
if (marking.MarkingColors.Count != proto.Sprites.Count)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.IdentityManagement;
|
||||
|
||||
namespace Content.Client.IdentityManagement;
|
||||
|
||||
public sealed class IdentitySystem : SharedIdentitySystem
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace Content.Client.Lobby.UI
|
||||
public void ReloadCharacterPickers()
|
||||
{
|
||||
_createNewCharacterButton.Orphan();
|
||||
Characters.DisposeAllChildren();
|
||||
Characters.RemoveAllChildren();
|
||||
|
||||
var numberOfFullSlots = 0;
|
||||
var characterButtonsGroup = new ButtonGroup();
|
||||
|
||||
@@ -491,7 +491,7 @@ namespace Content.Client.Lobby.UI
|
||||
/// </summary>
|
||||
public void RefreshTraits()
|
||||
{
|
||||
TraitsList.DisposeAllChildren();
|
||||
TraitsList.RemoveAllChildren();
|
||||
|
||||
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
|
||||
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
||||
@@ -632,7 +632,7 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
public void RefreshAntags()
|
||||
{
|
||||
AntagList.DisposeAllChildren();
|
||||
AntagList.RemoveAllChildren();
|
||||
var items = new[]
|
||||
{
|
||||
("humanoid-profile-editor-antag-preference-yes-button", 0),
|
||||
@@ -660,8 +660,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);
|
||||
@@ -824,7 +826,7 @@ namespace Content.Client.Lobby.UI
|
||||
/// </summary>
|
||||
public void RefreshJobs()
|
||||
{
|
||||
JobList.DisposeAllChildren();
|
||||
JobList.RemoveAllChildren();
|
||||
_jobCategories.Clear();
|
||||
_jobPriorities.Clear();
|
||||
var firstCategory = true;
|
||||
|
||||
@@ -42,7 +42,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
|
||||
{
|
||||
var protoMan = collection.Resolve<IPrototypeManager>();
|
||||
var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
|
||||
RestrictionsContainer.DisposeAllChildren();
|
||||
RestrictionsContainer.RemoveAllChildren();
|
||||
|
||||
if (_groupProto.MinLimit > 0)
|
||||
{
|
||||
@@ -71,7 +71,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
|
||||
});
|
||||
}
|
||||
|
||||
LoadoutsContainer.DisposeAllChildren();
|
||||
LoadoutsContainer.RemoveAllChildren();
|
||||
|
||||
// Get all loadout prototypes for this group.
|
||||
var validProtos = _groupProto.Loadouts.Select(id => protoMan.Index(id));
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed partial class LobbyCharacterPreviewPanel : Control
|
||||
|
||||
_previewDummy = uid;
|
||||
|
||||
ViewBox.DisposeAllChildren();
|
||||
ViewBox.RemoveAllChildren();
|
||||
var spriteView = new SpriteView
|
||||
{
|
||||
OverrideDirection = Direction.South,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
@@ -37,7 +37,7 @@ public sealed partial class MappingPrototypeList : Control
|
||||
{
|
||||
_prototypes.Clear();
|
||||
|
||||
PrototypeList.DisposeAllChildren();
|
||||
PrototypeList.RemoveAllChildren();
|
||||
|
||||
_prototypes.AddRange(prototypes);
|
||||
|
||||
@@ -99,7 +99,7 @@ public sealed partial class MappingPrototypeList : Control
|
||||
public void Search(List<MappingPrototype> prototypes)
|
||||
{
|
||||
_search.Clear();
|
||||
SearchList.DisposeAllChildren();
|
||||
SearchList.RemoveAllChildren();
|
||||
_lastIndices = (0, -1);
|
||||
|
||||
_search.AddRange(prototypes);
|
||||
|
||||
@@ -861,7 +861,7 @@ public sealed class MappingState : GameplayStateBase
|
||||
}
|
||||
else
|
||||
{
|
||||
button.ChildrenPrototypes.DisposeAllChildren();
|
||||
button.ChildrenPrototypes.RemoveAllChildren();
|
||||
button.CollapseButton.Label.Text = "▶";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -23,7 +28,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();
|
||||
@@ -64,7 +69,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._CP14.CloudShadow;
|
||||
@@ -35,7 +36,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;
|
||||
|
||||
@@ -56,10 +57,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))
|
||||
@@ -70,24 +73,41 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
//CP14 Overlays
|
||||
if (_entManager.TryGetComponent<CP14CloudShadowsComponent>(mapUid, out var shadows))
|
||||
{
|
||||
DrawCloudShadows(args, shadows, invMatrix);
|
||||
DrawCloudShadows(args, res, shadows, invMatrix);
|
||||
}
|
||||
//CP14 Overlays end
|
||||
|
||||
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;
|
||||
|
||||
@@ -152,6 +219,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;
|
||||
|
||||
@@ -53,8 +53,8 @@ public sealed partial class PowerMonitoringWindow
|
||||
// Selection action
|
||||
windowEntry.Button.OnButtonUp += args =>
|
||||
{
|
||||
windowEntry.SourcesContainer.DisposeAllChildren();
|
||||
windowEntry.LoadsContainer.DisposeAllChildren();
|
||||
windowEntry.SourcesContainer.RemoveAllChildren();
|
||||
windowEntry.LoadsContainer.RemoveAllChildren();
|
||||
ButtonAction(windowEntry, masterContainer);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed partial class OfferingWindow : FancyWindow,
|
||||
|
||||
public void ClearOptions()
|
||||
{
|
||||
Container.DisposeAllChildren();
|
||||
Container.RemoveAllChildren();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
|
||||
@@ -67,8 +67,8 @@ public sealed partial class DockingScreen : BoxContainer
|
||||
{
|
||||
DockingControl.BuildDocks(shuttle);
|
||||
var currentDock = DockingControl.ViewedDock;
|
||||
// DockedWith.DisposeAllChildren();
|
||||
DockPorts.DisposeAllChildren();
|
||||
// DockedWith.RemoveAllChildren();
|
||||
DockPorts.RemoveAllChildren();
|
||||
_ourDockButtons.Clear();
|
||||
|
||||
if (shuttle == null)
|
||||
|
||||
@@ -59,7 +59,7 @@ public sealed partial class EmergencyConsoleWindow : FancyWindow,
|
||||
// TODO: Loc and cvar for this.
|
||||
_earlyLaunchTime = scc.EarlyLaunchTime;
|
||||
|
||||
AuthorizationsContainer.DisposeAllChildren();
|
||||
AuthorizationsContainer.RemoveAllChildren();
|
||||
var remainingAuths = scc.AuthorizationsRequired - scc.Authorizations.Count;
|
||||
AuthorizationCount.Text = Loc.GetString("emergency-shuttle-ui-remaining", ("remaining", remainingAuths));
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ public sealed partial class MapScreen : BoxContainer
|
||||
private void ClearMapObjects()
|
||||
{
|
||||
_mapObjectControls.Clear();
|
||||
HyperspaceDestinations.DisposeAllChildren();
|
||||
HyperspaceDestinations.RemoveAllChildren();
|
||||
_pendingMapObjects.Clear();
|
||||
_mapObjects.Clear();
|
||||
_mapHeadings.Clear();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -25,9 +25,9 @@ namespace Content.Client.Strip
|
||||
|
||||
public void ClearButtons()
|
||||
{
|
||||
InventoryContainer.DisposeAllChildren();
|
||||
HandsContainer.DisposeAllChildren();
|
||||
SnareContainer.DisposeAllChildren();
|
||||
InventoryContainer.RemoveAllChildren();
|
||||
HandsContainer.RemoveAllChildren();
|
||||
SnareContainer.RemoveAllChildren();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
|
||||
|
||||
public void UpdateState(ThiefBackpackBoundUserInterfaceState state)
|
||||
{
|
||||
SetsGrid.DisposeAllChildren();
|
||||
SetsGrid.RemoveAllChildren();
|
||||
var selectedNumber = 0;
|
||||
foreach (var (set, info) in state.Sets)
|
||||
{
|
||||
|
||||
@@ -66,7 +66,8 @@ namespace Content.Client.UserInterface.Controls
|
||||
Viewport.StretchMode = filterMode switch
|
||||
{
|
||||
"nearest" => ScalingViewportStretchMode.Nearest,
|
||||
"bilinear" => ScalingViewportStretchMode.Bilinear
|
||||
"bilinear" => ScalingViewportStretchMode.Bilinear,
|
||||
_ => ScalingViewportStretchMode.Nearest
|
||||
};
|
||||
Viewport.IgnoreDimension = verticalFit ? ScalingViewportIgnoreDimension.Horizontal : ScalingViewportIgnoreDimension.None;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -19,7 +19,7 @@ namespace Content.Client.UserInterface.Controls
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
DisposeAllChildren();
|
||||
RemoveAllChildren();
|
||||
}
|
||||
|
||||
public void AddEntry(float amount, Color color, string? tooltip = null)
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Content.Client.UserInterface
|
||||
|
||||
public void UpdateValues(List<string> headers, List<string[]> values)
|
||||
{
|
||||
Values.DisposeAllChildren();
|
||||
Values.RemoveAllChildren();
|
||||
Values.Columns = headers.Count;
|
||||
|
||||
for (var i = 0; i < headers.Count; i++)
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls
|
||||
|
||||
public void Populate()
|
||||
{
|
||||
ButtonContainer.DisposeAllChildren();
|
||||
ButtonContainer.RemoveAllChildren();
|
||||
AddButtons();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
public void ClearEntries()
|
||||
{
|
||||
NoRolesMessage.Visible = true;
|
||||
EntryContainer.DisposeAllChildren();
|
||||
EntryContainer.RemoveAllChildren();
|
||||
_collapsibleBoxes.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
@@ -74,7 +74,7 @@ public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
|
||||
public void Clear()
|
||||
{
|
||||
ClearButtons();
|
||||
_grid.DisposeAllChildren();
|
||||
_grid.RemoveAllChildren();
|
||||
}
|
||||
|
||||
public IEnumerable<HandButton> GetButtons()
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Content.Client.Verbs.UI
|
||||
Close();
|
||||
|
||||
var menu = popup ?? _context.RootMenu;
|
||||
menu.MenuBody.DisposeAllChildren();
|
||||
menu.MenuBody.RemoveAllChildren();
|
||||
|
||||
CurrentTarget = target;
|
||||
CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, out ExtraCategories, force);
|
||||
@@ -207,7 +207,7 @@ namespace Content.Client.Verbs.UI
|
||||
/// </summary>
|
||||
public void AddServerVerbs(List<Verb>? verbs, ContextMenuPopup popup)
|
||||
{
|
||||
popup.MenuBody.DisposeAllChildren();
|
||||
popup.MenuBody.RemoveAllChildren();
|
||||
|
||||
// Verbs may be null if the server does not think we can see the target entity. This **should** not happen.
|
||||
if (verbs == null)
|
||||
|
||||
@@ -7,7 +7,11 @@ namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class StencilOverlay
|
||||
{
|
||||
private void DrawCloudShadows(in OverlayDrawArgs args, CP14CloudShadowsComponent cloudComp, Matrix3x2 invMatrix)
|
||||
private void DrawCloudShadows(
|
||||
in OverlayDrawArgs args,
|
||||
CachedResources res,
|
||||
CP14CloudShadowsComponent cloudComp,
|
||||
Matrix3x2 invMatrix)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var mapId = args.MapId;
|
||||
@@ -18,7 +22,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();
|
||||
@@ -51,7 +55,7 @@ public sealed partial class StencilOverlay
|
||||
|
||||
worldHandle.SetTransform(Matrix3x2.Identity);
|
||||
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilMask").Instance());
|
||||
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
|
||||
worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
|
||||
var curTime = _timing.RealTime;
|
||||
var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(cloudComp.ParallaxPath), curTime);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -55,7 +55,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);
|
||||
@@ -63,10 +63,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);
|
||||
@@ -74,14 +74,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);
|
||||
@@ -91,10 +91,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(() =>
|
||||
{
|
||||
@@ -103,7 +103,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.");
|
||||
}
|
||||
}
|
||||
@@ -285,7 +285,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
|
||||
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
|
||||
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(entMan.HasComponent<AudioComponent>);
|
||||
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(e => !entMan.HasComponent<AudioComponent>(e));
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
@@ -325,8 +325,8 @@ namespace Content.IntegrationTests.Tests
|
||||
// Check that the number of entities has gone back to the original value.
|
||||
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deletion: count didn't reset properly\n" +
|
||||
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
|
||||
Assert.That(client.EntMan.EntityCount, Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
|
||||
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
|
||||
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
|
||||
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
|
||||
$"Server count was {count}.\n" +
|
||||
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user