Merge pull request #241 from crystallpunk-14/ed-13-06-2024-upstream2

Ed 13 06 2024 upstream 3
This commit is contained in:
Ed
2024-06-13 23:20:26 +03:00
committed by GitHub
491 changed files with 23794 additions and 12205 deletions

2
.github/CODEOWNERS vendored
View File

@@ -15,6 +15,7 @@
/Content.*/GameTicking/ @moonheart08 @EmoGarbage404
/Resources/ServerInfo/ @moonheart08 @Chief-Engineer
/Resources/ServerInfo/Guidebook/ @moonheart08 @EmoGarbage404
/Resources/ServerInfo/Guidebook/ServerRules/ @Chief-Engineer
/Resources/engineCommandPerms.yml @moonheart08 @Chief-Engineer
/Resources/clientCommandPerms.yml @moonheart08 @Chief-Engineer
@@ -23,6 +24,7 @@
/Resources/Prototypes/Body/ @DrSmugleaf # suffering
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
/Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf
/Resources/Prototypes/Guidebook/rules.yml @Chief-Engineer
/Content.*/Body/ @DrSmugleaf
/Content.YAMLLinter @DrSmugleaf
/Content.Shared/Damage/ @DrSmugleaf

View File

@@ -1,19 +1,17 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Mind;
using Content.Server.Warps;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
@@ -58,15 +56,20 @@ public class PvsBenchmark
_pair.Server.CfgMan.SetCVar(CVars.NetPvsAsync, false);
_sys = _entMan.System<SharedTransformSystem>();
SetupAsync().Wait();
}
private async Task SetupAsync()
{
// Spawn the map
_pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
_pair.Server.WaitPost(() =>
await _pair.Server.WaitPost(() =>
{
var success = _entMan.System<MapLoaderSystem>().TryLoad(_mapId, Map, out _);
if (!success)
throw new Exception("Map load failed");
_pair.Server.MapMan.DoMapInitialize(_mapId);
}).Wait();
});
// Get list of ghost warp positions
_spawns = _entMan.AllComponentsList<WarpPointComponent>()
@@ -76,17 +79,19 @@ public class PvsBenchmark
Array.Resize(ref _players, PlayerCount);
// Spawn "Players".
_pair.Server.WaitPost(() =>
// Spawn "Players"
_players = await _pair.Server.AddDummySessions(PlayerCount);
await _pair.Server.WaitPost(() =>
{
var mind = _pair.Server.System<MindSystem>();
for (var i = 0; i < PlayerCount; i++)
{
var pos = _spawns[i % _spawns.Length];
var uid =_entMan.SpawnEntity("MobHuman", pos);
_pair.Server.ConsoleHost.ExecuteCommand($"setoutfit {_entMan.GetNetEntity(uid)} CaptainGear");
_players[i] = new DummySession{AttachedEntity = uid};
mind.ControlMob(_players[i].UserId, uid);
}
}).Wait();
});
// Repeatedly move players around so that they "explore" the map and see lots of entities.
// This will populate their PVS data with out-of-view entities.
@@ -168,20 +173,4 @@ public class PvsBenchmark
}).Wait();
_pair.Server.PvsTick(_players);
}
private sealed class DummySession : ICommonSession
{
public SessionStatus Status => SessionStatus.InGame;
public EntityUid? AttachedEntity {get; set; }
public NetUserId UserId => default;
public string Name => string.Empty;
public short Ping => default;
public INetChannel Channel { get; set; } = default!;
public LoginType AuthType => default;
public HashSet<EntityUid> ViewSubscriptions { get; } = new();
public DateTime ConnectedTime { get; set; }
public SessionState State => default!;
public SessionData Data => default!;
public bool ClientSide { get; set; }
}
}

View File

@@ -6,7 +6,8 @@
xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs"
xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab"
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab">
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab"
xmlns:baby="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab">
<TabContainer Name="MasterTabContainer">
<adminTab:AdminTab />
<adminbusTab:AdminbusTab />
@@ -14,6 +15,7 @@
<tabs:RoundTab />
<tabs:ServerTab />
<panic:PanicBunkerTab Name="PanicBunkerControl" Access="Public" />
<baby:BabyJailTab Name="BabyJailControl" Access="Public" />
<playerTab:PlayerTab Name="PlayerTabControl" Access="Public" />
<objectsTab:ObjectsTab Name="ObjectsTabControl" Access="Public" />
</TabContainer>

View File

@@ -21,8 +21,12 @@ namespace Content.Client.Administration.UI
MasterTabContainer.SetTabTitle(3, Loc.GetString("admin-menu-round-tab"));
MasterTabContainer.SetTabTitle(4, Loc.GetString("admin-menu-server-tab"));
MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-panic-bunker-tab"));
MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-players-tab"));
MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-objects-tab"));
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-baby-jail-tab"));
MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-players-tab"));
MasterTabContainer.SetTabTitle(8, Loc.GetString("admin-menu-objects-tab"));
}
protected override void Dispose(bool disposing)

View File

@@ -0,0 +1,6 @@
<controls:BabyJailStatusWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
Title="{Loc admin-ui-baby-jail-window-title}">
<RichTextLabel Name="MessageLabel" Access="Public" />
</controls:BabyJailStatusWindow>

View File

