merge remote wizden/master

This commit is contained in:
Dmitry 2026-07-19 16:10:56 +07:00
commit 816b7825ba
910 changed files with 20529 additions and 16602 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

4
.gitignore vendored
View file

@ -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

28
.vscode/tasks.json vendored
View file

@ -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",

View file

@ -0,0 +1,18 @@
using Content.Shared.Access.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Access;
/// <inheritdoc />
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();
}
}
}

View file

@ -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;
/// <summary>
/// Initializes a <see cref="AgentIDCardWindow"/> and updates it when new server messages are received.
/// </summary>
public sealed class AgentIDCardBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
/// <summary>
/// Initializes a <see cref="AgentIDCardWindow"/> and updates it when new server messages are received.
/// </summary>
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<AgentIDCardWindow>();
_window = this.CreateWindow<AgentIDCardWindow>();
_window.OnNameChanged += OnNameChanged;
_window.OnJobChanged += OnJobChanged;
_window.OnJobIconChanged += OnJobIconChanged;
_window.OnNameChanged += OnNameChanged;
_window.OnJobChanged += OnJobChanged;
_window.OnJobIconChanged += OnJobIconChanged;
}
ProtoId<JobIconPrototype> currentIcon = default;
if (EntMan.TryGetComponent<IdCardComponent>(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<JobIconPrototype> newJobIconId)
{
SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
}
if (_window == null)
return;
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not AgentIDCardBoundUserInterfaceState cast)
return;
if (!EntMan.TryGetComponent<IdCardComponent>(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<JobIconPrototype> newJobIconId)
{
SendPredictedMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
}
}

View file

@ -1,14 +1,71 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Loc agent-id-menu-title}">
<BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="150">
<Label Name="CurrentName" Text="{Loc 'agent-id-card-current-name'}" />
<LineEdit Name="NameLineEdit" />
<Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" />
<LineEdit Name="JobLineEdit" />
<Label Text="{Loc 'agent-id-card-job-icon-label'}"/>
<GridContainer Name="IconGrid" Columns="10">
<!-- Job icon buttons are generated in the code -->
</GridContainer>
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc agent-id-ui-menu-title}"
MinSize="765 398" Resizable="False">
<BoxContainer Orientation="Vertical" Margin="12" VerticalExpand="False">
<!-- Upper area: ID status -->
<BoxContainer Margin="0 0 0 8" MinHeight="80">
<PanelContainer StyleClasses="PanelInsetDark">
<TextureRect Name="CurrentJobIcon" Margin="8 0" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Center" SetSize="64 64" />
</PanelContainer>
<BoxContainer Margin="12 0" Orientation="Vertical" Align="Center">
<Label Name="CurrentName" StyleClasses="LabelHeadingBigger" SetHeight="32" />
<Label Name="CurrentJob" StyleClasses="LabelSubHeading" Margin="16 4 0 0" />
</BoxContainer>
</BoxContainer>
<!-- Center area: Tabs -->
<PanelContainer StyleClasses="PanelInsetDark" MinHeight="230" VerticalExpand="True">
<BoxContainer Orientation="Vertical">
<TabContainer Name="AgentTabs" Margin="8 8 8 0" VerticalExpand="True" MinHeight="180">
<!-- Settings tab -->
<ScrollContainer Name="TabSettings">
<BoxContainer Margin="8" Orientation="Vertical">
<Label Margin="4 2" Text="{Loc 'agent-id-ui-input-name'}" />
<LineEdit Name="NameLineEdit" SelectAllOnFocus="True" />
<Label Margin="4 2" Text="{Loc 'agent-id-ui-input-job'}" />
<LineEdit Name="JobLineEdit" SelectAllOnFocus="True" />
</BoxContainer>
</ScrollContainer>
<!-- Job icon tab -->
<BoxContainer Name="TabJobIcons" Margin="8">
<BoxContainer>
<GridContainer Name="JobGroupGrid" Columns="3" />
<!-- Group buttons are generated in code -->
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" VerticalExpand="True" />
<PanelContainer StyleClasses="PanelInsetDark" HorizontalExpand="True">
<BoxContainer Margin="8">
<GridContainer Name="IconGrid" Columns="4" />
<!-- Job icon buttons are generated in code -->
</BoxContainer>
</PanelContainer>
</BoxContainer>
<!-- TODO chameleon clothing tab -->
</TabContainer>
<controls:StripeBack Margin="8 -6 8 4" HasBottomEdge="False"> <!-- The negative margin allows the control to be flush with the TabContainer -->
<Label StyleClasses="LabelSubText" Margin="6 2" Text="{Loc 'agent-id-ui-os'}"
VerticalAlignment="Center" />
<Label StyleClasses="LabelSubText" Margin="6 2" Text="{Loc 'agent-id-ui-os-flavor'}"
VerticalAlignment="Center" HorizontalAlignment="Right" />
</controls:StripeBack>
</BoxContainer>
</PanelContainer>
<!-- Footer -->
<BoxContainer Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'agent-id-ui-footer-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'agent-id-ui-footer-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View file

@ -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<string>? OnNameChanged;
public event Action<string>? OnJobChanged;
public event Action<ProtoId<JobIconPrototype>>? 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<SpriteSystem>();
private const int JobIconColumnCount = 10;
NameLineEdit.OnTextEntered += e => CommitName(e.Text);
NameLineEdit.OnFocusExit += e => CommitName(e.Text);
public event Action<string>? OnNameChanged;
public event Action<string>? OnJobChanged;
JobLineEdit.OnTextEntered += e => CommitJob(e.Text);
JobLineEdit.OnFocusExit += e => CommitJob(e.Text);
public event Action<ProtoId<JobIconPrototype>>? 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"));
}
/// <summary>
/// Creates the job icons tab.
/// </summary>
public void SetAllowedIcons(IReadOnlyList<ProtoId<JobIconGroupPrototype>> jobGroups, ProtoId<JobIconPrototype> currentIcon)
{
JobGroupGrid.DisposeAllChildren();
IconGrid.DisposeAllChildren();
var jobGroupButtonGroup = new ButtonGroup();
ContainerButton? firstGroupButton = null;
IReadOnlyList<ProtoId<JobIconPrototype>>? firstGroupIcons = null;
ContainerButton? matchingGroupButton = null;
IReadOnlyList<ProtoId<JobIconPrototype>>? matchingGroupIcons = null;
foreach (var group in jobGroups)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>();
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<JobIconPrototype>().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);
}
/// <summary>
/// Creates the job icon subgroup within the job icons tab.
/// </summary>
private void SetJobIcons(IReadOnlyList<ProtoId<JobIconPrototype>> 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);
}
}

View file

@ -17,7 +17,7 @@
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<Control HorizontalExpand="True" />
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
<Button Visible="False" Name="PlayerLogs" Text="{Loc 'admin-player-actions-logs'}" StyleClasses="OpenRight" />
<Button Visible="False" Access="Public" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
<controls:ConfirmButton Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />

View file

@ -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;

View file

@ -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();
}
}

View file

@ -0,0 +1,19 @@
<ui:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'admin-event-log-title'}"
MinWidth="600"
MinHeight="180">
<BoxContainer Orientation="Vertical" Margin="10">
<BoxContainer Orientation="Horizontal" MinWidth="400">
<Label Text="{Loc admin-event-round}"/>
<SpinBox Name="RoundSpinBox" Value="0" MinWidth="150" Access="Public"/>
<Button Name="ResetRoundButton" Text="{Loc admin-event-round-reset}"
StyleClasses="Open"/>
</BoxContainer>
<TextEdit Name="EventTextEdit" HorizontalExpand="True" VerticalExpand="True" Access="Public"/>
<BoxContainer Orientation="Horizontal">
<Button Name="SendEventLog" Text="{Loc admin-event-send}" HorizontalAlignment="Left"
StyleClasses="Open" Access="Public"/>
</BoxContainer>
</BoxContainer>
</ui:FancyWindow>

View file

@ -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;
}
}

View file

@ -63,6 +63,8 @@ public sealed partial class AdminLogsControl : Control
public HashSet<LogImpact> 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<Guid> 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<LogType> selectedTypes, bool invert = false)
{
SelectedTypes.Clear();
@ -420,16 +438,14 @@ public sealed partial class AdminLogsControl : Control
public void SetPlayers(Dictionary<Guid, string> players)
{
var buttons = new SortedSet<AdminLogPlayerButton>(_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();

View file

@ -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);

View file

@ -12,6 +12,7 @@
<cc:CommandButton Command="permissions" Text="{Loc admin-player-actions-window-permissions}" />
<cc:CommandButton Command="announceui" Text="{Loc admin-player-actions-window-announce}"/>
<cc:UICommandButton Command="callshuttle" Text="{Loc admin-player-actions-window-shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
<cc:CommandButton Command="eventlog" Text="{Loc admin-player-actions-window-event-log}"/>
<cc:CommandButton Command="adminlogs" Text="{Loc admin-player-actions-window-admin-logs}"/>
<cc:CommandButton Command="faxui" Text="{Loc admin-player-actions-window-admin-fax}"/>
<cc:CommandButton Command="achatwindow" Text="{Loc admin-player-actions-window-admin-chat}"/>

View file

@ -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<SharedSpaceVillainArcadeComponent.PlayerAction>? 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;
}
}
}

View file

@ -0,0 +1,98 @@
<DefaultWindow
xmlns="https://spacestation14.io"
Title="{Loc 'spacevillain-menu-title'}"
MinWidth="540"
MinHeight="225">
<BoxContainer
Orientation="Vertical">
<!-- Info grid -->
<BoxContainer>
<!-- Player -->
<BoxContainer
Orientation="Vertical"
HorizontalExpand="True">
<!-- Name -->
<Label
Text="{Loc 'spacevillain-menu-label-player'}"
Align="Center"
HorizontalExpand="True"/>
<!-- Stats -->
<BoxContainer>
<!-- HP -->
<Label
Name="PlayerHpLabel"
Align="Center"
HorizontalExpand="True"
Modulate="#37e928"/>
<!-- MP -->
<Label
Name="PlayerMpLabel"
Align="Center"
HorizontalExpand="True"
Modulate="#3483ff"/>
</BoxContainer>
</BoxContainer>
<!-- Divider -->
<PanelContainer
StyleClasses="LowDivider"/>
<!-- Villain -->
<BoxContainer
Orientation="Vertical"
HorizontalExpand="True">
<!-- Name -->
<Label
Name="EnemyNameLabel"
Text="{Loc 'spacevillain-menu-label-enemy'}"
Align="Center"
HorizontalExpand="True"/>
<!-- Stats -->
<BoxContainer>
<!-- HP -->
<Label
Name="EnemyHpLabel"
Align="Center"
HorizontalExpand="True"
Modulate="#e94740"/>
<!-- MP -->
<Label
Name="EnemyMpLabel"
Align="Center"
HorizontalExpand="True"
Modulate="#3483ff"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Spacer -->
<Control VerticalExpand="True"/>
<!-- Player and enemy action messages -->
<Label Name="PlayerActionLabel"
Align="Left"/>
<Label Name="EnemyActionLabel"
Align="Right"/>
<!-- Spacer -->
<Control VerticalExpand="True"/>
<!-- Action button grid -->
<BoxContainer
Orientation="Horizontal">
<Button
Name="AttackButton"
Text="{Loc spacevillain-menu-button-attack}"
StyleClasses="OpenRight"
HorizontalExpand="True"/>
<Button
Name="HealButton"
Text="{Loc spacevillain-menu-button-heal}"
StyleClasses="ButtonSquare"
HorizontalExpand="True"/>
<Button
Name="RechargeButton"
Text="{Loc spacevillain-menu-button-recharge}"
StyleClasses="OpenLeft"
HorizontalExpand="True"/>
</BoxContainer>
<!-- New game button -->
<Button
Name="NewGameButton"
Text="{Loc spacevillain-menu-button-new-game}"/>
</BoxContainer>
</DefaultWindow>

View file

@ -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<SharedSpaceVillainArcadeComponent.PlayerAction>? 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;
}
}

View file

@ -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<AmbientMusicPrototype>() || obj.WasModified<RulesPrototype>())
if (obj.WasModified<AmbientMusicPrototype>())
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<RulesPrototype>(_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<RulesPrototype>(amb.Rules)))
if (!_conditions.TryConditions(player, amb.Conditions))
{
continue;
}
return amb;
}

View file

@ -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);
}

View file

@ -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);

View file

@ -6,23 +6,23 @@
VerticalExpand="True"
HorizontalExpand="True"
Margin="10 5 10 10">
<controls:TableContainer Columns="2" HorizontalExpand="True" VerticalExpand="True">
<TableContainer Columns="2" HorizontalExpand="True" VerticalExpand="True">
<RichTextLabel Name="PrimaryCutLabel" Text="{Loc 'cargo-funding-alloc-console-label-primary-cut'}"/>
<SpinBox Name="PrimaryCut"/>
<RichTextLabel Name="LockboxCutLabel" Text="{Loc 'cargo-funding-alloc-console-label-lockbox-cut'}"/>
<SpinBox Name="LockboxCut"/>
</controls:TableContainer>
</TableContainer>
<Label Name="HelpLabel" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="0 10"/>
<PanelContainer VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Top" MaxHeight="250">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E"/>
</PanelContainer.PanelOverride>
<controls:TableContainer Name="EntriesContainer" Columns="4" HorizontalExpand="True" VerticalExpand="True" Margin="5 0">
<TableContainer Name="EntriesContainer" Columns="4" HorizontalExpand="True" VerticalExpand="True" Margin="5 0">
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-account'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-code'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-balance'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-cut'}" HorizontalAlignment="Center"/>
</controls:TableContainer>
</TableContainer>
</PanelContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0">
<Button Name="SaveButton" Text="{Loc 'cargo-funding-alloc-console-button-save'}" Disabled="True"/>

View file

