diff --git a/.github/workflows/build-map-renderer.yml b/.github/workflows/build-map-renderer.yml
index 7e9e203f26..a20023a057 100644
--- a/.github/workflows/build-map-renderer.yml
+++ b/.github/workflows/build-map-renderer.yml
@@ -8,6 +8,10 @@ on:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name != 'merge_group' }}
+
jobs:
build:
if: github.actor != 'PJBot' && github.event.pull_request.draft == false
diff --git a/.github/workflows/build-test-debug.yml b/.github/workflows/build-test-debug.yml
index ebd00d2f3c..a01d09f60a 100644
--- a/.github/workflows/build-test-debug.yml
+++ b/.github/workflows/build-test-debug.yml
@@ -8,6 +8,10 @@ on:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name != 'merge_group' }}
+
jobs:
build:
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
diff --git a/.github/workflows/check-crlf.yml b/.github/workflows/check-crlf.yml
index 552ca4b097..5e4b0d0341 100644
--- a/.github/workflows/check-crlf.yml
+++ b/.github/workflows/check-crlf.yml
@@ -4,6 +4,10 @@ on:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true # We only care about latest.
+
jobs:
build:
name: CRLF Check
diff --git a/.github/workflows/no-submodule-update.yml b/.github/workflows/no-submodule-update.yml
index f2a9d72e37..10354eaed2 100644
--- a/.github/workflows/no-submodule-update.yml
+++ b/.github/workflows/no-submodule-update.yml
@@ -2,13 +2,36 @@ name: No submodule update checker
on:
pull_request:
+ types: [ opened, reopened, synchronize, ready_for_review ]
paths:
- 'RobustToolbox'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
jobs:
this_aint_right:
name: Submodule update in pr found
+ if: github.actor != 'PJBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- - name: Fail
- run: exit 1
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - name: Check changed files
+ shell: pwsh
+ run: |
+ $changedFiles = @(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")
+
+ if ($changedFiles.Count -eq 1 -and $changedFiles[0] -eq "RobustToolbox") {
+ Write-Host "Only RobustToolbox was changed; allowing standalone submodule update."
+ exit 0
+ }
+
+ Write-Host "RobustToolbox was updated alongside other files:"
+ foreach ($file in $changedFiles) {
+ Write-Host " - $file"
+ }
+ exit 1
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 1a9b417abf..35f385f625 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -79,6 +79,11 @@ jobs:
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
+ - name: Publish changelog (Discord)
+ run: Tools/actions_changelogs_since_last_run.py
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
# - name: Publish changelog (RSS)
# continue-on-error: true
diff --git a/.github/workflows/rsi-diff.yml b/.github/workflows/rsi-diff.yml
index 8ae6105ad3..079a476fe4 100644
--- a/.github/workflows/rsi-diff.yml
+++ b/.github/workflows/rsi-diff.yml
@@ -2,9 +2,14 @@ name: Diff RSIs
on:
pull_request_target:
+ types: [ opened, reopened, synchronize, ready_for_review ]
paths:
- '**.rsi/**.png'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
jobs:
diff:
name: Diff
diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml
index 28fd5036c9..4e161359fa 100644
--- a/.github/workflows/test-packaging.yml
+++ b/.github/workflows/test-packaging.yml
@@ -26,6 +26,10 @@ on:
- 'RobustToolbox'
- 'RobustToolbox/**'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name != 'merge_group' }}
+
jobs:
build:
name: Test Packaging
diff --git a/.github/workflows/validate-rgas.yml b/.github/workflows/validate-rgas.yml
index 66be781c9c..05caaa6a89 100644
--- a/.github/workflows/validate-rgas.yml
+++ b/.github/workflows/validate-rgas.yml
@@ -6,6 +6,10 @@ on:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name != 'merge_group' }}
+
jobs:
yaml-schema-validation:
name: YAML RGA schema validator
diff --git a/.github/workflows/validate-rsis.yml b/.github/workflows/validate-rsis.yml
index 70491a0ee2..7cb82a6e18 100644
--- a/.github/workflows/validate-rsis.yml
+++ b/.github/workflows/validate-rsis.yml
@@ -5,12 +5,18 @@ on:
branches: [ master, staging, stable ]
merge_group:
pull_request:
+ types: [ opened, reopened, synchronize, ready_for_review ]
paths:
- '**.rsi/**'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name != 'merge_group' }}
+
jobs:
validate_rsis:
name: Validate RSIs
+ if: github.actor != 'PJBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
diff --git a/.github/workflows/validate_mapfiles.yml b/.github/workflows/validate_mapfiles.yml
index 82004991ba..8458f4bfa7 100644
--- a/.github/workflows/validate_mapfiles.yml
+++ b/.github/workflows/validate_mapfiles.yml
@@ -6,6 +6,10 @@ on:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name != 'merge_group' }}
+
jobs:
yaml-schema-validation:
name: YAML map schema validator
diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml
index 15c17499ef..5b6a796193 100644
--- a/.github/workflows/yaml-linter.yml
+++ b/.github/workflows/yaml-linter.yml
@@ -7,6 +7,10 @@ on:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name != 'merge_group' }}
+
jobs:
build:
name: YAML Linter
diff --git a/.gitignore b/.gitignore
index fa68594da8..1ed759db39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -303,6 +303,10 @@ ehthumbs.db
# Merge driver stuff
Content.Tools/test/out.yml
+# Local changelog Discord debug files.
+Resources/Changelog/Old.yml
+Resources/Changelog/DiscordDebug.md
+
# Windows
desktop.ini
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 7f4efc8c9a..96838b16d6 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -13,13 +13,35 @@
"/consoleloggerparameters:'ForceNoAlign;NoSummary'" // Do not generate summary otherwise it leads to duplicate errors in Problems panel
],
"group": {
- "kind": "build",
- "isDefault": true
+ "kind": "build"
},
"presentation": {
"reveal": "silent"
},
- "problemMatcher": "$msCompile"
+ "problemMatcher": "$msCompile",
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "label": "build (C# Dev Kit)",
+ "detail": "needs C# Dev Kit extention. use the (build) task instead if you dont have the extention.",
+ "type": "dotnet",
+ "task": "build",
+ "icon": {
+ "id": "build",
+ "color": "terminal.ansiBlue"
+ },
+ "group": {
+ "kind": "build"
+ },
+ "presentation": {
+ "reveal": "silent"
+ },
+ "problemMatcher": "$msCompile",
+ "runOptions": {
+ "instanceLimit": 1
+ }
},
{
"label": "build-yaml-linter",
diff --git a/Content.Client/Access/AgentIdCardSystem.cs b/Content.Client/Access/AgentIdCardSystem.cs
new file mode 100644
index 0000000000..21b73a9841
--- /dev/null
+++ b/Content.Client/Access/AgentIdCardSystem.cs
@@ -0,0 +1,18 @@
+using Content.Shared.Access.Systems;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Access;
+
+///
+public sealed partial class AgentIdCardSystem : SharedAgentIdCardSystem
+{
+ [Dependency] private UserInterfaceSystem _ui = default!;
+
+ public override void UpdateUi(EntityUid entity)
+ {
+ if (_ui.TryGetOpenUi(entity, AgentIDCardUiKey.Key, out var bui))
+ {
+ bui.Update();
+ }
+ }
+}
diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
index 050756fcd1..69868abbdd 100644
--- a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
+++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
@@ -1,61 +1,64 @@
+using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.StatusIcon;
-using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
-namespace Content.Client.Access.UI
+namespace Content.Client.Access.UI;
+
+///
+/// Initializes a and updates it when new server messages are received.
+///
+public sealed class AgentIDCardBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
- ///
- /// Initializes a and updates it when new server messages are received.
- ///
- public sealed class AgentIDCardBoundUserInterface : BoundUserInterface
+ private AgentIDCardWindow? _window;
+
+ protected override void Open()
{
- private AgentIDCardWindow? _window;
+ base.Open();
- public AgentIDCardBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
- {
- }
+ if (!EntMan.TryGetComponent(Owner, out AgentIDCardComponent? agent))
+ return;
- protected override void Open()
- {
- base.Open();
+ _window = this.CreateWindow();
- _window = this.CreateWindow();
+ _window.OnNameChanged += OnNameChanged;
+ _window.OnJobChanged += OnJobChanged;
+ _window.OnJobIconChanged += OnJobIconChanged;
- _window.OnNameChanged += OnNameChanged;
- _window.OnJobChanged += OnJobChanged;
- _window.OnJobIconChanged += OnJobIconChanged;
- }
+ ProtoId currentIcon = default;
+ if (EntMan.TryGetComponent(Owner, out var card))
+ currentIcon = card.JobIcon;
- private void OnNameChanged(string newName)
- {
- SendMessage(new AgentIDCardNameChangedMessage(newName));
- }
+ _window.SetAllowedIcons(agent.IconGroups, currentIcon);
+ Update();
+ }
- private void OnJobChanged(string newJob)
- {
- SendMessage(new AgentIDCardJobChangedMessage(newJob));
- }
+ public override void Update()
+ {
+ base.Update();
- public void OnJobIconChanged(ProtoId newJobIconId)
- {
- SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
- }
+ if (_window == null)
+ return;
- ///
- /// Update the UI state based on server-sent info
- ///
- ///
- protected override void UpdateState(BoundUserInterfaceState state)
- {
- base.UpdateState(state);
- if (_window == null || state is not AgentIDCardBoundUserInterfaceState cast)
- return;
+ if (!EntMan.TryGetComponent(Owner, out var card))
+ return;
- _window.SetCurrentName(cast.CurrentName);
- _window.SetCurrentJob(cast.CurrentJob);
- _window.SetAllowedIcons(cast.CurrentJobIconId);
- }
+ _window.Update(card);
+ }
+
+ private void OnNameChanged(string newName)
+ {
+ SendPredictedMessage(new AgentIDCardNameChangedMessage(newName));
+ }
+
+ private void OnJobChanged(string newJob)
+ {
+ SendPredictedMessage(new AgentIDCardJobChangedMessage(newJob));
+ }
+
+ private void OnJobIconChanged(ProtoId newJobIconId)
+ {
+ SendPredictedMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
}
}
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml b/Content.Client/Access/UI/AgentIDCardWindow.xaml
index 7d091e4e16..6030851114 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml
@@ -1,14 +1,71 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
index 6a1255d2d8..d4b3824118 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -1,96 +1,183 @@
using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Access.Components;
+using Content.Shared.CCVar;
using Content.Shared.StatusIcon;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using System.Numerics;
-using System.Linq;
-namespace Content.Client.Access.UI
+namespace Content.Client.Access.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class AgentIDCardWindow : FancyWindow
{
- [GenerateTypedNameReferences]
- public sealed partial class AgentIDCardWindow : DefaultWindow
+ [Dependency] private IConfigurationManager _cfgManager = default!;
+ [Dependency] private IEntitySystemManager _entitySystem = default!;
+ [Dependency] private IPrototypeManager _prototypeManager = default!;
+ private readonly SpriteSystem _spriteSystem;
+
+ public event Action? OnNameChanged;
+ public event Action? OnJobChanged;
+
+ public event Action>? OnJobIconChanged;
+
+ public AgentIDCardWindow()
{
- [Dependency] private IPrototypeManager _prototypeManager = default!;
- [Dependency] private IEntitySystemManager _entitySystem = default!;
- private readonly SpriteSystem _spriteSystem;
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _spriteSystem = _entitySystem.GetEntitySystem();
- private const int JobIconColumnCount = 10;
+ NameLineEdit.OnTextEntered += e => CommitName(e.Text);
+ NameLineEdit.OnFocusExit += e => CommitName(e.Text);
- public event Action? OnNameChanged;
- public event Action? OnJobChanged;
+ JobLineEdit.OnTextEntered += e => CommitJob(e.Text);
+ JobLineEdit.OnFocusExit += e => CommitJob(e.Text);
- public event Action>? OnJobIconChanged;
+ NameLineEdit.IsValid = s => s.Length <= _cfgManager.GetCVar(CCVars.MaxNameLength);
+ JobLineEdit.IsValid = s => s.Length <= _cfgManager.GetCVar(CCVars.MaxIdJobLength);
- public AgentIDCardWindow()
+ AgentTabs.SetTabTitle(0, Loc.GetString("agent-id-ui-tab-settings"));
+ AgentTabs.SetTabTitle(1, Loc.GetString("agent-id-ui-tab-job-icons"));
+ }
+
+ ///
+ /// Creates the job icons tab.
+ ///
+ public void SetAllowedIcons(IReadOnlyList> jobGroups, ProtoId currentIcon)
+ {
+ JobGroupGrid.DisposeAllChildren();
+ IconGrid.DisposeAllChildren();
+
+ var jobGroupButtonGroup = new ButtonGroup();
+ ContainerButton? firstGroupButton = null;
+ IReadOnlyList>? firstGroupIcons = null;
+ ContainerButton? matchingGroupButton = null;
+ IReadOnlyList>? matchingGroupIcons = null;
+
+ foreach (var group in jobGroups)
{
- RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
- _spriteSystem = _entitySystem.GetEntitySystem();
+ if (!_prototypeManager.TryIndex(group, out var groupProto))
+ continue;
- NameLineEdit.OnTextEntered += e => OnNameChanged?.Invoke(e.Text);
- NameLineEdit.OnFocusExit += e => OnNameChanged?.Invoke(e.Text);
+ var groupButton = CreateGroupButton(groupProto, jobGroupButtonGroup);
+ groupButton.OnPressed += _ => SetJobIcons(groupProto.Icons);
- JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text);
- JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
- }
+ JobGroupGrid.AddChild(groupButton);
- public void SetAllowedIcons(string currentJobIconId)
- {
- IconGrid.RemoveAllChildren();
+ firstGroupButton ??= groupButton;
+ firstGroupIcons ??= groupProto.Icons;
- var jobIconButtonGroup = new ButtonGroup();
- var i = 0;
- var icons = _prototypeManager.EnumeratePrototypes().Where(icon => icon.AllowSelection).ToList();
- icons.Sort((x, y) => string.Compare(x.LocalizedJobName, y.LocalizedJobName, StringComparison.CurrentCulture));
- foreach (var jobIcon in icons)
+ // Prefer the group that actually contains the card's current icon so
+ // the opened window shows the relevant set rather than always the first.
+ if (matchingGroupButton == null && groupProto.Icons.Contains(currentIcon))
{
- String styleBase = StyleClass.ButtonOpenBoth;
- var modulo = i % JobIconColumnCount;
- if (modulo == 0)
- styleBase = StyleClass.ButtonOpenRight;
- else if (modulo == JobIconColumnCount - 1)
- styleBase = StyleClass.ButtonOpenLeft;
-
- // Generate buttons
- var jobIconButton = new Button
- {
- Access = AccessLevel.Public,
- StyleClasses = { styleBase },
- MaxSize = new Vector2(42, 28),
- Group = jobIconButtonGroup,
- Pressed = currentJobIconId == jobIcon.ID,
- ToolTip = jobIcon.LocalizedJobName
- };
-
- // Generate buttons textures
- var jobIconTexture = new TextureRect
- {
- Texture = _spriteSystem.Frame0(jobIcon.Icon),
- TextureScale = new Vector2(2.5f, 2.5f),
- Stretch = TextureRect.StretchMode.KeepCentered,
- };
-
- jobIconButton.AddChild(jobIconTexture);
- jobIconButton.OnPressed += _ => OnJobIconChanged?.Invoke(jobIcon.ID);
- IconGrid.AddChild(jobIconButton);
-
- i++;
+ matchingGroupButton = groupButton;
+ matchingGroupIcons = groupProto.Icons;
}
}
- public void SetCurrentName(string name)
- {
- NameLineEdit.Text = name;
- }
+ var selectedButton = matchingGroupButton ?? firstGroupButton;
+ var selectedIcons = matchingGroupIcons ?? firstGroupIcons;
- public void SetCurrentJob(string job)
+ if (selectedButton == null || selectedIcons == null)
+ return;
+
+ selectedButton.Pressed = true;
+ SetJobIcons(selectedIcons);
+ }
+
+ ///
+ /// Creates the job icon subgroup within the job icons tab.
+ ///
+ private void SetJobIcons(IReadOnlyList> jobIcons)
+ {
+ IconGrid.DisposeAllChildren();
+
+ foreach (var icon in jobIcons)
{
- JobLineEdit.Text = job;
+ if (!_prototypeManager.TryIndex(icon, out var iconProto))
+ continue;
+
+ var texture = _spriteSystem.Frame0(iconProto.Icon);
+
+ // Create new button
+ var jobIconButton = new TextureButton
+ {
+ Access = AccessLevel.Public,
+ ToolTip = Loc.GetString(iconProto.JobName),
+ TextureNormal = texture,
+ SetSize = new Vector2(32, 32),
+ };
+
+ // Finish button and add to UI
+ jobIconButton.OnPressed += _ => OnJobIconChanged?.Invoke(iconProto.ID);
+
+ IconGrid.AddChild(jobIconButton);
}
}
+
+ public void Update(IdCardComponent card)
+ {
+ var name = card.FullName ?? string.Empty;
+ var job = card.LocalizedJobTitle ?? string.Empty;
+
+ NameLineEdit.Text = name;
+ CurrentName.Text = name;
+ JobLineEdit.Text = job;
+ CurrentJob.Text = job;
+
+ var jobIconProto = _prototypeManager.Index(card.JobIcon);
+ CurrentJobIcon.Texture = _spriteSystem.Frame0(jobIconProto.Icon);
+ }
+
+ private ContainerButton CreateGroupButton(JobIconGroupPrototype groupProto, ButtonGroup buttonGroup)
+ {
+ var button = new ContainerButton
+ {
+ Access = AccessLevel.Public,
+ Group = buttonGroup,
+ ToggleMode = true,
+ StyleClasses = { StyleClass.ButtonOpenBoth },
+ Margin = new Thickness(8, 2)
+ };
+
+ button.AddChild(new BoxContainer
+ {
+ SeparationOverride = 8,
+ Children =
+ {
+ new TextureRect
+ {
+ Texture = _spriteSystem.Frame0(groupProto.Sprite),
+ Stretch = TextureRect.StretchMode.KeepAspectCentered,
+ SetSize = new Vector2(32, 32),
+ VerticalAlignment = VAlignment.Center,
+ },
+ new Label
+ {
+ Text = Loc.GetString(groupProto.GroupName),
+ HorizontalExpand = true,
+ VerticalAlignment = VAlignment.Center,
+ },
+ },
+ });
+
+ return button;
+ }
+
+ private void CommitJob(string job)
+ {
+ OnJobChanged?.Invoke(job);
+ }
+
+ private void CommitName(string name)
+ {
+ OnNameChanged?.Invoke(name);
+ }
}
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
index fdaa838201..1c67a6c581 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
@@ -17,7 +17,7 @@
-
+
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
index 369af1d53f..23e1a81037 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
@@ -156,11 +156,10 @@ namespace Content.Client.Administration.UI.Bwoink
return bch.LastMessage.CompareTo(ach.LastMessage);
};
-
- Bans.OnPressed += _ =>
+ PlayerLogs.OnPressed += _ =>
{
if (_currentPlayer is not null)
- _console.ExecuteCommand($"banlist \"{_currentPlayer.SessionId}\"");
+ _console.ExecuteCommand($"adminlogs \"{_currentPlayer.SessionId}\"");
};
Notes.OnPressed += _ =>
@@ -228,8 +227,8 @@ namespace Content.Client.Administration.UI.Bwoink
{
var disabled = _currentPlayer == null;
- Bans.Visible = _adminManager.HasFlag(AdminFlags.Ban);
- Bans.Disabled = !Bans.Visible || disabled;
+ PlayerLogs.Visible = _adminManager.HasFlag(AdminFlags.Logs);
+ PlayerLogs.Disabled = !PlayerLogs.Visible || disabled;
Notes.Visible = _adminManager.HasFlag(AdminFlags.ViewNotes);
Notes.Disabled = !Notes.Visible || disabled;
diff --git a/Content.Client/Administration/UI/EventLog/AdminEventLogEui.cs b/Content.Client/Administration/UI/EventLog/AdminEventLogEui.cs
new file mode 100644
index 0000000000..1691519d24
--- /dev/null
+++ b/Content.Client/Administration/UI/EventLog/AdminEventLogEui.cs
@@ -0,0 +1,63 @@
+using Content.Client.Eui;
+using Content.Shared.Administration.AdminEventLog;
+using Content.Shared.Eui;
+using JetBrains.Annotations;
+using Robust.Client.Player;
+using Robust.Shared.Utility;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+
+namespace Content.Client.Administration.UI.EventLog;
+
+[UsedImplicitly]
+public sealed partial class AdminEventLogEui : BaseEui
+{
+ [Dependency] private IPlayerManager _playerManager = default!;
+
+ private AdminEventLogWindow EventLogWindow { get; }
+
+ public AdminEventLogEui()
+ {
+ EventLogWindow = new AdminEventLogWindow();
+ EventLogWindow.SendEventLog.OnPressed += OnSendEventLogPressed;
+ }
+
+ public override void Opened()
+ {
+ base.Opened();
+
+ EventLogWindow.OpenCentered();
+ }
+
+ public override void Closed()
+ {
+ base.Closed();
+
+ EventLogWindow.Close();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ var s = (AdminEventLogEuiState)state;
+
+ EventLogWindow.SetCurrentRound(s.RoundId);
+ EventLogWindow.SetRoundSpinBox(s.RoundId);
+ }
+
+ private void OnSendEventLogPressed(ButtonEventArgs args)
+ {
+ var eventText = Rope.Collapse(EventLogWindow.EventTextEdit.TextRope).TrimStart();
+
+ if (string.IsNullOrEmpty(eventText))
+ return;
+
+ var message = new AdminEventLogEuiMsg(
+ EventLogWindow.RoundSpinBox.Value,
+ _playerManager.LocalSession!.Name,
+ eventText);
+
+ SendMessage(message);
+
+ // close window to avoid admin accidentally spamming event logs
+ EventLogWindow.Close();
+ }
+}
diff --git a/Content.Client/Administration/UI/EventLog/AdminEventLogWindow.xaml b/Content.Client/Administration/UI/EventLog/AdminEventLogWindow.xaml
new file mode 100644
index 0000000000..d0e0d6e1c7
--- /dev/null
+++ b/Content.Client/Administration/UI/EventLog/AdminEventLogWindow.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/EventLog/AdminEventLogWindow.xaml.cs b/Content.Client/Administration/UI/EventLog/AdminEventLogWindow.xaml.cs
new file mode 100644
index 0000000000..b5d939c86f
--- /dev/null
+++ b/Content.Client/Administration/UI/EventLog/AdminEventLogWindow.xaml.cs
@@ -0,0 +1,58 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+
+namespace Content.Client.Administration.UI.EventLog;
+
+[GenerateTypedNameReferences]
+public sealed partial class AdminEventLogWindow : FancyWindow
+{
+ [Dependency] private ILocalizationManager _localization = default!;
+
+ public AdminEventLogWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ RoundSpinBox.IsValid = i => i > 0 && i <= CurrentRound;
+ RoundSpinBox.ValueChanged += RoundSpinBoxChanged;
+ RoundSpinBox.InitDefaultButtons();
+
+ EventTextEdit.Placeholder = new Rope.Leaf(_localization.GetString("admin-event-description-placeholder"));
+
+ ResetRoundButton.OnPressed += ResetRoundPressed;
+ }
+
+ private int CurrentRound { get; set; }
+
+ public void SetCurrentRound(int round)
+ {
+ CurrentRound = round;
+ ResetRoundButton.Text = Loc.GetString("admin-event-round-reset-with-id", ("id", round));
+ UpdateResetButton();
+ }
+
+ public void SetRoundSpinBox(int round)
+ {
+ RoundSpinBox.Value = round;
+ UpdateResetButton();
+ }
+
+ private void RoundSpinBoxChanged(ValueChangedEventArgs args)
+ {
+ UpdateResetButton();
+ }
+
+ private void ResetRoundPressed(ButtonEventArgs args)
+ {
+ RoundSpinBox.Value = CurrentRound;
+ }
+
+ private void UpdateResetButton()
+ {
+ ResetRoundButton.Disabled = RoundSpinBox.Value == CurrentRound;
+ }
+}
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
index 7f779dd2f8..0a49ab127d 100644
--- a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
+++ b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
@@ -63,6 +63,8 @@ public sealed partial class AdminLogsControl : Control
public HashSet SelectedImpacts { get; } = new();
+ private bool firstTimeOpened = true;
+
public void SetCurrentRound(int round)
{
CurrentRound = round;
@@ -184,6 +186,22 @@ public sealed partial class AdminLogsControl : Control
UpdateLogs();
}
+ public void SelectPlayers(List players)
+ {
+ SelectedPlayers.Clear();
+ SelectedPlayers.UnionWith(players);
+
+ foreach (var control in PlayersContainer.Children)
+ {
+ if (control is not AdminLogPlayerButton player)
+ continue;
+
+ player.Pressed = SelectedPlayers.Contains(player.Id);
+ }
+
+ UpdateLogs();
+ }
+
public void SetTypesSelection(HashSet selectedTypes, bool invert = false)
{
SelectedTypes.Clear();
@@ -420,16 +438,14 @@ public sealed partial class AdminLogsControl : Control
public void SetPlayers(Dictionary players)
{
var buttons = new SortedSet(_adminLogPlayerButtonComparer);
- var allSelected = true;
+ // we retrieve everything if we open this window for the first time and the selected player list is empty
+ var allSelected = firstTimeOpened && SelectedPlayers.Count == 0;
foreach (var control in PlayersContainer.Children.ToArray())
{
if (control is not AdminLogPlayerButton player)
continue;
- if (!SelectedPlayers.Contains(player.Id))
- allSelected = false;
-
if (!players.Remove(player.Id))
continue;
@@ -457,6 +473,8 @@ public sealed partial class AdminLogsControl : Control
foreach (var player in buttons)
{
PlayersContainer.AddChild(player);
+
+ player.Pressed = SelectedPlayers.Contains(player.Id);
}
UpdateLogs();
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsEui.cs b/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
index d8323dcd5c..5a3976d3f1 100644
--- a/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
+++ b/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
@@ -232,6 +232,11 @@ public sealed partial class AdminLogsEui : BaseEui
if (setLogFilter.Search != null)
LogsControl.LogSearch.SetText(setLogFilter.Search);
+ if (setLogFilter.Players != null)
+ {
+ LogsControl.SelectPlayers(setLogFilter.Players);
+ }
+
if (setLogFilter.Types != null)
LogsControl.SetTypesSelection(setLogFilter.Types, setLogFilter.InvertTypes);
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
index 783d4c6106..9dc0d38ad3 100644
--- a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
@@ -12,6 +12,7 @@
+
diff --git a/Content.Client/Arcade/SpaceVillainArcadeMenu.cs b/Content.Client/Arcade/SpaceVillainArcadeMenu.cs
deleted file mode 100644
index 3d25d2208d..0000000000
--- a/Content.Client/Arcade/SpaceVillainArcadeMenu.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-using System.Numerics;
-using Content.Client.Arcade.UI;
-using Content.Shared.Arcade;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-
-namespace Content.Client.Arcade
-{
- public sealed class SpaceVillainArcadeMenu : DefaultWindow
- {
- private readonly Label _enemyNameLabel;
- private readonly Label _playerInfoLabel;
- private readonly Label _enemyInfoLabel;
- private readonly Label _playerActionLabel;
- private readonly Label _enemyActionLabel;
-
- private readonly Button[] _gameButtons = new Button[3]; //used to disable/enable all game buttons
-
- public event Action? OnPlayerAction;
-
- public SpaceVillainArcadeMenu()
- {
- MinSize = SetSize = new Vector2(300, 225);
- Title = Loc.GetString("spacevillain-menu-title");
-
- var grid = new GridContainer { Columns = 1 };
-
- var infoGrid = new GridContainer { Columns = 3 };
- infoGrid.AddChild(new Label { Text = Loc.GetString("spacevillain-menu-label-player"), Align = Label.AlignMode.Center });
- infoGrid.AddChild(new Label { Text = "|", Align = Label.AlignMode.Center });
- _enemyNameLabel = new Label { Align = Label.AlignMode.Center };
- infoGrid.AddChild(_enemyNameLabel);
-
- _playerInfoLabel = new Label { Align = Label.AlignMode.Center };
- infoGrid.AddChild(_playerInfoLabel);
- infoGrid.AddChild(new Label { Text = "|", Align = Label.AlignMode.Center });
- _enemyInfoLabel = new Label { Align = Label.AlignMode.Center };
- infoGrid.AddChild(_enemyInfoLabel);
- var centerContainer = new CenterContainer();
- centerContainer.AddChild(infoGrid);
- grid.AddChild(centerContainer);
-
- _playerActionLabel = new Label { Align = Label.AlignMode.Center };
- grid.AddChild(_playerActionLabel);
-
- _enemyActionLabel = new Label { Align = Label.AlignMode.Center };
- grid.AddChild(_enemyActionLabel);
-
- var buttonGrid = new GridContainer { Columns = 3 };
- _gameButtons[0] = new Button()
- {
- Text = Loc.GetString("spacevillain-menu-button-attack")
- };
-
- _gameButtons[0].OnPressed +=
- _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Attack);
- buttonGrid.AddChild(_gameButtons[0]);
-
- _gameButtons[1] = new Button()
- {
- Text = Loc.GetString("spacevillain-menu-button-heal")
- };
-
- _gameButtons[1].OnPressed +=
- _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Heal);
- buttonGrid.AddChild(_gameButtons[1]);
-
- _gameButtons[2] = new Button()
- {
- Text = Loc.GetString("spacevillain-menu-button-recharge")
- };
-
- _gameButtons[2].OnPressed +=
- _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Recharge);
- buttonGrid.AddChild(_gameButtons[2]);
-
- centerContainer = new CenterContainer();
- centerContainer.AddChild(buttonGrid);
- grid.AddChild(centerContainer);
-
- var newGame = new Button()
- {
- Text = Loc.GetString("spacevillain-menu-button-new-game")
- };
-
- newGame.OnPressed += _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.NewGame);
- grid.AddChild(newGame);
-
- ContentsContainer.AddChild(grid);
- }
-
- private void UpdateMetadata(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage message)
- {
- Title = message.GameTitle;
- _enemyNameLabel.Text = message.EnemyName;
-
- foreach (var gameButton in _gameButtons)
- {
- gameButton.Disabled = message.ButtonsDisabled;
- }
- }
-
- public void UpdateInfo(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeDataUpdateMessage message)
- {
- if (message is SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage metaMessage)
- UpdateMetadata(metaMessage);
-
- _playerInfoLabel.Text = $"HP: {message.PlayerHP} MP: {message.PlayerMP}";
- _enemyInfoLabel.Text = $"HP: {message.EnemyHP} MP: {message.EnemyMP}";
- _playerActionLabel.Text = message.PlayerActionMessage;
- _enemyActionLabel.Text = message.EnemyActionMessage;
- }
- }
-}
diff --git a/Content.Client/Arcade/SpaceVillainArcadeMenu.xaml b/Content.Client/Arcade/SpaceVillainArcadeMenu.xaml
new file mode 100644
index 0000000000..bc40fdb1c9
--- /dev/null
+++ b/Content.Client/Arcade/SpaceVillainArcadeMenu.xaml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Arcade/SpaceVillainArcadeMenu.xaml.cs b/Content.Client/Arcade/SpaceVillainArcadeMenu.xaml.cs
new file mode 100644
index 0000000000..3bfd31826b
--- /dev/null
+++ b/Content.Client/Arcade/SpaceVillainArcadeMenu.xaml.cs
@@ -0,0 +1,45 @@
+using Content.Shared.Arcade;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Arcade;
+
+[GenerateTypedNameReferences]
+public sealed partial class SpaceVillainArcadeMenu : DefaultWindow
+{
+ public event Action? OnPlayerAction;
+
+ public SpaceVillainArcadeMenu()
+ {
+ RobustXamlLoader.Load(this);
+ AttackButton.OnPressed += _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Attack);
+ HealButton.OnPressed += _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Heal);
+ RechargeButton.OnPressed += _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.Recharge);
+ NewGameButton.OnPressed += _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.NewGame);
+ }
+
+ private void UpdateMetadata(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage message)
+ {
+ Title = message.GameTitle;
+ EnemyNameLabel.Text = message.EnemyName;
+
+ AttackButton.Disabled = message.ButtonsDisabled;
+ HealButton.Disabled = message.ButtonsDisabled;
+ RechargeButton.Disabled = message.ButtonsDisabled;
+ NewGameButton.Disabled = message.ButtonsDisabled;
+ }
+
+ public void UpdateInfo(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeDataUpdateMessage message)
+ {
+ if (message is SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage metaMessage)
+ UpdateMetadata(metaMessage);
+
+ PlayerHpLabel.Text = Loc.GetString("spacevillain-menu-label-health", ("amount", message.PlayerHP));
+ PlayerMpLabel.Text = Loc.GetString("spacevillain-menu-label-magic", ("amount", message.PlayerMP));
+ EnemyHpLabel.Text = Loc.GetString("spacevillain-menu-label-health", ("amount", message.EnemyHP));
+ EnemyMpLabel.Text = Loc.GetString("spacevillain-menu-label-magic", ("amount", message.EnemyMP));
+ PlayerActionLabel.Text = message.PlayerActionMessage;
+ EnemyActionLabel.Text = message.EnemyActionMessage;
+ }
+}
diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
index f7660b2ec8..3d97cfe24a 100644
--- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
+++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
@@ -2,8 +2,8 @@ using System.Linq;
using Content.Client.Gameplay;
using Content.Shared.Audio;
using Content.Shared.CCVar;
+using Content.Shared.EntityConditions;
using Content.Shared.GameTicking;
-using Content.Shared.Random.Rules;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Audio;
@@ -25,11 +25,11 @@ public sealed partial class ContentAudioSystem
[Dependency] private IPlayerManager _player = default!;
[Dependency] private IRobustRandom _random = default!;
[Dependency] private IStateManager _state = default!;
- [Dependency] private RulesSystem _rules = default!;
[Dependency] private SharedAudioSystem _audio = default!;
+ [Dependency] private SharedEntityConditionsSystem _conditions = default!;
- private readonly TimeSpan _minAmbienceTime = TimeSpan.FromSeconds(60);
- private readonly TimeSpan _maxAmbienceTime = TimeSpan.FromSeconds(120);
+ private readonly TimeSpan _minAmbienceTime = TimeSpan.FromSeconds(40);
+ private readonly TimeSpan _maxAmbienceTime = TimeSpan.FromSeconds(100);
private const float AmbientMusicFadeTime = 10f;
private static float _volumeSlider;
@@ -88,7 +88,7 @@ public sealed partial class ContentAudioSystem
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
- if (obj.WasModified() || obj.WasModified())
+ if (obj.WasModified())
SetupAmbientSounds();
}
@@ -165,7 +165,7 @@ public sealed partial class ContentAudioSystem
{
var player = _player.LocalSession?.AttachedEntity;
- if (player == null || _musicProto == null || !_rules.IsTrue(player.Value, ProtoMan.Index(_musicProto.Rules)))
+ if (player == null || _musicProto == null || !_conditions.TryConditions(player.Value, _musicProto.Conditions))
{
FadeOut(_ambientMusicStream, duration: AmbientMusicFadeTime);
_musicProto = null;
@@ -192,9 +192,10 @@ public sealed partial class ContentAudioSystem
_musicProto = GetAmbience();
- if (_musicProto == null || _musicProto == _lastMusicProto)
+ if (_musicProto == null || _musicProto == _lastMusicProto && !_musicProto.AllowRepeat)
{
_interruptable = false;
+ _nextAudio = _timing.CurTime + _random.Next(_minAmbienceTime, _maxAmbienceTime);
return;
}
@@ -228,9 +229,7 @@ public sealed partial class ContentAudioSystem
private AmbientMusicPrototype? GetAmbience()
{
- var player = _player.LocalEntity;
-
- if (player == null)
+ if (_player.LocalEntity is not { } player)
return null;
var ev = new PlayAmbientMusicEvent();
@@ -244,8 +243,10 @@ public sealed partial class ContentAudioSystem
foreach (var amb in ambiences)
{
- if (!_rules.IsTrue(player.Value, ProtoMan.Index(amb.Rules)))
+ if (!_conditions.TryConditions(player, amb.Conditions))
+ {
continue;
+ }
return amb;
}
diff --git a/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml.cs b/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml.cs
index 09e418fd86..cd76b08155 100644
--- a/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml.cs
+++ b/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml.cs
@@ -28,10 +28,10 @@ public sealed partial class CryostorageEntryControl : BoxContainer
Body.Visible = data.ItemSlots.Count != 0 && data.HeldItems.Count != 0;
ItemsContainer.Children.Clear();
- foreach (var (name, itemName) in data.ItemSlots)
+ foreach (var (slotId, slotName, itemName) in data.ItemSlots)
{
- var control = new CryostorageSlotControl(name, itemName);
- control.Button.OnPressed += _ => SlotRemoveButtonPressed?.Invoke(name);
+ var control = new CryostorageSlotControl(slotName, itemName);
+ control.Button.OnPressed += _ => SlotRemoveButtonPressed?.Invoke(slotId);
ItemsContainer.AddChild(control);
}
diff --git a/Content.Client/Body/VisualBodySystem.cs b/Content.Client/Body/VisualBodySystem.cs
index ad44317924..9f6a777d86 100644
--- a/Content.Client/Body/VisualBodySystem.cs
+++ b/Content.Client/Body/VisualBodySystem.cs
@@ -2,7 +2,6 @@ using System.Linq;
using Content.Client.DisplacementMap;
using Content.Shared.Body;
using Content.Shared.CCVar;
-using Content.Shared.DisplacementMap;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid;
using Robust.Client.GameObjects;
@@ -239,6 +238,13 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem
);
numDisplacements++;
}
+
+ if (proto.Shaders is not null &&
+ proto.Shaders.TryGetValue(rsi.RsiState, out var shader))
+ {
+ // TODO: fix this when LayerSetShader is moved out of component
+ target.Comp.LayerSetShader(index + i + 1 + numDisplacements, shader);
+ }
}
applied.Add(marking);
diff --git a/Content.Client/Cargo/UI/FundingAllocationMenu.xaml b/Content.Client/Cargo/UI/FundingAllocationMenu.xaml
index 62f0ae924a..f796dc69f7 100644
--- a/Content.Client/Cargo/UI/FundingAllocationMenu.xaml
+++ b/Content.Client/Cargo/UI/FundingAllocationMenu.xaml
@@ -6,23 +6,23 @@
VerticalExpand="True"
HorizontalExpand="True"
Margin="10 5 10 10">
-
+
-
+
-
+
-
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml
index 7b5d116ad7..6796b8bd9e 100644
--- a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml
+++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml
@@ -42,7 +42,7 @@
-
+
diff --git a/Content.Client/DoAfter/DoAfterOverlay.cs b/Content.Client/DoAfter/DoAfterOverlay.cs
index 761584e06e..7d546fae0e 100644
--- a/Content.Client/DoAfter/DoAfterOverlay.cs
+++ b/Content.Client/DoAfter/DoAfterOverlay.cs
@@ -37,6 +37,15 @@ public sealed class DoAfterOverlay : Overlay
private const float StartX = 2;
private const float EndX = 22f;
+ // Time after which the doafter will lerp to max alpha.
+ private static readonly TimeSpan MaxAlphaTime = TimeSpan.FromSeconds(0.3f);
+
+ // After finishing, how long it takes to fade out to 0 alpha
+ private static readonly TimeSpan FadeoutAlphaTime = TimeSpan.FromSeconds(0.2f);
+
+ // Time after which the doafter will lerp to its final y offset.
+ private static readonly TimeSpan MaxYPosTime = TimeSpan.FromSeconds(0.5f);
+
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
public DoAfterOverlay(IEntityManager entManager, IPrototypeManager protoManager, IGameTiming timing, IPlayerManager player)
@@ -110,19 +119,29 @@ public sealed class DoAfterOverlay : Overlay
foreach (var doAfter in comp.DoAfters.Values)
{
// Hide some DoAfters from other players for stealthy actions (ie: thieving gloves)
- var alpha = 1f;
+ var maxAlpha = 1f;
if (doAfter.Args.Hidden || isInContainer)
{
if (uid != localEnt)
continue;
// Hints to the local player that this do-after is not visible to other players.
- alpha = 0.5f;
+ maxAlpha = 0.5f;
}
+ var elapsed = time - doAfter.StartTime;
+
+ var alpha = MathHelper.Lerp(0f, maxAlpha, (float)Math.Clamp(elapsed / MaxAlphaTime, 0.0, 1.0));
+ // fade out if doafter finished
+ if (elapsed >= doAfter.Args.Delay)
+ alpha = MathHelper.Lerp(maxAlpha, 0f, (float)Math.Clamp((elapsed - doAfter.Args.Delay) / FadeoutAlphaTime, 0.0, 1.0));
+
// Use the sprite itself if we know its bounds. This means short or tall sprites don't get overlapped
// by the bar.
- var yOffset = _sprite.GetLocalBounds((uid, sprite)).Height / 2f + 0.05f;
+ var spriteBounds = _sprite.GetLocalBounds((uid, sprite));
+ var yFinished = spriteBounds.Height / 2f + 0.05f;
+ var yStart = yFinished / 6f;
+ var yOffset = MathHelper.Lerp(yStart, yFinished, Easings.OutSine((float)Math.Clamp(elapsed / MaxYPosTime, 0.0, 1.0)));
// Position above the entity (we've already applied the matrix transform to the entity itself)
// Offset by the texture size for every do_after we have.
@@ -130,7 +149,7 @@ public sealed class DoAfterOverlay : Overlay
yOffset / scale + offset / EyeManager.PixelsPerMeter * scale);
// Draw the underlying bar texture
- handle.DrawTexture(_barTexture, position);
+ handle.DrawTexture(_barTexture, position, Color.White.WithAlpha(alpha));
Color color;
float elapsedRatio;
@@ -138,7 +157,7 @@ public sealed class DoAfterOverlay : Overlay
// if we're cancelled then flick red / off.
if (doAfter.CancelledTime != null)
{
- var elapsed = doAfter.CancelledTime.Value - doAfter.StartTime;
+ elapsed = doAfter.CancelledTime.Value - doAfter.StartTime;
elapsedRatio = (float)Math.Min(1, elapsed.TotalSeconds / doAfter.Args.Delay.TotalSeconds);
var cancelElapsed = (time - doAfter.CancelledTime.Value).TotalSeconds;
var flash = Math.Floor(cancelElapsed / FlashTime) % 2 == 0;
@@ -146,7 +165,6 @@ public sealed class DoAfterOverlay : Overlay
}
else
{
- var elapsed = time - doAfter.StartTime;
elapsedRatio = (float)Math.Min(1, elapsed.TotalSeconds / doAfter.Args.Delay.TotalSeconds);
color = GetProgressColor(elapsedRatio, alpha);
}
diff --git a/Content.Client/Guidebook/Richtext/Table.cs b/Content.Client/Guidebook/Richtext/Table.cs
index b6923c3698..f61955cef6 100644
--- a/Content.Client/Guidebook/Richtext/Table.cs
+++ b/Content.Client/Guidebook/Richtext/Table.cs
@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
-using Content.Client.UserInterface.Controls;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
namespace Content.Client.Guidebook.Richtext;
diff --git a/Content.Client/IconSmoothing/IconSmoothComponent.cs b/Content.Client/IconSmoothing/IconSmoothComponent.cs
index 7898e9dc1e..8881ef4733 100644
--- a/Content.Client/IconSmoothing/IconSmoothComponent.cs
+++ b/Content.Client/IconSmoothing/IconSmoothComponent.cs
@@ -1,84 +1,91 @@
using JetBrains.Annotations;
using Robust.Client.Graphics;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Prototypes;
-namespace Content.Client.IconSmoothing
+namespace Content.Client.IconSmoothing;
+
+///
+/// Makes sprites of other grid-aligned entities like us connect.
+///
+///
+/// The system is based on Baystation12's smoothwalling, and thus will work with those.
+/// To use, set base equal to the prefix of the corner states in the sprite base RSI.
+/// Any objects with the same key will connect.
+///
+[RegisterComponent]
+public sealed partial class IconSmoothComponent : Component
+{
+ [DataField]
+ public bool Enabled = true;
+
+ public (EntityUid?, Vector2i)? LastPosition;
+
+ ///
+ /// Objects with this key can smooth with us, and vice versa.
+ /// TODO: YAML HELL, Make smoothing one way.
+ ///
+ [DataField("key", required: true)]
+ public string SmoothKey { get; private set; }
+
+ ///
+ /// We will attempt to smooth with any objects that contain these keys.
+ /// Only works one way, objects with these keys will not smooth with us
+ ///
+ [DataField]
+ public List AdditionalKeys = new();
+
+ ///
+ /// Index override for our iconsmooth layers. If null, layers will appear on top.
+ ///
+ [DataField]
+ public int? Index;
+
+ ///
+ /// Prepended to the RSI state.
+ ///
+ [DataField("base")]
+ public string StateBase { get; set; } = string.Empty;
+
+ [DataField]
+ public ProtoId? Shader;
+
+ ///
+ /// Mode that controls how the icon should be selected.
+ ///
+ [DataField]
+ public IconSmoothingMode Mode = IconSmoothingMode.Corners;
+
+ ///
+ /// Used by to reduce redundant updates.
+ ///
+ internal int UpdateGeneration { get; set; }
+}
+
+///
+/// Controls the mode with which icon smoothing is calculated.
+///
+[PublicAPI]
+public enum IconSmoothingMode : byte
{
///
- /// Makes sprites of other grid-aligned entities like us connect.
+ /// Each icon is made up of 4 corners, each of which can get a different state depending on
+ /// adjacent entities clockwise, counter-clockwise and diagonal with the corner.
///
- ///
- /// The system is based on Baystation12's smoothwalling, and thus will work with those.
- /// To use, set base equal to the prefix of the corner states in the sprite base RSI.
- /// Any objects with the same key will connect.
- ///
- [RegisterComponent]
- public sealed partial class IconSmoothComponent : Component
- {
- [ViewVariables(VVAccess.ReadWrite), DataField("enabled")]
- public bool Enabled = true;
-
- public (EntityUid?, Vector2i)? LastPosition;
-
- ///
- /// We will smooth with other objects with the same key.
- ///
- [ViewVariables(VVAccess.ReadWrite), DataField("key")]
- public string? SmoothKey { get; private set; }
-
- ///
- /// Additional keys to smooth with.
- ///
- [DataField]
- public List AdditionalKeys = new();
-
- ///
- /// Prepended to the RSI state.
- ///
- [ViewVariables(VVAccess.ReadWrite), DataField("base")]
- public string StateBase { get; set; } = string.Empty;
-
- [DataField("shader", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? Shader;
-
- ///
- /// Mode that controls how the icon should be selected.
- ///
- [ViewVariables(VVAccess.ReadWrite), DataField("mode")]
- public IconSmoothingMode Mode = IconSmoothingMode.Corners;
-
- ///
- /// Used by to reduce redundant updates.
- ///
- internal int UpdateGeneration { get; set; }
- }
+ Corners,
///
- /// Controls the mode with which icon smoothing is calculated.
+ /// There are 16 icons, only one of which is used at once.
+ /// The icon selected is a bit field made up of the cardinal direction flags that have adjacent entities.
///
- [PublicAPI]
- public enum IconSmoothingMode : byte
- {
- ///
- /// Each icon is made up of 4 corners, each of which can get a different state depending on
- /// adjacent entities clockwise, counter-clockwise and diagonal with the corner.
- ///
- Corners,
+ CardinalFlags,
- ///
- /// There are 16 icons, only one of which is used at once.
- /// The icon selected is a bit field made up of the cardinal direction flags that have adjacent entities.
- ///
- CardinalFlags,
+ ///
+ /// The icon represents a triangular sprite with only 2 states, representing South / East being occupied or not.
+ ///
+ Diagonal,
- ///
- /// The icon represents a triangular sprite with only 2 states, representing South / East being occupied or not.
- ///
- Diagonal,
-
- ///
- /// Where this component contributes to our neighbors being calculated but we do not update our own sprite.
- ///
- NoSprite,
- }
+ ///
+ /// Where this component contributes to our neighbors being calculated but we do not update our own sprite.
+ ///
+ NoSprite,
}
diff --git a/Content.Client/IconSmoothing/IconSmoothSystem.cs b/Content.Client/IconSmoothing/IconSmoothSystem.cs
index 1028554f4d..9367f80838 100644
--- a/Content.Client/IconSmoothing/IconSmoothSystem.cs
+++ b/Content.Client/IconSmoothing/IconSmoothSystem.cs
@@ -88,13 +88,13 @@ namespace Content.Client.IconSmoothing
_sprite.LayerMapRemove(sprite, CornerLayers.SW);
var state0 = $"{component.StateBase}0";
- _sprite.LayerMapSet(sprite, CornerLayers.SE, _sprite.AddRsiLayer(sprite, state0));
+ _sprite.LayerMapSet(sprite, CornerLayers.SE, _sprite.AddRsiLayer(sprite, state0, index: component.Index));
_sprite.LayerSetDirOffset(sprite, CornerLayers.SE, DirectionOffset.None);
- _sprite.LayerMapSet(sprite, CornerLayers.NE, _sprite.AddRsiLayer(sprite, state0));
+ _sprite.LayerMapSet(sprite, CornerLayers.NE, _sprite.AddRsiLayer(sprite, state0, index: component.Index));
_sprite.LayerSetDirOffset(sprite, CornerLayers.NE, DirectionOffset.CounterClockwise);
- _sprite.LayerMapSet(sprite, CornerLayers.NW, _sprite.AddRsiLayer(sprite, state0));
+ _sprite.LayerMapSet(sprite, CornerLayers.NW, _sprite.AddRsiLayer(sprite, state0, index: component.Index));
_sprite.LayerSetDirOffset(sprite, CornerLayers.NW, DirectionOffset.Flip);
- _sprite.LayerMapSet(sprite, CornerLayers.SW, _sprite.AddRsiLayer(sprite, state0));
+ _sprite.LayerMapSet(sprite, CornerLayers.SW, _sprite.AddRsiLayer(sprite, state0, index: component.Index));
_sprite.LayerSetDirOffset(sprite, CornerLayers.SW, DirectionOffset.Clockwise);
}
@@ -369,7 +369,6 @@ namespace Content.Client.IconSmoothing
while (candidates.MoveNext(out var entity))
{
if (_iconSmoothQuery.TryGetComponent(entity, out var other) &&
- other.SmoothKey != null &&
(other.SmoothKey == smooth.SmoothKey || smooth.AdditionalKeys.Contains(other.SmoothKey)) &&
other.Enabled)
{
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
index ae5967fa91..76986e010d 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
@@ -36,6 +36,7 @@
+
+
diff --git a/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs b/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
index 4686035fc7..19bcc4019c 100644
--- a/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
+++ b/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
@@ -1,112 +1,55 @@
-using Content.Client.Chat.Managers;
using Content.Client.RichText;
-using Content.Shared.Chat;
-using Content.Shared.Radio;
using Content.Shared.Silicons.Laws;
-using Content.Shared.Speech;
using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Silicons.Laws.Ui;
[GenerateTypedNameReferences]
-public sealed partial class LawDisplay : Control
+public sealed partial class LawDisplay : Button
{
- [Dependency] private IPrototypeManager _prototypeManager = default!;
- [Dependency] private IChatManager _chatManager = default!;
- [Dependency] private IGameTiming _timing = default!;
- [Dependency] private EntityManager _entityManager = default!;
+ private const string LawDisplayUnselectedTexturePath = "/Textures/Interface/Alerts/Abilities/silenced.png";
+ private const string LawDisplaySelectedTexturePath = "/Textures/Interface/Emotes/vocal.png";
- private static readonly TimeSpan PressCooldown = TimeSpan.FromSeconds(3);
+ ///
+ /// The SiliconLaw tracked by this display.
+ ///
+ public SiliconLaw Law { get; }
- private readonly Dictionary _nextAllowedPress = new();
-
- public LawDisplay(EntityUid uid, SiliconLaw law, HashSet>? radioChannels)
+ public LawDisplay(SiliconLaw law)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- var identifier = law.LawIdentifierOverride ?? $"{law.Order}";
+ Law = law;
+
+ 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);
+ var lawDescription = Loc.GetString(Law.LawString);
- LawNumberLabel.SetMessage(FormattedMessage.FromMarkupPermissive(lawIdentifier), UserFormattableTags.SiliconAllowedTags);
- LawLabel.SetMessage(FormattedMessage.FromMarkupPermissive(lawDescription), UserFormattableTags.SiliconAllowedTags);
-
- // If you can't talk, you can't state your laws...
- if (!_entityManager.TryGetComponent(uid, out var speech) || speech.SpeechSounds is null)
- return;
-
- var localButton = new Button
- {
- Text = Loc.GetString("hud-chatbox-select-channel-Local"),
- Modulate = Color.DarkGray,
- StyleClasses = { "chatSelectorOptionButton" },
- MinHeight = 35,
- MinWidth = 75,
- };
-
- _nextAllowedPress[localButton] = TimeSpan.Zero;
-
- localButton.OnPressed += _ =>
- {
- _chatManager.SendMessage($"{lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Local);
- _nextAllowedPress[localButton] = _timing.CurTime + PressCooldown;
- };
-
- LawAnnouncementButtons.AddChild(localButton);
-
- if (radioChannels == null)
- return;
-
- foreach (var radioChannel in radioChannels)
- {
- if (!_prototypeManager.TryIndex(radioChannel, out var radioChannelProto))
- continue;
-
- var radioChannelButton = new Button
- {
- Text = Loc.GetString(radioChannelProto.Name),
- Modulate = radioChannelProto.Color,
- StyleClasses = { "chatSelectorOptionButton" },
- MinHeight = 35,
- MinWidth = 75,
- };
-
- _nextAllowedPress[radioChannelButton] = TimeSpan.Zero;
-
- radioChannelButton.OnPressed += _ =>
- {
- if (radioChannel == SharedChatSystem.CommonChannel)
- {
- _chatManager.SendMessage($"{SharedChatSystem.RadioCommonPrefix} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio);
- }
- else
- {
- _chatManager.SendMessage($"{SharedChatSystem.RadioChannelPrefix}{radioChannelProto.KeyCode} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio);
- }
- _nextAllowedPress[radioChannelButton] = _timing.CurTime + PressCooldown;
- };
-
- LawAnnouncementButtons.AddChild(radioChannelButton);
- }
+ LawNumberLabel.SetMessage(FormattedMessage.FromMarkupPermissive(lawIdentifier),
+ UserFormattableTags.SiliconAllowedTags);
+ LawLabel.SetMessage(FormattedMessage.FromMarkupPermissive(lawDescription),
+ UserFormattableTags.SiliconAllowedTags);
}
- protected override void FrameUpdate(FrameEventArgs args)
+ ///
+ /// Switches the indicator of this display depending on whether its law has been selected
+ ///
+ /// Whether or not the law associated with this display is selected
+ public void SetLawSelectedIndicators(bool isSelected)
{
- base.FrameUpdate(args);
-
- var curTime = _timing.CurTime;
- foreach (var (button, nextPress) in _nextAllowedPress)
+ if (isSelected)
{
- button.Disabled = curTime < nextPress;
+ AddStyleClass("positive");
+ LawSelectedIcon.TexturePath = LawDisplaySelectedTexturePath;
+ }
+ else
+ {
+ RemoveStyleClass("positive");
+ LawSelectedIcon.TexturePath = LawDisplayUnselectedTexturePath;
}
}
}
diff --git a/Content.Client/Silicons/Laws/Ui/SiliconLawMenu.xaml b/Content.Client/Silicons/Laws/Ui/SiliconLawMenu.xaml
index 97f6179e45..9946f30e4b 100644
--- a/Content.Client/Silicons/Laws/Ui/SiliconLawMenu.xaml
+++ b/Content.Client/Silicons/Laws/Ui/SiliconLawMenu.xaml
@@ -1,15 +1,34 @@
-
-
-
+
+
+
+
+
+
+
-
+
+ Margin="10 10 10 5">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Silicons/Laws/Ui/SiliconLawMenu.xaml.cs b/Content.Client/Silicons/Laws/Ui/SiliconLawMenu.xaml.cs
index a63494bc52..c2145547b2 100644
--- a/Content.Client/Silicons/Laws/Ui/SiliconLawMenu.xaml.cs
+++ b/Content.Client/Silicons/Laws/Ui/SiliconLawMenu.xaml.cs
@@ -1,29 +1,195 @@
+using System.Linq;
+using Content.Client.Chat.Managers;
using Content.Client.UserInterface.Controls;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Chat;
+using Content.Shared.Radio;
+using Content.Shared.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
+using Content.Shared.Speech;
using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.Silicons.Laws.Ui;
[GenerateTypedNameReferences]
public sealed partial class SiliconLawMenu : FancyWindow
{
+ [Dependency] private IPrototypeManager _prototypeManager = default!;
+ [Dependency] private IChatManager _chatManager = default!;
+ [Dependency] private IGameTiming _timing = default!;
+ [Dependency] private EntityManager _entityManager = default!;
+ [Dependency] private IRobustRandom _random = default!;
+
+ private static readonly TimeSpan AnnounceBaseCooldown = TimeSpan.FromSeconds(1);
+ private static readonly float MinChatCooldown = 1;
+ private static readonly float MaxChatCooldown = 2;
+
+ private static readonly List SelectableChatChannels =
+ new() { ChatSelectChannel.Local, ChatSelectChannel.Whisper };
+
+ private int _selectedChatChannelIdx;
+ private readonly List _selectableRadioChannels = new();
+
+ private readonly HashSet _selectedLaws = new();
+ private readonly Queue<(SiliconLaw law, TimeSpan announceTime)> _queuedLaws = new();
+ private TimeSpan _nextAllowedAnnouncePress = TimeSpan.Zero;
+
+ private EntityUid _owner;
+
public SiliconLawMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+
+ LawChatChannelOption.OnItemSelected += OnLawChatChannelSelected;
+ LawSelectAllButton.OnPressed += _ => OnMassSelectPressed(true);
+ LawSelectNoneButton.OnPressed += _ => OnMassSelectPressed(false);
+ LawAnnounceButton.OnPressed += OnLawAnnounceButtonPressed;
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ var curTime = _timing.CurTime;
+ LawAnnounceButton.Disabled = curTime < _nextAllowedAnnouncePress;
+ // Don't want to change the channel while currently announcing laws
+ LawChatChannelOption.Disabled = curTime < _nextAllowedAnnouncePress;
+
+ // Announce laws
+ // Skip if no queued laws or delay still active
+ if (_queuedLaws.Count < 1 || _queuedLaws.Peek().announceTime > curTime)
+ {
+ return;
+ }
+
+ var law = _queuedLaws.Dequeue().law;
+ 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);
+
+ // Local chat and whisper
+ if (_selectedChatChannelIdx < SelectableChatChannels.Count)
+ {
+ _chatManager.SendMessage($"{lawIdentifierPlaintext}: {lawDescriptionPlaintext}",
+ SelectableChatChannels[_selectedChatChannelIdx]);
+ }
+ // Radio
+ else
+ {
+ var radioChannelProto = _selectableRadioChannels[_selectedChatChannelIdx - SelectableChatChannels.Count];
+ var radioMessage = radioChannelProto.ID == SharedChatSystem.CommonChannel
+ ? $"{SharedChatSystem.RadioCommonPrefix} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}"
+ : $"{SharedChatSystem.RadioChannelPrefix}{radioChannelProto.KeyCode} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}";
+ _chatManager.SendMessage(radioMessage, ChatSelectChannel.Radio);
+ }
}
public void Update(EntityUid uid, SiliconLawBuiState state)
{
+ _owner = uid;
+
state.Laws.Sort();
LawDisplayContainer.Children.Clear();
+ Version.Text = Loc.GetString("laws-window-footer-right", ("version", state.Version));
+
+ LawChatChannelOption.Clear();
+ foreach (var chatChannel in SelectableChatChannels)
+ {
+ LawChatChannelOption.AddItem(chatChannel.ToString());
+ }
+
+ if (state.RadioChannels is not null)
+ {
+ foreach (var radioChannel in state.RadioChannels)
+ {
+ if (!_prototypeManager.TryIndex(radioChannel, out var radioChannelProto))
+ continue;
+
+ _selectableRadioChannels.Add(radioChannelProto);
+ LawChatChannelOption.AddItem(Loc.GetString(radioChannelProto.Name));
+ }
+ }
+
+ LawChatChannelOption.Select(_selectedChatChannelIdx);
foreach (var law in state.Laws)
{
- var control = new LawDisplay(uid, law, state.RadioChannels);
+ var control = new LawDisplay(law);
+ control.OnPressed += _ =>
+ {
+ if (_selectedLaws.Contains(law))
+ {
+ _selectedLaws.Remove(law);
+ control.SetLawSelectedIndicators(false);
+ }
+ else
+ {
+ _selectedLaws.Add(law);
+ control.SetLawSelectedIndicators(true);
+ }
+ };
LawDisplayContainer.AddChild(control);
}
}
+
+ private void OnMassSelectPressed(bool isPositive)
+ {
+ foreach (var child in LawDisplayContainer.Children)
+ {
+ if (child is not LawDisplay lawDisplay)
+ continue;
+
+ if (isPositive)
+ {
+ _selectedLaws.Add(lawDisplay.Law);
+ }
+ else
+ {
+ _selectedLaws.Remove(lawDisplay.Law);
+ }
+
+ lawDisplay.SetLawSelectedIndicators(isPositive);
+ }
+ }
+
+ private void OnLawChatChannelSelected(OptionButton.ItemSelectedEventArgs obj)
+ {
+ _selectedChatChannelIdx = LawChatChannelOption.GetIdx(obj.Id);
+ LawChatChannelOption.Select(_selectedChatChannelIdx);
+ }
+
+ private void OnLawAnnounceButtonPressed(BaseButton.ButtonEventArgs obj)
+ {
+ // If you have no laws selected, or you can't talk, you can't state your laws...
+ var cannotSpeak = !_entityManager.TryGetComponent(_owner, out _)
+ || !_entityManager.System().CanSpeak(_owner);
+ if (_selectedLaws.Count < 1 || cannotSpeak)
+ return;
+
+ var toAnnounce = _selectedLaws.ToList();
+ toAnnounce.Sort();
+
+ // Queue up laws to be announced
+ // The first message will be sent immediately, while future ones have a cooldown
+ var totalChatCooldown = TimeSpan.Zero;
+ foreach (var law in toAnnounce)
+ {
+ var chatCooldown = TimeSpan.FromSeconds(_random.NextFloat(MinChatCooldown, MaxChatCooldown));
+ _queuedLaws.Enqueue((law, _timing.CurTime + totalChatCooldown));
+ totalChatCooldown += chatCooldown;
+ }
+
+ // Disable button until all laws announced, plus an additional cooldown
+ _nextAllowedAnnouncePress = _timing.CurTime + totalChatCooldown + AnnounceBaseCooldown;
+ }
}
diff --git a/Content.Client/Speech/EntitySystems/SlurredSystem.cs b/Content.Client/Speech/EntitySystems/SlurredSystem.cs
index 5a18676c34..a4bb677edf 100644
--- a/Content.Client/Speech/EntitySystems/SlurredSystem.cs
+++ b/Content.Client/Speech/EntitySystems/SlurredSystem.cs
@@ -5,7 +5,7 @@ namespace Content.Client.Speech.EntitySystems;
public sealed class SlurredSystem : SharedSlurredSystem
{
- protected override string AccentuateInternal(EntityUid uid, SlurredAccentComponent comp, string message)
+ public override string Accentuate(string message, Entity? ent = null)
{
return message;
}
diff --git a/Content.Client/Speech/EntitySystems/StutteringSystem.cs b/Content.Client/Speech/EntitySystems/StutteringSystem.cs
index dc8646f217..9c98df4588 100644
--- a/Content.Client/Speech/EntitySystems/StutteringSystem.cs
+++ b/Content.Client/Speech/EntitySystems/StutteringSystem.cs
@@ -5,7 +5,7 @@ namespace Content.Client.Speech.EntitySystems;
public sealed class StutteringSystem : SharedStutteringSystem
{
- protected override string AccentuateInternal(EntityUid uid, StutteringAccentComponent comp, string message)
+ public override string Accentuate(string message, Entity? ent = null)
{
return message;
}
diff --git a/Content.Client/StationRecords/GeneralRecord.xaml.cs b/Content.Client/StationRecords/GeneralRecord.xaml.cs
index b69c6fb8bb..6907e75467 100644
--- a/Content.Client/StationRecords/GeneralRecord.xaml.cs
+++ b/Content.Client/StationRecords/GeneralRecord.xaml.cs
@@ -17,7 +17,7 @@ public sealed partial class GeneralRecord : Control
RecordName.Text = record.Name;
Age.Text = Loc.GetString("general-station-record-console-record-age", ("age", record.Age.ToString()));
Title.Text = Loc.GetString("general-station-record-console-record-title",
- ("job", Loc.GetString(record.JobTitle)));
+ ("job", record.JobTitle));
var species = Loc.GetString(prototypeManager.Index(record.Species).Name);
Species.Text = Loc.GetString("general-station-record-console-record-species", ("species", species));
Gender.Text = Loc.GetString("general-station-record-console-record-gender",
diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs
index e7bab71e38..c77735060b 100644
--- a/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs
+++ b/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs
@@ -1,12 +1,18 @@
+using Content.Shared.Station;
using Content.Shared.StationRecords;
+using Content.Shared.StationRecords.Components;
+using Content.Shared.StationRecords.Systems;
using Robust.Client.UserInterface;
namespace Content.Client.StationRecords;
-public sealed class GeneralStationRecordConsoleBoundUserInterface : BoundUserInterface
+public sealed partial class GeneralStationRecordConsoleBoundUserInterface : BoundUserInterface
{
[ViewVariables]
- private GeneralStationRecordConsoleWindow? _window = default!;
+ private GeneralStationRecordConsoleWindow? _window;
+
+ [Dependency] private SharedStationSystem _stationSys = default!;
+ [Dependency] private StationRecordsSystem _recordsSys = default!;
public GeneralStationRecordConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
@@ -17,20 +23,51 @@ public sealed class GeneralStationRecordConsoleBoundUserInterface : BoundUserInt
base.Open();
_window = this.CreateWindow();
- _window.OnKeySelected += key =>
- SendMessage(new SelectStationRecord(key));
- _window.OnFiltersChanged += (type, filterValue) =>
- SendMessage(new SetStationRecordFilter(type, filterValue));
- _window.OnDeleted += id => SendMessage(new DeleteStationRecord(id));
+ _window.OnKeySelected += SelectStationRecord;
+ _window.OnFiltersChanged += SetStationRecordFilter;
+ _window.OnDeleted += id => SendPredictedMessage(new DeleteStationRecord(id));
+ Update();
}
- protected override void UpdateState(BoundUserInterfaceState state)
+ public override void Update()
{
- base.UpdateState(state);
+ base.Update();
- if (state is not GeneralStationRecordConsoleState cast)
+ if (!EntMan.TryGetComponent(Owner, out GeneralStationRecordConsoleComponent? comp))
return;
- _window?.UpdateState(cast);
+ var owningStation = _stationSys.GetOwningStation(Owner);
+
+ if (!EntMan.TryGetComponent(owningStation, out StationRecordsComponent? stationRecords))
+ return;
+
+ var listing = _recordsSys.BuildListing((owningStation.Value, stationRecords), comp.Filter);
+
+ GeneralStationRecord? record = null;
+ if (comp.ActiveKey != null)
+ {
+ var key = new StationRecordKey(comp.ActiveKey.Value, owningStation.Value);
+ _recordsSys.TryGetRecord(key, out record, stationRecords);
+ }
+
+ _window?.UpdateState(comp.ActiveKey, record, listing, comp.Filter, comp.CanDeleteEntries);
+ }
+
+ private void SelectStationRecord(uint? key)
+ {
+ if (!EntMan.TryGetComponent(Owner, out GeneralStationRecordConsoleComponent? comp))
+ return;
+
+ comp.ActiveKey = key;
+ Update();
+ }
+
+ private void SetStationRecordFilter(StationRecordFilterType type, string value)
+ {
+ if (!EntMan.TryGetComponent(Owner, out GeneralStationRecordConsoleComponent? comp))
+ return;
+
+ comp.Filter = new StationRecordsFilter(type, value);
+ Update();
}
}
diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleSystem.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleSystem.cs
new file mode 100644
index 0000000000..2c769c7ec4
--- /dev/null
+++ b/Content.Client/StationRecords/GeneralStationRecordConsoleSystem.cs
@@ -0,0 +1,28 @@
+using Content.Shared.StationRecords;
+using Content.Shared.StationRecords.Components;
+using Content.Shared.StationRecords.Systems;
+
+namespace Content.Client.StationRecords;
+
+public sealed partial class GeneralStationRecordConsoleSystem : SharedGeneralStationRecordConsoleSystem
+{
+ [Dependency] private SharedUserInterfaceSystem _ui = default!;
+
+ protected override void UpdateUserInterface(Entity ent)
+ {
+ if (_ui.TryGetOpenUi(ent.Owner, GeneralStationRecordConsoleKey.Key, out var bui))
+ bui.Update();
+ }
+
+ // Needed so when a record is created or deleted, it appears in the UI instantly.
+ [SubscribeLocalEvent]
+ private void OnAfterHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _))
+ {
+ if (_ui.TryGetOpenUi(uid, GeneralStationRecordConsoleKey.Key, out var bui))
+ bui.Update();
+ }
+ }
+}
diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs
index 350af7d353..ff0001010c 100644
--- a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs
+++ b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs
@@ -74,24 +74,28 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
};
}
- public void UpdateState(GeneralStationRecordConsoleState state)
+ public void UpdateState(uint? selectedKey,
+ GeneralStationRecord? record,
+ Dictionary? recordListing,
+ StationRecordsFilter? filter,
+ bool canDeleteEntries)
{
- if (state.Filter != null)
+ if (filter != null)
{
- if (state.Filter.Type != _currentFilterType)
+ if (filter.Type != _currentFilterType)
{
- _currentFilterType = state.Filter.Type;
+ _currentFilterType = filter.Type;
}
- if (state.Filter.Value != StationRecordsFiltersValue.Text)
+ if (filter.Value != StationRecordsFiltersValue.Text)
{
- StationRecordsFiltersValue.Text = state.Filter.Value;
+ StationRecordsFiltersValue.Text = filter.Value;
}
}
StationRecordsFilterType.SelectId((int)_currentFilterType);
- if (state.RecordListing == null)
+ if (recordListing == null)
{
RecordListingStatus.Visible = true;
RecordListing.Visible = false;
@@ -105,17 +109,17 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
RecordListing.Visible = true;
RecordContainer.Visible = true;
- PopulateRecordListing(state.RecordListing!, state.SelectedKey);
+ PopulateRecordListing(recordListing, selectedKey);
- RecordContainerStatus.Visible = state.Record == null;
+ RecordContainerStatus.Visible = record == null;
- if (state.Record != null)
+ if (record != null)
{
- RecordContainerStatus.Visible = state.SelectedKey == null;
- RecordContainerStatus.Text = state.SelectedKey == null
+ RecordContainerStatus.Visible = selectedKey == null;
+ RecordContainerStatus.Text = selectedKey == null
? Loc.GetString("general-station-record-console-no-record-found")
: Loc.GetString("general-station-record-console-select-record-info");
- PopulateRecordContainer(state.Record, state.CanDeleteEntries, state.SelectedKey);
+ PopulateRecordContainer(record, canDeleteEntries, selectedKey);
}
else
{
diff --git a/Content.Client/StationRecords/StationRecordsSystem.cs b/Content.Client/StationRecords/StationRecordsSystem.cs
deleted file mode 100644
index 92ec076d0b..0000000000
--- a/Content.Client/StationRecords/StationRecordsSystem.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.StationRecords;
-
-namespace Content.Client.StationRecords;
-
-public sealed class StationRecordsSystem : SharedStationRecordsSystem
-{
-}
diff --git a/Content.Client/Stealth/StealthSystem.cs b/Content.Client/Stealth/StealthSystem.cs
index d31b998458..a33b11f0af 100644
--- a/Content.Client/Stealth/StealthSystem.cs
+++ b/Content.Client/Stealth/StealthSystem.cs
@@ -92,6 +92,7 @@ public sealed partial class StealthSystem : SharedStealthSystem
_shader.SetParameter("reference", reference);
_shader.SetParameter("visibility", visibility);
+ _shader.SetParameter("shimmer_frequency", component.ShimmerFrequency);
visibility = MathF.Max(0, visibility);
_sprite.SetColor((uid, args.Sprite), new Color(visibility, visibility, 1, 1));
diff --git a/Content.Client/Stylesheets/Sheetlets/LabelSheetlet.cs b/Content.Client/Stylesheets/Sheetlets/LabelSheetlet.cs
index fc23f9c7fb..ea75102b6c 100644
--- a/Content.Client/Stylesheets/Sheetlets/LabelSheetlet.cs
+++ b/Content.Client/Stylesheets/Sheetlets/LabelSheetlet.cs
@@ -26,6 +26,10 @@ public sealed class LabelSheetlet : Sheetlet
.Class(StyleClass.LabelHeadingBigger)
.Font(sheet.BaseFont.GetFont(20, FontKind.Bold))
.FontColor(sheet.HighlightPalette.Text),
+ E