@@ -0,0 +1,21 @@
using Content.Client.Message;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
/*
* TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
[GenerateTypedNameReferences]
public sealed partial class BabyJailStatusWindow : FancyWindow
{
public BabyJailStatusWindow()
{
RobustXamlLoader.Load(this);
MessageLabel.SetMarkup(Loc.GetString("admin-ui-baby-jail-is-enabled"));
}
}

View File

@@ -0,0 +1,26 @@
<controls:BabyJailTab
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="4">
<BoxContainer Orientation="Vertical">
<cc:CommandButton Name="EnabledButton" Command="babyjail" ToggleMode="True"
Text="{Loc admin-ui-baby-jail-disabled}"
ToolTip="{Loc admin-ui-baby-jail-tooltip}" />
<cc:CommandButton Name="ShowReasonButton" Command="babyjail_show_reason"
ToggleMode="True" Text="{Loc admin-ui-baby-jail-show-reason}"
ToolTip="{Loc admin-ui-baby-jail-show-reason-tooltip}" />
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-baby-jail-max-account-age}" MinWidth="175" />
<LineEdit Name="MaxAccountAge" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-minutes}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-baby-jail-max-overall-minutes}" MinWidth="175" />
<LineEdit Name="MaxOverallMinutes" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-minutes}" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:BabyJailTab>

View File

@@ -0,0 +1,75 @@
using Content.Shared.Administration.Events;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
/*
* TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
[GenerateTypedNameReferences]
public sealed partial class BabyJailTab : Control
{
[Dependency] private readonly IConsoleHost _console = default!;
private string _maxAccountAge;
private string _maxOverallMinutes;
public BabyJailTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
MaxAccountAge.OnTextEntered += args => SendMaxAccountAge(args.Text);
MaxAccountAge.OnFocusExit += args => SendMaxAccountAge(args.Text);
_maxAccountAge = MaxAccountAge.Text;
MaxOverallMinutes.OnTextEntered += args => SendMaxOverallMinutes(args.Text);
MaxOverallMinutes.OnFocusExit += args => SendMaxOverallMinutes(args.Text);
_maxOverallMinutes = MaxOverallMinutes.Text;
}
private void SendMaxAccountAge(string text)
{
if (string.IsNullOrWhiteSpace(text) ||
text == _maxAccountAge ||
!int.TryParse(text, out var minutes))
{
return;
}
_console.ExecuteCommand($"babyjail_max_account_age {minutes}");
}
private void SendMaxOverallMinutes(string text)
{
if (string.IsNullOrWhiteSpace(text) ||
text == _maxOverallMinutes ||
!int.TryParse(text, out var minutes))
{
return;
}
_console.ExecuteCommand($"babyjail_max_overall_minutes {minutes}");
}
public void UpdateStatus(BabyJailStatus status)
{
EnabledButton.Pressed = status.Enabled;
EnabledButton.Text = Loc.GetString(status.Enabled
? "admin-ui-baby-jail-enabled"
: "admin-ui-baby-jail-disabled"
);
EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null;
ShowReasonButton.Pressed = status.ShowReason;
MaxAccountAge.Text = status.MaxAccountAgeMinutes.ToString();
_maxAccountAge = MaxAccountAge.Text;
MaxOverallMinutes.Text = status.MaxOverallMinutes.ToString();
_maxOverallMinutes = MaxOverallMinutes.Text;
}
}

View File

@@ -1,15 +1,21 @@
<Control xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ot="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab"
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label HorizontalExpand="True" SizeFlagsStretchRatio="0.50"
Text="{Loc Object type:}" />
<LineEdit Name="SearchLineEdit" PlaceHolder="{Loc Search...}" HorizontalExpand="True" SizeFlagsStretchRatio="1"/>
<OptionButton Name="ObjectTypeOptions" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
</BoxContainer>
<cc:HSeparator/>
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" Name="ObjectList">
</BoxContainer>
</ScrollContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<ot:ObjectsTabHeader Name="ListHeader"/>
<cc:HSeparator/>
<co:SearchListContainer Name="SearchList" Access="Public" VerticalExpand="True"/>
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -1,5 +1,7 @@
using Content.Client.Station;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map.Components;
@@ -10,20 +12,20 @@ namespace Content.Client.Administration.UI.Tabs.ObjectsTab;
[GenerateTypedNameReferences]
public sealed partial class ObjectsTab : Control
{
[Dependency] private readonly EntityManager _entityManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly List<ObjectsTabEntry> _objects = new();
private List<ObjectsTabSelection> _selections = new();
private readonly List<ObjectsTabSelection> _selections = new();
private bool _ascending = false; // Set to false for descending order by default
private ObjectsTabHeader.Header _headerClicked = ObjectsTabHeader.Header.ObjectName;
private readonly Color _altColor = Color.FromHex("#292B38");
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
public event Action<ObjectsTabEntry, GUIBoundKeyEventArgs>? OnEntryKeyBindDown;
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
// Listen I could either have like 4 different event subscribers (for map / grid / station changes) and manage their lifetimes in AdminUIController
// OR
// I can do this.
private TimeSpan _updateFrequency = TimeSpan.FromSeconds(2);
private TimeSpan _nextUpdate = TimeSpan.FromSeconds(2);
private readonly TimeSpan _updateFrequency = TimeSpan.FromSeconds(2);
private TimeSpan _nextUpdate;
public ObjectsTab()
{
@@ -42,6 +44,30 @@ public sealed partial class ObjectsTab : Control
ObjectTypeOptions.AddItem(Enum.GetName((ObjectsTabSelection)type)!);
}
ListHeader.OnHeaderClicked += HeaderClicked;
SearchList.SearchBar = SearchLineEdit;
SearchList.GenerateItem += GenerateButton;
SearchList.DataFilterCondition += DataFilterCondition;
RefreshObjectList();
// Set initial selection and refresh the list to apply the initial sort order
var defaultSelection = ObjectsTabSelection.Grids;
ObjectTypeOptions.SelectId((int)defaultSelection); // Set the default selection
RefreshObjectList(defaultSelection); // Refresh the list with the default selection
// Initialize the next update time
_nextUpdate = TimeSpan.Zero;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_timing.CurTime < _nextUpdate)
return;
_nextUpdate = _timing.CurTime + _updateFrequency;
RefreshObjectList();
}
@@ -81,32 +107,70 @@ public sealed partial class ObjectsTab : Control
throw new ArgumentOutOfRangeException(nameof(selection), selection, null);
}
foreach (var control in _objects)
entities.Sort((a, b) =>
{
ObjectList.RemoveChild(control);
var valueA = GetComparableValue(a, _headerClicked);
var valueB = GetComparableValue(b, _headerClicked);
return _ascending ? Comparer<object>.Default.Compare(valueA, valueB) : Comparer<object>.Default.Compare(valueB, valueA);
});
var listData = new List<ObjectsListData>();
for (int index = 0; index < entities.Count; index++)
{
var info = entities[index];
listData.Add(new ObjectsListData(info, $"{info.Name} {info.Entity}", index % 2 == 0 ? _altColor : _defaultColor));
}
_objects.Clear();
foreach (var (name, nent) in entities)
{
var ctrl = new ObjectsTabEntry(name, nent);
_objects.Add(ctrl);
ObjectList.AddChild(ctrl);
ctrl.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(ctrl, args);
}
SearchList.PopulateList(listData);
}
protected override void FrameUpdate(FrameEventArgs args)
private void GenerateButton(ListData data, ListContainerButton button)
{
base.FrameUpdate(args);
if (_timing.CurTime < _nextUpdate)
if (data is not ObjectsListData { Info: var info, BackgroundColor: var backgroundColor })
return;
// I do not care for precision.
_nextUpdate = _timing.CurTime + _updateFrequency;
var entry = new ObjectsTabEntry(info.Name, info.Entity, new StyleBoxFlat { BackgroundColor = backgroundColor });
button.ToolTip = $"{info.Name}, {info.Entity}";
button.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(args, data);
button.AddChild(entry);
}
private bool DataFilterCondition(string filter, ListData listData)
{
if (listData is not ObjectsListData { FilteringString: var filteringString })
return false;
// If the filter is empty, do not filter out any entries
if (string.IsNullOrEmpty(filter))
return true;
return filteringString.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
}
private object GetComparableValue((string Name, NetEntity Entity) entity, ObjectsTabHeader.Header header)
{
return header switch
{
ObjectsTabHeader.Header.ObjectName => entity.Name,
ObjectsTabHeader.Header.EntityID => entity.Entity.ToString(),
_ => entity.Name
};
}
private void HeaderClicked(ObjectsTabHeader.Header header)
{
if (_headerClicked == header)
{
_ascending = !_ascending;
}
else
{
_headerClicked = header;
_ascending = true;
}
ListHeader.UpdateHeaderSymbols(_headerClicked, _ascending);
RefreshObjectList();
}
@@ -118,3 +182,4 @@ public sealed partial class ObjectsTab : Control
}
}
public record ObjectsListData((string Name, NetEntity Entity) Info, string FilteringString, Color BackgroundColor) : ListData;

View File

@@ -1,6 +1,6 @@
<ContainerButton xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls">
<PanelContainer Name="BackgroundColorPanel"/>
<PanelContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Name="BackgroundColorPanel">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
SeparationOverride="4">
@@ -14,4 +14,4 @@
HorizontalExpand="True"
ClipText="True"/>
</BoxContainer>
</ContainerButton>
</PanelContainer>

View File

@@ -1,19 +1,21 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Tabs.ObjectsTab;
[GenerateTypedNameReferences]
public sealed partial class ObjectsTabEntry : ContainerButton
public sealed partial class ObjectsTabEntry : PanelContainer
{
public NetEntity AssocEntity;
public ObjectsTabEntry(string name, NetEntity nent)
public ObjectsTabEntry(string name, NetEntity nent, StyleBox styleBox)
{
RobustXamlLoader.Load(this);
AssocEntity = nent;
EIDLabel.Text = nent.ToString();
NameLabel.Text = name;
BackgroundColorPanel.PanelOverride = styleBox;
}
}

View File

@@ -0,0 +1,21 @@
<Control xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
<PanelContainer Name="BackgroundColorPanel" Access="Public"/>
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
SeparationOverride="4">
<Label Name="ObjectNameLabel"
SizeFlagsStretchRatio="3"
HorizontalExpand="True"
ClipText="True"
Text="{Loc object-tab-object-name}"
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="EntityIDLabel"
SizeFlagsStretchRatio="3"
HorizontalExpand="True"
ClipText="True"
Text="{Loc object-tab-entity-id}"
MouseFilter="Pass"/>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,86 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
namespace Content.Client.Administration.UI.Tabs.ObjectsTab
{
[GenerateTypedNameReferences]
public sealed partial class ObjectsTabHeader : Control
{
public event Action<Header>? OnHeaderClicked;
private const string ArrowUp = "↑";
private const string ArrowDown = "↓";
public ObjectsTabHeader()
{
RobustXamlLoader.Load(this);
ObjectNameLabel.OnKeyBindDown += ObjectNameClicked;
EntityIDLabel.OnKeyBindDown += EntityIDClicked;
}
public Label GetHeader(Header header)
{
return header switch
{
Header.ObjectName => ObjectNameLabel,
Header.EntityID => EntityIDLabel,
_ => throw new ArgumentOutOfRangeException(nameof(header), header, null)
};
}
public void ResetHeaderText()
{
ObjectNameLabel.Text = Loc.GetString("object-tab-object-name");
EntityIDLabel.Text = Loc.GetString("object-tab-entity-id");
}
public void UpdateHeaderSymbols(Header headerClicked, bool ascending)
{
ResetHeaderText();
var arrow = ascending ? ArrowUp : ArrowDown;
GetHeader(headerClicked).Text += $" {arrow}";
}
private void HeaderClicked(GUIBoundKeyEventArgs args, Header header)
{
if (args.Function != EngineKeyFunctions.UIClick)
{
return;
}
OnHeaderClicked?.Invoke(header);
args.Handle();
}
private void ObjectNameClicked(GUIBoundKeyEventArgs args)
{
HeaderClicked(args, Header.ObjectName);
}
private void EntityIDClicked(GUIBoundKeyEventArgs args)
{
HeaderClicked(args, Header.EntityID);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
ObjectNameLabel.OnKeyBindDown -= ObjectNameClicked;
EntityIDLabel.OnKeyBindDown -= EntityIDClicked;
}
}
public enum Header
{
ObjectName,
EntityID
}
}
}

View File

@@ -31,12 +31,12 @@
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-panic-bunker-min-account-age}" MinWidth="175" />
<LineEdit Name="MinAccountAge" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-hours}" />
<Label Text="{Loc generic-minutes}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-panic-bunker-min-overall-hours}" MinWidth="175" />
<LineEdit Name="MinOverallHours" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-hours}" />
<Label Text="{Loc admin-ui-panic-bunker-min-overall-minutes}" MinWidth="175" />
<LineEdit Name="MinOverallMinutes" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-minutes}" />
</BoxContainer>
</BoxContainer>
</BoxContainer>

View File

@@ -12,7 +12,7 @@ public sealed partial class PanicBunkerTab : Control
[Dependency] private readonly IConsoleHost _console = default!;
private string _minAccountAge;
private string _minOverallHours;
private string _minOverallMinutes;
public PanicBunkerTab()
{
@@ -25,9 +25,9 @@ public sealed partial class PanicBunkerTab : Control
MinAccountAge.OnFocusExit += args => SendMinAccountAge(args.Text);
_minAccountAge = MinAccountAge.Text;
MinOverallHours.OnTextEntered += args => SendMinOverallHours(args.Text);
MinOverallHours.OnFocusExit += args => SendMinOverallHours(args.Text);
_minOverallHours = MinOverallHours.Text;
MinOverallMinutes.OnTextEntered += args => SendMinOverallMinutes(args.Text);
MinOverallMinutes.OnFocusExit += args => SendMinOverallMinutes(args.Text);
_minOverallMinutes = MinOverallMinutes.Text;
}
private void SendMinAccountAge(string text)
@@ -42,16 +42,16 @@ public sealed partial class PanicBunkerTab : Control
_console.ExecuteCommand($"panicbunker_min_account_age {minutes}");
}
private void SendMinOverallHours(string text)
private void SendMinOverallMinutes(string text)
{
if (string.IsNullOrWhiteSpace(text) ||
text == _minOverallHours ||
!int.TryParse(text, out var hours))
text == _minOverallMinutes ||
!int.TryParse(text, out var minutes))
{
return;
}
_console.ExecuteCommand($"panicbunker_min_overall_hours {hours}");
_console.ExecuteCommand($"panicbunker_min_overall_minutes {minutes}");
}
public void UpdateStatus(PanicBunkerStatus status)
@@ -68,10 +68,10 @@ public sealed partial class PanicBunkerTab : Control
CountDeadminnedButton.Pressed = status.CountDeadminnedAdmins;
ShowReasonButton.Pressed = status.ShowReason;
MinAccountAge.Text = status.MinAccountAgeHours.ToString();
MinAccountAge.Text = status.MinAccountAgeMinutes.ToString();
_minAccountAge = MinAccountAge.Text;
MinOverallHours.Text = status.MinOverallHours.ToString();
_minOverallHours = MinOverallHours.Text;
MinOverallMinutes.Text = status.MinOverallMinutes.ToString();
_minOverallMinutes = MinOverallMinutes.Text;
}
}

View File

@@ -53,6 +53,7 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
RefreshPlayerList(_adminSystem.PlayerList);
}
#region Antag Overlay
@@ -110,7 +111,9 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
_players = players;
PlayerCount.Text = $"Players: {_playerMan.PlayerCount}";
var sortedPlayers = new List<PlayerInfo>(players);
var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList();
var sortedPlayers = new List<PlayerInfo>(filteredPlayers);
sortedPlayers.Sort(Compare);
UpdateHeaderSymbols();

View File

@@ -27,6 +27,7 @@ namespace Content.Client.Construction
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -195,9 +196,8 @@ namespace Content.Client.Construction
if (GhostPresent(loc))
return false;
// This InRangeUnobstructed should probably be replaced with "is there something blocking us in that tile?"
var predicate = GetPredicate(prototype.CanBuildInImpassable, loc.ToMap(EntityManager, _transformSystem));
if (!_interactionSystem.InRangeUnobstructed(user, loc, 20f, predicate: predicate))
if (!_examineSystem.InRangeUnOccluded(user, loc, 20f, predicate: predicate))
return false;
if (!CheckConstructionConditions(prototype, loc, dir, user, showPopup: true))

View File

@@ -8,7 +8,7 @@
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True" Margin="10">
<BoxContainer SizeFlagsStretchRatio="2" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical">
<SpriteView Name="MachineSprite" Scale="4 4" HorizontalAlignment="Center" VerticalExpand="True" MinSize="128 128"/>
<EntityPrototypeView Name="MachineSprite" Scale="4 4" HorizontalAlignment="Center" VerticalExpand="True" MinSize="128 128"/>
<RichTextLabel Name="MachineNameLabel" HorizontalAlignment="Center" StyleClasses="LabelKeyText"/>
</BoxContainer>
<Control MinHeight="10"/>

View File

@@ -23,7 +23,6 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
private readonly ItemSlotsSystem _itemSlots;
private readonly FlatpackSystem _flatpack;
private readonly MaterialStorageSystem _materialStorage;
private readonly SpriteSystem _spriteSystem;
private readonly EntityUid _owner;
@@ -31,7 +30,6 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
public const string NoBoardEffectId = "FlatpackerNoBoardEffect";
private EntityUid? _currentBoard = EntityUid.Invalid;
private EntityUid? _machinePreview;
public event Action? PackButtonPressed;
@@ -43,7 +41,6 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
_itemSlots = _entityManager.System<ItemSlotsSystem>();
_flatpack = _entityManager.System<FlatpackSystem>();
_materialStorage = _entityManager.System<MaterialStorageSystem>();
_spriteSystem = _entityManager.System<SpriteSystem>();
_owner = uid;
@@ -57,17 +54,10 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
{
base.FrameUpdate(args);
if (_machinePreview is not { } && _entityManager.Deleted(_machinePreview))
{
_machinePreview = null;
MachineSprite.SetEntity(_machinePreview);
}
if (!_entityManager.TryGetComponent<FlatpackCreatorComponent>(_owner, out var flatpacker) ||
!_itemSlots.TryGetSlot(_owner, flatpacker.SlotId, out var itemSlot))
return;
MachineBoardComponent? machineBoardComp = null;
if (flatpacker.Packing)
{
PackButton.Disabled = true;
@@ -75,11 +65,10 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
else if (_currentBoard != null)
{
Dictionary<string, int> cost;
if (_entityManager.TryGetComponent(_currentBoard, out machineBoardComp) &&
machineBoardComp.Prototype is not null)
if (_entityManager.TryGetComponent<MachineBoardComponent>(_currentBoard, out var machineBoardComp))
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp));
else
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker));
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
PackButton.Disabled = !_materialStorage.CanChangeMaterialAmount(_owner, cost);
}
@@ -87,9 +76,6 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
if (_currentBoard == itemSlot.Item)
return;
if (_machinePreview != null)
_entityManager.DeleteEntity(_machinePreview);
_currentBoard = itemSlot.Item;
CostHeaderLabel.Visible = _currentBoard != null;
InsertLabel.Visible = _currentBoard == null;
@@ -99,35 +85,32 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
string? prototype = null;
Dictionary<string, int>? cost = null;
if (machineBoardComp != null || _entityManager.TryGetComponent(_currentBoard, out machineBoardComp))
if (_entityManager.TryGetComponent<MachineBoardComponent>(_currentBoard, out var newMachineBoardComp))
{
prototype = machineBoardComp.Prototype;
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp));
prototype = newMachineBoardComp.Prototype;
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, newMachineBoardComp));
}
else if (_entityManager.TryGetComponent<ComputerBoardComponent>(_currentBoard, out var computerBoard))
{
prototype = computerBoard.Prototype;
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker));
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
}
if (prototype is not null && cost is not null)
{
var proto = _prototypeManager.Index<EntityPrototype>(prototype);
_machinePreview = _entityManager.Spawn(proto.ID);
_spriteSystem.ForceUpdate(_machinePreview.Value);
MachineSprite.SetPrototype(prototype);
MachineNameLabel.SetMessage(proto.Name);
CostLabel.SetMarkup(GetCostString(cost));
}
}
else
{
_machinePreview = _entityManager.Spawn(NoBoardEffectId);
MachineSprite.SetPrototype(NoBoardEffectId);
CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
MachineNameLabel.SetMessage(" ");
PackButton.Disabled = true;
}
MachineSprite.SetEntity(_machinePreview);
}
private string GetCostString(Dictionary<string, int> costs)
@@ -149,7 +132,7 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
("amount", amountText),
("material", Loc.GetString(matProto.Name)));
msg.AddMarkup(text);
msg.TryAddMarkup(text, out _);
if (i != orderedCosts.Length - 1)
msg.PushNewline();
@@ -157,12 +140,4 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
return msg.ToMarkup();
}
public override void Close()
{
base.Close();
_entityManager.DeleteEntity(_machinePreview);
_machinePreview = null;
}
}

View File

@@ -2,11 +2,9 @@ using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
using Content.Client.Eui;
using Content.Client.Flash;
using Content.Client.Fullscreen;
using Content.Client.GhostKick;
using Content.Client.Guidebook;
using Content.Client.Info;
using Content.Client.Input;
using Content.Client.IoC;
using Content.Client.Launcher;
@@ -53,7 +51,6 @@ namespace Content.Client.Entry
[Dependency] private readonly IScreenshotHook _screenshotHook = default!;
[Dependency] private readonly FullscreenHook _fullscreenHook = default!;
[Dependency] private readonly ChangelogManager _changelogManager = default!;
[Dependency] private readonly RulesManager _rulesManager = default!;
[Dependency] private readonly ViewportManager _viewportManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
@@ -126,7 +123,6 @@ namespace Content.Client.Entry
_screenshotHook.Initialize();
_fullscreenHook.Initialize();
_changelogManager.Initialize();
_rulesManager.Initialize();
_viewportManager.Initialize();
_ghostKick.Initialize();
_extendedDisconnectInformation.Initialize();

View File

@@ -4,10 +4,12 @@ using Content.Client.Lobby;
using Content.Client.RoundEnd;
using Content.Shared.GameTicking;
using Content.Shared.GameWindow;
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.GameTicking.Managers
{
@@ -17,10 +19,9 @@ namespace Content.Client.GameTicking.Managers
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
private Dictionary<NetEntity, Dictionary<string, uint?>> _jobsAvailable = new();
private Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> _jobsAvailable = new();
private Dictionary<NetEntity, string> _stationNames = new();
[ViewVariables] public bool AreWeReady { get; private set; }
@@ -32,13 +33,13 @@ namespace Content.Client.GameTicking.Managers
[ViewVariables] public TimeSpan StartTime { get; private set; }
[ViewVariables] public new bool Paused { get; private set; }
[ViewVariables] public IReadOnlyDictionary<NetEntity, Dictionary<string, uint?>> JobsAvailable => _jobsAvailable;
[ViewVariables] public IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> JobsAvailable => _jobsAvailable;
[ViewVariables] public IReadOnlyDictionary<NetEntity, string> StationNames => _stationNames;
public event Action? InfoBlobUpdated;
public event Action? LobbyStatusUpdated;
public event Action? LobbyLateJoinStatusUpdated;
public event Action<IReadOnlyDictionary<NetEntity, Dictionary<string, uint?>>>? LobbyJobsAvailableUpdated;
public event Action<IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>>>? LobbyJobsAvailableUpdated;
public override void Initialize()
{
@@ -69,7 +70,7 @@ namespace Content.Client.GameTicking.Managers
// reading the console. E.g., logs like this one could leak the nuke station/grid:
// > Grid NT-Arrivals 1101 (122/n25896) changed parent. Old parent: map 10 (121/n25895). New parent: FTL (123/n26470)
#if !DEBUG
_map.Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning;
EntityManager.System<SharedMapSystem>().Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning;
#endif
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Content.Shared.Guidebook;
using Robust.Shared.Prototypes;
namespace Content.Client.Guidebook.Components;
@@ -13,9 +14,8 @@ public sealed partial class GuideHelpComponent : Component
/// What guides to include show when opening the guidebook. The first entry will be used to select the currently
/// selected guidebook.
/// </summary>
[DataField("guides", customTypeSerializer: typeof(PrototypeIdListSerializer<GuideEntryPrototype>), required: true)]
[ViewVariables]
public List<string> Guides = new();
[DataField(required: true)]
public List<ProtoId<GuideEntryPrototype>> Guides = new();
/// <summary>
/// Whether or not to automatically include the children of the given guides.

View File

@@ -2,7 +2,7 @@
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:fancyTree="clr-namespace:Content.Client.UserInterface.Controls.FancyTree"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="750 700"
SetSize="900 700"
MinSize="100 200"
Resizable="True"
Title="{Loc 'guidebook-window-title'}">
@@ -18,12 +18,16 @@
Name="SearchBar"
PlaceHolder="{Loc 'guidebook-filter-placeholder-text'}"
HorizontalExpand="True"
Margin="0 5 10 5">
Margin="0 5 10 5">
</LineEdit>
</BoxContainer>
<BoxContainer Access="Internal" Name="ReturnContainer" Orientation="Horizontal" HorizontalAlignment="Right" Visible="False">
<Button Name="HomeButton" Text="{Loc 'ui-rules-button-home'}" Margin="0 0 10 0"/>
</BoxContainer>
<ScrollContainer Name="Scroll" HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
<Control>
<BoxContainer Orientation="Vertical" Name="EntryContainer" Margin="5 5 5 5" Visible="False"/>
<BoxContainer Orientation="Vertical" Name="EntryContainer" Margin="5 5 5 5" Visible="False">
</BoxContainer>
<BoxContainer Orientation="Vertical" Name="Placeholder" Margin="5 5 5 5">
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text'}"/>
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text-2'}"/>

View File

@@ -1,15 +1,17 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client.Guidebook.RichText;
using Content.Client.UserInterface.ControlExtensions;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Controls.FancyTree;
using JetBrains.Annotations;
using Content.Client.UserInterface.Systems.Info;
using Content.Shared.CCVar;
using Content.Shared.Guidebook;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
namespace Content.Client.Guidebook.Controls;
@@ -19,7 +21,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
private Dictionary<string, GuideEntry> _entries = new();
private Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> _entries = new();
public GuidebookWindow()
{
@@ -37,7 +39,13 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
private void OnSelectionChanged(TreeItem? item)
{
if (item != null && item.Metadata is GuideEntry entry)
{
ShowGuide(entry);
var isRulesEntry = entry.RuleEntry;
ReturnContainer.Visible = isRulesEntry;
HomeButton.OnPressed += _ => ShowGuide(entry);
}
else
ClearSelectedGuide();
}
@@ -69,10 +77,10 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
}
public void UpdateGuides(
Dictionary<string, GuideEntry> entries,
List<string>? rootEntries = null,
string? forceRoot = null,
string? selected = null)
Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> entries,
List<ProtoId<GuideEntryPrototype>>? rootEntries = null,
ProtoId<GuideEntryPrototype>? forceRoot = null,
ProtoId<GuideEntryPrototype>? selected = null)
{
_entries = entries;
RepopulateTree(rootEntries, forceRoot);
@@ -98,11 +106,11 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
}
}
private IEnumerable<GuideEntry> GetSortedEntries(List<string>? rootEntries)
private IEnumerable<GuideEntry> GetSortedEntries(List<ProtoId<GuideEntryPrototype>>? rootEntries)
{
if (rootEntries == null)
{
HashSet<string> entries = new(_entries.Keys);
HashSet<ProtoId<GuideEntryPrototype>> entries = new(_entries.Keys);
foreach (var entry in _entries.Values)
{
if (entry.Children.Count > 0)
@@ -111,7 +119,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
.Select(childId => _entries[childId])
.OrderBy(childEntry => childEntry.Priority)
.ThenBy(childEntry => Loc.GetString(childEntry.Name))
.Select(childEntry => childEntry.Id)
.Select(childEntry => new ProtoId<GuideEntryPrototype>(childEntry.Id))
.ToList();
entry.Children = sortedChildren;
@@ -127,13 +135,13 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
.ThenBy(rootEntry => Loc.GetString(rootEntry.Name));
}
private void RepopulateTree(List<string>? roots = null, string? forcedRoot = null)
private void RepopulateTree(List<ProtoId<GuideEntryPrototype>>? roots = null, ProtoId<GuideEntryPrototype>? forcedRoot = null)
{
Tree.Clear();
HashSet<string> addedEntries = new();
HashSet<ProtoId<GuideEntryPrototype>> addedEntries = new();
TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot, null, addedEntries);
TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries);
foreach (var entry in GetSortedEntries(roots))
{
if (!entry.CrystallPunkAllowed) continue; //CrystallPunk guidebook filter
@@ -142,17 +150,23 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
Tree.SetAllExpanded(true);
}
private TreeItem? AddEntry(string id, TreeItem? parent, HashSet<string> addedEntries)
private TreeItem? AddEntry(ProtoId<GuideEntryPrototype> id, TreeItem? parent, HashSet<ProtoId<GuideEntryPrototype>> addedEntries)
{
if (!_entries.TryGetValue(id, out var entry))
return null;
if (!addedEntries.Add(id))
{
// TODO GUIDEBOOK Maybe allow duplicate entries?
// E.g., for adding medicine under both chemicals & the chemist job
Logger.Error($"Adding duplicate guide entry: {id}");
return null;
}
var rulesProto = UserInterfaceManager.GetUIController<InfoUIController>().GetCoreRuleEntry();
if (entry.RuleEntry && entry.Id != rulesProto.Id)
return null;
var item = Tree.AddItem(parent);
item.Metadata = entry;
var name = Loc.GetString(entry.Name);

View File

@@ -1,7 +1,10 @@
using System.Linq;
using Content.Client.Guidebook.Richtext;
using Content.Shared.Guidebook;
using Pidgin;
using Robust.Client.UserInterface;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Sandboxing;
using static Pidgin.Parser;
@@ -13,7 +16,9 @@ namespace Content.Client.Guidebook;
/// </summary>
public sealed partial class DocumentParsingManager
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
private readonly Dictionary<string, Parser<char, Control>> _tagControlParsers = new();
@@ -37,6 +42,21 @@ public sealed partial class DocumentParsingManager
ControlParser = SkipWhitespaces.Then(_controlParser.Many());
}
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
{
if (!_prototype.TryIndex(entryId, out var entry))
return false;
using var file = _resourceManager.ContentFileReadText(entry.Text);
return TryAddMarkup(control, file.ReadToEnd(), log);
}
public bool TryAddMarkup(Control control, GuideEntry entry, bool log = true)
{
using var file = _resourceManager.ContentFileReadText(entry.Text);
return TryAddMarkup(control, file.ReadToEnd(), log);
}
public bool TryAddMarkup(Control control, string text, bool log = true)
{
try

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Client.Guidebook.Components;
using Content.Client.Light;
using Content.Client.Verbs;
using Content.Shared.Guidebook;
using Content.Shared.Interaction;
using Content.Shared.Light.Components;
using Content.Shared.Speech;
@@ -31,10 +32,14 @@ public sealed class GuidebookSystem : EntitySystem
[Dependency] private readonly RgbLightControllerSystem _rgbLightControllerSystem = default!;
[Dependency] private readonly SharedPointLightSystem _pointLightSystem = default!;
[Dependency] private readonly TagSystem _tags = default!;
[Dependency] private readonly IPrototypeManager _proto = default!; //CrystallPunk guidebook filter
public event Action<List<string>, List<string>?, string?, bool, string?>? OnGuidebookOpen;
public event Action<List<ProtoId<GuideEntryPrototype>>,
List<ProtoId<GuideEntryPrototype>>?,
ProtoId<GuideEntryPrototype>?,
bool,
ProtoId<GuideEntryPrototype>?>? OnGuidebookOpen;
public const string GuideEmbedTag = "GuideEmbeded";
private EntityUid _defaultUser;
@@ -99,7 +104,7 @@ public sealed class GuidebookSystem : EntitySystem
});
}
public void OpenHelp(List<string> guides)
public void OpenHelp(List<ProtoId<GuideEntryPrototype>> guides)
{
OnGuidebookOpen?.Invoke(guides, null, null, true, guides[0]);
}

View File

@@ -1,25 +0,0 @@
using Content.Shared.Info;
using Robust.Shared.Log;
namespace Content.Client.Info;
public sealed class InfoSystem : EntitySystem
{
public RulesMessage Rules = new RulesMessage("Server Rules", "The server did not send any rules.");
[Dependency] private readonly RulesManager _rules = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<RulesMessage>(OnRulesReceived);
Log.Debug("Requested server info.");
RaiseNetworkEvent(new RequestRulesMessage());
}
private void OnRulesReceived(RulesMessage message, EntitySessionEventArgs eventArgs)
{
Log.Debug("Received server rules.");
Rules = message;
_rules.UpdateRules();
}
}

View File

@@ -1,10 +1,8 @@
using System.Numerics;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
namespace Content.Client.Info
@@ -12,7 +10,6 @@ namespace Content.Client.Info
public sealed class RulesAndInfoWindow : DefaultWindow
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly RulesManager _rules = default!;
public RulesAndInfoWindow()
{
@@ -22,8 +19,14 @@ namespace Content.Client.Info
var rootContainer = new TabContainer();
var rulesList = new Info();
var tutorialList = new Info();
var rulesList = new RulesControl
{
Margin = new Thickness(10)
};
var tutorialList = new Info
{
Margin = new Thickness(10)
};
rootContainer.AddChild(rulesList);
rootContainer.AddChild(tutorialList);
@@ -31,7 +34,6 @@ namespace Content.Client.Info
TabContainer.SetTabTitle(rulesList, Loc.GetString("ui-info-tab-rules"));
TabContainer.SetTabTitle(tutorialList, Loc.GetString("ui-info-tab-tutorial"));
AddSection(rulesList, _rules.RulesSection());
PopulateTutorial(tutorialList);
Contents.AddChild(rootContainer);

View File

@@ -1,6 +1,18 @@
<BoxContainer xmlns="https://spacestation14.io"
Name="InfoContainer"
Orientation="Vertical"
Margin="2 2 0 0"
SeparationOverride="10"
Access="Public"/>
<BoxContainer xmlns="https://spacestation14.io" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<Control HorizontalExpand="True" VerticalExpand="True" HorizontalAlignment="Stretch">
<ScrollContainer Name="Scroll" HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="RulesContainer" VerticalExpand="True" Margin="0 0 5 0"/>
</ScrollContainer>
<BoxContainer Margin="0 0 15 0" HorizontalExpand="True" HorizontalAlignment="Right">
<Button Name="BackButton"
Text="{Loc 'ui-rules-button-back'}"
HorizontalAlignment="Right"
VerticalAlignment="Top"/>
<Control MinWidth="5"/>
<Button Name="HomeButton"
Text="{Loc 'ui-rules-button-home'}"
HorizontalAlignment="Right"
VerticalAlignment="Top"/>
</BoxContainer>
</Control>
</BoxContainer>

View File

@@ -1,22 +1,54 @@
using System.IO;
using Content.Shared.CCVar;
using Content.Client.Guidebook;
using Content.Client.Guidebook.RichText;
using Content.Client.UserInterface.Systems.Info;
using Content.Shared.Guidebook;
using Robust.Client.AutoGenerated;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client.Info;
[GenerateTypedNameReferences]
public sealed partial class RulesControl : BoxContainer
public sealed partial class RulesControl : BoxContainer, ILinkClickHandler
{
[Dependency] private readonly RulesManager _rules = default!;
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
private string? _currentEntry;
private readonly Stack<string> _priorEntries = new();
public RulesControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
AddChild(_rules.RulesSection());
SetGuide();
HomeButton.OnPressed += _ => SetGuide();
BackButton.OnPressed += _ => SetGuide(_priorEntries.Pop(), false);
}
public void HandleClick(string link)
{
SetGuide(link);
}
private void SetGuide(ProtoId<GuideEntryPrototype>? entry = null, bool addToPrior = true)
{
var coreEntry = UserInterfaceManager.GetUIController<InfoUIController>().GetCoreRuleEntry();
entry ??= coreEntry;
Scroll.SetScrollValue(default);
RulesContainer.Children.Clear();
if (!_parsingMan.TryAddMarkup(RulesContainer, entry.Value))
return;
if (addToPrior && _currentEntry != null)
_priorEntries.Push(_currentEntry);
_currentEntry = entry.Value;
HomeButton.Visible = entry.Value != coreEntry.Id;
BackButton.Visible = _priorEntries.Count != 0 && _priorEntries.Peek() != entry.Value;
}
}

View File

@@ -1,105 +0,0 @@
using Content.Client.Lobby;
using Content.Client.Gameplay;
using Content.Shared.CCVar;
using Content.Shared.Info;
using Robust.Client.Console;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
namespace Content.Client.Info;
public sealed class RulesManager : SharedRulesManager
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
private InfoSection rulesSection = new InfoSection("", "", false);
private bool _shouldShowRules = false;
private RulesPopup? _activePopup;
public void Initialize()
{
_netManager.RegisterNetMessage<ShouldShowRulesPopupMessage>(OnShouldShowRules);
_netManager.RegisterNetMessage<ShowRulesPopupMessage>(OnShowRulesPopupMessage);
_netManager.RegisterNetMessage<RulesAcceptedMessage>();
_stateManager.OnStateChanged += OnStateChanged;
_consoleHost.RegisterCommand("fuckrules", "", "", (_, _, _) =>
{
OnAcceptPressed();
});
}
private void OnShouldShowRules(ShouldShowRulesPopupMessage message)
{
_shouldShowRules = true;
}
private void OnShowRulesPopupMessage(ShowRulesPopupMessage message)
{
ShowRules(message.PopupTime);
}
private void OnStateChanged(StateChangedEventArgs args)
{
if (args.NewState is not (GameplayState or LobbyState))
return;
if (!_shouldShowRules)
return;
_shouldShowRules = false;
ShowRules(_configManager.GetCVar(CCVars.RulesWaitTime));
}
private void ShowRules(float time)
{
if (_activePopup != null)
return;
_activePopup = new RulesPopup
{
Timer = time
};
_activePopup.OnQuitPressed += OnQuitPressed;
_activePopup.OnAcceptPressed += OnAcceptPressed;
_userInterfaceManager.WindowRoot.AddChild(_activePopup);
LayoutContainer.SetAnchorPreset(_activePopup, LayoutContainer.LayoutPreset.Wide);
}
private void OnQuitPressed()
{
_consoleHost.ExecuteCommand("quit");
}
private void OnAcceptPressed()
{
_netManager.ClientSendMessage(new RulesAcceptedMessage());
_activePopup?.Orphan();
_activePopup = null;
}
public void UpdateRules()
{
var rules = _sysMan.GetEntitySystem<InfoSystem>().Rules;
rulesSection.SetText(rules.Title, rules.Text, true);
}
public Control RulesSection()
{
rulesSection = new InfoSection("", "", false);
UpdateRules();
return rulesSection;
}
}

View File

@@ -5,20 +5,20 @@
MouseFilter="Stop">
<parallax:ParallaxControl />
<Control VerticalExpand="True"
MaxWidth="650">
MaxWidth="800"
MaxHeight="900">
<PanelContainer StyleClasses="windowPanel" />
<ScrollContainer HScrollEnabled="False">
<BoxContainer Orientation="Vertical" SeparationOverride="10">
<info:RulesControl />
<BoxContainer Orientation="Vertical" SeparationOverride="10" Margin="10 10 5 10">
<info:RulesControl/>
<Label Name="WaitLabel" />
<BoxContainer Orientation="Horizontal">
<Button Name="AcceptButton"
Text="{Loc 'ui-rules-accept'}"
Disabled="True" />
<Button Name="QuitButton"
StyleClasses="Caution"
Text="{Loc 'ui-escape-quit'}" />
</BoxContainer>
</BoxContainer>
</ScrollContainer>
</Control>
</Control>

View File

@@ -1,9 +1,7 @@
using System;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
using Robust.Shared.Timing;
namespace Content.Client.Info;

View File

@@ -4,7 +4,6 @@ using Content.Client.Chat.Managers;
using Content.Client.Clickable;
using Content.Client.Eui;
using Content.Client.GhostKick;
using Content.Client.Info;
using Content.Client.Launcher;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
@@ -41,7 +40,6 @@ namespace Content.Client.IoC
collection.Register<EuiManager, EuiManager>();
collection.Register<IVoteManager, VoteManager>();
collection.Register<ChangelogManager, ChangelogManager>();
collection.Register<RulesManager, RulesManager>();
collection.Register<ViewportManager, ViewportManager>();
collection.Register<ISharedAdminLogManager, SharedAdminLogManager>();
collection.Register<GhostKickManager>();

View File

@@ -290,7 +290,7 @@ namespace Content.Client.LateJoin
}
}
private void JobsAvailableUpdated(IReadOnlyDictionary<NetEntity, Dictionary<string, uint?>> updatedJobs)
private void JobsAvailableUpdated(IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> updatedJobs)
{
foreach (var stationEntries in updatedJobs)
{
@@ -337,10 +337,10 @@ namespace Content.Client.LateJoin
public Label JobLabel { get; }
public string JobId { get; }
public string JobLocalisedName { get; }
public uint? Amount { get; private set; }
public int? Amount { get; private set; }
private bool _initialised = false;
public JobButton(Label jobLabel, string jobId, string jobLocalisedName, uint? amount)
public JobButton(Label jobLabel, ProtoId<JobPrototype> jobId, string jobLocalisedName, int? amount)
{
JobLabel = jobLabel;
JobId = jobId;
@@ -350,7 +350,7 @@ namespace Content.Client.LateJoin
_initialised = true;
}
public void RefreshLabel(uint? amount)
public void RefreshLabel(int? amount)
{
if (Amount == amount && _initialised)
{

View File

@@ -207,8 +207,11 @@ namespace Content.Client.Light
public static Color GetCurrentRgbColor(TimeSpan curTime, TimeSpan offset, Entity<RgbLightControllerComponent> rgb)
{
var delta = (float)(curTime - offset).TotalSeconds;
var entOffset = Math.Abs(rgb.Owner.Id * 0.09817f);
var hue = (delta * rgb.Comp.CycleRate + entOffset) % 1;
return Color.FromHsv(new Vector4(
(float) (((curTime.TotalSeconds - offset.TotalSeconds) * rgb.Comp.CycleRate + Math.Abs(rgb.Owner.Id * 0.1)) % 1),
MathF.Abs(hue),
1.0f,
1.0f,
1.0f

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
using Content.Client.Inventory;
using Content.Client.Lobby.UI;
@@ -41,6 +42,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
[UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
[UISystemDependency] private readonly GuidebookSystem _guide = default!;
private CharacterSetupGui? _characterSetup;
private HumanoidProfileEditor? _profileEditor;
@@ -232,6 +234,8 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
_requirements,
_markings);
_profileEditor.OnOpenGuidebook += _guide.OpenHelp;
_characterSetup = new CharacterSetupGui(EntityManager, _prototypeManager, _resourceCache, _preferencesManager, _profileEditor);
_characterSetup.CloseButton.OnPressed += _ =>
@@ -302,7 +306,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
{
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
return _prototypeManager.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
return _prototypeManager.Index<JobPrototype>(highPriorityJob.Id ?? SharedGameTicker.FallbackOverflowJob);
}
public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)

View File

@@ -53,9 +53,9 @@ public sealed partial class CharacterPickerButton : ContainerButton
.LoadProfileEntity(humanoid, null, true);
var highPriorityJob = humanoid.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key;
if (highPriorityJob != null)
if (highPriorityJob != default)
{
var jobName = prototypeManager.Index<JobPrototype>(highPriorityJob).LocalizedName;
var jobName = prototypeManager.Index(highPriorityJob).LocalizedName;
description = $"{description}\n{jobName}";
}
}

View File

@@ -1,7 +1,6 @@
using System.IO;
using System.Linq;
using System.Numerics;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
using Content.Client.Lobby.UI.Loadouts;
using Content.Client.Lobby.UI.Roles;
@@ -13,13 +12,13 @@ using Content.Shared._CP14.Humanoid;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
using Content.Shared.Guidebook;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Content.Shared.Traits;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -97,6 +96,8 @@ namespace Content.Client.Lobby.UI
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultSpeciesGuidebook = "Species";
public event Action<List<ProtoId<GuideEntryPrototype>>>? OnOpenGuidebook;
private ISawmill _sawmill;
public HumanoidProfileEditor(
@@ -372,6 +373,7 @@ namespace Content.Client.Lobby.UI
{
PreferenceUnavailableButton.SelectId(args.Id);
Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
SetDirty();
};
_jobCategories = new Dictionary<string, BoxContainer>();
@@ -616,13 +618,15 @@ namespace Content.Client.Lobby.UI
{
Margin = new Thickness(3f, 3f, 3f, 0f),
};
selector.OnOpenGuidebook += OnOpenGuidebook;
var title = Loc.GetString(antag.Name);
var description = Loc.GetString(antag.Objective);
selector.Setup(items, title, 250, description);
selector.Setup(items, title, 250, description, guides: antag.Guides);
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
if (!_requirements.CheckRoleTime(antag.Requirements, out var reason))
var requirements = _entManager.System<SharedRoleSystem>().GetAntagRequirement(antag);
if (!_requirements.CheckRoleTime(requirements, out var reason))
{
selector.LockRequirements(reason);
Profile = Profile?.WithAntagPreference(antag.ID, false);
@@ -754,6 +758,10 @@ namespace Content.Client.Lobby.UI
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
{
// TODO GUIDEBOOK
// make the species guide book a field on the species prototype.
// I.e., do what jobs/antags do.
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var page = DefaultSpeciesGuidebook;
@@ -762,10 +770,10 @@ namespace Content.Client.Lobby.UI
if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary<string, GuideEntry>();
var dict = new Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry>();
dict.Add(DefaultSpeciesGuidebook, guideRoot);
//TODO: Don't close the guidebook if its already open, just go to the correct page
guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
guidebookController.OpenGuidebook(dict, includeChildren:true, selected: page);
}
}
@@ -860,6 +868,7 @@ namespace Content.Client.Lobby.UI
{
Margin = new Thickness(3f, 3f, 3f, 0f),
};
selector.OnOpenGuidebook += OnOpenGuidebook;
var icon = new TextureRect
{
@@ -868,7 +877,7 @@ namespace Content.Client.Lobby.UI
};
var jobIcon = _prototypeManager.Index(job.Icon);
icon.Texture = jobIcon.Icon.Frame0();
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon, job.Guides);
if (!_requirements.IsAllowed(job, out var reason))
{

View File

@@ -1,9 +1,14 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Horizontal">
<Label Name="TitleLabel"
Margin="5 0"
MouseFilter="Stop"/>
<BoxContainer Name="OptionsContainer"
SetWidth="400"/>
<Label Name="TitleLabel"
Margin="5 0"
MouseFilter="Stop"/>
<!--21 was the height of OptionsContainer at the time that this button was added. So I am limiting the texture to 21x21-->
<Control SetSize="21 21">
<TextureButton Name="Help" StyleClasses="HelpButton"/>
</Control>
<BoxContainer Name="OptionsContainer"
SetWidth="400"/>
</BoxContainer>

View File

@@ -1,10 +1,12 @@
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Guidebook;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Lobby.UI.Roles;
@@ -17,8 +19,10 @@ public sealed partial class RequirementsSelector : BoxContainer
{
private readonly RadioOptions<int> _options;
private readonly StripeBack _lockStripe;
private List<ProtoId<GuideEntryPrototype>>? _guides;
public event Action<int>? OnSelected;
public event Action<List<ProtoId<GuideEntryPrototype>>>? OnOpenGuidebook;
public int Selected => _options.SelectedId;
@@ -60,18 +64,33 @@ public sealed partial class RequirementsSelector : BoxContainer
requirementsLabel
}
};
Help.OnPressed += _ =>
{
if (_guides != null)
OnOpenGuidebook?.Invoke(_guides);
};
}
/// <summary>
/// Actually adds the controls.
/// </summary>
public void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
public void Setup(
(string, int)[] items,
string title,
int titleSize,
string? description,
TextureRect? icon = null,
List<ProtoId<GuideEntryPrototype>>? guides = null)
{
foreach (var (text, value) in items)
{
_options.AddItem(Loc.GetString(text), value);
}
Help.Visible = guides != null;
_guides = guides;
TitleLabel.Text = title;
TitleLabel.MinSize = new Vector2(titleSize, 0f);
TitleLabel.ToolTip = description;

View File

@@ -0,0 +1,20 @@
using Content.Shared.MapText;
using Robust.Client.Graphics;
namespace Content.Client.MapText;
[RegisterComponent]
public sealed partial class MapTextComponent : SharedMapTextComponent
{
/// <summary>
/// The font that gets cached on component init or state changes
/// </summary>
[ViewVariables]
public VectorFont? CachedFont;
/// <summary>
/// The text currently being displayed. This is either <see cref="SharedMapTextComponent.Text"/> or the
/// localized text <see cref="SharedMapTextComponent.LocText"/> or
/// </summary>
public string CachedText = string.Empty;
}

View File

@@ -0,0 +1,85 @@
using System.Numerics;
using Content.Shared.MapText;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
namespace Content.Client.MapText;
/// <summary>
/// Draws map text as an overlay
/// </summary>
public sealed class MapTextOverlay : Overlay
{
private readonly IConfigurationManager _configManager;
private readonly IEntityManager _entManager;
private readonly IUserInterfaceManager _uiManager;
private readonly SharedTransformSystem _transform;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public MapTextOverlay(
IConfigurationManager configManager,
IEntityManager entManager,
IUserInterfaceManager uiManager,
SharedTransformSystem transform,
IResourceCache resourceCache,
IPrototypeManager prototypeManager)
{
_configManager = configManager;
_entManager = entManager;
_uiManager = uiManager;
_transform = transform;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.ViewportControl == null)
return;
args.DrawingHandle.SetTransform(Matrix3x2.Identity);
var scale = _configManager.GetCVar(CVars.DisplayUIScale);
if (scale == 0f)
scale = _uiManager.DefaultUIScale;
DrawWorld(args.ScreenHandle, args, scale);
args.DrawingHandle.UseShader(null);
}
private void DrawWorld(DrawingHandleScreen handle, OverlayDrawArgs args, float scale)
{
if ( args.ViewportControl == null)
return;
var matrix = args.ViewportControl.GetWorldToScreenMatrix();
var query = _entManager.AllEntityQueryEnumerator<MapTextComponent>();
// Enlarge bounds to try prevent pop-in due to large text.
var bounds = args.WorldBounds.Enlarged(2);
while(query.MoveNext(out var uid, out var mapText))
{
var mapPos = _transform.GetMapCoordinates(uid);
if (mapPos.MapId != args.MapId)
continue;
if (!bounds.Contains(mapPos.Position))
continue;
if (mapText.CachedFont == null)
continue;
var pos = Vector2.Transform(mapPos.Position, matrix) + mapText.Offset;
var dimensions = handle.GetDimensions(mapText.CachedFont, mapText.CachedText, scale);
handle.DrawString(mapText.CachedFont, pos - dimensions / 2f, mapText.CachedText, scale, mapText.Color);
}
}
}

View File

@@ -0,0 +1,81 @@
using Content.Shared.MapText;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Configuration;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.MapText;
/// <inheritdoc/>
public sealed class MapTextSystem : SharedMapTextSystem
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
private MapTextOverlay _overlay = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<MapTextComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<MapTextComponent, ComponentHandleState>(HandleCompState);
_overlay = new MapTextOverlay(_configManager, EntityManager, _uiManager, _transform, _resourceCache, _prototypeManager);
_overlayManager.AddOverlay(_overlay);
// TODO move font prototype to robust.shared, then use ProtoId<FontPrototype>
DebugTools.Assert(_prototypeManager.HasIndex<FontPrototype>(SharedMapTextComponent.DefaultFont));
}
private void OnComponentStartup(Entity<MapTextComponent> ent, ref ComponentStartup args)
{
CacheText(ent.Comp);
// TODO move font prototype to robust.shared, then use ProtoId<FontPrototype>
DebugTools.Assert(_prototypeManager.HasIndex<FontPrototype>(ent.Comp.FontId));
}
private void HandleCompState(Entity<MapTextComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not MapTextComponentState state)
return;
ent.Comp.Text = state.Text;
ent.Comp.LocText = state.LocText;
ent.Comp.Color = state.Color;
ent.Comp.FontId = state.FontId;
ent.Comp.FontSize = state.FontSize;
ent.Comp.Offset = state.Offset;
CacheText(ent.Comp);
}
private void CacheText(MapTextComponent component)
{
component.CachedFont = null;
component.CachedText = string.IsNullOrWhiteSpace(component.Text)
? Loc.GetString(component.LocText)
: component.Text;
if (!_prototypeManager.TryIndex<FontPrototype>(component.FontId, out var fontPrototype))
{
component.CachedText = Loc.GetString("map-text-font-error");
component.Color = Color.Red;
if(_prototypeManager.TryIndex<FontPrototype>(SharedMapTextComponent.DefaultFont, out var @default))
component.CachedFont = new VectorFont(_resourceCache.GetResource<FontResource>(@default.Path), 14);
return;
}
var fontResource = _resourceCache.GetResource<FontResource>(fontPrototype.Path);
component.CachedFont = new VectorFont(fontResource, component.FontSize);
}
}

View File

@@ -87,6 +87,6 @@ public sealed partial class MaterialStorageControl : ScrollContainer
}
_currentMaterials = mats;
NoMatsLabel.Visible = ChildCount == 1;
NoMatsLabel.Visible = MaterialList.ChildCount == 1;
}
}

View File

@@ -21,8 +21,16 @@ namespace Content.Client.PDA
protected override void Open()
{
base.Open();
if (_menu == null)
CreateMenu();
_menu?.OpenCenteredLeft();
}
private void CreateMenu()
{
_menu = new PdaMenu();
_menu.OpenCenteredLeft();
_menu.OnClose += Close;
_menu.FlashLightToggleButton.OnToggled += _ =>
{

View File

@@ -106,7 +106,13 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
if (player == null)
return true;
return CheckRoleTime(job.Requirements, out reason);
return CheckRoleTime(job, out reason);
}
public bool CheckRoleTime(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
{
var reqs = _entManager.System<SharedRoleSystem>().GetJobRequirement(job);
return CheckRoleTime(reqs, out reason);
}
public bool CheckRoleTime(HashSet<JobRequirement>? requirements, [NotNullWhen(false)] out FormattedMessage? reason)

View File

@@ -9,6 +9,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Silicons.Laws.Ui;
@@ -27,6 +28,8 @@ public sealed partial class LawDisplay : Control
var identifier = law.LawIdentifierOverride ?? $"{law.Order}";
var lawIdentifier = Loc.GetString("laws-ui-law-header", ("id", identifier));
var lawDescription = Loc.GetString(law.LawString);
var lawIdentifierPlaintext = FormattedMessage.RemoveMarkupPermissive(lawIdentifier);
var lawDescriptionPlaintext = FormattedMessage.RemoveMarkupPermissive(lawDescription);
LawNumberLabel.SetMarkup(lawIdentifier);
LawLabel.SetMessage(lawDescription);
@@ -46,7 +49,7 @@ public sealed partial class LawDisplay : Control
localButton.OnPressed += _ =>
{
_chatManager.SendMessage($"{lawIdentifier}: {lawDescription}", ChatSelectChannel.Local);
_chatManager.SendMessage($"{lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Local);
};
LawAnnouncementButtons.AddChild(localButton);
@@ -73,9 +76,9 @@ public sealed partial class LawDisplay : Control
switch (radioChannel)
{
case SharedChatSystem.CommonChannel:
_chatManager.SendMessage($"{SharedChatSystem.RadioCommonPrefix} {lawIdentifier}: {lawDescription}", ChatSelectChannel.Radio); break;
_chatManager.SendMessage($"{SharedChatSystem.RadioCommonPrefix} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio); break;
default:
_chatManager.SendMessage($"{SharedChatSystem.RadioChannelPrefix}{radioChannelProto.KeyCode} {lawIdentifier}: {lawDescription}", ChatSelectChannel.Radio); break;
_chatManager.SendMessage($"{SharedChatSystem.RadioChannelPrefix}{radioChannelProto.KeyCode} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio); break;
}
};

View File

@@ -78,6 +78,8 @@ namespace Content.Client.Stylesheets
public const string StyleClassLabelSmall = "LabelSmall";
public const string StyleClassButtonBig = "ButtonBig";
public const string StyleClassButtonHelp = "HelpButton";
public const string StyleClassPopupMessageSmall = "PopupMessageSmall";
public const string StyleClassPopupMessageSmallCaution = "PopupMessageSmallCaution";
public const string StyleClassPopupMessageMedium = "PopupMessageMedium";
@@ -1346,6 +1348,10 @@ namespace Content.Client.Stylesheets
new StyleProperty(PanelContainer.StylePropertyPanel, new StyleBoxFlat { BackgroundColor = NanoGold, ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2}),
}),
Element<TextureButton>()
.Class(StyleClassButtonHelp)
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/VerbIcons/information.svg.192dpi.png")),
// Labels ---
Element<Label>().Class(StyleClassLabelBig)
.Prop(Label.StylePropertyFont, notoSans16),

View File

@@ -1,6 +1,7 @@
using System.Numerics;
using Content.Client.Guidebook;
using Content.Client.Guidebook.Components;
using Content.Shared.Guidebook;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
@@ -32,8 +33,8 @@ namespace Content.Client.UserInterface.Controls
set => WindowTitle.Text = value;
}
private List<string>? _helpGuidebookIds;
public List<string>? HelpGuidebookIds
private List<ProtoId<GuideEntryPrototype>>? _helpGuidebookIds;
public List<ProtoId<GuideEntryPrototype>>? HelpGuidebookIds
{
get => _helpGuidebookIds;
set

View File

@@ -14,6 +14,8 @@ public sealed class ScrambleTag : IMarkupTag
{
[Dependency] private readonly IGameTiming _timing = default!;
private const int MaxScrambleLength = 32;
public string Name => "scramble";
public string TextBefore(MarkupNode node)
@@ -29,8 +31,9 @@ public sealed class ScrambleTag : IMarkupTag
var seed = (int) (_timing.CurTime.TotalMilliseconds / rate);
var rand = new Random(seed + node.GetHashCode());
var charOptions = chars.ToCharArray();
var realLength = MathF.Min(length.Value, MaxScrambleLength);
var sb = new StringBuilder();
for (var i = 0; i < length; i++)
for (var i = 0; i < realLength; i++)
{
var index = rand.Next() % charOptions.Length;
sb.Append(charOptions[index]);

View File

@@ -289,6 +289,10 @@ public sealed class ActionButton : Control, IEntityControl
{
if (_action.IconOn != null)
SetActionIcon(_spriteSys.Frame0(_action.IconOn));
else if (_action.Icon != null)
SetActionIcon(_spriteSys.Frame0(_action.Icon));
else
SetActionIcon(null);
if (_action.BackgroundOn != null)
_buttonBackgroundTexture = _spriteSys.Frame0(_action.BackgroundOn);

View File

@@ -3,6 +3,7 @@ using Content.Client.Administration.Systems;
using Content.Client.Administration.UI;
using Content.Client.Administration.UI.Tabs.ObjectsTab;
using Content.Client.Administration.UI.Tabs.PanicBunkerTab;
using Content.Client.Administration.UI.Tabs.BabyJailTab;
using Content.Client.Administration.UI.Tabs.PlayerTab;
using Content.Client.Gameplay;
using Content.Client.Lobby;
@@ -37,11 +38,13 @@ public sealed class AdminUIController : UIController,
private AdminMenuWindow? _window;
private MenuButton? AdminButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.AdminButton;
private PanicBunkerStatus? _panicBunker;
private BabyJailStatus? _babyJail;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<PanicBunkerChangedEvent>(OnPanicBunkerUpdated);
SubscribeNetworkEvent<BabyJailChangedEvent>(OnBabyJailUpdated);
}
private void OnPanicBunkerUpdated(PanicBunkerChangedEvent msg, EntitySessionEventArgs args)
@@ -56,6 +59,18 @@ public sealed class AdminUIController : UIController,
}
}
private void OnBabyJailUpdated(BabyJailChangedEvent msg, EntitySessionEventArgs args)
{
var showDialog = _babyJail == null && msg.Status.Enabled;
_babyJail = msg.Status;
_window?.BabyJailControl.UpdateStatus(msg.Status);
if (showDialog)
{
UIManager.CreateWindow<BabyJailStatusWindow>().OpenCentered();
}
}
public void OnStateEntered(GameplayState state)
{
EnsureWindow();
@@ -101,6 +116,13 @@ public sealed class AdminUIController : UIController,
if (_panicBunker != null)
_window.PanicBunkerControl.UpdateStatus(_panicBunker);
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
if (_babyJail != null)
_window.BabyJailControl.UpdateStatus(_babyJail);
_window.PlayerTabControl.OnEntryKeyBindDown += PlayerTabEntryKeyBindDown;
_window.ObjectsTabControl.OnEntryKeyBindDown += ObjectsTabEntryKeyBindDown;
_window.OnOpen += OnWindowOpen;
@@ -198,9 +220,12 @@ public sealed class AdminUIController : UIController,
args.Handle();
}
private void ObjectsTabEntryKeyBindDown(ObjectsTabEntry entry, GUIBoundKeyEventArgs args)
private void ObjectsTabEntryKeyBindDown(GUIBoundKeyEventArgs args, ListData? data)
{
var uid = entry.AssocEntity;
if (data is not ObjectsListData { Info: var info })
return;
var uid = info.Entity;
var function = args.Function;
if (function == EngineKeyFunctions.UIClick)

View File

@@ -3,10 +3,15 @@ using Content.Client.Gameplay;
using Content.Client.Guidebook;
using Content.Client.Guidebook.Controls;
using Content.Client.Lobby;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.UserInterface.Controls;
using Content.Shared.CCVar;
using Content.Shared.Guidebook;
using Content.Shared.Input;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
using static Robust.Client.UserInterface.Controls.BaseButton;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
@@ -18,21 +23,25 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
{
[UISystemDependency] private readonly GuidebookSystem _guidebookSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly JobRequirementsManager _jobRequirements = default!;
private const int PlaytimeOpenGuidebook = 60;
private GuidebookWindow? _guideWindow;
private MenuButton? GuidebookButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.GuidebookButton;
public void OnStateEntered(LobbyState state)
{
HandleStateEntered();
HandleStateEntered(state);
}
public void OnStateEntered(GameplayState state)
{
HandleStateEntered();
HandleStateEntered(state);
}
private void HandleStateEntered()
private void HandleStateEntered(State state)
{
DebugTools.Assert(_guideWindow == null);
@@ -41,6 +50,14 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
_guideWindow.OnClose += OnWindowClosed;
_guideWindow.OnOpen += OnWindowOpen;
if (state is LobbyState &&
_jobRequirements.FetchOverallPlaytime() < TimeSpan.FromMinutes(PlaytimeOpenGuidebook))
{
OpenGuidebook();
_guideWindow.RecenterWindow(new(0.5f, 0.5f));
_guideWindow.SetPositionFirst();
}
// setup keybinding
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenGuidebook,
@@ -74,12 +91,12 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
public void OnSystemLoaded(GuidebookSystem system)
{
_guidebookSystem.OnGuidebookOpen += ToggleGuidebook;
_guidebookSystem.OnGuidebookOpen += OpenGuidebook;
}
public void OnSystemUnloaded(GuidebookSystem system)
{
_guidebookSystem.OnGuidebookOpen -= ToggleGuidebook;
_guidebookSystem.OnGuidebookOpen -= OpenGuidebook;
}
internal void UnloadButton()
@@ -103,10 +120,29 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
ToggleGuidebook();
}
public void ToggleGuidebook()
{
if (_guideWindow == null)
return;
if (_guideWindow.IsOpen)
{
UIManager.ClickSound();
_guideWindow.Close();
}
else
{
OpenGuidebook();
}
}
private void OnWindowClosed()
{
if (GuidebookButton != null)
GuidebookButton.Pressed = false;
if (_guideWindow != null)
_guideWindow.ReturnContainer.Visible = false;
}
private void OnWindowOpen()
@@ -127,30 +163,25 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
/// <param name="includeChildren">Whether or not to automatically include child entries. If false, this will ONLY
/// show the specified entries</param>
/// <param name="selected">The guide whose contents should be displayed when the guidebook is opened</param>
public void ToggleGuidebook(
Dictionary<string, GuideEntry>? guides = null,
List<string>? rootEntries = null,
string? forceRoot = null,
public void OpenGuidebook(
Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry>? guides = null,
List<ProtoId<GuideEntryPrototype>>? rootEntries = null,
ProtoId<GuideEntryPrototype>? forceRoot = null,
bool includeChildren = true,
string? selected = null)
ProtoId<GuideEntryPrototype>? selected = null)
{
if (_guideWindow == null)
return;
if (_guideWindow.IsOpen)
{
UIManager.ClickSound();
_guideWindow.Close();
return;
}
if (GuidebookButton != null)
GuidebookButton.SetClickPressed(!_guideWindow.IsOpen);
selected ??= _configuration.GetCVar(CCVars.DefaultGuide);
if (guides == null)
{
guides = _prototypeManager.EnumeratePrototypes<GuideEntryPrototype>()
.ToDictionary(x => x.ID, x => (GuideEntry) x);
.ToDictionary(x => new ProtoId<GuideEntryPrototype>(x.ID), x => (GuideEntry) x);
}
else if (includeChildren)
{
@@ -171,17 +202,17 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
_guideWindow.OpenCenteredRight();
}
public void ToggleGuidebook(
List<string> guideList,
List<string>? rootEntries = null,
string? forceRoot = null,
public void OpenGuidebook(
List<ProtoId<GuideEntryPrototype>> guideList,
List<ProtoId<GuideEntryPrototype>>? rootEntries = null,
ProtoId<GuideEntryPrototype>? forceRoot = null,
bool includeChildren = true,
string? selected = null)
ProtoId<GuideEntryPrototype>? selected = null)
{
Dictionary<string, GuideEntry> guides = new();
Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> guides = new();
foreach (var guideId in guideList)
{
if (!_prototypeManager.TryIndex<GuideEntryPrototype>(guideId, out var guide))
if (!_prototypeManager.TryIndex(guideId, out var guide))
{
Logger.Error($"Encountered unknown guide prototype: {guideId}");
continue;
@@ -189,17 +220,29 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
guides.Add(guideId, guide);
}
ToggleGuidebook(guides, rootEntries, forceRoot, includeChildren, selected);
OpenGuidebook(guides, rootEntries, forceRoot, includeChildren, selected);
}
private void RecursivelyAddChildren(GuideEntry guide, Dictionary<string, GuideEntry> guides)
public void CloseGuidebook()
{
if (_guideWindow == null)
return;
if (_guideWindow.IsOpen)
{
UIManager.ClickSound();
_guideWindow.Close();
}
}
private void RecursivelyAddChildren(GuideEntry guide, Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> guides)
{
foreach (var childId in guide.Children)
{
if (guides.ContainsKey(childId))
continue;
if (!_prototypeManager.TryIndex<GuideEntryPrototype>(childId, out var child))
if (!_prototypeManager.TryIndex(childId, out var child))
{
Logger.Error($"Encountered unknown guide prototype: {childId} as a child of {guide.Id}. If the child is not a prototype, it must be directly provided.");
continue;

View File

@@ -1,13 +1,49 @@
using Content.Client.Gameplay;
using Content.Client.Gameplay;
using Content.Client.Info;
using Content.Shared.CCVar;
using Content.Shared.Guidebook;
using Content.Shared.Info;
using Robust.Client.Console;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Systems.Info;
public sealed class InfoUIController : UIController, IOnStateExited<GameplayState>
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private RulesPopup? _rulesPopup;
private RulesAndInfoWindow? _infoWindow;
public override void Initialize()
{
base.Initialize();
_netManager.RegisterNetMessage<RulesAcceptedMessage>();
_netManager.RegisterNetMessage<ShowRulesPopupMessage>(OnShowRulesPopupMessage);
_consoleHost.RegisterCommand("fuckrules",
"",
"",
(_, _, _) =>
{
OnAcceptPressed();
});
}
private void OnShowRulesPopupMessage(ShowRulesPopupMessage message)
{
ShowRules(message.PopupTime);
}
public void OnStateExited(GameplayState state)
{
if (_infoWindow == null)
@@ -17,12 +53,46 @@ public sealed class InfoUIController : UIController, IOnStateExited<GameplayStat
_infoWindow = null;
}
private void ShowRules(float time)
{
if (_rulesPopup != null)
return;
_rulesPopup = new RulesPopup
{
Timer = time
};
_rulesPopup.OnQuitPressed += OnQuitPressed;
_rulesPopup.OnAcceptPressed += OnAcceptPressed;
UIManager.WindowRoot.AddChild(_rulesPopup);
LayoutContainer.SetAnchorPreset(_rulesPopup, LayoutContainer.LayoutPreset.Wide);
}
private void OnQuitPressed()
{
_consoleHost.ExecuteCommand("quit");
}
private void OnAcceptPressed()
{
_netManager.ClientSendMessage(new RulesAcceptedMessage());
_rulesPopup?.Orphan();
_rulesPopup = null;
}
public GuideEntryPrototype GetCoreRuleEntry()
{
var guide = _cfg.GetCVar(CCVars.RulesFile);
var guideEntryPrototype = _prototype.Index<GuideEntryPrototype>(guide);
return guideEntryPrototype;
}
public void OpenWindow()
{
if (_infoWindow == null || _infoWindow.Disposed)
{
_infoWindow = UIManager.CreateWindow<RulesAndInfoWindow>();
}
_infoWindow?.OpenCentered();
}

View File

@@ -7,6 +7,7 @@ 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;
@@ -133,27 +134,73 @@ public sealed partial class TestPair
}
/// <summary>
/// Helper method for enabling or disabling a antag role
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
/// </summary>
public async Task SetAntagPref(ProtoId<AntagPrototype> id, bool value)
public async Task SetAntagPreference(ProtoId<AntagPrototype> id, bool value, NetUserId? user = null)
{
user ??= Client.User!.Value;
if (user is not {} userId)
return;
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(userId);
var prefs = prefMan.GetPreferences(Client.User!.Value);
// what even is the point of ICharacterProfile if we always cast it to HumanoidCharacterProfile to make it usable?
var profile = (HumanoidCharacterProfile) prefs.SelectedCharacter;
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
Assert.That(profile.AntagPreferences.Contains(id), Is.EqualTo(!value));
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var newProfile = profile.WithAntagPreference(id, value);
_modifiedProfiles.Add(userId);
await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait());
}
await Server.WaitPost(() =>
/// <summary>
/// Set a user's job preferences. Modified preferences are automatically reset at the end of the test.
/// </summary>
public async Task SetJobPriority(ProtoId<JobPrototype> id, JobPriority value, NetUserId? user = null)
{
user ??= Client.User!.Value;
if (user is { } userId)
await SetJobPriorities(userId, (id, value));
}
/// <inheritdoc cref="SetJobPriority"/>
public async Task SetJobPriorities(params (ProtoId<JobPrototype>, JobPriority)[] priorities)
=> await SetJobPriorities(Client.User!.Value, priorities);
/// <inheritdoc cref="SetJobPriority"/>
public async Task SetJobPriorities(NetUserId user, params (ProtoId<JobPrototype>, JobPriority)[] priorities)
{
var highCount = priorities.Count(x => x.Item2 == JobPriority.High);
Assert.That(highCount, Is.LessThanOrEqualTo(1), "Cannot have more than one high priority job");
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(user);
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(profile.JobPriorities);
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
if (highCount != 0)
{
prefMan.SetProfile(Client.User.Value, prefs.SelectedCharacterIndex, newProfile).Wait();
});
foreach (var (key, priority) in dictionary)
{
if (priority == JobPriority.High)
dictionary[key] = JobPriority.Medium;
}
}
// And why the fuck does it always create a new preference and profile object instead of just reusing them?
var newPrefs = prefMan.GetPreferences(Client.User.Value);
var newProf = (HumanoidCharacterProfile) newPrefs.SelectedCharacter;
Assert.That(newProf.AntagPreferences.Contains(id), Is.EqualTo(value));
foreach (var (job, priority) in priorities)
{
if (priority == JobPriority.Never)
dictionary.Remove(job);
else
dictionary[job] = priority;
}
var newProfile = profile.WithJobPriorities(dictionary);
_modifiedProfiles.Add(user);
await Server.WaitPost(() => prefMan.SetProfile(user, 0, newProfile).Wait());
}
}

View File

@@ -2,10 +2,12 @@
using System.IO;
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Preferences.Managers;
using Content.Shared.CCVar;
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;
@@ -34,6 +36,11 @@ public sealed partial class TestPair : IAsyncDisposable
private async Task OnCleanDispose()
{
await Server.WaitIdleAsync();
await Client.WaitIdleAsync();
await ResetModifiedPreferences();
await Server.RemoveAllDummySessions();
if (TestMap != null)
{
await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
@@ -79,6 +86,16 @@ public sealed partial class TestPair : IAsyncDisposable
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
}
private async Task ResetModifiedPreferences()
{
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
foreach (var user in _modifiedProfiles)
{
await Server.WaitPost(() => prefMan.SetProfile(user, 0, new HumanoidCharacterProfile()).Wait());
}
_modifiedProfiles.Clear();
}
public async ValueTask CleanReturnAsync()
{
if (State != PairState.InUse)

View File

@@ -26,6 +26,8 @@ public sealed partial class TestPair
public readonly List<string> TestHistory = new();
public PoolSettings Settings = default!;
public TestMapData? TestMap;
private List<NetUserId> _modifiedProfiles = new();
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
@@ -37,7 +39,8 @@ public sealed partial class TestPair
client = Client;
}
public ICommonSession? Player => Server.PlayerMan.Sessions.FirstOrDefault();
public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;

View File

@@ -28,6 +28,7 @@ public static partial class PoolManager
(CCVars.EmergencyShuttleEnabled.Name, "false"),
(CCVars.ProcgenPreload.Name, "false"),
(CCVars.WorldgenEnabled.Name, "false"),
(CCVars.GatewayGeneratorEnabled.Name, "false"),
(CVars.ReplayClientRecordingEnabled.Name, "false"),
(CVars.ReplayServerRecordingEnabled.Name, "false"),
(CCVars.GameDummyTicker.Name, "true"),

View File

@@ -340,6 +340,7 @@ namespace Content.IntegrationTests.Tests
"MapGrid",
"Broadphase",
"StationData", // errors when removed mid-round
"StationJobs",
"Actor", // We aren't testing actor components, those need their player session set.
"BlobFloorPlanBuilder", // Implodes if unconfigured.
"DebrisFeaturePlacerController", // Above.

View File

@@ -52,7 +52,7 @@ public sealed class AntagPreferenceTest
Assert.That(pool.Count, Is.EqualTo(0));
// Opt into the traitor role.
await pair.SetAntagPref("Traitor", true);
await pair.SetAntagPreference("Traitor", true);
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
@@ -63,7 +63,7 @@ public sealed class AntagPreferenceTest
Assert.That(sessions.Count, Is.EqualTo(1));
// opt back out
await pair.SetAntagPref("Traitor", false);
await pair.SetAntagPreference("Traitor", false);
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);

View File

@@ -57,8 +57,17 @@ public sealed class NukeOpsTest
Assert.That(client.AttachedEntity, Is.Null);
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
// Add several dummy players
var dummies = await pair.Server.AddDummySessions(3);
await pair.RunTicksSync(5);
// Opt into the nukies role.
await pair.SetAntagPref("NukeopsCommander", true);
await pair.SetAntagPreference("NukeopsCommander", true);
await pair.SetAntagPreference( "NukeopsMedic", true, dummies[1].UserId);
// Initially, the players have no attached entities
Assert.That(pair.Player?.AttachedEntity, Is.Null);
Assert.That(dummies.All(x => x.AttachedEntity == null));
// There are no grids or maps
Assert.That(entMan.Count<MapComponent>(), Is.Zero);
@@ -75,17 +84,20 @@ public sealed class NukeOpsTest
Assert.That(entMan.Count<NukeOperativeSpawnerComponent>(), Is.Zero);
// Ready up and start nukeops
await pair.WaitClientCommand("toggleready True");
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay));
ticker.ToggleReadyAll(true);
Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.ReadyToPlay));
await pair.WaitCommand("forcepreset Nukeops");
await pair.RunTicksSync(10);
// Game should have started
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.JoinedGame));
Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.JoinedGame));
Assert.That(client.EntMan.EntityExists(client.AttachedEntity));
var dummyEnts = dummies.Select(x => x.AttachedEntity ?? default).ToArray();
var player = pair.Player!.AttachedEntity!.Value;
Assert.That(entMan.EntityExists(player));
Assert.That(dummyEnts.All(e => entMan.EntityExists(e)));
// Maps now exist
Assert.That(entMan.Count<MapComponent>(), Is.GreaterThan(0));
@@ -96,8 +108,8 @@ public sealed class NukeOpsTest
// And we now have nukie related components
Assert.That(entMan.Count<NukeopsRuleComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<NukeopsRoleComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<NukeOperativeComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<NukeopsRoleComponent>(), Is.EqualTo(2));
Assert.That(entMan.Count<NukeOperativeComponent>(), Is.EqualTo(2));
Assert.That(entMan.Count<NukeOpsShuttleComponent>(), Is.EqualTo(1));
// The player entity should be the nukie commander
@@ -107,11 +119,36 @@ public sealed class NukeOpsTest
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind));
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
var roles = roleSys.MindGetAllRoles(mind);
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent);
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
// The second dummy player should be a medic
var dummyMind = mindSys.GetMind(dummyEnts[1])!.Value;
Assert.That(entMan.HasComponent<NukeOperativeComponent>(dummyEnts[1]));
Assert.That(roleSys.MindIsAntagonist(dummyMind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False);
roles = roleSys.MindGetAllRoles(dummyMind);
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic" && x.Component is NukeopsRoleComponent);
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
// The other two players should have just spawned in as normal.
CheckDummy(0);
CheckDummy(2);
void CheckDummy(int i)
{
var ent = dummyEnts[i];
var mind = mindSys.GetMind(ent)!.Value;
Assert.That(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
Assert.That(roleSys.MindIsAntagonist(mind), Is.False);
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind), Is.False);
Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False);
Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True);
Assert.That(roleSys.MindGetAllRoles(mind).Any(x => x.Component is NukeopsRoleComponent), Is.False);
}
// The game rule exists, and all the stations/shuttles/maps are properly initialized
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single().Component;
var gridsRule = entMan.AllComponents<RuleGridsComponent>().Single().Component;
@@ -178,7 +215,7 @@ public sealed class NukeOpsTest
// While we're at it, lets make sure they aren't naked. I don't know how many inventory slots all mobs will be
// likely to have in the future. But nukies should probably have at least 3 slots with something in them.
var enumerator = invSys.GetSlotEnumerator(player);
int total = 0;
var total = 0;
while (enumerator.NextItem(out _))
{
total++;
@@ -199,7 +236,6 @@ public sealed class NukeOpsTest
}
ticker.SetGamePreset((GamePresetPrototype?)null);
await pair.SetAntagPref("NukeopsCommander", false);
await pair.CleanReturnAsync();
}
}

View File

@@ -3,6 +3,7 @@ using Content.Client.Guidebook.Richtext;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
using System.Linq;
using Content.Shared.Guidebook;
namespace Content.IntegrationTests.Tests.Guidebook;

View File

@@ -2,6 +2,8 @@
using System.Linq;
using Content.Server.Construction.Components;
using Content.Shared.Construction.Components;
using Content.Shared.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests;
@@ -49,12 +51,11 @@ public sealed class MachineBoardTest
Assert.Multiple(() =>
{
Assert.That(mId, Is.Not.Null, $"Machine board {p.ID} does not have a corresponding machine.");
Assert.That(protoMan.TryIndex<EntityPrototype>(mId, out var mProto),
$"Machine board {p.ID}'s corresponding machine has an invalid prototype.");
Assert.That(mProto.TryGetComponent<MachineComponent>(out var mComp),
$"Machine board {p.ID}'s corresponding machine {mId} does not have MachineComponent");
Assert.That(mComp.BoardPrototype, Is.EqualTo(p.ID),
Assert.That(mComp.Board, Is.EqualTo(p.ID),
$"Machine {mId}'s BoardPrototype is not equal to it's corresponding machine board, {p.ID}");
});
}
@@ -101,4 +102,40 @@ public sealed class MachineBoardTest
await pair.CleanReturnAsync();
}
/// <summary>
/// Ensures that every single computer board's corresponding entity
/// is a computer that can be properly deconstructed to the correct board
/// </summary>
[Test]
public async Task TestValidateBoardComponentRequirements()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
foreach (var p in protoMan.EnumeratePrototypes<EntityPrototype>()
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => !_ignoredPrototypes.Contains(p.ID)))
{
if (!p.TryGetComponent<MachineBoardComponent>(out var board, entMan.ComponentFactory))
continue;
Assert.Multiple(() =>
{
foreach (var component in board.ComponentRequirements.Keys)
{
Assert.That(entMan.ComponentFactory.TryGetRegistration(component, out _), $"Invalid component requirement {component} specified on machine board entity {p}");
}
});
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -60,7 +60,8 @@ public sealed class MaterialArbitrageTest
}
// Lets assume the possible lathe for resource multipliers:
var multiplier = MathF.Pow(LatheComponent.DefaultPartRatingMaterialUseMultiplier, MachinePartComponent.MaxRating - 1);
// TODO: each recipe can technically have its own cost multiplier associated with it, so this test needs redone to factor that in.
var multiplier = MathF.Pow(0.85f, 3);
// create construction dictionary
Dictionary<string, ConstructionComponent> constructionRecipes = new();

View File

@@ -249,22 +249,15 @@ namespace Content.IntegrationTests.Tests
// Test all availableJobs have spawnPoints
// This is done inside gamemap test because loading the map takes ages and we already have it.
var jobList = entManager.GetComponent<StationJobsComponent>(station).RoundStartJobList
.Where(x => x.Value != 0)
.Select(x => x.Key);
var spawnPoints = entManager.EntityQuery<SpawnPointComponent>()
.Where(spawnpoint => spawnpoint.SpawnType == SpawnPointType.Job)
.Select(spawnpoint => spawnpoint.Job.ID)
.Distinct();
List<string> missingSpawnPoints = new();
foreach (var spawnpoint in jobList.Except(spawnPoints))
{
if (protoManager.Index<JobPrototype>(spawnpoint).SetPreference)
missingSpawnPoints.Add(spawnpoint);
}
var comp = entManager.GetComponent<StationJobsComponent>(station);
var jobs = new HashSet<ProtoId<JobPrototype>>(comp.SetupAvailableJobs.Keys);
Assert.That(missingSpawnPoints, Has.Count.EqualTo(0),
$"There is no spawnpoint for {string.Join(", ", missingSpawnPoints)} on {mapProto}.");
var spawnPoints = entManager.EntityQuery<SpawnPointComponent>()
.Where(x => x.SpawnType == SpawnPointType.Job)
.Select(x => x.Job!.Value);
jobs.ExceptWith(spawnPoints);
Assert.That(jobs, Is.Empty,$"There is no spawnpoints for {string.Join(", ", jobs)} on {mapProto}.");
}
try

View File

@@ -0,0 +1,222 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.IntegrationTests.Pair;
using Content.Server.GameTicking;
using Content.Server.Mind;
using Content.Server.Roles;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Round;
[TestFixture]
public sealed class JobTest
{/*
private static ProtoId<JobPrototype> _passenger = "Passenger";
private static ProtoId<JobPrototype> _engineer = "StationEngineer";
private static ProtoId<JobPrototype> _captain = "Captain";
private static string _map = "JobTestMap";
[TestPrototypes]
public static string JobTestMap = @$"
- type: gameMap
id: {_map}
mapName: {_map}
mapPath: /Maps/Test/empty.yml
minPlayers: 0
stations:
Empty:
stationProto: StandardNanotrasenStation
components:
- type: StationNameSetup
mapNameTemplate: ""Empty""
- type: StationJobs
availableJobs:
{_passenger}: [ -1, -1 ]
{_engineer}: [ -1, -1 ]
{_captain}: [ 1, 1 ]
";
public void AssertJob(TestPair pair, ProtoId<JobPrototype> job, NetUserId? user = null, bool isAntag = false)
{
var jobSys = pair.Server.System<SharedJobSystem>();
var mindSys = pair.Server.System<MindSystem>();
var roleSys = pair.Server.System<RoleSystem>();
var ticker = pair.Server.System<GameTicker>();
user ??= pair.Client.User!.Value;
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
Assert.That(ticker.PlayerGameStatuses[user.Value], Is.EqualTo(PlayerGameStatus.JoinedGame));
var uid = pair.Server.PlayerMan.SessionsDict.GetValueOrDefault(user.Value)?.AttachedEntity;
Assert.That(pair.Server.EntMan.EntityExists(uid));
var mind = mindSys.GetMind(uid!.Value);
Assert.That(pair.Server.EntMan.EntityExists(mind));
Assert.That(jobSys.MindTryGetJobId(mind, out var actualJob));
Assert.That(actualJob, Is.EqualTo(job));
Assert.That(roleSys.MindIsAntagonist(mind), Is.EqualTo(isAntag));
}
/// <summary>
/// Simple test that checks that starting the round spawns the player into the test map as a passenger.
/// </summary>
[Test]
public async Task StartRoundTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
var ticker = pair.Server.System<GameTicker>();
// Initially in the lobby
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(pair.Client.AttachedEntity, Is.Null);
Assert.That(ticker.PlayerGameStatuses[pair.Client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
// Ready up and start the round
ticker.ToggleReadyAll(true);
Assert.That(ticker.PlayerGameStatuses[pair.Client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay));
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _passenger);
await pair.Server.WaitPost(() => ticker.RestartRound());
await pair.CleanReturnAsync();
}
/// <summary>
/// Check that job preferences are respected.
/// </summary>
[Test]
public async Task JobPreferenceTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
var ticker = pair.Server.System<GameTicker>();
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(pair.Client.AttachedEntity, Is.Null);
await pair.SetJobPriorities((_passenger, JobPriority.Medium), (_engineer, JobPriority.High));
ticker.ToggleReadyAll(true);
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _engineer);
await pair.Server.WaitPost(() => ticker.RestartRound());
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
await pair.SetJobPriorities((_passenger, JobPriority.High), (_engineer, JobPriority.Medium));
ticker.ToggleReadyAll(true);
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _passenger);
await pair.Server.WaitPost(() => ticker.RestartRound());
await pair.CleanReturnAsync();
}
/// <summary>
/// Check high priority jobs (e.g., captain) are selected before other roles, even if it means a player does not
/// get their preferred job.
/// </summary>
[Test]
public async Task JobWeightTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
var ticker = pair.Server.System<GameTicker>();
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(pair.Client.AttachedEntity, Is.Null);
var captain = pair.Server.ProtoMan.Index(_captain);
var engineer = pair.Server.ProtoMan.Index(_engineer);
var passenger = pair.Server.ProtoMan.Index(_passenger);
Assert.That(captain.Weight, Is.GreaterThan(engineer.Weight));
Assert.That(engineer.Weight, Is.EqualTo(passenger.Weight));
await pair.SetJobPriorities((_passenger, JobPriority.Medium), (_engineer, JobPriority.High), (_captain, JobPriority.Low));
ticker.ToggleReadyAll(true);
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _captain);
await pair.Server.WaitPost(() => ticker.RestartRound());
await pair.CleanReturnAsync();
}
/// <summary>
/// Check that jobs are preferentially given to players that have marked those jobs as higher priority.
/// </summary>
[Test]
public async Task JobPriorityTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
var ticker = pair.Server.System<GameTicker>();
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(pair.Client.AttachedEntity, Is.Null);
await pair.Server.AddDummySessions(5);
await pair.RunTicksSync(5);
var engineers = pair.Server.PlayerMan.Sessions.Select(x => x.UserId).ToList();
var captain = engineers[3];
engineers.RemoveAt(3);
await pair.SetJobPriorities(captain, (_captain, JobPriority.High), (_engineer, JobPriority.Medium));
foreach (var engi in engineers)
{
await pair.SetJobPriorities(engi, (_captain, JobPriority.Medium), (_engineer, JobPriority.High));
}
ticker.ToggleReadyAll(true);
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _captain, captain);
Assert.Multiple(() =>
{
foreach (var engi in engineers)
{
AssertJob(pair, _engineer, engi);
}
});
await pair.Server.WaitPost(() => ticker.RestartRound());
await pair.CleanReturnAsync();
}*/
}