@ -42,7 +42,7 @@
<RichTextLabel Name="InitiatorName" VerticalAlignment="Stretch"/>
<RichTextLabel Name="WantedReason" VerticalAlignment="Stretch"/>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
<controls:TableContainer Name="HistoryTable" Columns="3" Visible="False" HorizontalAlignment="Stretch" />
<TableContainer Name="HistoryTable" Columns="3" Visible="False" HorizontalAlignment="Stretch" />
</BoxContainer>
</ScrollContainer>
</BoxContainer>

View file

@ -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);
}

View file

@ -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;

View file

@ -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;
/// <summary>
/// Makes sprites of other grid-aligned entities like us connect.
/// </summary>
/// <remarks>
/// The system is based on Baystation12's smoothwalling, and thus will work with those.
/// To use, set <c>base</c> equal to the prefix of the corner states in the sprite base RSI.
/// Any objects with the same <c>key</c> will connect.
/// </remarks>
[RegisterComponent]
public sealed partial class IconSmoothComponent : Component
{
[DataField]
public bool Enabled = true;
public (EntityUid?, Vector2i)? LastPosition;
/// <summary>
/// Objects with this key can smooth with us, and vice versa.
/// TODO: YAML HELL, Make smoothing one way.
/// </summary>
[DataField("key", required: true)]
public string SmoothKey { get; private set; }
/// <summary>
/// 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
/// </summary>
[DataField]
public List<string> AdditionalKeys = new();
/// <summary>
/// Index override for our iconsmooth layers. If null, layers will appear on top.
/// </summary>
[DataField]
public int? Index;
/// <summary>
/// Prepended to the RSI state.
/// </summary>
[DataField("base")]
public string StateBase { get; set; } = string.Empty;
[DataField]
public ProtoId<ShaderPrototype>? Shader;
/// <summary>
/// Mode that controls how the icon should be selected.
/// </summary>
[DataField]
public IconSmoothingMode Mode = IconSmoothingMode.Corners;
/// <summary>
/// Used by <see cref="IconSmoothSystem"/> to reduce redundant updates.
/// </summary>
internal int UpdateGeneration { get; set; }
}
/// <summary>
/// Controls the mode with which icon smoothing is calculated.
/// </summary>
[PublicAPI]
public enum IconSmoothingMode : byte
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// The system is based on Baystation12's smoothwalling, and thus will work with those.
/// To use, set <c>base</c> equal to the prefix of the corner states in the sprite base RSI.
/// Any objects with the same <c>key</c> will connect.
/// </remarks>
[RegisterComponent]
public sealed partial class IconSmoothComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("enabled")]
public bool Enabled = true;
public (EntityUid?, Vector2i)? LastPosition;
/// <summary>
/// We will smooth with other objects with the same key.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("key")]
public string? SmoothKey { get; private set; }
/// <summary>
/// Additional keys to smooth with.
/// </summary>
[DataField]
public List<string> AdditionalKeys = new();
/// <summary>
/// Prepended to the RSI state.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("base")]
public string StateBase { get; set; } = string.Empty;
[DataField("shader", customTypeSerializer:typeof(PrototypeIdSerializer<ShaderPrototype>))]
public string? Shader;
/// <summary>
/// Mode that controls how the icon should be selected.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("mode")]
public IconSmoothingMode Mode = IconSmoothingMode.Corners;
/// <summary>
/// Used by <see cref="IconSmoothSystem"/> to reduce redundant updates.
/// </summary>
internal int UpdateGeneration { get; set; }
}
Corners,
/// <summary>
/// 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.
/// </summary>
[PublicAPI]
public enum IconSmoothingMode : byte
{
/// <summary>
/// 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.
/// </summary>
Corners,
CardinalFlags,
/// <summary>
/// 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.
/// </summary>
CardinalFlags,
/// <summary>
/// The icon represents a triangular sprite with only 2 states, representing South / East being occupied or not.
/// </summary>
Diagonal,
/// <summary>
/// The icon represents a triangular sprite with only 2 states, representing South / East being occupied or not.
/// </summary>
Diagonal,
/// <summary>
/// Where this component contributes to our neighbors being calculated but we do not update our own sprite.
/// </summary>
NoSprite,
}
/// <summary>
/// Where this component contributes to our neighbors being calculated but we do not update our own sprite.
/// </summary>
NoSprite,
}

View file

@ -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)
{

View file

@ -36,6 +36,7 @@
<ui:RandomizeLockButton For="Sex" LabelText="{Loc 'humanoid-profile-editor-randomize-sex-button'}"/>
<ui:RandomizeLockButton For="Skin" LabelText="{Loc 'humanoid-profile-editor-randomize-skin-button'}"/>
<ui:RandomizeLockButton For="Eyes" LabelText="{Loc 'humanoid-profile-editor-randomize-eye-button'}"/>
<ui:RandomizeLockButton For="Markings" LabelText="{Loc 'humanoid-profile-editor-randomize-markings-button'}"/>
</GridContainer>
<Button
Name="RandomizeUnlockedButton"

View file

@ -1,4 +1,4 @@
<Control xmlns="https://spacestation14.io"
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
@ -21,6 +21,7 @@
<CheckBox Name="EventMusicCheckBox" Text="{Loc 'ui-options-event-music'}" />
<CheckBox Name="AdminSoundsCheckBox" Text="{Loc 'ui-options-admin-sounds'}" />
<CheckBox Name="BwoinkSoundCheckBox" Text="{Loc 'ui-options-bwoink-sound'}" />
<CheckBox Name="AudioHrtfCheckBox" Text="{Loc 'ui-options-audio-hrtf'}" />
</BoxContainer>
</BoxContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />

View file

@ -72,6 +72,7 @@ public sealed partial class AudioTab : Control
Control.AddOptionCheckBox(CCVars.EventMusicEnabled, EventMusicCheckBox);
Control.AddOptionCheckBox(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox);
Control.AddOptionCheckBox(CCVars.BwoinkSoundEnabled, BwoinkSoundCheckBox);
Control.AddOptionCheckBox(CCVars.AudioHrtf, AudioHrtfCheckBox);
Control.Initialize();
}

View file

@ -2,30 +2,63 @@
xmlns="https://spacestation14.io"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
HorizontalExpand="True"
Orientation="Vertical"
>
Orientation="Vertical">
<customControls:HSeparator></customControls:HSeparator>
<BoxContainer>
<Label StyleClasses="SiliconLawPositionLabel" Name="PositionText" Margin="5 0 0 0"></Label>
<BoxContainer
Orientation="Vertical"
VerticalAlignment="Center"
Margin="0 0 5 0">
<Button
Name="MoveUp"
Text="{Loc silicon-law-ui-minus-one}"
StyleClasses="OpenLeft">
</Button>
<Label
StyleClasses="SiliconLawPositionLabel"
Name="PositionText"
Align="Center">
</Label>
<Button
Name="MoveDown"
Text="{Loc silicon-law-ui-plus-one}"
StyleClasses="OpenRight">
</Button>
</BoxContainer>
<PanelContainer
Margin="20 10 0 0"
Margin="0 10 10 0"
MinHeight="128"
>
HorizontalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1B"></graphics:StyleBoxFlat>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal" SeparationOverride="5">
<TextEdit Name="LawContent" HorizontalExpand="True" Editable="True" MinWidth="500" MinHeight="80"></TextEdit>
<BoxContainer
Orientation="Horizontal"
SeparationOverride="5">
<TextEdit
Name="LawContent"
HorizontalExpand="True"
Editable="True">
</TextEdit>
</BoxContainer>
</PanelContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="0 5 0 0" MaxHeight="64" Align="Begin">
<Button Name="MoveUp" Text="{Loc silicon-law-ui-minus-one}" StyleClasses="OpenRight"></Button>
<Button Name="MoveDown" Text="{Loc silicon-law-ui-plus-one}" StyleClasses="OpenLeft"></Button>
<CheckBox Name="Corrupted" Text="{Loc silicon-law-ui-check-corrupted}" ToolTip="{Loc silicon-law-ui-check-corrupted-tooltip}"></CheckBox>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Align="End" Margin="0 10 5 10">
<Button Name="Delete" Text="{Loc silicon-law-ui-delete}" ModulateSelfOverride="Red"></Button>
<BoxContainer
Orientation="Horizontal"
Margin="5 10 5 10">
<CheckBox
Name="Corrupted"
Text="{Loc silicon-law-ui-check-corrupted}"
ToolTip="{Loc silicon-law-ui-check-corrupted-tooltip}">
</CheckBox>
<BoxContainer
HorizontalExpand="True"> <!--> This is here just to seperate the two buttons on the UI. I hate this. <!-->
</BoxContainer>
<Button
Name="Delete"
Text="{Loc silicon-law-ui-delete}"
ModulateSelfOverride="Red"
Margin="5 0 0 0">
</Button>
</BoxContainer>
</BoxContainer>

View file

@ -2,6 +2,7 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Client.Silicons.Laws.SiliconLawEditUi;
@ -9,10 +10,9 @@ namespace Content.Client.Silicons.Laws.SiliconLawEditUi;
[GenerateTypedNameReferences]
public sealed partial class SiliconLawContainer : BoxContainer
{
public const string StyleClassSiliconLawPositionLabel = "SiliconLawPositionLabel";
[Dependency] private IRobustRandom _random = default!;
public static readonly string CorruptedString =
Loc.GetString("ion-storm-law-scrambled-number", ("length", 5));
public const string StyleClassSiliconLawPositionLabel = "SiliconLawPositionLabel";
private SiliconLaw? _law;
@ -24,6 +24,7 @@ public sealed partial class SiliconLawContainer : BoxContainer
public SiliconLawContainer()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
MoveUp.OnPressed += _ => MoveLawUp?.Invoke(_law!);
MoveDown.OnPressed += _ => MoveLawDown?.Invoke(_law!);
@ -31,11 +32,22 @@ public sealed partial class SiliconLawContainer : BoxContainer
{
if (Corrupted.Pressed)
{
_law!.LawIdentifierOverride = CorruptedString;
_law!.LawIdentifierOverride = Loc.GetString(
"ion-storm-law-scrambled-number",
(
"length",
_random.Next(
SharedSiliconLawSystem.IonStormIdentifierMinLength,
SharedSiliconLawSystem.IonStormIdentifierMaxLength
)
)
);
_law!.Corrupted = true;
}
else
{
_law!.LawIdentifierOverride = null;
_law!.Corrupted = false;
}
};
@ -49,13 +61,6 @@ public sealed partial class SiliconLawContainer : BoxContainer
_law = law;
LawContent.TextRope = new Rope.Leaf(Loc.GetString(law.LawString));
PositionText.Text = law.Order.ToString();
if (!string.IsNullOrEmpty(law.LawIdentifierOverride))
{
Corrupted.Pressed = true;
}
else
{
Corrupted.Pressed = false;
}
Corrupted.Pressed = law.Corrupted;
}
}

View file

@ -2,21 +2,42 @@
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc silicon-law-ui-title}"
MinSize="560 400"
>
MinSize="300 225"
SetSize="450 525">
<!-->
this shit does not layout properly unless I put the horizontal boxcontainer inside of a vertical one
????
<!-->
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" Align="End">
<Button Name="NewLawButton" Text="{Loc silicon-law-ui-new-law}" MaxSize="256 64" StyleClasses="OpenRight"></Button>
<Button Name="Save" Text="{Loc silicon-law-ui-save}" MaxSize="256 64" Access="Public" StyleClasses="OpenLeft"></Button>
<BoxContainer
Orientation="Vertical">
<BoxContainer
Orientation="Horizontal"
Align="Center">
<Button
Name="NewLawButton"
Text="{Loc silicon-law-ui-new-law}"
MaxSize="256 64"
StyleClasses="OpenRight">
</Button>
<Button
Name="Save"
Text="{Loc silicon-law-ui-save}"
MaxSize="256 64"
Access="Public"
StyleClasses="OpenLeft">
</Button>
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Margin="4 60 0 0">
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Name="LawContainer" Access="Public" VerticalExpand="True" />
<BoxContainer
Orientation="Vertical"
Margin="4 31 5 5">
<ScrollContainer
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer
Orientation="Vertical"
Name="LawContainer"
Access="Public" />
</ScrollContainer>
</BoxContainer>
</controls:FancyWindow>

View file

@ -1,19 +1,27 @@
<Control xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="0 0 0 10">
<PanelContainer VerticalExpand="True" StyleClasses="BackgroundDark">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
Margin="5 5 5 5">
<RichTextLabel Name="LawNumberLabel" StyleClasses="LabelKeyText"/>
<customControls:HSeparator Margin="0 5 0 5"/>
<RichTextLabel Name="LawLabel"/>
<BoxContainer Name="LawAnnouncementButtons" Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
<Label Text="{Loc laws-ui-state-law}" StyleClasses="highlight"/>
<Control Margin="5 0 0 0" />
<!-- Buttons are added via C# code-->
<Button
xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="0 0 0 5"
StyleClasses="ButtonSquare">
<PanelContainer
VerticalExpand="True"
Margin="0 5"
StyleClasses="BackgroundDark">
<BoxContainer
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
Margin="5 5 5 5">
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="LawNumberLabel" StyleClasses="LabelKeyText" />
<BoxContainer
HorizontalExpand="True"> <!--> This is here just to seperate the two elements on the UI. I hate this. <!-->
</BoxContainer>
<TextureRect Name="LawSelectedIcon" TexturePath="/Textures/Interface/Alerts/Abilities/silenced.png" />
</BoxContainer>
<customControls:HSeparator Margin="0 5 0 5" />
<RichTextLabel Name="LawLabel" />
</BoxContainer>
</PanelContainer>
</Control>
</Button>

View file

@ -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);
/// <summary>
/// The SiliconLaw tracked by this display.
/// </summary>
public SiliconLaw Law { get; }
private readonly Dictionary<Button, TimeSpan> _nextAllowedPress = new();
public LawDisplay(EntityUid uid, SiliconLaw law, HashSet<ProtoId<RadioChannelPrototype>>? 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<SpeechComponent>(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<RadioChannelPrototype>(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)
/// <summary>
/// Switches the indicator of this display depending on whether its law has been selected
/// </summary>
/// <param name="isSelected">Whether or not the law associated with this display is selected</param>
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;
}
}
}