View File

@@ -7,7 +7,6 @@ using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -46,8 +45,6 @@ public sealed class StationJobsTest
stationProto: StandardNanotrasenStation
components:
- type: StationJobs
overflowJobs:
- Passenger
availableJobs:
TMime: [0, -1]
TAssistant: [-1, -1]
@@ -164,7 +161,6 @@ public sealed class StationJobsTest
var server = pair.Server;
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
@@ -215,6 +211,8 @@ public sealed class StationJobsTest
var server = pair.Server;
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var name = compFact.GetComponentName<StationJobsComponent>();
await server.WaitAssertion(() =>
{
@@ -233,11 +231,14 @@ public sealed class StationJobsTest
{
foreach (var (stationId, station) in gameMap.Stations)
{
if (!station.StationComponentOverrides.TryGetComponent("StationJobs", out var comp))
if (!station.StationComponentOverrides.TryGetComponent(name, out var comp))
continue;
foreach (var (job, _) in ((StationJobsComponent) comp).SetupAvailableJobs)
foreach (var (job, array) in ((StationJobsComponent) comp).SetupAvailableJobs)
{
Assert.That(array.Length, Is.EqualTo(2));
Assert.That(array[0] is -1 or >= 0);
Assert.That(array[1] is -1 or >= 0);
Assert.That(invalidJobs, Does.Not.Contain(job), $"Station {stationId} contains job prototype {job} which cannot be present roundstart.");
}
}

View File

@@ -4,6 +4,7 @@ using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Input;
using Content.Shared.PDA;
using Content.Shared.Storage;
using Content.Shared.Timing;
using Robust.Client.UserInterface;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
@@ -27,11 +28,22 @@ public sealed class StorageInteractionTest : InteractionTest
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.False);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.False);
await Server.WaitPost(() => SEntMan.RemoveComponent<UseDelayComponent>(STarget!.Value));
await RunTicks(5);
// Activating the backpack opens the UI
await Activate();
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.False);
// Activating it again closes the UI
await Activate();
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.False);
// Open it again
await Activate();
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
// Pick up a PDA
var pda = await PlaceInHands("PassengerPDA");
var sPda = ToServer(pda);

View File

@@ -11,6 +11,11 @@ if (!CommandLineArgs.TryParse(args, out var parsed))
if (parsed.WipeRelease)
WipeRelease();
else
{
// Ensure the release directory exists. Otherwise, the packaging will fail.
Directory.CreateDirectory("release");
}
if (!parsed.SkipBuild)
WipeBin();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RemoveLastReadRules : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "last_read_rules",
table: "player");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "last_read_rules",
table: "player",
type: "timestamp with time zone",
nullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ReturnLastReadRules : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "last_read_rules",
table: "player",
type: "timestamp with time zone",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "last_read_rules",
table: "player");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RemoveLastReadRules : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "last_read_rules",
table: "player");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "last_read_rules",
table: "player",
type: "TEXT",
nullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ReturnLastReadRules : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "last_read_rules",
table: "player",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "last_read_rules",
table: "player");
}
}
}

View File

@@ -889,6 +889,10 @@ namespace Content.Server.Database
Whitelist = 1,
Full = 2,
Panic = 3,
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
BabyJail = 4,
}
public class ServerBanHit

View File

@@ -0,0 +1,139 @@
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Server)]
public sealed class BabyJailCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "babyjail";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var toggle = Toggle(CCVars.BabyJailEnabled, shell, args, _cfg);
if (toggle == null)
return;
shell.WriteLine(Loc.GetString(toggle.Value ? "babyjail-command-enabled" : "babyjail-command-disabled"));
}
public static bool? Toggle(CVarDef<bool> cvar, IConsoleShell shell, string[] args, IConfigurationManager config)
{
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
return null;
}
var enabled = config.GetCVar(cvar);
switch (args.Length)
{
case 0:
enabled = !enabled;
break;
case 1 when !bool.TryParse(args[0], out enabled):
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
return null;
}
config.SetCVar(cvar, enabled);
return enabled;
}
}
[AdminCommand(AdminFlags.Server)]
public sealed class BabyJailShowReasonCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "babyjail_show_reason";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var toggle = BabyJailCommand.Toggle(CCVars.BabyJailShowReason, shell, args, _cfg);
if (toggle == null)
return;
shell.WriteLine(Loc.GetString(toggle.Value
? "babyjail-command-show-reason-enabled"
: "babyjail-command-show-reason-disabled"
));
}
}
[AdminCommand(AdminFlags.Server)]
public sealed class BabyJailMinAccountAgeCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "babyjail_max_account_age";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
{
var current = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge);
shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-is", ("minutes", current)));
break;
}
case > 1:
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
return;
}
if (!int.TryParse(args[0], out var minutes))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}
_cfg.SetCVar(CCVars.BabyJailMaxAccountAge, minutes);
shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-set", ("minutes", minutes)));
}
}
[AdminCommand(AdminFlags.Server)]
public sealed class BabyJailMinOverallHoursCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "babyjail_max_overall_minutes";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
{
var current = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
shell.WriteLine(Loc.GetString("babyjail-command-max-overall-minutes-is", ("minutes", current)));
break;
}
case > 1:
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
return;
}
if (!int.TryParse(args[0], out var hours))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}
_cfg.SetCVar(CCVars.BabyJailMaxOverallMinutes, hours);
shell.WriteLine(Loc.GetString("babyjail-command-overall-minutes-set", ("hours", hours)));
}
}