View file

@ -1,15 +1,34 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'laws-ui-menu-title'}"
MinSize="200 100"
SetSize="450 515">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<PanelContainer VerticalExpand="True" Margin="10 10 10 10">
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'laws-ui-menu-title'}"
MinSize="450 225"
SetSize="450 525">
<BoxContainer
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer
Orientation="Horizontal"
Align="Center"
Margin="0 10 0 0"
HorizontalExpand="True">
<Button
Name="LawSelectAllButton"
Text="{Loc 'laws-ui-select-all'}"
ToolTip="{Loc 'laws-ui-select-all-tooltip'}"
MaxSize="256 64"
StyleClasses="OpenRight" />
<Button
Name="LawSelectNoneButton"
Text="{Loc 'laws-ui-select-none'}"
MaxSize="256 64"
StyleClasses="OpenLeft" />
</BoxContainer>
<PanelContainer VerticalExpand="True" Margin="10 10 10 5">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E"/>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<ScrollContainer
HScrollEnabled="False"
@ -19,9 +38,39 @@
Name="LawDisplayContainer"
Orientation="Vertical"
VerticalExpand="True"
Margin="10 10 10 0">
Margin="10 10 10 5">
</BoxContainer>
</ScrollContainer>
</PanelContainer>
<BoxContainer
Orientation="Horizontal"
Align="Center"
HorizontalExpand="True">
<OptionButton
Name="LawChatChannelOption"
MaxSize="256 64"
StyleClasses="OpenRight" />
<Button
Name="LawAnnounceButton"
Text="{Loc 'laws-ui-state-law'}"
MaxSize="256 64"
StyleClasses="OpenLeft" />
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical" Margin="0 5 0 0">
<PanelContainer />
<PanelContainer StyleClasses="LowDivider" />
<!--Todo: Change Logo based on Laws (Change to Syndicate Logo when Emagged)-->
<BoxContainer Name="Footer" Orientation="Horizontal" Margin="10 0 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'laws-window-footer-left'}" StyleClasses="WindowFooterText" />
<Label
Name="Version" Text="{Loc 'laws-window-footer-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect
StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View file

@ -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<ChatSelectChannel> SelectableChatChannels =
new() { ChatSelectChannel.Local, ChatSelectChannel.Whisper };
private int _selectedChatChannelIdx;
private readonly List<RadioChannelPrototype> _selectableRadioChannels = new();
private readonly HashSet<SiliconLaw> _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<SpeechComponent>(_owner, out _)
|| !_entityManager.System<ActionBlockerSystem>().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;
}
}

View file

@ -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<SlurredAccentComponent>? ent = null)
{
return message;
}

View file

@ -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<StutteringAccentComponent>? ent = null)
{
return message;
}

View file

@ -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<SpeciesPrototype>(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",

View file

@ -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<GeneralStationRecordConsoleWindow>();
_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();
}
}

View file

@ -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<GeneralStationRecordConsoleComponent> 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<StationRecordsComponent> ent, ref AfterAutoHandleStateEvent args)
{
var query = EntityQueryEnumerator<GeneralStationRecordConsoleComponent>();
while (query.MoveNext(out var uid, out _))
{
if (_ui.TryGetOpenUi(uid, GeneralStationRecordConsoleKey.Key, out var bui))
bui.Update();
}
}
}

View file

@ -74,24 +74,28 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
};
}
public void UpdateState(GeneralStationRecordConsoleState state)
public void UpdateState(uint? selectedKey,
GeneralStationRecord? record,
Dictionary<uint, string>? 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
{

View file

@ -1,7 +0,0 @@
using Content.Shared.StationRecords;
namespace Content.Client.StationRecords;
public sealed class StationRecordsSystem : SharedStationRecordsSystem
{
}

View file

@ -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));

View file

@ -26,6 +26,10 @@ public sealed class LabelSheetlet : Sheetlet<PalettedStylesheet>
.Class(StyleClass.LabelHeadingBigger)
.Font(sheet.BaseFont.GetFont(20, FontKind.Bold))
.FontColor(sheet.HighlightPalette.Text),
E<Label>()
.Class(StyleClass.LabelSubHeading)
.Font(sheet.BaseFont.GetFont(14, FontKind.Italic))
.FontColor(sheet.HighlightPalette.TextDark),
E<Label>()
.Class(StyleClass.LabelSubText)
.Font(sheet.BaseFont.GetFont(10))

View file

@ -21,6 +21,13 @@ public sealed class PanelSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet,
{
BackgroundColor = sheet.SecondaryPalette.BackgroundDark,
};
var boxInsetDark = new StyleBoxFlat()
{
BackgroundColor = sheet.SecondaryPalette.BackgroundDark,
BorderColor = sheet.PrimaryPalette.Background,
BorderThickness = new Thickness(2f),
};
var boxPositive = new StyleBoxFlat { BackgroundColor = sheet.PositivePalette.Background };
var boxNegative = new StyleBoxFlat { BackgroundColor = sheet.NegativePalette.Background };
var boxHighlight = new StyleBoxFlat { BackgroundColor = sheet.HighlightPalette.Background };
@ -36,6 +43,7 @@ public sealed class PanelSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet,
E<PanelContainer>().Class(StyleClass.PanelLight).Panel(boxLight),
E<PanelContainer>().Class(StyleClass.PanelDark).Panel(boxDark),
E<PanelContainer>().Class(StyleClass.PanelDropTarget).Panel(boxDropTarget),
E<PanelContainer>().Class(StyleClass.PanelInsetDark).Panel(boxInsetDark),
E<PanelContainer>().Class(StyleClass.Positive).Panel(boxPositive),
E<PanelContainer>().Class(StyleClass.Negative).Panel(boxNegative),

View file

@ -37,6 +37,7 @@ public static class StyleClass
public const string LabelHeading = "LabelHeading";
public const string LabelHeadingBigger = "LabelHeadingBigger";
public const string LabelSubHeading = "LabelSubHeading";
public const string LabelSubText = "LabelSubText";
public const string LabelKeyText = "LabelKeyText";
public const string LabelWeak = "LabelWeak"; // replaces `LabelSecondaryColor`
@ -52,6 +53,7 @@ public static class StyleClass
public const string PanelDark = "PanelDark";
public const string PanelLight = "PanelLight";
public const string PanelDropTarget = "PanelDropTarget";
public const string PanelInsetDark = "PanelInsetDark";
public const string ButtonOpenRight = "OpenRight";
public const string ButtonOpenLeft = "OpenLeft";

View file

@ -1,285 +0,0 @@
using System.Numerics;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Controls;
// This control is not part of engine because I quickly wrote it in 2 hours at 2 AM and don't want to deal with
// API stabilization and/or figuring out relation to GridContainer.
// Grid layout is a complicated problem and I don't want to commit another half-baked thing into the engine.
// It's probably sufficient for its use case (RichTextLabel tables for rules/guidebook).
// Despite that, it's still better comment the shit half of you write on a regular basis.
//
// EMO: thank you PJB i was going to kill myself.
/// <summary>
/// Displays children in a tabular grid. Unlike <see cref="GridContainer"/>,
/// properly handles layout constraints so putting word-wrapping <see cref="RichTextLabel"/> in it should work.
/// </summary>
/// <remarks>
/// All children are automatically laid out in <see cref="Columns"/> columns.
/// The first control is in the top left, laid out per row from there.
/// </remarks>
[Virtual]
public class TableContainer : Container
{
private int _columns = 1;
/// <summary>
/// The absolute minimum width a column can be forced to.
/// </summary>
/// <remarks>
/// <para>
/// If a column *asks* for less width than this (small contents), it can still be smaller.
/// But if it asks for more it cannot go below this width.
/// </para>
/// </remarks>
public float MinForcedColumnWidth { get; set; } = 50;
// Scratch space used while calculating layout, cached to avoid regular allocations during layout pass.
private ColumnData[] _columnDataCache = [];
private RowData[] _rowDataCache = [];
/// <summary>
/// How many columns should be displayed.
/// </summary>
public int Columns
{
get => _columns;
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 1, nameof(value));
_columns = value;
}
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
ResetCachedArrays();
// Do a first pass measuring all child controls as if they're given infinite space.
// This gives us a maximum width the columns want, which we use to proportion them later.
var columnIdx = 0;
foreach (var child in Children)
{
ref var column = ref _columnDataCache[columnIdx];
child.Measure(new Vector2(float.PositiveInfinity, float.PositiveInfinity));
column.MaxWidth = Math.Max(column.MaxWidth, child.DesiredSize.X);
columnIdx += 1;
if (columnIdx == _columns)
columnIdx = 0;
}
// Calculate Slack and MinWidth for all columns. Also calculate sums for all columns.
var totalMinWidth = 0f;
var totalMaxWidth = 0f;
var totalSlack = 0f;
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
column.MinWidth = Math.Min(column.MaxWidth, MinForcedColumnWidth);
column.Slack = column.MaxWidth - column.MinWidth;
totalMinWidth += column.MinWidth;
totalMaxWidth += column.MaxWidth;
totalSlack += column.Slack;
}
if (totalMaxWidth <= availableSize.X)
{
// We want less horizontal space than we're given. Huh, that's convenient.
// Just set assigned width to be however much they asked for.
// We could probably skip the second measure pass in this scenario,
// but that's just an optimization, so I don't care right now.
//
// There's probably a very clever way to make this behavior work with the else block of logic,
// just by fiddling with the math.
// I'm dumb, it's 4:30 AM. Yeah, I *started* at 2 AM.
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
column.AssignedWidth = column.MaxWidth;
}
}
else
{
// We don't have enough horizontal space,
// at least without causing *some* sort of word wrapping (assuming text contents).
//
// Assign horizontal space proportional to the wanted maximum size of the columns.
var assignableWidth = Math.Max(0, availableSize.X - totalMinWidth);
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
var slackRatio = column.Slack / totalSlack;
column.AssignedWidth = column.MinWidth + slackRatio * assignableWidth;
}
}
// Go over controls for a second measuring pass, this time giving them their assigned measure width.
// This will give us a height to slot into per-row data.
// We still measure assuming infinite vertical space.
// This control can't properly handle being constrained on the Y axis.
columnIdx = 0;
var rowIdx = 0;
foreach (var child in Children)
{
ref var column = ref _columnDataCache[columnIdx];
ref var row = ref _rowDataCache[rowIdx];
child.Measure(new Vector2(column.AssignedWidth, float.PositiveInfinity));
row.MeasuredHeight = Math.Max(row.MeasuredHeight, child.DesiredSize.Y);
columnIdx += 1;
if (columnIdx == _columns)
{
columnIdx = 0;
rowIdx += 1;
}
}
// Sum up height of all rows to get final measured table height.
var totalHeight = 0f;
for (var r = 0; r < _rowDataCache.Length; r++)
{
ref var row = ref _rowDataCache[r];
totalHeight += row.MeasuredHeight;
}
return new Vector2(Math.Min(availableSize.X, totalMaxWidth), totalHeight);
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
// TODO: Expand to fit given vertical space.
// Calculate MinWidth and Slack sums again from column data.
// We could've cached these from measure but whatever.
var totalMinWidth = 0f;
var totalSlack = 0f;
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
totalMinWidth += column.MinWidth;
totalSlack += column.Slack;
}
// Calculate new width based on final given size, also assign horizontal positions of all columns.
var assignableWidth = Math.Max(0, finalSize.X - totalMinWidth);
var xPos = 0f;
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
var slackRatio = column.Slack / totalSlack;
column.ArrangedWidth = column.MinWidth + slackRatio * assignableWidth;
column.ArrangedX = xPos;
xPos += column.ArrangedWidth;
}
// Do actual arrangement row-by-row.
var arrangeY = 0f;
for (var r = 0; r < _rowDataCache.Length; r++)
{
ref var row = ref _rowDataCache[r];
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
var index = c + r * _columns;
if (index >= ChildCount) // Quit early if we don't actually fill out the row.
break;
var child = GetChild(c + r * _columns);
child.Arrange(UIBox2.FromDimensions(column.ArrangedX, arrangeY, column.ArrangedWidth, row.MeasuredHeight));
}
arrangeY += row.MeasuredHeight;
}
return finalSize with { Y = arrangeY };
}
/// <summary>
/// Ensure cached array space is allocated to correct size and is reset to a clean slate.
/// </summary>
private void ResetCachedArrays()
{
// 1-argument Array.Clear() is not currently available in sandbox (added in .NET 6).
if (_columnDataCache.Length != _columns)
_columnDataCache = new ColumnData[_columns];
Array.Clear(_columnDataCache, 0, _columnDataCache.Length);
var rowCount = ChildCount / _columns;
if (ChildCount % _columns != 0)
rowCount += 1;
if (rowCount != _rowDataCache.Length)
_rowDataCache = new RowData[rowCount];
Array.Clear(_rowDataCache, 0, _rowDataCache.Length);
}
/// <summary>
/// Per-column data used during layout.
/// </summary>
private struct ColumnData
{
// Measure data.
/// <summary>
/// The maximum width any control in this column wants, if given infinite space.
/// Maximum of all controls on the column.
/// </summary>
public float MaxWidth;
/// <summary>
/// The minimum width this column may be given.
/// This is either <see cref="MaxWidth"/> or <see cref="TableContainer.MinForcedColumnWidth"/>.
/// </summary>
public float MinWidth;
/// <summary>
/// Difference between max and min width; how much this column can expand from its minimum.
/// </summary>
public float Slack;
/// <summary>
/// How much horizontal space this column was assigned at measure time.
/// </summary>
public float AssignedWidth;
// Arrange data.
/// <summary>
/// How much horizontal space this column was assigned at arrange time.
/// </summary>
public float ArrangedWidth;
/// <summary>
/// The horizontal position this column was assigned at arrange time.
/// </summary>
public float ArrangedX;
}
private struct RowData
{
// Measure data.
/// <summary>
/// How much height the tallest control on this row was measured at,
/// measuring for infinite vertical space but assigned column width.
/// </summary>
public float MeasuredHeight;
}
}