View File

@@ -139,7 +139,7 @@ public sealed class PanicBunkerMinAccountAgeCommand : LocalizedCommands
if (args.Length == 0)
{
var current = _cfg.GetCVar(CCVars.PanicBunkerMinAccountAge);
shell.WriteLine(Loc.GetString("panicbunker-command-min-account-age-is", ("hours", current / 60)));
shell.WriteLine(Loc.GetString("panicbunker-command-min-account-age-is", ("minutes", current)));
}
if (args.Length > 1)
@@ -148,30 +148,30 @@ public sealed class PanicBunkerMinAccountAgeCommand : LocalizedCommands
return;
}
if (!int.TryParse(args[0], out var hours))
if (!int.TryParse(args[0], out var minutes))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}
_cfg.SetCVar(CCVars.PanicBunkerMinAccountAge, hours * 60);
shell.WriteLine(Loc.GetString("panicbunker-command-min-account-age-set", ("hours", hours)));
_cfg.SetCVar(CCVars.PanicBunkerMinAccountAge, minutes);
shell.WriteLine(Loc.GetString("panicbunker-command-min-account-age-set", ("minutes", minutes)));
}
}
[AdminCommand(AdminFlags.Server)]
public sealed class PanicBunkerMinOverallHoursCommand : LocalizedCommands
public sealed class PanicBunkerMinOverallMinutesCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "panicbunker_min_overall_hours";
public override string Command => "panicbunker_min_overall_minutes";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
var current = _cfg.GetCVar(CCVars.PanicBunkerMinOverallHours);
shell.WriteLine(Loc.GetString("panicbunker-command-min-overall-hours-is", ("minutes", current)));
var current = _cfg.GetCVar(CCVars.PanicBunkerMinOverallMinutes);
shell.WriteLine(Loc.GetString("panicbunker-command-min-overall-minutes-is", ("minutes", current)));
}
if (args.Length > 1)
@@ -180,13 +180,13 @@ public sealed class PanicBunkerMinOverallHoursCommand : LocalizedCommands
return;
}
if (!int.TryParse(args[0], out var hours))
if (!int.TryParse(args[0], out var minutes))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}
_cfg.SetCVar(CCVars.PanicBunkerMinOverallHours, hours);
shell.WriteLine(Loc.GetString("panicbunker-command-overall-hours-age-set", ("hours", hours)));
_cfg.SetCVar(CCVars.PanicBunkerMinOverallMinutes, minutes);
shell.WriteLine(Loc.GetString("panicbunker-command-overall-minutes-age-set", ("minutes", minutes)));
}
}