View file

@ -0,0 +1,11 @@
using Content.Shared.Wall;
using Robust.Shared.ComponentTrees;
using Robust.Shared.Physics;
namespace Content.Client.Wall.Components;
[RegisterComponent]
public sealed partial class WallMountTreeComponent : Component, IComponentTreeComponent<WallMountComponent>
{
public DynamicTree<ComponentTreeEntry<WallMountComponent>> Tree { get; set; } = default!;
}

View file

@ -0,0 +1,22 @@
using System.Numerics;
using Content.Client.Wall.Components;
using Content.Shared.Wall;
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.Physics;
namespace Content.Client.Wall.Systems;
public sealed partial class WallMountTreeSystem : ComponentTreeSystem<WallMountTreeComponent, WallMountComponent>
{
[Dependency] private SpriteSystem _sprite = default!;
protected override bool DoFrameUpdate => true;
protected override bool DoTickUpdate => false;
protected override bool Recursive => false;
protected override Box2 ExtractAabb(in ComponentTreeEntry<WallMountComponent> entry, Vector2 pos, Angle rot)
{
return _sprite.CalculateBounds((entry.Uid, Comp<SpriteComponent>(entry.Uid)), pos, rot, default).CalcBoundingBox();
}
}

View file

@ -0,0 +1,187 @@
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Tag;
using Content.Shared.Wall;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Wall.Systems;
/// <summary>
/// Manages the directional visibility overlay for wall-mounted entities.
/// </summary>
public sealed partial class WallMountVisibilitySystem : EntitySystem
{
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IOverlayManager _overlay = default!;
[Dependency] private SharedMapSystem _map = default!;
[Dependency] private SpriteSystem _sprite = default!;
[Dependency] private TagSystem _tag = default!;
[Dependency] private TransformSystem _xform = default!;
[Dependency] private WallMountTreeSystem _tree = default!;
[Dependency] private EntityQuery<MapGridComponent> _gridQuery = default!;
[Dependency] private EntityQuery<SpriteComponent> _spriteQuery = default!;
/// <summary>
/// Tags that block visibility when present on the same tile.
/// </summary>
private static readonly ProtoId<TagPrototype>[] BlockingTags = ["Wall"];
/// <summary>
/// Caches for whether a tile has any blocking entity.
/// </summary>
private readonly Dictionary<(EntityUid Grid, Vector2i Tile), bool> _tileCache = [];
private WallMountVisibilityOverlay _overlayInstance = default!;
/// <summary>
/// Whether directional visibility is currently enabled.
/// </summary>
internal bool DirectionalVisibilityEnabled = true;
public override void Initialize()
{
base.Initialize();
_overlayInstance = new WallMountVisibilityOverlay(_timing, _map, _sprite, _xform, _tree, this, _gridQuery, _spriteQuery);
Subs.CVar(_cfg, CCVars.WallMountDirectionalVisibility, OnDirectionalVisibilityChanged, true);
}
public override void Shutdown()
{
base.Shutdown();
_overlay.RemoveOverlay(_overlayInstance);
}
private void OnDirectionalVisibilityChanged(bool enabled)
{
DirectionalVisibilityEnabled = enabled;
if (enabled)
_overlay.AddOverlay(_overlayInstance);
else
{
_overlay.RemoveOverlay(_overlayInstance);
SetAllVisible(true);
}
}
/// <summary>
/// Invalidates tile cache when anchor state changes for a blocking entity.
/// </summary>
[SubscribeLocalEvent]
private void OnTagAnchorChanged(Entity<TagComponent> ent, ref AnchorStateChangedEvent args)
{
if (!_tag.HasAnyTag(ent.Comp, BlockingTags))
return;
var xform = args.Transform;
if (xform.GridUid is not { } gridUid)
return;
if (!_gridQuery.TryGetComponent(gridUid, out var grid))
return;
var tile = _map.TileIndicesFor(gridUid, grid, xform.Coordinates);
_tileCache.Remove((gridUid, tile));
}
/// <summary>
/// Makes the entity visible again on component shutdown.
/// </summary>
[SubscribeLocalEvent]
private void OnWallMountShutdown(Entity<WallMountComponent> ent, ref ComponentShutdown args)
{
if (TerminatingOrDeleted(ent))
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
_sprite.SetVisible((ent, sprite), true);
}
/// <summary>
/// Makes the entity visible again if directional visibility is disabled for this mount.
/// </summary>
[SubscribeLocalEvent]
private void OnWallMountAfterHandleState(Entity<WallMountComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.DirectionalVisibility)
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
_sprite.SetVisible((ent, sprite), true);
}
/// <summary>
/// Removes all cached entries for a grid that is being removed.
/// </summary>
[SubscribeLocalEvent]
private void OnGridRemoval(GridRemovalEvent ev)
{
foreach (var key in _tileCache.Keys.Where(k => k.Grid == ev.EntityUid).ToList())
{
_tileCache.Remove(key);
}
}
/// <summary>
/// Clears tile cache and resets all wall-mount visibility on round restart.
/// </summary>
[SubscribeNetworkEvent]
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
_tileCache.Clear();
SetAllVisible(true);
}
/// <summary>
/// Forces all wall-mount entities to become visible or hidden.
/// </summary>
internal void SetAllVisible(bool visible)
{
var query = AllEntityQuery<WallMountComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out _, out var sprite))
{
_sprite.SetVisible((uid, sprite), visible);
}
}
/// <summary>
/// Checks whether the tile contains any anchored blocking entity.
/// </summary>
internal bool IsTileBlocked(EntityUid gridUid, Vector2i tile, EntityUid? ignoreUid = null)
{
if (!_gridQuery.TryGetComponent(gridUid, out var grid))
return false;
var key = (gridUid, tile);
if (_tileCache.TryGetValue(key, out var cached))
return cached;
var enumerator = _map.GetAnchoredEntitiesEnumerator(gridUid, grid, tile);
while (enumerator.MoveNext(out var anchored))
{
if (anchored == ignoreUid)
continue;
if (!_tag.HasAnyTag(anchored.Value, BlockingTags))
continue;
return _tileCache[key] = true;
}
return _tileCache[key] = false;
}
}

View file

@ -0,0 +1,88 @@
using Robust.Client.GameObjects;
namespace Content.Client.Wall;
/// <summary>
/// Tracks fade progress for a wall-mounted entity.
/// </summary>
internal struct WallMountFadeState
{
public float OriginalAlpha;
public float CurrentAlpha;
public float TargetAlpha;
/// <summary>
/// Sprite alpha after applying the fade multiplier.
/// </summary>
public readonly float EffectiveAlpha => OriginalAlpha * CurrentAlpha;
/// <summary>
/// Whether the entity should be rendered as visible at the current fade level.
/// </summary>
public readonly bool IsVisible => CurrentAlpha > 0f;
/// <summary>
/// Creates a new fade state snapped immediately to the given target alpha.
/// </summary>
public static WallMountFadeState Snapped(float originalAlpha, float targetAlpha)
{
return new()
{
OriginalAlpha = originalAlpha,
CurrentAlpha = targetAlpha,
TargetAlpha = targetAlpha,
};
}
/// <summary>
/// Moves <see cref="CurrentAlpha"/> one step towards <see cref="TargetAlpha"/>.
/// </summary>
public void StepTowards(float fadeStep)
{
if (MathHelper.CloseTo(CurrentAlpha, TargetAlpha))
return;
var delta = TargetAlpha - CurrentAlpha;
var step = MathF.Sign(delta) * MathF.Min(MathF.Abs(delta), fadeStep);
CurrentAlpha = Math.Clamp(CurrentAlpha + step, 0f, 1f);
}
}
/// <summary>
/// Per-viewport fade state for wall-mounted entities.
/// </summary>
internal sealed class ViewportFadeState(SpriteSystem sprite, EntityQuery<SpriteComponent> spriteQuery) : IDisposable
{
private readonly SpriteSystem _sprite = sprite;
private readonly EntityQuery<SpriteComponent> _spriteQuery = spriteQuery;
/// <summary>
/// Fade states for entities tracked in this viewport.
/// </summary>
public readonly Dictionary<EntityUid, WallMountFadeState> FadeStates = [];
/// <summary>
/// Entities visible this frame.
/// </summary>
public readonly HashSet<EntityUid> SeenThisFrame = [];
/// <summary>
/// Whether FOV was enabled during the previous frame.
/// </summary>
public bool WasFovEnabled = true;
public void Dispose()
{
foreach (var (uid, state) in FadeStates)
{
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
continue;
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.OriginalAlpha));
_sprite.SetVisible((uid, sprite), true);
}
FadeStates.Clear();
SeenThisFrame.Clear();
}
}

View file

@ -0,0 +1,242 @@
using System.Linq;
using System.Numerics;
using Content.Client.Graphics;
using Content.Client.Wall.Systems;
using Content.Shared.Wall;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
namespace Content.Client.Wall;
/// <summary>
/// Renders wall-mounted entities conditionally based on their facing arc relative to the viewport's eye.
/// </summary>
public sealed partial class WallMountVisibilityOverlay(
IGameTiming timing,
SharedMapSystem map,
SpriteSystem sprite,
TransformSystem xform,
WallMountTreeSystem tree,
WallMountVisibilitySystem visibility,
EntityQuery<MapGridComponent> gridQuery,
EntityQuery<SpriteComponent> spriteQuery) : Overlay
{
private readonly IGameTiming _timing = timing;
private readonly SharedMapSystem _map = map;
private readonly SpriteSystem _sprite = sprite;
private readonly TransformSystem _xform = xform;
private readonly WallMountTreeSystem _tree = tree;
private readonly WallMountVisibilitySystem _visibility = visibility;
private readonly EntityQuery<MapGridComponent> _gridQuery = gridQuery;
private readonly EntityQuery<SpriteComponent> _spriteQuery = spriteQuery;
/// <summary>
/// Caches <see cref="ViewportFadeState"/> instances per viewport.
/// </summary>
private readonly OverlayResourceCache<ViewportFadeState> _fadeCache = new();
/// <summary>
/// Original sprite alphas, shared across viewports to avoid capturing values modified by another viewport.
/// </summary>
private readonly Dictionary<EntityUid, float> _originalAlphas = [];
/// <summary>
/// Alpha change per second during fade.
/// </summary>
private const float FadeSpeed = 9f;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
protected override void Draw(in OverlayDrawArgs args)
{
if (!_visibility.DirectionalVisibilityEnabled)
return;
if (args.Viewport.Eye is not { } eye)
return;
var viewportState = _fadeCache.GetForViewport(args.Viewport, _ => new ViewportFadeState(_sprite, _spriteQuery));
if (!eye.DrawFov)
{
HandleFovDisabled(args, viewportState);
return;
}
viewportState.WasFovEnabled = true;
var fadeStep = FadeSpeed * (float)_timing.FrameTime.TotalSeconds;
var matrix = args.Viewport.GetWorldToLocalMatrix();
viewportState.SeenThisFrame.Clear();
ProcessVisibleEntities(args, eye, matrix, fadeStep, viewportState);
// Remove entities that left the viewport this frame.
foreach (var uid in viewportState.FadeStates.Keys.Except(viewportState.SeenThisFrame).ToList())
{
RemoveTrackedEntity(uid, viewportState);
}
ApplyFadeToVisibleEntities(viewportState);
}
/// <summary>
/// When FOV gets disabled, clears fade state and restores sprites.
/// On subsequent frames, keeps wall-mounts visible and restores alpha modified by other viewports.
/// </summary>
private void HandleFovDisabled(in OverlayDrawArgs args, ViewportFadeState viewportState)
{
if (viewportState.WasFovEnabled)
{
ClearViewportFadeState(viewportState);
viewportState.WasFovEnabled = false;
}
// Restore alpha modified by other viewports.
foreach (var entity in _tree.QueryAabb(args.MapId, args.WorldBounds))
{
var uid = entity.Uid;
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
continue;
if (_originalAlphas.Remove(uid, out var origAlpha))
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(origAlpha));
_sprite.SetVisible((uid, sprite), true);
}
}
/// <summary>
/// Updates fade state for all wall-mounted entities in the viewport.
/// </summary>
private void ProcessVisibleEntities(in OverlayDrawArgs args, IEye eye, Matrix3x2 matrix, float fadeStep, ViewportFadeState viewportState)
{
foreach (var entity in _tree.QueryAabb(args.MapId, args.WorldBounds))
{
var (wallmount, xform) = entity;
var uid = entity.Uid;
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
continue;
// Capture original alpha before any viewport modifies it.
if (!_originalAlphas.TryGetValue(uid, out var originalAlpha))
_originalAlphas[uid] = originalAlpha = sprite.Color.A;
viewportState.SeenThisFrame.Add(uid);
var targetAlpha = ComputeTargetAlpha(uid, wallmount, xform, eye, matrix);
UpdateFadeState(uid, originalAlpha, targetAlpha, fadeStep, viewportState);
}
}
/// <summary>
/// Returns 1 if the entity is within its facing arc relative to the eye, 0 otherwise.
/// </summary>
private float ComputeTargetAlpha(EntityUid uid, WallMountComponent wallmount, TransformComponent xform, IEye eye, Matrix3x2 matrix)
{
if (!wallmount.DirectionalVisibility || wallmount.Arc >= Math.Tau)
return 1f;
if (xform.GridUid is not { } gridUid || !_gridQuery.TryGetComponent(gridUid, out var grid))
return 1f;
var tile = _map.TileIndicesFor(gridUid, grid, xform.Coordinates);
if (!_visibility.IsTileBlocked(gridUid, tile, uid))
return 1f;
var (pos, rot) = _xform.GetWorldPositionRotation(xform);
var facingAngle = rot + eye.Rotation + wallmount.Direction;
var entityScreenPos = Vector2.Transform(pos, matrix);
var eyeScreenPos = Vector2.Transform(eye.Position.Position, matrix);
var toEntity = entityScreenPos - eyeScreenPos;
var eyeToEntityAngle = (toEntity with { X = -toEntity.X }).ToWorldAngle();
var angleDiff = Angle.ShortestDistance(eyeToEntityAngle, facingAngle);
return Math.Abs(angleDiff) < wallmount.Arc / 2 ? 1f : 0f;
}
/// <summary>
/// Applies the current fade state to all entities seen this frame.
/// </summary>
private void ApplyFadeToVisibleEntities(ViewportFadeState viewportState)
{
foreach (var uid in viewportState.SeenThisFrame)
{
if (!viewportState.FadeStates.TryGetValue(uid, out var state))
continue;
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
{
RemoveTrackedEntity(uid, viewportState);
continue;
}
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.EffectiveAlpha));
_sprite.SetVisible((uid, sprite), state.IsVisible);
}
}
/// <summary>
/// Updates fade target for a tracked entity. Newly seen entities snap immediately without fading.
/// </summary>
private static void UpdateFadeState(EntityUid uid, float originalAlpha, float targetAlpha, float fadeStep, ViewportFadeState viewportState)
{
if (viewportState.FadeStates.TryGetValue(uid, out var state))
{
state.TargetAlpha = targetAlpha;
state.StepTowards(fadeStep);
viewportState.FadeStates[uid] = state;
return;
}
viewportState.FadeStates[uid] = WallMountFadeState.Snapped(originalAlpha, targetAlpha);
}
private void RestoreSprite(EntityUid uid, WallMountFadeState state)
{
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
return;
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.OriginalAlpha));
_sprite.SetVisible((uid, sprite), true);
}
/// <summary>
/// Removes an entity from fade tracking and restores its sprite.
/// </summary>
private void RemoveTrackedEntity(EntityUid uid, ViewportFadeState viewportState)
{
if (viewportState.FadeStates.Remove(uid, out var state))
RestoreSprite(uid, state);
_originalAlphas.Remove(uid);
}
/// <summary>
/// Clears all fade tracking for a viewport and restores sprites.
/// </summary>
private void ClearViewportFadeState(ViewportFadeState viewportState)
{
foreach (var (uid, state) in viewportState.FadeStates)
{
RestoreSprite(uid, state);
_originalAlphas.Remove(uid);
}
viewportState.FadeStates.Clear();
viewportState.SeenThisFrame.Clear();
}
protected override void DisposeBehavior()
{
_fadeCache.Dispose();
base.DisposeBehavior();
}
}

View file

@ -7,13 +7,16 @@ namespace Content.Client.Weapons.Melee.UI;
/// <summary>
/// Initializes a <see cref="MeleeSpeechWindow"/> and updates it when new server messages are received.
/// </summary>
public sealed class MeleeSpeechBoundUserInterface : BoundUserInterface
public sealed partial class MeleeSpeechBoundUserInterface : BoundUserInterface
{
[Dependency] private IEntityManager _entManager = default!;
[ViewVariables]
private MeleeSpeechWindow? _window;
public MeleeSpeechBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
@ -21,7 +24,14 @@ public sealed class MeleeSpeechBoundUserInterface : BoundUserInterface
base.Open();
_window = this.CreateWindow<MeleeSpeechWindow>();
_window.OnBattlecryEntered += OnBattlecryChanged;
if (_entManager.TryGetComponent(Owner, out MeleeSpeechComponent? speech))
{
_window.SetInitialBattlecry(speech!.Battlecry);
_window.SetMaxBattlecryLength(speech!.MaxBattlecryLength);
}
_window.OnBattlecryChanged += OnBattlecryChanged;
}
private void OnBattlecryChanged(string newBattlecry)

View file

@ -1,7 +1,9 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Loc melee-speech-menu-title}">
<BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="150" >
Title="{Loc melee-speech-menu-title}"
SetWidth="150"
MinWidth="150">
<BoxContainer Orientation="Vertical" SeparationOverride="4" >
<Label Name="CurrentBattlecry" Text="{Loc 'melee-speech-current-battlecry'}" />
<LineEdit Name="BattlecryLineEdit" />
</BoxContainer>

View file

@ -7,22 +7,37 @@ namespace Content.Client.Weapons.Melee.UI;
[GenerateTypedNameReferences]
public sealed partial class MeleeSpeechWindow : DefaultWindow
{
public event Action<string>? OnBattlecryEntered;
public event Action<string>? OnBattlecryChanged;
public MeleeSpeechWindow()
{
RobustXamlLoader.Load(this);
BattlecryLineEdit.OnTextEntered += e => OnBattlecryEntered?.Invoke(e.Text);
BattlecryLineEdit.OnTextChanged += e => OnBattlecryChanged?.Invoke(e.Text);
}
public void SetCurrentBattlecry(string battlecry)
{
BattlecryLineEdit.Text = battlecry;
if (!BattlecryLineEdit.HasKeyboardFocus())
BattlecryLineEdit.Text = battlecry;
}
public void SetInitialBattlecry(string? battlecry)
{
if (battlecry != null)
{
BattlecryLineEdit.Text = battlecry;
BattlecryLineEdit.CursorPosition = battlecry.Length;
BattlecryLineEdit.SelectionStart = 0;
}
BattlecryLineEdit.GrabKeyboardFocus();
}
public void SetMaxBattlecryLength(int maxLength)
{
BattlecryLineEdit.IsValid = s => s.Length <= maxLength;
}
}

View file