View File

@@ -93,7 +93,7 @@ public sealed class PlayTimeAddRoleCommand : IConsoleCommand
}
_playTimeTracking.AddTimeToTracker(player, role, TimeSpan.FromMinutes(minutes));
var time = _playTimeTracking.GetOverallPlaytime(player);
var time = _playTimeTracking.GetPlayTimeForTracker(player, role);
shell.WriteLine(Loc.GetString("cmd-playtime_addrole-succeed",
("username", userName),
("role", role),

View File

@@ -65,6 +65,10 @@ public sealed partial class AdminLogManager
{
players.Add(actor.PlayerSession.UserId.UserId);
}
else if (value is SerializablePlayer player)
{
players.Add(player.Player.UserId.UserId);
}
}
return (JsonSerializer.SerializeToDocument(parsed, _jsonOptions), players);

View File

@@ -7,6 +7,7 @@ using Content.Server.Database;
using Content.Server.Players;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Info;
using Content.Shared.Players;
using Robust.Server.Console;
using Robust.Server.Player;

View File

@@ -41,7 +41,7 @@ public sealed partial class ServerApi : IPostInjectInit
CCVars.PanicBunkerCountDeadminnedAdmins.Name,
CCVars.PanicBunkerShowReason.Name,
CCVars.PanicBunkerMinAccountAge.Name,
CCVars.PanicBunkerMinOverallHours.Name,
CCVars.PanicBunkerMinOverallMinutes.Name,
CCVars.PanicBunkerCustomReason.Name,
];