@ -521,7 +521,7 @@ public sealed partial class GunSystem : SharedGunSystem
private bool CheckFixtures(Entity<FixturesComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
if (!Resolve(entity, ref entity.Comp, false))
return false;
// TODO: Maybe also check that our cursor is intersecting a valid fixture?

View file

@ -0,0 +1,64 @@
using Content.IntegrationTests.Fixtures.Attributes;
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.Stack;
using static Content.IntegrationTests.Tests.Stacks.StackTestPrototypes;
namespace Content.IntegrationTests.Tests.Stacks;
[TestFixture]
[TestOf(typeof(StackSystem))]
public sealed class StackInteractionTest : InteractionTest
{
[SidedDependency(Side.Server)] private readonly StackSystem _sStackSystem = default!;
[Test]
[Description("Test that using a stack on a stack will combine them to the hand.")]
public async Task InteractUsingTest()
{
var held = await Spawn(StackEnt1);
await Pickup(held);
await SpawnTarget(StackEnt1);
await Interact();
using (Assert.EnterMultipleScope())
{
// Assert that the held stack has the full count
// And the ground stack was deleted
Assert.That(_sStackSystem.GetCount(ToServer(held)), Is.EqualTo(2));
Assert.That(SEntMan.EntityExists(ToServer(Target)), Is.False);
}
}
[Test]
[Description("Test alt interact with a stack splits it in half.")]
public async Task SplitTest()
{
await SpawnTarget(StackEnt30);
await Interact(altInteract: true);
var targetCount = 0;
var heldCount = 0;
await Server.WaitPost(() =>
{
targetCount = _sStackSystem.GetCount(STarget.Value);
if (HandSys.GetActiveItem((ToServer(Player), Hands)) is { } held)
heldCount = _sStackSystem.GetCount(held);
});
// Check that the count was evenly split
using (Assert.EnterMultipleScope())
{
Assert.That(targetCount, Is.EqualTo(15));
Assert.That(heldCount, Is.EqualTo(15));
}
}
// TODO test split verb
// I don't know how to navigate the right click menu in integration tests to find verbs
// TODO a test for eating a stack
// Currently the player supplied by InteractionTest doesn't have a body or a stomach to eat with
// And BodySystem has no API for cleanly adding the needed parts
}

View file

@ -0,0 +1,68 @@
namespace Content.IntegrationTests.Tests.Stacks;
public static class StackTestPrototypes
{
public const string StackPrototype = "TestStack";
public const string StackEntEdible = "StackEntEdible";
public const string StackEnt1 = "StackEnt1";
private const string StackCount1 = "1";
public const string StackEnt2 = "StackEnt2";
private const string StackCount2 = "2";
public const string StackEnt30 = "StackEnt30";
private const string StackCount30 = "30"; // Also the maximum size of the test stack
[TestPrototypes]
public const string Prototypes =
@$"
- type: stack
id: {StackPrototype}
name: stack-steel
spawn: {StackEnt1}
maxCount: {StackCount30}
- type: entity
id: {StackEnt1}
components:
- type: Stack
stackType: {StackPrototype}
count: {StackCount1}
- type: Item
- type: Physics
bodyType: Dynamic
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeCircle
radius: 0.35
layer:
- Impassable
- type: entity
parent: {StackEnt1}
id: {StackEnt2}
components:
- type: Stack
count: {StackCount2}
- type: entity
parent: {StackEnt1}
id: {StackEnt30}
components:
- type: Stack
count: {StackCount30}
- type: entity
parent: {StackEnt1}
id: {StackEntEdible}
components:
- type: Edible
- type: Solution
id: food
solution:
reagents:
- ReagentId: Nothing
Quantity: 5
";
}

View file

@ -0,0 +1,153 @@
using System.Collections.Generic;
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Server.Stack;
using Content.Shared.Stacks;
using Robust.Shared.GameObjects;
using static Content.IntegrationTests.Tests.Stacks.StackTestPrototypes;
namespace Content.IntegrationTests.Tests.Stacks;
[TestFixture]
[TestOf(typeof(StackSystem))]
public sealed class StackTest : GameTest
{
[SidedDependency(Side.Server)] private readonly StackSystem _sStackSystem = default!;
[Test]
[Description("Tests for SharedStackSystem.SetCount .")]
public async Task SetTest()
{
var stack = await Spawn(StackEnt1);
// Raising the count
await Server.WaitPost(() => _sStackSystem.SetCount((stack, null), 2));
Assert.That(_sStackSystem.GetCount(stack), Is.EqualTo(2));
// Lowering the count
await Server.WaitPost(() =>_sStackSystem.SetCount((stack, null), 1));
Assert.That(_sStackSystem.GetCount(stack), Is.EqualTo(1));
// Setting above the max count clamps to max
await Server.WaitPost(() =>_sStackSystem.SetCount((stack, null), 31));
Assert.That(_sStackSystem.GetCount(stack), Is.EqualTo(30));
// Setting to 0 deletes the stack
await Server.WaitPost(() =>_sStackSystem.SetCount((stack, null), 0));
await Server.WaitRunTicks(1);
Assert.That(SEntMan.EntityCount, Is.Zero);
}
[Test]
[Description("Tests that SharedStackSystem.MergeStacks functions as expected with small numbers.")]
public async Task MergeTest()
{
var stacks = new HashSet<EntityUid>();
await Server.WaitPost(() =>
{
stacks =
[
SSpawn(StackEnt1),
SSpawn(StackEnt2),
];
_sStackSystem.MergeStacks(ref stacks);
});
// Wait for the queue deletion of the empty stacks
await Server.WaitRunTicks(1);
using (Assert.EnterMultipleScope())
{
// Assert that only one entity was returned
// And that it has the correct count
Assert.That(stacks, Has.Count.EqualTo(1));
Assert.That(_sStackSystem.GetCount(stacks.First()), Is.EqualTo(3));
// Assert that the other stack was set to zero and deleted
Assert.That(SEntMan.EntityCount, Is.EqualTo(1));
}
}
[Test]
[Description("Tests that SharedStackSystem.MergeStacks functions as expected with large numbers.")]
public async Task MergeOverflowTest()
{
var stacks = new HashSet<EntityUid>();
await Server.WaitPost(() =>
{
stacks =
[
SSpawn(StackEnt1),
SSpawn(StackEnt2),
SSpawn(StackEnt30),
];
_sStackSystem.MergeStacks(ref stacks);
});
// Wait for the queue deletion of the empty stacks
await Server.WaitRunTicks(1);
var count = 0;
await Server.WaitPost(() =>
{
foreach (var stack in stacks)
{
count += _sStackSystem.GetCount(stack);
}
});
using (Assert.EnterMultipleScope())
{
// Assert that both stacks were returned
// And that the empty stack was deleted
Assert.That(stacks, Has.Count.EqualTo(2));
Assert.That(SEntMan.EntityCount, Is.EqualTo(2));
// Assert we have the same count as what we spawned
Assert.That(count, Is.EqualTo(33));
}
}
[Test]
[Description("Test for SharedStackSystem.TryMergeToContacts .")]
public async Task MergeContactsTest()
{
var map = await Pair.CreateTestMap();
await Server.WaitIdleAsync();
// Spawn two stacks at the same position so they're contacting
var donor = await SpawnAtPosition(StackEnt1, map.GridCoords);
var receiver = await SpawnAtPosition(StackEnt1, map.GridCoords);
_sStackSystem.TryMergeToContacts(donor);
// Wait for queue deletion
await Server.WaitRunTicks(1);
using (Assert.EnterMultipleScope())
{
// Assert that the receiver has the total count
// And that the donor was deleted
Assert.That(_sStackSystem.GetCount(receiver), Is.EqualTo(2));
Assert.That(SEntMan.EntityExists(donor), Is.False);
}
// Now test for when there's more count than the receiver can hold
donor = await SpawnAtPosition(StackEnt30, map.GridCoords);
await Server.WaitPost(() => _sStackSystem.TryMergeToContacts(donor));
using (Assert.EnterMultipleScope())
{
// Assert that the receiver is at its maximum count
// And that the donor has the remainder of the spawned count
Assert.That(_sStackSystem.GetCount(receiver), Is.EqualTo(30));
Assert.That(_sStackSystem.GetCount(donor), Is.EqualTo(2));
}
}
}

View file

@ -169,17 +169,15 @@ public static partial class GameDataScrounger
// Take a directory off the stack.
var dir = explorationStack.Pop();
if (ignoreList.Contains(dir))
continue; // It's all abstract anyway.
var ignoredDir = ignoreList.Contains(Path.GetFullPath(dir));
explorationStack.AddRange(Directory.EnumerateDirectories(dir));
foreach (var file in Directory.EnumerateFiles(dir, "*.yml"))
{
if (ignoreList.Contains(file))
continue; // It's all abstract anyway.
var ignored = ignoredDir || ignoreList.Contains(Path.GetFullPath(file));
foreach (var (kind, id) in IndexPrototypesIn(file))
foreach (var (kind, id) in IndexPrototypesIn(file, ignored))
{
// alternate universe where .net has rust's Entry api.
if (!_prototypeIndex.TryGetValue(kind, out var list))
@ -204,8 +202,9 @@ public static partial class GameDataScrounger
/// yielding all (type, id) pairs.
/// </summary>
/// <param name="file">The file to index.</param>
/// <param name="ignored">Whether or not the file is ignored. This treats the entire file as abstract.</param>
/// <returns>An enumerator of all prototypes in the file, regardless of kind.</returns>
private static IEnumerable<(string type, string id)> IndexPrototypesIn(string file)
private static IEnumerable<(string type, string id)> IndexPrototypesIn(string file, bool ignored = false)
{
var stream = new YamlStream();
@ -223,7 +222,7 @@ public static partial class GameDataScrounger
var id = entryMapping[IdNode];
var type = entryMapping[TypeNode];
var @abstract = false;
var @abstract = ignored;
if (entryMapping.TryGetNode("abstract", out YamlScalarNode? abstractNode))
{
// TODO: This technically will exclude prototypes that use the abstract field for their own stuff,
@ -359,7 +358,7 @@ public static partial class GameDataScrounger
if (entry is not YamlScalarNode { Value: {} value })
throw new Exception($"An entry in {path} is not a valid YAML scalar/string literal. Entry: {entry}");
ignores.Add(value);
ignores.Add(Path.GetFullPath($"{resDir}{value}"));
}
}
}

View file

@ -1,7 +0,0 @@
namespace Content.Server.Access.Components;
/// <summary>
/// Allows an ID card to copy accesses from other IDs and to change the name, job title and job icon via an interface.
/// </summary>
[RegisterComponent]
public sealed partial class AgentIDCardComponent : Component { }

View file

@ -1,173 +1,59 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Access.Components;
using Content.Server.Clothing.Systems;
using Content.Server.Implants;
using Content.Server.Popups;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Clothing.Components;
using Content.Shared.Implants;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.PDA;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Content.Shared.UserInterface;
using Content.Shared.VoiceMask;
using Robust.Server.GameObjects;
namespace Content.Server.Access.Systems
namespace Content.Server.Access.Systems;
/// <inheritdoc />
public sealed partial class AgentIdCardSystem : SharedAgentIdCardSystem
{
public sealed partial class AgentIDCardSystem : SharedAgentIdCardSystem
[Dependency] private SharedIdCardSystem _card = default!;
[Dependency] private ChameleonClothingSystem _chameleon = default!;
[Dependency] private ChameleonControllerSystem _chamController = default!;
[SubscribeLocalEvent]
private void OnChameleonControllerOutfitChangedItem(Entity<AgentIDCardComponent> ent, ref InventoryRelayedEvent<ChameleonControllerOutfitSelectedEvent> args)
{
[Dependency] private PopupSystem _popupSystem = default!;
[Dependency] private IdCardSystem _cardSystem = default!;
[Dependency] private UserInterfaceSystem _uiSystem = default!;
[Dependency] private ChameleonClothingSystem _chameleon = default!;
[Dependency] private ChameleonControllerSystem _chamController = default!;
[Dependency] private LockSystem _lock = default!;
[Dependency] private SharedJobStatusSystem _jobStatus = default!;
if (!TryComp<IdCardComponent>(ent, out var idCardComp))
return;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AgentIDCardComponent, AfterInteractEvent>(OnAfterInteract);
// BUI
SubscribeLocalEvent<AgentIDCardComponent, AfterActivatableUIOpenEvent>(AfterUIOpen);
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardNameChangedMessage>(OnNameChanged);
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobChangedMessage>(OnJobChanged);
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobIconChangedMessage>(OnJobIconChanged);
SubscribeLocalEvent<AgentIDCardComponent, InventoryRelayedEvent<ChameleonControllerOutfitSelectedEvent>>(OnChameleonControllerOutfitChangedItem);
SubscribeLocalEvent<AgentIDCardComponent, InventoryRelayedEvent<VoiceMaskNameUpdatedEvent>>(OnVoiceMaskNameChanged);
}
ProtoMan.Resolve(args.Args.ChameleonOutfit.Job, out var jobProto);
private void OnChameleonControllerOutfitChangedItem(Entity<AgentIDCardComponent> ent, ref InventoryRelayedEvent<ChameleonControllerOutfitSelectedEvent> args)
{
if (!TryComp<IdCardComponent>(ent, out var idCardComp))
return;
var jobIcon = args.Args.ChameleonOutfit.Icon ?? jobProto?.Icon;
var jobName = args.Args.ChameleonOutfit.Name ?? jobProto?.Name ?? "";
ProtoMan.Resolve(args.Args.ChameleonOutfit.Job, out var jobProto);
if (jobIcon != null)
_card.TryChangeJobIcon(ent, ProtoMan.Index(jobIcon.Value), idCardComp);
var jobIcon = args.Args.ChameleonOutfit.Icon ?? jobProto?.Icon;
var jobName = args.Args.ChameleonOutfit.Name ?? jobProto?.Name ?? "";
if (jobName != "")
_card.TryChangeJobTitle(ent, Loc.GetString(jobName), idCardComp);
if (jobIcon != null)
_cardSystem.TryChangeJobIcon(ent, ProtoMan.Index(jobIcon.Value), idCardComp);
// If you have forced departments use those over the jobs actual departments.
if (args.Args.ChameleonOutfit.Departments?.Count > 0)
_card.TryChangeJobDepartment(ent, args.Args.ChameleonOutfit.Departments, idCardComp);
else if (jobProto != null)
_card.TryChangeJobDepartment(ent, jobProto, idCardComp);
if (jobName != "")
_cardSystem.TryChangeJobTitle(ent, Loc.GetString(jobName), idCardComp);
// Ensure that you chameleon IDs in PDAs correctly. Yes this is sus...
// If you have forced departments use those over the jobs actual departments.
if (args.Args.ChameleonOutfit?.Departments?.Count > 0)
_cardSystem.TryChangeJobDepartment(ent, args.Args.ChameleonOutfit.Departments, idCardComp);
else if (jobProto != null)
_cardSystem.TryChangeJobDepartment(ent, jobProto, idCardComp);
// There is one weird interaction: If the job / icon don't match the PDAs job the chameleon will be updated
// to the PDAs IDs sprite but the icon and job title will not match. There isn't a way to get around this
// really as there is no tie between job -> pda or pda -> job.
// Ensure that you chameleon IDs in PDAs correctly. Yes this is sus...
var idSlotGear = _chamController.GetGearForSlot(args, "id");
if (idSlotGear == null)
return;
// There is one weird interaction: If the job / icon don't match the PDAs job the chameleon will be updated
// to the PDAs IDs sprite but the icon and job title will not match. There isn't a way to get around this
// really as there is no tie between job -> pda or pda -> job.
var proto = ProtoMan.Index(idSlotGear);
if (!proto.TryComp<PdaComponent>(out var comp, EntityManager.ComponentFactory))
return;
var idSlotGear = _chamController.GetGearForSlot(args, "id");
if (idSlotGear == null)
return;
var proto = ProtoMan.Index(idSlotGear);
if (!proto.TryComp<PdaComponent>(out var comp, EntityManager.ComponentFactory))
return;
if (TryComp<ChameleonClothingComponent>(ent, out var chameleonComp) && chameleonComp.CanBeSetByController)
_chameleon.SetSelectedPrototype(ent, comp.IdCard, component: chameleonComp);
}
private void OnVoiceMaskNameChanged(Entity<AgentIDCardComponent> ent, ref InventoryRelayedEvent<VoiceMaskNameUpdatedEvent> args)
{
if (!TryComp<IdCardComponent>(ent, out var idCard))
return;
if (!args.Args.VoiceMask.Comp.ChangeIDName)
return;
_cardSystem.TryChangeFullName(ent, args.Args.NewName, idCard);
}
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || _lock.IsLocked(uid) ||
!TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
return;
if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid))
return;
var beforeLength = access.Tags.Count;
access.Tags.UnionWith(targetAccess.Tags);
var addedLength = access.Tags.Count - beforeLength;
_popupSystem.PopupEntity(Loc.GetString("agent-id-new", ("number", addedLength), ("card", args.Target)), args.Target.Value, args.User);
if (addedLength > 0)
Dirty(uid, access);
}
private void AfterUIOpen(EntityUid uid, AgentIDCardComponent component, AfterActivatableUIOpenEvent args)
{
if (!_uiSystem.HasUi(uid, AgentIDCardUiKey.Key))
return;
if (!TryComp<IdCardComponent>(uid, out var idCard))
return;
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.LocalizedJobTitle ?? "", idCard.JobIcon);
_uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state);
}
private void OnJobChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCardJobChangedMessage args)
{
if (!TryComp<IdCardComponent>(uid, out var idCard))
return;
_cardSystem.TryChangeJobTitle(uid, args.Job, idCard);
}
private void OnNameChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCardNameChangedMessage args)
{
if (!TryComp<IdCardComponent>(uid, out var idCard))
return;
_cardSystem.TryChangeFullName(uid, args.Name, idCard);
}
private void OnJobIconChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCardJobIconChangedMessage args)
{
if (!TryComp<IdCardComponent>(uid, out var idCard))
return;
if (!ProtoMan.Resolve(args.JobIconId, out var jobIcon))
return;
_cardSystem.TryChangeJobIcon(uid, jobIcon, idCard);
if (TryFindJobProtoFromIcon(jobIcon, out var job))
_cardSystem.TryChangeJobDepartment(uid, job, idCard);
_jobStatus.UpdateStatus(Transform(uid).ParentUid);
}
private bool TryFindJobProtoFromIcon(JobIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job)
{
foreach (var jobPrototype in ProtoMan.EnumeratePrototypes<JobPrototype>())
{
if (jobPrototype.Icon == jobIcon.ID)
{
job = jobPrototype;
return true;
}
}
job = null;
return false;
}
if (TryComp<ChameleonClothingComponent>(ent, out var chameleonComp) && chameleonComp.CanBeSetByController)
_chameleon.SetSelectedPrototype(ent, comp.IdCard, component: chameleonComp);
}
}

View file

@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Chat.Systems;
using Content.Server.Containers;
using Content.Server.StationRecords.Systems;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
@ -15,6 +14,8 @@ using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Roles;
using Content.Shared.StationRecords;
using Content.Shared.StationRecords.Components;
using Content.Shared.StationRecords.Systems;
using Content.Shared.Throwing;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
@ -230,7 +231,7 @@ public sealed partial class IdCardConsoleSystem : SharedIdCardConsoleSystem
return _accessReader.IsAllowed(id.Value, uid, reader);
}
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, ProtoId<AccessLevelPrototype> newJobTitle, JobPrototype? newJobProto)
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, string newJobTitle, JobPrototype? newJobProto)
{
if (!TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|| keyStorage.Key is not { } key

View file

@ -0,0 +1,69 @@
using Content.Server.Administration.Managers;
using Content.Server.Discord.WebhookMessages;
using Content.Server.EUI;
using Content.Server.GameTicking;
using Content.Shared.Administration;
using Content.Shared.Administration.AdminEventLog;
using Content.Shared.CCVar;
using Content.Shared.Eui;
using Robust.Shared.Configuration;
namespace Content.Server.Administration.AdminEventLog;
/// <summary>
/// Sends a message to discord after a admin logs a event
/// </summary>
public sealed partial class AdminEventLogEui : BaseEui
{
[Dependency] private IAdminManager _adminManager = default!;
[Dependency] private IConfigurationManager _config = default!;
[Dependency] private EventWebhook _eventWebhook = default!;
[Dependency] private IEntityManager _e = default!;
private int CurrentRoundId => _e.System<GameTicker>().RoundId;
public AdminEventLogEui()
{
IoCManager.InjectDependencies(this);
}
public override async void Opened()
{
base.Opened();
_adminManager.OnPermsChanged += OnPermsChanged;
StateDirty();
}
public override void Closed()
{
base.Closed();
_adminManager.OnPermsChanged -= OnPermsChanged;
}
public override EuiStateBase GetNewState()
{
var state = new AdminEventLogEuiState(CurrentRoundId);
return state;
}
public override async void HandleMessage(EuiMessageBase msg)
{
var message = (AdminEventLogEuiMsg)msg;
_eventWebhook.TrySendMessage(
message.AdminUser,
message.RoundId,
message.EventDescription,
_config.GetCVar(CCVars.DiscordEventWebhook));
}
private void OnPermsChanged(AdminPermsChangedEventArgs args)
{
if (args.Player == Player && !_adminManager.HasAdminFlag(Player, AdminFlags.Admin))
{
Close();
}
}
}

View file

@ -0,0 +1,26 @@
using Content.Server.Administration.AdminEventLog;
using Content.Server.EUI;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed partial class OpenAdminEventLogCommand : LocalizedCommands
{
[Dependency] private EuiManager _euiManager = default!;
public override string Command => "eventlog";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}
var ui = new AdminEventLogEui();
_euiManager.OpenEui(ui, player);
}
}

View file

@ -1,6 +1,7 @@
using Content.Server.Administration.Logs;
using Content.Server.EUI;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
@ -8,20 +9,49 @@ namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Logs)]
public sealed partial class OpenAdminLogsCommand : LocalizedEntityCommands
{
[Dependency] private EuiManager _euiManager = default!;
[Dependency] private IAdminLogManager _adminLogManager = default!;
[Dependency] private IPlayerLocator _locator = default!;
[Dependency] private IPlayerManager _players = default!;
public override string Command => Cmd;
public const string Cmd = "adminlogs";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
if (shell.Player is not { } admin)
{
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}
var ui = new AdminLogsEui();
_euiManager.OpenEui(ui, player);
Guid? player = null;
switch (args.Length)
{
case 1 when Guid.TryParse(args[0], out var playerGuid):
player = playerGuid;
break;
case 1:
var dbGuid = await _locator.LookupIdByNameAsync(args[0]);
if (dbGuid == null)
{
shell.WriteError(Loc.GetString("cmd-admin-logs-wrong-target", ("user", args[0])));
return;
}
player = dbGuid.UserId;
break;
}
_adminLogManager.OpenEui(admin, targetPlayer: player);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length != 1)
return CompletionResult.Empty;
var options = _players.Sessions.Select(c => c.Name).OrderBy(c => c).ToArray();
return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-admin-logs-hint"));
}
}

View file

@ -1,6 +1,7 @@
using Content.Server.Speech.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.Random;
namespace Content.Server.Administration.Commands;
@ -40,6 +41,7 @@ public sealed partial class OwoifyCommand : IConsoleCommand
var owoSys = _entManager.System<OwOAccentSystem>();
var metaDataSys = _entManager.System<MetaDataSystem>();
metaDataSys.SetEntityName(eUid.Value, owoSys.Accentuate(meta.EntityName), meta);
metaDataSys.SetEntityDescription(eUid.Value, owoSys.Accentuate(meta.EntityDescription), meta);
}

View file

@ -28,8 +28,7 @@ namespace Content.Server.Administration.Commands
if (string.IsNullOrEmpty(colorArg))
return;
var color = Color.TryFromHex(colorArg);
if (!color.HasValue)
if (!Color.TryFromHex(colorArg, out var color))
{
shell.WriteError(Loc.GetString("shell-invalid-color-hex"));
return;
@ -37,10 +36,10 @@ namespace Content.Server.Administration.Commands
var userId = shell.Player.UserId;
// Save the DB
_dbManager.SaveAdminOOCColorAsync(userId, color.Value);
_dbManager.SaveAdminOOCColorAsync(userId, color);
// Update the cached preference
var prefs = _preferenceManager.GetPreferences(userId);
prefs.AdminOOCColor = color.Value;
prefs.AdminOOCColor = color;
}
}
}

View file

@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Systems;
using Content.Server.Database;
using Content.Server.EUI;
using Content.Server.GameTicking;
using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
@ -29,6 +30,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
{
[Dependency] private IConfigurationManager _configuration = default!;
[Dependency] private ILogManager _logManager = default!;
[Dependency] private EuiManager _euis = default!;
[Dependency] private IServerDbManager _db = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IDynamicTypeFactory _typeFactory = default!;
@ -633,4 +635,16 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
{
return _db.CountAdminLogs(round);
}
public void OpenEui(ICommonSession admin, string? search = null, Guid? targetPlayer = null)
{
var ui = new AdminLogsEui();
_euis.OpenEui(ui, admin);
List<Guid>? userList = null;
if (targetPlayer is not null)
userList = [targetPlayer.Value];
ui.SetLogFilter(search, userList);
}
}

View file

@ -142,10 +142,11 @@ public sealed partial class AdminLogsEui : BaseEui
}
}
public void SetLogFilter(string? search = null, bool invertTypes = false, HashSet<LogType>? types = null)
public void SetLogFilter(string? search = null, List<Guid>? players = null, bool invertTypes = false, HashSet<LogType>? types = null)
{
var message = new SetLogFilter(
search,
players,
invertTypes,
types);

View file

@ -3,6 +3,8 @@ using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.GameTicking;
using Content.Shared.Administration.Logs;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration.Logs;
@ -24,4 +26,5 @@ public interface IAdminLogManager : ISharedAdminLogManager
IAsyncEnumerable<JsonDocument> CurrentRoundJson(LogFilter? filter = null);
Task<Round> CurrentRound();
Task<int> CountLogs(int round);
void OpenEui(ICommonSession admin, string? search = null, Guid? targetPlayer = null);
}

View file

@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.Mind;
using Content.Shared.Administration;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
@ -17,6 +18,7 @@ namespace Content.Server.Administration.Systems;
public sealed partial class AdminQuickInfoSystem : EntitySystem
{
[Dependency] private IAdminManager _adminManager = null!;
[Dependency] private MindSystem _mind = null!;
public override void Initialize()
{
@ -41,9 +43,9 @@ public sealed partial class AdminQuickInfoSystem : EntitySystem
NetUserId? lastPlayer = null;
// Check the last mind that was attached to the entity and get its userid.
if (TryComp<MindContainerComponent>(ent, out var comp) && TryComp<MindComponent>(comp.LastMind, out var mindComp))
if (_mind.TryGetLastMind(ent.Value, out var lastMind))
{
lastPlayer = mindComp.UserId;
lastPlayer = lastMind.Value.Comp.UserId;
}
var metadata = MetaData(ent.Value);
return new QuickInfoShared.SingleEntityInfo(e, true, metadata.EntityName, metadata.EntityPrototype?.ID, lastPlayer);

View file

@ -6,7 +6,6 @@ using Content.Server.Hands.Systems;
using Content.Server.Mind;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Popups;
using Content.Server.StationRecords.Systems;
using Content.Shared.Administration;
using Content.Shared.Administration.Events;
using Content.Shared.CCVar;
@ -23,6 +22,8 @@ using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.StationRecords;
using Content.Shared.StationRecords.Components;
using Content.Shared.StationRecords.Systems;
using Content.Shared.Throwing;
using Robust.Server.GameObjects;
using Robust.Server.Player;

View file

@ -36,6 +36,7 @@ using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
using Content.Shared.Chemistry.Components;
using Content.Shared.Mind;
using static Content.Shared.Configurable.ConfigurationComponent;
namespace Content.Server.Administration.Systems
@ -47,6 +48,7 @@ namespace Content.Server.Administration.Systems
{
[Dependency] private IConGroupController _groupController = default!;
[Dependency] private IConsoleHost _console = default!;
[Dependency] private IAdminLogManager _adminLogs = default!;
[Dependency] private IAdminManager _adminManager = default!;
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private SharedMapSystem _map = default!;
@ -243,6 +245,22 @@ namespace Content.Server.Administration.Systems
});
}
// Player Admin Logs. this is distinct from entity logs above as it will get the logs of the _player_
// that last owned the mind of the entity you are right-clicking on. words.
if (_mindSystem.TryGetLastMindOwner(args.Target, out var lastOwner))
{
args.Verbs.Add(new Verb()
{
Priority = -3,
Text = Loc.GetString("admin-verbs-admin-logs-player"),
Category = VerbCategory.Admin,
Act = () =>
{
_adminLogs.OpenEui(player, targetPlayer: lastOwner);
},
});
}
// Freeze
var frozen = TryComp<AdminFrozenComponent>(args.Target, out var frozenComp);
var frozenAndMuted = frozenComp?.Muted ?? false;

View file

@ -9,11 +9,11 @@ namespace Content.Server.Arcade.SpaceVillain;
public sealed partial class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent
{
/// <summary>
/// Unused flag that can be hacked via wires.
/// Name suggests that it was intended to either make the health/mana values underflow while playing the game or turn the arcade machine into an infinite prize fountain.
/// Flag that can be hacked via wires.
/// If true, makes the game skip checking game end conditions, and sets Uncapped to both fighters when a new game is started.
/// </summary>
[ViewVariables]
public bool OverflowFlag;
public bool UncappedFlag;
/// <summary>
/// The current session of the SpaceVillain game for this arcade machine.

View file

@ -14,7 +14,12 @@ public sealed partial class SpaceVillainGame
public int Hp
{
get => _hp;
set => _hp = MathHelper.Clamp(value, 0, HpMax);
set
{
_hp = value;
if (!Uncapped)
_hp = MathHelper.Clamp(_hp, 0, HpMax);
}
}
private int _hp;
@ -28,7 +33,7 @@ public sealed partial class SpaceVillainGame
set
{
_hpMax = Math.Max(value, 0);
Hp = MathHelper.Clamp(Hp, 0, HpMax);
Hp = _hp; // Re-clamp the HP value
}
}
private int _hpMax;
@ -40,7 +45,12 @@ public sealed partial class SpaceVillainGame
public int Mp
{
get => _mp;
set => _mp = MathHelper.Clamp(value, 0, MpMax);
set
{
_mp = value;
if (!Uncapped)
_mp = MathHelper.Clamp(_mp, 0, MpMax);
}
}
private int _mp;
@ -54,7 +64,7 @@ public sealed partial class SpaceVillainGame
set
{
_mpMax = Math.Max(value, 0);
Mp = MathHelper.Clamp(Mp, 0, MpMax);
Mp = _mp; // Re-clamp the MP value
}
}
private int _mpMax;
@ -64,5 +74,11 @@ public sealed partial class SpaceVillainGame
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Invincible = false;
/// <summary>
/// Whether the given fighter's HP and MP values are capped between 0 and their respective Max values.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Uncapped = false;
}
}

View file