View File

@@ -4,7 +4,6 @@ using Content.Server.Chat.Managers;
using Content.Server.Forensics;
using Content.Server.GameTicking;
using Content.Server.Hands.Systems;
using Content.Server.IdentityManagement;
using Content.Server.Mind;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Popups;
@@ -62,6 +61,7 @@ namespace Content.Server.Administration.Systems
private readonly HashSet<NetUserId> _roundActivePlayers = new();
public readonly PanicBunkerStatus PanicBunker = new();
public readonly BabyJailStatus BabyJail = new();
public override void Initialize()
{
@@ -69,14 +69,26 @@ namespace Content.Server.Administration.Systems
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_adminManager.OnPermsChanged += OnAdminPermsChanged;
_playTime.SessionPlayTimeUpdated += OnSessionPlayTimeUpdated;
// Panic Bunker Settings
Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerCountDeadminnedAdmins, OnPanicBunkerCountDeadminnedAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnShowReasonChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinOverallHours, OnPanicBunkerMinOverallHoursChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
// Baby Jail Settings
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
@@ -188,6 +200,7 @@ namespace Content.Server.Administration.Systems
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_adminManager.OnPermsChanged -= OnAdminPermsChanged;
_playTime.SessionPlayTimeUpdated -= OnSessionPlayTimeUpdated;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
@@ -249,6 +262,17 @@ namespace Content.Server.Administration.Systems
SendPanicBunkerStatusAll();
}
private void OnBabyJailChanged(bool enabled)
{
BabyJail.Enabled = enabled;
_chat.SendAdminAlert(Loc.GetString(enabled
? "admin-ui-baby-jail-enabled-admin-alert"
: "admin-ui-baby-jail-disabled-admin-alert"
));
SendBabyJailStatusAll();
}
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
{
PanicBunker.DisableWithAdmins = enabled;
@@ -267,24 +291,42 @@ namespace Content.Server.Administration.Systems
UpdatePanicBunker();
}
private void OnShowReasonChanged(bool enabled)
private void OnPanicBunkerShowReasonChanged(bool enabled)
{
PanicBunker.ShowReason = enabled;
SendPanicBunkerStatusAll();
}
private void OnBabyJailShowReasonChanged(bool enabled)
{
BabyJail.ShowReason = enabled;
SendBabyJailStatusAll();
}
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
{
PanicBunker.MinAccountAgeHours = minutes / 60;
PanicBunker.MinAccountAgeMinutes = minutes;
SendPanicBunkerStatusAll();
}
private void OnPanicBunkerMinOverallHoursChanged(int hours)
private void OnBabyJailMaxAccountAgeChanged(int minutes)
{
PanicBunker.MinOverallHours = hours;
BabyJail.MaxAccountAgeMinutes = minutes;
SendBabyJailStatusAll();
}
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
{
PanicBunker.MinOverallMinutes = minutes;
SendPanicBunkerStatusAll();
}
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
{
BabyJail.MaxOverallMinutes = minutes;
SendBabyJailStatusAll();
}
private void UpdatePanicBunker()
{
var admins = PanicBunker.CountDeadminnedAdmins
@@ -326,6 +368,15 @@ namespace Content.Server.Administration.Systems
}
}
private void SendBabyJailStatusAll()
{
var ev = new BabyJailChangedEvent(BabyJail);
foreach (var admin in _adminManager.AllAdmins)
{
RaiseNetworkEvent(ev, admin);
}
}
/// <summary>
/// Erases a player from the round.
/// This removes them and any trace of them from the round, deleting their
@@ -396,5 +447,10 @@ namespace Content.Server.Administration.Systems
_gameTicker.SpawnObserver(player);
}
private void OnSessionPlayTimeUpdated(ICommonSession session)
{
UpdatePlayerList(session);
}
}
}

View File