@ -71,7 +71,8 @@ public sealed partial class SpaceVillainGame
HpMax = 30,
Hp = 30,
MpMax = 10,
Mp = 10
Mp = 10,
Uncapped = arcade.UncappedFlag,
};
VillainChar = new()
@ -79,7 +80,8 @@ public sealed partial class SpaceVillainGame
HpMax = 45,
Hp = 45,
MpMax = 20,
Mp = 20
Mp = 20,
Uncapped = arcade.UncappedFlag,
};
}
@ -210,6 +212,9 @@ public sealed partial class SpaceVillainGame
/// <returns>A bool indicating if the game should continue.</returns>
private bool CheckGameConditions(EntityUid uid, SpaceVillainArcadeComponent arcade)
{
if (arcade.UncappedFlag)
return true;
switch (
PlayerChar.Hp > 0 && PlayerChar.Mp > 0,
VillainChar.Hp > 0 && VillainChar.Mp > 0

View file

@ -7,7 +7,7 @@ namespace Content.Server.Arcade;
public sealed partial class ArcadeOverflowWireAction : BaseToggleWireAction
{
public override Color Color { get; set; } = Color.Red;
public override Color Color { get; set; } = Color.OrangeRed;
public override string Name { get; set; } = "wire-name-arcade-overflow";
public override object? StatusKey { get; } = SharedSpaceVillainArcadeComponent.Indicators.HealthLimiter;
@ -16,14 +16,19 @@ public sealed partial class ArcadeOverflowWireAction : BaseToggleWireAction
{
if (EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(owner, out var arcade))
{
arcade.OverflowFlag = !setting;
arcade.UncappedFlag = !setting;
if (arcade.Game != null)
{
arcade.Game.PlayerChar.Uncapped = !setting;
arcade.Game.VillainChar.Uncapped = !setting;
}
}
}
public override bool GetValue(EntityUid owner)
{
return EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(owner, out var arcade)
&& !arcade.OverflowFlag;
&& !arcade.UncappedFlag;
}
public override StatusLightState? GetLightState(Wire wire)
@ -31,7 +36,7 @@ public sealed partial class ArcadeOverflowWireAction : BaseToggleWireAction
if (EntityManager.HasComponent<SpaceVillainArcadeComponent>(wire.Owner))
{
return !GetValue(wire.Owner)
? StatusLightState.BlinkingSlow
? StatusLightState.Off
: StatusLightState.On;
}

View file

@ -91,8 +91,7 @@ public sealed partial class GasCanisterSystem : SharedGasCanisterSystem
{
_atmos.React(entity.Comp.Air, entity.Comp);
if (!TryComp<NodeContainerComponent>(entity, out var nodeContainer)
|| !TryComp<AppearanceComponent>(entity, out var appearance))
if (!TryComp<NodeContainerComponent>(entity, out var nodeContainer))
return;
if (!_nodeContainer.TryGetNode(nodeContainer, entity.Comp.PortName, out PortablePipeNode? portNode))
@ -125,26 +124,10 @@ public sealed partial class GasCanisterSystem : SharedGasCanisterSystem
if (MathHelper.CloseToPercent(entity.Comp.Air.Pressure, entity.Comp.LastPressure))
return;
DirtyUI(entity, entity.Comp, nodeContainer);
entity.Comp.LastPressure = entity.Comp.Air.Pressure;
if (entity.Comp.Air.Pressure < 10)
{
_appearance.SetData(entity, GasCanisterVisuals.PressureState, 0, appearance);
}
else if (entity.Comp.Air.Pressure < Atmospherics.OneAtmosphere)
{
_appearance.SetData(entity, GasCanisterVisuals.PressureState, 1, appearance);
}
else if (entity.Comp.Air.Pressure < (15 * Atmospherics.OneAtmosphere))
{
_appearance.SetData(entity, GasCanisterVisuals.PressureState, 2, appearance);
}
else
{
_appearance.SetData(entity, GasCanisterVisuals.PressureState, 3, appearance);
}
DirtyUI(entity, entity.Comp, nodeContainer);
UpdateAppearance(entity);
}
/// <summary>

View file

@ -5,7 +5,6 @@ using Content.Shared.Physics;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
@ -19,7 +18,6 @@ public sealed partial class BeamSystem : SharedBeamSystem
[Dependency] private TransformSystem _transform = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private SharedBroadphaseSystem _broadphase = default!;
[Dependency] private SharedPhysicsSystem _physics = default!;
private static readonly EntProtoId VirtualBeamEntityControllerId = "VirtualBeamEntityController";
@ -81,23 +79,22 @@ public sealed partial class BeamSystem : SharedBeamSystem
var ent = Spawn(prototype, beamSpawnPos, rotation: userAngle);
var shape = new EdgeShape(distanceCorrection, new Vector2(0, 0));
if (!TryComp<PhysicsComponent>(ent, out var physics) || !TryComp<BeamComponent>(ent, out var beam))
if (!TryComp<BeamComponent>(ent, out var beam))
return;
FixturesComponent? manager = null;
_fixture.TryCreateFixture(
ent,
shape,
"BeamBody",
hard: false,
collisionMask: (int)CollisionGroup.ItemMask,
collisionLayer: (int)CollisionGroup.MobLayer,
manager: manager,
body: physics);
if (TryComp<PhysicsComponent>(ent, out var physics) && physics.CanCollide)
{
_fixture.TryCreateFixture(
ent,
shape,
BeamComponent.FixtureID,
hard: false,
collisionMask: (int)CollisionGroup.ItemMask,
collisionLayer: (int)CollisionGroup.MobLayer,
body: physics);
_physics.SetBodyType(ent, BodyType.Dynamic, manager: manager, body: physics);
_physics.SetCanCollide(ent, true, manager: manager, body: physics);
_broadphase.RegenerateContacts((ent, physics, manager));
_broadphase.RegenerateContacts((ent, physics));
}
var distanceLength = distanceCorrection.Length();

View file

@ -1,12 +1,7 @@
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Ghost;
using Content.Server.Hands.Systems;
using Content.Server.Inventory;
using Content.Server.Popups;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.StationRecords.Systems;
using Content.Shared.Access.Systems;
using Content.Shared.Bed.Cryostorage;
using Content.Shared.Chat;
@ -18,15 +13,19 @@ using Content.Shared.Hands.Components;
using Content.Shared.Mind.Components;
using Content.Shared.StationRecords;
using Content.Shared.UserInterface;
using Robust.Server.Audio;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using System.Globalization;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Station;
using Content.Shared.StationRecords.Components;
using Content.Shared.StationRecords.Systems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Bed.Cryostorage;
@ -35,20 +34,20 @@ public sealed partial class CryostorageSystem : SharedCryostorageSystem
{
[Dependency] private IChatManager _chatManager = default!;
[Dependency] private IPlayerManager _playerManager = default!;
[Dependency] private AudioSystem _audio = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private AccessReaderSystem _accessReader = default!;
[Dependency] private ChatSystem _chatSystem = default!;
[Dependency] private SharedChatSystem _chatSystem = default!;
[Dependency] private ClimbSystem _climb = default!;
[Dependency] private ContainerSystem _container = default!;
[Dependency] private SharedContainerSystem _container = default!;
[Dependency] private GhostSystem _ghostSystem = default!;
[Dependency] private HandsSystem _hands = default!;
[Dependency] private ServerInventorySystem _inventory = default!;
[Dependency] private PopupSystem _popup = default!;
[Dependency] private StationSystem _station = default!;
[Dependency] private SharedHandsSystem _hands = default!;
[Dependency] private InventorySystem _inventory = default!;
[Dependency] private SharedPopupSystem _popup = default!;
[Dependency] private SharedStationSystem _station = default!;
[Dependency] private StationJobsSystem _stationJobs = default!;
[Dependency] private StationRecordsSystem _stationRecords = default!;
[Dependency] private TransformSystem _transform = default!;
[Dependency] private UserInterfaceSystem _ui = default!;
[Dependency] private SharedTransformSystem _transform = default!;
[Dependency] private SharedUserInterfaceSystem _ui = default!;
/// <inheritdoc/>
public override void Initialize()
@ -317,7 +316,7 @@ public sealed partial class CryostorageSystem : SharedCryostorageSystem
if (HasComp<AttachedClothingComponent>(item))
continue;
data.ItemSlots.Add(slotDef.Name, Name(item));
data.ItemSlots.Add((slotDef.Name, slotDef.DisplayName, Name(item)));
}
foreach (var hand in _hands.EnumerateHands(uid))

View file

@ -132,7 +132,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
// If the original does not have the component, then the clone shouldn't have it either.
RemComp(clone, componentRegistration.Type);
if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
if (TryComp(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
{
CopyComp(original, clone, sourceComp);
}

View file

@ -27,7 +27,7 @@ namespace Content.Server.Construction
if (!ProtoMan.TryIndex(msg.ConstructionId, out ConstructionPrototype? prototype))
return;
if(GetGuide(prototype) is {} guide)
if (GetGuide(prototype) is { } guide)
RaiseNetworkEvent(new ResponseConstructionGuide(msg.ConstructionId, guide), args.SenderSession.Channel);
}
@ -46,10 +46,10 @@ namespace Content.Server.Construction
if (component.DeconstructionNode == null)
return;
if (GetCurrentNode(uid, component) is not {} currentNode)
if (GetCurrentNode(uid, component) is not { } currentNode)
return;
if (graph.Path(currentNode.Name, component.DeconstructionNode) is not {} path || path.Length == 0)
if (graph.Path(currentNode.Name, component.DeconstructionNode) is not { } path || path.Length == 0)
return;
Verb verb = new();
@ -57,7 +57,7 @@ namespace Content.Server.Construction
//TODO VERBS add more construction verbs? Until then, removing construction category
verb.Text = Loc.GetString("deconstructible-verb-begin-deconstruct");
verb.Icon = new SpriteSpecifier.Texture(
new ("/Textures/Interface/hammer_scaled.svg.192dpi.png"));
new("/Textures/Interface/hammer_scaled.svg.192dpi.png"));
verb.Act = () =>
{
@ -80,7 +80,7 @@ namespace Content.Server.Construction
{
using (args.PushGroup(nameof(ConstructionComponent)))
{
if (GetTargetNode(uid, component) is {} target)
if (GetTargetNode(uid, component) is { } target)
{
if (target.Name == component.DeconstructionNode)
{
@ -88,13 +88,22 @@ namespace Content.Server.Construction
}
else
{
args.PushMarkup(Loc.GetString(
"construction-component-to-create-header",
("targetName", target.Name)) + "\n");
// Try to get the name of the prototype on the node, if one exists.
var targetProtoId = target.Entity.GetId(uid, args.Examiner, new(EntityManager));
if (targetProtoId != null
&& ProtoMan.TryIndex(targetProtoId, out var targetPrototype))
{
args.PushMarkup(Loc.GetString("construction-component-to-create-prototype-header",
("targetName", targetPrototype.Name)) + "\n");
}
else
{
args.PushMarkup(Loc.GetString("construction-component-to-create-header") + "\n");
}
}
}
if (component.EdgeIndex == null && GetTargetEdge(uid, component) is {} targetEdge)
if (component.EdgeIndex == null && GetTargetEdge(uid, component) is { } targetEdge)
{
var preventStepExamine = false;
@ -108,7 +117,7 @@ namespace Content.Server.Construction
return;
}
if (GetCurrentEdge(uid, component) is {} edge)
if (GetCurrentEdge(uid, component) is { } edge)
{
var preventStepExamine = false;
@ -148,12 +157,12 @@ namespace Content.Server.Construction
return null;
// If either the start node or the target node are missing, do nothing.
if (GetNodeFromGraph(graph, construction.StartNode) is not {} startNode
|| GetNodeFromGraph(graph, construction.TargetNode) is not {} targetNode)
if (GetNodeFromGraph(graph, construction.StartNode) is not { } startNode
|| GetNodeFromGraph(graph, construction.TargetNode) is not { } targetNode)
return null;
// If there's no path from start to target, do nothing.
if (graph.Path(construction.StartNode, construction.TargetNode) is not {} path
if (graph.Path(construction.StartNode, construction.TargetNode) is not { } path
|| path.Length == 0)
return null;
@ -196,7 +205,7 @@ namespace Content.Server.Construction
// Now actually list the construction conditions.
foreach (var condition in construction.Conditions)
{
if (condition.GenerateGuideEntry() is not {} conditionEntry)
if (condition.GenerateGuideEntry() is not { } conditionEntry)
continue;
conditionEntry.Padding += 4;
@ -207,7 +216,7 @@ namespace Content.Server.Construction
node = path[index++];
// Add a bit of padding if there will be more steps after this.
if(node != targetNode)
if (node != targetNode)
entries.Add(new ConstructionGuideEntry());
continue;

View file

@ -2,7 +2,6 @@ using System.Linq;
using Content.Server.Administration;
using Content.Server.EUI;
using Content.Server.Station.Systems;
using Content.Server.StationRecords.Systems;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
@ -10,6 +9,9 @@ using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Content.Shared.Station.Components;
using Content.Shared.StationRecords;
using Content.Shared.StationRecords.Components;
using Content.Shared.StationRecords.Events;
using Content.Shared.StationRecords.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player;
@ -34,7 +36,7 @@ public sealed partial class CrewManifestSystem : EntitySystem
public override void Initialize()
{
SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(AfterGeneralRecordCreated);
SubscribeLocalEvent<GeneralRecordCreatedEvent>(GeneralRecordCreated);
SubscribeLocalEvent<RecordModifiedEvent>(OnRecordModified);
SubscribeLocalEvent<RecordRemovedEvent>(OnRecordRemoved);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
@ -72,19 +74,19 @@ public sealed partial class CrewManifestSystem : EntitySystem
// Not a big fan of this one. Rebuilds the crew manifest every time
// somebody spawns in, meaning that at round start, it rebuilds the crew manifest
// wrt the amount of players readied up.
private void AfterGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
private void GeneralRecordCreated(ref GeneralRecordCreatedEvent ev)
{
BuildCrewManifest(ev.Key.OriginStation);
UpdateEuis(ev.Key.OriginStation);
}
private void OnRecordModified(RecordModifiedEvent ev)
private void OnRecordModified(ref RecordModifiedEvent ev)
{
BuildCrewManifest(ev.Key.OriginStation);
UpdateEuis(ev.Key.OriginStation);
}
private void OnRecordRemoved(RecordRemovedEvent ev)
private void OnRecordRemoved(ref RecordRemovedEvent ev)
{
BuildCrewManifest(ev.Key.OriginStation);
UpdateEuis(ev.Key.OriginStation);

View file

@ -1,8 +1,6 @@
using Content.Server.Popups;
using Content.Server.Radio.EntitySystems;
using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Server.StationRecords.Systems;
using Content.Shared.Access.Systems;
using Content.Shared.CriminalRecords;
using Content.Shared.CriminalRecords.Components;
@ -12,11 +10,12 @@ using Content.Shared.StationRecords;
using Robust.Server.GameObjects;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.IdentityManagement;
using Content.Shared.Security.Components;
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Roles.Jobs;
using Content.Shared.StationRecords.Components;
using Content.Shared.StationRecords.Events;
using Content.Shared.StationRecords.Systems;
namespace Content.Server.CriminalRecords.Systems;
@ -38,7 +37,7 @@ public sealed partial class CriminalRecordsConsoleSystem : SharedCriminalRecords
public override void Initialize()
{
SubscribeLocalEvent<CriminalRecordsConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
SubscribeLocalEvent<CriminalRecordsConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
SubscribeLocalEvent<CriminalRecordsConsoleComponent, GeneralRecordCreatedEvent>(UpdateUserInterface);
Subs.BuiEvents<CriminalRecordsConsoleComponent>(CriminalRecordsConsoleKey.Key, subs =>
{
@ -64,6 +63,7 @@ public sealed partial class CriminalRecordsConsoleSystem : SharedCriminalRecords
ent.Comp.ActiveKey = msg.SelectedKey;
UpdateUserInterface(ent);
}
private void OnStatusFilterPressed(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordSetStatusFilter msg)
{
ent.Comp.FilterStatus = msg.FilterStatus;

View file

@ -1,12 +1,12 @@
using Content.Server.Chat.Systems;
using Content.Server.Station.Systems;
using Content.Server.StationRecords.Systems;
using Content.Shared.CriminalRecords;
using Content.Shared.CriminalRecords.Components;
using Content.Shared.CriminalRecords.Systems;
using Content.Shared.Random.Helpers;
using Content.Shared.Security;
using Content.Shared.StationRecords;
using Content.Shared.StationRecords.Systems;
using Robust.Shared.Random;
namespace Content.Server.CriminalRecords.Systems;

View file

@ -1,7 +1,4 @@
using System.Linq;
using Content.Server.CartridgeLoader;
using Content.Server.CartridgeLoader.Cartridges;
using Content.Server.StationRecords.Systems;
using Content.Shared.CriminalRecords;
using Content.Shared.CriminalRecords.Systems;
using Content.Shared.Security;
@ -10,6 +7,8 @@ using Content.Server.GameTicking;
using Content.Server.Station.Systems;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.StationRecords.Events;
using Content.Shared.StationRecords.Systems;
namespace Content.Server.CriminalRecords.Systems;
@ -32,14 +31,14 @@ public sealed partial class CriminalRecordsSystem : SharedCriminalRecordsSystem
{
base.Initialize();
SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(OnGeneralRecordCreated);
SubscribeLocalEvent<GeneralRecordCreatedEvent>(OnGeneralRecordCreated);
SubscribeLocalEvent<WantedListCartridgeComponent, CriminalRecordChangedEvent>(OnRecordChanged);
SubscribeLocalEvent<WantedListCartridgeComponent, CartridgeUiReadyEvent>(OnCartridgeUiReady);
SubscribeLocalEvent<WantedListCartridgeComponent, CriminalHistoryAddedEvent>(OnHistoryAdded);
SubscribeLocalEvent<WantedListCartridgeComponent, CriminalHistoryRemovedEvent>(OnHistoryRemoved);
}
private void OnGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
private void OnGeneralRecordCreated(ref GeneralRecordCreatedEvent ev)
{
_records.AddRecordEntry(ev.Key, new CriminalRecord());
_records.Synchronize(ev.Key);

View file

@ -1,7 +1,7 @@
using Content.Shared.Delivery;
using Content.Shared.Power.EntitySystems;
using Content.Shared.EntityTable;
using Content.Shared.StationRecords;
using Content.Shared.StationRecords.Components;
using Robust.Shared.Random;
using Robust.Shared.Timing;

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