@@ -131,59 +131,6 @@ namespace Content.Server.Administration.Systems
prayerVerb.Impact = LogImpact.Low;
args.Verbs.Add(prayerVerb);
// Freeze
var frozen = TryComp<AdminFrozenComponent>(args.Target, out var frozenComp);
var frozenAndMuted = frozenComp?.Muted ?? false;
if (!frozen)
{
args.Verbs.Add(new Verb
{
Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze.
Text = Loc.GetString("admin-verbs-freeze"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")),
Act = () =>
{
EnsureComp<AdminFrozenComponent>(args.Target);
},
Impact = LogImpact.Medium,
});
}
if (!frozenAndMuted)
{
// allow you to additionally mute someone when they are already frozen
args.Verbs.Add(new Verb
{
Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze.
Text = Loc.GetString("admin-verbs-freeze-and-mute"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")),
Act = () =>
{
_freeze.FreezeAndMute(args.Target);
},
Impact = LogImpact.Medium,
});
}
if (frozen)
{
args.Verbs.Add(new Verb
{
Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze.
Text = Loc.GetString("admin-verbs-unfreeze"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")),
Act = () =>
{
RemComp<AdminFrozenComponent>(args.Target);
},
Impact = LogImpact.Medium,
});
}
// Erase
args.Verbs.Add(new Verb
{
@@ -263,6 +210,60 @@ namespace Content.Server.Administration.Systems
});
}
// Freeze
var frozen = TryComp<AdminFrozenComponent>(args.Target, out var frozenComp);
var frozenAndMuted = frozenComp?.Muted ?? false;
if (!frozen)
{
args.Verbs.Add(new Verb
{
Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze.
Text = Loc.GetString("admin-verbs-freeze"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")),
Act = () =>
{
EnsureComp<AdminFrozenComponent>(args.Target);
},
Impact = LogImpact.Medium,
});
}
if (!frozenAndMuted)
{
// allow you to additionally mute someone when they are already frozen
args.Verbs.Add(new Verb
{
Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze.
Text = Loc.GetString("admin-verbs-freeze-and-mute"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")),
Act = () =>
{
_freeze.FreezeAndMute(args.Target);
},
Impact = LogImpact.Medium,
});
}
if (frozen)
{
args.Verbs.Add(new Verb
{
Priority = -1, // This is just so it doesn't change position in the menu between freeze/unfreeze.
Text = Loc.GetString("admin-verbs-unfreeze"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/snow.svg.192dpi.png")),
Act = () =>
{
RemComp<AdminFrozenComponent>(args.Target);
},
Impact = LogImpact.Medium,
});
}
// Admin Logs
if (_adminManager.HasAdminFlag(player, AdminFlags.Logs))
{

View File

@@ -182,7 +182,7 @@ public sealed class AmeNodeGroup : BaseNodeGroup
// Fuel is squared so more fuel vastly increases power and efficiency
// We divide by the number of cores so a larger AME is less efficient at the same fuel settings
// this results in all AMEs having the same efficiency at the same fuel-per-core setting
return 2000000f * fuel * fuel / cores;
return 20000f * fuel * fuel / cores;
}
public int GetTotalStability()

View File

@@ -264,7 +264,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// </summary>
public void MakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
{
var antagEnt = (EntityUid?) null;
EntityUid? antagEnt = null;
var isSpawner = false;
if (session != null)
@@ -285,17 +285,16 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
var getEntEv = new AntagSelectEntityEvent(session, ent);
RaiseLocalEvent(ent, ref getEntEv, true);
if (!getEntEv.Handled)
{
throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
}
antagEnt = getEntEv.Entity;
}
if (antagEnt is not { } player)
{
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
if (session != null)
ent.Comp.SelectedSessions.Remove(session);
return;
}
var getPosEv = new AntagSelectLocationEvent(session, ent);
RaiseLocalEvent(ent, ref getPosEv, true);
@@ -313,6 +312,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
{
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
if (session != null)
ent.Comp.SelectedSessions.Remove(session);
return;
}
@@ -374,6 +375,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// </summary>
public bool IsSessionValid(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, EntityUid? mind = null)
{
// TODO ROLE TIMERS
// Check if antag role requirements are met
if (session == null)
return true;

View File

@@ -10,21 +10,21 @@ public sealed partial class AtmosphereSystem
SubscribeLocalEvent<BreathToolComponent, ComponentShutdown>(OnBreathToolShutdown);
}
private void OnBreathToolShutdown(EntityUid uid, BreathToolComponent component, ComponentShutdown args)
private void OnBreathToolShutdown(Entity<BreathToolComponent> entity, ref ComponentShutdown args)
{
DisconnectInternals(component);
DisconnectInternals(entity);
}
public void DisconnectInternals(BreathToolComponent component)
public void DisconnectInternals(Entity<BreathToolComponent> entity)
{
var old = component.ConnectedInternalsEntity;
component.ConnectedInternalsEntity = null;
var old = entity.Comp.ConnectedInternalsEntity;
entity.Comp.ConnectedInternalsEntity = null;
if (TryComp<InternalsComponent>(old, out var internalsComponent))
{
_internals.DisconnectBreathTool((old.Value, internalsComponent));
_internals.DisconnectBreathTool((old.Value, internalsComponent), entity.Owner);
}
component.IsFunctional = false;
entity.Comp.IsFunctional = false;
}
}

View File

@@ -220,7 +220,7 @@ namespace Content.Server.Atmos.EntitySystems
public bool CanConnectToInternals(GasTankComponent component)
{
var internals = GetInternalsComponent(component, component.User);
return internals != null && internals.BreathToolEntity != null && !component.IsValveOpen;
return internals != null && internals.BreathTools.Count != 0 && !component.IsValveOpen;
}
public void ConnectToInternals(Entity<GasTankComponent> ent)

View File

@@ -13,7 +13,7 @@ namespace Content.Server.Body.Components
public EntityUid? GasTankEntity;
[ViewVariables]
public EntityUid? BreathToolEntity;
public HashSet<EntityUid> BreathTools { get; set; } = new();
/// <summary>
/// Toggle Internals delay when the target is not you.

View File

@@ -44,7 +44,7 @@ public sealed class InternalsSystem : EntitySystem
private void OnStartingGear(EntityUid uid, InternalsComponent component, ref StartingGearEquippedEvent args)
{
if (component.BreathToolEntity == null)
if (component.BreathTools.Count == 0)
return;
if (component.GasTankEntity != null)
@@ -111,7 +111,7 @@ public sealed class InternalsSystem : EntitySystem
}
// If they're not on then check if we have a mask to use
if (internals.BreathToolEntity is null)
if (internals.BreathTools.Count == 0)
{
_popupSystem.PopupEntity(Loc.GetString("internals-no-breath-tool"), uid, user);
return;
@@ -178,28 +178,24 @@ public sealed class InternalsSystem : EntitySystem
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
}
public void DisconnectBreathTool(Entity<InternalsComponent> ent)
public void DisconnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
{
var old = ent.Comp.BreathToolEntity;
ent.Comp.BreathToolEntity = null;
ent.Comp.BreathTools.Remove(toolEntity);
if (TryComp(old, out BreathToolComponent? breathTool))
{
_atmos.DisconnectInternals(breathTool);
if (TryComp(toolEntity, out BreathToolComponent? breathTool))
_atmos.DisconnectInternals((toolEntity, breathTool));
if (ent.Comp.BreathTools.Count == 0)
DisconnectTank(ent);
}
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
{
if (TryComp(ent.Comp.BreathToolEntity, out BreathToolComponent? tool))
{
_atmos.DisconnectInternals(tool);
}
if (!ent.Comp.BreathTools.Add(toolEntity))
return;
ent.Comp.BreathToolEntity = toolEntity;
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
@@ -217,7 +213,7 @@ public sealed class InternalsSystem : EntitySystem
public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
{
if (ent.Comp.BreathToolEntity is null)
if (ent.Comp.BreathTools.Count == 0)
return false;
if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
@@ -236,14 +232,14 @@ public sealed class InternalsSystem : EntitySystem
public bool AreInternalsWorking(InternalsComponent component)
{
return TryComp(component.BreathToolEntity, out BreathToolComponent? breathTool)
return TryComp(component.BreathTools.FirstOrNull(), out BreathToolComponent? breathTool)
&& breathTool.IsFunctional
&& HasComp<GasTankComponent>(component.GasTankEntity);
}
private short GetSeverity(InternalsComponent component)
{
if (component.BreathToolEntity is null || !AreInternalsWorking(component))
if (component.BreathTools.Count == 0 || !AreInternalsWorking(component))
return 2;
// If pressure in the tank is below low pressure threshold, flash warning on internals UI
@@ -266,7 +262,7 @@ public sealed class InternalsSystem : EntitySystem
// 3. in-hand tanks
// 4. pocket/belt tanks
if (!Resolve(user, ref user.Comp1, ref user.Comp2, ref user.Comp3))
if (!Resolve(user, ref user.Comp2, ref user.Comp3))
return null;
if (_inventory.TryGetSlotEntity(user, "back", out var backEntity, user.Comp2, user.Comp3) &&

View File

@@ -59,7 +59,7 @@ public sealed class LungSystem : EntitySystem
{
if (args.IsToggled || args.IsEquip)
{
_atmos.DisconnectInternals(ent.Comp);
_atmos.DisconnectInternals(ent);
}
else
{

View File

@@ -95,7 +95,7 @@ public sealed class RespiratorSystem : EntitySystem
if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown)
{
respirator.LastGaspEmoteTime = _gameTiming.CurTime;
_chat.TryEmoteWithChat(uid, respirator.GaspEmote, ignoreActionBlocker: true);
_chat.TryEmoteWithChat(uid, respirator.GaspEmote, ChatTransmitRange.HideChat, ignoreActionBlocker: true);
}
TakeSuffocationDamage((uid, respirator));

View File

@@ -124,6 +124,7 @@ public sealed partial class CargoSystem
("item", Loc.GetString(entry.Name)))}");
msg.PushNewline();
}
msg.AddMarkup(Loc.GetString("bounty-console-manifest-reward", ("reward", prototype.Reward)));
_paperSystem.SetContent(uid, msg.ToMarkup(), paper);
}

View File

@@ -4,6 +4,7 @@ using System.Text;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.Examine;
using Content.Server.GameTicking;
using Content.Server.Speech.Components;
using Content.Server.Speech.EntitySystems;
@@ -14,6 +15,7 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
@@ -60,6 +62,7 @@ public sealed partial class ChatSystem : SharedChatSystem
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
public const int VoiceRange = 10; // how far voice goes in world units
public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units
@@ -504,8 +507,7 @@ public sealed partial class ChatSystem : SharedChatSystem
if (data.Range <= WhisperClearRange)
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
//If listener is too far, they only hear fragments of the message
//Collisiongroup.Opaque is not ideal for this use. Preferably, there should be a check specifically with "Can Ent1 see Ent2" in mind
else if (_interactionSystem.InRangeUnobstructed(source, listener, WhisperMuffledRange, Shared.Physics.CollisionGroup.Opaque)) //Shared.Physics.CollisionGroup.Opaque
else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange))
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel);
//If listener is too far and has no line of sight, they can't identify the whisperer's identity
else

View File

@@ -199,9 +199,13 @@ namespace Content.Server.Communications
if (_emergency.EmergencyShuttleArrived || !_roundEndSystem.CanCallOrRecall())
return false;
// Ensure that we can communicate with the shuttle (either call or recall)
if (!comp.CanShuttle)
return false;
// Calling shuttle checks
if (_roundEndSystem.ExpectedCountdownEnd is null)
return comp.CanShuttle;
return true;
// Recalling shuttle checks
var recallThreshold = _cfg.GetCVar(CCVars.EmergencyRecallTurningPoint);

View File

@@ -13,6 +13,9 @@ using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Timing;
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Server.Connection
{
@@ -125,6 +128,10 @@ namespace Content.Server.Connection
}
}
/*
* TODO: Jesus H Christ what is this utter mess of a function
* TODO: Break this apart into is constituent steps.
*/
private async Task<(ConnectionDenyReason, string, List<ServerBanDef>? bansHit)?> ShouldDeny(
NetConnectingArgs e)
{
@@ -179,9 +186,9 @@ namespace Content.Server.Connection
("reason", Loc.GetString("panic-bunker-account-reason-account", ("minutes", minMinutesAge)))), null);
}
var minOverallHours = _cfg.GetCVar(CCVars.PanicBunkerMinOverallHours);
var minOverallMinutes = _cfg.GetCVar(CCVars.PanicBunkerMinOverallMinutes);
var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
var haveMinOverallTime = overallTime != null && overallTime.TimeSpent.TotalHours > minOverallHours;
var haveMinOverallTime = overallTime != null && overallTime.TimeSpent.TotalMinutes > minOverallMinutes;
// Use the custom reason if it exists & they don't have the minimum time
if (customReason != string.Empty && !haveMinOverallTime && !bypassAllowed)
@@ -193,7 +200,7 @@ namespace Content.Server.Connection
{
return (ConnectionDenyReason.Panic,
Loc.GetString("panic-bunker-account-denied-reason",
("reason", Loc.GetString("panic-bunker-account-reason-overall", ("hours", minOverallHours)))), null);
("reason", Loc.GetString("panic-bunker-account-reason-overall", ("minutes", minOverallMinutes)))), null);
}
if (!validAccountAge || !haveMinOverallTime && !bypassAllowed)
@@ -202,6 +209,14 @@ namespace Content.Server.Connection
}
}
if (_cfg.GetCVar(CCVars.BabyJailEnabled) && adminData == null)
{
var result = await IsInvalidConnectionDueToBabyJail(userId, e);
if (result.IsInvalid)
return (ConnectionDenyReason.BabyJail, result.Reason, null);
}
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
status == PlayerGameStatus.JoinedGame;
@@ -231,6 +246,57 @@ namespace Content.Server.Connection
return null;
}
private async Task<(bool IsInvalid, string Reason)> IsInvalidConnectionDueToBabyJail(NetUserId userId, NetConnectingArgs e)
{
// If you're whitelisted then bypass this whole thing
if (await _db.GetWhitelistStatusAsync(userId))
return (false, "");
// Initial cvar retrieval
var showReason = _cfg.GetCVar(CCVars.BabyJailShowReason);
var reason = _cfg.GetCVar(CCVars.BabyJailCustomReason);
var maxAccountAgeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge);
var maxPlaytimeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
// Wait some time to lookup data
var record = await _dbManager.GetPlayerRecordByUserId(userId);
var isAccountAgeInvalid = record == null || record.FirstSeenTime.CompareTo(DateTimeOffset.Now - TimeSpan.FromMinutes(maxAccountAgeMinutes)) <= 0;
if (isAccountAgeInvalid && showReason)
{
var locAccountReason = reason != string.Empty
? reason
: Loc.GetString("baby-jail-account-denied-reason",
("reason",
Loc.GetString(
"baby-jail-account-reason-account",
("minutes", maxAccountAgeMinutes))));
return (true, locAccountReason);
}
var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
var isTotalPlaytimeInvalid = overallTime == null || overallTime.TimeSpent.TotalMinutes >= maxPlaytimeMinutes;
if (isTotalPlaytimeInvalid && showReason)
{
var locPlaytimeReason = reason != string.Empty
? reason
: Loc.GetString("baby-jail-account-denied-reason",
("reason",
Loc.GetString(
"baby-jail-account-reason-overall",
("minutes", maxPlaytimeMinutes))));
return (true, locPlaytimeReason);
}
if (!showReason && isTotalPlaytimeInvalid || isAccountAgeInvalid)
return (true, Loc.GetString("baby-jail-account-denied"));
return (false, "");
}
private bool HasTemporaryBypass(NetUserId user)
{
return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime;

Some files were not shown because too many files have changed in this diff Show More