diff --git a/.editorconfig b/.editorconfig
index 58d0d332bb..370c3f9dee 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,9 +9,10 @@ indent_style = space
tab_width = 4
# New line preferences
-end_of_line = crlf:suggestion
+#end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
+max_line_length = 120
#### .NET Coding Conventions ####
@@ -71,7 +72,7 @@ csharp_style_expression_bodied_constructors = false:suggestion
#csharp_style_expression_bodied_indexers = true:silent
#csharp_style_expression_bodied_lambdas = true:silent
#csharp_style_expression_bodied_local_functions = false:silent
-csharp_style_expression_bodied_methods = false:suggestion
+csharp_style_expression_bodied_methods = true:suggestion
#csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:suggestion
@@ -104,7 +105,6 @@ csharp_preferred_modifier_order = public, private, protected, internal, new, abs
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
-csharp_style_namespace_declarations = file_scoped:suggestion
#### C# Formatting Rules ####
@@ -337,7 +337,11 @@ dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
# ReSharper properties
resharper_braces_for_ifelse = required_for_multiline
+resharper_csharp_wrap_arguments_style = chop_if_long
+resharper_csharp_wrap_parameters_style = chop_if_long
resharper_keep_existing_attribute_arrangement = true
+resharper_wrap_chained_binary_patterns = chop_if_long
+resharper_wrap_chained_method_calls = chop_if_long
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
indent_size = 2
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
deleted file mode 100644
index 2dc3757588..0000000000
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-contact_links:
- - name: Report a Security Vulnerability
- url: https://github.com/space-wizards/space-station-14/blob/master/SECURITY.md
- about: Please report security vulnerabilities to the Space Wizards privately so they can fix them before they are publicly disclosed.
- - name: Toolshed Feature Request
- url: https://github.com/space-wizards/space-station-14/issues/new?assignees=moonheart08&labels=Toolshed&projects=&template=toolshed-feature-request.md&title=%5BTOOLSHED+REQUEST%5D
- about: Suggest a feature for Toolshed (for game admins/developers)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index aba549332f..51f9de8baf 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,8 +1,8 @@
---
-name: Request a Feature
-about: "Template for noting future planned features. Please ask for approval in the Discord if you aren't an organization Member before posting a feature request"
+name: Request a feature
+about: "Please outline your request in Discord first if you aren't a maintainer."
title: ''
-labels: ''
+labels: ["Type: Feature"]
assignees: ''
---
diff --git a/.github/ISSUE_TEMPLATE/issue_report.md b/.github/ISSUE_TEMPLATE/issue_report.md
index ab82181197..c74f24554a 100644
--- a/.github/ISSUE_TEMPLATE/issue_report.md
+++ b/.github/ISSUE_TEMPLATE/issue_report.md
@@ -1,8 +1,8 @@
---
-name: Report an Issue
-about: "Any general issues you have during play or with the codebase"
+name: Report an issue
+about: "Any issues found in gameplay or the codebase"
title: ''
-labels: ''
+labels: 'Type: Bug'
assignees: ''
---
diff --git a/.github/labeler.yml b/.github/labeler.yml
index eb01eeecc4..4cfe775ed4 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,39 +1,56 @@
"Changes: Audio":
- - "**/*.ogg"
-
+ - changed-files:
+ - any-glob-to-any-file: "**/*.ogg"
+
"Changes: C#":
- - "**/*.cs"
+ - changed-files:
+ - any-glob-to-any-file: "**/*.cs"
"Changes: Config":
- - "**/*.toml"
- - "**/*.config"
- - "*.json"
- - ".github/*.yml"
- - ".github/*.json"
- - ".vscode/*.json"
- - ".editorconfig"
+- changed-files:
+ - any-glob-to-any-file:
+ - "**/*.toml"
+ - "**/*.config"
+ - "*.json"
+ - ".github/*.yml"
+ - ".github/*.json"
+ - ".vscode/*.json"
+ - ".editorconfig"
"Changes: Documentation":
- - "**/*.xml"
- - "**/*.md"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.xml"
+ - "**/*.md"
"Changes: Localization":
-- 'Resources/Locale/**/*.ftl'
+ - changed-files:
+ - any-glob-to-any-file: 'Resources/Locale/**/*.ftl'
"Changes: Map":
- - "Resources/Maps/**/*.yml"
- - "Resources/Prototypes/Maps/**/*.yml"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "Resources/Maps/**/*.yml"
+ - "Resources/Prototypes/Maps/**/*.yml"
"Changes: Sprite":
- - "**/*.rsi/*.png"
- - "**/*.rsi/*.json"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.rsi/*.png"
+ - "**/*.rsi/*.json"
"Changes: UI":
- - "**/*.xaml*"
+ - changed-files:
+ - any-glob-to-any-file: "**/*.xaml*"
"Changes: YML":
- - any: ["**/*.yml"]
- all: ["!Resources/Maps/**/*.yml", "!Resources/Prototypes/Maps/**/*.yml"]
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.yml"
+ - all-globs-to-all-files:
+ - "!Resources/Maps/**/*.yml"
+ - "!Resources/Prototypes/Maps/**/*.yml"
"Changes: Workflow":
- - ".github/workflows/*.yml"
+ - changed-files:
+ - any-glob-to-any-file: ".github/workflows/*.yml"
diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
index 877273d764..79cfdd83cd 100644
--- a/.github/workflows/changelog.yml
+++ b/.github/workflows/changelog.yml
@@ -16,41 +16,40 @@ jobs:
permissions:
contents: write
steps:
- - name: Checkout Master
- uses: actions/checkout@v3
- with:
- token: ${{ secrets.BOT_TOKEN }}
- ref: "${{ vars.CHANGELOG_BRANCH }}"
+ - name: Checkout Master
+ uses: actions/checkout@v3
+ with:
+ token: ${{ secrets.BOT_TOKEN }}
+ ref: ${{ vars.CHANGELOG_BRANCH }}
- - name: Setup Git
- run: |
- git config --global user.name "${{ vars.CHANGELOG_USER }}"
- git config --global user.email "${{ vars.CHANGELOG_EMAIL }}"
- shell: bash
+ - name: Setup Git
+ run: |
+ git config --global user.name "${{ vars.CHANGELOG_USER }}"
+ git config --global user.email "${{ vars.CHANGELOG_EMAIL }}"
+ shell: bash
- - name: Setup Node
- uses: actions/setup-node@v3
- with:
- node-version: 18.x
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
- - name: Install Dependencies
- run: |
- cd "Tools/changelogs"
- npm install
- shell: bash
- continue-on-error: true
+ - name: Install Dependencies
+ run: |
+ cd "Tools/changelogs"
+ npm install
+ shell: bash
- - name: Generate Changelog
- run: |
- cd "Tools/changelogs"
- node changelog.js
- shell: bash
- continue-on-error: true
+ - name: Generate Changelog
+ run: |
+ cd "Tools/changelogs"
+ node changelog.js
+ shell: bash
- - name: Commit Changelog
- run: |
- git add *.yml
- git commit -m "${{ vars.CHANGELOG_MESSAGE }} (#${{ env.PR_NUMBER }})"
- git push
- shell: bash
- continue-on-error: true
+ - name: Commit Changelog
+ run: |
+ git pull origin master
+ git add *.yml
+ git commit -m "${{ vars.CHANGELOG_MESSAGE }} (#${{ env.PR_NUMBER }})"
+ git push
+ shell: bash
+ continue-on-error: true
diff --git a/.github/workflows/conflict-labeler.yml b/.github/workflows/conflict-labeler.yml
index 152d3a9f3c..1bba677022 100644
--- a/.github/workflows/conflict-labeler.yml
+++ b/.github/workflows/conflict-labeler.yml
@@ -1,18 +1,20 @@
name: Check Merge Conflicts
on:
- push:
- branches:
- - master
pull_request_target:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - ready_for_review
jobs:
Label:
- if: github.actor != 'PJBot' && github.actor != 'DeltaV-Bot' && github.actor != 'SimpleStation14'
+ if: ( github.event.pull_request.draft == false ) && ( github.actor != 'PJBot' && github.actor != 'DeltaV-Bot' && github.actor != 'SimpleStation14' )
runs-on: ubuntu-latest
steps:
- name: Check for Merge Conflicts
- uses: ike709/actions-label-merge-conflict@9eefdd17e10566023c46d2dc6dc04fcb8ec76142
+ uses: eps1lon/actions-label-merge-conflict@v3.0.0
with:
dirtyLabel: "Status: Merge Conflict"
repoToken: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/discord-changelog.yml b/.github/workflows/discord-changelog.yml
new file mode 100644
index 0000000000..74be415432
--- /dev/null
+++ b/.github/workflows/discord-changelog.yml
@@ -0,0 +1,24 @@
+name: Discord Changelog
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: '0 6 * * *'
+
+jobs:
+ publish_changelog:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: checkout
+ uses: actions/checkout@v3
+ with:
+ ref: master
+
+ - name: Publish changelog
+ run: Tools/actions_changelogs_since_last_run.py
+ env:
+ CHANGELOG_DIR: ${{ vars.CHANGELOG_DIR }}
+ CHANGELOG_WEBHOOK: ${{ secrets.CHANGELOG_WEBHOOK }}
+ GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }}
+ continue-on-error: true
diff --git a/.github/workflows/labeler-pr.yml b/.github/workflows/labeler-pr.yml
index efb051f4cc..2fd754b15e 100644
--- a/.github/workflows/labeler-pr.yml
+++ b/.github/workflows/labeler-pr.yml
@@ -6,8 +6,9 @@ on:
jobs:
labeler:
if: github.actor != 'PJBot' && github.actor != 'DeltaV-Bot' && github.actor != 'SimpleStation14'
+ permissions:
+ contents: read
+ pull-requests: write
runs-on: ubuntu-latest
steps:
- - uses: actions/labeler@v3
- with:
- repo-token: "${{ secrets.GITHUB_TOKEN }}"
+ - uses: actions/labeler@v5
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 177e6a0fe6..d9cfd3b25b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -41,39 +41,17 @@ jobs:
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
- - name: Update Build Info
- run: Tools/gen_build_info.py
-
- - name: Shuffle files around
- run: |
- mkdir "release/${{ github.sha }}"
- mv release/*.zip "release/${{ github.sha }}"
-
- - name: Upload files to centcomm
- uses: appleboy/scp-action@master
- with:
- host: ${{ secrets.PUBLISH_HOST }}
- username: ${{ secrets.PUBLISH_USER }}
- key: ${{ secrets.PUBLISH_KEY }}
- port: ${{ secrets.PUBLISH_PORT }}
- source: "release/${{ github.sha }}"
- target: "/var/www/builds.delta-v.org/delta-v/builds/"
- strip_components: 1
-
- - name: Update manifest JSON
- uses: appleboy/ssh-action@master
- with:
- host: ${{ secrets.PUBLISH_HOST }}
- username: ${{ secrets.PUBLISH_USER }}
- key: ${{ secrets.PUBLISH_KEY }}
- port: ${{ secrets.PUBLISH_PORT }}
- script: /home/deltav/publish/push.ps1 ${{ github.sha }}
-
- - name: Publish changelog (Discord)
- run: Tools/actions_changelogs_since_last_run.py
+ - name: Publish version
+ run: Tools/publish_multi_request.py
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
+ PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+ GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
+
+ # - 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)
run: Tools/actions_changelog_rss.py
diff --git a/.github/workflows/update-credits.yml b/.github/workflows/update-credits.yml
index 69a8bc1988..5dc6299c6c 100644
--- a/.github/workflows/update-credits.yml
+++ b/.github/workflows/update-credits.yml
@@ -19,6 +19,8 @@ jobs:
- name: Get this week's Contributors
shell: pwsh
+ env:
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: Tools/dump_github_contributors.ps1 > Resources/Credits/GitHub.txt
# TODO
diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs
index 7caa995836..cc41d62575 100644
--- a/Content.Benchmarks/MapLoadBenchmark.cs
+++ b/Content.Benchmarks/MapLoadBenchmark.cs
@@ -46,7 +46,7 @@ public async Task Cleanup()
PoolManager.Shutdown();
}
- public static readonly string[] MapsSource = { "Empty", "Box", "Aspid", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Gemini", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry" };
+ public static readonly string[] MapsSource = { "Empty", "Dev", "CentComm", "TestTeg", };
[ParamsSource(nameof(MapsSource))]
public string Map;
diff --git a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
index de51b2fb19..8512107b69 100644
--- a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
+++ b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
@@ -58,7 +58,7 @@ await _pair.Server.WaitPost(() =>
for (var i = 0; i < N; i++)
{
_entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
- _spawnSys.EquipStartingGear(_entity, _gear, null);
+ _spawnSys.EquipStartingGear(_entity, _gear);
server.EntMan.DeleteEntity(_entity);
}
});
diff --git a/Content.Client/Access/IdCardSystem.cs b/Content.Client/Access/IdCardSystem.cs
index fcf2bf57de..e0c02976f7 100644
--- a/Content.Client/Access/IdCardSystem.cs
+++ b/Content.Client/Access/IdCardSystem.cs
@@ -2,6 +2,4 @@
namespace Content.Client.Access;
-public sealed class IdCardSystem : SharedIdCardSystem
-{
-}
+public sealed class IdCardSystem : SharedIdCardSystem;
diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
index 73f18aec8d..c3fac8cb92 100644
--- a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
+++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
@@ -40,9 +40,9 @@ private void OnJobChanged(string newJob)
SendMessage(new AgentIDCardJobChangedMessage(newJob));
}
- public void OnJobIconChanged(string newJobIcon)
+ public void OnJobIconChanged(string newJobIconId)
{
- SendMessage(new AgentIDCardJobIconChangedMessage(newJobIcon));
+ SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
}
///
@@ -57,7 +57,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
_window.SetCurrentName(cast.CurrentName);
_window.SetCurrentJob(cast.CurrentJob);
- _window.SetAllowedIcons(cast.Icons);
+ _window.SetAllowedIcons(cast.Icons, cast.CurrentJobIconId);
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
index beca0c41ba..9a38c0c485 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -38,7 +38,7 @@ public AgentIDCardWindow(AgentIDCardBoundUserInterface bui)
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
}
- public void SetAllowedIcons(HashSet icons)
+ public void SetAllowedIcons(HashSet icons, string currentJobIconId)
{
IconGrid.DisposeAllChildren();
@@ -79,6 +79,10 @@ public void SetAllowedIcons(HashSet icons)
jobIconButton.AddChild(jobIconTexture);
jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIcon.ID);
IconGrid.AddChild(jobIconButton);
+
+ if (jobIconId.Equals(currentJobIconId))
+ jobIconButton.Pressed = true;
+
i++;
}
}
diff --git a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
index 5b7011c195..a321b4121e 100644
--- a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
+++ b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
@@ -1,6 +1,5 @@
using Content.Shared.Access;
using Content.Shared.Access.Components;
-using Content.Shared.Access;
using Content.Shared.Access.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs
index b992e77256..0bc65eb935 100644
--- a/Content.Client/Actions/ActionsSystem.cs
+++ b/Content.Client/Actions/ActionsSystem.cs
@@ -65,6 +65,7 @@ private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponen
return;
component.Whitelist = state.Whitelist;
+ component.Blacklist = state.Blacklist;
component.CanTargetSelf = state.CanTargetSelf;
BaseHandleState(uid, component, state);
}
@@ -248,7 +249,10 @@ public void TriggerAction(EntityUid actionId, BaseActionComponent action)
if (action.ClientExclusive)
{
if (instantAction.Event != null)
+ {
instantAction.Event.Performer = user;
+ instantAction.Event.Action = actionId;
+ }
PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
}
diff --git a/Content.Client/Administration/Components/HeadstandComponent.cs b/Content.Client/Administration/Components/HeadstandComponent.cs
index d95e74576b..a4e3bfc5aa 100644
--- a/Content.Client/Administration/Components/HeadstandComponent.cs
+++ b/Content.Client/Administration/Components/HeadstandComponent.cs
@@ -3,7 +3,7 @@
namespace Content.Client.Administration.Components;
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent]
public sealed partial class HeadstandComponent : SharedHeadstandComponent
{
diff --git a/Content.Client/Administration/Components/KillSignComponent.cs b/Content.Client/Administration/Components/KillSignComponent.cs
index 1cf47b93ff..91c44ef3f2 100644
--- a/Content.Client/Administration/Components/KillSignComponent.cs
+++ b/Content.Client/Administration/Components/KillSignComponent.cs
@@ -3,6 +3,5 @@
namespace Content.Client.Administration.Components;
-[NetworkedComponent, RegisterComponent]
-public sealed partial class KillSignComponent : SharedKillSignComponent
-{ }
+[RegisterComponent]
+public sealed partial class KillSignComponent : SharedKillSignComponent;
diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs
index fdd62fb6a2..0f740c8104 100644
--- a/Content.Client/Administration/Managers/ClientAdminManager.cs
+++ b/Content.Client/Administration/Managers/ClientAdminManager.cs
@@ -126,12 +126,15 @@ void IPostInjectInit.PostInject()
public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false)
{
- return uid == _player.LocalEntity ? _adminData : null;
+ if (uid == _player.LocalEntity && (_adminData?.Active ?? includeDeAdmin))
+ return _adminData;
+
+ return null;
}
public AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false)
{
- if (_player.LocalUser == session.UserId)
+ if (_player.LocalUser == session.UserId && (_adminData?.Active ?? includeDeAdmin))
return _adminData;
return null;
diff --git a/Content.Client/Administration/Systems/AdminVerbSystem.cs b/Content.Client/Administration/Systems/AdminVerbSystem.cs
index e0f84bc4f0..dced59bbf2 100644
--- a/Content.Client/Administration/Systems/AdminVerbSystem.cs
+++ b/Content.Client/Administration/Systems/AdminVerbSystem.cs
@@ -1,3 +1,6 @@
+using Content.Shared.Administration;
+using Content.Shared.Administration.Managers;
+using Content.Shared.Mind.Components;
using Content.Shared.Verbs;
using Robust.Client.Console;
using Robust.Shared.Utility;
@@ -11,10 +14,12 @@ sealed class AdminVerbSystem : EntitySystem
{
[Dependency] private readonly IClientConGroupController _clientConGroupController = default!;
[Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
+ [Dependency] private readonly ISharedAdminManager _admin = default!;
public override void Initialize()
{
SubscribeLocalEvent>(AddAdminVerbs);
+
}
private void AddAdminVerbs(GetVerbsEvent args)
@@ -33,6 +38,24 @@ private void AddAdminVerbs(GetVerbsEvent args)
};
args.Verbs.Add(verb);
}
+
+ if (!_admin.IsAdmin(args.User))
+ return;
+
+ if (_admin.HasAdminFlag(args.User, AdminFlags.Admin))
+ args.ExtraCategories.Add(VerbCategory.Admin);
+
+ if (_admin.HasAdminFlag(args.User, AdminFlags.Fun) && HasComp(args.Target))
+ args.ExtraCategories.Add(VerbCategory.Antag);
+
+ if (_admin.HasAdminFlag(args.User, AdminFlags.Debug))
+ args.ExtraCategories.Add(VerbCategory.Debug);
+
+ if (_admin.HasAdminFlag(args.User, AdminFlags.Fun))
+ args.ExtraCategories.Add(VerbCategory.Smite);
+
+ if (_admin.HasAdminFlag(args.User, AdminFlags.Admin))
+ args.ExtraCategories.Add(VerbCategory.Tricks);
}
}
}
diff --git a/Content.Client/Administration/UI/AdminUIHelpers.cs b/Content.Client/Administration/UI/AdminUIHelpers.cs
index 89ab33e931..7fa8172891 100644
--- a/Content.Client/Administration/UI/AdminUIHelpers.cs
+++ b/Content.Client/Administration/UI/AdminUIHelpers.cs
@@ -50,7 +50,7 @@ public static bool TryConfirm(Button button, Dictionary
diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml.cs b/Content.Client/Lathe/UI/RecipeControl.xaml.cs
index bf85ff7d93..47b6b5932c 100644
--- a/Content.Client/Lathe/UI/RecipeControl.xaml.cs
+++ b/Content.Client/Lathe/UI/RecipeControl.xaml.cs
@@ -2,8 +2,8 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Graphics;
namespace Content.Client.Lathe.UI;
@@ -13,12 +13,12 @@ public sealed partial class RecipeControl : Control
public Action? OnButtonPressed;
public Func TooltipTextSupplier;
- public RecipeControl(LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, Texture? texture = null)
+ public RecipeControl(LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, List textures)
{
RobustXamlLoader.Load(this);
RecipeName.Text = recipe.Name;
- RecipeTexture.Texture = texture;
+ RecipeTextures.Textures = textures;
Button.Disabled = !canProduce;
TooltipTextSupplier = tooltipTextSupplier;
Button.TooltipSupplier = SupplyTooltip;
diff --git a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs
index a2a7fb2531..8077406730 100644
--- a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs
+++ b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs
@@ -52,7 +52,7 @@ protected override void OnAppearanceChange(EntityUid uid, ExpendableLightCompone
case ExpendableLightState.Lit:
_audioSystem.Stop(comp.PlayingStream);
comp.PlayingStream = _audioSystem.PlayPvs(
- comp.LoopedSound, uid, SharedExpendableLightComponent.LoopedSoundParams)?.Entity;
+ comp.LoopedSound, uid)?.Entity;
if (args.Sprite.LayerMapTryGet(ExpendableLightVisualLayers.Overlay, out var layerIdx, true))
{
diff --git a/Content.Client/Preferences/ClientPreferencesManager.cs b/Content.Client/Lobby/ClientPreferencesManager.cs
similarity index 99%
rename from Content.Client/Preferences/ClientPreferencesManager.cs
rename to Content.Client/Lobby/ClientPreferencesManager.cs
index aca7159504..2926968657 100644
--- a/Content.Client/Preferences/ClientPreferencesManager.cs
+++ b/Content.Client/Lobby/ClientPreferencesManager.cs
@@ -10,7 +10,7 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
-namespace Content.Client.Preferences
+namespace Content.Client.Lobby
{
///
/// Receives and from the server during the initial
diff --git a/Content.Client/Preferences/IClientPreferencesManager.cs b/Content.Client/Lobby/IClientPreferencesManager.cs
similarity index 94%
rename from Content.Client/Preferences/IClientPreferencesManager.cs
rename to Content.Client/Lobby/IClientPreferencesManager.cs
index e55d6b600c..1b72593ad0 100644
--- a/Content.Client/Preferences/IClientPreferencesManager.cs
+++ b/Content.Client/Lobby/IClientPreferencesManager.cs
@@ -1,7 +1,7 @@
using System;
using Content.Shared.Preferences;
-namespace Content.Client.Preferences
+namespace Content.Client.Lobby
{
public interface IClientPreferencesManager
{
diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs
index bed52217a9..2e728f552a 100644
--- a/Content.Client/Lobby/LobbyState.cs
+++ b/Content.Client/Lobby/LobbyState.cs
@@ -3,8 +3,6 @@
using Content.Client.LateJoin;
using Content.Client.Lobby.UI;
using Content.Client.Message;
-using Content.Client.Preferences;
-using Content.Client.Preferences.UI;
using Content.Client.UserInterface.Systems.Chat;
using Content.Client.Voting;
using Robust.Client;
@@ -12,8 +10,6 @@
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Configuration;
-using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -25,59 +21,39 @@ public sealed class LobbyState : Robust.Client.State.State
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
- [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IVoteManager _voteManager = default!;
- [Dependency] private readonly IConfigurationManager _configurationManager = default!;
-
- [ViewVariables] private CharacterSetupGui? _characterSetup;
private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!;
protected override Type? LinkedScreenType { get; } = typeof(LobbyGui);
- private LobbyGui? _lobby;
+ public LobbyGui? Lobby;
protected override void Startup()
{
if (_userInterfaceManager.ActiveScreen == null)
return;
- _lobby = (LobbyGui) _userInterfaceManager.ActiveScreen;
+ Lobby = (LobbyGui) _userInterfaceManager.ActiveScreen;
var chatController = _userInterfaceManager.GetUIController();
_gameTicker = _entityManager.System();
_contentAudioSystem = _entityManager.System();
_contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo;
- _characterSetup = new CharacterSetupGui(_entityManager, _resourceCache, _preferencesManager,
- _prototypeManager, _configurationManager);
- LayoutContainer.SetAnchorPreset(_characterSetup, LayoutContainer.LayoutPreset.Wide);
- _lobby.CharacterSetupState.AddChild(_characterSetup);
chatController.SetMainChat(true);
- _voteManager.SetPopupContainer(_lobby.VoteContainer);
-
- _characterSetup.CloseButton.OnPressed += _ =>
- {
- _lobby.SwitchState(LobbyGui.LobbyGuiState.Default);
- };
+ _voteManager.SetPopupContainer(Lobby.VoteContainer);
+ LayoutContainer.SetAnchorPreset(Lobby, LayoutContainer.LayoutPreset.Wide);
+ Lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; // The eye of refactor gazes upon you...
- _characterSetup.SaveButton.OnPressed += _ =>
- {
- _characterSetup.Save();
- _userInterfaceManager.GetUIController().UpdateCharacterUI();
- };
-
- LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
- _lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you...
UpdateLobbyUi();
- _lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
- _lobby.ReadyButton.OnPressed += OnReadyPressed;
- _lobby.ReadyButton.OnToggled += OnReadyToggled;
+ Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
+ Lobby.ReadyButton.OnPressed += OnReadyPressed;
+ Lobby.ReadyButton.OnToggled += OnReadyToggled;
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
@@ -95,20 +71,23 @@ protected override void Shutdown()
_voteManager.ClearPopupContainer();
- _lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
- _lobby!.ReadyButton.OnPressed -= OnReadyPressed;
- _lobby!.ReadyButton.OnToggled -= OnReadyToggled;
+ Lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
+ Lobby!.ReadyButton.OnPressed -= OnReadyPressed;
+ Lobby!.ReadyButton.OnToggled -= OnReadyToggled;
- _lobby = null;
+ Lobby = null;
+ }
- _characterSetup?.Dispose();
- _characterSetup = null;
+ public void SwitchState(LobbyGui.LobbyGuiState state)
+ {
+ // Yeah I hate this but LobbyState contains all the badness for now
+ Lobby?.SwitchState(state);
}
private void OnSetupPressed(BaseButton.ButtonEventArgs args)
{
SetReady(false);
- _lobby!.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup);
+ Lobby?.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup);
}
private void OnReadyPressed(BaseButton.ButtonEventArgs args)
@@ -128,20 +107,20 @@ public override void FrameUpdate(FrameEventArgs e)
{
if (_gameTicker.IsGameStarted)
{
- _lobby!.StartTime.Text = string.Empty;
+ Lobby!.StartTime.Text = string.Empty;
var roundTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
- _lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-time", ("hours", roundTime.Hours), ("minutes", roundTime.Minutes));
+ Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-time", ("hours", roundTime.Hours), ("minutes", roundTime.Minutes));
return;
}
- _lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started");
+ Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started");
string text;
if (_gameTicker.Paused)
text = Loc.GetString("lobby-state-paused");
else if (_gameTicker.StartTime < _gameTiming.CurTime)
{
- _lobby!.StartTime.Text = Loc.GetString("lobby-state-soon");
+ Lobby!.StartTime.Text = Loc.GetString("lobby-state-soon");
return;
}
else
@@ -156,7 +135,7 @@ public override void FrameUpdate(FrameEventArgs e)
text = $"{difference.Minutes}:{difference.Seconds:D2}";
}
- _lobby!.StartTime.Text = Loc.GetString("lobby-state-round-start-countdown-text", ("timeLeft", text));
+ Lobby!.StartTime.Text = Loc.GetString("lobby-state-round-start-countdown-text", ("timeLeft", text));
}
private void LobbyStatusUpdated()
@@ -167,36 +146,36 @@ private void LobbyStatusUpdated()
private void LobbyLateJoinStatusUpdated()
{
- _lobby!.ReadyButton.Disabled = _gameTicker.DisallowedLateJoin;
+ Lobby!.ReadyButton.Disabled = _gameTicker.DisallowedLateJoin;
}
private void UpdateLobbyUi()
{
if (_gameTicker.IsGameStarted)
{
- _lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state");
- _lobby!.ReadyButton.ToggleMode = false;
- _lobby!.ReadyButton.Pressed = false;
- _lobby!.ObserveButton.Disabled = false;
+ Lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state");
+ Lobby!.ReadyButton.ToggleMode = false;
+ Lobby!.ReadyButton.Pressed = false;
+ Lobby!.ObserveButton.Disabled = false;
}
else
{
- _lobby!.StartTime.Text = string.Empty;
- _lobby!.ReadyButton.Text = Loc.GetString(_lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready");
- _lobby!.ReadyButton.ToggleMode = true;
- _lobby!.ReadyButton.Disabled = false;
- _lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
- _lobby!.ObserveButton.Disabled = true;
+ Lobby!.StartTime.Text = string.Empty;
+ Lobby!.ReadyButton.Text = Loc.GetString(Lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready");
+ Lobby!.ReadyButton.ToggleMode = true;
+ Lobby!.ReadyButton.Disabled = false;
+ Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
+ Lobby!.ObserveButton.Disabled = true;
}
if (_gameTicker.ServerInfoBlob != null)
- _lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
+ Lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
}
private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev)
{
if (ev.SoundtrackFilename == null)
- _lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
+ Lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
else if (ev.SoundtrackFilename != null
&& _resourceCache.TryGetResource(ev.SoundtrackFilename, out var lobbySongResource))
{
@@ -214,16 +193,16 @@ private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev)
("songTitle", title),
("songArtist", artist));
- _lobby!.LobbySong.SetMarkup(markup);
+ Lobby!.LobbySong.SetMarkup(markup);
}
}
private void UpdateLobbyBackground()
{
if (_gameTicker.LobbyBackground != null)
- _lobby!.Background.Texture = _resourceCache.GetResource(_gameTicker.LobbyBackground);
+ Lobby!.Background.Texture = _resourceCache.GetResource(_gameTicker.LobbyBackground);
else
- _lobby!.Background.Texture = null;
+ Lobby!.Background.Texture = null;
}
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index 47ab651c10..26643cb603 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -3,156 +3,252 @@
using Content.Client.Inventory;
using Content.Client.Lobby.UI;
using Content.Client.Players.PlayTimeTracking;
-using Content.Client.Preferences;
-using Content.Client.Preferences.UI;
+using Content.Shared.CCVar;
+using Content.Shared.Clothing.Loadouts.Prototypes;
using Content.Shared.Clothing.Loadouts.Systems;
using Content.Shared.GameTicking;
-using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
+using Content.Shared.Traits;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using static Content.Shared.Humanoid.SharedHumanoidAppearanceSystem;
+using CharacterSetupGui = Content.Client.Lobby.UI.CharacterSetupGui;
+using HumanoidProfileEditor = Content.Client.Lobby.UI.HumanoidProfileEditor;
namespace Content.Client.Lobby;
public sealed class LobbyUIController : UIController, IOnStateEntered, IOnStateExited
{
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
- [Dependency] private readonly IStateManager _stateManager = default!;
+ [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+ [Dependency] private readonly IFileDialogManager _dialogManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IStateManager _stateManager = default!;
+ [Dependency] private readonly JobRequirementsManager _requirements = default!;
+ [Dependency] private readonly MarkingManager _markings = default!;
[Dependency] private readonly JobRequirementsManager _jobRequirements = default!;
[UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly LoadoutSystem _loadouts = default!;
- private LobbyCharacterPanel? _previewPanel;
+ private CharacterSetupGui? _characterSetup;
private HumanoidProfileEditor? _profileEditor;
- /*
- * Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
- * that is shared too.
- */
+ /// This is the character preview panel in the chat. This should only update if their character updates
+ private LobbyCharacterPreviewPanel? PreviewPanel => GetLobbyPreview();
- ///
- /// Preview dummy for role gear.
- ///
- private EntityUid? _previewDummy;
+ /// This is the modified profile currently being edited
+ private HumanoidCharacterProfile? EditedProfile => _profileEditor?.Profile;
+ private int? EditedSlot => _profileEditor?.CharacterSlot;
- [Access(typeof(HumanoidProfileEditor))]
- public bool UpdateClothes = true;
- [Access(typeof(HumanoidProfileEditor))]
- public bool ShowClothes = true;
- [Access(typeof(HumanoidProfileEditor))]
- public bool ShowLoadouts = true;
-
- // TODO: Load the species directly and don't update entity ever.
- public event Action? PreviewDummyUpdated;
public override void Initialize()
{
base.Initialize();
+ _prototypeManager.PrototypesReloaded += OnPrototypesReloaded;
_preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
+ _requirements.Updated += OnRequirementsUpdated;
+
+ _configurationManager.OnValueChanged(CCVars.FlavorText, _ => _profileEditor?.RefreshFlavorText());
+ _configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
+ _configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
+
+ _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
+ }
+
+ public void OnStateEntered(LobbyState state)
+ {
+ PreviewPanel?.SetLoaded(_preferencesManager.ServerDataLoaded);
+ ReloadCharacterSetup();
+ }
+
+ public void OnStateExited(LobbyState state)
+ {
+ PreviewPanel?.SetLoaded(false);
+ _characterSetup?.Dispose();
+ _profileEditor?.Dispose();
+ _characterSetup = null;
+ _profileEditor = null;
}
+
private void PreferencesDataLoaded()
{
- if (_previewDummy != null)
- EntityManager.DeleteEntity(_previewDummy);
+ PreviewPanel?.SetLoaded(true);
+
+ if (_stateManager.CurrentState is not LobbyState)
+ return;
- UpdateCharacterUI();
+ ReloadCharacterSetup();
}
- public void OnStateEntered(LobbyState state)
+ private LobbyCharacterPreviewPanel? GetLobbyPreview()
{
+ return _stateManager.CurrentState is LobbyState lobby ? lobby.Lobby?.CharacterPreview : null;
}
- public void OnStateExited(LobbyState state)
+ private void OnRequirementsUpdated()
{
- EntityManager.DeleteEntity(_previewDummy);
- _previewDummy = null;
+ if (_profileEditor == null)
+ return;
+
+ _profileEditor.RefreshAntags();
+ _profileEditor.RefreshJobs();
}
- public void SetPreviewPanel(LobbyCharacterPanel? panel)
+ private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
{
- _previewPanel = panel;
- UpdateCharacterUI();
+ if (_profileEditor == null)
+ return;
+
+ if (obj.WasModified())
+ _profileEditor.RefreshSpecies();
+
+ if (obj.WasModified())
+ _profileEditor.RefreshAntags();
+
+ if (obj.WasModified()
+ || obj.WasModified())
+ _profileEditor.RefreshJobs();
+
+ if (obj.WasModified())
+ _profileEditor.UpdateTraits(null, true);
+
+ if (obj.WasModified())
+ _profileEditor.UpdateLoadouts(null, true);
}
- public void SetProfileEditor(HumanoidProfileEditor? editor)
+
+ /// Reloads every single character setup control
+ public void ReloadCharacterSetup()
{
- _profileEditor = editor;
- UpdateCharacterUI();
+ RefreshLobbyPreview();
+ var (characterGui, profileEditor) = EnsureGui();
+ characterGui.ReloadCharacterPickers();
+ profileEditor.SetProfile(
+ (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
+ _preferencesManager.Preferences?.SelectedCharacterIndex);
}
- public void UpdateCharacterUI()
+ /// Refreshes the character preview in the lobby chat
+ private void RefreshLobbyPreview()
{
- // Test moment
- if (_stateManager.CurrentState is not LobbyState)
+ if (PreviewPanel == null)
return;
- if (!_preferencesManager.ServerDataLoaded)
+ // Get selected character, load it, then set it
+ var character = _preferencesManager.Preferences?.SelectedCharacter;
+
+ if (character is not HumanoidCharacterProfile humanoid)
{
- _previewPanel?.SetLoaded(false);
+ PreviewPanel.SetSprite(EntityUid.Invalid);
+ PreviewPanel.SetSummaryText(string.Empty);
return;
}
- var maybeProfile = _profileEditor?.Profile ?? (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
+ var dummy = LoadProfileEntity(humanoid, true);
+ PreviewPanel.SetSprite(dummy);
+ PreviewPanel.SetSummaryText(humanoid.Summary);
+ }
- if (_previewDummy == null
- || maybeProfile.Species != EntityManager.GetComponent(_previewDummy.Value).Species)
- {
- RespawnDummy(maybeProfile);
- _previewPanel?.SetSprite(_previewDummy!.Value);
- }
+ private void RefreshProfileEditor()
+ {
+ _profileEditor?.RefreshAntags();
+ _profileEditor?.RefreshJobs();
+ }
- _previewPanel?.SetLoaded(true);
+ private void SaveProfile()
+ {
+ DebugTools.Assert(EditedProfile != null);
- if (_previewDummy == null)
+ if (EditedProfile == null || EditedSlot == null)
return;
- _previewPanel?.SetSummaryText(maybeProfile.Summary);
- _humanoid.LoadProfile(_previewDummy.Value, maybeProfile);
+ var selected = _preferencesManager.Preferences?.SelectedCharacterIndex;
+ if (selected == null)
+ return;
+ _preferencesManager.UpdateCharacter(EditedProfile, EditedSlot.Value);
+ ReloadCharacterSetup();
+ }
- if (UpdateClothes)
+ private (CharacterSetupGui, HumanoidProfileEditor) EnsureGui()
+ {
+ if (_characterSetup != null && _profileEditor != null)
{
- RemoveDummyClothes(_previewDummy.Value);
- if (ShowClothes)
- GiveDummyJobClothes(_previewDummy.Value, GetPreferredJob(maybeProfile), maybeProfile);
- if (ShowLoadouts)
- _loadouts.ApplyCharacterLoadout(_previewDummy.Value, GetPreferredJob(maybeProfile), maybeProfile,
- _jobRequirements.GetRawPlayTimeTrackers(), _jobRequirements.IsWhitelisted());
- UpdateClothes = false;
+ _characterSetup.Visible = true;
+ _profileEditor.Visible = true;
+ return (_characterSetup, _profileEditor);
}
- PreviewDummyUpdated?.Invoke(_previewDummy.Value);
- }
+ _profileEditor = new HumanoidProfileEditor(
+ _preferencesManager,
+ _configurationManager,
+ EntityManager,
+ _dialogManager,
+ _playerManager,
+ _prototypeManager,
+ _requirements,
+ _markings);
+ _characterSetup = new CharacterSetupGui(EntityManager, _prototypeManager, _resourceCache, _preferencesManager, _profileEditor);
- public void RespawnDummy(HumanoidCharacterProfile profile)
- {
- if (_previewDummy != null)
- RemoveDummyClothes(_previewDummy.Value);
+ _characterSetup.CloseButton.OnPressed += _ =>
+ {
+ // Reset sliders etc.
+ _profileEditor.SetProfile(null, null);
+ _profileEditor.Visible = false;
+ if (_stateManager.CurrentState is LobbyState lobbyGui)
+ {
+ lobbyGui.SwitchState(LobbyGui.LobbyGuiState.Default);
+ }
+ };
+
+ _profileEditor.Save += SaveProfile;
+
+ _characterSetup.SelectCharacter += args =>
+ {
+ _preferencesManager.SelectCharacter(args);
+ ReloadCharacterSetup();
+ };
+
+ _characterSetup.DeleteCharacter += args =>
+ {
+ _preferencesManager.DeleteCharacter(args);
- EntityManager.DeleteEntity(_previewDummy);
- _previewDummy = EntityManager.SpawnEntity(
- _prototypeManager.Index(profile.Species).DollPrototype, MapCoordinates.Nullspace);
+ if (EditedSlot == args)
+ // Reload everything
+ ReloadCharacterSetup();
+ else
+ // Only need to reload character pickers
+ _characterSetup?.ReloadCharacterPickers();
+ };
- UpdateClothes = true;
+ if (_stateManager.CurrentState is LobbyState lobby)
+ lobby.Lobby?.CharacterSetupState.AddChild(_characterSetup);
+
+ return (_characterSetup, _profileEditor);
}
- ///
- /// Gets the highest priority job for the profile.
- ///
+ #region Helpers
+
+ /// Gets the highest priority job for the profile.
public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile)
{
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
- // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is ReSharper smoking?)
return _prototypeManager.Index(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
}
@@ -166,9 +262,7 @@ public void RemoveDummyClothes(EntityUid dummy)
EntityManager.DeleteEntity(unequippedItem.Value);
}
- ///
- /// Applies the highest priority job's clothes and loadouts to the dummy.
- ///
+ /// Applies the highest priority job's clothes and loadouts to the dummy.
public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile)
{
var job = GetPreferredJob(profile);
@@ -176,9 +270,7 @@ public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile
_loadouts.ApplyCharacterLoadout(dummy, job, profile, _jobRequirements.GetRawPlayTimeTrackers(), _jobRequirements.IsWhitelisted());
}
- ///
- /// Applies the specified job's clothes to the dummy.
- ///
+ /// Applies the specified job's clothes to the dummy.
public void GiveDummyJobClothes(EntityUid dummy, JobPrototype job, HumanoidCharacterProfile profile)
{
if (!_inventory.TryGetSlots(dummy, out var slots)
@@ -202,8 +294,28 @@ public void GiveDummyJobClothes(EntityUid dummy, JobPrototype job, HumanoidChara
}
}
- public EntityUid? GetPreviewDummy()
+ /// Loads the profile onto a dummy entity
+ public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, bool jobClothes)
{
- return _previewDummy;
+ EntityUid dummyEnt;
+
+ if (humanoid is not null)
+ {
+ var dummy = _prototypeManager.Index(humanoid.Species).DollPrototype;
+ dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
+ }
+ else
+ dummyEnt = EntityManager.SpawnEntity(
+ _prototypeManager.Index(DefaultSpecies).DollPrototype,
+ MapCoordinates.Nullspace);
+
+ _humanoid.LoadProfile(dummyEnt, humanoid);
+
+ if (humanoid != null && jobClothes)
+ GiveDummyJobClothesLoadout(dummyEnt, humanoid);
+
+ return dummyEnt;
}
+
+ #endregion
}
diff --git a/Content.Client/Lobby/UI/CharacterPickerButton.xaml b/Content.Client/Lobby/UI/CharacterPickerButton.xaml
new file mode 100644
index 0000000000..fa428b9b61
--- /dev/null
+++ b/Content.Client/Lobby/UI/CharacterPickerButton.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs b/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs
new file mode 100644
index 0000000000..68833e13ea
--- /dev/null
+++ b/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs
@@ -0,0 +1,88 @@
+using System.Linq;
+using Content.Client.Humanoid;
+using Content.Shared.Clothing;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Roles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Lobby.UI;
+
+/// Holds character data on the side of the setup GUI
+[GenerateTypedNameReferences]
+public sealed partial class CharacterPickerButton : ContainerButton
+{
+ private IEntityManager _entManager;
+
+ private EntityUid _previewDummy;
+
+ /// Invoked if we should delete the attached character
+ public event Action? OnDeletePressed;
+
+ public CharacterPickerButton(
+ IEntityManager entityManager,
+ IPrototypeManager prototypeManager,
+ ButtonGroup group,
+ ICharacterProfile profile,
+ bool isSelected)
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = entityManager;
+ AddStyleClass(StyleClassButton);
+ ToggleMode = true;
+ Group = group;
+ var description = profile.Name;
+
+ if (profile is not HumanoidCharacterProfile humanoid)
+ {
+ _previewDummy = entityManager.SpawnEntity(prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
+ }
+ else
+ {
+ _previewDummy = UserInterfaceManager.GetUIController()
+ .LoadProfileEntity(humanoid, true);
+
+ var highPriorityJob = humanoid.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key;
+ if (highPriorityJob != null)
+ {
+ var jobName = prototypeManager.Index(highPriorityJob).LocalizedName;
+ description = $"{description}\n{jobName}";
+ }
+ }
+
+ Pressed = isSelected;
+ DeleteButton.Visible = !isSelected;
+
+ View.SetEntity(_previewDummy);
+ DescriptionLabel.Text = description;
+
+ ConfirmDeleteButton.OnPressed += _ =>
+ {
+ Parent?.RemoveChild(this);
+ Parent?.RemoveChild(ConfirmDeleteButton);
+ OnDeletePressed?.Invoke();
+ };
+
+ DeleteButton.OnPressed += _ =>
+ {
+ DeleteButton.Visible = false;
+ ConfirmDeleteButton.Visible = true;
+ };
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _entManager.DeleteEntity(_previewDummy);
+ _previewDummy = default;
+ }
+}
diff --git a/Content.Client/Preferences/UI/CharacterSetupGui.xaml b/Content.Client/Lobby/UI/CharacterSetupGui.xaml
similarity index 91%
rename from Content.Client/Preferences/UI/CharacterSetupGui.xaml
rename to Content.Client/Lobby/UI/CharacterSetupGui.xaml
index 35067eebfd..f83be26588 100644
--- a/Content.Client/Preferences/UI/CharacterSetupGui.xaml
+++ b/Content.Client/Lobby/UI/CharacterSetupGui.xaml
@@ -17,10 +17,6 @@
-
+ /// Holds the entire character setup GUI, from character picks to individual character editing.
+ ///
+ [GenerateTypedNameReferences]
+ public sealed partial class CharacterSetupGui : Control
+ {
+ private readonly IClientPreferencesManager _preferencesManager;
+ private readonly IEntityManager _entManager;
+ private readonly IPrototypeManager _protomanager;
+
+ private readonly Button _createNewCharacterButton;
+
+ public event Action? SelectCharacter;
+ public event Action? DeleteCharacter;
+
+ public CharacterSetupGui(
+ IEntityManager entManager,
+ IPrototypeManager protoManager,
+ IResourceCache resourceCache,
+ IClientPreferencesManager preferencesManager,
+ HumanoidProfileEditor profileEditor)
+ {
+ RobustXamlLoader.Load(this);
+ _preferencesManager = preferencesManager;
+ _entManager = entManager;
+ _protomanager = protoManager;
+
+ var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
+ var back = new StyleBoxTexture
+ {
+ Texture = panelTex,
+ Modulate = new Color(37, 37, 42)
+ };
+ back.SetPatchMargin(StyleBox.Margin.All, 10);
+
+ BackgroundPanel.PanelOverride = back;
+
+ _createNewCharacterButton = new Button
+ {
+ Text = Loc.GetString("character-setup-gui-create-new-character-button"),
+ };
+
+ _createNewCharacterButton.OnPressed += args =>
+ {
+ preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
+ ReloadCharacterPickers();
+ args.Event.Handle();
+ };
+
+ CharEditor.AddChild(profileEditor);
+ RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
+
+ StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
+ }
+
+ ///
+ /// Disposes and reloads all character picker buttons from the preferences data.
+ ///
+ public void ReloadCharacterPickers()
+ {
+ _createNewCharacterButton.Orphan();
+ Characters.DisposeAllChildren();
+
+ var numberOfFullSlots = 0;
+ var characterButtonsGroup = new ButtonGroup();
+
+ if (!_preferencesManager.ServerDataLoaded)
+ {
+ return;
+ }
+
+ _createNewCharacterButton.ToolTip =
+ Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
+ ("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots));
+
+ var selectedSlot = _preferencesManager.Preferences?.SelectedCharacterIndex;
+
+ foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
+ {
+ numberOfFullSlots++;
+ var characterPickerButton = new CharacterPickerButton(_entManager,
+ _protomanager,
+ characterButtonsGroup,
+ character,
+ slot == selectedSlot);
+
+ Characters.AddChild(characterPickerButton);
+
+ characterPickerButton.OnPressed += args =>
+ {
+ SelectCharacter?.Invoke(slot);
+ };
+
+ characterPickerButton.OnDeletePressed += () =>
+ {
+ DeleteCharacter?.Invoke(slot);
+ };
+ }
+
+ _createNewCharacterButton.Disabled = numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
+ Characters.AddChild(_createNewCharacterButton);
+ }
+ }
+}
diff --git a/Content.Client/Preferences/UI/HighlightedContainer.xaml b/Content.Client/Lobby/UI/HighlightedContainer.xaml
similarity index 100%
rename from Content.Client/Preferences/UI/HighlightedContainer.xaml
rename to Content.Client/Lobby/UI/HighlightedContainer.xaml
diff --git a/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs b/Content.Client/Lobby/UI/HighlightedContainer.xaml.cs
similarity index 88%
rename from Content.Client/Preferences/UI/HighlightedContainer.xaml.cs
rename to Content.Client/Lobby/UI/HighlightedContainer.xaml.cs
index 68294d0f05..084c1c3709 100644
--- a/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs
+++ b/Content.Client/Lobby/UI/HighlightedContainer.xaml.cs
@@ -2,7 +2,7 @@
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
-namespace Content.Client.Preferences.UI;
+namespace Content.Client.Lobby.UI;
[GenerateTypedNameReferences]
public sealed partial class HighlightedContainer : PanelContainer
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
new file mode 100644
index 0000000000..df364546ba
--- /dev/null
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
new file mode 100644
index 0000000000..3f526981a4
--- /dev/null
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -0,0 +1,2339 @@
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using Content.Client.Administration.UI;
+using Content.Client.Guidebook;
+using Content.Client.Humanoid;
+using Content.Client.Message;
+using Content.Client.Players.PlayTimeTracking;
+using Content.Client.UserInterface.Controls;
+using Content.Client.UserInterface.Systems.Guidebook;
+using Content.Shared.CCVar;
+using Content.Shared.Clothing.Components;
+using Content.Shared.Clothing.Loadouts.Prototypes;
+using Content.Shared.Customization.Systems;
+using Content.Shared.GameTicking;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Preferences;
+using Content.Shared.Roles;
+using Content.Shared.StatusIcon;
+using Content.Shared.Traits;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.Utility;
+using Robust.Client.Player;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Physics;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using Direction = Robust.Shared.Maths.Direction;
+
+namespace Content.Client.Lobby.UI
+{
+ [GenerateTypedNameReferences]
+ public sealed partial class HumanoidProfileEditor : BoxContainer
+ {
+ private readonly IConfigurationManager _cfgManager;
+ private readonly IEntityManager _entManager;
+ private readonly IFileDialogManager _dialogManager;
+ private readonly IPlayerManager _playerManager;
+ private readonly IPrototypeManager _prototypeManager;
+ private readonly IClientPreferencesManager _preferencesManager;
+ private readonly MarkingManager _markingManager;
+ private readonly JobRequirementsManager _requirements;
+ private readonly CharacterRequirementsSystem _characterRequirementsSystem;
+ private readonly LobbyUIController _controller;
+ private FlavorText.FlavorText? _flavorText;
+ private BoxContainer _ccustomspecienamecontainerEdit => CCustomSpecieName;
+ private LineEdit _customspecienameEdit => CCustomSpecieNameEdit;
+ private TextEdit? _flavorTextEdit;
+
+ /// If we're attempting to save
+ public event Action? Save;
+ private bool _exporting;
+ private bool _isDirty;
+
+ /// The character slot for the current profile
+ public int? CharacterSlot;
+ /// The work in progress profile being edited
+ public HumanoidCharacterProfile? Profile;
+ /// Entity Used for the profile editor preview
+ public EntityUid PreviewDummy;
+ /// Temporary override of their selected job, used to preview roles
+ public JobPrototype? JobOverride;
+
+ private List _species = new();
+ private List<(string, RequirementsSelector)> _jobPriorities = new();
+ private readonly Dictionary _jobCategories;
+
+ private Dictionary _confirmationData = new();
+ private List _traitPreferences = new();
+ private int _traitCount;
+ private List _loadoutPreferences = new();
+
+ private Direction _previewRotation = Direction.North;
+ private ColorSelectorSliders _rgbSkinColorSelector;
+
+ public event Action? OnProfileChanged;
+
+ [ValidatePrototypeId]
+ private const string DefaultSpeciesGuidebook = "Species";
+
+ public HumanoidProfileEditor(
+ IClientPreferencesManager preferencesManager,
+ IConfigurationManager cfgManager,
+ IEntityManager entManager,
+ IFileDialogManager dialogManager,
+ IPlayerManager playerManager,
+ IPrototypeManager prototypeManager,
+ JobRequirementsManager requirements,
+ MarkingManager markings)
+ {
+ RobustXamlLoader.Load(this);
+ _cfgManager = cfgManager;
+ _entManager = entManager;
+ _dialogManager = dialogManager;
+ _playerManager = playerManager;
+ _prototypeManager = prototypeManager;
+ _markingManager = markings;
+ _preferencesManager = preferencesManager;
+ _requirements = requirements;
+ _characterRequirementsSystem = _entManager.System();
+ _controller = UserInterfaceManager.GetUIController();
+
+ ImportButton.OnPressed += args => { ImportProfile(); };
+ ExportButton.OnPressed += args => { ExportProfile(); };
+ SaveButton.OnPressed += args => { Save?.Invoke(); };
+ ResetButton.OnPressed += args =>
+ {
+ SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
+ _preferencesManager.Preferences?.SelectedCharacterIndex);
+ };
+
+ #region Left
+
+ #region Name
+
+ NameEdit.OnTextChanged += args => { SetName(args.Text); };
+ NameRandomize.OnPressed += _ => RandomizeName();
+ RandomizeEverything.OnPressed += _ => { RandomizeProfile(); };
+ WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
+
+ #endregion Name
+
+ #region Custom Specie Name
+
+ _customspecienameEdit.OnTextChanged += args => { SetCustomSpecieName(args.Text); };
+
+ #endregion CustomSpecieName
+
+ #region Appearance
+
+ Appearance.Orphan();
+ CTabContainer.AddTab(Appearance, Loc.GetString("humanoid-profile-editor-appearance-tab"));
+
+ #region Sex
+
+ SexButton.OnItemSelected += args =>
+ {
+ SexButton.SelectId(args.Id);
+ SetSex((Sex) args.Id);
+ };
+
+ #endregion Sex
+
+ #region Age
+
+ AgeEdit.OnTextChanged += args =>
+ {
+ if (!int.TryParse(args.Text, out var newAge))
+ return;
+
+ SetAge(newAge);
+ };
+
+ #endregion Age
+
+ #region Gender
+
+ PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male);
+ PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female);
+ PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene);
+ PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter);
+
+ PronounsButton.OnItemSelected += args =>
+ {
+ PronounsButton.SelectId(args.Id);
+ SetGender((Gender) args.Id);
+ };
+
+ #endregion Gender
+
+ #region Species
+
+ RefreshSpecies();
+
+ SpeciesButton.OnItemSelected += args =>
+ {
+ SpeciesButton.SelectId(args.Id);
+ SetSpecies(_species[args.Id].ID);
+ UpdateHairPickers();
+ OnSkinColorOnValueChanged();
+ UpdateCustomSpecieNameEdit();
+ UpdateHeightWidthSliders();
+ };
+
+ #endregion Species
+
+ #region Height
+
+ var prototype = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
+
+ UpdateHeightWidthSliders();
+ UpdateDimensions(SliderUpdate.Both);
+
+ HeightSlider.OnValueChanged += _ => UpdateDimensions(SliderUpdate.Height);
+ WidthSlider.OnValueChanged += _ => UpdateDimensions(SliderUpdate.Width);
+
+ HeightReset.OnPressed += _ =>
+ {
+ HeightSlider.Value = prototype.DefaultHeight;
+ UpdateDimensions(SliderUpdate.Height);
+ };
+
+ WidthReset.OnPressed += _ =>
+ {
+ WidthSlider.Value = prototype.DefaultWidth;
+ UpdateDimensions(SliderUpdate.Width);
+ };
+
+ #endregion Height
+
+ #region Skin
+
+
+ Skin.OnValueChanged += _ => { OnSkinColorOnValueChanged(); };
+ RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new());
+ _rgbSkinColorSelector.OnColorChanged += _ => { OnSkinColorOnValueChanged(); };
+
+ #endregion
+
+ #region Hair
+
+ HairStylePicker.OnMarkingSelect += newStyle =>
+ {
+ if (Profile is null)
+ return;
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithHairStyleName(newStyle.id));
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ HairStylePicker.OnColorChanged += newColor =>
+ {
+ if (Profile is null)
+ return;
+ Profile = Profile.WithCharacterAppearance(
+ Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
+ UpdateCMarkingsHair();
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ FacialHairPicker.OnMarkingSelect += newStyle =>
+ {
+ if (Profile is null)
+ return;
+ Profile = Profile.WithCharacterAppearance(
+ Profile.Appearance.WithFacialHairStyleName(newStyle.id));
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ FacialHairPicker.OnColorChanged += newColor =>
+ {
+ if (Profile is null)
+ return;
+ Profile = Profile.WithCharacterAppearance(
+ Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
+ UpdateCMarkingsFacialHair();
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ HairStylePicker.OnSlotRemove += _ =>
+ {
+ if (Profile is null)
+ return;
+ Profile = Profile.WithCharacterAppearance(
+ Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle)
+ );
+ UpdateHairPickers();
+ UpdateCMarkingsHair();
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ FacialHairPicker.OnSlotRemove += _ =>
+ {
+ if (Profile is null)
+ return;
+ Profile = Profile.WithCharacterAppearance(
+ Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle)
+ );
+ UpdateHairPickers();
+ UpdateCMarkingsFacialHair();
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ HairStylePicker.OnSlotAdd += delegate()
+ {
+ if (Profile is null)
+ return;
+
+ var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, Profile.Species).Keys
+ .FirstOrDefault();
+
+ if (string.IsNullOrEmpty(hair))
+ return;
+
+ Profile = Profile.WithCharacterAppearance(
+ Profile.Appearance.WithHairStyleName(hair)
+ );
+
+ UpdateHairPickers();
+ UpdateCMarkingsHair();
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ FacialHairPicker.OnSlotAdd += delegate()
+ {
+ if (Profile is null)
+ return;
+
+ var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, Profile.Species).Keys
+ .FirstOrDefault();
+
+ if (string.IsNullOrEmpty(hair))
+ return;
+
+ Profile = Profile.WithCharacterAppearance(
+ Profile.Appearance.WithFacialHairStyleName(hair)
+ );
+
+ UpdateHairPickers();
+ UpdateCMarkingsFacialHair();
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ #endregion Hair
+
+ #region SpawnPriority
+
+ foreach (var value in Enum.GetValues())
+ SpawnPriorityButton.AddItem(Loc.GetString($"humanoid-profile-editor-preference-spawn-priority-{value.ToString().ToLower()}"), (int) value);
+
+ SpawnPriorityButton.OnItemSelected += args =>
+ {
+ SpawnPriorityButton.SelectId(args.Id);
+ SetSpawnPriority((SpawnPriorityPreference) args.Id);
+ };
+
+ #endregion SpawnPriority
+
+ #region Eyes
+
+ EyeColorPicker.OnEyeColorPicked += newColor =>
+ {
+ if (Profile is null)
+ return;
+ Profile = Profile.WithCharacterAppearance(
+ Profile.Appearance.WithEyeColor(newColor));
+ Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
+ IsDirty = true;
+ ReloadProfilePreview();
+ };
+
+ #endregion Eyes
+
+ #endregion Appearance
+
+ #region Jobs
+
+ Jobs.Orphan();
+ CTabContainer.AddTab(Jobs, Loc.GetString("humanoid-profile-editor-jobs-tab"));
+
+ PreferenceUnavailableButton.AddItem(
+ Loc.GetString(
+ "humanoid-profile-editor-preference-unavailable-stay-in-lobby-button"),
+ (int) PreferenceUnavailableMode.StayInLobby);
+ PreferenceUnavailableButton.AddItem(
+ Loc.GetString(
+ "humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button",
+ ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))),
+ (int) PreferenceUnavailableMode.SpawnAsOverflow);
+
+ PreferenceUnavailableButton.OnItemSelected += args =>
+ {
+ PreferenceUnavailableButton.SelectId(args.Id);
+
+ Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
+ IsDirty = true;
+ };
+
+ _jobCategories = new Dictionary();
+
+ #endregion Jobs
+
+ #region Antags
+
+ Antags.Orphan();
+ CTabContainer.AddTab(Antags, Loc.GetString("humanoid-profile-editor-antags-tab"));
+
+ #endregion Antags
+
+ #region Traits
+
+ // Set up the traits tab
+ TraitsTab.Orphan();
+ CTabContainer.AddTab(TraitsTab, Loc.GetString("humanoid-profile-editor-traits-tab"));
+ _traitPreferences = new List();
+
+ // Show/Hide the traits tab if they ever get enabled/disabled
+ var traitsEnabled = cfgManager.GetCVar(CCVars.GameTraitsEnabled);
+ CTabContainer.SetTabVisible(3, traitsEnabled);
+ cfgManager.OnValueChanged(CCVars.GameTraitsEnabled,
+ enabled => CTabContainer.SetTabVisible(3, enabled));
+
+ TraitsShowUnusableButton.OnToggled += args => UpdateTraits(args.Pressed);
+ TraitsRemoveUnusableButton.OnPressed += _ => TryRemoveUnusableTraits();
+
+ UpdateTraits(false);
+
+ #endregion
+
+ #region Loadouts
+
+ // Set up the loadouts tab
+ LoadoutsTab.Orphan();
+ CTabContainer.AddTab(LoadoutsTab, Loc.GetString("humanoid-profile-editor-loadouts-tab"));
+ _loadoutPreferences = new List();
+
+ // Show/Hide the loadouts tab if they ever get enabled/disabled
+ var loadoutsEnabled = cfgManager.GetCVar(CCVars.GameLoadoutsEnabled);
+ CTabContainer.SetTabVisible(4, loadoutsEnabled);
+ ShowLoadouts.Visible = loadoutsEnabled;
+ cfgManager.OnValueChanged(CCVars.GameLoadoutsEnabled, LoadoutsChanged);
+
+ LoadoutsShowUnusableButton.OnToggled += args => UpdateLoadouts(args.Pressed);
+ LoadoutsRemoveUnusableButton.OnPressed += _ => TryRemoveUnusableLoadouts();
+
+ UpdateLoadouts(false);
+
+ #endregion
+
+ #region Markings
+
+ MarkingsTab.Orphan();
+ CTabContainer.AddTab(MarkingsTab, Loc.GetString("humanoid-profile-editor-markings-tab"));
+
+ Markings.OnMarkingAdded += OnMarkingChange;
+ Markings.OnMarkingRemoved += OnMarkingChange;
+ Markings.OnMarkingColorChange += OnMarkingChange;
+ Markings.OnMarkingRankChange += OnMarkingChange;
+
+ #endregion Markings
+
+ RefreshFlavorText();
+
+ #region Dummy
+
+ SpriteRotateLeft.OnPressed += _ =>
+ {
+ _previewRotation = _previewRotation.TurnCw();
+ SetPreviewRotation(_previewRotation);
+ };
+ SpriteRotateRight.OnPressed += _ =>
+ {
+ _previewRotation = _previewRotation.TurnCcw();
+ SetPreviewRotation(_previewRotation);
+ };
+
+ #endregion Dummy
+
+ #endregion Left
+
+ ShowClothes.OnToggled += args => { ReloadProfilePreview(); };
+
+ SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
+ UpdateSpeciesGuidebookIcon();
+
+ ReloadPreview();
+ IsDirty = false;
+ }
+
+ /// Refreshes the flavor text editor status
+ public void RefreshFlavorText()
+ {
+ if (_cfgManager.GetCVar(CCVars.FlavorText))
+ {
+ if (_flavorText != null)
+ return;
+
+ _flavorText = new FlavorText.FlavorText();
+ _flavorText.OnFlavorTextChanged += OnFlavorTextChange;
+ _flavorTextEdit = _flavorText.CFlavorTextInput;
+ CTabContainer.AddTab(_flavorText, Loc.GetString("humanoid-profile-editor-flavortext-tab"));
+ }
+ else
+ {
+ if (_flavorText == null)
+ return;
+
+ CTabContainer.RemoveChild(_flavorText);
+ _flavorText.OnFlavorTextChanged -= OnFlavorTextChange;
+ _flavorText.Dispose();
+ _flavorText = null;
+ _flavorTextEdit?.Dispose();
+ _flavorTextEdit = null;
+ }
+ }
+
+ /// Refreshes the species selector
+ public void RefreshSpecies()
+ {
+ SpeciesButton.Clear();
+ _species.Clear();
+
+ _species.AddRange(_prototypeManager.EnumeratePrototypes().Where(o => o.RoundStart));
+ var speciesIds = _species.Select(o => o.ID).ToList();
+
+ for (var i = 0; i < _species.Count; i++)
+ {
+ SpeciesButton.AddItem(Loc.GetString(_species[i].Name), i);
+
+ if (Profile?.Species.Equals(_species[i].ID) == true)
+ SpeciesButton.SelectId(i);
+ }
+
+ // If our species isn't available, reset it to default
+ if (Profile != null && !speciesIds.Contains(Profile.Species))
+ SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
+ }
+
+ public void RefreshAntags()
+ {
+ AntagList.DisposeAllChildren();
+ var items = new[]
+ {
+ ("humanoid-profile-editor-antag-preference-yes-button", 0),
+ ("humanoid-profile-editor-antag-preference-no-button", 1)
+ };
+
+ foreach (var antag in _prototypeManager.EnumeratePrototypes().OrderBy(a => Loc.GetString(a.Name)))
+ {
+ if (!antag.SetPreference)
+ continue;
+
+ var antagContainer = new BoxContainer()
+ {
+ Orientation = LayoutOrientation.Horizontal,
+ };
+
+ var selector = new RequirementsSelector()
+ {
+ Margin = new Thickness(3f, 3f, 3f, 0f),
+ };
+
+ var title = Loc.GetString(antag.Name);
+ var description = Loc.GetString(antag.Objective);
+ selector.Setup(items, title, 250, description);
+ selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
+
+ if (!_characterRequirementsSystem.CheckRequirementsValid(
+ antag.Requirements ?? new(),
+ _controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
+ Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
+ _requirements.GetRawPlayTimeTrackers(),
+ _requirements.IsWhitelisted(),
+ antag,
+ _entManager,
+ _prototypeManager,
+ _cfgManager,
+ out var reasons))
+ {
+ var reason = _characterRequirementsSystem.GetRequirementsText(reasons);
+ selector.LockRequirements(reason);
+ Profile = Profile?.WithAntagPreference(antag.ID, false);
+ SetDirty();
+ }
+ else
+ selector.UnlockRequirements();
+
+ selector.OnSelected += preference =>
+ {
+ Profile = Profile?.WithAntagPreference(antag.ID, preference == 0);
+ SetDirty();
+ };
+
+ antagContainer.AddChild(selector);
+ AntagList.AddChild(antagContainer);
+ }
+ }
+
+ private void SetDirty()
+ {
+ // If it equals default then reset the button.
+ if (Profile == null || _preferencesManager.Preferences?.SelectedCharacter.MemberwiseEquals(Profile) == true)
+ {
+ IsDirty = false;
+ return;
+ }
+
+ //TODO: Check if profile matches default
+ IsDirty = true;
+ }
+
+ /// Reloads the entire dummy entity for preview
+ /// This is expensive so not recommended to run if you have a slider
+ private void ReloadPreview()
+ {
+ _entManager.DeleteEntity(PreviewDummy);
+ PreviewDummy = EntityUid.Invalid;
+
+ if (Profile == null || !_prototypeManager.HasIndex(Profile.Species))
+ return;
+
+ PreviewDummy = _controller.LoadProfileEntity(Profile, ShowClothes.Pressed);
+ SpriteView.SetEntity(PreviewDummy);
+ }
+
+ /// Resets the profile to the defaults
+ public void ResetToDefault()
+ {
+ SetProfile(
+ (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
+ _preferencesManager.Preferences?.SelectedCharacterIndex);
+ }
+
+ /// Sets the editor to the specified profile with the specified slot
+ public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
+ {
+ Profile = profile?.Clone();
+ CharacterSlot = slot;
+ IsDirty = false;
+ JobOverride = null;
+
+ UpdateNameEdit();
+ UpdateSexControls();
+ UpdateGenderControls();
+ UpdateSkinColor();
+ UpdateSpawnPriorityControls();
+ UpdateFlavorTextEdit();
+ UpdateCustomSpecieNameEdit();
+ UpdateAgeEdit();
+ UpdateEyePickers();
+ UpdateSaveButton();
+ UpdateMarkings();
+ UpdateHairPickers();
+ UpdateCMarkingsHair();
+ UpdateCMarkingsFacialHair();
+ UpdateHeightWidthSliders();
+ UpdateWeight();
+ UpdateCharacterRequired();
+
+ RefreshAntags();
+ RefreshJobs();
+ RefreshSpecies();
+ RefreshFlavorText();
+ ReloadPreview();
+
+ if (Profile != null)
+ PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
+ }
+
+ /// A slim reload that only updates the entity itself and not any of the job entities, etc
+ private void ReloadProfilePreview()
+ {
+ if (Profile == null || !_entManager.EntityExists(PreviewDummy))
+ return;
+
+ _entManager.System().LoadProfile(PreviewDummy, Profile);
+ SetPreviewRotation(_previewRotation);
+ TraitsTabs.UpdateTabMerging();
+ LoadoutsTabs.UpdateTabMerging();
+ }
+
+ private void LoadoutsChanged(bool enabled)
+ {
+ CTabContainer.SetTabVisible(4, enabled);
+ ShowLoadouts.Visible = enabled;
+ }
+
+ private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
+ {
+ var guidebookController = UserInterfaceManager.GetUIController();
+ var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
+ var page = DefaultSpeciesGuidebook;
+ if (_prototypeManager.HasIndex(species))
+ page = species;
+
+ if (_prototypeManager.TryIndex(DefaultSpeciesGuidebook, out var guideRoot))
+ {
+ var dict = new Dictionary { { DefaultSpeciesGuidebook, guideRoot } };
+ //TODO: Don't close the guidebook if its already open, just go to the correct page
+ guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
+ }
+ }
+
+ /// Refreshes all job selectors
+ public void RefreshJobs()
+ {
+ JobList.DisposeAllChildren();
+ _jobCategories.Clear();
+ _jobPriorities.Clear();
+ var firstCategory = true;
+
+ var departments = _prototypeManager.EnumeratePrototypes().ToArray();
+ Array.Sort(departments, DepartmentUIComparer.Instance);
+
+ var items = new[]
+ {
+ ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
+ ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
+ ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
+ ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
+ };
+
+ foreach (var department in departments)
+ {
+ var departmentName = Loc.GetString($"department-{department.ID}");
+
+ if (!_jobCategories.TryGetValue(department.ID, out var category))
+ {
+ category = new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical,
+ Name = department.ID,
+ ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
+ ("departmentName", departmentName))
+ };
+
+ if (firstCategory)
+ firstCategory = false;
+ else
+ category.AddChild(new Control { MinSize = new Vector2(0, 23) });
+
+ category.AddChild(new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#464966") },
+ Children =
+ {
+ new Label
+ {
+ Text = Loc.GetString("humanoid-profile-editor-department-jobs-label",
+ ("departmentName", departmentName)),
+ Margin = new Thickness(5f, 0, 0, 0),
+ },
+ },
+ });
+
+ _jobCategories[department.ID] = category;
+ JobList.AddChild(category);
+ }
+
+ var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId))
+ .Where(job => job.SetPreference)
+ .ToArray();
+
+ Array.Sort(jobs, JobUIComparer.Instance);
+
+ foreach (var job in jobs)
+ {
+ var jobContainer = new BoxContainer { Orientation = LayoutOrientation.Horizontal, };
+ var selector = new RequirementsSelector { Margin = new Thickness(3f, 3f, 3f, 0f) };
+
+ var icon = new TextureRect
+ {
+ TextureScale = new Vector2(2, 2),
+ VerticalAlignment = VAlignment.Center
+ };
+ var jobIcon = _prototypeManager.Index(job.Icon);
+ icon.Texture = jobIcon.Icon.Frame0();
+ selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
+
+ if (!_requirements.CheckJobWhitelist(job, out var reason))
+ selector.LockRequirements(reason);
+ else if (!_characterRequirementsSystem.CheckRequirementsValid(
+ job.Requirements ?? new(),
+ job,
+ Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
+ _requirements.GetRawPlayTimeTrackers(),
+ _requirements.IsWhitelisted(),
+ job,
+ _entManager,
+ _prototypeManager,
+ _cfgManager,
+ out var reasons))
+ selector.LockRequirements(_characterRequirementsSystem.GetRequirementsText(reasons));
+ else
+ selector.UnlockRequirements();
+
+ selector.OnSelected += selectedPrio =>
+ {
+ var selectedJobPrio = (JobPriority) selectedPrio;
+ Profile = Profile?.WithJobPriority(job.ID, selectedJobPrio);
+
+ foreach (var (jobId, other) in _jobPriorities)
+ {
+ // Sync other selectors with the same job in case of multiple department jobs
+ if (jobId == job.ID)
+ other.Select(selectedPrio);
+ else if (selectedJobPrio == JobPriority.High &&
+ (JobPriority) other.Selected == JobPriority.High)
+ {
+ // Lower any other high priorities to medium.
+ other.Select((int) JobPriority.Medium);
+ Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium);
+ }
+ }
+
+ // TODO: Only reload on high change (either to or from).
+ ReloadPreview();
+ UpdateJobPriorities();
+ SetDirty();
+ };
+
+ _jobPriorities.Add((job.ID, selector));
+ jobContainer.AddChild(selector);
+ category.AddChild(jobContainer);
+ }
+ }
+
+ UpdateJobPriorities();
+ }
+
+ private void ToggleClothes(BaseButton.ButtonEventArgs _)
+ {
+ //TODO: Optimization
+ // _controller.ShowClothes = ShowClothes.Pressed;
+ // _controller.UpdateCharacterUI();
+ }
+
+ private void ToggleLoadouts(BaseButton.ButtonEventArgs _)
+ {
+ //TODO: Optimization
+ // _controller.ShowLoadouts = ShowLoadouts.Pressed;
+ // _controller.UpdateCharacterUI();
+ }
+
+ private void UpdateRoleRequirements()
+ {
+ JobList.DisposeAllChildren();
+ _jobCategories.Clear();
+ _jobPriorities.Clear();
+ var firstCategory = true;
+
+ var departments = _prototypeManager.EnumeratePrototypes().ToArray();
+ Array.Sort(departments, DepartmentUIComparer.Instance);
+
+ var items = new[]
+ {
+ ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
+ ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
+ ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
+ ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
+ };
+
+ foreach (var department in departments)
+ {
+ var departmentName = Loc.GetString($"department-{department.ID}");
+
+ if (!_jobCategories.TryGetValue(department.ID, out var category))
+ {
+ category = new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical,
+ Name = department.ID,
+ ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
+ ("departmentName", departmentName))
+ };
+
+ if (firstCategory)
+ firstCategory = false;
+ else
+ {
+ category.AddChild(new Control
+ {
+ MinSize = new Vector2(0, 23),
+ });
+ }
+
+ category.AddChild(new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")},
+ Children =
+ {
+ new Label
+ {
+ Text = Loc.GetString("humanoid-profile-editor-department-jobs-label",
+ ("departmentName", departmentName)),
+ Margin = new Thickness(5f, 0, 0, 0)
+ }
+ }
+ });
+
+ _jobCategories[department.ID] = category;
+ JobList.AddChild(category);
+ }
+
+ var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId))
+ .Where(job => job.SetPreference)
+ .ToArray();
+ Array.Sort(jobs, JobUIComparer.Instance);
+
+ foreach (var job in jobs)
+ {
+ var jobContainer = new BoxContainer()
+ {
+ Orientation = LayoutOrientation.Horizontal,
+ };
+
+ var selector = new RequirementsSelector { Margin = new Thickness(3f, 3f, 3f, 0f), };
+
+ var icon = new TextureRect
+ {
+ TextureScale = new Vector2(2, 2),
+ VerticalAlignment = VAlignment.Center
+ };
+ var jobIcon = _prototypeManager.Index(job.Icon);
+ icon.Texture = jobIcon.Icon.Frame0();
+ selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
+
+ if (!_requirements.CheckJobWhitelist(job, out var reason))
+ selector.LockRequirements(reason);
+ else if (!_characterRequirementsSystem.CheckRequirementsValid(
+ job.Requirements ?? new(),
+ job,
+ Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
+ _requirements.GetRawPlayTimeTrackers(),
+ _requirements.IsWhitelisted(),
+ job,
+ _entManager,
+ _prototypeManager,
+ _cfgManager,
+ out var reasons))
+ selector.LockRequirements(_characterRequirementsSystem.GetRequirementsText(reasons));
+ else
+ selector.UnlockRequirements();
+
+ category.AddChild(selector);
+ _jobPriorities.Add((job.ID, selector));
+ EnsureJobRequirementsValid(); // DeltaV
+
+ selector.OnSelected += priority =>
+ {
+ foreach (var (jobId, other) in _jobPriorities)
+ {
+ // Sync other selectors with the same job in case of multiple department jobs
+ if (jobId == job.ID)
+ other.Select(other.Selected);
+ else if ((JobPriority) other.Selected == JobPriority.High && (JobPriority) other.Selected == JobPriority.High)
+ {
+ // Lower any other high priorities to medium.
+ other.Select((int) JobPriority.Medium);
+ Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium);
+ }
+ }
+
+ Profile = Profile?.WithJobPriority(job.ID, (JobPriority) priority);
+ ReloadPreview();
+ SetDirty();
+ UpdateCharacterRequired();
+ };
+
+ _jobPriorities.Add((job.ID, selector));
+ category.AddChild(jobContainer);
+ }
+ }
+
+ if (Profile is not null)
+ UpdateJobPriorities();
+ }
+
+ /// DeltaV - Make sure that no invalid job priorities get through
+ private void EnsureJobRequirementsValid()
+ {
+ foreach (var (jobId, selector) in _jobPriorities)
+ {
+ var proto = _prototypeManager.Index(jobId);
+ if ((JobPriority) selector.Selected == JobPriority.Never
+ || _requirements.CheckJobWhitelist(proto, out _)
+ || _characterRequirementsSystem.CheckRequirementsValid(
+ proto.Requirements ?? new(),
+ proto,
+ Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
+ _requirements.GetRawPlayTimeTrackers(),
+ _requirements.IsWhitelisted(),
+ proto,
+ _entManager,
+ _prototypeManager,
+ _cfgManager,
+ out _))
+ continue;
+
+ selector.Select((int) JobPriority.Never);
+ Profile = Profile?.WithJobPriority(proto.ID, JobPriority.Never);
+ }
+ }
+
+ private void OnFlavorTextChange(string content)
+ {
+ if (Profile is null)
+ return;
+
+ Profile = Profile.WithFlavorText(content);
+ IsDirty = true;
+ }
+
+ private void OnMarkingChange(MarkingSet markings)
+ {
+ if (Profile is null)
+ return;
+
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
+ SetDirty();
+ ReloadProfilePreview();
+ }
+
+ private void OnSkinColorOnValueChanged()
+ {
+ if (Profile is null)
+ return;
+
+ var skin = _prototypeManager.Index(Profile.Species).SkinColoration;
+ var skinColor = _prototypeManager.Index(Profile.Species).DefaultSkinTone;
+
+ switch (skin)
+ {
+ case HumanoidSkinColor.HumanToned:
+ {
+ if (!Skin.Visible)
+ {
+ Skin.Visible = true;
+ RgbSkinColorContainer.Visible = false;
+ }
+
+ var color = SkinColor.HumanSkinTone((int) Skin.Value);
+
+ Markings.CurrentSkinColor = color;
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
+ break;
+ }
+ case HumanoidSkinColor.Hues:
+ {
+ if (!RgbSkinColorContainer.Visible)
+ {
+ Skin.Visible = false;
+ RgbSkinColorContainer.Visible = true;
+ }
+
+ Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
+ break;
+ }
+ case HumanoidSkinColor.TintedHues:
+ case HumanoidSkinColor.TintedHuesSkin: // DeltaV - Tone blending
+ {
+ if (!RgbSkinColorContainer.Visible)
+ {
+ Skin.Visible = false;
+ RgbSkinColorContainer.Visible = true;
+ }
+
+ var color = skin switch // DeltaV - Tone blending
+ {
+ HumanoidSkinColor.TintedHues => SkinColor.TintedHues(_rgbSkinColorSelector.Color),
+ HumanoidSkinColor.TintedHuesSkin => SkinColor.TintedHuesSkin(_rgbSkinColorSelector.Color, skinColor),
+ _ => Color.White
+ };
+
+ Markings.CurrentSkinColor = color;
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
+ break;
+ }
+ case HumanoidSkinColor.VoxFeathers:
+ {
+ if (!RgbSkinColorContainer.Visible)
+ {
+ Skin.Visible = false;
+ RgbSkinColorContainer.Visible = true;
+ }
+
+ var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
+
+ Markings.CurrentSkinColor = color;
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
+ break;
+ }
+ }
+
+ SetDirty();
+ ReloadProfilePreview();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _entManager.DeleteEntity(PreviewDummy);
+ PreviewDummy = EntityUid.Invalid;
+
+ _cfgManager.UnsubValueChanged(CCVars.GameLoadoutsEnabled, LoadoutsChanged);
+ }
+
+ private void SetAge(int newAge)
+ {
+ Profile = Profile?.WithAge(newAge);
+ ReloadPreview();
+ IsDirty = true;
+ }
+
+ private void SetSex(Sex newSex)
+ {
+ Profile = Profile?.WithSex(newSex);
+ // for convenience, default to most common gender when new sex is selected
+ switch (newSex)
+ {
+ case Sex.Male:
+ Profile = Profile?.WithGender(Gender.Male);
+ break;
+ case Sex.Female:
+ Profile = Profile?.WithGender(Gender.Female);
+ break;
+ default:
+ Profile = Profile?.WithGender(Gender.Epicene);
+ break;
+ }
+ UpdateGenderControls();
+ Markings.SetSex(newSex);
+ ReloadProfilePreview();
+ SetDirty();
+ }
+
+ private void SetGender(Gender newGender)
+ {
+ Profile = Profile?.WithGender(newGender);
+ ReloadPreview();
+ IsDirty = true;
+ }
+
+ private void SetSpecies(string newSpecies)
+ {
+ Profile = Profile?.WithSpecies(newSpecies);
+ OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
+ Markings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
+ UpdateSexControls(); // Update sex for new species
+ UpdateCharacterRequired();
+ // Changing species provides inaccurate sliders without these
+ UpdateHeightWidthSliders();
+ UpdateWeight();
+ UpdateSpeciesGuidebookIcon();
+ IsDirty = true;
+ ReloadProfilePreview();
+ }
+
+ private void SetName(string newName)
+ {
+ Profile = Profile?.WithName(newName);
+ IsDirty = true;
+ }
+
+ private void SetCustomSpecieName(string customname)
+ {
+ Profile = Profile?.WithCustomSpeciesName(customname);
+ IsDirty = true;
+ }
+
+ private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
+ {
+ Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
+ IsDirty = true;
+ }
+
+ private void SetProfileHeight(float height)
+ {
+ Profile = Profile?.WithHeight(height);
+ IsDirty = true;
+ ReloadProfilePreview();
+ }
+
+ private void SetProfileWidth(float width)
+ {
+ Profile = Profile?.WithWidth(width);
+ IsDirty = true;
+ ReloadProfilePreview();
+ }
+
+ private bool IsDirty
+ {
+ get => _isDirty;
+ set
+ {
+ if (_isDirty == value)
+ return;
+
+ _isDirty = value;
+ UpdateSaveButton();
+ }
+ }
+
+ private void UpdateNameEdit()
+ {
+ NameEdit.Text = Profile?.Name ?? "";
+ }
+
+ private void UpdateCustomSpecieNameEdit()
+ {
+ var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
+ _customspecienameEdit.Text = string.IsNullOrEmpty(Profile?.Customspeciename) ? Loc.GetString(species.Name) : Profile.Customspeciename;
+ _ccustomspecienamecontainerEdit.Visible = species.CustomName;
+ }
+
+ private void UpdateFlavorTextEdit()
+ {
+ if (_flavorTextEdit != null)
+ _flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? "");
+ }
+
+ private void UpdateAgeEdit()
+ {
+ AgeEdit.Text = Profile?.Age.ToString() ?? "";
+ }
+
+ /// Updates selected job priorities to the profile's
+ private void UpdateJobPriorities()
+ {
+ foreach (var (jobId, prioritySelector) in _jobPriorities)
+ {
+ var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never;
+ prioritySelector.Select((int) priority);
+ }
+ }
+
+ private void UpdateSexControls()
+ {
+ if (Profile == null)
+ return;
+
+ SexButton.Clear();
+
+ var sexes = new List();
+
+ // Add species sex options, default to just none if we are in bizzaro world and have no species
+ if (_prototypeManager.TryIndex(Profile.Species, out var speciesProto))
+ {
+ foreach (var sex in speciesProto.Sexes)
+ sexes.Add(sex);
+ }
+ else
+ sexes.Add(Sex.Unsexed);
+
+ // Add button for each sex
+ foreach (var sex in sexes)
+ SexButton.AddItem(Loc.GetString($"humanoid-profile-editor-sex-{sex.ToString().ToLower()}-text"), (int) sex);
+
+ if (sexes.Contains(Profile.Sex))
+ SexButton.SelectId((int) Profile.Sex);
+ else
+ SexButton.SelectId((int) sexes[0]);
+ }
+
+ private void UpdateSkinColor()
+ {
+ if (Profile == null)
+ return;
+
+ var skin = _prototypeManager.Index(Profile.Species).SkinColoration;
+
+ switch (skin)
+ {
+ case HumanoidSkinColor.HumanToned:
+ {
+ if (!Skin.Visible)
+ {
+ Skin.Visible = true;
+ RgbSkinColorContainer.Visible = false;
+ }
+
+ Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
+ break;
+ }
+ case HumanoidSkinColor.Hues:
+ {
+ if (!RgbSkinColorContainer.Visible)
+ {
+ Skin.Visible = false;
+ RgbSkinColorContainer.Visible = true;
+ }
+
+ // Set the RGB values to the direct values otherwise
+ _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
+ break;
+ }
+ case HumanoidSkinColor.TintedHues:
+ {
+ if (!RgbSkinColorContainer.Visible)
+ {
+ Skin.Visible = false;
+ RgbSkinColorContainer.Visible = true;
+ }
+
+ // Set the RGB values to the direct values otherwise
+ _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
+ break;
+ }
+ case HumanoidSkinColor.VoxFeathers:
+ {
+ if (!RgbSkinColorContainer.Visible)
+ {
+ Skin.Visible = false;
+ RgbSkinColorContainer.Visible = true;
+ }
+
+ _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
+
+ break;
+ }
+ }
+ }
+
+ public void UpdateSpeciesGuidebookIcon()
+ {
+ SpeciesInfoButton.StyleClasses.Clear();
+
+ var species = Profile?.Species;
+ if (species is null
+ || !_prototypeManager.TryIndex(species, out var speciesProto)
+ || !_prototypeManager.HasIndex(species))
+ return;
+
+ const string style = "SpeciesInfoDefault";
+ SpeciesInfoButton.StyleClasses.Add(style);
+ }
+
+ private void UpdateMarkings()
+ {
+ if (Profile == null)
+ return;
+
+ Markings.SetData(Profile.Appearance.Markings, Profile.Species, Profile.Sex, Profile.Appearance.SkinColor,
+ Profile.Appearance.EyeColor);
+ }
+
+ private void UpdateGenderControls()
+ {
+ if (Profile == null)
+ return;
+
+ PronounsButton.SelectId((int) Profile.Gender);
+ }
+
+ private void UpdateSpawnPriorityControls()
+ {
+ if (Profile == null)
+ return;
+
+ SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
+ }
+
+ private void UpdateHeightWidthSliders()
+ {
+ var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
+
+ HeightSlider.MinValue = species.MinHeight;
+ HeightSlider.MaxValue = species.MaxHeight;
+ HeightSlider.Value = Profile?.Height ?? species.DefaultHeight;
+
+ WidthSlider.MinValue = species.MinWidth;
+ WidthSlider.MaxValue = species.MaxWidth;
+ WidthSlider.Value = Profile?.Width ?? species.DefaultWidth;
+
+ var height = MathF.Round(species.AverageHeight * HeightSlider.Value);
+ HeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
+
+ var width = MathF.Round(species.AverageWidth * WidthSlider.Value);
+ WidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", (int) width));
+ }
+
+ private enum SliderUpdate
+ {
+ Height,
+ Width,
+ Both
+ }
+
+ private void UpdateDimensions(SliderUpdate updateType)
+ {
+ var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
+
+ if (Profile == null) return;
+
+ var heightValue = Math.Clamp(HeightSlider.Value, species.MinHeight, species.MaxHeight);
+ var widthValue = Math.Clamp(WidthSlider.Value, species.MinWidth, species.MaxWidth);
+ var sizeRatio = species.SizeRatio;
+ var ratio = heightValue / widthValue;
+
+ if (updateType == SliderUpdate.Height || updateType == SliderUpdate.Both)
+ if (ratio < 1 / sizeRatio || ratio > sizeRatio)
+ widthValue = heightValue / (ratio < 1 / sizeRatio ? (1 / sizeRatio) : sizeRatio);
+
+ if (updateType == SliderUpdate.Width || updateType == SliderUpdate.Both)
+ if (ratio < 1 / sizeRatio || ratio > sizeRatio)
+ heightValue = widthValue * (ratio < 1 / sizeRatio ? (1 / sizeRatio) : sizeRatio);
+
+
+ heightValue = Math.Clamp(heightValue, species.MinHeight, species.MaxHeight);
+ widthValue = Math.Clamp(widthValue, species.MinWidth, species.MaxWidth);
+
+ HeightSlider.Value = heightValue;
+ WidthSlider.Value = widthValue;
+
+ SetProfileHeight(heightValue);
+ SetProfileWidth(widthValue);
+
+ var height = MathF.Round(species.AverageHeight * HeightSlider.Value);
+ HeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
+
+ var width = MathF.Round(species.AverageWidth * WidthSlider.Value);
+ WidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", (int) width));
+
+ UpdateWeight();
+ }
+
+ private void UpdateWeight()
+ {
+ if (Profile == null)
+ return;
+
+ var species = _species.Find(x => x.ID == Profile.Species) ?? _species.First();
+ _prototypeManager.Index(species.Prototype).TryGetComponent(out var fixture);
+
+ if (fixture != null)
+ {
+ var radius = fixture.Fixtures["fix1"].Shape.Radius;
+ var density = fixture.Fixtures["fix1"].Density;
+ var avg = (Profile.Width + Profile.Height) / 2;
+ var weight = MathF.Round(MathF.PI * MathF.Pow(radius * avg, 2) * density);
+ WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) weight));
+ }
+ else // Whelp, the fixture doesn't exist, guesstimate it instead
+ WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) 71));
+
+ SpriteView.InvalidateMeasure();
+ }
+
+ private void UpdateHairPickers()
+ {
+ if (Profile == null)
+ return;
+
+ var hairMarking = Profile.Appearance.HairStyleId switch
+ {
+ HairStyles.DefaultHairStyle => new List(),
+ _ => new() { new(Profile.Appearance.HairStyleId, new List() { Profile.Appearance.HairColor }) },
+ };
+
+ var facialHairMarking = Profile.Appearance.FacialHairStyleId switch
+ {
+ HairStyles.DefaultFacialHairStyle => new List(),
+ _ => new() { new(Profile.Appearance.FacialHairStyleId, new List() { Profile.Appearance.FacialHairColor }) },
+ };
+
+ HairStylePicker.UpdateData(
+ hairMarking,
+ Profile.Species,
+ 1);
+ FacialHairPicker.UpdateData(
+ facialHairMarking,
+ Profile.Species,
+ 1);
+ }
+
+ private void UpdateCMarkingsHair()
+ {
+ if (Profile == null)
+ return;
+
+ // hair color
+ Color? hairColor = null;
+ if ( Profile.Appearance.HairStyleId != HairStyles.DefaultHairStyle &&
+ _markingManager.Markings.TryGetValue(Profile.Appearance.HairStyleId, out var hairProto))
+ {
+ if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, hairProto, _prototypeManager))
+ {
+ hairColor = _markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out _, _prototypeManager)
+ ? Profile.Appearance.SkinColor
+ : Profile.Appearance.HairColor;
+ }
+ }
+
+ if (hairColor != null)
+ Markings.HairMarking = new(Profile.Appearance.HairStyleId, new List { hairColor.Value });
+ else
+ Markings.HairMarking = null;
+ }
+
+ private void UpdateCMarkingsFacialHair()
+ {
+ if (Profile == null)
+ return;
+
+ // Facial hair color
+ Color? facialHairColor = null;
+ if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle &&
+ _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto))
+ {
+ if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, facialHairProto, _prototypeManager))
+ {
+ facialHairColor = _markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out _, _prototypeManager)
+ ? Profile.Appearance.SkinColor
+ : Profile.Appearance.FacialHairColor;
+ }
+ }
+
+ if (facialHairColor != null)
+ Markings.FacialHairMarking = new(Profile.Appearance.FacialHairStyleId, new List { facialHairColor.Value });
+ else
+ Markings.FacialHairMarking = null;
+ }
+
+ private void UpdateEyePickers()
+ {
+ if (Profile == null)
+ return;
+
+ Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
+ EyeColorPicker.SetData(Profile.Appearance.EyeColor);
+ }
+
+ private void UpdateSaveButton()
+ {
+ SaveButton.Disabled = Profile is null || !IsDirty;
+ ResetButton.Disabled = Profile is null || !IsDirty;
+ }
+
+ private void RandomizeProfile()
+ {
+ Profile = HumanoidCharacterProfile.Random();
+ SetProfile(Profile, CharacterSlot);
+ SetDirty();
+ }
+
+ private void SetPreviewRotation(Direction direction)
+ {
+ SpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
+ }
+
+ private void RandomizeName()
+ {
+ if (Profile == null)
+ return;
+ var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
+ SetName(name);
+ UpdateNameEdit();
+ }
+
+ private async void ImportProfile()
+ {
+ if (_exporting || CharacterSlot == null || Profile == null)
+ return;
+
+ StartExport();
+ await using var file = await _dialogManager.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
+
+ if (file == null)
+ {
+ EndExport();
+ return;
+ }
+
+ try
+ {
+ var profile = _entManager.System().FromStream(file, _playerManager.LocalSession!);
+ var oldProfile = Profile;
+ SetProfile(profile, CharacterSlot);
+
+ IsDirty = !profile.MemberwiseEquals(oldProfile);
+ }
+ catch (Exception exc)
+ {
+ Logger.Error($"Error when importing profile\n{exc.StackTrace}");
+ }
+ finally
+ {
+ EndExport();
+ }
+ }
+
+ private async void ExportProfile()
+ {
+ if (Profile == null || _exporting)
+ return;
+
+ StartExport();
+ var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
+
+ if (file == null)
+ {
+ EndExport();
+ return;
+ }
+
+ try
+ {
+ var dataNode = _entManager.System().ToDataNode(Profile);
+ await using var writer = new StreamWriter(file.Value.fileStream);
+ dataNode.Write(writer);
+ }
+ catch (Exception exc)
+ {
+ Logger.Error($"Error when exporting profile\n{exc.StackTrace}");
+ }
+ finally
+ {
+ EndExport();
+ await file.Value.fileStream.DisposeAsync();
+ }
+ }
+
+ private void StartExport()
+ {
+ _exporting = true;
+ ImportButton.Disabled = true;
+ ExportButton.Disabled = true;
+ }
+
+ private void EndExport()
+ {
+ _exporting = false;
+ ImportButton.Disabled = false;
+ ExportButton.Disabled = false;
+ }
+
+ #region Traits
+
+ #region Updates
+
+ private void UpdateTraitPreferences()
+ {
+ var points = _cfgManager.GetCVar(CCVars.GameTraitsDefaultPoints);
+ _traitCount = 0;
+
+ foreach (var preferenceSelector in _traitPreferences)
+ {
+ var traitId = preferenceSelector.Trait.ID;
+ var preference = Profile?.TraitPreferences.Contains(traitId) ?? false;
+
+ preferenceSelector.Preference = preference;
+
+ if (!preference)
+ continue;
+
+ points += preferenceSelector.Trait.Points;
+ _traitCount += 1;
+ }
+
+ TraitPointsBar.Value = points;
+ TraitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-header",
+ ("points", points), ("traits", _traitCount),
+ ("maxTraits", _cfgManager.GetCVar(CCVars.GameTraitsMax)));
+
+ // Set the remove unusable button's label to have the correct amount of unusable traits
+ TraitsRemoveUnusableButton.Text = Loc.GetString("humanoid-profile-editor-traits-remove-unusable-button",
+ ("count", _traits
+ .Where(t => _traitPreferences
+ .Where(tps => tps.Preference).Select(tps => tps.Trait).Contains(t.Key))
+ .Count(t => !t.Value)));
+ AdminUIHelpers.RemoveConfirm(TraitsRemoveUnusableButton, _confirmationData);
+
+ IsDirty = true;
+ ReloadProfilePreview();
+ }
+
+ // Yeah this is mostly just copied from UpdateLoadouts
+ // This whole file is bad though and a lot of loadout code came from traits originally
+ //TODO Make this file not hell
+ private Dictionary _traits = new();
+ public void UpdateTraits(bool? showUnusable = null, bool reload = false)
+ {
+ showUnusable ??= TraitsShowUnusableButton.Pressed;
+
+ // Reset trait points so you don't get -14 points or something for no reason
+ var points = _cfgManager.GetCVar(CCVars.GameTraitsDefaultPoints);
+ TraitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-points-label", ("points", points), ("max", points));
+ TraitPointsBar.MaxValue = points;
+ TraitPointsBar.Value = points;
+
+ // Reset the whole UI and delete caches
+ if (reload)
+ {
+ foreach (var tab in TraitsTabs.Tabs)
+ TraitsTabs.RemoveTab(tab);
+ _loadoutPreferences.Clear();
+ }
+
+
+ // Get the highest priority job to use for trait filtering
+ var highJob = _controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies());
+
+ _traits.Clear();
+ foreach (var trait in _prototypeManager.EnumeratePrototypes())
+ {
+ var usable = _characterRequirementsSystem.CheckRequirementsValid(
+ trait.Requirements,
+ highJob,
+ Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
+ _requirements.GetRawPlayTimeTrackers(),
+ _requirements.IsWhitelisted(),
+ trait,
+ _entManager,
+ _prototypeManager,
+ _cfgManager,
+ out _
+ );
+ _traits.Add(trait, usable);
+
+ if (_traitPreferences.FindIndex(lps => lps.Trait.ID == trait.ID) is not (not -1 and var i))
+ continue;
+
+ var selector = _traitPreferences[i];
+ selector.Valid = usable;
+ selector.ShowUnusable = showUnusable.Value;
+ }
+
+ if (_traits.Count == 0)
+ {
+ TraitsTabs.AddTab(new Label { Text = Loc.GetString("humanoid-profile-editor-traits-no-traits") },
+ Loc.GetString("trait-category-Uncategorized"));
+ return;
+ }
+
+
+ var uncategorized = TraitsTabs.Contents.FirstOrDefault(c => c.Name == "Uncategorized");
+ if (uncategorized == null)
+ {
+ uncategorized = new BoxContainer
+ {
+ Name = "Uncategorized",
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ // I hate ScrollContainers
+ Children =
+ {
+ new ScrollContainer
+ {
+ HScrollEnabled = false,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ Children =
+ {
+ new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ },
+ },
+ },
+ },
+ };
+
+ TraitsTabs.AddTab(uncategorized, Loc.GetString("trait-category-Uncategorized"));
+ }
+
+ // Create a Dictionary/tree of categories and subcategories
+ var cats = CreateTree(_prototypeManager.EnumeratePrototypes()
+ .Where(c => c.Root)
+ .OrderBy(c => Loc.GetString($"trait-category-{c.ID}"))
+ .ToList());
+ var categories = new Dictionary();
+ foreach (var (key, value) in cats)
+ categories.Add(key, value);
+
+ // Create the UI elements for the category tree
+ CreateCategoryUI(categories, TraitsTabs);
+
+ // Fill categories with traits
+ foreach (var (trait, usable) in _traits
+ .OrderBy(l => -l.Key.Points)
+ .ThenBy(l => l.Key.ID)
+ .ThenBy(l => Loc.GetString($"trait-name-{l.Key.ID}")))
+ {
+ if (_traitPreferences.Select(lps => lps.Trait.ID).Contains(trait.ID))
+ {
+ var first = _traitPreferences.First(lps => lps.Trait.ID == trait.ID);
+ first.Valid = usable;
+ first.ShowUnusable = showUnusable.Value;
+ continue;
+ }
+
+ var selector = new TraitPreferenceSelector(
+ trait, highJob, Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
+ _entManager, _prototypeManager, _cfgManager, _characterRequirementsSystem, _requirements);
+ selector.Valid = usable;
+ selector.ShowUnusable = showUnusable.Value;
+ AddSelector(selector);
+
+ // Look for an existing category tab
+ var match = FindCategory(trait.Category, TraitsTabs);
+
+ // If there is no category put it in Uncategorized (this shouldn't happen)
+ (match ?? uncategorized).Children.First().Children.First().AddChild(selector);
+ }
+
+ // Hide any empty tabs
+ HideEmptyTabs(_prototypeManager.EnumeratePrototypes().ToList());
+
+ UpdateTraitPreferences();
+ return;
+
+
+ void CreateCategoryUI(Dictionary tree, NeoTabContainer parent)
+ {
+ foreach (var (key, value) in tree)
+ {
+ // If the category's container exists already, ignore it
+ if (parent.Contents.Any(c => c.Name == key))
+ continue;
+
+ // If the value is a list of TraitPrototypes, create a final tab for them
+ if (value is List)
+ {
+ var category = new BoxContainer
+ {
+ Name = key,
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ Children =
+ {
+ new ScrollContainer
+ {
+ HScrollEnabled = false,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ Children =
+ {
+ new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ },
+ },
+ },
+ },
+ };
+
+ parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
+ }
+ // If the value is a dictionary, create a new tab for it and recursively call this function to fill it
+ else
+ {
+ var category = new NeoTabContainer
+ {
+ Name = key,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ SeparatorMargin = new Thickness(0),
+ };
+
+ parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
+ CreateCategoryUI((Dictionary) value, category);
+ }
+ }
+ }
+
+ void AddSelector(TraitPreferenceSelector selector)
+ {
+ _traitPreferences.Add(selector);
+ selector.PreferenceChanged += preference =>
+ {
+ // Make sure they have enough trait points
+ preference = CheckPoints(preference ? selector.Trait.Points : -selector.Trait.Points, preference);
+ // Make sure they have enough trait slots
+ preference = preference ? _traitCount < _cfgManager.GetCVar(CCVars.GameTraitsMax) : preference;
+
+ // Update Preferences
+ Profile = Profile?.WithTraitPreference(selector.Trait.ID, preference);
+ IsDirty = true;
+ UpdateTraitPreferences();
+ UpdateCharacterRequired();
+ };
+ }
+
+ bool CheckPoints(int points, bool preference)
+ {
+ var temp = TraitPointsBar.Value + points;
+ return preference ? !(temp < 0) : temp < 0;
+ }
+ }
+
+ #endregion
+
+ #region Functions
+
+ private Dictionary CreateTree(List cats)
+ {
+ var tree = new Dictionary();
+ foreach (var category in cats)
+ {
+ // If the category is already in the tree, ignore it
+ if (tree.ContainsKey(category.ID))
+ continue;
+
+ // Categories don't have a Parent field, so we need to instead check the SubCategories of every Category
+ var subCategories = category.SubCategories.Where(subCategory => !tree.ContainsKey(subCategory)).ToList();
+ // If there are no subcategories, add a loadout spot to the dictionary
+ if (subCategories.Count == 0)
+ {
+ tree.Add(category.ID, new List());
+ continue;
+ }
+
+ // If there are subcategories, we need to add them to the dictionary as well
+ var subCategoryTree = CreateTree(subCategories.Select(c => _prototypeManager.Index(c)).ToList());
+ tree.Add(category.ID, subCategoryTree);
+ }
+
+ return tree;
+ }
+
+ private void HideEmptyTabs(List cats)
+ {
+ foreach (var tab in cats.Select(category => FindCategory(category.ID, TraitsTabs)))
+ {
+ // If it's empty, hide it
+ if (tab != null)
+ ((NeoTabContainer) tab.Parent!.Parent!.Parent!.Parent!).SetTabVisible(tab, tab.Children.First().Children.First().Children.Any());
+
+ // If it has a parent tab container, hide it if it's empty
+ if (tab?.Parent?.Parent is NeoTabContainer parent)
+ {
+ var parentCats = parent.Contents.Select(c => _prototypeManager.Index(c.Name!)).ToList();
+ HideEmptyTabs(parentCats);
+ }
+ }
+ }
+
+ private void TryRemoveUnusableTraits()
+ {
+ // Confirm the user wants to remove unusable loadouts
+ if (!AdminUIHelpers.TryConfirm(TraitsRemoveUnusableButton, _confirmationData))
+ return;
+
+ // Remove unusable traits
+ foreach (var (trait, _) in _traits.Where(l => !l.Value).ToList())
+ Profile = Profile?.WithTraitPreference(trait.ID, false);
+ UpdateCharacterRequired();
+ }
+
+ #endregion
+
+ #endregion
+
+ #region Loadouts
+
+ #region Updates
+
+ private void UpdateLoadoutPreferences()
+ {
+ var points = _cfgManager.GetCVar(CCVars.GameLoadoutsPoints);
+ LoadoutPointsBar.Value = points;
+ LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
+
+ foreach (var preferenceSelector in _loadoutPreferences)
+ {
+ var loadoutId = preferenceSelector.Loadout.ID;
+ var preference = Profile?.LoadoutPreferences.Contains(loadoutId) ?? false;
+
+ preferenceSelector.Preference = preference;
+
+ if (preference)
+ {
+ points -= preferenceSelector.Loadout.Cost;
+ LoadoutPointsBar.Value = points;
+ LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", LoadoutPointsBar.MaxValue));
+ }
+ }
+
+ // Set the remove unusable button's label to have the correct amount of unusable loadouts
+ LoadoutsRemoveUnusableButton.Text = Loc.GetString("humanoid-profile-editor-loadouts-remove-unusable-button",
+ ("count", _loadouts
+ .Where(l => _loadoutPreferences
+ .Where(lps => lps.Preference).Select(lps => lps.Loadout).Contains(l.Key))
+ .Count(l => !l.Value
+ || !_loadoutPreferences.Find(lps => lps.Loadout == l.Key)!.Wearable)));
+ AdminUIHelpers.RemoveConfirm(LoadoutsRemoveUnusableButton, _confirmationData);
+
+ IsDirty = true;
+ //TODO: Optimization
+ // _controller.UpdateClothes = true;
+ ReloadProfilePreview();
+ }
+
+ private Dictionary _loadouts = new();
+ private Dictionary _dummyLoadouts = new();
+ public void UpdateLoadouts(bool? showUnusable = null, bool reload = false)
+ {
+ showUnusable ??= LoadoutsShowUnusableButton.Pressed;
+
+ // Reset loadout points so you don't get -14 points or something for no reason
+ var points = _cfgManager.GetCVar(CCVars.GameLoadoutsPoints);
+ LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
+ LoadoutPointsBar.MaxValue = points;
+ LoadoutPointsBar.Value = points;
+
+ // Reset the whole UI and delete caches
+ if (reload)
+ {
+ foreach (var tab in LoadoutsTabs.Tabs)
+ LoadoutsTabs.RemoveTab(tab);
+ foreach (var uid in _dummyLoadouts)
+ _entManager.QueueDeleteEntity(uid.Value);
+ _loadoutPreferences.Clear();
+ }
+
+
+ // Get the highest priority job to use for loadout filtering
+ var highJob = _controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies());
+
+ _loadouts.Clear();
+ foreach (var loadout in _prototypeManager.EnumeratePrototypes())
+ {
+ var usable = _characterRequirementsSystem.CheckRequirementsValid(
+ loadout.Requirements,
+ highJob ?? new JobPrototype(),
+ Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
+ _requirements.GetRawPlayTimeTrackers(),
+ _requirements.IsWhitelisted(),
+ loadout,
+ _entManager,
+ _prototypeManager,
+ _cfgManager,
+ out _
+ );
+ _loadouts.Add(loadout, usable);
+
+ if (_loadoutPreferences.FindIndex(lps => lps.Loadout.ID == loadout.ID) is not (not -1 and var i))
+ continue;
+
+ var selector = _loadoutPreferences[i];
+ UpdateSelector(selector, usable);
+ }
+
+ if (_loadouts.Count == 0)
+ {
+ LoadoutsTabs.AddTab(new Label { Text = Loc.GetString("humanoid-profile-editor-loadouts-no-loadouts") },
+ Loc.GetString("loadout-category-Uncategorized"));
+ return;
+ }
+
+
+ var uncategorized = LoadoutsTabs.Contents.FirstOrDefault(c => c.Name == "Uncategorized");
+ if (uncategorized == null)
+ {
+ uncategorized = new BoxContainer
+ {
+ Name = "Uncategorized",
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ // I hate ScrollContainers
+ Children =
+ {
+ new ScrollContainer
+ {
+ HScrollEnabled = false,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ Children =
+ {
+ new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ },
+ },
+ },
+ },
+ };
+
+ LoadoutsTabs.AddTab(uncategorized, Loc.GetString("loadout-category-Uncategorized"));
+ }
+
+ // Create a Dictionary/tree of categories and subcategories
+ var cats = CreateTree(_prototypeManager.EnumeratePrototypes()
+ .Where(c => c.Root)
+ .OrderBy(c => Loc.GetString($"loadout-category-{c.ID}"))
+ .ToList());
+ var categories = new Dictionary();
+ foreach (var (key, value) in cats)
+ categories.Add(key, value);
+
+ // Create the UI elements for the category tree
+ CreateCategoryUI(categories, LoadoutsTabs);
+
+ // Fill categories with loadouts
+ foreach (var (loadout, usable) in _loadouts
+ .OrderBy(l => l.Key.ID)
+ .ThenBy(l => Loc.GetString($"loadout-name-{l.Key.ID}"))
+ .ThenBy(l => l.Key.Cost))
+ {
+ if (_loadoutPreferences.Select(lps => lps.Loadout.ID).Contains(loadout.ID))
+ {
+ var first = _loadoutPreferences.First(lps => lps.Loadout.ID == loadout.ID);
+ UpdateSelector(first, usable);
+ continue;
+ }
+
+ var selector = new LoadoutPreferenceSelector(
+ loadout, highJob ?? new JobPrototype(),
+ Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(), ref _dummyLoadouts,
+ _entManager, _prototypeManager, _cfgManager, _characterRequirementsSystem, _requirements);
+ UpdateSelector(selector, usable);
+ AddSelector(selector);
+
+ // Look for an existing category tab
+ var match = FindCategory(loadout.Category, LoadoutsTabs);
+
+ // If there is no category put it in Uncategorized (this shouldn't happen)
+ (match ?? uncategorized).Children.First().Children.First().AddChild(selector);
+ }
+
+ // Hide any empty tabs
+ HideEmptyTabs(_prototypeManager.EnumeratePrototypes().ToList());
+
+ UpdateLoadoutPreferences();
+ return;
+
+
+ void UpdateSelector(LoadoutPreferenceSelector selector, bool usable)
+ {
+ selector.Valid = usable;
+ selector.ShowUnusable = showUnusable.Value;
+
+ foreach (var item in selector.Loadout.Items)
+ {
+ if (_dummyLoadouts.TryGetValue(selector.Loadout.ID + selector.Loadout.Items.IndexOf(item), out var entity)
+ && _entManager.GetComponent(entity).EntityPrototype!.ID == item)
+ {
+ if (!_entManager.HasComponent(entity))
+ {
+ selector.Wearable = true;
+ continue;
+ }
+ selector.Wearable = _characterRequirementsSystem.CanEntityWearItem(PreviewDummy, entity);
+ continue;
+ }
+
+ entity = _entManager.SpawnEntity(item, MapCoordinates.Nullspace);
+ _dummyLoadouts[selector.Loadout.ID + selector.Loadout.Items.IndexOf(item)] = entity;
+
+ if (!_entManager.HasComponent(entity))
+ {
+ selector.Wearable = true;
+ continue;
+ }
+ selector.Wearable = _characterRequirementsSystem.CanEntityWearItem(PreviewDummy, entity);
+ }
+ }
+
+ void CreateCategoryUI(Dictionary tree, NeoTabContainer parent)
+ {
+ foreach (var (key, value) in tree)
+ {
+ // If the category's container exists already, ignore it
+ if (parent.Contents.Any(c => c.Name == key))
+ continue;
+
+ // If the value is a list of LoadoutPrototypes, create a final tab for them
+ if (value is List)
+ {
+ var category = new BoxContainer
+ {
+ Name = key,
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ Children =
+ {
+ new ScrollContainer
+ {
+ HScrollEnabled = false,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ Children =
+ {
+ new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ },
+ },
+ },
+ },
+ };
+
+ parent.AddTab(category, Loc.GetString($"loadout-category-{key}"));
+ }
+ // If the value is a dictionary, create a new tab for it and recursively call this function to fill it
+ else
+ {
+ var category = new NeoTabContainer
+ {
+ Name = key,
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ SeparatorMargin = new Thickness(0),
+ };
+
+ parent.AddTab(category, Loc.GetString($"loadout-category-{key}"));
+ CreateCategoryUI((Dictionary) value, category);
+ }
+ }
+ }
+
+ void AddSelector(LoadoutPreferenceSelector selector)
+ {
+ _loadoutPreferences.Add(selector);
+ selector.PreferenceChanged += preference =>
+ {
+ // Make sure they have enough loadout points
+ preference = preference ? CheckPoints(-selector.Loadout.Cost, preference) : CheckPoints(selector.Loadout.Cost, preference);
+
+ // Update Preferences
+ Profile = Profile?.WithLoadoutPreference(selector.Loadout.ID, preference);
+ IsDirty = true;
+ UpdateLoadoutPreferences();
+ UpdateCharacterRequired();
+ };
+ }
+
+ bool CheckPoints(int points, bool preference)
+ {
+ var temp = LoadoutPointsBar.Value + points;
+ return preference ? !(temp < 0) : temp < 0;
+ }
+ }
+
+ #endregion
+
+ #region Functions
+
+ private Dictionary CreateTree(List cats)
+ {
+ var tree = new Dictionary();
+ foreach (var category in cats)
+ {
+ // If the category is already in the tree, ignore it
+ if (tree.ContainsKey(category.ID))
+ continue;
+
+ // Categories don't have a Parent field, so we need to instead check the SubCategories of every Category
+ var subCategories = category.SubCategories.Where(subCategory => !tree.ContainsKey(subCategory)).ToList();
+ // If there are no subcategories, add a loadout spot to the dictionary
+ if (subCategories.Count == 0)
+ {
+ tree.Add(category.ID, new List());
+ continue;
+ }
+
+ // If there are subcategories, we need to add them to the dictionary as well
+ var subCategoryTree = CreateTree(subCategories.Select(c => _prototypeManager.Index(c)).ToList());
+ tree.Add(category.ID, subCategoryTree);
+ }
+
+ return tree;
+ }
+
+ private BoxContainer? FindCategory(string id, NeoTabContainer parent)
+ {
+ BoxContainer? match = null;
+ foreach (var child in parent.Contents)
+ {
+ if (string.IsNullOrEmpty(child.Name))
+ continue;
+
+ if (child.Name == id)
+ match = (BoxContainer?) child;
+ }
+
+ if (match != null)
+ return match;
+
+ foreach (var subcategory in parent.Contents.Where(c => c is NeoTabContainer).Cast())
+ match = FindCategory(id, subcategory);
+
+ return match;
+ }
+
+ private void HideEmptyTabs(List cats)
+ {
+ foreach (var tab in cats.Select(category => FindCategory(category.ID, LoadoutsTabs)))
+ {
+ // If it's empty, hide it
+ if (tab != null)
+ ((NeoTabContainer) tab.Parent!.Parent!.Parent!.Parent!).SetTabVisible(tab, tab.Children.First().Children.First().Children.Any());
+
+ // If it has a parent tab container, hide it if it's empty
+ if (tab?.Parent?.Parent is NeoTabContainer parent)
+ {
+ var parentCats = parent.Contents.Select(c => _prototypeManager.Index(c.Name!)).ToList();
+ HideEmptyTabs(parentCats);
+ }
+ }
+ }
+
+ private void TryRemoveUnusableLoadouts()
+ {
+ // Confirm the user wants to remove unusable loadouts
+ if (!AdminUIHelpers.TryConfirm(LoadoutsRemoveUnusableButton, _confirmationData))
+ return;
+
+ // Remove unusable and unwearable loadouts
+ foreach (var (loadout, _) in
+ _loadouts.Where(l =>
+ !l.Value || !_loadoutPreferences.Find(lps => lps.Loadout.ID == l.Key.ID)!.Wearable).ToList())
+ Profile = Profile?.WithLoadoutPreference(loadout.ID, false);
+ UpdateCharacterRequired();
+ }
+
+ #endregion
+
+ #endregion
+
+ private void UpdateCharacterRequired()
+ {
+ UpdateRoleRequirements();
+ UpdateTraits(TraitsShowUnusableButton.Pressed);
+ UpdateLoadouts(LoadoutsShowUnusableButton.Pressed);
+ }
+ }
+}
diff --git a/Content.Client/Lobby/UI/LoadoutPreferenceSelector.xaml b/Content.Client/Lobby/UI/LoadoutPreferenceSelector.xaml
new file mode 100644
index 0000000000..a6fbf162bc
--- /dev/null
+++ b/Content.Client/Lobby/UI/LoadoutPreferenceSelector.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/LoadoutPreferenceSelector.xaml.cs b/Content.Client/Lobby/UI/LoadoutPreferenceSelector.xaml.cs
new file mode 100644
index 0000000000..36dd5179c0
--- /dev/null
+++ b/Content.Client/Lobby/UI/LoadoutPreferenceSelector.xaml.cs
@@ -0,0 +1,169 @@
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using Content.Client.Players.PlayTimeTracking;
+using Content.Client.Stylesheets;
+using Content.Shared.Clothing.Loadouts.Prototypes;
+using Content.Shared.Customization.Systems;
+using Content.Shared.Preferences;
+using Content.Shared.Roles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+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.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Lobby.UI;
+
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutPreferenceSelector : Control
+{
+ public LoadoutPrototype Loadout { get; }
+
+ public bool Valid;
+ private bool _showUnusable;
+ public bool ShowUnusable
+ {
+ get => _showUnusable;
+ set
+ {
+ _showUnusable = value;
+ Visible = Valid && _wearable || _showUnusable;
+ PreferenceButton.RemoveStyleClass(StyleBase.ButtonDanger);
+ PreferenceButton.AddStyleClass(Valid ? "" : StyleBase.ButtonDanger);
+ }
+ }
+
+ private bool _wearable;
+ public bool Wearable
+ {
+ get => _wearable;
+ set
+ {
+ _wearable = value;
+ Visible = Valid && _wearable || _showUnusable;
+ PreferenceButton.RemoveStyleClass(StyleBase.ButtonCaution);
+ PreferenceButton.AddStyleClass(_wearable ? "" : StyleBase.ButtonCaution);
+ }
+ }
+
+ public bool Preference
+ {
+ get => PreferenceButton.Pressed;
+ set => PreferenceButton.Pressed = value;
+ }
+
+ public event Action? PreferenceChanged;
+
+ public LoadoutPreferenceSelector(LoadoutPrototype loadout, JobPrototype highJob,
+ HumanoidCharacterProfile profile, ref Dictionary entities,
+ IEntityManager entityManager, IPrototypeManager prototypeManager, IConfigurationManager configManager,
+ CharacterRequirementsSystem characterRequirementsSystem, JobRequirementsManager jobRequirementsManager)
+ {
+ RobustXamlLoader.Load(this);
+
+ Loadout = loadout;
+
+ SpriteView previewLoadout;
+ if (!entities.TryGetValue(loadout.ID + 0, out var dummyLoadoutItem))
+ {
+ // Get the first item in the loadout to be the preview
+ dummyLoadoutItem = entityManager.SpawnEntity(loadout.Items.First(), MapCoordinates.Nullspace);
+
+ // Create a sprite preview of the loadout item
+ previewLoadout = new SpriteView
+ {
+ Scale = new Vector2(1, 1),
+ OverrideDirection = Direction.South,
+ VerticalAlignment = VAlignment.Center,
+ SizeFlagsStretchRatio = 1,
+ };
+ previewLoadout.SetEntity(dummyLoadoutItem);
+ }
+ else
+ {
+ // Create a sprite preview of the loadout item
+ previewLoadout = new SpriteView
+ {
+ Scale = new Vector2(1, 1),
+ OverrideDirection = Direction.South,
+ VerticalAlignment = VAlignment.Center,
+ SizeFlagsStretchRatio = 1,
+ };
+ previewLoadout.SetEntity(dummyLoadoutItem);
+ }
+
+
+ // Create a checkbox to get the loadout
+ PreferenceButton.AddChild(new BoxContainer
+ {
+ Children =
+ {
+ new Label
+ {
+ Text = loadout.Cost.ToString(),
+ StyleClasses = { StyleBase.StyleClassLabelHeading },
+ MinWidth = 32,
+ MaxWidth = 32,
+ ClipText = true,
+ Margin = new Thickness(0, 0, 8, 0),
+ },
+ new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#2f2f2f") },
+ Children =
+ {
+ previewLoadout,
+ },
+ },
+ new Label
+ {
+ Text = Loc.GetString($"loadout-name-{loadout.ID}") == $"loadout-name-{loadout.ID}"
+ ? entityManager.GetComponent(dummyLoadoutItem).EntityName
+ : Loc.GetString($"loadout-name-{loadout.ID}"),
+ Margin = new Thickness(8, 0, 0, 0),
+ },
+ },
+ });
+ PreferenceButton.OnToggled += OnPreferenceButtonToggled;
+
+ var tooltip = new StringBuilder();
+ // Add the loadout description to the tooltip if there is one
+ var desc = !Loc.TryGetString($"loadout-description-{loadout.ID}", out var description)
+ ? entityManager.GetComponent(dummyLoadoutItem).EntityDescription
+ : description;
+ if (!string.IsNullOrEmpty(desc))
+ tooltip.Append($"{Loc.GetString(desc)}");
+
+
+ // Get requirement reasons
+ characterRequirementsSystem.CheckRequirementsValid(
+ loadout.Requirements, highJob, profile, new Dictionary(),
+ jobRequirementsManager.IsWhitelisted(), loadout,
+ entityManager, prototypeManager, configManager,
+ out var reasons);
+
+ // Add requirement reasons to the tooltip
+ foreach (var reason in reasons)
+ tooltip.Append($"\n{reason.ToMarkup()}");
+
+ // Combine the tooltip and format it in the checkbox supplier
+ if (tooltip.Length > 0)
+ {
+ var formattedTooltip = new Tooltip();
+ formattedTooltip.SetMessage(FormattedMessage.FromMarkupPermissive(tooltip.ToString()));
+ PreferenceButton.TooltipSupplier = _ => formattedTooltip;
+ }
+ }
+
+ private void OnPreferenceButtonToggled(BaseButton.ButtonToggledEventArgs args)
+ {
+ PreferenceChanged?.Invoke(Preference);
+ }
+}
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPanel.xaml.cs b/Content.Client/Lobby/UI/LobbyCharacterPanel.xaml.cs
index 160f1d7201..97bbe66726 100644
--- a/Content.Client/Lobby/UI/LobbyCharacterPanel.xaml.cs
+++ b/Content.Client/Lobby/UI/LobbyCharacterPanel.xaml.cs
@@ -15,7 +15,7 @@ public sealed partial class LobbyCharacterPanel : Control
public LobbyCharacterPanel()
{
RobustXamlLoader.Load(this);
- UserInterfaceManager.GetUIController().SetPreviewPanel(this);
+ IoCManager.InjectDependencies(this);
}
public void SetLoaded(bool value)
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml
new file mode 100644
index 0000000000..7901238cde
--- /dev/null
+++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
new file mode 100644
index 0000000000..14709f8b1f
--- /dev/null
+++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
@@ -0,0 +1,63 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Lobby.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LobbyCharacterPreviewPanel : Control
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+
+ public Button CharacterSetupButton => CharacterSetup;
+
+ private EntityUid? _previewDummy;
+
+ public LobbyCharacterPreviewPanel()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ public void SetLoaded(bool value)
+ {
+ Loaded.Visible = value;
+ Unloaded.Visible = !value;
+ }
+
+ public void SetSummaryText(string value)
+ {
+ Summary.Text = value;
+ }
+
+ public void SetSprite(EntityUid uid)
+ {
+ if (_previewDummy != null)
+ {
+ _entManager.DeleteEntity(_previewDummy);
+ }
+
+ _previewDummy = uid;
+
+ ViewBox.DisposeAllChildren();
+ var spriteView = new SpriteView
+ {
+ OverrideDirection = Direction.South,
+ Scale = new Vector2(4f, 4f),
+ MaxSize = new Vector2(112, 112),
+ Stretch = SpriteView.StretchMode.Fill,
+ };
+ spriteView.SetEntity(uid);
+ ViewBox.AddChild(spriteView);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _entManager.DeleteEntity(_previewDummy);
+ _previewDummy = null;
+ }
+}
diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml b/Content.Client/Lobby/UI/LobbyGui.xaml
index ea9c63d6cd..4a158fd811 100644
--- a/Content.Client/Lobby/UI/LobbyGui.xaml
+++ b/Content.Client/Lobby/UI/LobbyGui.xaml
@@ -14,7 +14,7 @@
Stretch="KeepAspectCovered" />
-
+
@@ -94,7 +94,7 @@
-
+
diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml.cs b/Content.Client/Lobby/UI/LobbyGui.xaml.cs
index 5a0b580262..3ab2caad78 100644
--- a/Content.Client/Lobby/UI/LobbyGui.xaml.cs
+++ b/Content.Client/Lobby/UI/LobbyGui.xaml.cs
@@ -1,3 +1,4 @@
+
using Content.Client.Message;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Robust.Client.AutoGenerated;
@@ -8,10 +9,9 @@
namespace Content.Client.Lobby.UI
{
[GenerateTypedNameReferences]
- internal sealed partial class LobbyGui : UIScreen
+ public sealed partial class LobbyGui : UIScreen
{
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
- [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
public LobbyGui()
{
@@ -23,7 +23,7 @@ public LobbyGui()
LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
- OptionsButton.OnPressed += _ => _userInterfaceManager.GetUIController().ToggleWindow();
+ OptionsButton.OnPressed += _ => UserInterfaceManager.GetUIController().ToggleWindow();
}
public void SwitchState(LobbyGuiState state)
@@ -40,13 +40,13 @@ public void SwitchState(LobbyGuiState state)
case LobbyGuiState.CharacterSetup:
CharacterSetupState.Visible = true;
- var actualWidth = (float) _userInterfaceManager.RootControl.PixelWidth;
+ var actualWidth = (float) UserInterfaceManager.RootControl.PixelWidth;
var setupWidth = (float) LeftSide.PixelWidth;
if (1 - (setupWidth / actualWidth) > 0.30)
- {
RightSide.Visible = false;
- }
+
+ UserInterfaceManager.GetUIController().ReloadCharacterSetup();
break;
}
@@ -54,14 +54,10 @@ public void SwitchState(LobbyGuiState state)
public enum LobbyGuiState : byte
{
- ///
- /// The default state, i.e., what's seen on launch.
- ///
+ /// The default state, i.e., what's seen on launch.
Default,
- ///
- /// The character setup state.
- ///
- CharacterSetup
+ /// The character setup state.
+ CharacterSetup,
}
}
}
diff --git a/Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs b/Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs
index 2d37cb97df..f37f59db3d 100644
--- a/Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs
+++ b/Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs
@@ -2,23 +2,20 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
-namespace Content.Client.Lobby.UI
+namespace Content.Client.Lobby.UI;
+
+[GenerateTypedNameReferences]
+[UsedImplicitly]
+public sealed partial class ObserveWarningWindow : DefaultWindow
{
- [GenerateTypedNameReferences]
- [UsedImplicitly]
- internal sealed partial class ObserveWarningWindow : DefaultWindow
+ public ObserveWarningWindow()
{
- public ObserveWarningWindow()
- {
- Title = Loc.GetString("observe-warning-window-title");
- RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
+ Title = Loc.GetString("observe-warning-window-title");
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
- ObserveButton.OnPressed += _ => { this.Close(); };
- NevermindButton.OnPressed += _ => { this.Close(); };
- }
+ ObserveButton.OnPressed += _ => { Close(); };
+ NevermindButton.OnPressed += _ => { Close(); };
}
}
diff --git a/Content.Client/Lobby/UI/RequirementsSelector.xaml b/Content.Client/Lobby/UI/RequirementsSelector.xaml
new file mode 100644
index 0000000000..8a867fe8b2
--- /dev/null
+++ b/Content.Client/Lobby/UI/RequirementsSelector.xaml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/Content.Client/Lobby/UI/RequirementsSelector.xaml.cs b/Content.Client/Lobby/UI/RequirementsSelector.xaml.cs
new file mode 100644
index 0000000000..d2f0fe1665
--- /dev/null
+++ b/Content.Client/Lobby/UI/RequirementsSelector.xaml.cs
@@ -0,0 +1,115 @@
+using System.Numerics;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Lobby.UI;
+
+/// A generic locking selector
+[GenerateTypedNameReferences]
+public sealed partial class RequirementsSelector : BoxContainer
+{
+ private readonly RadioOptions _options;
+ private readonly StripeBack _lockStripe;
+
+ public event Action? OnSelected;
+
+ public int Selected => _options.SelectedId;
+
+
+ public RequirementsSelector()
+ {
+ RobustXamlLoader.Load(this);
+ _options = new RadioOptions(RadioOptionsLayout.Horizontal)
+ {
+ FirstButtonStyle = StyleBase.ButtonOpenRight,
+ ButtonStyle = StyleBase.ButtonOpenBoth,
+ LastButtonStyle = StyleBase.ButtonOpenLeft,
+ HorizontalExpand = true,
+ };
+
+ //Override default radio option button width
+ _options.GenerateItem = GenerateButton;
+
+ _options.OnItemSelected += args =>
+ {
+ _options.Select(args.Id);
+ OnSelected?.Invoke(args.Id);
+ };
+
+
+ var requirementsLabel = new Label
+ {
+ Text = Loc.GetString("role-timer-locked"),
+ Visible = true,
+ HorizontalAlignment = HAlignment.Center,
+ StyleClasses = {StyleBase.StyleClassLabelSubText},
+ };
+
+ _lockStripe = new StripeBack
+ {
+ Visible = false,
+ HorizontalExpand = true,
+ HasMargins = false,
+ MouseFilter = MouseFilterMode.Stop,
+ Children =
+ {
+ requirementsLabel,
+ },
+ };
+ }
+
+ /// Actually adds the controls.
+ public void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
+ {
+ foreach (var (text, value) in items)
+ _options.AddItem(Loc.GetString(text), value);
+
+ TitleLabel.Text = title;
+ TitleLabel.MinSize = new Vector2(titleSize, 0f);
+ TitleLabel.ToolTip = description;
+
+ if (icon != null)
+ {
+ AddChild(icon);
+ icon.SetPositionFirst();
+ }
+
+ OptionsContainer.AddChild(_options);
+ OptionsContainer.AddChild(_lockStripe);
+ }
+
+ public void LockRequirements(FormattedMessage requirements)
+ {
+ var tooltip = new Tooltip();
+ tooltip.SetMessage(requirements);
+ _lockStripe.TooltipSupplier = _ => tooltip;
+ _lockStripe.Visible = true;
+ _options.Visible = false;
+ }
+
+ public void UnlockRequirements()
+ {
+ _lockStripe.Visible = false;
+ _options.Visible = true;
+ }
+
+ private Button GenerateButton(string text, int value)
+ {
+ return new Button
+ {
+ Text = text,
+ MinWidth = 90,
+ HorizontalExpand = true,
+ };
+ }
+
+ public void Select(int id)
+ {
+ _options.Select(id);
+ }
+}
diff --git a/Content.Client/Lobby/UI/TraitPreferenceSelector.xaml b/Content.Client/Lobby/UI/TraitPreferenceSelector.xaml
new file mode 100644
index 0000000000..a6fbf162bc
--- /dev/null
+++ b/Content.Client/Lobby/UI/TraitPreferenceSelector.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/TraitPreferenceSelector.xaml.cs b/Content.Client/Lobby/UI/TraitPreferenceSelector.xaml.cs
new file mode 100644
index 0000000000..e5c7a5a0fb
--- /dev/null
+++ b/Content.Client/Lobby/UI/TraitPreferenceSelector.xaml.cs
@@ -0,0 +1,110 @@
+using System.Text;
+using Content.Client.Players.PlayTimeTracking;
+using Content.Client.Stylesheets;
+using Content.Shared.Customization.Systems;
+using Content.Shared.Preferences;
+using Content.Shared.Roles;
+using Content.Shared.Traits;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+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 Robust.Shared.Utility;
+
+namespace Content.Client.Lobby.UI;
+
+
+[GenerateTypedNameReferences]
+public sealed partial class TraitPreferenceSelector : Control
+{
+ public TraitPrototype Trait { get; }
+
+ public bool Valid;
+ private bool _showUnusable;
+ public bool ShowUnusable
+ {
+ get => _showUnusable;
+ set
+ {
+ _showUnusable = value;
+ Visible = Valid || _showUnusable;
+ PreferenceButton.RemoveStyleClass(StyleBase.ButtonDanger);
+ PreferenceButton.AddStyleClass(Valid ? "" : StyleBase.ButtonDanger);
+ }
+ }
+
+ public bool Preference
+ {
+ get => PreferenceButton.Pressed;
+ set => PreferenceButton.Pressed = value;
+ }
+
+ public event Action? PreferenceChanged;
+
+ public TraitPreferenceSelector(TraitPrototype trait, JobPrototype highJob, HumanoidCharacterProfile profile,
+ IEntityManager entityManager, IPrototypeManager prototypeManager, IConfigurationManager configManager,
+ CharacterRequirementsSystem characterRequirementsSystem, JobRequirementsManager jobRequirementsManager)
+ {
+ RobustXamlLoader.Load(this);
+
+ Trait = trait;
+
+ // Create a checkbox to get the loadout
+ PreferenceButton.AddChild(new BoxContainer
+ {
+ Children =
+ {
+ new Label
+ {
+ Text = trait.Points.ToString(),
+ StyleClasses = { StyleBase.StyleClassLabelHeading },
+ MinWidth = 32,
+ MaxWidth = 32,
+ ClipText = true,
+ Margin = new Thickness(0, 0, 8, 0),
+ },
+ new Label
+ {
+ Text = Loc.GetString($"trait-name-{trait.ID}"),
+ Margin = new Thickness(8, 0, 0, 0),
+ },
+ },
+ });
+ PreferenceButton.OnToggled += OnPrefButtonToggled;
+
+ var tooltip = new StringBuilder();
+ // Add the loadout description to the tooltip if there is one
+ var desc = Loc.GetString($"trait-description-{trait.ID}");
+ if (!string.IsNullOrEmpty(desc) && desc != $"trait-description-{trait.ID}")
+ tooltip.Append(desc);
+
+
+ // Get requirement reasons
+ characterRequirementsSystem.CheckRequirementsValid(
+ trait.Requirements, highJob, profile, new Dictionary(),
+ jobRequirementsManager.IsWhitelisted(), trait,
+ entityManager, prototypeManager, configManager,
+ out var reasons);
+
+ // Add requirement reasons to the tooltip
+ foreach (var reason in reasons)
+ tooltip.Append($"\n{reason.ToMarkup()}");
+
+ // Combine the tooltip and format it in the checkbox supplier
+ if (tooltip.Length > 0)
+ {
+ var formattedTooltip = new Tooltip();
+ formattedTooltip.SetMessage(FormattedMessage.FromMarkupPermissive(tooltip.ToString()));
+ PreferenceButton.TooltipSupplier = _ => formattedTooltip;
+ }
+ }
+
+ private void OnPrefButtonToggled(BaseButton.ButtonToggledEventArgs args)
+ {
+ PreferenceChanged?.Invoke(Preference);
+ }
+}
diff --git a/Content.Client/Magic/MagicSystem.cs b/Content.Client/Magic/MagicSystem.cs
new file mode 100644
index 0000000000..03aa9eb56d
--- /dev/null
+++ b/Content.Client/Magic/MagicSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Magic;
+
+namespace Content.Client.Magic;
+
+public sealed class MagicSystem : SharedMagicSystem;
diff --git a/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs b/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs
index bfbf2efe4f..f6979bf8d7 100644
--- a/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs
+++ b/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs
@@ -72,9 +72,6 @@ protected override void Dispose(bool disposing)
if (!disposing)
return;
- if (_window != null)
- _window.OnClose -= Close;
-
_window?.Dispose();
}
}
diff --git a/Content.Client/MagicMirror/MagicMirrorSystem.cs b/Content.Client/MagicMirror/MagicMirrorSystem.cs
new file mode 100644
index 0000000000..9b0b1dea0b
--- /dev/null
+++ b/Content.Client/MagicMirror/MagicMirrorSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.MagicMirror;
+
+namespace Content.Client.MagicMirror;
+
+public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
+{
+
+}
diff --git a/Content.Client/Maps/GridDraggingSystem.cs b/Content.Client/Maps/GridDraggingSystem.cs
index 16357c8983..3c4912a187 100644
--- a/Content.Client/Maps/GridDraggingSystem.cs
+++ b/Content.Client/Maps/GridDraggingSystem.cs
@@ -101,7 +101,7 @@ public override void Update(float frameTime)
if (!_mapManager.TryFindGridAt(mousePos, out var gridUid, out var grid))
return;
- StartDragging(gridUid, Transform(gridUid).InvWorldMatrix.Transform(mousePos.Position));
+ StartDragging(gridUid, Vector2.Transform(mousePos.Position, Transform(gridUid).InvWorldMatrix));
}
if (!TryComp(_dragging, out var xform))
@@ -116,7 +116,7 @@ public override void Update(float frameTime)
return;
}
- var localToWorld = xform.WorldMatrix.Transform(_localPosition);
+ var localToWorld = Vector2.Transform(_localPosition, xform.WorldMatrix);
if (localToWorld.EqualsApprox(mousePos.Position, 0.01f)) return;
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
index 7f98e3e0c3..5e068f1e9c 100644
--- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
+++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
@@ -76,7 +76,7 @@ private void OnPreview(BaseButton.ButtonEventArgs eventArgs)
TextEditPanel.Visible = !_preview;
PreviewPanel.Visible = _preview;
- PreviewLabel.SetMarkup(Rope.Collapse(ContentField.TextRope));
+ PreviewLabel.SetMarkupPermissive(Rope.Collapse(ContentField.TextRope));
}
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
diff --git a/Content.Client/Mech/Ui/MechMenu.xaml.cs b/Content.Client/Mech/Ui/MechMenu.xaml.cs
index 8d1d936031..fad7648808 100644
--- a/Content.Client/Mech/Ui/MechMenu.xaml.cs
+++ b/Content.Client/Mech/Ui/MechMenu.xaml.cs
@@ -35,9 +35,17 @@ public void UpdateMechStats()
IntegrityDisplayBar.Value = integrityPercent.Float();
IntegrityDisplay.Text = Loc.GetString("mech-integrity-display", ("amount", (integrityPercent*100).Int()));
- var energyPercent = mechComp.Energy / mechComp.MaxEnergy;
- EnergyDisplayBar.Value = energyPercent.Float();
- EnergyDisplay.Text = Loc.GetString("mech-energy-display", ("amount", (energyPercent*100).Int()));
+ if (mechComp.MaxEnergy != 0f)
+ {
+ var energyPercent = mechComp.Energy / mechComp.MaxEnergy;
+ EnergyDisplayBar.Value = energyPercent.Float();
+ EnergyDisplay.Text = Loc.GetString("mech-energy-display", ("amount", (energyPercent*100).Int()));
+ }
+ else
+ {
+ EnergyDisplayBar.Value = 0f;
+ EnergyDisplay.Text = Loc.GetString("mech-energy-missing");
+ }
SlotDisplay.Text = Loc.GetString("mech-slot-display",
("amount", mechComp.MaxEquipmentAmount - mechComp.EquipmentContainer.ContainedEntities.Count));
diff --git a/Content.Client/Medical/Surgery/SurgeryBui.cs b/Content.Client/Medical/Surgery/SurgeryBui.cs
new file mode 100644
index 0000000000..a49d5ec06b
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryBui.cs
@@ -0,0 +1,358 @@
+using Content.Client.Xenonids.UI;
+using Content.Client.Administration.UI.CustomControls;
+using Content.Shared.Medical.Surgery;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Client.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Medical.Surgery;
+
+[UsedImplicitly]
+public sealed class SurgeryBui : BoundUserInterface
+{
+ [Dependency] private readonly IEntityManager _entities = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+
+ private readonly SurgerySystem _system;
+ [ViewVariables]
+ private SurgeryWindow? _window;
+ private EntityUid? _part;
+ private bool _isBody;
+ private (EntityUid Ent, EntProtoId Proto)? _surgery;
+ private readonly List _previousSurgeries = new();
+ public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) => _system = _entities.System();
+
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ if (_window is null
+ || message is not SurgeryBuiRefreshMessage)
+ return;
+
+ RefreshUI();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ if (state is not SurgeryBuiState s)
+ return;
+
+ Update(s);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ _window?.Dispose();
+ }
+
+ private void Update(SurgeryBuiState state)
+ {
+ if (!_entities.TryGetComponent(_player.LocalEntity, out SurgeryTargetComponent? surgeryTargetComp)
+ || !surgeryTargetComp.CanOperate)
+ return;
+
+ if (_window == null)
+ {
+ _window = new SurgeryWindow();
+ _window.OnClose += Close;
+ _window.Title = Loc.GetString("surgery-ui-window-title");
+
+ _window.PartsButton.OnPressed += _ =>
+ {
+ _part = null;
+ _isBody = false;
+ _surgery = null;
+ _previousSurgeries.Clear();
+ View(ViewType.Parts);
+ };
+
+ _window.SurgeriesButton.OnPressed += _ =>
+ {
+ _surgery = null;
+ _previousSurgeries.Clear();
+
+ if (!_entities.TryGetNetEntity(_part, out var netPart)
+ || State is not SurgeryBuiState s
+ || !s.Choices.TryGetValue(netPart.Value, out var surgeries))
+ return;
+
+ OnPartPressed(netPart.Value, surgeries);
+ };
+
+ _window.StepsButton.OnPressed += _ =>
+ {
+ if (!_entities.TryGetNetEntity(_part, out var netPart)
+ || _previousSurgeries.Count == 0)
+ return;
+
+ var last = _previousSurgeries[^1];
+ _previousSurgeries.RemoveAt(_previousSurgeries.Count - 1);
+
+ if (_system.GetSingleton(last) is not { } previousId
+ || !_entities.TryGetComponent(previousId, out SurgeryComponent? previous))
+ return;
+
+ OnSurgeryPressed((previousId, previous), netPart.Value, last);
+ };
+ }
+
+ _window.Surgeries.DisposeAllChildren();
+ _window.Steps.DisposeAllChildren();
+ _window.Parts.DisposeAllChildren();
+ View(ViewType.Parts);
+
+ var oldSurgery = _surgery;
+ var oldPart = _part;
+ _part = null;
+ _surgery = null;
+
+ var options = new List<(NetEntity netEntity, EntityUid entity, string Name, BodyPartType? PartType)>();
+ foreach (var choice in state.Choices.Keys)
+ if (_entities.TryGetEntity(choice, out var ent))
+ {
+ if (_entities.TryGetComponent(ent, out BodyPartComponent? part))
+ options.Add((choice, ent.Value, _entities.GetComponent(ent.Value).EntityName, part.PartType));
+ else if (_entities.TryGetComponent(ent, out BodyComponent? body))
+ options.Add((choice, ent.Value, _entities.GetComponent(ent.Value).EntityName, null));
+ }
+
+ options.Sort((a, b) =>
+ {
+ int GetScore(BodyPartType? partType)
+ {
+ return partType switch
+ {
+ BodyPartType.Head => 1,
+ BodyPartType.Torso => 2,
+ BodyPartType.Arm => 3,
+ BodyPartType.Hand => 4,
+ BodyPartType.Leg => 5,
+ BodyPartType.Foot => 6,
+ // BodyPartType.Tail => 7, No tails yet!
+ BodyPartType.Other => 8,
+ _ => 9
+ };
+ }
+
+ return GetScore(a.PartType) - GetScore(b.PartType);
+ });
+
+ foreach (var (netEntity, entity, partName, _) in options)
+ {
+ //var netPart = _entities.GetNetEntity(part.Owner);
+ var surgeries = state.Choices[netEntity];
+ var partButton = new XenoChoiceControl();
+
+ partButton.Set(partName, null);
+ partButton.Button.OnPressed += _ => OnPartPressed(netEntity, surgeries);
+
+ _window.Parts.AddChild(partButton);
+
+ foreach (var surgeryId in surgeries)
+ {
+ if (_system.GetSingleton(surgeryId) is not { } surgery ||
+ !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp))
+ continue;
+
+ if (oldPart == entity && oldSurgery?.Proto == surgeryId)
+ OnSurgeryPressed((surgery, surgeryComp), netEntity, surgeryId);
+ }
+
+ if (oldPart == entity && oldSurgery == null)
+ OnPartPressed(netEntity, surgeries);
+ }
+
+
+ if (!_window.IsOpen)
+ _window.OpenCentered();
+ }
+
+ private void AddStep(EntProtoId stepId, NetEntity netPart, EntProtoId surgeryId)
+ {
+ if (_window == null
+ || _system.GetSingleton(stepId) is not { } step)
+ return;
+
+ var stepName = new FormattedMessage();
+ stepName.AddText(_entities.GetComponent(step).EntityName);
+ var stepButton = new SurgeryStepButton { Step = step };
+ stepButton.Button.OnPressed += _ => SendMessage(new SurgeryStepChosenBuiMsg(netPart, surgeryId, stepId, _isBody));
+
+ _window.Steps.AddChild(stepButton);
+ }
+
+ private void OnSurgeryPressed(Entity surgery, NetEntity netPart, EntProtoId surgeryId)
+ {
+ if (_window == null)
+ return;
+
+ _part = _entities.GetEntity(netPart);
+ _isBody = _entities.HasComponent(_part);
+ _surgery = (surgery, surgeryId);
+
+ _window.Steps.DisposeAllChildren();
+
+ // This apparently does not consider if theres multiple surgery requirements in one surgery. Maybe thats fine.
+ if (surgery.Comp.Requirement is { } requirementId && _system.GetSingleton(requirementId) is { } requirement)
+ {
+ var label = new XenoChoiceControl();
+ label.Button.OnPressed += _ =>
+ {
+ _previousSurgeries.Add(surgeryId);
+
+ if (_entities.TryGetComponent(requirement, out SurgeryComponent? requirementComp))
+ OnSurgeryPressed((requirement, requirementComp), netPart, requirementId);
+ };
+
+ var msg = new FormattedMessage();
+ var surgeryName = _entities.GetComponent(requirement).EntityName;
+ msg.AddMarkup($"[bold]{Loc.GetString("surgery-ui-window-require")}: {surgeryName}[/bold]");
+ label.Set(msg, null);
+
+ _window.Steps.AddChild(label);
+ _window.Steps.AddChild(new HSeparator { Margin = new Thickness(0, 0, 0, 1) });
+ }
+ foreach (var stepId in surgery.Comp.Steps)
+ AddStep(stepId, netPart, surgeryId);
+
+ View(ViewType.Steps);
+ RefreshUI();
+ }
+
+ private void OnPartPressed(NetEntity netPart, List surgeryIds)
+ {
+ if (_window == null)
+ return;
+
+ _part = _entities.GetEntity(netPart);
+ _isBody = _entities.HasComponent(_part);
+ _window.Surgeries.DisposeAllChildren();
+
+ var surgeries = new List<(Entity Ent, EntProtoId Id, string Name)>();
+ foreach (var surgeryId in surgeryIds)
+ {
+ if (_system.GetSingleton(surgeryId) is not { } surgery ||
+ !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp))
+ {
+ continue;
+ }
+
+ var name = _entities.GetComponent(surgery).EntityName;
+ surgeries.Add(((surgery, surgeryComp), surgeryId, name));
+ }
+
+ surgeries.Sort((a, b) =>
+ {
+ var priority = a.Ent.Comp.Priority.CompareTo(b.Ent.Comp.Priority);
+ if (priority != 0)
+ return priority;
+
+ return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
+ });
+
+ foreach (var surgery in surgeries)
+ {
+ var surgeryButton = new XenoChoiceControl();
+ surgeryButton.Set(surgery.Name, null);
+
+ surgeryButton.Button.OnPressed += _ => OnSurgeryPressed(surgery.Ent, netPart, surgery.Id);
+ _window.Surgeries.AddChild(surgeryButton);
+ }
+
+ RefreshUI();
+ View(ViewType.Surgeries);
+ }
+
+ private void RefreshUI()
+ {
+ if (_window == null
+ || !_window.IsOpen
+ || _part == null
+ || !_entities.HasComponent(_surgery?.Ent)
+ || !_entities.TryGetComponent(_player.LocalEntity ?? EntityUid.Invalid, out SurgeryTargetComponent? surgeryComp)
+ || !surgeryComp.CanOperate)
+ return;
+
+ var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent);
+ var i = 0;
+ foreach (var child in _window.Steps.Children)
+ {
+ if (child is not SurgeryStepButton stepButton)
+ continue;
+
+ var status = StepStatus.Incomplete;
+ if (next == null)
+ status = StepStatus.Complete;
+ else if (next.Value.Surgery.Owner != _surgery.Value.Ent)
+ status = StepStatus.Incomplete;
+ else if (next.Value.Step == i)
+ status = StepStatus.Next;
+ else if (i < next.Value.Step)
+ status = StepStatus.Complete;
+
+ stepButton.Button.Disabled = status != StepStatus.Next;
+
+ var stepName = new FormattedMessage();
+ stepName.AddText(_entities.GetComponent(stepButton.Step).EntityName);
+
+ if (status == StepStatus.Complete)
+ stepButton.Button.Modulate = Color.Green;
+ else
+ {
+ stepButton.Button.Modulate = Color.White;
+ if (_player.LocalEntity is { } player
+ && status == StepStatus.Next
+ && !_system.CanPerformStep(player, Owner, _part.Value, stepButton.Step, false, out var popup, out var reason, out _))
+ stepButton.ToolTip = popup;
+ }
+
+ var texture = _entities.GetComponentOrNull(stepButton.Step)?.Icon?.Default;
+ stepButton.Set(stepName, texture);
+ i++;
+ }
+ }
+
+ private void View(ViewType type)
+ {
+ if (_window == null)
+ return;
+
+ _window.PartsButton.Parent!.Margin = new Thickness(0, 0, 0, 10);
+
+ _window.Parts.Visible = type == ViewType.Parts;
+ _window.PartsButton.Disabled = type == ViewType.Parts;
+
+ _window.Surgeries.Visible = type == ViewType.Surgeries;
+ _window.SurgeriesButton.Disabled = type != ViewType.Steps;
+
+ _window.Steps.Visible = type == ViewType.Steps;
+ _window.StepsButton.Disabled = type != ViewType.Steps || _previousSurgeries.Count == 0;
+
+ if (_entities.TryGetComponent(_part, out MetaDataComponent? partMeta) &&
+ _entities.TryGetComponent(_surgery?.Ent, out MetaDataComponent? surgeryMeta))
+ _window.Title = $"Surgery - {partMeta.EntityName}, {surgeryMeta.EntityName}";
+ else if (partMeta != null)
+ _window.Title = $"Surgery - {partMeta.EntityName}";
+ else
+ _window.Title = "Surgery";
+ }
+
+ private enum ViewType
+ {
+ Parts,
+ Surgeries,
+ Steps
+ }
+
+ private enum StepStatus
+ {
+ Next,
+ Complete,
+ Incomplete
+ }
+}
diff --git a/Content.Client/Medical/Surgery/SurgeryStepButton.xaml b/Content.Client/Medical/Surgery/SurgeryStepButton.xaml
new file mode 100644
index 0000000000..7fbf9543e9
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryStepButton.xaml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/Content.Client/Medical/Surgery/SurgeryStepButton.xaml.cs b/Content.Client/Medical/Surgery/SurgeryStepButton.xaml.cs
new file mode 100644
index 0000000000..31bf1e0752
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryStepButton.xaml.cs
@@ -0,0 +1,16 @@
+using Content.Client.Xenonids.UI;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Medical.Surgery;
+
+[GenerateTypedNameReferences]
+public sealed partial class SurgeryStepButton : XenoChoiceControl
+{
+ public EntityUid Step { get; set; }
+
+ public SurgeryStepButton()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Medical/Surgery/SurgerySystem.cs b/Content.Client/Medical/Surgery/SurgerySystem.cs
new file mode 100644
index 0000000000..cbf1aeee48
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgerySystem.cs
@@ -0,0 +1,11 @@
+using Content.Shared.Medical.Surgery;
+
+namespace Content.Client.Medical.Surgery;
+
+public sealed class SurgerySystem : SharedSurgerySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+}
diff --git a/Content.Client/Medical/Surgery/SurgeryWindow.xaml b/Content.Client/Medical/Surgery/SurgeryWindow.xaml
new file mode 100644
index 0000000000..bba801a8a5
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryWindow.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Medical/Surgery/SurgeryWindow.xaml.cs b/Content.Client/Medical/Surgery/SurgeryWindow.xaml.cs
new file mode 100644
index 0000000000..1b579e7408
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryWindow.xaml.cs
@@ -0,0 +1,14 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Medical.Surgery;
+
+[GenerateTypedNameReferences]
+public sealed partial class SurgeryWindow : DefaultWindow
+{
+ public SurgeryWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Message/RichTextLabelExt.cs b/Content.Client/Message/RichTextLabelExt.cs
index ab6d17bf44..f3de61be12 100644
--- a/Content.Client/Message/RichTextLabelExt.cs
+++ b/Content.Client/Message/RichTextLabelExt.cs
@@ -5,9 +5,27 @@ namespace Content.Client.Message;
public static class RichTextLabelExt
{
+
+
+ ///
+ /// Sets the labels markup.
+ ///
+ ///
+ /// Invalid markup will cause exceptions to be thrown. Don't use this for user input!
+ ///
public static RichTextLabel SetMarkup(this RichTextLabel label, string markup)
{
label.SetMessage(FormattedMessage.FromMarkup(markup));
return label;
}
+
+ ///
+ /// Sets the labels markup.
+ /// Uses FormatedMessage.FromMarkupPermissive which treats invalid markup as text.
+ ///
+ public static RichTextLabel SetMarkupPermissive(this RichTextLabel label, string markup)
+ {
+ label.SetMessage(FormattedMessage.FromMarkupPermissive(markup));
+ return label;
+ }
}
diff --git a/Content.Client/MouseRotator/MouseRotatorSystem.cs b/Content.Client/MouseRotator/MouseRotatorSystem.cs
index 44e8205355..363c8248c9 100644
--- a/Content.Client/MouseRotator/MouseRotatorSystem.cs
+++ b/Content.Client/MouseRotator/MouseRotatorSystem.cs
@@ -45,13 +45,19 @@ public override void Update(float frameTime)
// only raise event if the cardinal direction has changed
if (rotator.Simple4DirMode)
{
- var angleDir = angle.GetCardinalDir();
- if (angleDir == curRot.GetCardinalDir())
+ var eyeRot = _eye.CurrentEye.Rotation; // camera rotation
+ var angleDir = (angle + eyeRot).GetCardinalDir(); // apply GetCardinalDir in the camera frame, not in the world frame
+ if (angleDir == (curRot + eyeRot).GetCardinalDir())
return;
- RaisePredictiveEvent(new RequestMouseRotatorRotationSimpleEvent()
+ var rotation = angleDir.ToAngle() - eyeRot; // convert back to world frame
+ if (rotation >= Math.PI) // convert to [-PI, +PI)
+ rotation -= 2 * Math.PI;
+ else if (rotation < -Math.PI)
+ rotation += 2 * Math.PI;
+ RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
{
- Direction = angleDir,
+ Rotation = rotation
});
return;
diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs
index 709601a57b..d3ae509152 100644
--- a/Content.Client/NPC/PathfindingSystem.cs
+++ b/Content.Client/NPC/PathfindingSystem.cs
@@ -223,7 +223,7 @@ private void DrawScreen(OverlayDrawArgs args, DrawingHandleScreen screenHandle)
foreach (var crumb in chunk.Value)
{
- var crumbMapPos = worldMatrix.Transform(_system.GetCoordinate(chunk.Key, crumb.Coordinates));
+ var crumbMapPos = Vector2.Transform(_system.GetCoordinate(chunk.Key, crumb.Coordinates), worldMatrix);
var distance = (crumbMapPos - mouseWorldPos.Position).Length();
if (distance < nearestDistance)
@@ -292,7 +292,7 @@ private void DrawScreen(OverlayDrawArgs args, DrawingHandleScreen screenHandle)
foreach (var poly in tile)
{
- if (poly.Box.Contains(invGridMatrix.Transform(mouseWorldPos.Position)))
+ if (poly.Box.Contains(Vector2.Transform(mouseWorldPos.Position, invGridMatrix)))
{
nearest = poly;
break;
@@ -488,7 +488,7 @@ private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle)
if (neighborMap.MapId != args.MapId)
continue;
- neighborPos = invMatrix.Transform(neighborMap.Position);
+ neighborPos = Vector2.Transform(neighborMap.Position, invMatrix);
}
else
{
@@ -576,7 +576,7 @@ private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle)
}
}
- worldHandle.SetTransform(Matrix3.Identity);
+ worldHandle.SetTransform(Matrix3x2.Identity);
}
}
}
diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs
index 264c297b63..80c98f143b 100644
--- a/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs
+++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs
@@ -88,6 +88,7 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
if (!disposing) return;
+ _linkMenu?.Dispose();
_listMenu?.Dispose();
_configurationMenu?.Dispose();
}
diff --git a/Content.Client/NodeContainer/NodeVisualizationOverlay.cs b/Content.Client/NodeContainer/NodeVisualizationOverlay.cs
index 691bcb41db..4e8a4a10ca 100644
--- a/Content.Client/NodeContainer/NodeVisualizationOverlay.cs
+++ b/Content.Client/NodeContainer/NodeVisualizationOverlay.cs
@@ -199,7 +199,7 @@ private void DrawWorld(in OverlayDrawArgs overlayDrawArgs)
}
- handle.SetTransform(Matrix3.Identity);
+ handle.SetTransform(Matrix3x2.Identity);
_gridIndex.Clear();
}
diff --git a/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs
new file mode 100644
index 0000000000..16dbecb793
--- /dev/null
+++ b/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared.Nutrition.EntitySystems;
+
+namespace Content.Client.Nutrition.EntitySystems;
+
+public sealed class DrinkSystem : SharedDrinkSystem
+{
+}
diff --git a/Content.Client/Nutrition/EntitySystems/FoodGuideDataSystem.cs b/Content.Client/Nutrition/EntitySystems/FoodGuideDataSystem.cs
new file mode 100644
index 0000000000..37c7a25e21
--- /dev/null
+++ b/Content.Client/Nutrition/EntitySystems/FoodGuideDataSystem.cs
@@ -0,0 +1,30 @@
+using Content.Client.Chemistry.EntitySystems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Nutrition.EntitySystems;
+
+public sealed class FoodGuideDataSystem : SharedFoodGuideDataSystem
+{
+ public override void Initialize()
+ {
+ SubscribeNetworkEvent(OnReceiveRegistryUpdate);
+ }
+
+ private void OnReceiveRegistryUpdate(FoodGuideRegistryChangedEvent message)
+ {
+ Registry = message.Changeset;
+ }
+
+ public bool TryGetData(EntProtoId result, out FoodGuideEntry entry)
+ {
+ var index = Registry.FindIndex(it => it.Result == result);
+ if (index == -1)
+ {
+ entry = default;
+ return false;
+ }
+
+ entry = Registry[index];
+ return true;
+ }
+}
diff --git a/Content.Client/Nyanotrasen/Overlays/DogVisionOverlay.cs b/Content.Client/Nyanotrasen/Overlays/DogVisionOverlay.cs
deleted file mode 100644
index 95cfc683e0..0000000000
--- a/Content.Client/Nyanotrasen/Overlays/DogVisionOverlay.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using Robust.Client.Graphics;
-using Robust.Client.Player;
-using Robust.Shared.Enums;
-using Robust.Shared.Prototypes;
-using Content.Shared.Abilities;
-
-namespace Content.Client.Nyanotrasen.Overlays;
-
-public sealed partial class DogVisionOverlay : Overlay
-{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] IEntityManager _entityManager = default!;
-
-
- public override bool RequestScreenTexture => true;
- public override OverlaySpace Space => OverlaySpace.WorldSpace;
- private readonly ShaderInstance _dogVisionShader;
-
- public DogVisionOverlay()
- {
- IoCManager.InjectDependencies(this);
- _dogVisionShader = _prototypeManager.Index("DogVision").Instance().Duplicate();
- }
-
- protected override void Draw(in OverlayDrawArgs args)
- {
- if (ScreenTexture == null)
- return;
- if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} player)
- return;
- if (!_entityManager.HasComponent(player))
- return;
-
- _dogVisionShader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
-
-
- var worldHandle = args.WorldHandle;
- var viewport = args.WorldBounds;
- worldHandle.SetTransform(Matrix3.Identity);
- worldHandle.UseShader(_dogVisionShader);
- worldHandle.DrawRect(viewport, Color.White);
- }
-}
diff --git a/Content.Client/Nyanotrasen/Overlays/DogVisionSystem.cs b/Content.Client/Nyanotrasen/Overlays/DogVisionSystem.cs
deleted file mode 100644
index 2da90e877e..0000000000
--- a/Content.Client/Nyanotrasen/Overlays/DogVisionSystem.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using Content.Shared.Abilities;
-using Content.Shared.DeltaV.CCVars;
-using Robust.Client.Graphics;
-using Robust.Shared.Configuration;
-
-namespace Content.Client.Nyanotrasen.Overlays;
-
-public sealed partial class DogVisionSystem : EntitySystem
-{
- [Dependency] private readonly IOverlayManager _overlayMan = default!;
- [Dependency] private readonly IConfigurationManager _cfg = default!;
-
- private DogVisionOverlay _overlay = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnDogVisionInit);
- SubscribeLocalEvent(OnDogVisionShutdown);
-
- Subs.CVar(_cfg, DCCVars.NoVisionFilters, OnNoVisionFiltersChanged);
-
- _overlay = new();
- }
-
- private void OnDogVisionInit(EntityUid uid, DogVisionComponent component, ComponentInit args)
- {
- if (!_cfg.GetCVar(DCCVars.NoVisionFilters))
- _overlayMan.AddOverlay(_overlay);
- }
-
- private void OnDogVisionShutdown(EntityUid uid, DogVisionComponent component, ComponentShutdown args)
- {
- _overlayMan.RemoveOverlay(_overlay);
- }
-
- private void OnNoVisionFiltersChanged(bool enabled)
- {
- if (enabled)
- _overlayMan.RemoveOverlay(_overlay);
- else
- _overlayMan.AddOverlay(_overlay);
- }
-}
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml
index 69daaa2cea..ab3b88ca4e 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml
+++ b/Content.Client/Options/UI/OptionsMenu.xaml
@@ -1,6 +1,5 @@
@@ -9,6 +8,5 @@
-
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml.cs b/Content.Client/Options/UI/OptionsMenu.xaml.cs
index bb2c1ce0ed..c3a8e66470 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml.cs
+++ b/Content.Client/Options/UI/OptionsMenu.xaml.cs
@@ -20,7 +20,6 @@ public OptionsMenu()
Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-controls"));
Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio"));
Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-network"));
- Tabs.SetTabTitle(5, Loc.GetString("ui-options-tab-deltav")); // DeltaV specific settings
UpdateTabs();
}
diff --git a/Content.Client/Options/UI/Tabs/AudioTab.xaml b/Content.Client/Options/UI/Tabs/AudioTab.xaml
index 8dd723d446..78b0e82629 100644
--- a/Content.Client/Options/UI/Tabs/AudioTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AudioTab.xaml
@@ -117,6 +117,9 @@
+
diff --git a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
index 0207ed5c47..7da80d774b 100644
--- a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
@@ -30,97 +30,89 @@ public AudioTab()
ApplyButton.OnPressed += OnApplyButtonPressed;
ResetButton.OnPressed += OnResetButtonPressed;
- MasterVolumeSlider.OnValueChanged += OnMasterVolumeSliderChanged;
- MidiVolumeSlider.OnValueChanged += OnMidiVolumeSliderChanged;
- AmbientMusicVolumeSlider.OnValueChanged += OnAmbientMusicVolumeSliderChanged;
- AmbienceVolumeSlider.OnValueChanged += OnAmbienceVolumeSliderChanged;
- AmbienceSoundsSlider.OnValueChanged += OnAmbienceSoundsSliderChanged;
- LobbyVolumeSlider.OnValueChanged += OnLobbyVolumeSliderChanged;
- InterfaceVolumeSlider.OnValueChanged += OnInterfaceVolumeSliderChanged;
- AnnouncerVolumeSlider.OnValueChanged += OnAnnouncerVolumeSliderChanged;
- LobbyMusicCheckBox.OnToggled += OnLobbyMusicCheckToggled;
- RestartSoundsCheckBox.OnToggled += OnRestartSoundsCheckToggled;
- EventMusicCheckBox.OnToggled += OnEventMusicCheckToggled;
- AdminSoundsCheckBox.OnToggled += OnAdminSoundsCheckToggled;
+
+ AttachUpdateChangesHandler(
+ MasterVolumeSlider,
+ MidiVolumeSlider,
+ AmbientMusicVolumeSlider,
+ AmbienceVolumeSlider,
+ AmbienceSoundsSlider,
+ LobbyVolumeSlider,
+ InterfaceVolumeSlider,
+ AnnouncerVolumeSlider,
+
+ LobbyMusicCheckBox,
+ RestartSoundsCheckBox,
+ EventMusicCheckBox,
+ AnnouncerDisableMultipleSoundsCheckBox,
+ AdminSoundsCheckBox
+ );
AmbienceSoundsSlider.MinValue = _cfg.GetCVar(CCVars.MinMaxAmbientSourcesConfigured);
AmbienceSoundsSlider.MaxValue = _cfg.GetCVar(CCVars.MaxMaxAmbientSourcesConfigured);
Reset();
+ return;
+
+ void AttachUpdateChangesHandler(params Control[] controls)
+ {
+ foreach (var control in controls)
+ {
+ switch (control)
+ {
+ case Slider slider:
+ slider.OnValueChanged += _ => UpdateChanges();
+ break;
+ case CheckBox checkBox:
+ checkBox.OnToggled += _ => UpdateChanges();
+ break;
+ }
+ }
+ }
}
protected override void Dispose(bool disposing)
{
ApplyButton.OnPressed -= OnApplyButtonPressed;
ResetButton.OnPressed -= OnResetButtonPressed;
- MasterVolumeSlider.OnValueChanged -= OnMasterVolumeSliderChanged;
- MidiVolumeSlider.OnValueChanged -= OnMidiVolumeSliderChanged;
- AmbientMusicVolumeSlider.OnValueChanged -= OnAmbientMusicVolumeSliderChanged;
- AmbienceVolumeSlider.OnValueChanged -= OnAmbienceVolumeSliderChanged;
- LobbyVolumeSlider.OnValueChanged -= OnLobbyVolumeSliderChanged;
- InterfaceVolumeSlider.OnValueChanged -= OnInterfaceVolumeSliderChanged;
- AnnouncerVolumeSlider.OnValueChanged -= OnAnnouncerVolumeSliderChanged;
- base.Dispose(disposing);
- }
-
- private void OnLobbyVolumeSliderChanged(Range obj)
- {
- UpdateChanges();
- }
-
- private void OnInterfaceVolumeSliderChanged(Range obj)
- {
- UpdateChanges();
- }
-
- private void OnAmbientMusicVolumeSliderChanged(Range obj)
- {
- UpdateChanges();
- }
-
- private void OnAmbienceVolumeSliderChanged(Range obj)
- {
- UpdateChanges();
- }
-
- private void OnAmbienceSoundsSliderChanged(Range obj)
- {
- UpdateChanges();
- }
- private void OnMasterVolumeSliderChanged(Range range)
- {
- _audio.SetMasterGain(MasterVolumeSlider.Value / 100f * ContentAudioSystem.MasterVolumeMultiplier);
- UpdateChanges();
- }
-
- private void OnMidiVolumeSliderChanged(Range range)
- {
- UpdateChanges();
- }
-
- private void OnAnnouncerVolumeSliderChanged(Range range)
- {
- UpdateChanges();
- }
+ DetachUpdateChangesHandler(
+ MasterVolumeSlider,
+ MidiVolumeSlider,
+ AmbientMusicVolumeSlider,
+ AmbienceVolumeSlider,
+ AmbienceSoundsSlider,
+ LobbyVolumeSlider,
+ InterfaceVolumeSlider,
+ AnnouncerVolumeSlider,
+
+ LobbyMusicCheckBox,
+ RestartSoundsCheckBox,
+ EventMusicCheckBox,
+ AnnouncerDisableMultipleSoundsCheckBox,
+ AdminSoundsCheckBox
+ );
- private void OnLobbyMusicCheckToggled(BaseButton.ButtonEventArgs args)
- {
- UpdateChanges();
- }
- private void OnRestartSoundsCheckToggled(BaseButton.ButtonEventArgs args)
- {
- UpdateChanges();
- }
- private void OnEventMusicCheckToggled(BaseButton.ButtonEventArgs args)
- {
- UpdateChanges();
+ base.Dispose(disposing);
+ return;
+
+ void DetachUpdateChangesHandler(params Control[] controls)
+ {
+ foreach (var control in controls)
+ {
+ switch (control)
+ {
+ case Slider slider:
+ slider.OnValueChanged -= _ => UpdateChanges();
+ break;
+ case CheckBox checkBox:
+ checkBox.OnToggled -= _ => UpdateChanges();
+ break;
+ }
+ }
+ }
}
- private void OnAdminSoundsCheckToggled(BaseButton.ButtonEventArgs args)
- {
- UpdateChanges();
- }
private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
{
@@ -139,6 +131,7 @@ private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
_cfg.SetCVar(CCVars.LobbyMusicEnabled, LobbyMusicCheckBox.Pressed);
_cfg.SetCVar(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox.Pressed);
_cfg.SetCVar(CCVars.EventMusicEnabled, EventMusicCheckBox.Pressed);
+ _cfg.SetCVar(CCVars.AnnouncerDisableMultipleSounds, AnnouncerDisableMultipleSoundsCheckBox.Pressed);
_cfg.SetCVar(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox.Pressed);
_cfg.SaveToFile();
UpdateChanges();
@@ -164,6 +157,7 @@ private void Reset()
LobbyMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.LobbyMusicEnabled);
RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled);
EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled);
+ AnnouncerDisableMultipleSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.AnnouncerDisableMultipleSounds);
AdminSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.AdminSoundsEnabled);
UpdateChanges();
}
@@ -190,10 +184,12 @@ private void UpdateChanges()
var isLobbySame = LobbyMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.LobbyMusicEnabled);
var isRestartSoundsSame = RestartSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.RestartSoundsEnabled);
var isEventSame = EventMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.EventMusicEnabled);
+ var isAnnouncerDisableMultipleSoundsSame = AnnouncerDisableMultipleSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.AnnouncerDisableMultipleSounds);
var isAdminSoundsSame = AdminSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.AdminSoundsEnabled);
var isEverythingSame = isMasterVolumeSame && isMidiVolumeSame && isAmbientVolumeSame
&& isAmbientMusicVolumeSame && isAmbientSoundsSame && isLobbySame && isRestartSoundsSame && isEventSame
- && isAdminSoundsSame && isLobbyVolumeSame && isInterfaceVolumeSame && isAnnouncerVolumeSame;
+ && isAnnouncerDisableMultipleSoundsSame && isAdminSoundsSame && isLobbyVolumeSame
+ && isInterfaceVolumeSame && isAnnouncerVolumeSame;
ApplyButton.Disabled = isEverythingSame;
ResetButton.Disabled = isEverythingSame;
MasterVolumeLabel.Text =
diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
index 118b85b87b..ec1b9aa002 100644
--- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
+++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
@@ -36,6 +36,9 @@
+
diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
index 3113e644ba..a22adf3e63 100644
--- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
@@ -67,6 +67,12 @@ public GraphicsTab()
UpdateApplyButton();
};
+ ViewportVerticalFitCheckBox.OnToggled += _ =>
+ {
+ UpdateViewportScale();
+ UpdateApplyButton();
+ };
+
IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled;
ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled;
ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled;
@@ -79,6 +85,7 @@ public GraphicsTab()
ViewportScaleSlider.Value = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
ViewportStretchCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportStretch);
IntegerScalingCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0;
+ ViewportVerticalFitCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportVerticalFit);
ViewportLowResCheckBox.Pressed = !_cfg.GetCVar(CCVars.ViewportScaleRender);
ParallaxLowQualityCheckBox.Pressed = _cfg.GetCVar(CCVars.ParallaxLowQuality);
FpsCounterCheckBox.Pressed = _cfg.GetCVar(CCVars.HudFpsCounterVisible);
@@ -111,6 +118,7 @@ private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
_cfg.SetCVar(CCVars.ViewportFixedScaleFactor, (int) ViewportScaleSlider.Value);
_cfg.SetCVar(CCVars.ViewportSnapToleranceMargin,
IntegerScalingCheckBox.Pressed ? CCVars.ViewportSnapToleranceMargin.DefaultValue : 0);
+ _cfg.SetCVar(CCVars.ViewportVerticalFit, ViewportVerticalFitCheckBox.Pressed);
_cfg.SetCVar(CCVars.ViewportScaleRender, !ViewportLowResCheckBox.Pressed);
_cfg.SetCVar(CCVars.ParallaxLowQuality, ParallaxLowQualityCheckBox.Pressed);
_cfg.SetCVar(CCVars.HudFpsCounterVisible, FpsCounterCheckBox.Pressed);
@@ -140,6 +148,7 @@ private void UpdateApplyButton()
var isVPStretchSame = ViewportStretchCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportStretch);
var isVPScaleSame = (int) ViewportScaleSlider.Value == _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
var isIntegerScalingSame = IntegerScalingCheckBox.Pressed == (_cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0);
+ var isVPVerticalFitSame = ViewportVerticalFitCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportVerticalFit);
var isVPResSame = ViewportLowResCheckBox.Pressed == !_cfg.GetCVar(CCVars.ViewportScaleRender);
var isPLQSame = ParallaxLowQualityCheckBox.Pressed == _cfg.GetCVar(CCVars.ParallaxLowQuality);
var isFpsCounterVisibleSame = FpsCounterCheckBox.Pressed == _cfg.GetCVar(CCVars.HudFpsCounterVisible);
@@ -152,6 +161,7 @@ private void UpdateApplyButton()
isVPStretchSame &&
isVPScaleSame &&
isIntegerScalingSame &&
+ isVPVerticalFitSame &&
isVPResSame &&
isPLQSame &&
isFpsCounterVisibleSame &&
@@ -235,6 +245,8 @@ private void UpdateViewportScale()
{
ViewportScaleBox.Visible = !ViewportStretchCheckBox.Pressed;
IntegerScalingCheckBox.Visible = ViewportStretchCheckBox.Pressed;
+ ViewportVerticalFitCheckBox.Visible = ViewportStretchCheckBox.Pressed;
+ ViewportWidthSlider.Visible = ViewportWidthSliderDisplay.Visible = !ViewportStretchCheckBox.Pressed || ViewportStretchCheckBox.Pressed && !ViewportVerticalFitCheckBox.Pressed;
ViewportScaleText.Text = Loc.GetString("ui-options-vp-scale", ("scale", ViewportScaleSlider.Value));
}
diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
index 13e456985a..c89659c3ab 100644
--- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
@@ -97,12 +97,30 @@ private void HandleToggleWalk(BaseButton.ButtonToggledEventArgs args)
_deferCommands.Add(_inputManager.SaveToUserData);
}
+ private void HandleHoldLookUp(BaseButton.ButtonToggledEventArgs args)
+ {
+ _cfg.SetCVar(CCVars.HoldLookUp, args.Pressed);
+ _cfg.SaveToFile();
+ }
+
+ private void HandleDefaultWalk(BaseButton.ButtonToggledEventArgs args)
+ {
+ _cfg.SetCVar(CCVars.DefaultWalk, args.Pressed);
+ _cfg.SaveToFile();
+ }
+
private void HandleStaticStorageUI(BaseButton.ButtonToggledEventArgs args)
{
_cfg.SetCVar(CCVars.StaticStorageUI, args.Pressed);
_cfg.SaveToFile();
}
+ private void HandleToggleAutoGetUp(BaseButton.ButtonToggledEventArgs args)
+ {
+ _cfg.SetCVar(CCVars.AutoGetUp, args.Pressed);
+ _cfg.SaveToFile();
+ }
+
public KeyRebindTab()
{
IoCManager.InjectDependencies(this);
@@ -161,6 +179,7 @@ void AddCheckBox(string checkBoxName, bool currentState, Action
+
+/// A simple overlay that applies a colored tint to the screen.
+///
+public sealed class ColorTintOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] IEntityManager _entityManager = default!;
+
+ public override bool RequestScreenTexture => true;
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ private readonly ShaderInstance _shader;
+
+ ///
+ /// The color to tint the screen to as RGB on a scale of 0-1.
+ ///
+ public Robust.Shared.Maths.Vector3? TintColor = null;
+ ///
+ /// The percent to tint the screen by on a scale of 0-1.
+ ///
+ public float? TintAmount = null;
+
+ public ColorTintOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _shader = _prototype.Index("ColorTint").InstanceUnique();
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (_player.LocalEntity is not { Valid: true } player
+ || !_entityManager.HasComponent(player))
+ return false;
+
+ return base.BeforeDraw(in args);
+ }
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture is null)
+ return;
+
+ _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+ if (TintColor != null)
+ _shader.SetParameter("tint_color", (Robust.Shared.Maths.Vector3) TintColor);
+ if (TintAmount != null)
+ _shader.SetParameter("tint_amount", (float) TintAmount);
+
+ var worldHandle = args.WorldHandle;
+ var viewport = args.WorldBounds;
+ worldHandle.SetTransform(Matrix3x2.Identity);
+ worldHandle.UseShader(_shader);
+ worldHandle.DrawRect(viewport, Color.White);
+ worldHandle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Overlays/DogVisionOverlay.cs b/Content.Client/Overlays/DogVisionOverlay.cs
new file mode 100644
index 0000000000..022a31d696
--- /dev/null
+++ b/Content.Client/Overlays/DogVisionOverlay.cs
@@ -0,0 +1,50 @@
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Content.Shared.Traits.Assorted.Components;
+
+namespace Content.Client.Overlays;
+
+public sealed partial class DogVisionOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] IEntityManager _entityManager = default!;
+
+
+ public override bool RequestScreenTexture => true;
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ private readonly ShaderInstance _dogVisionShader;
+
+ public DogVisionOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _dogVisionShader = _prototypeManager.Index("DogVision").Instance().Duplicate();
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (_playerManager.LocalEntity is not { Valid: true } player
+ || !_entityManager.HasComponent(player))
+ return false;
+
+ return base.BeforeDraw(in args);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture is null)
+ return;
+
+ _dogVisionShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+
+ var worldHandle = args.WorldHandle;
+ var viewport = args.WorldBounds;
+ worldHandle.SetTransform(Matrix3x2.Identity);
+ worldHandle.UseShader(_dogVisionShader);
+ worldHandle.DrawRect(viewport, Color.White);
+ worldHandle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Overlays/DogVisionSystem.cs b/Content.Client/Overlays/DogVisionSystem.cs
new file mode 100644
index 0000000000..5c80c307cb
--- /dev/null
+++ b/Content.Client/Overlays/DogVisionSystem.cs
@@ -0,0 +1,66 @@
+using Content.Shared.Traits.Assorted.Components;
+using Content.Shared.CCVar;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
+
+namespace Content.Client.Overlays;
+
+public sealed partial class DogVisionSystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerMan = default!;
+
+ private DogVisionOverlay _overlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnDogVisionInit);
+ SubscribeLocalEvent(OnDogVisionShutdown);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ Subs.CVar(_cfg, CCVars.NoVisionFilters, OnNoVisionFiltersChanged);
+
+ _overlay = new();
+ }
+
+ private void OnDogVisionInit(EntityUid uid, DogVisionComponent component, ComponentInit args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnDogVisionShutdown(EntityUid uid, DogVisionComponent component, ComponentShutdown args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, DogVisionComponent component, LocalPlayerAttachedEvent args)
+ {
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnPlayerDetached(EntityUid uid, DogVisionComponent component, LocalPlayerDetachedEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnNoVisionFiltersChanged(bool enabled)
+ {
+ if (enabled)
+ _overlayMan.RemoveOverlay(_overlay);
+ else
+ _overlayMan.AddOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/Overlays/EntityHealthBarOverlay.cs b/Content.Client/Overlays/EntityHealthBarOverlay.cs
index c1c0ae93ec..c96225c0c6 100644
--- a/Content.Client/Overlays/EntityHealthBarOverlay.cs
+++ b/Content.Client/Overlays/EntityHealthBarOverlay.cs
@@ -43,8 +43,8 @@ protected override void Draw(in OverlayDrawArgs args)
var xformQuery = _entManager.GetEntityQuery();
const float scale = 1f;
- var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale));
- var rotationMatrix = Matrix3.CreateRotation(-rotation);
+ var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
+ var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);
var query = _entManager.AllEntityQueryEnumerator();
while (query.MoveNext(out var uid,
@@ -80,10 +80,10 @@ protected override void Draw(in OverlayDrawArgs args)
}
var worldPosition = _transform.GetWorldPosition(xform);
- var worldMatrix = Matrix3.CreateTranslation(worldPosition);
+ var worldMatrix = Matrix3Helpers.CreateTranslation(worldPosition);
- Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld);
- Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty);
+ var scaledWorld = Matrix3x2.Multiply(scaleMatrix, worldMatrix);
+ var matty = Matrix3x2.Multiply(rotationMatrix, scaledWorld);
handle.SetTransform(matty);
@@ -116,7 +116,7 @@ protected override void Draw(in OverlayDrawArgs args)
handle.DrawRect(pixelDarken, Black.WithAlpha(128));
}
- handle.SetTransform(Matrix3.Identity);
+ handle.SetTransform(Matrix3x2.Identity);
}
///
diff --git a/Content.Client/Overlays/EtherealOverlay.cs b/Content.Client/Overlays/EtherealOverlay.cs
new file mode 100644
index 0000000000..594a3656c8
--- /dev/null
+++ b/Content.Client/Overlays/EtherealOverlay.cs
@@ -0,0 +1,49 @@
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Content.Shared.Shadowkin;
+
+namespace Content.Client.Overlays;
+
+public sealed class EtherealOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] IEntityManager _entityManager = default!;
+
+ public override bool RequestScreenTexture => true;
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
+ private readonly ShaderInstance _shader;
+
+ public EtherealOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _shader = _prototype.Index("Ethereal").InstanceUnique();
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (_player.LocalEntity is not { Valid: true } player
+ || !_entityManager.HasComponent(player))
+ return false;
+
+ return base.BeforeDraw(in args);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture is null)
+ return;
+
+ _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+
+ var worldHandle = args.WorldHandle;
+ var viewport = args.WorldBounds;
+ worldHandle.SetTransform(Matrix3x2.Identity);
+ worldHandle.UseShader(_shader);
+ worldHandle.DrawRect(viewport, Color.White);
+ worldHandle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Overlays/SaturationScaleOverlay.cs b/Content.Client/Overlays/SaturationScaleOverlay.cs
new file mode 100644
index 0000000000..d3a27a9724
--- /dev/null
+++ b/Content.Client/Overlays/SaturationScaleOverlay.cs
@@ -0,0 +1,54 @@
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Content.Shared.Mood;
+using Content.Shared.Overlays;
+
+namespace Content.Client.Overlays;
+
+public sealed class SaturationScaleOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] IEntityManager _entityManager = default!;
+
+ public override bool RequestScreenTexture => true;
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ private readonly ShaderInstance _shader;
+ private const float Saturation = 0.5f;
+
+
+ public SaturationScaleOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _shader = _prototypeManager.Index("SaturationScale").Instance().Duplicate();
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (_playerManager.LocalEntity is not { Valid: true } player
+ || !_entityManager.HasComponent(player))
+ return false;
+
+ return base.BeforeDraw(in args);
+ }
+
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture is null)
+ return;
+
+ _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+ _shader.SetParameter("saturation", Saturation);
+
+ var handle = args.WorldHandle;
+ handle.SetTransform(Matrix3x2.Identity);
+ handle.UseShader(_shader);
+ handle.DrawRect(args.WorldBounds, Color.White);
+ handle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Overlays/SaturationScaleSystem.cs b/Content.Client/Overlays/SaturationScaleSystem.cs
new file mode 100644
index 0000000000..b5932e3a49
--- /dev/null
+++ b/Content.Client/Overlays/SaturationScaleSystem.cs
@@ -0,0 +1,63 @@
+using Content.Shared.GameTicking;
+using Content.Shared.Mood;
+using Content.Shared.Overlays;
+using Robust.Client.Graphics;
+using Robust.Shared.Player;
+
+namespace Content.Client.Overlays;
+
+public sealed class SaturationScaleSystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerMan = default!;
+
+ private SaturationScaleOverlay _overlay = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _overlay = new();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ SubscribeNetworkEvent(RoundRestartCleanup);
+ }
+
+
+ private void RoundRestartCleanup(RoundRestartCleanupEvent ev)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnPlayerDetached(EntityUid uid, SaturationScaleOverlayComponent component, PlayerDetachedEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, SaturationScaleOverlayComponent component, PlayerAttachedEvent args)
+ {
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnShutdown(EntityUid uid, SaturationScaleOverlayComponent component, ComponentShutdown args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnInit(EntityUid uid, SaturationScaleOverlayComponent component, ComponentInit args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.AddOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
new file mode 100644
index 0000000000..8f23cd510c
--- /dev/null
+++ b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Overlays;
+using Content.Shared.Security.Components;
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Overlays;
+
+public sealed class ShowCriminalRecordIconsSystem : EquipmentHudSystem
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetStatusIconsEvent);
+ }
+
+ private void OnGetStatusIconsEvent(EntityUid uid, CriminalRecordComponent component, ref GetStatusIconsEvent ev)
+ {
+ if (!IsActive || ev.InContainer)
+ return;
+
+ if (_prototype.TryIndex(component.StatusIcon.Id, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
+ }
+}
diff --git a/Content.Client/Overlays/ShowHungerIconsSystem.cs b/Content.Client/Overlays/ShowHungerIconsSystem.cs
index 58551b30c2..b1c0f3a1a0 100644
--- a/Content.Client/Overlays/ShowHungerIconsSystem.cs
+++ b/Content.Client/Overlays/ShowHungerIconsSystem.cs
@@ -1,14 +1,13 @@
+using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Overlays;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowHungerIconsSystem : EquipmentHudSystem
{
- [Dependency] private readonly IPrototypeManager _prototypeMan = default!;
+ [Dependency] private readonly HungerSystem _hunger = default!;
public override void Initialize()
{
@@ -17,42 +16,12 @@ public override void Initialize()
SubscribeLocalEvent(OnGetStatusIconsEvent);
}
- private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent hungerComponent, ref GetStatusIconsEvent args)
+ private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent component, ref GetStatusIconsEvent ev)
{
- if (!IsActive || args.InContainer)
+ if (!IsActive || ev.InContainer)
return;
- var hungerIcons = DecideHungerIcon(uid, hungerComponent);
-
- args.StatusIcons.AddRange(hungerIcons);
- }
-
- private IReadOnlyList DecideHungerIcon(EntityUid uid, HungerComponent hungerComponent)
- {
- var result = new List();
-
- switch (hungerComponent.CurrentThreshold)
- {
- case HungerThreshold.Overfed:
- if (_prototypeMan.TryIndex("HungerIconOverfed", out var overfed))
- {
- result.Add(overfed);
- }
- break;
- case HungerThreshold.Peckish:
- if (_prototypeMan.TryIndex("HungerIconPeckish", out var peckish))
- {
- result.Add(peckish);
- }
- break;
- case HungerThreshold.Starving:
- if (_prototypeMan.TryIndex("HungerIconStarving", out var starving))
- {
- result.Add(starving);
- }
- break;
- }
-
- return result;
+ if (_hunger.TryGetStatusIconPrototype(component, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/ShowJobIconsSystem.cs b/Content.Client/Overlays/ShowJobIconsSystem.cs
new file mode 100644
index 0000000000..e24b99f3e8
--- /dev/null
+++ b/Content.Client/Overlays/ShowJobIconsSystem.cs
@@ -0,0 +1,60 @@
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Overlays;
+using Content.Shared.PDA;
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Overlays;
+
+public sealed class ShowJobIconsSystem : EquipmentHudSystem
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+
+ [ValidatePrototypeId]
+ private const string JobIconForNoId = "JobIconNoId";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetStatusIconsEvent);
+ }
+
+ private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent ev)
+ {
+ if (!IsActive || ev.InContainer)
+ return;
+
+ var iconId = JobIconForNoId;
+
+ if (_accessReader.FindAccessItemsInventory(uid, out var items))
+ {
+ foreach (var item in items)
+ {
+ // ID Card
+ if (TryComp(item, out var id))
+ {
+ iconId = id.JobIcon;
+ break;
+ }
+
+ // PDA
+ if (TryComp(item, out var pda)
+ && pda.ContainedId != null
+ && TryComp(pda.ContainedId, out id))
+ {
+ iconId = id.JobIcon;
+ break;
+ }
+ }
+ }
+
+ if (_prototype.TryIndex(iconId, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
+ else
+ Log.Error($"Invalid job icon prototype: {iconPrototype}");
+ }
+}
diff --git a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
new file mode 100644
index 0000000000..8bf39b875f
--- /dev/null
+++ b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Mindshield.Components;
+using Content.Shared.Overlays;
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Overlays;
+
+public sealed class ShowMindShieldIconsSystem : EquipmentHudSystem
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetStatusIconsEvent);
+ }
+
+ private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component, ref GetStatusIconsEvent ev)
+ {
+ if (!IsActive || ev.InContainer)
+ return;
+
+ if (_prototype.TryIndex(component.MindShieldStatusIcon.Id, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
+ }
+}
diff --git a/Content.Client/Overlays/ShowSecurityIconsSystem.cs b/Content.Client/Overlays/ShowSecurityIconsSystem.cs
deleted file mode 100644
index 7a4abd05e0..0000000000
--- a/Content.Client/Overlays/ShowSecurityIconsSystem.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using Content.Shared.Access.Components;
-using Content.Shared.Access.Systems;
-using Content.Shared.Mindshield.Components;
-using Content.Shared.Overlays;
-using Content.Shared.PDA;
-using Content.Shared.Security.Components;
-using Content.Shared.StatusIcon;
-using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Overlays;
-
-public sealed class ShowSecurityIconsSystem : EquipmentHudSystem
-{
- [Dependency] private readonly IPrototypeManager _prototypeMan = default!;
- [Dependency] private readonly AccessReaderSystem _accessReader = default!;
-
- [ValidatePrototypeId]
- private const string JobIconForNoId = "JobIconNoId";
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnGetStatusIconsEvent);
- }
-
- private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent @event)
- {
- if (!IsActive || @event.InContainer)
- {
- return;
- }
-
- var securityIcons = DecideSecurityIcon(uid);
-
- @event.StatusIcons.AddRange(securityIcons);
- }
-
- private IReadOnlyList DecideSecurityIcon(EntityUid uid)
- {
- var result = new List();
-
- var jobIconToGet = JobIconForNoId;
- if (_accessReader.FindAccessItemsInventory(uid, out var items))
- {
- foreach (var item in items)
- {
- // ID Card
- if (TryComp(item, out IdCardComponent? id))
- {
- jobIconToGet = id.JobIcon;
- break;
- }
-
- // PDA
- if (TryComp(item, out PdaComponent? pda)
- && pda.ContainedId != null
- && TryComp(pda.ContainedId, out id))
- {
- jobIconToGet = id.JobIcon;
- break;
- }
- }
- }
-
- if (_prototypeMan.TryIndex(jobIconToGet, out var jobIcon))
- result.Add(jobIcon);
- else
- Log.Error($"Invalid job icon prototype: {jobIcon}");
-
- if (TryComp(uid, out var comp))
- {
- if (_prototypeMan.TryIndex(comp.MindShieldStatusIcon.Id, out var icon))
- result.Add(icon);
- }
-
- if (TryComp(uid, out var record))
- {
- if(_prototypeMan.TryIndex(record.StatusIcon.Id, out var criminalIcon))
- result.Add(criminalIcon);
- }
-
- return result;
- }
-}
diff --git a/Content.Client/Overlays/ShowSyndicateIconsSystem.cs b/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
index a640726685..660ef198e1 100644
--- a/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
+++ b/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
@@ -1,10 +1,11 @@
using Content.Shared.Overlays;
-using Content.Shared.StatusIcon.Components;
using Content.Shared.NukeOps;
using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
+
public sealed class ShowSyndicateIconsSystem : EquipmentHudSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
@@ -16,28 +17,13 @@ public override void Initialize()
SubscribeLocalEvent(OnGetStatusIconsEvent);
}
- private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent nukeOperativeComponent, ref GetStatusIconsEvent args)
+ private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent component, ref GetStatusIconsEvent ev)
{
- if (!IsActive || args.InContainer)
- {
+ if (!IsActive || ev.InContainer)
return;
- }
-
- var syndicateIcons = SyndicateIcon(uid, nukeOperativeComponent);
-
- args.StatusIcons.AddRange(syndicateIcons);
- }
-
- private IReadOnlyList SyndicateIcon(EntityUid uid, NukeOperativeComponent nukeOperativeComponent)
- {
- var result = new List();
-
- if (_prototype.TryIndex(nukeOperativeComponent.SyndStatusIcon, out var syndicateicon))
- {
- result.Add(syndicateicon);
- }
- return result;
+ if (_prototype.TryIndex(component.SyndStatusIcon, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/ShowThirstIconsSystem.cs b/Content.Client/Overlays/ShowThirstIconsSystem.cs
index f9d6d0ab25..b08aa4340b 100644
--- a/Content.Client/Overlays/ShowThirstIconsSystem.cs
+++ b/Content.Client/Overlays/ShowThirstIconsSystem.cs
@@ -1,14 +1,13 @@
+using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Overlays;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowThirstIconsSystem : EquipmentHudSystem
{
- [Dependency] private readonly IPrototypeManager _prototypeMan = default!;
+ [Dependency] private readonly ThirstSystem _thirst = default!;
public override void Initialize()
{
@@ -17,42 +16,12 @@ public override void Initialize()
SubscribeLocalEvent(OnGetStatusIconsEvent);
}
- private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent thirstComponent, ref GetStatusIconsEvent args)
+ private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent component, ref GetStatusIconsEvent ev)
{
- if (!IsActive || args.InContainer)
+ if (!IsActive || ev.InContainer)
return;
- var thirstIcons = DecideThirstIcon(uid, thirstComponent);
-
- args.StatusIcons.AddRange(thirstIcons);
- }
-
- private IReadOnlyList DecideThirstIcon(EntityUid uid, ThirstComponent thirstComponent)
- {
- var result = new List();
-
- switch (thirstComponent.CurrentThirstThreshold)
- {
- case ThirstThreshold.OverHydrated:
- if (_prototypeMan.TryIndex("ThirstIconOverhydrated", out var overhydrated))
- {
- result.Add(overhydrated);
- }
- break;
- case ThirstThreshold.Thirsty:
- if (_prototypeMan.TryIndex("ThirstIconThirsty", out var thirsty))
- {
- result.Add(thirsty);
- }
- break;
- case ThirstThreshold.Parched:
- if (_prototypeMan.TryIndex("ThirstIconParched", out var parched))
- {
- result.Add(parched);
- }
- break;
- }
-
- return result;
+ if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype!);
}
}
diff --git a/Content.Client/Overlays/StencilOverlay.RestrictedRange.cs b/Content.Client/Overlays/StencilOverlay.RestrictedRange.cs
index 9581fec37b..d29564caa9 100644
--- a/Content.Client/Overlays/StencilOverlay.RestrictedRange.cs
+++ b/Content.Client/Overlays/StencilOverlay.RestrictedRange.cs
@@ -7,7 +7,7 @@ namespace Content.Client.Overlays;
public sealed partial class StencilOverlay
{
- private void DrawRestrictedRange(in OverlayDrawArgs args, RestrictedRangeComponent rangeComp, Matrix3 invMatrix)
+ private void DrawRestrictedRange(in OverlayDrawArgs args, RestrictedRangeComponent rangeComp, Matrix3x2 invMatrix)
{
var worldHandle = args.WorldHandle;
var renderScale = args.Viewport.RenderScale.X;
@@ -16,7 +16,7 @@ private void DrawRestrictedRange(in OverlayDrawArgs args, RestrictedRangeCompone
var length = zoom.X;
var bufferRange = MathF.Min(10f, rangeComp.Range);
- var pixelCenter = invMatrix.Transform(rangeComp.Origin);
+ var pixelCenter = Vector2.Transform(rangeComp.Origin, invMatrix);
// Something something offset?
var vertical = args.Viewport.Size.Y;
@@ -44,7 +44,7 @@ private void DrawRestrictedRange(in OverlayDrawArgs args, RestrictedRangeCompone
worldHandle.DrawRect(localAABB, Color.White);
}, Color.Transparent);
- worldHandle.SetTransform(Matrix3.Identity);
+ worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index("StencilMask").Instance());
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
diff --git a/Content.Client/Overlays/StencilOverlay.Weather.cs b/Content.Client/Overlays/StencilOverlay.Weather.cs
index 31bc88af45..29ed157a79 100644
--- a/Content.Client/Overlays/StencilOverlay.Weather.cs
+++ b/Content.Client/Overlays/StencilOverlay.Weather.cs
@@ -10,7 +10,7 @@ public sealed partial class StencilOverlay
{
private List> _grids = new();
- private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha, Matrix3 invMatrix)
+ private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha, Matrix3x2 invMatrix)
{
var worldHandle = args.WorldHandle;
var mapId = args.MapId;
@@ -32,7 +32,7 @@ private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto,
foreach (var grid in _grids)
{
var matrix = _transform.GetWorldMatrix(grid, xformQuery);
- Matrix3.Multiply(in matrix, in invMatrix, out var matty);
+ var matty = Matrix3x2.Multiply(matrix, invMatrix);
worldHandle.SetTransform(matty);
foreach (var tile in grid.Comp.GetTilesIntersecting(worldAABB))
@@ -52,7 +52,7 @@ private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto,
}, Color.Transparent);
- worldHandle.SetTransform(Matrix3.Identity);
+ worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index("StencilMask").Instance());
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
@@ -62,7 +62,7 @@ private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto,
worldHandle.UseShader(_protoManager.Index("StencilDraw").Instance());
_parallax.DrawParallax(worldHandle, worldAABB, sprite, curTime, position, Vector2.Zero, modulate: (weatherProto.Color ?? Color.White).WithAlpha(alpha));
- worldHandle.SetTransform(Matrix3.Identity);
+ worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(null);
}
}
diff --git a/Content.Client/Overlays/StencilOverlay.cs b/Content.Client/Overlays/StencilOverlay.cs
index e475dca759..78b1c4d2b1 100644
--- a/Content.Client/Overlays/StencilOverlay.cs
+++ b/Content.Client/Overlays/StencilOverlay.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Content.Client.Parallax;
using Content.Client.Weather;
using Content.Shared.Salvage;
@@ -72,6 +73,6 @@ protected override void Draw(in OverlayDrawArgs args)
}
args.WorldHandle.UseShader(null);
- args.WorldHandle.SetTransform(Matrix3.Identity);
+ args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
}
diff --git a/Content.Client/Overlays/UltraVisionOverlay.cs b/Content.Client/Overlays/UltraVisionOverlay.cs
new file mode 100644
index 0000000000..a12aa94ea8
--- /dev/null
+++ b/Content.Client/Overlays/UltraVisionOverlay.cs
@@ -0,0 +1,50 @@
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Content.Shared.Traits.Assorted.Components;
+
+namespace Content.Client.Overlays;
+
+public sealed partial class UltraVisionOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] IEntityManager _entityManager = default!;
+
+
+ public override bool RequestScreenTexture => true;
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ private readonly ShaderInstance _ultraVisionShader;
+
+ public UltraVisionOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _ultraVisionShader = _prototypeManager.Index("UltraVision").Instance().Duplicate();
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (_playerManager.LocalEntity is not { Valid: true } player
+ || !_entityManager.HasComponent(player))
+ return false;
+
+ return base.BeforeDraw(in args);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture is null)
+ return;
+
+ _ultraVisionShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+
+ var worldHandle = args.WorldHandle;
+ var viewport = args.WorldBounds;
+ worldHandle.SetTransform(Matrix3x2.Identity);
+ worldHandle.UseShader(_ultraVisionShader);
+ worldHandle.DrawRect(viewport, Color.White);
+ worldHandle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Overlays/UltraVisionSystem.cs b/Content.Client/Overlays/UltraVisionSystem.cs
new file mode 100644
index 0000000000..e8e6cdfa72
--- /dev/null
+++ b/Content.Client/Overlays/UltraVisionSystem.cs
@@ -0,0 +1,66 @@
+using Content.Shared.Traits.Assorted.Components;
+using Content.Shared.CCVar;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
+
+namespace Content.Client.Overlays;
+
+public sealed partial class UltraVisionSystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerMan = default!;
+
+ private UltraVisionOverlay _overlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUltraVisionInit);
+ SubscribeLocalEvent(OnUltraVisionShutdown);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ Subs.CVar(_cfg, CCVars.NoVisionFilters, OnNoVisionFiltersChanged);
+
+ _overlay = new();
+ }
+
+ private void OnUltraVisionInit(EntityUid uid, UltraVisionComponent component, ComponentInit args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnUltraVisionShutdown(EntityUid uid, UltraVisionComponent component, ComponentShutdown args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, UltraVisionComponent component, LocalPlayerAttachedEvent args)
+ {
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnPlayerDetached(EntityUid uid, UltraVisionComponent component, LocalPlayerDetachedEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnNoVisionFiltersChanged(bool enabled)
+ {
+ if (enabled)
+ _overlayMan.RemoveOverlay(_overlay);
+ else
+ _overlayMan.AddOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/PDA/PdaBoundUserInterface.cs b/Content.Client/PDA/PdaBoundUserInterface.cs
index ef9d6e8b9b..07352b512b 100644
--- a/Content.Client/PDA/PdaBoundUserInterface.cs
+++ b/Content.Client/PDA/PdaBoundUserInterface.cs
@@ -21,7 +21,6 @@ public PdaBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
protected override void Open()
{
base.Open();
- SendMessage(new PdaRequestUpdateInterfaceMessage());
_menu = new PdaMenu();
_menu.OpenCenteredLeft();
_menu.OnClose += Close;
@@ -32,17 +31,17 @@ protected override void Open()
_menu.EjectIdButton.OnPressed += _ =>
{
- SendMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaIdSlotId));
+ SendPredictedMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaIdSlotId));
};
_menu.EjectPenButton.OnPressed += _ =>
{
- SendMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaPenSlotId));
+ SendPredictedMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaPenSlotId));
};
_menu.EjectPaiButton.OnPressed += _ =>
{
- SendMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaPaiSlotId));
+ SendPredictedMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaPaiSlotId));
};
_menu.ActivateMusicButton.OnPressed += _ =>
diff --git a/Content.Client/Paint/PaintVisualizerSystem.cs b/Content.Client/Paint/PaintVisualizerSystem.cs
new file mode 100644
index 0000000000..a00314cd68
--- /dev/null
+++ b/Content.Client/Paint/PaintVisualizerSystem.cs
@@ -0,0 +1,101 @@
+using Robust.Client.GameObjects;
+using static Robust.Client.GameObjects.SpriteComponent;
+using Content.Shared.Clothing;
+using Content.Shared.Hands;
+using Content.Shared.Paint;
+using Robust.Client.Graphics;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Paint;
+
+public sealed class PaintedVisualizerSystem : VisualizerSystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnHeldVisualsUpdated);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnEquipmentVisualsUpdated);
+ }
+
+
+ protected override void OnAppearanceChange(EntityUid uid, PaintedComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null
+ || !_appearance.TryGetData(uid, PaintVisuals.Painted, out bool isPainted))
+ return;
+
+ var shader = _protoMan.Index(component.ShaderName).Instance();
+ foreach (var spriteLayer in args.Sprite.AllLayers)
+ {
+ if (spriteLayer is not Layer layer)
+ continue;
+
+ if (layer.Shader == null || layer.Shader == shader)
+ {
+ layer.Shader = shader;
+ layer.Color = component.Color;
+ }
+ }
+ }
+
+ private void OnShutdown(EntityUid uid, PaintedComponent component, ref ComponentShutdown args)
+ {
+ if (!TryComp(uid, out SpriteComponent? sprite))
+ return;
+ component.BeforeColor = sprite.Color;
+
+ if (Terminating(uid))
+ return;
+
+ foreach (var spriteLayer in sprite.AllLayers)
+ {
+ if (spriteLayer is not Layer layer
+ || layer.Shader != _protoMan.Index(component.ShaderName).Instance())
+ continue;
+
+ layer.Shader = null;
+ if (layer.Color == component.Color)
+ layer.Color = component.BeforeColor;
+ }
+ }
+
+ private void OnHeldVisualsUpdated(EntityUid uid, PaintedComponent component, HeldVisualsUpdatedEvent args) =>
+ UpdateVisuals(component, args);
+ private void OnEquipmentVisualsUpdated(EntityUid uid, PaintedComponent component, EquipmentVisualsUpdatedEvent args) =>
+ UpdateVisuals(component, args);
+ private void UpdateVisuals(PaintedComponent component, EntityEventArgs args)
+ {
+ var layers = new HashSet();
+ var entity = EntityUid.Invalid;
+
+ switch (args)
+ {
+ case HeldVisualsUpdatedEvent hgs:
+ layers = hgs.RevealedLayers;
+ entity = hgs.User;
+ break;
+ case EquipmentVisualsUpdatedEvent eqs:
+ layers = eqs.RevealedLayers;
+ entity = eqs.Equipee;
+ break;
+ }
+
+ if (layers.Count == 0 || !TryComp(entity, out SpriteComponent? sprite))
+ return;
+
+ foreach (var revealed in layers)
+ {
+ if (!sprite.LayerMapTryGet(revealed, out var layer))
+ continue;
+
+ sprite.LayerSetShader(layer, component.ShaderName);
+ sprite.LayerSetColor(layer, component.Color);
+ }
+ }
+}
diff --git a/Content.Client/Paper/PaperComponent.cs b/Content.Client/Paper/PaperComponent.cs
index d197cd3721..1dc827bf7e 100644
--- a/Content.Client/Paper/PaperComponent.cs
+++ b/Content.Client/Paper/PaperComponent.cs
@@ -1,9 +1,6 @@
using Content.Shared.Paper;
-using Robust.Shared.GameStates;
namespace Content.Client.Paper;
-[NetworkedComponent, RegisterComponent]
-public sealed partial class PaperComponent : SharedPaperComponent
-{
-}
+[RegisterComponent]
+public sealed partial class PaperComponent : SharedPaperComponent;
diff --git a/Content.Client/Paper/UI/StampLabel.xaml.cs b/Content.Client/Paper/UI/StampLabel.xaml.cs
index 6a8eb5f98f..be6d52baea 100644
--- a/Content.Client/Paper/UI/StampLabel.xaml.cs
+++ b/Content.Client/Paper/UI/StampLabel.xaml.cs
@@ -50,7 +50,7 @@ protected override void Draw(DrawingHandleScreen handle)
base.Draw(handle);
// Restore a sane transform+shader
- handle.SetTransform(Matrix3.Identity);
+ handle.SetTransform(Matrix3x2.Identity);
handle.UseShader(null);
}
}
diff --git a/Content.Client/Paper/UI/StampWidget.xaml.cs b/Content.Client/Paper/UI/StampWidget.xaml.cs
index a04508aeba..487e0732b4 100644
--- a/Content.Client/Paper/UI/StampWidget.xaml.cs
+++ b/Content.Client/Paper/UI/StampWidget.xaml.cs
@@ -53,7 +53,7 @@ protected override void Draw(DrawingHandleScreen handle)
base.Draw(handle);
// Restore a sane transform+shader
- handle.SetTransform(Matrix3.Identity);
+ handle.SetTransform(Matrix3x2.Identity);
handle.UseShader(null);
}
}
diff --git a/Content.Client/Parkstation/Overlays/Shaders/NearsightedOverlays.cs b/Content.Client/Parkstation/Overlays/Shaders/NearsightedOverlays.cs
deleted file mode 100644
index 8235d86792..0000000000
--- a/Content.Client/Parkstation/Overlays/Shaders/NearsightedOverlays.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using Content.Shared.Parkstation.Traits.Components;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.Player;
-using Robust.Shared.Enums;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Timing;
-
-namespace Content.Client.Parkstation.Overlays.Shaders;
-
-public sealed class NearsightedOverlay : Overlay
-{
- [Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
-
- public override OverlaySpace Space => OverlaySpace.WorldSpace;
- private readonly ShaderInstance _nearsightShader;
-
- public float Radius;
- private float _oldRadius;
- public float Darkness;
- private float _oldDarkness;
-
- private float _lerpTime;
- public float LerpDuration;
-
-
- public NearsightedOverlay()
- {
- IoCManager.InjectDependencies(this);
- _nearsightShader = _prototypeManager.Index("GradientCircleMask").InstanceUnique();
- }
-
- protected override bool BeforeDraw(in OverlayDrawArgs args)
- {
- // Check if the player has a NearsightedComponent and is controlling it
- if (!_entityManager.TryGetComponent(_playerManager.LocalPlayer?.ControlledEntity, out NearsightedComponent? nearComp) ||
- _playerManager.LocalPlayer?.ControlledEntity != nearComp.Owner)
- return false;
-
- // Check if the player has an EyeComponent and if the overlay should be drawn for this eye
- if (!_entityManager.TryGetComponent(_playerManager.LocalPlayer?.ControlledEntity, out EyeComponent? eyeComp) ||
- args.Viewport.Eye != eyeComp.Eye)
- return false;
-
- return true;
- }
-
- protected override void Draw(in OverlayDrawArgs args)
- {
- // We already checked if they have a NearsightedComponent and are controlling it in BeforeDraw, so we assume this hasn't changed
- var nearComp = _entityManager.GetComponent(_playerManager.LocalPlayer!.ControlledEntity!.Value);
-
- // Set LerpDuration based on nearComp.LerpDuration
- LerpDuration = nearComp.LerpDuration;
-
- // Set the radius and darkness values based on whether the player is wearing glasses or not
- if (nearComp.Active)
- {
- Radius = nearComp.EquippedRadius;
- Darkness = nearComp.EquippedAlpha;
- }
- else
- {
- Radius = nearComp.Radius;
- Darkness = nearComp.Alpha;
- }
-
-
- var viewport = args.WorldAABB;
- var handle = args.WorldHandle;
- var distance = args.ViewportBounds.Width;
-
- var lastFrameTime = (float) _timing.FrameTime.TotalSeconds;
-
-
- // If the current radius value is different from the previous one, lerp between them
- if (!MathHelper.CloseTo(_oldRadius, Radius, 0.001f))
- {
- _lerpTime += lastFrameTime;
- var t = MathHelper.Clamp(_lerpTime / LerpDuration, 0f, 1f); // Calculate lerp time
- _oldRadius = MathHelper.Lerp(_oldRadius, Radius, t); // Lerp between old and new radius values
- }
- // If the current radius value is the same as the previous one, reset the lerp time and old radius value
- else
- {
- _lerpTime = 0f;
- _oldRadius = Radius;
- }
-
- // If the current darkness value is different from the previous one, lerp between them
- if (!MathHelper.CloseTo(_oldDarkness, Darkness, 0.001f))
- {
- _lerpTime += lastFrameTime;
- var t = MathHelper.Clamp(_lerpTime / LerpDuration, 0f, 1f); // Calculate lerp time
- _oldDarkness = MathHelper.Lerp(_oldDarkness, Darkness, t); // Lerp between old and new darkness values
- }
- // If the current darkness value is the same as the previous one, reset the lerp time and old darkness value
- else
- {
- _lerpTime = 0f;
- _oldDarkness = Darkness;
- }
-
-
- // Calculate the outer and inner radii based on the current radius value
- var outerMaxLevel = 0.6f * distance;
- var outerMinLevel = 0.06f * distance;
- var innerMaxLevel = 0.02f * distance;
- var innerMinLevel = 0.02f * distance;
-
- var outerRadius = outerMaxLevel - _oldRadius * (outerMaxLevel - outerMinLevel);
- var innerRadius = innerMaxLevel - _oldRadius * (innerMaxLevel - innerMinLevel);
-
- // Set the shader parameters and draw the overlay
- _nearsightShader.SetParameter("time", 0.0f);
- _nearsightShader.SetParameter("color", new Vector3(1f, 1f, 1f));
- _nearsightShader.SetParameter("darknessAlphaOuter", _oldDarkness);
- _nearsightShader.SetParameter("innerCircleRadius", innerRadius);
- _nearsightShader.SetParameter("innerCircleMaxRadius", innerRadius);
- _nearsightShader.SetParameter("outerCircleRadius", outerRadius);
- _nearsightShader.SetParameter("outerCircleMaxRadius", outerRadius + 0.2f * distance);
- handle.UseShader(_nearsightShader);
- handle.DrawRect(viewport, Color.Black);
-
- handle.UseShader(null);
- }
-}
diff --git a/Content.Client/Parkstation/Overlays/Systems/NearsightedSystems.cs b/Content.Client/Parkstation/Overlays/Systems/NearsightedSystems.cs
deleted file mode 100644
index c42d7866c4..0000000000
--- a/Content.Client/Parkstation/Overlays/Systems/NearsightedSystems.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using Content.Client.Parkstation.Overlays.Shaders;
-using Content.Shared.Inventory.Events;
-using Content.Shared.Parkstation.Traits;
-using Content.Shared.Parkstation.Traits.Components;
-using Content.Shared.Tag;
-using Robust.Client.Graphics;
-
-namespace Content.Client.Parkstation.Overlays.Systems;
-
-public sealed class NearsightedSystem : EntitySystem
-{
- [Dependency] private readonly IOverlayManager _overlayMan = default!;
-
- private NearsightedOverlay _overlay = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- _overlay = new NearsightedOverlay();
-
- SubscribeLocalEvent(OnStartup);
- SubscribeLocalEvent(OnEquip);
- SubscribeLocalEvent(OnUnEquip);
- }
-
-
- private void OnStartup(EntityUid uid, NearsightedComponent component, ComponentStartup args)
- {
- UpdateShader(component, false);
- }
-
- private void OnEquip(GotEquippedEvent args)
- {
- // Note: it would be cleaner to check if the glasses are being equipped
- // to the eyes rather than the pockets using `args.SlotFlags.HasFlag(SlotFlags.EYES)`,
- // but this field is not present on GotUnequippedEvent. This method is
- // used for both equip and unequip to make it consistent between checks.
- if (TryComp(args.Equipee, out var nearsighted) &&
- EnsureComp(args.Equipment).Tags.Contains("GlassesNearsight") &&
- args.Slot == "eyes")
- UpdateShader(nearsighted, true);
- }
-
- private void OnUnEquip(GotUnequippedEvent args)
- {
- if (TryComp(args.Equipee, out var nearsighted) &&
- EnsureComp(args.Equipment).Tags.Contains("GlassesNearsight") &&
- args.Slot == "eyes")
- UpdateShader(nearsighted, false);
- }
-
-
- private void UpdateShader(NearsightedComponent component, bool booLean)
- {
- while (_overlayMan.HasOverlay())
- {
- _overlayMan.RemoveOverlay(_overlay);
- }
-
- component.Active = booLean;
- _overlayMan.AddOverlay(_overlay);
- }
-}
diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs
index 31042854d4..9d453e5518 100644
--- a/Content.Client/Physics/Controllers/MoverController.cs
+++ b/Content.Client/Physics/Controllers/MoverController.cs
@@ -1,9 +1,12 @@
+using Content.Shared.CCVar;
using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Physics;
using Robust.Client.Player;
+using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -12,6 +15,7 @@ namespace Content.Client.Physics.Controllers
{
public sealed class MoverController : SharedMoverController
{
+ [Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -26,6 +30,8 @@ public override void Initialize()
SubscribeLocalEvent(OnUpdatePredicted);
SubscribeLocalEvent(OnUpdateRelayTargetPredicted);
SubscribeLocalEvent(OnUpdatePullablePredicted);
+
+ Subs.CVar(_config, CCVars.DefaultWalk, _ => RaiseNetworkEvent(new UpdateInputCVarsMessage()));
}
private void OnUpdatePredicted(EntityUid uid, InputMoverComponent component, ref UpdateIsPredictedEvent args)
diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs
index bd7dfc1117..9aeb792a42 100644
--- a/Content.Client/Pinpointer/NavMapSystem.cs
+++ b/Content.Client/Pinpointer/NavMapSystem.cs
@@ -1,109 +1,63 @@
-using System.Numerics;
using Content.Shared.Pinpointer;
-using Robust.Client.Graphics;
-using Robust.Shared.Enums;
using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
namespace Content.Client.Pinpointer;
-public sealed class NavMapSystem : SharedNavMapSystem
+public sealed partial class NavMapSystem : SharedNavMapSystem
{
public override void Initialize()
{
base.Initialize();
+
SubscribeLocalEvent(OnHandleState);
}
private void OnHandleState(EntityUid uid, NavMapComponent component, ref ComponentHandleState args)
{
- if (args.Current is not NavMapComponentState state)
- return;
-
- component.Chunks.Clear();
+ Dictionary modifiedChunks;
+ Dictionary beacons;
- foreach (var (origin, data) in state.TileData)
+ switch (args.Current)
{
- component.Chunks.Add(origin, new NavMapChunk(origin)
+ case NavMapDeltaState delta:
{
- TileData = data,
- });
- }
-
- component.Beacons.Clear();
- component.Beacons.AddRange(state.Beacons);
-
- component.Airlocks.Clear();
- component.Airlocks.AddRange(state.Airlocks);
- }
-}
-
-public sealed class NavMapOverlay : Overlay
-{
- private readonly IEntityManager _entManager;
- private readonly IMapManager _mapManager;
-
- public override OverlaySpace Space => OverlaySpace.WorldSpace;
-
- private List> _grids = new();
-
- public NavMapOverlay(IEntityManager entManager, IMapManager mapManager)
- {
- _entManager = entManager;
- _mapManager = mapManager;
- }
-
- protected override void Draw(in OverlayDrawArgs args)
- {
- var query = _entManager.GetEntityQuery();
- var xformQuery = _entManager.GetEntityQuery();
- var scale = Matrix3.CreateScale(new Vector2(1f, 1f));
-
- _grids.Clear();
- _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
-
- foreach (var grid in _grids)
- {
- if (!query.TryGetComponent(grid, out var navMap) || !xformQuery.TryGetComponent(grid.Owner, out var xform))
- continue;
-
- // TODO: Faster helper method
- var (_, _, matrix, invMatrix) = xform.GetWorldPositionRotationMatrixWithInv();
-
- var localAABB = invMatrix.TransformBox(args.WorldBounds);
- Matrix3.Multiply(in scale, in matrix, out var matty);
-
- args.WorldHandle.SetTransform(matty);
+ modifiedChunks = delta.ModifiedChunks;
+ beacons = delta.Beacons;
+ foreach (var index in component.Chunks.Keys)
+ {
+ if (!delta.AllChunks!.Contains(index))
+ component.Chunks.Remove(index);
+ }
- for (var x = Math.Floor(localAABB.Left); x <= Math.Ceiling(localAABB.Right); x += SharedNavMapSystem.ChunkSize * grid.Comp.TileSize)
+ break;
+ }
+ case NavMapState state:
{
- for (var y = Math.Floor(localAABB.Bottom); y <= Math.Ceiling(localAABB.Top); y += SharedNavMapSystem.ChunkSize * grid.Comp.TileSize)
+ modifiedChunks = state.Chunks;
+ beacons = state.Beacons;
+ foreach (var index in component.Chunks.Keys)
{
- var floored = new Vector2i((int) x, (int) y);
-
- var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
-
- if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
- continue;
-
- // TODO: Okay maybe I should just use ushorts lmao...
- for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
- {
- var value = (int) Math.Pow(2, i);
-
- var mask = chunk.TileData & value;
-
- if (mask == 0x0)
- continue;
-
- var tile = chunk.Origin * SharedNavMapSystem.ChunkSize + SharedNavMapSystem.GetTile(mask);
- args.WorldHandle.DrawRect(new Box2(tile * grid.Comp.TileSize, (tile + 1) * grid.Comp.TileSize), Color.Aqua, false);
- }
+ if (!state.Chunks.ContainsKey(index))
+ component.Chunks.Remove(index);
}
+
+ break;
}
+ default:
+ return;
}
- args.WorldHandle.SetTransform(Matrix3.Identity);
+ foreach (var (origin, chunk) in modifiedChunks)
+ {
+ var newChunk = new NavMapChunk(origin);
+ Array.Copy(chunk, newChunk.TileData, chunk.Length);
+ component.Chunks[origin] = newChunk;
+ }
+
+ component.Beacons.Clear();
+ foreach (var (nuid, beacon) in beacons)
+ {
+ component.Beacons[nuid] = beacon;
+ }
}
}
diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs
index 677092e191..3c99a18818 100644
--- a/Content.Client/Pinpointer/UI/NavMapControl.cs
+++ b/Content.Client/Pinpointer/UI/NavMapControl.cs
@@ -16,6 +16,9 @@
using Robust.Shared.Timing;
using System.Numerics;
using JetBrains.Annotations;
+using Content.Shared.Atmos;
+using System.Linq;
+using Robust.Shared.Utility;
namespace Content.Client.Pinpointer.UI;
@@ -27,6 +30,7 @@ public partial class NavMapControl : MapGridControl
{
[Dependency] private IResourceCache _cache = default!;
private readonly SharedTransformSystem _transformSystem;
+ private readonly SharedNavMapSystem _navMapSystem;
public EntityUid? Owner;
public EntityUid? MapUid;
@@ -40,7 +44,10 @@ public partial class NavMapControl : MapGridControl
// Tracked data
public Dictionary TrackedCoordinates = new();
public Dictionary TrackedEntities = new();
- public Dictionary>? TileGrid = default!;
+
+ public List<(Vector2, Vector2)> TileLines = new();
+ public List<(Vector2, Vector2)> TileRects = new();
+ public List<(Vector2[], Color)> TilePolygons = new();
// Default colors
public Color WallColor = new(102, 217, 102);
@@ -53,14 +60,23 @@ public partial class NavMapControl : MapGridControl
protected static float MinDisplayedRange = 8f;
protected static float MaxDisplayedRange = 128f;
protected static float DefaultDisplayedRange = 48f;
+ protected float MinmapScaleModifier = 0.075f;
+ protected float FullWallInstep = 0.165f;
+ protected float ThinWallThickness = 0.165f;
+ protected float ThinDoorThickness = 0.30f;
// Local variables
- private float _updateTimer = 0.25f;
+ private float _updateTimer = 1.0f;
private Dictionary _sRGBLookUp = new();
protected Color BackgroundColor;
protected float BackgroundOpacity = 0.9f;
private int _targetFontsize = 8;
+ private Dictionary _horizLines = new();
+ private Dictionary _horizLinesReversed = new();
+ private Dictionary _vertLines = new();
+ private Dictionary _vertLinesReversed = new();
+
// Components
private NavMapComponent? _navMap;
private MapGridComponent? _grid;
@@ -72,6 +88,7 @@ public partial class NavMapControl : MapGridControl
private readonly Label _zoom = new()
{
VerticalAlignment = VAlignment.Top,
+ HorizontalExpand = true,
Margin = new Thickness(8f, 8f),
};
@@ -80,6 +97,7 @@ public partial class NavMapControl : MapGridControl
Text = Loc.GetString("navmap-recenter"),
VerticalAlignment = VAlignment.Top,
HorizontalAlignment = HAlignment.Right,
+ HorizontalExpand = true,
Margin = new Thickness(8f, 4f),
Disabled = true,
};
@@ -87,9 +105,10 @@ public partial class NavMapControl : MapGridControl
private readonly CheckBox _beacons = new()
{
Text = Loc.GetString("navmap-toggle-beacons"),
- Margin = new Thickness(4f, 0f),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
+ HorizontalExpand = true,
+ Margin = new Thickness(4f, 0f),
Pressed = true,
};
@@ -98,6 +117,8 @@ public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDispl
IoCManager.InjectDependencies(this);
_transformSystem = EntManager.System();
+ _navMapSystem = EntManager.System();
+
BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
RectClipContent = true;
@@ -112,6 +133,8 @@ public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDispl
BorderColor = StyleNano.PanelDark
},
VerticalExpand = false,
+ HorizontalExpand = true,
+ SetWidth = 650f,
Children =
{
new BoxContainer()
@@ -130,6 +153,7 @@ public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDispl
var topContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
+ HorizontalExpand = true,
Children =
{
topPanel,
@@ -157,6 +181,9 @@ public void ForceNavMapUpdate()
{
EntManager.TryGetComponent(MapUid, out _navMap);
EntManager.TryGetComponent(MapUid, out _grid);
+ EntManager.TryGetComponent(MapUid, out _xform);
+ EntManager.TryGetComponent(MapUid, out _physics);
+ EntManager.TryGetComponent(MapUid, out _fixtures);
UpdateNavMap();
}
@@ -191,7 +218,7 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args)
// Convert to a world position
var unscaledPosition = (localPosition - MidPointVector) / MinimapScale;
- var worldPosition = _transformSystem.GetWorldMatrix(_xform).Transform(new Vector2(unscaledPosition.X, -unscaledPosition.Y) + offset);
+ var worldPosition = Vector2.Transform(new Vector2(unscaledPosition.X, -unscaledPosition.Y) + offset, _transformSystem.GetWorldMatrix(_xform));
// Find closest tracked entity in range
var closestEntity = NetEntity.Invalid;
@@ -251,119 +278,93 @@ protected override void Draw(DrawingHandleScreen handle)
EntManager.TryGetComponent(MapUid, out _physics);
EntManager.TryGetComponent(MapUid, out _fixtures);
+ if (_navMap == null || _grid == null || _xform == null)
+ return;
+
// Map re-centering
_recenter.Disabled = DrawRecenter();
- _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange ):0.0}"));
-
- if (_navMap == null || _xform == null)
- return;
+ // Update zoom text
+ _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange):0.0}"));
+ // Update offset with physics local center
var offset = Offset;
if (_physics != null)
offset += _physics.LocalCenter;
- // Draw tiles
- if (_fixtures != null)
+ var offsetVec = new Vector2(offset.X, -offset.Y);
+
+ // Wall sRGB
+ if (!_sRGBLookUp.TryGetValue(WallColor, out var wallsRGB))
+ {
+ wallsRGB = Color.ToSrgb(WallColor);
+ _sRGBLookUp[WallColor] = wallsRGB;
+ }
+
+ // Draw floor tiles
+ if (TilePolygons.Any())
{
Span verts = new Vector2[8];
- foreach (var fixture in _fixtures.Fixtures.Values)
+ foreach (var (polygonVerts, polygonColor) in TilePolygons)
{
- if (fixture.Shape is not PolygonShape poly)
- continue;
-
- for (var i = 0; i < poly.VertexCount; i++)
+ for (var i = 0; i < polygonVerts.Length; i++)
{
- var vert = poly.Vertices[i] - offset;
-
+ var vert = polygonVerts[i] - offset;
verts[i] = ScalePosition(new Vector2(vert.X, -vert.Y));
}
- handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..poly.VertexCount], TileColor);
+ handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..polygonVerts.Length], polygonColor);
}
}
- var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
-
- // Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order
- // to figure out where they should be drawn. However, we don't *need* to do check these every frame.
- // Instead, lets periodically update where to draw each line and then store these points in a list.
- // Then we can just run through the list each frame and draw the lines without any extra computation.
-
- // Draw walls
- if (TileGrid != null && TileGrid.Count > 0)
+ // Draw map lines
+ if (TileLines.Any())
{
- var walls = new ValueList();
+ var lines = new ValueList(TileLines.Count * 2);
- foreach ((var chunk, var chunkedLines) in TileGrid)
+ foreach (var (o, t) in TileLines)
{
- var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
+ var origin = ScalePosition(o - offsetVec);
+ var terminus = ScalePosition(t - offsetVec);
- if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
- continue;
-
- if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
- continue;
-
- foreach (var chunkedLine in chunkedLines)
- {
- var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
- var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
-
- walls.Add(start);
- walls.Add(end);
- }
+ lines.Add(origin);
+ lines.Add(terminus);
}
- if (walls.Count > 0)
- {
- if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
- {
- sRGB = Color.ToSrgb(WallColor);
- _sRGBLookUp[WallColor] = sRGB;
- }
-
- handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB);
- }
+ if (lines.Count > 0)
+ handle.DrawPrimitives(DrawPrimitiveTopology.LineList, lines.Span, wallsRGB);
}
- var airlockBuffer = Vector2.One * (MinimapScale / 2.25f) * 0.75f;
- var airlockLines = new ValueList();
- var foobarVec = new Vector2(1, -1);
-
- foreach (var airlock in _navMap.Airlocks)
+ // Draw map rects
+ if (TileRects.Any())
{
- var position = airlock.Position - offset;
- position = ScalePosition(position with { Y = -position.Y });
- airlockLines.Add(position + airlockBuffer);
- airlockLines.Add(position - airlockBuffer * foobarVec);
-
- airlockLines.Add(position + airlockBuffer);
- airlockLines.Add(position + airlockBuffer * foobarVec);
-
- airlockLines.Add(position - airlockBuffer);
- airlockLines.Add(position + airlockBuffer * foobarVec);
-
- airlockLines.Add(position - airlockBuffer);
- airlockLines.Add(position - airlockBuffer * foobarVec);
+ var rects = new ValueList(TileRects.Count * 8);
- airlockLines.Add(position + airlockBuffer * -Vector2.UnitY);
- airlockLines.Add(position - airlockBuffer * -Vector2.UnitY);
- }
-
- if (airlockLines.Count > 0)
- {
- if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
+ foreach (var (lt, rb) in TileRects)
{
- sRGB = Color.ToSrgb(WallColor);
- _sRGBLookUp[WallColor] = sRGB;
+ var leftTop = ScalePosition(lt - offsetVec);
+ var rightBottom = ScalePosition(rb - offsetVec);
+
+ var rightTop = new Vector2(rightBottom.X, leftTop.Y);
+ var leftBottom = new Vector2(leftTop.X, rightBottom.Y);
+
+ rects.Add(leftTop);
+ rects.Add(rightTop);
+ rects.Add(rightTop);
+ rects.Add(rightBottom);
+ rects.Add(rightBottom);
+ rects.Add(leftBottom);
+ rects.Add(leftBottom);
+ rects.Add(leftTop);
}
- handle.DrawPrimitives(DrawPrimitiveTopology.LineList, airlockLines.Span, sRGB);
+ if (rects.Count > 0)
+ handle.DrawPrimitives(DrawPrimitiveTopology.LineList, rects.Span, wallsRGB);
}
+ // Invoke post wall drawing action
if (PostWallDrawingAction != null)
PostWallDrawingAction.Invoke(handle);
@@ -373,10 +374,10 @@ protected override void Draw(DrawingHandleScreen handle)
var rectBuffer = new Vector2(5f, 3f);
// Calculate font size for current zoom level
- var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize , 0);
+ var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
var font = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
- foreach (var beacon in _navMap.Beacons)
+ foreach (var beacon in _navMap.Beacons.Values)
{
var position = beacon.Position - offset;
position = ScalePosition(position with { Y = -position.Y });
@@ -400,7 +401,7 @@ protected override void Draw(DrawingHandleScreen handle)
if (mapPos.MapId != MapId.Nullspace)
{
- var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
+ var position = Vector2.Transform(mapPos.Position, _transformSystem.GetInvWorldMatrix(_xform)) - offset;
position = ScalePosition(new Vector2(position.X, -position.Y));
handle.DrawCircle(position, float.Sqrt(MinimapScale) * 2f, value.Color);
@@ -409,8 +410,6 @@ protected override void Draw(DrawingHandleScreen handle)
}
// Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually)
- var iconVertexUVs = new Dictionary<(Texture, Color), ValueList>();
-
foreach (var blip in TrackedEntities.Values)
{
if (blip.Blinks && !lit)
@@ -419,39 +418,18 @@ protected override void Draw(DrawingHandleScreen handle)
if (blip.Texture == null)
continue;
- if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs))
- vertexUVs = new();
-
var mapPos = blip.Coordinates.ToMap(EntManager, _transformSystem);
if (mapPos.MapId != MapId.Nullspace)
{
- var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
+ var position = Vector2.Transform(mapPos.Position, _transformSystem.GetInvWorldMatrix(_xform)) - offset;
position = ScalePosition(new Vector2(position.X, -position.Y));
- var scalingCoefficient = 2.5f;
- var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale);
-
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f)));
- }
-
- iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs;
- }
+ var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
+ var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height);
- foreach ((var (texture, color), var vertexUVs) in iconVertexUVs)
- {
- if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
- {
- sRGB = Color.ToSrgb(color);
- _sRGBLookUp[color] = sRGB;
+ handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color);
}
-
- handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB);
}
}
@@ -470,123 +448,265 @@ protected override void FrameUpdate(FrameEventArgs args)
protected virtual void UpdateNavMap()
{
- if (_navMap == null || _grid == null)
+ // Clear stale values
+ TilePolygons.Clear();
+ TileLines.Clear();
+ TileRects.Clear();
+
+ UpdateNavMapFloorTiles();
+ UpdateNavMapWallLines();
+ UpdateNavMapAirlocks();
+ }
+
+ private void UpdateNavMapFloorTiles()
+ {
+ if (_fixtures == null)
return;
- TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid);
+ var verts = new Vector2[8];
+
+ foreach (var fixture in _fixtures.Fixtures.Values)
+ {
+ if (fixture.Shape is not PolygonShape poly)
+ continue;
+
+ for (var i = 0; i < poly.VertexCount; i++)
+ {
+ var vert = poly.Vertices[i];
+ verts[i] = new Vector2(MathF.Round(vert.X), MathF.Round(vert.Y));
+ }
+
+ TilePolygons.Add((verts[..poly.VertexCount], TileColor));
+ }
}
- public Dictionary> GetDecodedWallChunks
- (Dictionary chunks,
- MapGridComponent grid)
+ private void UpdateNavMapWallLines()
{
- var decodedOutput = new Dictionary>();
+ if (_navMap == null || _grid == null)
+ return;
- foreach ((var chunkOrigin, var chunk) in chunks)
- {
- var list = new List();
+ // We'll use the following dictionaries to combine collinear wall lines
+ _horizLines.Clear();
+ _horizLinesReversed.Clear();
+ _vertLines.Clear();
+ _vertLinesReversed.Clear();
- // TODO: Okay maybe I should just use ushorts lmao...
- for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
+ const int southMask = (int) AtmosDirection.South << (int) NavMapChunkType.Wall;
+ const int eastMask = (int) AtmosDirection.East << (int) NavMapChunkType.Wall;
+ const int westMask = (int) AtmosDirection.West << (int) NavMapChunkType.Wall;
+ const int northMask = (int) AtmosDirection.North << (int) NavMapChunkType.Wall;
+
+ foreach (var (chunkOrigin, chunk) in _navMap.Chunks)
+ {
+ for (var i = 0; i < SharedNavMapSystem.ArraySize; i++)
{
- var value = (int) Math.Pow(2, i);
+ var tileData = chunk.TileData[i] & SharedNavMapSystem.WallMask;
+ if (tileData == 0)
+ continue;
- var mask = chunk.TileData & value;
+ tileData >>= (int) NavMapChunkType.Wall;
- if (mask == 0x0)
+ var relativeTile = SharedNavMapSystem.GetTileFromIndex(i);
+ var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * _grid.TileSize;
+
+ if (tileData != SharedNavMapSystem.AllDirMask)
+ {
+ AddRectForThinWall(tileData, tile);
continue;
+ }
- // Alright now we'll work out our edges
- var relativeTile = SharedNavMapSystem.GetTile(mask);
- var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
- var position = new Vector2(tile.X, -tile.Y);
+ tile = tile with { Y = -tile.Y };
NavMapChunk? neighborChunk;
- bool neighbor;
// North edge
- if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
- {
- neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
- (neighborChunk.TileData &
- SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
- }
- else
- {
- var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
- neighbor = (chunk.TileData & flag) != 0x0;
- }
+ var neighborData = 0;
+ if (relativeTile.Y != SharedNavMapSystem.ChunkSize - 1)
+ neighborData = chunk.TileData[i+1];
+ else if (_navMap.Chunks.TryGetValue(chunkOrigin + Vector2i.Up, out neighborChunk))
+ neighborData = neighborChunk.TileData[i + 1 - SharedNavMapSystem.ChunkSize];
- if (!neighbor)
+ if ((neighborData & southMask) == 0)
{
- // Add points
- list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize)));
+ AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize),
+ tile + new Vector2i(_grid.TileSize, -_grid.TileSize), _horizLines,
+ _horizLinesReversed);
}
// East edge
- if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
- {
- neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
- (neighborChunk.TileData &
- SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
- }
- else
- {
- var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
- neighbor = (chunk.TileData & flag) != 0x0;
- }
+ neighborData = 0;
+ if (relativeTile.X != SharedNavMapSystem.ChunkSize - 1)
+ neighborData = chunk.TileData[i+SharedNavMapSystem.ChunkSize];
+ else if (_navMap.Chunks.TryGetValue(chunkOrigin + Vector2i.Right, out neighborChunk))
+ neighborData = neighborChunk.TileData[i + SharedNavMapSystem.ChunkSize - SharedNavMapSystem.ArraySize];
- if (!neighbor)
+ if ((neighborData & westMask) == 0)
{
- // Add points
- list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
+ AddOrUpdateNavMapLine(tile + new Vector2i(_grid.TileSize, -_grid.TileSize),
+ tile + new Vector2i(_grid.TileSize, 0), _vertLines, _vertLinesReversed);
}
// South edge
- if (relativeTile.Y == 0)
- {
- neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
- (neighborChunk.TileData &
- SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
- }
- else
- {
- var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1));
- neighbor = (chunk.TileData & flag) != 0x0;
- }
+ neighborData = 0;
+ if (relativeTile.Y != 0)
+ neighborData = chunk.TileData[i-1];
+ else if (_navMap.Chunks.TryGetValue(chunkOrigin + Vector2i.Down, out neighborChunk))
+ neighborData = neighborChunk.TileData[i - 1 + SharedNavMapSystem.ChunkSize];
- if (!neighbor)
+ if ((neighborData & northMask) == 0)
{
- // Add points
- list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position));
+ AddOrUpdateNavMapLine(tile, tile + new Vector2i(_grid.TileSize, 0), _horizLines,
+ _horizLinesReversed);
}
// West edge
- if (relativeTile.X == 0)
- {
- neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
- (neighborChunk.TileData &
- SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
- }
- else
+ neighborData = 0;
+ if (relativeTile.X != 0)
+ neighborData = chunk.TileData[i-SharedNavMapSystem.ChunkSize];
+ else if (_navMap.Chunks.TryGetValue(chunkOrigin + Vector2i.Left, out neighborChunk))
+ neighborData = neighborChunk.TileData[i - SharedNavMapSystem.ChunkSize + SharedNavMapSystem.ArraySize];
+
+ if ((neighborData & eastMask) == 0)
{
- var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0));
- neighbor = (chunk.TileData & flag) != 0x0;
+ AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, _vertLines,
+ _vertLinesReversed);
}
- if (!neighbor)
+ // Add a diagonal line for interiors. Unless there are a lot of double walls, there is no point combining these
+ TileLines.Add((tile + new Vector2(0, -_grid.TileSize), tile + new Vector2(_grid.TileSize, 0)));
+ }
+ }
+
+ // Record the combined lines
+ foreach (var (origin, terminal) in _horizLines)
+ {
+ TileLines.Add((origin, terminal));
+ }
+
+ foreach (var (origin, terminal) in _vertLines)
+ {
+ TileLines.Add((origin, terminal));
+ }
+ }
+
+ private void UpdateNavMapAirlocks()
+ {
+ if (_navMap == null || _grid == null)
+ return;
+
+ foreach (var chunk in _navMap.Chunks.Values)
+ {
+ for (var i = 0; i < SharedNavMapSystem.ArraySize; i++)
+ {
+ var tileData = chunk.TileData[i] & SharedNavMapSystem.AirlockMask;
+ if (tileData == 0)
+ continue;
+
+ tileData >>= (int) NavMapChunkType.Airlock;
+
+ var relative = SharedNavMapSystem.GetTileFromIndex(i);
+ var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relative) * _grid.TileSize;
+
+ // If the edges of an airlock tile are not all occupied, draw a thin airlock for each edge
+ if (tileData != SharedNavMapSystem.AllDirMask)
{
- // Add point
- list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize)));
+ AddRectForThinAirlock(tileData, tile);
+ continue;
}
- // Draw a diagonal line for interiors.
- list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
+ // Otherwise add a single full tile airlock
+ TileRects.Add((new Vector2(tile.X + FullWallInstep, -tile.Y - FullWallInstep),
+ new Vector2(tile.X - FullWallInstep + 1f, -tile.Y + FullWallInstep - 1)));
+
+ TileLines.Add((new Vector2(tile.X + 0.5f, -tile.Y - FullWallInstep),
+ new Vector2(tile.X + 0.5f, -tile.Y + FullWallInstep - 1)));
+ }
+ }
+ }
+
+ private void AddRectForThinWall(int tileData, Vector2i tile)
+ {
+ var leftTop = new Vector2(-0.5f, 0.5f - ThinWallThickness);
+ var rightBottom = new Vector2(0.5f, 0.5f);
+
+ for (var i = 0; i < SharedNavMapSystem.Directions; i++)
+ {
+ var dirMask = 1 << i;
+ if ((tileData & dirMask) == 0)
+ continue;
+
+ var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
+
+ // TODO NAVMAP
+ // Consider using faster rotation operations, given that these are always 90 degree increments
+ var angle = -((AtmosDirection) dirMask).ToAngle();
+ TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
+ }
+ }
+
+ private void AddRectForThinAirlock(int tileData, Vector2i tile)
+ {
+ var leftTop = new Vector2(-0.5f + FullWallInstep, 0.5f - FullWallInstep - ThinDoorThickness);
+ var rightBottom = new Vector2(0.5f - FullWallInstep, 0.5f - FullWallInstep);
+ var centreTop = new Vector2(0f, 0.5f - FullWallInstep - ThinDoorThickness);
+ var centreBottom = new Vector2(0f, 0.5f - FullWallInstep);
+
+ for (var i = 0; i < SharedNavMapSystem.Directions; i++)
+ {
+ var dirMask = 1 << i;
+ if ((tileData & dirMask) == 0)
+ continue;
+
+ var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
+ var angle = -((AtmosDirection) dirMask).ToAngle();
+ TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
+ TileLines.Add((angle.RotateVec(centreTop) + tilePosition, angle.RotateVec(centreBottom) + tilePosition));
+ }
+ }
+
+ protected void AddOrUpdateNavMapLine(
+ Vector2i origin,
+ Vector2i terminus,
+ Dictionary lookup,
+ Dictionary lookupReversed)
+ {
+ Vector2i foundTermius;
+ Vector2i foundOrigin;
+
+ // Does our new line end at the beginning of an existing line?
+ if (lookup.Remove(terminus, out foundTermius))
+ {
+ DebugTools.Assert(lookupReversed[foundTermius] == terminus);
+
+ // Does our new line start at the end of an existing line?
+ if (lookupReversed.Remove(origin, out foundOrigin))
+ {
+ // Our new line just connects two existing lines
+ DebugTools.Assert(lookup[foundOrigin] == origin);
+ lookup[foundOrigin] = foundTermius;
+ lookupReversed[foundTermius] = foundOrigin;
+ }
+ else
+ {
+ // Our new line precedes an existing line, extending it further to the left
+ lookup[origin] = foundTermius;
+ lookupReversed[foundTermius] = origin;
}
+ return;
+ }
- decodedOutput.Add(chunkOrigin, list);
+ // Does our new line start at the end of an existing line?
+ if (lookupReversed.Remove(origin, out foundOrigin))
+ {
+ // Our new line just extends an existing line further to the right
+ DebugTools.Assert(lookup[foundOrigin] == origin);
+ lookup[foundOrigin] = terminus;
+ lookupReversed[terminus] = foundOrigin;
+ return;
}
- return decodedOutput;
+ // Completely disconnected line segment.
+ lookup.Add(origin, terminus);
+ lookupReversed.Add(terminus, origin);
}
protected Vector2 GetOffset()
@@ -612,15 +732,3 @@ public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, b
Selectable = selectable;
}
}
-
-public struct NavMapLine
-{
- public readonly Vector2 Origin;
- public readonly Vector2 Terminus;
-
- public NavMapLine(Vector2 origin, Vector2 terminus)
- {
- Origin = origin;
- Terminus = terminus;
- }
-}
diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index a2f8061d05..286358b85e 100644
--- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
+++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.CCVar;
using Content.Shared.Customization.Systems;
+using Content.Shared.Players.JobWhitelist;
using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
@@ -18,13 +19,13 @@ public sealed partial class JobRequirementsManager : ISharedPlaytimeManager
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IClientNetManager _net = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
private readonly Dictionary _roles = new();
private readonly List _roleBans = new();
-
private ISawmill _sawmill = default!;
-
+ private readonly List _jobWhitelists = new();
public event Action? Updated;
public void Initialize()
@@ -35,6 +36,7 @@ public void Initialize()
_net.RegisterNetMessage(RxRoleBans);
_net.RegisterNetMessage(RxPlayTime);
_net.RegisterNetMessage(RxWhitelist);
+ _net.RegisterNetMessage(RxJobWhitelist);
_client.RunLevelChanged += ClientOnRunLevelChanged;
}
@@ -78,6 +80,28 @@ private void RxPlayTime(MsgPlayTime message)
Updated?.Invoke();
}
+ private void RxJobWhitelist(MsgJobWhitelist message)
+ {
+ _jobWhitelists.Clear();
+ _jobWhitelists.AddRange(message.Whitelist);
+ Updated?.Invoke();
+ }
+
+ public bool CheckJobWhitelist(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
+ {
+ reason = default;
+ if (!_cfg.GetCVar(CCVars.GameRoleWhitelist))
+ return true;
+
+ if (job.Whitelisted && !_jobWhitelists.Contains(job.ID))
+ {
+ reason = FormattedMessage.FromUnformatted(Loc.GetString("role-not-whitelisted"));
+ return false;
+ }
+
+ return true;
+ }
+
public TimeSpan FetchOverallPlaytime()
{
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs
new file mode 100644
index 0000000000..5ba4878c6d
--- /dev/null
+++ b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Polymorph.Components;
+using Content.Shared.Polymorph.Systems;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Polymorph.Systems;
+
+public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ private EntityQuery _appearanceQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _appearanceQuery = GetEntityQuery();
+
+ SubscribeLocalEvent(OnHandleState);
+ }
+
+ private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ CopyComp(ent);
+ CopyComp(ent);
+ CopyComp(ent);
+
+ // reload appearance to hopefully prevent any invisible layers
+ if (_appearanceQuery.TryComp(ent, out var appearance))
+ _appearance.QueueUpdate(ent, appearance);
+ }
+}
diff --git a/Content.Client/Popups/PopupOverlay.cs b/Content.Client/Popups/PopupOverlay.cs
index fb6bb3bf56..77eeb611f5 100644
--- a/Content.Client/Popups/PopupOverlay.cs
+++ b/Content.Client/Popups/PopupOverlay.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Content.Shared.Examine;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -55,7 +56,7 @@ protected override void Draw(in OverlayDrawArgs args)
if (args.ViewportControl == null)
return;
- args.DrawingHandle.SetTransform(Matrix3.Identity);
+ args.DrawingHandle.SetTransform(Matrix3x2.Identity);
args.DrawingHandle.UseShader(_shader);
var scale = _configManager.GetCVar(CVars.DisplayUIScale);
@@ -90,7 +91,7 @@ private void DrawWorld(DrawingHandleScreen worldHandle, OverlayDrawArgs args, fl
e => e == popup.InitialPos.EntityId || e == ourEntity, entMan: _entManager))
continue;
- var pos = matrix.Transform(mapPos.Position);
+ var pos = Vector2.Transform(mapPos.Position, matrix);
_controller.DrawPopup(popup, worldHandle, pos, scale);
}
}
diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs
index 3faa392e58..1ef8dfba2d 100644
--- a/Content.Client/Popups/PopupSystem.cs
+++ b/Content.Client/Popups/PopupSystem.cs
@@ -5,7 +5,6 @@
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
-using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
@@ -163,6 +162,15 @@ public override void PopupEntity(string? message, EntityUid uid, Filter filter,
PopupEntity(message, uid, type);
}
+ public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small)
+ {
+ if (recipient == null)
+ return;
+
+ if (_timing.IsFirstTimePredicted)
+ PopupCursor(message, recipient.Value, type);
+ }
+
public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
{
if (recipient == null)
@@ -172,6 +180,15 @@ public override void PopupClient(string? message, EntityUid uid, EntityUid? reci
PopupEntity(message, uid, recipient.Value, type);
}
+ public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
+ {
+ if (recipient == null)
+ return;
+
+ if (_timing.IsFirstTimePredicted)
+ PopupCoordinates(message, coordinates, recipient.Value, type);
+ }
+
public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
{
if (TryComp(uid, out TransformComponent? transform))
diff --git a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
new file mode 100644
index 0000000000..60ed8d87b9
--- /dev/null
+++ b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
@@ -0,0 +1,21 @@
+using Content.Shared.Power.Components;
+using Content.Shared.UserInterface;
+using Content.Shared.Wires;
+
+namespace Content.Client.Power;
+
+public sealed class ActivatableUIRequiresPowerSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnActivate);
+ }
+
+ private void OnActivate(EntityUid uid, ActivatableUIRequiresPowerComponent component, ActivatableUIOpenAttemptEvent args)
+ {
+ // Client can't predict the power properly at the moment so rely upon the server to do it.
+ args.Cancel();
+ }
+}
diff --git a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
index 902d6bb7e6..d5057416cf 100644
--- a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
+++ b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
@@ -5,6 +5,7 @@
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
using System.Numerics;
+using static Content.Shared.Power.SharedPowerMonitoringConsoleSystem;
namespace Content.Client.Power;
@@ -23,8 +24,13 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks;
public List HiddenLineGroups = new();
- public Dictionary>? PowerCableNetwork;
- public Dictionary>? FocusCableNetwork;
+ public List PowerCableNetwork = new();
+ public List FocusCableNetwork = new();
+
+ private Dictionary[] _horizLines = [new(), new(), new()];
+ private Dictionary[] _horizLinesReversed = [new(), new(), new()];
+ private Dictionary[] _vertLines = [new(), new(), new()];
+ private Dictionary[] _vertLinesReversed = [new(), new(), new()];
private MapGridComponent? _grid;
@@ -48,15 +54,15 @@ protected override void UpdateNavMap()
if (!_entManager.TryGetComponent(Owner, out var cableNetworks))
return;
- if (!_entManager.TryGetComponent(MapUid, out _grid))
- return;
-
- PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks, _grid);
- FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks, _grid);
+ PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks);
+ FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks);
}
public void DrawAllCableNetworks(DrawingHandleScreen handle)
{
+ if (!_entManager.TryGetComponent(MapUid, out _grid))
+ return;
+
// Draw full cable network
if (PowerCableNetwork != null && PowerCableNetwork.Count > 0)
{
@@ -69,36 +75,29 @@ public void DrawAllCableNetworks(DrawingHandleScreen handle)
DrawCableNetwork(handle, FocusCableNetwork, Color.White);
}
- public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary> fullCableNetwork, Color modulator)
+ public void DrawCableNetwork(DrawingHandleScreen handle, List fullCableNetwork, Color modulator)
{
+ if (!_entManager.TryGetComponent(MapUid, out _grid))
+ return;
+
var offset = GetOffset();
- var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
+ offset = offset with { Y = -offset.Y };
if (WorldRange / WorldMaxRange > 0.5f)
{
var cableNetworks = new ValueList[3];
- foreach ((var chunk, var chunkedLines) in fullCableNetwork)
+ foreach (var line in fullCableNetwork)
{
- var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
-
- if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
+ if (HiddenLineGroups.Contains(line.Group))
continue;
- if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
- continue;
-
- foreach (var chunkedLine in chunkedLines)
- {
- if (HiddenLineGroups.Contains(chunkedLine.Group))
- continue;
-
- var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
- var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
+ var cableOffset = _powerCableOffsets[(int) line.Group];
+ var start = ScalePosition(line.Origin + cableOffset - offset);
+ var end = ScalePosition(line.Terminus + cableOffset - offset);
- cableNetworks[(int) chunkedLine.Group].Add(start);
- cableNetworks[(int) chunkedLine.Group].Add(end);
- }
+ cableNetworks[(int) line.Group].Add(start);
+ cableNetworks[(int) line.Group].Add(end);
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableNetworks.Length; cableNetworkIdx++)
@@ -124,48 +123,39 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary[3];
- foreach ((var chunk, var chunkedLines) in fullCableNetwork)
+ foreach (var line in fullCableNetwork)
{
- var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
-
- if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
+ if (HiddenLineGroups.Contains(line.Group))
continue;
- if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
- continue;
-
- foreach (var chunkedLine in chunkedLines)
- {
- if (HiddenLineGroups.Contains(chunkedLine.Group))
- continue;
-
- var leftTop = ScalePosition(new Vector2
- (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
- Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- - new Vector2(offset.X, -offset.Y));
-
- var rightTop = ScalePosition(new Vector2
- (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
- Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- - new Vector2(offset.X, -offset.Y));
-
- var leftBottom = ScalePosition(new Vector2
- (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
- Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- - new Vector2(offset.X, -offset.Y));
-
- var rightBottom = ScalePosition(new Vector2
- (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
- Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- - new Vector2(offset.X, -offset.Y));
-
- cableVertexUVs[(int) chunkedLine.Group].Add(leftBottom);
- cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
- cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
- cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
- cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
- cableVertexUVs[(int) chunkedLine.Group].Add(rightTop);
- }
+ var cableOffset = _powerCableOffsets[(int) line.Group];
+
+ var leftTop = ScalePosition(new Vector2
+ (Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
+ Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+ + cableOffset - offset);
+
+ var rightTop = ScalePosition(new Vector2
+ (Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
+ Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+ + cableOffset - offset);
+
+ var leftBottom = ScalePosition(new Vector2
+ (Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
+ Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+ + cableOffset - offset);
+
+ var rightBottom = ScalePosition(new Vector2
+ (Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
+ Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+ + cableOffset - offset);
+
+ cableVertexUVs[(int) line.Group].Add(leftBottom);
+ cableVertexUVs[(int) line.Group].Add(leftTop);
+ cableVertexUVs[(int) line.Group].Add(rightBottom);
+ cableVertexUVs[(int) line.Group].Add(leftTop);
+ cableVertexUVs[(int) line.Group].Add(rightBottom);
+ cableVertexUVs[(int) line.Group].Add(rightTop);
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableVertexUVs.Length; cableNetworkIdx++)
@@ -188,34 +178,43 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary>? GetDecodedPowerCableChunks(Dictionary? chunks, MapGridComponent? grid)
+ public List GetDecodedPowerCableChunks(Dictionary? chunks)
{
- if (chunks == null || grid == null)
- return null;
+ var decodedOutput = new List();
- var decodedOutput = new Dictionary>();
+ if (!_entManager.TryGetComponent(MapUid, out _grid))
+ return decodedOutput;
- foreach ((var chunkOrigin, var chunk) in chunks)
- {
- var list = new List();
+ if (chunks == null)
+ return decodedOutput;
+
+ Array.ForEach(_horizLines, x=> x.Clear());
+ Array.ForEach(_horizLinesReversed, x=> x.Clear());
+ Array.ForEach(_vertLines, x=> x.Clear());
+ Array.ForEach(_vertLinesReversed, x=> x.Clear());
- for (int cableIdx = 0; cableIdx < chunk.PowerCableData.Length; cableIdx++)
+ foreach (var (chunkOrigin, chunk) in chunks)
+ {
+ for (var cableIdx = 0; cableIdx < 3; cableIdx++)
{
- var chunkMask = chunk.PowerCableData[cableIdx];
+ var horizLines = _horizLines[cableIdx];
+ var horizLinesReversed = _horizLinesReversed[cableIdx];
+ var vertLines = _vertLines[cableIdx];
+ var vertLinesReversed = _vertLinesReversed[cableIdx];
- Vector2 offset = _powerCableOffsets[cableIdx];
+ var chunkMask = chunk.PowerCableData[cableIdx];
- for (var chunkIdx = 0; chunkIdx < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; chunkIdx++)
+ for (var chunkIdx = 0; chunkIdx < ChunkSize * ChunkSize; chunkIdx++)
{
- var value = (int) Math.Pow(2, chunkIdx);
+ var value = 1 << chunkIdx;
var mask = chunkMask & value;
if (mask == 0x0)
continue;
- var relativeTile = SharedNavMapSystem.GetTile(mask);
- var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
- var position = new Vector2(tile.X, -tile.Y);
+ var relativeTile = GetTileFromIndex(chunkIdx);
+ var tile = (chunk.Origin * ChunkSize + relativeTile) * _grid.TileSize;
+ tile = tile with { Y = -tile.Y };
PowerCableChunk neighborChunk;
bool neighbor;
@@ -223,56 +222,65 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary 0)
- decodedOutput.Add(chunkOrigin, list);
+ for (var index = 0; index < _vertLines.Length; index++)
+ {
+ var vertLines = _vertLines[index];
+ foreach (var (origin, terminal) in vertLines)
+ {
+ decodedOutput.Add(new PowerMonitoringConsoleLine(origin + gridOffset, terminal + gridOffset,
+ (PowerMonitoringConsoleLineGroup) index));
+ }
}
return decodedOutput;
diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.cs
index edc0eaa18a..81fe1f4d04 100644
--- a/Content.Client/Power/PowerMonitoringWindow.xaml.cs
+++ b/Content.Client/Power/PowerMonitoringWindow.xaml.cs
@@ -170,9 +170,6 @@ public void ShowEntites
NavMap.TrackedEntities[mon.Value] = blip;
}
- // Update nav map
- NavMap.ForceNavMapUpdate();
-
// If the entry group doesn't match the current tab, the data is out dated, do not use it
if (allEntries.Length > 0 && allEntries[0].Group != GetCurrentPowerMonitoringConsoleGroup())
return;
diff --git a/Content.Client/Preferences/UI/AntagPreferenceSelector.cs b/Content.Client/Preferences/UI/AntagPreferenceSelector.cs
deleted file mode 100644
index 4a339d3f65..0000000000
--- a/Content.Client/Preferences/UI/AntagPreferenceSelector.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using Content.Client.Players.PlayTimeTracking;
-using Content.Shared.Customization.Systems;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Robust.Shared.Configuration;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Preferences.UI;
-
-public sealed class AntagPreferenceSelector : RequirementsSelector
-{
- // 0 is yes and 1 is no
- public bool Preference
- {
- get => Options.SelectedValue == 0;
- set => Options.Select(value && !Disabled ? 0 : 1);
- }
-
- public event Action? PreferenceChanged;
-
- public AntagPreferenceSelector(AntagPrototype proto, JobPrototype highJob) : base(proto, highJob)
- {
- Options.OnItemSelected += _ => PreferenceChanged?.Invoke(Preference);
-
- var items = new[]
- {
- ("humanoid-profile-editor-antag-preference-yes-button", 0),
- ("humanoid-profile-editor-antag-preference-no-button", 1),
- };
- var title = Loc.GetString(proto.Name);
- var description = Loc.GetString(proto.Objective);
- Setup(items, title, 250, description);
-
- // Immediately lock requirements if they aren't met.
- // Another function checks Disabled after creating the selector so this has to be done now
- var requirements = IoCManager.Resolve();
- var prefs = IoCManager.Resolve();
- var entMan = IoCManager.Resolve();
- var characterReqs = entMan.System();
- var protoMan = IoCManager.Resolve();
- var configMan = IoCManager.Resolve();
-
- if (proto.Requirements != null
- && !characterReqs.CheckRequirementsValid(
- proto.Requirements,
- highJob,
- (HumanoidCharacterProfile) (prefs.Preferences?.SelectedCharacter ?? HumanoidCharacterProfile.DefaultWithSpecies()),
- requirements.GetRawPlayTimeTrackers(),
- requirements.IsWhitelisted(),
- entMan,
- protoMan,
- configMan,
- out var reasons))
- LockRequirements(characterReqs.GetRequirementsText(reasons));
- }
-}
diff --git a/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs b/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs
deleted file mode 100644
index 5165db5479..0000000000
--- a/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs
+++ /dev/null
@@ -1,257 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using Content.Client.Humanoid;
-using Content.Client.Info;
-using Content.Client.Info.PlaytimeStats;
-using Content.Client.Lobby;
-using Content.Client.Lobby.UI;
-using Content.Client.Resources;
-using Content.Client.Stylesheets;
-using Content.Shared.Clothing.Loadouts.Systems;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Robust.Client.AutoGenerated;
-using Robust.Client.Graphics;
-using Robust.Client.ResourceManagement;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Configuration;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-using Direction = Robust.Shared.Maths.Direction;
-
-namespace Content.Client.Preferences.UI
-{
- [GenerateTypedNameReferences]
- public sealed partial class CharacterSetupGui : Control
- {
- private readonly IClientPreferencesManager _preferencesManager;
- private readonly IEntityManager _entityManager;
- private readonly IPrototypeManager _prototypeManager;
- private readonly Button _createNewCharacterButton;
- private readonly HumanoidProfileEditor _humanoidProfileEditor;
-
- public CharacterSetupGui(
- IEntityManager entityManager,
- IResourceCache resourceCache,
- IClientPreferencesManager preferencesManager,
- IPrototypeManager prototypeManager,
- IConfigurationManager configurationManager)
- {
- RobustXamlLoader.Load(this);
- _entityManager = entityManager;
- _prototypeManager = prototypeManager;
- _preferencesManager = preferencesManager;
-
- var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
- var back = new StyleBoxTexture
- {
- Texture = panelTex,
- Modulate = new Color(37, 37, 42)
- };
- back.SetPatchMargin(StyleBox.Margin.All, 10);
-
- BackgroundPanel.PanelOverride = back;
-
- _createNewCharacterButton = new Button
- {
- Text = Loc.GetString("character-setup-gui-create-new-character-button"),
- };
- _createNewCharacterButton.OnPressed += args =>
- {
- preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
- UpdateUI();
- args.Event.Handle();
- };
-
- _humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager);
- _humanoidProfileEditor.OnProfileChanged += ProfileChanged;
- CharEditor.AddChild(_humanoidProfileEditor);
-
- UpdateUI();
-
- RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
-
- StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
- preferencesManager.OnServerDataLoaded += UpdateUI;
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (!disposing)
- return;
-
- _preferencesManager.OnServerDataLoaded -= UpdateUI;
- }
-
- public void Save() => _humanoidProfileEditor.Save();
-
- private void ProfileChanged(ICharacterProfile profile, int profileSlot)
- {
- _humanoidProfileEditor.UpdateControls();
- UpdateUI();
- }
-
- private void UpdateUI()
- {
- UserInterfaceManager.GetUIController().UpdateCharacterUI();
- var numberOfFullSlots = 0;
- var characterButtonsGroup = new ButtonGroup();
- Characters.RemoveAllChildren();
-
- if (!_preferencesManager.ServerDataLoaded)
- return;
-
- _createNewCharacterButton.ToolTip =
- Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
- ("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots));
-
- foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
- {
- numberOfFullSlots++;
- var characterPickerButton = new CharacterPickerButton(_entityManager,
- _preferencesManager,
- _prototypeManager,
- characterButtonsGroup,
- character);
- Characters.AddChild(characterPickerButton);
-
- var characterIndexCopy = slot;
- characterPickerButton.OnPressed += args =>
- {
- _humanoidProfileEditor.Profile = (HumanoidCharacterProfile)character;
- _humanoidProfileEditor.CharacterSlot = characterIndexCopy;
- _humanoidProfileEditor.UpdateControls();
- _preferencesManager.SelectCharacter(character);
- UpdateUI();
- args.Event.Handle();
- };
- }
-
- _createNewCharacterButton.Disabled =
- numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
- Characters.AddChild(_createNewCharacterButton);
- // TODO: Move this shit to the Lobby UI controller
- }
-
- ///
- /// Shows individual characters on the side of the character GUI.
- ///
- private sealed class CharacterPickerButton : ContainerButton
- {
- private EntityUid _previewDummy;
-
- public CharacterPickerButton(
- IEntityManager entityManager,
- IClientPreferencesManager preferencesManager,
- IPrototypeManager prototypeManager,
- ButtonGroup group,
- ICharacterProfile profile)
- {
- AddStyleClass(StyleClassButton);
- ToggleMode = true;
- Group = group;
-
- var humanoid = profile as HumanoidCharacterProfile;
- if (humanoid is not null)
- {
- var dummy = prototypeManager.Index(humanoid.Species).DollPrototype;
- _previewDummy = entityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
- }
- else
- {
- _previewDummy = entityManager.SpawnEntity(prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
- }
-
- EntitySystem.Get().LoadProfile(_previewDummy, (HumanoidCharacterProfile)profile);
-
- if (humanoid != null)
- {
- var controller = UserInterfaceManager.GetUIController();
- controller.GiveDummyJobClothesLoadout(_previewDummy, humanoid);
- }
-
- var isSelectedCharacter = profile == preferencesManager.Preferences?.SelectedCharacter;
-
- if (isSelectedCharacter)
- Pressed = true;
-
- var view = new SpriteView
- {
- Scale = new Vector2(2, 2),
- OverrideDirection = Direction.South
- };
- view.SetEntity(_previewDummy);
-
- var description = profile.Name;
-
- var highPriorityJob = humanoid?.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key;
- if (highPriorityJob != null)
- {
- var jobName = IoCManager.Resolve().Index(highPriorityJob).LocalizedName;
- description = $"{description}\n{jobName}";
- }
-
- var descriptionLabel = new Label
- {
- Text = description,
- ClipText = true,
- HorizontalExpand = true
- };
- var deleteButton = new Button
- {
- Text = Loc.GetString("character-setup-gui-character-picker-button-delete-button"),
- Visible = !isSelectedCharacter,
- };
- var confirmDeleteButton = new Button
- {
- Text = Loc.GetString("character-setup-gui-character-picker-button-confirm-delete-button"),
- Visible = false,
- };
- confirmDeleteButton.ModulateSelfOverride = StyleNano.ButtonColorCautionDefault;
- confirmDeleteButton.OnPressed += _ =>
- {
- Parent?.RemoveChild(this);
- Parent?.RemoveChild(confirmDeleteButton);
- preferencesManager.DeleteCharacter(profile);
- };
- deleteButton.OnPressed += _ =>
- {
- deleteButton.Visible = false;
- confirmDeleteButton.Visible = true;
- };
-
- var internalHBox = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- SeparationOverride = 0,
- Children =
- {
- view,
- descriptionLabel,
- deleteButton,
- confirmDeleteButton
- }
- };
-
- AddChild(internalHBox);
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (!disposing)
- return;
-
- IoCManager.Resolve().DeleteEntity(_previewDummy);
- _previewDummy = default;
- }
- }
- }
-}
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs
deleted file mode 100644
index e12da12d0a..0000000000
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Content.Shared.Preferences;
-
-namespace Content.Client.Preferences.UI;
-
-public sealed partial class HumanoidProfileEditor
-{
- private void RandomizeEverything()
- {
- Profile = HumanoidCharacterProfile.Random();
- UpdateControls();
- IsDirty = true;
- }
-
- private void RandomizeName()
- {
- if (Profile == null)
- return;
- var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
- SetName(name);
- UpdateNameEdit();
- }
-}
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
deleted file mode 100644
index ebf794954e..0000000000
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
+++ /dev/null
@@ -1,203 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
deleted file mode 100644
index 954a705fce..0000000000
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
+++ /dev/null
@@ -1,1900 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using Content.Client.Guidebook;
-using Content.Client.Humanoid;
-using Content.Client.Lobby;
-using Content.Client.Message;
-using Content.Client.Players.PlayTimeTracking;
-using Content.Client.Roles;
-using Content.Client.UserInterface.Systems.Guidebook;
-using Content.Shared.CCVar;
-using Content.Shared.Clothing.Loadouts.Prototypes;
-using Content.Shared.Clothing.Loadouts.Systems;
-using Content.Shared.Customization.Systems;
-using Content.Shared.GameTicking;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Markings;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Content.Shared.Roles.Jobs;
-using Content.Shared.Traits;
-using Robust.Client.AutoGenerated;
-using Robust.Client.Graphics;
-using Robust.Client.Player;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Client.Utility;
-using Robust.Shared.Configuration;
-using Robust.Shared.Enums;
-using Robust.Shared.Physics;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-using Direction = Robust.Shared.Maths.Direction;
-
-namespace Content.Client.Preferences.UI
-{
- [GenerateTypedNameReferences]
- public sealed partial class HumanoidProfileEditor : BoxContainer
- {
- private readonly IEntityManager _entityManager;
- private readonly IPrototypeManager _prototypeManager;
- private readonly IClientPreferencesManager _preferencesManager;
- private readonly IConfigurationManager _configurationManager;
- private readonly MarkingManager _markingManager;
- private readonly JobRequirementsManager _requirements;
- private readonly CharacterRequirementsSystem _characterRequirementsSystem;
- private readonly LobbyUIController _controller;
-
- private LineEdit _ageEdit => CAgeEdit;
- private LineEdit _nameEdit => CNameEdit;
- private TextEdit? _flavorTextEdit;
- private Button _nameRandomButton => CNameRandomize;
- private Button _randomizeEverythingButton => CRandomizeEverything;
- private RichTextLabel _warningLabel => CWarningLabel;
- private Button _saveButton => CSaveButton;
- private OptionButton _sexButton => CSexButton;
- private OptionButton _genderButton => CPronounsButton;
- private Slider _skinColor => CSkin;
- private OptionButton _clothingButton => CClothingButton;
- private OptionButton _backpackButton => CBackpackButton;
- private OptionButton _spawnPriorityButton => CSpawnPriorityButton;
- private SingleMarkingPicker _hairPicker => CHairStylePicker;
- private SingleMarkingPicker _facialHairPicker => CFacialHairPicker;
- private EyeColorPicker _eyesPicker => CEyeColorPicker;
- private Slider _heightSlider => CHeightSlider;
- private Slider _widthSlider => CWidthSlider;
-
- private TabContainer _tabContainer => CTabContainer;
- private BoxContainer _jobList => CJobList;
- private BoxContainer _antagList => CAntagList;
- private Label _traitPointsLabel => TraitPointsLabel;
- private int _traitCount;
- private ProgressBar _traitPointsBar => TraitPointsBar;
- private Button _traitsShowUnusableButton => TraitsShowUnusableButton;
- private BoxContainer _traitsTab => CTraitsTab;
- private TabContainer _traitsTabs => CTraitsTabs;
- private Label _loadoutPointsLabel => LoadoutPointsLabel;
- private ProgressBar _loadoutPointsBar => LoadoutPointsBar;
- private Button _loadoutsShowUnusableButton => LoadoutsShowUnusableButton;
- private BoxContainer _loadoutsTab => CLoadoutsTab;
- private TabContainer _loadoutsTabs => CLoadoutsTabs;
- private readonly List _jobPriorities;
- private OptionButton _preferenceUnavailableButton => CPreferenceUnavailableButton;
- private readonly Dictionary _jobCategories;
- private readonly List _speciesList;
- private readonly List _antagPreferences = new();
- private readonly List _traitPreferences;
- private readonly List _loadoutPreferences;
-
- private SpriteView _previewSpriteView => CSpriteView;
- private Button _previewRotateLeftButton => CSpriteRotateLeft;
- private Button _previewRotateRightButton => CSpriteRotateRight;
- private Direction _previewRotation = Direction.North;
-
- private BoxContainer _rgbSkinColorContainer => CRgbSkinColorContainer;
- private ColorSelectorSliders _rgbSkinColorSelector;
-
- private bool _isDirty;
- public int CharacterSlot;
- public HumanoidCharacterProfile? Profile;
-
- public event Action? OnProfileChanged;
-
- [ValidatePrototypeId]
- private const string DefaultSpeciesGuidebook = "Species";
-
- public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager,
- IConfigurationManager configurationManager)
- {
- RobustXamlLoader.Load(this);
- _entityManager = IoCManager.Resolve();
- _prototypeManager = prototypeManager;
- _preferencesManager = preferencesManager;
- _configurationManager = configurationManager;
- _markingManager = IoCManager.Resolve();
- _characterRequirementsSystem = _entityManager.System();
- _controller = UserInterfaceManager.GetUIController();
-
- _controller.SetProfileEditor(this);
- _controller.PreviewDummyUpdated += OnDummyUpdate;
- _previewSpriteView.SetEntity(_controller.GetPreviewDummy());
-
- #region Left
-
- #region Name
-
- _nameEdit.OnTextChanged += args => { SetName(args.Text); };
- _nameRandomButton.OnPressed += args => RandomizeName();
- _randomizeEverythingButton.OnPressed += args => { RandomizeEverything(); };
- _warningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
-
- #endregion Name
-
- #region Appearance
-
- _tabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
-
- ShowClothes.OnPressed += ToggleClothes;
- ShowLoadouts.OnPressed += ToggleLoadouts;
-
- #region Sex
-
- _sexButton.OnItemSelected += args =>
- {
- _sexButton.SelectId(args.Id);
- SetSex((Sex) args.Id);
- };
-
- #endregion Sex
-
- #region Age
-
- _ageEdit.OnTextChanged += args =>
- {
- if (!int.TryParse(args.Text, out var newAge))
- return;
- SetAge(newAge);
- };
-
- #endregion Age
-
- #region Gender
-
- _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male);
- _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female);
- _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene);
- _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter);
-
- _genderButton.OnItemSelected += args =>
- {
- _genderButton.SelectId(args.Id);
- SetGender((Gender) args.Id);
- };
-
- #endregion Gender
-
- #region Species
-
- _speciesList = prototypeManager.EnumeratePrototypes().Where(o => o.RoundStart).ToList();
- for (var i = 0; i < _speciesList.Count; i++)
- {
- var name = Loc.GetString(_speciesList[i].Name);
- CSpeciesButton.AddItem(name, i);
- }
-
- CSpeciesButton.OnItemSelected += args =>
- {
- CSpeciesButton.SelectId(args.Id);
- SetSpecies(_speciesList[args.Id].ID);
- UpdateHairPickers();
- OnSkinColorOnValueChanged();
- };
-
- #endregion Species
-
- #region Height
-
- var prototype = _speciesList.Find(x => x.ID == Profile?.Species) ?? _speciesList.First();
-
- _heightSlider.MinValue = prototype.MinHeight;
- _heightSlider.MaxValue = prototype.MaxHeight;
- _heightSlider.Value = Profile?.Height ?? prototype.DefaultHeight;
- var height = MathF.Round(prototype.AverageHeight * _heightSlider.Value);
- CHeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
-
- _heightSlider.OnValueChanged += args =>
- {
- if (Profile is null)
- return;
-
- prototype = _speciesList.Find(x => x.ID == Profile.Species) ?? _speciesList.First(); // Just in case
-
- var value = Math.Clamp(args.Value, prototype.MinHeight, prototype.MaxHeight);
- var height = MathF.Round(prototype.AverageHeight * value);
- CHeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
- SetProfileHeight(value);
- UpdateWeight();
- };
-
- CHeightReset.OnPressed += _ =>
- {
- _heightSlider.Value = prototype.DefaultHeight;
- SetProfileHeight(prototype.DefaultHeight);
- UpdateWeight();
- };
-
-
- _widthSlider.MinValue = prototype.MinWidth;
- _widthSlider.MaxValue = prototype.MaxWidth;
- _widthSlider.Value = Profile?.Width ?? prototype.DefaultWidth;
- var width = MathF.Round(prototype.AverageWidth * _widthSlider.Value);
- CWidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", width));
-
- _widthSlider.OnValueChanged += args =>
- {
- if (Profile is null)
- return;
-
- prototype = _speciesList.Find(x => x.ID == Profile.Species) ?? _speciesList.First(); // Just in case
-
- var value = Math.Clamp(args.Value, prototype.MinWidth, prototype.MaxWidth);
- var width = MathF.Round(prototype.AverageWidth * value);
- CWidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", width));
- SetProfileWidth(value);
- UpdateWeight();
- };
-
- CWidthReset.OnPressed += _ =>
- {
- _widthSlider.Value = prototype.DefaultWidth;
- SetProfileWidth(prototype.DefaultWidth);
- UpdateWeight();
- };
-
- prototypeManager.Index(prototype.Prototype).TryGetComponent(out var fixture);
- if (fixture != null)
- {
- var radius = fixture.Fixtures["fix1"].Shape.Radius;
- var density = fixture.Fixtures["fix1"].Density;
- var avg = (_widthSlider.Value + _heightSlider.Value) / 2;
- var weight = MathF.Round(MathF.PI * MathF.Pow(radius * avg, 2) * density);
- CWeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) weight));
- }
- else
- {
- // Whelp, the fixture doesn't exist, guesstimate it instead
- CWeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) 71));
- }
-
- #endregion Height
-
- #region Skin
-
-
- _skinColor.OnValueChanged += _ =>
- {
- OnSkinColorOnValueChanged();
- };
-
- _rgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders());
- _rgbSkinColorSelector.OnColorChanged += _ =>
- {
- OnSkinColorOnValueChanged();
- };
-
- #endregion
-
- #region Hair
-
- _hairPicker.OnMarkingSelect += newStyle =>
- {
- if (Profile is null)
- return;
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithHairStyleName(newStyle.id));
- IsDirty = true;
- UpdatePreview();
- };
-
- _hairPicker.OnColorChanged += newColor =>
- {
- if (Profile is null)
- return;
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
- UpdateCMarkingsHair();
- IsDirty = true;
- UpdatePreview();
- };
-
- _facialHairPicker.OnMarkingSelect += newStyle =>
- {
- if (Profile is null)
- return;
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithFacialHairStyleName(newStyle.id));
- IsDirty = true;
- UpdatePreview();
- };
-
- _facialHairPicker.OnColorChanged += newColor =>
- {
- if (Profile is null)
- return;
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
- UpdateCMarkingsFacialHair();
- IsDirty = true;
- UpdatePreview();
- };
-
- _hairPicker.OnSlotRemove += _ =>
- {
- if (Profile is null)
- return;
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle)
- );
- UpdateHairPickers();
- UpdateCMarkingsHair();
- IsDirty = true;
- UpdatePreview();
- };
-
- _facialHairPicker.OnSlotRemove += _ =>
- {
- if (Profile is null)
- return;
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle)
- );
- UpdateHairPickers();
- UpdateCMarkingsFacialHair();
- IsDirty = true;
- UpdatePreview();
- };
-
- _hairPicker.OnSlotAdd += delegate()
- {
- if (Profile is null)
- return;
-
- var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, Profile.Species).Keys
- .FirstOrDefault();
-
- if (string.IsNullOrEmpty(hair))
- return;
-
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithHairStyleName(hair)
- );
-
- UpdateHairPickers();
- UpdateCMarkingsHair();
- IsDirty = true;
- UpdatePreview();
- };
-
- _facialHairPicker.OnSlotAdd += delegate()
- {
- if (Profile is null)
- return;
-
- var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, Profile.Species).Keys
- .FirstOrDefault();
-
- if (string.IsNullOrEmpty(hair))
- return;
-
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithFacialHairStyleName(hair)
- );
-
- UpdateHairPickers();
- UpdateCMarkingsFacialHair();
- IsDirty = true;
- UpdatePreview();
- };
-
- #endregion Hair
-
- #region Clothing
-
- _clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpsuit"), (int) ClothingPreference.Jumpsuit);
- _clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpskirt"), (int) ClothingPreference.Jumpskirt);
-
- _clothingButton.OnItemSelected += args =>
- {
- _clothingButton.SelectId(args.Id);
- SetClothing((ClothingPreference) args.Id);
- };
-
- #endregion Clothing
-
- #region Backpack
-
- _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-backpack"), (int) BackpackPreference.Backpack);
- _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-satchel"), (int) BackpackPreference.Satchel);
- _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-duffelbag"), (int) BackpackPreference.Duffelbag);
-
- _backpackButton.OnItemSelected += args =>
- {
- _backpackButton.SelectId(args.Id);
- SetBackpack((BackpackPreference) args.Id);
- };
-
- #endregion Backpack
-
- #region SpawnPriority
-
- foreach (var value in Enum.GetValues())
- {
- _spawnPriorityButton.AddItem(Loc.GetString($"humanoid-profile-editor-preference-spawn-priority-{value.ToString().ToLower()}"), (int) value);
- }
-
- _spawnPriorityButton.OnItemSelected += args =>
- {
- _spawnPriorityButton.SelectId(args.Id);
- SetSpawnPriority((SpawnPriorityPreference) args.Id);
- };
-
- #endregion SpawnPriority
-
- #region Eyes
-
- _eyesPicker.OnEyeColorPicked += newColor =>
- {
- if (Profile is null)
- return;
- Profile = Profile.WithCharacterAppearance(
- Profile.Appearance.WithEyeColor(newColor));
- CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor;
- IsDirty = true;
- UpdatePreview();
- };
-
- #endregion Eyes
-
- #endregion Appearance
-
- #region Jobs
-
- _tabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-jobs-tab"));
-
- _preferenceUnavailableButton.AddItem(
- Loc.GetString("humanoid-profile-editor-preference-unavailable-stay-in-lobby-button"),
- (int) PreferenceUnavailableMode.StayInLobby);
- _preferenceUnavailableButton.AddItem(
- Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button",
- ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))),
- (int) PreferenceUnavailableMode.SpawnAsOverflow);
-
- _preferenceUnavailableButton.OnItemSelected += args =>
- {
- _preferenceUnavailableButton.SelectId(args.Id);
-
- Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
- IsDirty = true;
- };
-
- _jobPriorities = new List();
- _jobCategories = new Dictionary();
- _requirements = IoCManager.Resolve();
- _requirements.Updated += UpdateAntagRequirements;
- _requirements.Updated += UpdateRoleRequirements;
- UpdateAntagRequirements();
- UpdateRoleRequirements();
-
- #endregion Jobs
-
- #region Antags
-
- _tabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
-
- #endregion Antags
-
- #region Traits
-
- // Set up the traits tab
- _tabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
- _traitPreferences = new List();
-
- // Show/Hide the traits tab if they ever get enabled/disabled
- var traitsEnabled = configurationManager.GetCVar(CCVars.GameTraitsEnabled);
- _tabContainer.SetTabVisible(3, traitsEnabled);
- configurationManager.OnValueChanged(CCVars.GameTraitsEnabled,
- enabled => _tabContainer.SetTabVisible(3, enabled));
-
- _traitsShowUnusableButton.OnToggled += args => UpdateTraits(args.Pressed);
-
- UpdateTraits(false);
-
- #endregion
-
- #region Loadouts
-
- // Set up the loadouts tab
- _tabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-loadouts-tab"));
- _loadoutPreferences = new List();
-
- // Show/Hide the loadouts tab if they ever get enabled/disabled
- var loadoutsEnabled = configurationManager.GetCVar(CCVars.GameLoadoutsEnabled);
- _tabContainer.SetTabVisible(4, loadoutsEnabled);
- ShowLoadouts.Visible = loadoutsEnabled;
- configurationManager.OnValueChanged(CCVars.GameLoadoutsEnabled,
- enabled => LoadoutsChanged(enabled));
-
- _loadoutsShowUnusableButton.OnToggled += args => UpdateLoadouts(args.Pressed);
-
- UpdateLoadouts(false);
-
- #endregion
-
- #region Save
-
- _saveButton.OnPressed += _ => { Save(); };
-
- #endregion Save
-
- #region Markings
- _tabContainer.SetTabTitle(5, Loc.GetString("humanoid-profile-editor-markings-tab"));
-
- CMarkings.OnMarkingAdded += OnMarkingChange;
- CMarkings.OnMarkingRemoved += OnMarkingChange;
- CMarkings.OnMarkingColorChange += OnMarkingChange;
- CMarkings.OnMarkingRankChange += OnMarkingChange;
-
- #endregion Markings
-
- #region FlavorText
-
- if (configurationManager.GetCVar(CCVars.FlavorText))
- {
- var flavorText = new FlavorText.FlavorText();
- _tabContainer.AddChild(flavorText);
- _tabContainer.SetTabTitle(_tabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-flavortext-tab"));
- _flavorTextEdit = flavorText.CFlavorTextInput;
-
- flavorText.OnFlavorTextChanged += OnFlavorTextChange;
- }
-
- #endregion FlavorText
-
- #region Dummy
-
- _previewRotateLeftButton.OnPressed += _ =>
- {
- _previewRotation = _previewRotation.TurnCw();
- SetPreviewRotation(_previewRotation);
- };
- _previewRotateRightButton.OnPressed += _ =>
- {
- _previewRotation = _previewRotation.TurnCcw();
- SetPreviewRotation(_previewRotation);
- };
-
- #endregion Dummy
-
- #endregion Left
-
- if (preferencesManager.ServerDataLoaded)
- LoadServerData();
-
- preferencesManager.OnServerDataLoaded += LoadServerData;
-
- SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
-
- UpdateSpeciesGuidebookIcon();
-
- IsDirty = false;
- }
-
-
- private void LoadoutsChanged(bool enabled)
- {
- _tabContainer.SetTabVisible(4, enabled);
- ShowLoadouts.Visible = enabled;
- }
-
- private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
- {
- var guidebookController = UserInterfaceManager.GetUIController();
- var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
- var page = DefaultSpeciesGuidebook;
- if (_prototypeManager.HasIndex(species))
- page = species;
-
- if (_prototypeManager.TryIndex(DefaultSpeciesGuidebook, out var guideRoot))
- {
- var dict = new Dictionary { { DefaultSpeciesGuidebook, guideRoot } };
- //TODO: Don't close the guidebook if its already open, just go to the correct page
- guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
- }
- }
-
- private void ToggleClothes(BaseButton.ButtonEventArgs _)
- {
- _controller.ShowClothes = ShowClothes.Pressed;
- _controller.UpdateCharacterUI();
- }
-
- private void ToggleLoadouts(BaseButton.ButtonEventArgs _)
- {
- _controller.ShowLoadouts = ShowLoadouts.Pressed;
- _controller.UpdateCharacterUI();
- }
-
- private void OnDummyUpdate(EntityUid value)
- {
- _previewSpriteView.SetEntity(value);
- }
-
- private void UpdateAntagRequirements()
- {
- _antagList.DisposeAllChildren();
- _antagPreferences.Clear();
-
- foreach (var antag in _prototypeManager.EnumeratePrototypes().OrderBy(a => Loc.GetString(a.Name)))
- {
- if (!antag.SetPreference)
- continue;
-
- var selector = new AntagPreferenceSelector(antag,
- _jobPriorities.FirstOrDefault(j => j.Priority == JobPriority.High)?.HighJob
- ?? new())
- { Margin = new Thickness(3f, 3f, 3f, 0f) };
- _antagList.AddChild(selector);
- _antagPreferences.Add(selector);
- if (selector.Disabled)
- {
- Profile = Profile?.WithAntagPreference(antag.ID, false);
- IsDirty = true;
- }
-
- selector.PreferenceChanged += preference =>
- {
- Profile = Profile?.WithAntagPreference(antag.ID, preference);
- IsDirty = true;
- };
- }
- }
-
- private void UpdateRoleRequirements()
- {
- _jobList.DisposeAllChildren();
- _jobPriorities.Clear();
- _jobCategories.Clear();
- var firstCategory = true;
-
- var departments = _prototypeManager.EnumeratePrototypes().ToArray();
- Array.Sort(departments, DepartmentUIComparer.Instance);
-
- foreach (var department in departments)
- {
- var departmentName = Loc.GetString($"department-{department.ID}");
-
- if (!_jobCategories.TryGetValue(department.ID, out var category))
- {
- category = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- Name = department.ID,
- ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
- ("departmentName", departmentName))
- };
-
- if (firstCategory)
- {
- firstCategory = false;
- }
- else
- {
- category.AddChild(new Control
- {
- MinSize = new Vector2(0, 23),
- });
- }
-
- category.AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")},
- Children =
- {
- new Label
- {
- Text = Loc.GetString("humanoid-profile-editor-department-jobs-label",
- ("departmentName", departmentName)),
- Margin = new Thickness(5f, 0, 0, 0)
- }
- }
- });
-
- _jobCategories[department.ID] = category;
- _jobList.AddChild(category);
- }
-
- var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId))
- .Where(job => job.SetPreference)
- .ToArray();
- Array.Sort(jobs, JobUIComparer.Instance);
-
- foreach (var job in jobs)
- {
- var selector = new JobPrioritySelector(job, _prototypeManager);
-
- if (!_characterRequirementsSystem.CheckRequirementsValid(
- job.Requirements ?? new(),
- job,
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
- _requirements.GetRawPlayTimeTrackers(),
- _requirements.IsWhitelisted(),
- _entityManager,
- _prototypeManager,
- _configurationManager,
- out var reasons))
- selector.LockRequirements(_characterRequirementsSystem.GetRequirementsText(reasons));
-
- category.AddChild(selector);
- _jobPriorities.Add(selector);
- EnsureJobRequirementsValid(); // DeltaV
-
- selector.PriorityChanged += priority =>
- {
- Profile = Profile?.WithJobPriority(job.ID, priority);
- IsDirty = true;
-
- foreach (var jobSelector in _jobPriorities)
- {
- // Sync other selectors with the same job in case of multiple department jobs
- if (jobSelector.Proto == selector.Proto)
- {
- jobSelector.Priority = priority;
- }
- else if (priority == JobPriority.High && jobSelector.Priority == JobPriority.High)
- {
- // Lower any other high priorities to medium.
- jobSelector.Priority = JobPriority.Medium;
- Profile = Profile?.WithJobPriority(jobSelector.Proto.ID, JobPriority.Medium);
- }
- }
- };
-
- }
- }
-
- if (Profile is not null)
- {
- UpdateJobPriorities();
- }
- }
-
- ///
- /// DeltaV - Make sure that no invalid job priorities get through.
- ///
- private void EnsureJobRequirementsValid()
- {
- var changed = false;
- foreach (var selector in _jobPriorities)
- {
- if (selector.Priority == JobPriority.Never
- || _characterRequirementsSystem.CheckRequirementsValid(
- selector.Proto.Requirements ?? new(),
- selector.Proto,
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
- _requirements.GetRawPlayTimeTrackers(),
- _requirements.IsWhitelisted(),
- _entityManager,
- _prototypeManager,
- _configurationManager,
- out _))
- continue;
-
- selector.Priority = JobPriority.Never;
- Profile = Profile?.WithJobPriority(selector.Proto.ID, JobPriority.Never);
- changed = true;
- }
-
- if (!changed)
- return;
-
- Save();
- }
-
- private void OnFlavorTextChange(string content)
- {
- if (Profile is null)
- return;
-
- Profile = Profile.WithFlavorText(content);
- IsDirty = true;
- }
-
- private void OnMarkingChange(MarkingSet markings)
- {
- if (Profile is null)
- return;
-
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
- IsDirty = true;
- UpdatePreview();
- }
-
- private void OnSkinColorOnValueChanged()
- {
- if (Profile is null)
- return;
-
- var skin = _prototypeManager.Index(Profile.Species).SkinColoration;
- var skinColor = _prototypeManager.Index(Profile.Species).DefaultSkinTone;
-
- switch (skin)
- {
- case HumanoidSkinColor.HumanToned:
- {
- if (!_skinColor.Visible)
- {
- _skinColor.Visible = true;
- _rgbSkinColorContainer.Visible = false;
- }
-
- var color = SkinColor.HumanSkinTone((int) _skinColor.Value);
-
- CMarkings.CurrentSkinColor = color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
- break;
- }
- case HumanoidSkinColor.Hues:
- {
- if (!_rgbSkinColorContainer.Visible)
- {
- _skinColor.Visible = false;
- _rgbSkinColorContainer.Visible = true;
- }
-
- CMarkings.CurrentSkinColor = _rgbSkinColorSelector.Color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
- break;
- }
- case HumanoidSkinColor.TintedHues:
- case HumanoidSkinColor.TintedHuesSkin: // DeltaV - Tone blending
- {
- if (!_rgbSkinColorContainer.Visible)
- {
- _skinColor.Visible = false;
- _rgbSkinColorContainer.Visible = true;
- }
-
- var color = skin switch // DeltaV - Tone blending
- {
- HumanoidSkinColor.TintedHues => SkinColor.TintedHues(_rgbSkinColorSelector.Color),
- HumanoidSkinColor.TintedHuesSkin => SkinColor.TintedHuesSkin(_rgbSkinColorSelector.Color, skinColor),
- _ => Color.White
- };
-
- CMarkings.CurrentSkinColor = color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
- break;
- }
- }
-
- IsDirty = true;
- UpdatePreview();
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (!disposing)
- return;
-
- _controller.PreviewDummyUpdated -= OnDummyUpdate;
- _requirements.Updated -= UpdateAntagRequirements;
- _requirements.Updated -= UpdateRoleRequirements;
- _preferencesManager.OnServerDataLoaded -= LoadServerData;
-
- _configurationManager.UnsubValueChanged(CCVars.GameLoadoutsEnabled, LoadoutsChanged);
- }
-
- private void LoadServerData()
- {
- Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
- CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
-
- UpdateAntagRequirements();
- UpdateRoleRequirements();
- UpdateControls();
- EnsureJobRequirementsValid(); // DeltaV
- }
-
- private void SetAge(int newAge)
- {
- Profile = Profile?.WithAge(newAge);
- IsDirty = true;
- }
-
- private void SetSex(Sex newSex)
- {
- Profile = Profile?.WithSex(newSex);
- // for convenience, default to most common gender when new sex is selected
- switch (newSex)
- {
- case Sex.Male:
- Profile = Profile?.WithGender(Gender.Male);
- break;
- case Sex.Female:
- Profile = Profile?.WithGender(Gender.Female);
- break;
- default:
- Profile = Profile?.WithGender(Gender.Epicene);
- break;
- }
- UpdateGenderControls();
- CMarkings.SetSex(newSex);
- IsDirty = true;
- UpdatePreview();
- }
-
- private void SetGender(Gender newGender)
- {
- Profile = Profile?.WithGender(newGender);
- IsDirty = true;
- }
-
- private void SetSpecies(string newSpecies)
- {
- Profile = Profile?.WithSpecies(newSpecies);
- OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
- CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
- UpdateSexControls(); // Update sex for new species
- // Changing species provides inaccurate sliders without these
- UpdateHeightControls();
- UpdateWidthControls();
- UpdateWeight();
- UpdateSpeciesGuidebookIcon();
- IsDirty = true;
- UpdatePreview();
- }
-
- private void SetName(string newName)
- {
- Profile = Profile?.WithName(newName);
- IsDirty = true;
- }
-
- private void SetClothing(ClothingPreference newClothing)
- {
- Profile = Profile?.WithClothingPreference(newClothing);
- IsDirty = true;
- _controller.UpdateClothes = true;
- UpdatePreview();
- }
-
- private void SetBackpack(BackpackPreference newBackpack)
- {
- Profile = Profile?.WithBackpackPreference(newBackpack);
- IsDirty = true;
- _controller.UpdateClothes = true;
- UpdatePreview();
- }
-
- private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
- {
- Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
- IsDirty = true;
- }
-
- private void SetProfileHeight(float height)
- {
- Profile = Profile?.WithHeight(height);
- IsDirty = true;
- UpdatePreview();
- }
-
- private void SetProfileWidth(float width)
- {
- Profile = Profile?.WithWidth(width);
- IsDirty = true;
- UpdatePreview();
- }
-
- public void Save()
- {
- IsDirty = false;
-
- if (Profile == null)
- return;
-
- _preferencesManager.UpdateCharacter(Profile, CharacterSlot);
- OnProfileChanged?.Invoke(Profile, CharacterSlot);
- }
-
- private bool IsDirty
- {
- get => _isDirty;
- set
- {
- _isDirty = value;
- UpdateSaveButton();
- }
- }
-
- private void UpdateNameEdit()
- {
- _nameEdit.Text = Profile?.Name ?? "";
- }
-
- private void UpdateFlavorTextEdit()
- {
- if(_flavorTextEdit != null)
- _flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? "");
- }
-
- private void UpdateAgeEdit()
- {
- _ageEdit.Text = Profile?.Age.ToString() ?? "";
- }
-
- private void UpdateSexControls()
- {
- if (Profile == null)
- return;
-
- _sexButton.Clear();
-
- var sexes = new List();
-
- // Add species sex options, default to just none if we are in bizzaro world and have no species
- if (_prototypeManager.TryIndex(Profile.Species, out var speciesProto))
- foreach (var sex in speciesProto.Sexes)
- sexes.Add(sex);
- else
- sexes.Add(Sex.Unsexed);
-
- // Add button for each sex
- foreach (var sex in sexes)
- _sexButton.AddItem(Loc.GetString($"humanoid-profile-editor-sex-{sex.ToString().ToLower()}-text"), (int) sex);
-
- if (sexes.Contains(Profile.Sex))
- _sexButton.SelectId((int) Profile.Sex);
- else
- _sexButton.SelectId((int) sexes[0]);
- }
-
- private void UpdateSkinColor()
- {
- if (Profile == null)
- return;
-
- var skin = _prototypeManager.Index(Profile.Species).SkinColoration;
-
- switch (skin)
- {
- case HumanoidSkinColor.HumanToned:
- {
- if (!_skinColor.Visible)
- {
- _skinColor.Visible = true;
- _rgbSkinColorContainer.Visible = false;
- }
-
- _skinColor.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
- break;
- }
- case HumanoidSkinColor.Hues:
- {
- if (!_rgbSkinColorContainer.Visible)
- {
- _skinColor.Visible = false;
- _rgbSkinColorContainer.Visible = true;
- }
-
- // Set the RGB values to the direct values otherwise
- _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
- break;
- }
- case HumanoidSkinColor.TintedHues:
- {
- if (!_rgbSkinColorContainer.Visible)
- {
- _skinColor.Visible = false;
- _rgbSkinColorContainer.Visible = true;
- }
-
- // Set the RGB values to the direct values otherwise
- _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
- break;
- }
- }
- }
-
- public void UpdateSpeciesGuidebookIcon()
- {
- SpeciesInfoButton.StyleClasses.Clear();
-
- var species = Profile?.Species;
- if (species is null
- || !_prototypeManager.TryIndex(species, out var speciesProto)
- || !_prototypeManager.HasIndex(species))
- return;
-
- const string style = "SpeciesInfoDefault";
- SpeciesInfoButton.StyleClasses.Add(style);
- }
-
- private void UpdateMarkings()
- {
- if (Profile == null)
- return;
-
- CMarkings.SetData(Profile.Appearance.Markings, Profile.Species, Profile.Sex, Profile.Appearance.SkinColor,
- Profile.Appearance.EyeColor);
- }
-
- private void UpdateSpecies()
- {
- if (Profile == null)
- return;
-
- CSpeciesButton.Select(_speciesList.FindIndex(x => x.ID == Profile.Species));
- }
-
- private void UpdateGenderControls()
- {
- if (Profile == null)
- return;
-
- _genderButton.SelectId((int) Profile.Gender);
- }
-
- private void UpdateClothingControls()
- {
- if (Profile == null)
- return;
-
- _clothingButton.SelectId((int) Profile.Clothing);
- }
-
- private void UpdateBackpackControls()
- {
- if (Profile == null)
- return;
-
- _backpackButton.SelectId((int) Profile.Backpack);
- }
-
- private void UpdateSpawnPriorityControls()
- {
- if (Profile == null)
- return;
-
- _spawnPriorityButton.SelectId((int) Profile.SpawnPriority);
- }
-
- private void UpdateHeightControls()
- {
- if (Profile == null)
- return;
-
- var species = _speciesList.Find(x => x.ID == Profile.Species) ?? _speciesList.First();
-
- _heightSlider.MinValue = species.MinHeight;
- _heightSlider.Value = Profile.Height;
- _heightSlider.MaxValue = species.MaxHeight;
-
- var height = MathF.Round(species.AverageHeight * _heightSlider.Value);
- CHeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
- }
-
- private void UpdateWidthControls()
- {
- if (Profile == null)
- return;
-
- var species = _speciesList.Find(x => x.ID == Profile.Species) ?? _speciesList.First();
-
- _widthSlider.MinValue = species.MinWidth;
- _widthSlider.Value = Profile.Width;
- _widthSlider.MaxValue = species.MaxWidth;
-
- var width = MathF.Round(species.AverageWidth * _widthSlider.Value);
- CWidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", (int) width));
- }
-
- private void UpdateWeight()
- {
- if (Profile == null)
- return;
-
- var species = _speciesList.Find(x => x.ID == Profile.Species) ?? _speciesList.First();
- _prototypeManager.Index(species.Prototype).TryGetComponent(out var fixture);
-
- if (fixture != null)
- {
- var radius = fixture.Fixtures["fix1"].Shape.Radius;
- var density = fixture.Fixtures["fix1"].Density;
- var avg = (Profile.Width + Profile.Height) / 2;
- var weight = MathF.Round(MathF.PI * MathF.Pow(radius * avg, 2) * density);
- CWeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) weight));
- }
-
- _previewSpriteView.InvalidateMeasure();
- }
-
- private void UpdateHairPickers()
- {
- if (Profile == null)
- return;
-
- var hairMarking = Profile.Appearance.HairStyleId switch
- {
- HairStyles.DefaultHairStyle => new List(),
- _ => new() { new(Profile.Appearance.HairStyleId, new List() { Profile.Appearance.HairColor }) },
- };
-
- var facialHairMarking = Profile.Appearance.FacialHairStyleId switch
- {
- HairStyles.DefaultFacialHairStyle => new List(),
- _ => new() { new(Profile.Appearance.FacialHairStyleId, new List() { Profile.Appearance.FacialHairColor }) },
- };
-
- _hairPicker.UpdateData(
- hairMarking,
- Profile.Species,
- 1);
- _facialHairPicker.UpdateData(
- facialHairMarking,
- Profile.Species,
- 1);
- }
-
- private void UpdateCMarkingsHair()
- {
- if (Profile == null)
- return;
-
- // hair color
- Color? hairColor = null;
- if ( Profile.Appearance.HairStyleId != HairStyles.DefaultHairStyle &&
- _markingManager.Markings.TryGetValue(Profile.Appearance.HairStyleId, out var hairProto))
- if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, hairProto, _prototypeManager))
- hairColor = _markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out _, _prototypeManager)
- ? Profile.Appearance.SkinColor
- : Profile.Appearance.HairColor;
-
- if (hairColor != null)
- CMarkings.HairMarking = new(Profile.Appearance.HairStyleId, new List { hairColor.Value });
- else
- CMarkings.HairMarking = null;
- }
-
- private void UpdateCMarkingsFacialHair()
- {
- if (Profile == null)
- return;
-
- // facial hair color
- Color? facialHairColor = null;
- if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle &&
- _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto))
- if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, facialHairProto, _prototypeManager))
- facialHairColor = _markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out _, _prototypeManager)
- ? Profile.Appearance.SkinColor
- : Profile.Appearance.FacialHairColor;
-
- if (facialHairColor != null)
- CMarkings.FacialHairMarking = new(Profile.Appearance.FacialHairStyleId, new List { facialHairColor.Value });
- else
- CMarkings.FacialHairMarking = null;
- }
-
- private void UpdateEyePickers()
- {
- if (Profile == null)
- return;
-
- CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor;
- _eyesPicker.SetData(Profile.Appearance.EyeColor);
- }
-
- private void UpdateSaveButton()
- {
- _saveButton.Disabled = Profile is null || !IsDirty;
- }
-
- private void UpdatePreview()
- {
- if (Profile is null)
- return;
-
- SetPreviewRotation(_previewRotation);
- _controller.UpdateCharacterUI();
- }
-
- private void SetPreviewRotation(Direction direction)
- {
- _previewSpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
- }
-
- public void UpdateControls()
- {
- if (Profile is null)
- return;
-
- UpdateNameEdit();
- UpdateFlavorTextEdit();
- UpdateSexControls();
- UpdateGenderControls();
- UpdateSkinColor();
- UpdateSpecies();
- UpdateClothingControls();
- UpdateBackpackControls();
- UpdateSpawnPriorityControls();
- UpdateAgeEdit();
- UpdateEyePickers();
- UpdateSaveButton();
- UpdateJobPriorities();
- UpdateAntagPreferences();
- UpdateTraitPreferences();
- UpdateLoadouts(_loadoutsShowUnusableButton.Pressed);
- UpdateLoadoutPreferences();
- UpdateMarkings();
- UpdateHairPickers();
- UpdateCMarkingsHair();
- UpdateCMarkingsFacialHair();
- UpdateHeightControls();
- UpdateWidthControls();
- UpdateWeight();
-
- _preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
- }
-
- private void UpdateJobPriorities()
- {
- foreach (var prioritySelector in _jobPriorities)
- {
- var jobId = prioritySelector.Proto.ID;
-
- var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never;
-
- prioritySelector.Priority = priority;
- }
- }
-
- private void UpdateAntagPreferences()
- {
- foreach (var preferenceSelector in _antagPreferences)
- {
- var antagId = preferenceSelector.Proto.ID;
- var preference = Profile?.AntagPreferences.Contains(antagId) ?? false;
- preferenceSelector.Preference = preference;
- }
- }
-
- private void UpdateTraitPreferences()
- {
- var points = _configurationManager.GetCVar(CCVars.GameTraitsDefaultPoints);
- _traitCount = 0;
-
- foreach (var preferenceSelector in _traitPreferences)
- {
- var traitId = preferenceSelector.Trait.ID;
- var preference = Profile?.TraitPreferences.Contains(traitId) ?? false;
-
- preferenceSelector.Preference = preference;
-
- if (!preference)
- continue;
-
- points += preferenceSelector.Trait.Points;
- _traitCount += 1;
- }
-
- _traitPointsBar.Value = points;
- _traitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-header",
- ("points", points), ("traits", _traitCount),
- ("maxTraits", _configurationManager.GetCVar(CCVars.GameTraitsMax)));
-
- IsDirty = true;
- UpdatePreview();
- }
-
- // Yeah this is mostly just copied from UpdateLoadouts
- // This whole file is bad though and a lot of loadout code came from traits originally
- //TODO Make this file not hell
- private void UpdateTraits(bool showUnusable)
- {
- // Reset trait points so you don't get -14 points or something for no reason
- var points = _configurationManager.GetCVar(CCVars.GameTraitsDefaultPoints);
- _traitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-header",
- ("points", points), ("traits", 0),
- ("maxTraits", _configurationManager.GetCVar(CCVars.GameTraitsMax)));
- _traitPointsBar.MaxValue = points;
- _traitPointsBar.Value = points;
-
- // Clear current listings
- _traitPreferences.Clear();
- _traitsTabs.DisposeAllChildren();
-
-
- // Get the highest priority job to use for trait filtering
- var highJob = _jobPriorities.FirstOrDefault(j => j.Priority == JobPriority.High);
-
- // Get all trait prototypes
- var enumeratedTraits = _prototypeManager.EnumeratePrototypes().ToList();
- // Get all trait categories
- var categories = _prototypeManager.EnumeratePrototypes().ToList();
-
- // If showUnusable is false filter out traits that are unusable based on your current character setup
- var traits = enumeratedTraits.Where(trait =>
- showUnusable || // Ignore everything if this is true
- _characterRequirementsSystem.CheckRequirementsValid(
- trait.Requirements,
- highJob?.Proto ?? new JobPrototype(),
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
- _requirements.GetRawPlayTimeTrackers(),
- _requirements.IsWhitelisted(),
- _entityManager,
- _prototypeManager,
- _configurationManager,
- out _
- )
- ).ToList();
-
- // Traits to highlight red when showUnusable is true
- var traitsUnusable = enumeratedTraits.Where(trait =>
- _characterRequirementsSystem.CheckRequirementsValid(
- trait.Requirements,
- highJob?.Proto ?? new JobPrototype(),
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
- _requirements.GetRawPlayTimeTrackers(),
- _requirements.IsWhitelisted(),
- _entityManager,
- _prototypeManager,
- _configurationManager,
- out _
- )
- ).ToList();
-
- // Every trait not in the traits list
- var otherTraits = enumeratedTraits.Where(trait => !traits.Contains(trait)).ToList();
-
-
- if (traits.Count == 0)
- {
- _traitsTab.AddChild(new Label { Text = Loc.GetString("humanoid-profile-editor-traits-no-traits") });
- return;
- }
-
- // Make Uncategorized category
- var uncategorized = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- VerticalExpand = true,
- Name = "Uncategorized_0",
- // I hate ScrollContainers
- Children =
- {
- new ScrollContainer
- {
- HScrollEnabled = false,
- HorizontalExpand = true,
- VerticalExpand = true,
- Children =
- {
- new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- HorizontalExpand = true,
- VerticalExpand = true,
- },
- },
- },
- },
- };
-
- _traitsTabs.AddChild(uncategorized);
- _traitsTabs.SetTabTitle(0, Loc.GetString("trait-category-Uncategorized"));
-
-
- // Make categories
- var currentCategory = 1; // 1 because we already made 0 as Uncategorized, I am not not zero-indexing :)
- foreach (var category in categories.OrderBy(c => Loc.GetString($"trait-category-{c.ID}")))
- {
- // Check for existing category
- BoxContainer? match = null;
- foreach (var child in _traitsTabs.Children)
- {
- if (string.IsNullOrEmpty(child.Name))
- continue;
-
- if (child.Name.Split("_")[0] == category.ID)
- match = (BoxContainer) child;
- }
-
- // If there is a category do nothing
- if (match != null)
- continue;
-
- // If not, make it
- var box = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- VerticalExpand = true,
- Name = $"{category.ID}_{currentCategory}",
- // I hate ScrollContainers
- Children =
- {
- new ScrollContainer
- {
- HScrollEnabled = false,
- HorizontalExpand = true,
- VerticalExpand = true,
- Children =
- {
- new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- HorizontalExpand = true,
- VerticalExpand = true,
- },
- },
- },
- },
- };
-
- _traitsTabs.AddChild(box);
- _traitsTabs.SetTabTitle(currentCategory, Loc.GetString($"trait-category-{category.ID}"));
- currentCategory++;
- }
-
-
- // Fill categories
- foreach (var trait in traits.OrderBy(t => -t.Points).ThenBy(t => Loc.GetString($"trait-name-{t.ID}")))
- {
- var selector = new TraitPreferenceSelector(trait, highJob?.Proto ?? new JobPrototype(),
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
- traitsUnusable.Contains(trait) ? "" : "ButtonColorRed",
- _entityManager, _prototypeManager, _configurationManager, _characterRequirementsSystem,
- _requirements);
-
- // Look for an existing trait category
- BoxContainer? match = null;
- foreach (var child in _traitsTabs.Children)
- {
- if (string.IsNullOrEmpty(child.Name))
- continue;
-
- // This is fucked up
- if (child.Name.Split("_")[0] == trait.Category
- && child.Children.FirstOrDefault()?.Children.FirstOrDefault(g =>
- g.GetType() == typeof(BoxContainer)) is { } g)
- match = (BoxContainer) g;
- }
-
- // If there is no category put it in Uncategorized
- if (string.IsNullOrEmpty(match?.Parent?.Parent?.Name)
- || match.Parent.Parent.Name.Split("_")[0] != trait.Category)
- uncategorized.AddChild(selector);
- else
- match.AddChild(selector);
-
-
- AddSelector(selector, trait.Points, trait.ID);
- }
-
- // Add the selected unusable traits to the point counter
- foreach (var trait in otherTraits.OrderBy(t => -t.Points).ThenBy(t => Loc.GetString($"trait-name-{t.ID}")))
- {
- var selector = new TraitPreferenceSelector(trait, highJob?.Proto ?? new JobPrototype(),
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(), "",
- _entityManager, _prototypeManager, _configurationManager, _characterRequirementsSystem,
- _requirements);
-
-
- AddSelector(selector, trait.Points, trait.ID);
- }
-
-
- // Hide Uncategorized tab if it's empty, other tabs already shouldn't exist if they're empty
- _traitsTabs.SetTabVisible(0, uncategorized.Children.First().Children.First().Children.Any());
-
- // Add fake tabs until tab container is happy
- for (var i = _traitsTabs.ChildCount - 1; i < _traitsTabs.CurrentTab; i++)
- {
- _traitsTabs.AddChild(new BoxContainer());
- _traitsTabs.SetTabVisible(i + 1, false);
- }
-
- UpdateTraitPreferences();
- return;
-
-
- void AddSelector(TraitPreferenceSelector selector, int points, string id)
- {
- if (points > 0)
- _traitPointsBar.MaxValue += points;
-
- _traitPreferences.Add(selector);
- selector.PreferenceChanged += preference =>
- {
- // Make sure they have enough trait points
- preference = preference ? CheckPoints(points, preference) : CheckPoints(-points, preference);
- // Don't allow having too many traits
- preference = preference && _traitCount + 1 <= _configurationManager.GetCVar(CCVars.GameTraitsMax);
-
- // Update Preferences
- Profile = Profile?.WithTraitPreference(id, preference);
- UpdatePreview();
- UpdateTraitPreferences();
- UpdateTraits(_traitsShowUnusableButton.Pressed);
- UpdateLoadouts(_loadoutsShowUnusableButton.Pressed);
- };
- }
-
- bool CheckPoints(int points, bool preference)
- {
- var temp = _traitPointsBar.Value + points;
- return preference ? !(temp < 0) : temp < 0;
- }
- }
-
- private void UpdateLoadoutPreferences()
- {
- var points = _configurationManager.GetCVar(CCVars.GameLoadoutsPoints);
- _loadoutPointsBar.Value = points;
- _loadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
-
- foreach (var preferenceSelector in _loadoutPreferences)
- {
- var loadoutId = preferenceSelector.Loadout.ID;
- var preference = Profile?.LoadoutPreferences.Contains(loadoutId) ?? false;
-
- preferenceSelector.Preference = preference;
-
- if (preference)
- {
- points -= preferenceSelector.Loadout.Cost;
- _loadoutPointsBar.Value = points;
- _loadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", _loadoutPointsBar.MaxValue));
- }
- }
-
- IsDirty = true;
- _controller.UpdateClothes = true;
- UpdatePreview();
- }
-
- private void UpdateLoadouts(bool showUnusable)
- {
- // Reset loadout points so you don't get -14 points or something for no reason
- var points = _configurationManager.GetCVar(CCVars.GameLoadoutsPoints);
- _loadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
- _loadoutPointsBar.MaxValue = points;
- _loadoutPointsBar.Value = points;
-
- // Clear current listings
- _loadoutPreferences.Clear();
- _loadoutsTabs.DisposeAllChildren();
-
-
- // Get the highest priority job to use for loadout filtering
- var highJob = _jobPriorities.FirstOrDefault(j => j.Priority == JobPriority.High);
-
- // Get all loadout prototypes
- var enumeratedLoadouts = _prototypeManager.EnumeratePrototypes().ToList();
- // Get all loadout categories
- var categories = _prototypeManager.EnumeratePrototypes().ToList();
-
- // If showUnusable is false filter out loadouts that are unusable based on your current character setup
- var loadouts = enumeratedLoadouts.Where(loadout =>
- showUnusable || // Ignore everything if this is true
- _characterRequirementsSystem.CheckRequirementsValid(
- loadout.Requirements,
- highJob?.Proto ?? new JobPrototype(),
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
- _requirements.GetRawPlayTimeTrackers(),
- _requirements.IsWhitelisted(),
- _entityManager,
- _prototypeManager,
- _configurationManager,
- out _
- )
- ).ToList();
-
- // Loadouts to highlight red when showUnusable is true
- var loadoutsUnusable = enumeratedLoadouts.Where(loadout =>
- _characterRequirementsSystem.CheckRequirementsValid(
- loadout.Requirements,
- highJob?.Proto ?? new JobPrototype(),
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
- _requirements.GetRawPlayTimeTrackers(),
- _requirements.IsWhitelisted(),
- _entityManager,
- _prototypeManager,
- _configurationManager,
- out _
- )
- ).ToList();
-
- // Every loadout not in the loadouts list
- var otherLoadouts = enumeratedLoadouts.Where(loadout => !loadouts.Contains(loadout)).ToList();
-
-
- if (loadouts.Count == 0)
- {
- _loadoutsTab.AddChild(new Label { Text = Loc.GetString("humanoid-profile-editor-loadouts-no-loadouts") });
- return;
- }
-
- // Make Uncategorized category
- var uncategorized = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- VerticalExpand = true,
- Name = "Uncategorized_0",
- // I hate ScrollContainers
- Children =
- {
- new ScrollContainer
- {
- HScrollEnabled = false,
- HorizontalExpand = true,
- VerticalExpand = true,
- Children =
- {
- new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- HorizontalExpand = true,
- VerticalExpand = true,
- },
- },
- },
- },
- };
-
- _loadoutsTabs.AddChild(uncategorized);
- _loadoutsTabs.SetTabTitle(0, Loc.GetString("loadout-category-Uncategorized"));
-
-
- // Make categories
- var currentCategory = 1; // 1 because we already made 0 as Uncategorized, I am not not zero-indexing :)
- foreach (var category in categories.OrderBy(c => Loc.GetString($"loadout-category-{c.ID}")))
- {
- // Check for existing category
- BoxContainer? match = null;
- foreach (var child in _loadoutsTabs.Children)
- {
- if (string.IsNullOrEmpty(child.Name))
- continue;
-
- // This is fucked up
- if (child.Name.Split("_")[0] == category.ID
- && child.Children.FirstOrDefault()?.Children.FirstOrDefault(g =>
- g.GetType() == typeof(BoxContainer)) is { } g)
- match = (BoxContainer) g;
- }
-
- // If there is a category do nothing
- if (match != null)
- continue;
-
- // If not, make it
- var box = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- VerticalExpand = true,
- Name = $"{category.ID}_{currentCategory}",
- // I hate ScrollContainers
- Children =
- {
- new ScrollContainer
- {
- HScrollEnabled = false,
- HorizontalExpand = true,
- VerticalExpand = true,
- Children =
- {
- new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- HorizontalExpand = true,
- VerticalExpand = true,
- },
- },
- },
- },
- };
-
- _loadoutsTabs.AddChild(box);
- _loadoutsTabs.SetTabTitle(currentCategory, Loc.GetString($"loadout-category-{category.ID}"));
- currentCategory++;
- }
-
-
- // Fill categories
- foreach (var loadout in loadouts.OrderBy(l => l.Cost).ThenBy(l => Loc.GetString($"loadout-{l.ID}-name")))
- {
- var selector = new LoadoutPreferenceSelector(loadout, highJob?.Proto ?? new JobPrototype(),
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
- loadoutsUnusable.Contains(loadout) ? "" : "ButtonColorRed",
- _entityManager, _prototypeManager, _configurationManager, _characterRequirementsSystem,
- _requirements);
-
- // Look for an existing loadout category
- BoxContainer? match = null;
- foreach (var child in _loadoutsTabs.Children)
- {
- if (string.IsNullOrEmpty(child.Name))
- continue;
-
- if (child.Name.Split("_")[0] == loadout.Category)
- match = (BoxContainer) child.Children.First().Children.First();
- }
-
- // If there is no category put it in Uncategorized
- if (string.IsNullOrEmpty(match?.Parent?.Parent?.Name)
- || match.Parent.Parent.Name.Split("_")[0] != loadout.Category)
- uncategorized.AddChild(selector);
- else
- match.AddChild(selector);
-
-
- AddSelector(selector, loadout.Cost, loadout.ID);
- }
-
- // Add the selected unusable loadouts to the point counter
- foreach (var loadout in otherLoadouts.OrderBy(l => l.Cost).ThenBy(l => Loc.GetString($"loadout-{l.ID}-name")))
- {
- var selector = new LoadoutPreferenceSelector(loadout, highJob?.Proto ?? new JobPrototype(),
- Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(), "",
- _entityManager, _prototypeManager, _configurationManager, _characterRequirementsSystem,
- _requirements);
-
-
- AddSelector(selector, loadout.Cost, loadout.ID);
- }
-
-
- // Hide Uncategorized tab if it's empty, other tabs already shouldn't exist if they're empty
- _loadoutsTabs.SetTabVisible(0, uncategorized.Children.First().Children.First().Children.Any());
-
- // Add fake tabs until tab container is happy
- for (var i = _loadoutsTabs.ChildCount - 1; i < _loadoutsTabs.CurrentTab; i++)
- {
- _loadoutsTabs.AddChild(new BoxContainer());
- _loadoutsTabs.SetTabVisible(i + 1, false);
- }
-
- UpdateLoadoutPreferences();
- return;
-
-
- void AddSelector(LoadoutPreferenceSelector selector, int points, string id)
- {
- _loadoutPreferences.Add(selector);
- selector.PreferenceChanged += preference =>
- {
- // Make sure they have enough loadout points
- preference = preference ? CheckPoints(-points, preference) : CheckPoints(points, preference);
-
- // Update Preferences
- Profile = Profile?.WithLoadoutPreference(id, preference);
- IsDirty = true;
- UpdateLoadoutPreferences();
- UpdateLoadouts(_loadoutsShowUnusableButton.Pressed);
- UpdateTraits(_traitsShowUnusableButton.Pressed);
- };
- }
-
- bool CheckPoints(int points, bool preference)
- {
- var temp = _loadoutPointsBar.Value + points;
- return preference ? !(temp < 0) : temp < 0;
- }
- }
- }
-}
diff --git a/Content.Client/Preferences/UI/JobPrioritySelector.cs b/Content.Client/Preferences/UI/JobPrioritySelector.cs
deleted file mode 100644
index f66102d644..0000000000
--- a/Content.Client/Preferences/UI/JobPrioritySelector.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Numerics;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Content.Shared.StatusIcon;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.Utility;
-using Robust.Shared.CPUJob.JobQueues;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Preferences.UI;
-
-public sealed class JobPrioritySelector : RequirementsSelector
-{
- public JobPriority Priority
- {
- get => (JobPriority) Options.SelectedValue;
- set => Options.SelectByValue((int) value);
- }
-
- public event Action? PriorityChanged;
-
- public JobPrioritySelector(JobPrototype proto, IPrototypeManager protoMan) : base(proto, proto)
- {
- Options.OnItemSelected += _ => PriorityChanged?.Invoke(Priority);
-
- var items = new[]
- {
- ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
- ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
- ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
- ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
- };
-
- var icon = new TextureRect
- {
- TextureScale = new Vector2(2, 2),
- VerticalAlignment = VAlignment.Center,
- };
- var jobIcon = protoMan.Index(proto.Icon);
- icon.Texture = jobIcon.Icon.Frame0();
-
- Setup(items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
- }
-}
diff --git a/Content.Client/Preferences/UI/LoadoutPreferenceSelector.cs b/Content.Client/Preferences/UI/LoadoutPreferenceSelector.cs
deleted file mode 100644
index 82d8fd65b3..0000000000
--- a/Content.Client/Preferences/UI/LoadoutPreferenceSelector.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using System.Text;
-using Content.Client.Players.PlayTimeTracking;
-using Content.Client.Stylesheets;
-using Content.Shared.Clothing.Loadouts.Prototypes;
-using Content.Shared.Customization.Systems;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Shared.Configuration;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Client.Preferences.UI;
-
-
-public sealed class LoadoutPreferenceSelector : Control
-{
- public LoadoutPrototype Loadout { get; }
- private readonly Button _button;
-
- public bool Preference
- {
- get => _button.Pressed;
- set => _button.Pressed = value;
- }
-
- public event Action? PreferenceChanged;
-
- public LoadoutPreferenceSelector(LoadoutPrototype loadout, JobPrototype highJob,
- HumanoidCharacterProfile profile, string style, IEntityManager entityManager, IPrototypeManager prototypeManager,
- IConfigurationManager configManager, CharacterRequirementsSystem characterRequirementsSystem,
- JobRequirementsManager jobRequirementsManager)
- {
- Loadout = loadout;
-
- // Display the first item in the loadout as a preview
- // TODO: Maybe allow custom icons to be specified in the prototype?
- var dummyLoadoutItem = entityManager.SpawnEntity(loadout.Items.First(), MapCoordinates.Nullspace);
-
- // Create a sprite preview of the loadout item
- var previewLoadout = new SpriteView
- {
- Scale = new Vector2(1, 1),
- OverrideDirection = Direction.South,
- VerticalAlignment = VAlignment.Center,
- SizeFlagsStretchRatio = 1,
- };
- previewLoadout.SetEntity(dummyLoadoutItem);
-
-
- // Create a checkbox to get the loadout
- _button = new Button
- {
- ToggleMode = true,
- StyleClasses = { StyleBase.ButtonOpenLeft },
- Children =
- {
- new BoxContainer
- {
- Children =
- {
- new Label
- {
- Text = loadout.Cost.ToString(),
- StyleClasses = { StyleBase.StyleClassLabelHeading },
- MinWidth = 32,
- MaxWidth = 32,
- ClipText = true,
- Margin = new Thickness(0, 0, 8, 0),
- },
- new Label
- {
- Text = Loc.GetString($"loadout-name-{loadout.ID}") == $"loadout-name-{loadout.ID}"
- ? entityManager.GetComponent(dummyLoadoutItem).EntityName
- : Loc.GetString($"loadout-name-{loadout.ID}"),
- },
- },
- },
- },
- };
- _button.OnToggled += OnButtonToggled;
- _button.AddStyleClass(style);
-
- var tooltip = new StringBuilder();
- // Add the loadout description to the tooltip if there is one
- var desc = !Loc.TryGetString($"loadout-description-{loadout.ID}", out var description)
- ? entityManager.GetComponent(dummyLoadoutItem).EntityDescription
- : description;
- if (!string.IsNullOrEmpty(desc))
- tooltip.Append($"{Loc.GetString(desc)}");
-
-
- // Get requirement reasons
- characterRequirementsSystem.CheckRequirementsValid(
- loadout.Requirements, highJob, profile, new Dictionary(),
- jobRequirementsManager.IsWhitelisted(),
- entityManager, prototypeManager, configManager,
- out var reasons);
-
- // Add requirement reasons to the tooltip
- foreach (var reason in reasons)
- tooltip.Append($"\n{reason.ToMarkup()}");
-
- // Combine the tooltip and format it in the checkbox supplier
- if (tooltip.Length > 0)
- {
- var formattedTooltip = new Tooltip();
- formattedTooltip.SetMessage(FormattedMessage.FromMarkupPermissive(tooltip.ToString()));
- _button.TooltipSupplier = _ => formattedTooltip;
- }
-
-
- // Add the loadout preview and the checkbox to the control
- AddChild(new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- Children = { previewLoadout, _button },
- });
- }
-
- private void OnButtonToggled(BaseButton.ButtonToggledEventArgs args)
- {
- PreferenceChanged?.Invoke(Preference);
- }
-}
diff --git a/Content.Client/Preferences/UI/RequirementsSelector.cs b/Content.Client/Preferences/UI/RequirementsSelector.cs
deleted file mode 100644
index 83b9695288..0000000000
--- a/Content.Client/Preferences/UI/RequirementsSelector.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System.Numerics;
-using Content.Client.Stylesheets;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Roles;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Client.Preferences.UI;
-
-public abstract class RequirementsSelector : BoxContainer where T : IPrototype
-{
- public T Proto { get; }
- public JobPrototype HighJob { get; }
- public bool Disabled => _lockStripe.Visible;
-
- protected readonly RadioOptions Options;
- private readonly StripeBack _lockStripe;
-
- protected RequirementsSelector(T proto, JobPrototype highJob)
- {
- Proto = proto;
- HighJob = highJob;
-
- Options = new RadioOptions(RadioOptionsLayout.Horizontal)
- {
- FirstButtonStyle = StyleBase.ButtonOpenRight,
- ButtonStyle = StyleBase.ButtonOpenBoth,
- LastButtonStyle = StyleBase.ButtonOpenLeft,
- };
- //Override default radio option button width
- Options.GenerateItem = GenerateButton;
- Options.OnItemSelected += args => Options.Select(args.Id);
-
- var requirementsLabel = new Label
- {
- Text = Loc.GetString("role-timer-locked"),
- Visible = true,
- HorizontalAlignment = HAlignment.Center,
- StyleClasses = {StyleBase.StyleClassLabelSubText},
- };
-
- _lockStripe = new StripeBack
- {
- Visible = false,
- HorizontalExpand = true,
- MouseFilter = MouseFilterMode.Stop,
- Children = { requirementsLabel },
- };
- }
-
- ///
- /// Actually adds the controls, must be called in the inheriting class' constructor.
- ///
- protected void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
- {
- foreach (var (text, value) in items)
- Options.AddItem(Loc.GetString(text), value);
-
- var titleLabel = new Label
- {
- Margin = new Thickness(5f, 0, 5f, 0),
- Text = title,
- MinSize = new Vector2(titleSize, 0),
- MouseFilter = MouseFilterMode.Stop,
- ToolTip = description
- };
-
- var container = new BoxContainer { Orientation = LayoutOrientation.Horizontal, };
-
- if (icon != null)
- container.AddChild(icon);
- container.AddChild(titleLabel);
- container.AddChild(Options);
- container.AddChild(_lockStripe);
-
- AddChild(container);
- }
-
- public void LockRequirements(FormattedMessage requirements)
- {
- var tooltip = new Tooltip();
- tooltip.SetMessage(requirements);
- _lockStripe.TooltipSupplier = _ => tooltip;
- _lockStripe.Visible = true;
- Options.Visible = false;
- }
-
- // TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
- public void UnlockRequirements()
- {
- _lockStripe.Visible = false;
- Options.Visible = true;
- }
-
- private Button GenerateButton(string text, int value)
- {
- return new Button
- {
- Text = text,
- MinWidth = 90
- };
- }
-}
diff --git a/Content.Client/Preferences/UI/TraitPreferenceSelector.cs b/Content.Client/Preferences/UI/TraitPreferenceSelector.cs
deleted file mode 100644
index e9ce1a5e9b..0000000000
--- a/Content.Client/Preferences/UI/TraitPreferenceSelector.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System.Text;
-using Content.Client.Players.PlayTimeTracking;
-using Content.Client.Stylesheets;
-using Content.Shared.Customization.Systems;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Content.Shared.Traits;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Shared.Configuration;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Client.Preferences.UI;
-
-
-public sealed class TraitPreferenceSelector : Control
-{
- public TraitPrototype Trait { get; }
- private readonly Button _button;
-
- public bool Preference
- {
- get => _button.Pressed;
- set => _button.Pressed = value;
- }
-
- public event Action? PreferenceChanged;
-
- public TraitPreferenceSelector(TraitPrototype trait, JobPrototype highJob,
- HumanoidCharacterProfile profile, string style, IEntityManager entityManager,
- IPrototypeManager prototypeManager,
- IConfigurationManager configManager, CharacterRequirementsSystem characterRequirementsSystem,
- JobRequirementsManager jobRequirementsManager)
- {
- Trait = trait;
-
- // Create a checkbox to get the loadout
- _button = new Button
- {
- VerticalAlignment = Control.VAlignment.Center,
- ToggleMode = true,
- StyleClasses = { StyleBase.ButtonOpenLeft },
- Children =
- {
- new BoxContainer
- {
- Children =
- {
- new Label
- {
- Text = trait.Points.ToString(),
- StyleClasses = { StyleBase.StyleClassLabelHeading },
- MinWidth = 32,
- MaxWidth = 32,
- ClipText = true,
- Margin = new Thickness(0, 0, 8, 0),
- },
- new Label { Text = Loc.GetString($"trait-name-{trait.ID}") },
- },
- },
- },
- };
- _button.OnToggled += OnButtonToggled;
- _button.AddStyleClass(style);
-
- var tooltip = new StringBuilder();
- // Add the loadout description to the tooltip if there is one
- var desc = Loc.GetString($"trait-description-{trait.ID}");
- if (!string.IsNullOrEmpty(desc) && desc != $"trait-description-{trait.ID}")
- tooltip.Append(desc);
-
-
- // Get requirement reasons
- characterRequirementsSystem.CheckRequirementsValid(
- trait.Requirements, highJob, profile, new Dictionary(),
- jobRequirementsManager.IsWhitelisted(),
- entityManager, prototypeManager, configManager,
- out var reasons);
-
- // Add requirement reasons to the tooltip
- foreach (var reason in reasons)
- tooltip.Append($"\n{reason.ToMarkup()}");
-
- // Combine the tooltip and format it in the checkbox supplier
- if (tooltip.Length > 0)
- {
- var formattedTooltip = new Tooltip();
- formattedTooltip.SetMessage(FormattedMessage.FromMarkupPermissive(tooltip.ToString()));
- _button.TooltipSupplier = _ => formattedTooltip;
- }
-
-
- // Add the loadout preview and the checkbox to the control
- AddChild(new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- Children = { _button },
- });
- }
-
- private void OnButtonToggled(BaseButton.ButtonToggledEventArgs args)
- {
- PreferenceChanged?.Invoke(Preference);
- }
-}
diff --git a/Content.Client/RCD/AlignRCDConstruction.cs b/Content.Client/RCD/AlignRCDConstruction.cs
index da7b22c91a..d5425425a7 100644
--- a/Content.Client/RCD/AlignRCDConstruction.cs
+++ b/Content.Client/RCD/AlignRCDConstruction.cs
@@ -16,9 +16,9 @@ public sealed class AlignRCDConstruction : PlacementMode
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly SharedMapSystem _mapSystem = default!;
- [Dependency] private readonly RCDSystem _rcdSystem = default!;
- [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ private readonly SharedMapSystem _mapSystem;
+ private readonly RCDSystem _rcdSystem;
+ private readonly SharedTransformSystem _transformSystem;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
@@ -32,12 +32,7 @@ public sealed class AlignRCDConstruction : PlacementMode
///
public AlignRCDConstruction(PlacementManager pMan) : base(pMan)
{
- var dependencies = IoCManager.Instance!;
- _entityManager = dependencies.Resolve();
- _mapManager = dependencies.Resolve();
- _playerManager = dependencies.Resolve();
- _stateManager = dependencies.Resolve();
-
+ IoCManager.InjectDependencies(this);
_mapSystem = _entityManager.System();
_rcdSystem = _entityManager.System();
_transformSystem = _entityManager.System();
diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs
index 51ec66ea44..3eb0397a69 100644
--- a/Content.Client/RCD/RCDMenu.xaml.cs
+++ b/Content.Client/RCD/RCDMenu.xaml.cs
@@ -68,7 +68,7 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui)
tooltip = Loc.GetString(entProto.Name);
}
- tooltip = char.ToUpper(tooltip[0]) + tooltip.Remove(0, 1);
+ tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1));
var button = new RCDMenuButton()
{
@@ -119,6 +119,12 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui)
SendRCDSystemMessageAction += bui.SendRCDSystemMessage;
}
+ private static string OopsConcat(string a, string b)
+ {
+ // This exists to prevent Roslyn being clever and compiling something that fails sandbox checks.
+ return a + b;
+ }
+
private void AddRCDMenuButtonOnClickActions(Control control)
{
var radialContainer = control as RadialContainer;
diff --git a/Content.Client/RadialSelector/RadialSelectorMenuBUI.cs b/Content.Client/RadialSelector/RadialSelectorMenuBUI.cs
new file mode 100644
index 0000000000..6b2a89f7a9
--- /dev/null
+++ b/Content.Client/RadialSelector/RadialSelectorMenuBUI.cs
@@ -0,0 +1,202 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Construction.Prototypes;
+using Content.Shared.RadialSelector;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+
+// ReSharper disable InconsistentNaming
+
+namespace Content.Client.RadialSelector;
+
+[UsedImplicitly]
+public sealed class RadialSelectorMenuBUI : BoundUserInterface
+{
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IResourceCache _resources = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+
+ private readonly SpriteSystem _spriteSystem;
+
+ private readonly RadialMenu _menu;
+
+ // Used to clearing on state changing
+ private readonly HashSet _cachedContainers = new();
+
+ private bool _openCentered;
+ private readonly Vector2 ItemSize = Vector2.One * 64;
+
+ public RadialSelectorMenuBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ _spriteSystem = _entManager.System();
+ _menu = new RadialMenu
+ {
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ BackButtonStyleClass = "RadialMenuBackButton",
+ CloseButtonStyleClass = "RadialMenuCloseButton"
+ };
+ }
+
+ protected override void Open()
+ {
+ _menu.OnClose += Close;
+
+ if (_openCentered)
+ _menu.OpenCentered();
+ else
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / _displayManager.ScreenSize);
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not RadialSelectorState radialSelectorState)
+ return;
+
+ ClearExistingContainers();
+ CreateMenu(radialSelectorState.Entries);
+ _openCentered = radialSelectorState.OpenCentered;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ _menu.Dispose();
+ }
+
+ private void CreateMenu(List entries, string parentCategory = "")
+ {
+ var container = new RadialContainer
+ {
+ Name = !string.IsNullOrEmpty(parentCategory) ? parentCategory : "Main",
+ Radius = 48f + 24f * MathF.Log(entries.Count),
+ };
+
+ _menu.AddChild(container);
+ _cachedContainers.Add(container);
+
+ foreach (var entry in entries)
+ {
+ if (entry.Category != null)
+ {
+ var button = CreateButton(entry.Category.Name, _spriteSystem.Frame0(entry.Category.Icon));
+ button.TargetLayer = entry.Category.Name;
+ CreateMenu(entry.Category.Entries, entry.Category.Name);
+ container.AddChild(button);
+ }
+ else if (entry.Prototype != null)
+ {
+ var name = GetName(entry.Prototype);
+ var icon = GetTextures(entry);
+ var button = CreateButton(name, icon);
+ button.OnButtonUp += _ =>
+ {
+ var msg = new RadialSelectorSelectedMessage(entry.Prototype);
+ SendPredictedMessage(msg);
+ };
+
+ container.AddChild(button);
+ }
+ }
+ }
+
+ private string GetName(string proto)
+ {
+ if (_protoManager.TryIndex(proto, out var prototype))
+ return prototype.Name;
+
+ if (_protoManager.TryIndex(proto, out ConstructionPrototype? constructionPrototype))
+ return constructionPrototype.Name;
+
+ return proto;
+ }
+
+ private List GetTextures(RadialSelectorEntry entry)
+ {
+ var result = new List();
+ if (entry.Icon is not null)
+ {
+ result.Add(_spriteSystem.Frame0(entry.Icon));
+ return result;
+ }
+
+ if (_protoManager.TryIndex(entry.Prototype!, out var prototype))
+ {
+ result.AddRange(SpriteComponent.GetPrototypeTextures(prototype, _resources).Select(o => o.Default));
+ return result;
+ }
+
+ if (_protoManager.TryIndex(entry.Prototype!, out ConstructionPrototype? constructionProto))
+ {
+ result.Add(_spriteSystem.Frame0(constructionProto.Icon));
+ return result;
+ }
+
+ // No icons provided and no icons found in prototypes. There's nothing we can do.
+ return result;
+ }
+
+ private RadialMenuTextureButton CreateButton(string name, Texture icon)
+ {
+ var button = new RadialMenuTextureButton
+ {
+ ToolTip = Loc.GetString(name),
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = ItemSize
+ };
+
+ var iconScale = ItemSize / icon.Size;
+ var texture = new TextureRect
+ {
+ VerticalAlignment = Control.VAlignment.Center,
+ HorizontalAlignment = Control.HAlignment.Center,
+ Texture = icon,
+ TextureScale = iconScale
+ };
+
+ button.AddChild(texture);
+ return button;
+ }
+
+ private RadialMenuTextureButton CreateButton(string name, List icons)
+ {
+ var button = new RadialMenuTextureButton
+ {
+ ToolTip = Loc.GetString(name),
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = ItemSize
+ };
+
+ var iconScale = ItemSize / icons[0].Size;
+ var texture = new LayeredTextureRect
+ {
+ VerticalAlignment = Control.VAlignment.Center,
+ HorizontalAlignment = Control.HAlignment.Center,
+ Textures = icons,
+ TextureScale = iconScale
+ };
+
+ button.AddChild(texture);
+ return button;
+ }
+
+ private void ClearExistingContainers()
+ {
+ foreach (var container in _cachedContainers)
+ _menu.RemoveChild(container);
+
+ _cachedContainers.Clear();
+ }
+}
diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs
index 2ee7e30ec9..d00e319eed 100644
--- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs
+++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs
@@ -195,9 +195,16 @@ private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component,
if (uid != _player.LocalEntity)
return;
- if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace)
+ if (args.Transform.MapUid != null || args.OldMapId == null)
return;
+ if (_spectatorData != null)
+ {
+ // Currently scrubbing/setting the replay tick
+ // the observer will get respawned once the state was applied
+ return;
+ }
+
// The entity being spectated from was moved to null-space.
// This was probably because they were spectating some entity in a client-side replay that left PVS range.
// Simple respawn the ghost.
diff --git a/Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs b/Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs
new file mode 100644
index 0000000000..0219c965cd
--- /dev/null
+++ b/Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared.Robotics.Systems;
+
+namespace Content.Client.Robotics.Systems;
+
+public sealed class RoboticsConsoleSystem : SharedRoboticsConsoleSystem
+{
+}
diff --git a/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs b/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs
new file mode 100644
index 0000000000..6185979eee
--- /dev/null
+++ b/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs
@@ -0,0 +1,50 @@
+using Content.Shared.Robotics;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Robotics.UI;
+
+public sealed class RoboticsConsoleBoundUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ public RoboticsConsoleWindow _window = default!;
+
+ public RoboticsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new RoboticsConsoleWindow(Owner);
+ _window.OnDisablePressed += address =>
+ {
+ SendMessage(new RoboticsConsoleDisableMessage(address));
+ };
+ _window.OnDestroyPressed += address =>
+ {
+ SendMessage(new RoboticsConsoleDestroyMessage(address));
+ };
+ _window.OnClose += Close;
+
+ _window.OpenCentered();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not RoboticsConsoleState cast)
+ return;
+
+ _window?.UpdateState(cast);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ _window?.Dispose();
+ }
+}
diff --git a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml
new file mode 100644
index 0000000000..a3b3978790
--- /dev/null
+++ b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
new file mode 100644
index 0000000000..367114f2aa
--- /dev/null
+++ b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
@@ -0,0 +1,147 @@
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Lock;
+using Content.Shared.Robotics;
+using Content.Shared.Robotics.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Robotics.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class RoboticsConsoleWindow : FancyWindow
+{
+ [Dependency] private readonly IEntityManager _entMan = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ private readonly LockSystem _lock;
+ private readonly SpriteSystem _sprite;
+
+ public Action? OnDisablePressed;
+ public Action? OnDestroyPressed;
+
+ private Entity _console;
+ private string? _selected;
+ private Dictionary _cyborgs = new();
+
+ public RoboticsConsoleWindow(EntityUid console)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _lock = _entMan.System();
+ _sprite = _entMan.System();
+
+ _console = (console, _entMan.GetComponent(console), null);
+ _entMan.TryGetComponent(_console, out _console.Comp2);
+
+ Cyborgs.OnItemSelected += args =>
+ {
+ if (Cyborgs[args.ItemIndex].Metadata is not string address)
+ return;
+
+ _selected = address;
+ PopulateData();
+ };
+ Cyborgs.OnItemDeselected += _ =>
+ {
+ _selected = null;
+ PopulateData();
+ };
+
+ // these won't throw since buttons are only visible if a borg is selected
+ DisableButton.OnPressed += _ =>
+ {
+ OnDisablePressed?.Invoke(_selected!);
+ };
+ DestroyButton.OnPressed += _ =>
+ {
+ OnDestroyPressed?.Invoke(_selected!);
+ };
+
+ // cant put multiple styles in xaml for some reason
+ DestroyButton.StyleClasses.Add(StyleBase.ButtonCaution);
+ }
+
+ public void UpdateState(RoboticsConsoleState state)
+ {
+ _cyborgs = state.Cyborgs;
+
+ // clear invalid selection
+ if (_selected is {} selected && !_cyborgs.ContainsKey(selected))
+ _selected = null;
+
+ var hasCyborgs = _cyborgs.Count > 0;
+ NoCyborgs.Visible = !hasCyborgs;
+ CyborgsContainer.Visible = hasCyborgs;
+ PopulateCyborgs();
+
+ PopulateData();
+
+ var locked = _lock.IsLocked((_console, _console.Comp2));
+ DangerZone.Visible = !locked;
+ LockedMessage.Visible = locked;
+ }
+
+ private void PopulateCyborgs()
+ {
+ // _selected might get set to null when recreating so copy it first
+ var selected = _selected;
+ Cyborgs.Clear();
+ foreach (var (address, data) in _cyborgs)
+ {
+ var item = Cyborgs.AddItem(data.Name, _sprite.Frame0(data.ChassisSprite!), metadata: address);
+ item.Selected = address == selected;
+ }
+ _selected = selected;
+ }
+
+ private void PopulateData()
+ {
+ if (_selected is not {} selected)
+ {
+ SelectCyborg.Visible = true;
+ BorgContainer.Visible = false;
+ return;
+ }
+
+ SelectCyborg.Visible = false;
+ BorgContainer.Visible = true;
+
+ var data = _cyborgs[selected];
+ var model = data.ChassisName;
+
+ BorgSprite.Texture = _sprite.Frame0(data.ChassisSprite!);
+
+ var batteryColor = data.Charge switch {
+ < 0.2f => "red",
+ < 0.4f => "orange",
+ < 0.6f => "yellow",
+ < 0.8f => "green",
+ _ => "blue"
+ };
+
+ var text = new FormattedMessage();
+ text.PushMarkup(Loc.GetString("robotics-console-model", ("name", model)));
+ text.AddMarkup(Loc.GetString("robotics-console-designation"));
+ text.AddText($" {data.Name}\n"); // prevent players trolling by naming borg [color=red]satan[/color]
+ text.PushMarkup(Loc.GetString("robotics-console-battery", ("charge", (int) (data.Charge * 100f)), ("color", batteryColor)));
+ text.PushMarkup(Loc.GetString("robotics-console-brain", ("brain", data.HasBrain)));
+ text.AddMarkup(Loc.GetString("robotics-console-modules", ("count", data.ModuleCount)));
+ BorgInfo.SetMessage(text);
+
+ // how the turntables
+ DisableButton.Disabled = !data.HasBrain;
+ DestroyButton.Disabled = _timing.CurTime < _console.Comp1.NextDestroy;
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ DestroyButton.Disabled = _timing.CurTime < _console.Comp1.NextDestroy;
+ }
+}
diff --git a/Content.Client/RoundEnd/RoundEndSummaryUIController.cs b/Content.Client/RoundEnd/RoundEndSummaryUIController.cs
new file mode 100644
index 0000000000..cf824833ef
--- /dev/null
+++ b/Content.Client/RoundEnd/RoundEndSummaryUIController.cs
@@ -0,0 +1,51 @@
+using Content.Client.GameTicking.Managers;
+using Content.Shared.GameTicking;
+using Content.Shared.Input;
+using JetBrains.Annotations;
+using Robust.Client.Input;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Player;
+
+namespace Content.Client.RoundEnd;
+
+[UsedImplicitly]
+public sealed class RoundEndSummaryUIController : UIController,
+ IOnSystemLoaded
+{
+ [Dependency] private readonly IInputManager _input = default!;
+
+ private RoundEndSummaryWindow? _window;
+
+ private void ToggleScoreboardWindow(ICommonSession? session = null)
+ {
+ if (_window == null)
+ return;
+
+ if (_window.IsOpen)
+ {
+ _window.Close();
+ }
+ else
+ {
+ _window.OpenCenteredRight();
+ _window.MoveToFront();
+ }
+ }
+
+ public void OpenRoundEndSummaryWindow(RoundEndMessageEvent message)
+ {
+ // Don't open duplicate windows (mainly for replays).
+ if (_window?.RoundId == message.RoundId)
+ return;
+
+ _window = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText,
+ message.RoundDuration, message.RoundId, message.AllPlayersEndInfo, EntityManager);
+ }
+
+ public void OnSystemLoaded(ClientGameTicker system)
+ {
+ _input.SetInputCommand(ContentKeyFunctions.ToggleRoundEndSummaryWindow,
+ InputCmdHandler.FromDelegate(ToggleScoreboardWindow));
+ }
+}
diff --git a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
index 5b73c77934..9c9f83a427 100644
--- a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
+++ b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
@@ -2,7 +2,6 @@
using System.Numerics;
using Content.Client.Message;
using Content.Shared.GameTicking;
-using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Utility;
diff --git a/Content.Client/Salvage/UI/OfferingWindowOption.xaml.cs b/Content.Client/Salvage/UI/OfferingWindowOption.xaml.cs
index 7855577e69..ecff59095b 100644
--- a/Content.Client/Salvage/UI/OfferingWindowOption.xaml.cs
+++ b/Content.Client/Salvage/UI/OfferingWindowOption.xaml.cs
@@ -74,12 +74,12 @@ public bool Claimed
if (_claimed)
{
- ClaimButton.AddStyleClass(StyleBase.ButtonCaution);
+ ClaimButton.AddStyleClass(StyleBase.ButtonDanger);
ClaimButton.Text = Loc.GetString("offering-window-claimed");
}
else
{
- ClaimButton.RemoveStyleClass(StyleBase.ButtonCaution);
+ ClaimButton.RemoveStyleClass(StyleBase.ButtonDanger);
ClaimButton.Text = Loc.GetString("offering-window-claim");
}
}
diff --git a/Content.Client/Shadowkin/EtherealSystem.cs b/Content.Client/Shadowkin/EtherealSystem.cs
new file mode 100644
index 0000000000..cb289a87f1
--- /dev/null
+++ b/Content.Client/Shadowkin/EtherealSystem.cs
@@ -0,0 +1,52 @@
+using Content.Shared.Shadowkin;
+using Robust.Client.Graphics;
+using Robust.Shared.Player;
+using Content.Client.Overlays;
+
+namespace Content.Client.Shadowkin;
+
+public sealed partial class EtherealSystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerMan = default!;
+
+ private EtherealOverlay _overlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(Onhutdown);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ _overlay = new();
+ }
+
+ private void OnInit(EntityUid uid, EtherealComponent component, ComponentInit args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void Onhutdown(EntityUid uid, EtherealComponent component, ComponentShutdown args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, EtherealComponent component, LocalPlayerAttachedEvent args)
+ {
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnPlayerDetached(EntityUid uid, EtherealComponent component, LocalPlayerDetachedEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/Shadowkin/ShadowkinSystem.cs b/Content.Client/Shadowkin/ShadowkinSystem.cs
new file mode 100644
index 0000000000..d8e1b69fc7
--- /dev/null
+++ b/Content.Client/Shadowkin/ShadowkinSystem.cs
@@ -0,0 +1,114 @@
+using Content.Shared.Shadowkin;
+using Content.Shared.CCVar;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
+using Content.Shared.Humanoid;
+using Content.Shared.Abilities.Psionics;
+using Content.Client.Overlays;
+
+namespace Content.Client.Shadowkin;
+
+public sealed partial class ShadowkinSystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerMan = default!;
+
+ private ColorTintOverlay _overlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(Onhutdown);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ Subs.CVar(_cfg, CCVars.NoVisionFilters, OnNoVisionFiltersChanged);
+
+ _overlay = new();
+ }
+
+ private void OnInit(EntityUid uid, ShadowkinComponent component, ComponentInit args)
+ {
+ if (uid != _playerMan.LocalEntity
+ || _cfg.GetCVar(CCVars.NoVisionFilters))
+ return;
+
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void Onhutdown(EntityUid uid, ShadowkinComponent component, ComponentShutdown args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, ShadowkinComponent component, LocalPlayerAttachedEvent args)
+ {
+ if (_cfg.GetCVar(CCVars.NoVisionFilters))
+ return;
+
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnPlayerDetached(EntityUid uid, ShadowkinComponent component, LocalPlayerDetachedEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnNoVisionFiltersChanged(bool enabled)
+ {
+ if (enabled)
+ _overlayMan.RemoveOverlay(_overlay);
+ else
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (_cfg.GetCVar(CCVars.NoVisionFilters))
+ return;
+
+ var uid = _playerMan.LocalEntity;
+ if (uid == null
+ || !TryComp(uid, out var comp)
+ || !TryComp(uid, out var humanoid))
+ return;
+
+ // 1/3 = 0.333...
+ // intensity = min + (power / max)
+ // intensity = intensity / 0.333
+ // intensity = clamp intensity min, max
+
+ var tintIntensity = 0.65f;
+ if (TryComp(uid, out var magic))
+ {
+ var min = 0.45f;
+ var max = 0.75f;
+ tintIntensity = Math.Clamp(min + (magic.Mana / magic.MaxMana) * 0.333f, min, max);
+ }
+
+ UpdateShader(new Vector3(humanoid.EyeColor.R, humanoid.EyeColor.G, humanoid.EyeColor.B), tintIntensity);
+ }
+
+ private void UpdateShader(Vector3? color, float? intensity)
+ {
+ while (_overlayMan.HasOverlay())
+ _overlayMan.RemoveOverlay(_overlay);
+
+ if (color != null)
+ _overlay.TintColor = color;
+ if (intensity != null)
+ _overlay.TintAmount = intensity;
+
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
+ _overlayMan.AddOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/ShortConstruction/ShortConstructionSystem.cs b/Content.Client/ShortConstruction/ShortConstructionSystem.cs
new file mode 100644
index 0000000000..492411977b
--- /dev/null
+++ b/Content.Client/ShortConstruction/ShortConstructionSystem.cs
@@ -0,0 +1,46 @@
+using Content.Client.Construction;
+using Content.Shared.Construction.Prototypes;
+using Content.Shared.RadialSelector;
+using Content.Shared.ShortConstruction;
+using Robust.Client.Placement;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client.ShortConstruction;
+
+public sealed class ShortConstructionSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPlacementManager _placement = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
+ [Dependency] private readonly ConstructionSystem _construction = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnItemRecieved);
+ }
+
+ private void OnItemRecieved(Entity ent, ref RadialSelectorSelectedMessage args)
+ {
+ if (!_proto.TryIndex(args.SelectedItem, out ConstructionPrototype? prototype) ||
+ !_gameTiming.IsFirstTimePredicted)
+ return;
+
+ if (prototype.Type == ConstructionType.Item)
+ {
+ _construction.TryStartItemConstruction(prototype.ID);
+ return;
+ }
+
+ _placement.BeginPlacing(new PlacementInformation
+ {
+ IsTile = false,
+ PlacementOption = prototype.PlacementMode
+ },
+ new ConstructionPlacementHijack(_construction, prototype));
+ }
+}
diff --git a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
index 7086fd0541..d5154a87be 100644
--- a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
+++ b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Content.Shared.Shuttles.Events;
using Content.Shared.Shuttles.Systems;
using Robust.Client.Graphics;
@@ -75,6 +76,6 @@ protected override void Draw(in OverlayDrawArgs args)
args.WorldHandle.SetTransform(xform.WorldMatrix);
args.WorldHandle.DrawRect(Position.Value, Color.Red.WithAlpha(100));
- args.WorldHandle.SetTransform(Matrix3.Identity);
+ args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
}
diff --git a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs
index 284c668190..b50d8fa6b2 100644
--- a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Shuttles.Components;
using Robust.Client.AutoGenerated;
@@ -100,7 +101,7 @@ protected void DrawCircles(DrawingHandleScreen handle)
var textDimensions = handle.GetDimensions(Font, text, UIScale);
handle.DrawCircle(origin, scaledRadius, color, false);
- handle.DrawString(Font, ScalePosition(new Vector2(0f, -radius)) - new Vector2(0f, textDimensions.Y), text, color);
+ handle.DrawString(Font, ScalePosition(new Vector2(0f, -radius)) - new Vector2(0f, textDimensions.Y), text, UIScale, color);
}
const int gridLinesRadial = 8;
@@ -115,7 +116,7 @@ protected void DrawCircles(DrawingHandleScreen handle)
}
}
- protected void DrawGrid(DrawingHandleScreen handle, Matrix3 matrix, Entity grid, Color color, float alpha = 0.01f)
+ protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 matrix, Entity grid, Color color, float alpha = 0.01f)
{
var rator = Maps.GetAllTilesEnumerator(grid.Owner, grid.Comp);
var minimapScale = MinimapScale;
@@ -289,7 +290,7 @@ private record struct GridDrawJob : IParallelRobustJob
public float MinimapScale;
public Vector2 MidPoint;
- public Matrix3 Matrix;
+ public Matrix3x2 Matrix;
public List Vertices;
public Vector2[] ScaledVertices;
@@ -297,7 +298,7 @@ private record struct GridDrawJob : IParallelRobustJob
public void Execute(int index)
{
var vert = Vertices[index];
- var adjustedVert = Matrix.Transform(vert);
+ var adjustedVert = Vector2.Transform(vert, Matrix);
adjustedVert = adjustedVert with { Y = -adjustedVert.Y };
var scaledVert = ScalePosition(adjustedVert, MinimapScale, MidPoint);
diff --git a/Content.Client/Shuttles/UI/DockObject.xaml.cs b/Content.Client/Shuttles/UI/DockObject.xaml.cs
index 9dae6b7a4d..d7d565a23e 100644
--- a/Content.Client/Shuttles/UI/DockObject.xaml.cs
+++ b/Content.Client/Shuttles/UI/DockObject.xaml.cs
@@ -1,6 +1,7 @@
using System.Text;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Systems;
+using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -12,7 +13,10 @@ namespace Content.Client.Shuttles.UI;
[GenerateTypedNameReferences]
public sealed partial class DockObject : PanelContainer
{
+ [PublicAPI]
public event Action? UndockPressed;
+
+ [PublicAPI]
public event Action? ViewPressed;
public BoxContainer ContentsContainer => Contents;
diff --git a/Content.Client/Shuttles/UI/NavScreen.xaml.cs b/Content.Client/Shuttles/UI/NavScreen.xaml.cs
index b7b757ea48..91d95aaa04 100644
--- a/Content.Client/Shuttles/UI/NavScreen.xaml.cs
+++ b/Content.Client/Shuttles/UI/NavScreen.xaml.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Content.Shared.Shuttles.BUIStates;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -68,7 +69,7 @@ protected override void Draw(DrawingHandleScreen handle)
}
var (_, worldRot, worldMatrix) = _xformSystem.GetWorldPositionRotationMatrix(gridXform);
- var worldPos = worldMatrix.Transform(gridBody.LocalCenter);
+ var worldPos = Vector2.Transform(gridBody.LocalCenter, worldMatrix);
// Get the positive reduced angle.
var displayRot = -worldRot.Reduced();
diff --git a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
index f03c440295..31f0eecad7 100644
--- a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
@@ -108,10 +108,10 @@ protected override void Draw(DrawingHandleScreen handle)
var gridNent = EntManager.GetNetEntity(GridEntity);
var mapPos = _xformSystem.ToMapCoordinates(_coordinates.Value);
var ourGridMatrix = _xformSystem.GetWorldMatrix(gridXform.Owner);
- var dockMatrix = Matrix3.CreateTransform(_coordinates.Value.Position, Angle.Zero);
- Matrix3.Multiply(dockMatrix, ourGridMatrix, out var offsetMatrix);
+ var dockMatrix = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, Angle.Zero);
+ var worldFromDock = Matrix3x2.Multiply(dockMatrix, ourGridMatrix);
- offsetMatrix = offsetMatrix.Invert();
+ Matrix3x2.Invert(worldFromDock, out var offsetMatrix);
// Draw nearby grids
var controlBounds = PixelSizeBox;
@@ -137,7 +137,7 @@ protected override void Draw(DrawingHandleScreen handle)
continue;
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
- Matrix3.Multiply(in gridMatrix, in offsetMatrix, out var matty);
+ var matty = Matrix3x2.Multiply(gridMatrix, offsetMatrix);
var color = _shuttles.GetIFFColor(grid.Owner, grid.Owner == GridEntity, component: iffComp);
DrawGrid(handle, matty, grid, color);
@@ -151,23 +151,23 @@ protected override void Draw(DrawingHandleScreen handle)
if (ViewedDock == dock.Entity)
continue;
- var position = matty.Transform(dock.Coordinates.Position);
+ var position = Vector2.Transform(dock.Coordinates.Position, matty);
- var otherDockRotation = Matrix3.CreateRotation(dock.Angle);
+ var otherDockRotation = Matrix3Helpers.CreateRotation(dock.Angle);
var scaledPos = ScalePosition(position with {Y = -position.Y});
if (!controlBounds.Contains(scaledPos.Floored()))
continue;
// Draw the dock's collision
- var collisionBL = matty.Transform(dock.Coordinates.Position +
- otherDockRotation.Transform(new Vector2(-0.2f, -0.7f)));
- var collisionBR = matty.Transform(dock.Coordinates.Position +
- otherDockRotation.Transform(new Vector2(0.2f, -0.7f)));
- var collisionTR = matty.Transform(dock.Coordinates.Position +
- otherDockRotation.Transform(new Vector2(0.2f, -0.5f)));
- var collisionTL = matty.Transform(dock.Coordinates.Position +
- otherDockRotation.Transform(new Vector2(-0.2f, -0.5f)));
+ var collisionBL = Vector2.Transform(dock.Coordinates.Position +
+ Vector2.Transform(new Vector2(-0.2f, -0.7f), otherDockRotation), matty);
+ var collisionBR = Vector2.Transform(dock.Coordinates.Position +
+ Vector2.Transform(new Vector2(0.2f, -0.7f), otherDockRotation), matty);
+ var collisionTR = Vector2.Transform(dock.Coordinates.Position +
+ Vector2.Transform(new Vector2(0.2f, -0.5f), otherDockRotation), matty);
+ var collisionTL = Vector2.Transform(dock.Coordinates.Position +
+ Vector2.Transform(new Vector2(-0.2f, -0.5f), otherDockRotation), matty);
var verts = new[]
{
@@ -195,10 +195,10 @@ protected override void Draw(DrawingHandleScreen handle)
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, verts, otherDockConnection);
// Draw the dock itself
- var dockBL = matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f));
- var dockBR = matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f));
- var dockTR = matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f));
- var dockTL = matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f));
+ var dockBL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f), matty);
+ var dockBR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f), matty);
+ var dockTR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f), matty);
+ var dockTL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f), matty);
verts = new[]
{
@@ -308,14 +308,14 @@ protected override void Draw(DrawingHandleScreen handle)
// Draw the dock's collision
var invertedPosition = Vector2.Zero;
invertedPosition.Y = -invertedPosition.Y;
- var rotation = Matrix3.CreateRotation(-_angle.Value + MathF.PI);
+ var rotation = Matrix3Helpers.CreateRotation(-_angle.Value + MathF.PI);
var ourDockConnection = new UIBox2(
- ScalePosition(rotation.Transform(new Vector2(-0.2f, -0.7f))),
- ScalePosition(rotation.Transform(new Vector2(0.2f, -0.5f))));
+ ScalePosition(Vector2.Transform(new Vector2(-0.2f, -0.7f), rotation)),
+ ScalePosition(Vector2.Transform(new Vector2(0.2f, -0.5f), rotation)));
var ourDock = new UIBox2(
- ScalePosition(rotation.Transform(new Vector2(-0.5f, 0.5f))),
- ScalePosition(rotation.Transform(new Vector2(0.5f, -0.5f))));
+ ScalePosition(Vector2.Transform(new Vector2(-0.5f, 0.5f), rotation)),
+ ScalePosition(Vector2.Transform(new Vector2(0.5f, -0.5f), rotation)));
var dockColor = Color.Magenta;
var connectionColor = Color.Pink;
diff --git a/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
index 2f35a8dffd..8bd4a338cb 100644
--- a/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
@@ -114,7 +114,7 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args)
var beaconsOnly = EntManager.TryGetComponent(mapUid, out FTLDestinationComponent? destComp) &&
destComp.BeaconsOnly;
- var mapTransform = Matrix3.CreateInverseTransform(Offset, Angle.Zero);
+ var mapTransform = Matrix3Helpers.CreateInverseTransform(Offset, Angle.Zero);
if (beaconsOnly && TryGetBeacon(_beacons, mapTransform, args.RelativePixelPosition, PixelRect, out var foundBeacon, out _))
{
@@ -203,7 +203,7 @@ private void DrawParallax(DrawingHandleScreen handle)
///
///
///
- private List GetViewportMapObjects(Matrix3 matty, List mapObjects)
+ private List GetViewportMapObjects(Matrix3x2 matty, List mapObjects)
{
var results = new List();
var enlargement = new Vector2i((int) (16 * UIScale), (int) (16 * UIScale));
@@ -217,7 +217,7 @@ private List GetViewportMapObjects(Matrix3 matty, List m
var mapCoords = _shuttles.GetMapCoordinates(mapObj);
- var relativePos = matty.Transform(mapCoords.Position);
+ var relativePos = Vector2.Transform(mapCoords.Position, matty);
relativePos = relativePos with { Y = -relativePos.Y };
var uiPosition = ScalePosition(relativePos);
@@ -250,7 +250,7 @@ protected override void Draw(DrawingHandleScreen handle)
DrawParallax(handle);
var viewedMapUid = _mapManager.GetMapEntityId(ViewingMap);
- var matty = Matrix3.CreateInverseTransform(Offset, Angle.Zero);
+ var matty = Matrix3Helpers.CreateInverseTransform(Offset, Angle.Zero);
var realTime = _timing.RealTime;
var viewBox = new Box2(Offset - WorldRangeVector, Offset + WorldRangeVector);
var viewportObjects = GetViewportMapObjects(matty, mapObjects);
@@ -267,7 +267,7 @@ protected override void Draw(DrawingHandleScreen handle)
var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(shuttleXform);
gridPos = Maps.GetGridPosition((gridUid, gridPhysics), gridPos, gridRot);
- var gridRelativePos = matty.Transform(gridPos);
+ var gridRelativePos = Vector2.Transform(gridPos, matty);
gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
var gridUiPos = ScalePosition(gridRelativePos);
@@ -296,7 +296,7 @@ protected override void Draw(DrawingHandleScreen handle)
continue;
}
- var adjustedPos = matty.Transform(mapCoords.Position);
+ var adjustedPos = Vector2.Transform(mapCoords.Position, matty);
var localPos = ScalePosition(adjustedPos with { Y = -adjustedPos.Y});
handle.DrawCircle(localPos, exclusion.Range * MinimapScale, exclusionColor.WithAlpha(0.05f));
handle.DrawCircle(localPos, exclusion.Range * MinimapScale, exclusionColor, filled: false);
@@ -319,7 +319,7 @@ protected override void Draw(DrawingHandleScreen handle)
foreach (var (beaconName, coords, mapO) in GetBeacons(viewportObjects, matty, controlLocalBounds))
{
- var localPos = matty.Transform(coords.Position);
+ var localPos = Vector2.Transform(coords.Position, matty);
localPos = localPos with { Y = -localPos.Y };
var beaconUiPos = ScalePosition(localPos);
var mapObject = GetMapObject(localPos, Angle.Zero, scale: 0.75f, scalePosition: true);
@@ -360,7 +360,7 @@ protected override void Draw(DrawingHandleScreen handle)
var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(grid.Owner);
gridPos = Maps.GetGridPosition((grid, gridPhysics), gridPos, gridRot);
- var gridRelativePos = matty.Transform(gridPos);
+ var gridRelativePos = Vector2.Transform(gridPos, matty);
gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
var gridUiPos = ScalePosition(gridRelativePos);
@@ -439,7 +439,7 @@ protected override void Draw(DrawingHandleScreen handle)
var color = ftlFree ? Color.LimeGreen : Color.Magenta;
- var gridRelativePos = matty.Transform(gridPos);
+ var gridRelativePos = Vector2.Transform(gridPos, matty);
gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
var gridUiPos = ScalePosition(gridRelativePos);
@@ -512,7 +512,7 @@ private void AddMapObject(List edges, List verts, ValueList
/// Returns the beacons that intersect the viewport.
///
- private IEnumerable<(string Beacon, MapCoordinates Coordinates, IMapObject MapObject)> GetBeacons(List mapObjs, Matrix3 mapTransform, UIBox2i area)
+ private IEnumerable<(string Beacon, MapCoordinates Coordinates, IMapObject MapObject)> GetBeacons(List mapObjs, Matrix3x2 mapTransform, UIBox2i area)
{
foreach (var mapO in mapObjs)
{
@@ -520,7 +520,7 @@ private void AddMapObject(List edges, List verts, ValueList GetMapObject(Vector2 localPos, Angle angle, float sca
return mapObj;
}
- private bool TryGetBeacon(IEnumerable mapObjects, Matrix3 mapTransform, Vector2 mousePos, UIBox2i area, out ShuttleBeaconObject foundBeacon, out Vector2 foundLocalPos)
+ private bool TryGetBeacon(IEnumerable mapObjects, Matrix3x2 mapTransform, Vector2 mousePos, UIBox2i area, out ShuttleBeaconObject foundBeacon, out Vector2 foundLocalPos)
{
// In pixels
const float BeaconSnapRange = 32f;
@@ -579,7 +579,7 @@ private bool TryGetBeacon(IEnumerable mapObjects, Matrix3 mapTransfo
if (!_shuttles.CanFTLBeacon(beaconObj.Coordinates))
continue;
- var position = mapTransform.Transform(beaconCoords.Position);
+ var position = Vector2.Transform(beaconCoords.Position, mapTransform);
var localPos = ScalePosition(position with {Y = -position.Y});
// If beacon not on screen then ignore it.
diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
index 00ee6890b2..0b8720add2 100644
--- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
@@ -137,10 +137,10 @@ protected override void Draw(DrawingHandleScreen handle)
var mapPos = _transform.ToMapCoordinates(_coordinates.Value);
var offset = _coordinates.Value.Position;
- var posMatrix = Matrix3.CreateTransform(offset, _rotation.Value);
+ var posMatrix = Matrix3Helpers.CreateTransform(offset, _rotation.Value);
var (_, ourEntRot, ourEntMatrix) = _transform.GetWorldPositionRotationMatrix(_coordinates.Value.EntityId);
- Matrix3.Multiply(posMatrix, ourEntMatrix, out var ourWorldMatrix);
- var ourWorldMatrixInvert = ourWorldMatrix.Invert();
+ var ourWorldMatrix = Matrix3x2.Multiply(posMatrix, ourEntMatrix);
+ Matrix3x2.Invert(ourWorldMatrix, out var ourWorldMatrixInvert);
// Draw our grid in detail
var ourGridId = xform.GridUid;
@@ -148,7 +148,7 @@ protected override void Draw(DrawingHandleScreen handle)
fixturesQuery.HasComponent(ourGridId.Value))
{
var ourGridMatrix = _transform.GetWorldMatrix(ourGridId.Value);
- Matrix3.Multiply(in ourGridMatrix, in ourWorldMatrixInvert, out var matrix);
+ var matrix = Matrix3x2.Multiply(ourGridMatrix, ourWorldMatrixInvert);
var color = _shuttles.GetIFFColor(ourGridId.Value, self: true);
DrawGrid(handle, matrix, (ourGridId.Value, ourGrid), color);
@@ -194,7 +194,7 @@ protected override void Draw(DrawingHandleScreen handle)
continue;
var gridMatrix = _transform.GetWorldMatrix(gUid);
- Matrix3.Multiply(in gridMatrix, in ourWorldMatrixInvert, out var matty);
+ var matty = Matrix3x2.Multiply(gridMatrix, ourWorldMatrixInvert);
var color = _shuttles.GetIFFColor(grid, self: false, iff);
// Others default:
@@ -207,7 +207,7 @@ protected override void Draw(DrawingHandleScreen handle)
{
var gridBounds = grid.Comp.LocalAABB;
- var gridCentre = matty.Transform(gridBody.LocalCenter);
+ var gridCentre = Vector2.Transform(gridBody.LocalCenter, matty);
gridCentre.Y = -gridCentre.Y;
var distance = gridCentre.Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
@@ -242,7 +242,7 @@ protected override void Draw(DrawingHandleScreen handle)
}
}
- private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3 matrix)
+ private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3x2 matrix)
{
if (!ShowDocks)
return;
@@ -255,7 +255,7 @@ private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3 matrix
foreach (var state in docks)
{
var position = state.Coordinates.Position;
- var uiPosition = matrix.Transform(position);
+ var uiPosition = Vector2.Transform(position, matrix);
if (uiPosition.Length() > (WorldRange * 2f) - DockScale)
continue;
@@ -264,10 +264,10 @@ private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3 matrix
var verts = new[]
{
- matrix.Transform(position + new Vector2(-DockScale, -DockScale)),
- matrix.Transform(position + new Vector2(DockScale, -DockScale)),
- matrix.Transform(position + new Vector2(DockScale, DockScale)),
- matrix.Transform(position + new Vector2(-DockScale, DockScale)),
+ Vector2.Transform(position + new Vector2(-DockScale, -DockScale), matrix),
+ Vector2.Transform(position + new Vector2(DockScale, -DockScale), matrix),
+ Vector2.Transform(position + new Vector2(DockScale, DockScale), matrix),
+ Vector2.Transform(position + new Vector2(-DockScale, DockScale), matrix),
};
for (var i = 0; i < verts.Length; i++)
diff --git a/Content.Client/Standing/LayingDownSystem.cs b/Content.Client/Standing/LayingDownSystem.cs
new file mode 100644
index 0000000000..d45d481134
--- /dev/null
+++ b/Content.Client/Standing/LayingDownSystem.cs
@@ -0,0 +1,89 @@
+using Content.Shared.Buckle;
+using Content.Shared.Rotation;
+using Content.Shared.Standing;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Standing;
+
+public sealed class LayingDownSystem : SharedLayingDownSystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly StandingStateSystem _standing = default!;
+ [Dependency] private readonly AnimationPlayerSystem _animation = default!;
+ [Dependency] private readonly SharedBuckleSystem _buckle = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMovementInput);
+ SubscribeNetworkEvent(OnCheckAutoGetUp);
+ }
+
+ public override void Update(float frameTime)
+ {
+ // Update draw depth of laying down entities as necessary
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var layingDown, out var standing, out var sprite))
+ {
+ // Do not modify the entities draw depth if it's modified externally
+ if (sprite.DrawDepth != layingDown.NormalDrawDepth && sprite.DrawDepth != layingDown.CrawlingUnderDrawDepth)
+ continue;
+
+ sprite.DrawDepth = standing.CurrentState is StandingState.Lying && layingDown.IsCrawlingUnder
+ ? layingDown.CrawlingUnderDrawDepth
+ : layingDown.NormalDrawDepth;
+ }
+
+ query.Dispose();
+ }
+
+ private void OnMovementInput(EntityUid uid, LayingDownComponent component, MoveEvent args)
+ {
+ if (!_timing.IsFirstTimePredicted
+ || !_standing.IsDown(uid)
+ || _buckle.IsBuckled(uid)
+ || _animation.HasRunningAnimation(uid, "rotate")
+ || !TryComp(uid, out var transform)
+ || !TryComp(uid, out var sprite)
+ || !TryComp(uid, out var rotationVisuals))
+ return;
+
+ var rotation = transform.LocalRotation + (_eyeManager.CurrentEye.Rotation - (transform.LocalRotation - transform.WorldRotation));
+
+ if (rotation.GetDir() is Direction.SouthEast or Direction.East or Direction.NorthEast or Direction.North)
+ {
+ rotationVisuals.HorizontalRotation = Angle.FromDegrees(270);
+ sprite.Rotation = Angle.FromDegrees(270);
+ return;
+ }
+
+ rotationVisuals.HorizontalRotation = Angle.FromDegrees(90);
+ sprite.Rotation = Angle.FromDegrees(90);
+ }
+
+ private void OnCheckAutoGetUp(CheckAutoGetUpEvent ev, EntitySessionEventArgs args)
+ {
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ var uid = GetEntity(ev.User);
+
+ if (!TryComp(uid, out var transform) || !TryComp(uid, out var rotationVisuals))
+ return;
+
+ var rotation = transform.LocalRotation + (_eyeManager.CurrentEye.Rotation - (transform.LocalRotation - transform.WorldRotation));
+
+ if (rotation.GetDir() is Direction.SouthEast or Direction.East or Direction.NorthEast or Direction.North)
+ {
+ rotationVisuals.HorizontalRotation = Angle.FromDegrees(270);
+ return;
+ }
+
+ rotationVisuals.HorizontalRotation = Angle.FromDegrees(90);
+ }
+}
diff --git a/Content.Client/Station/StationSpawningSystem.cs b/Content.Client/Station/StationSpawningSystem.cs
index 65da518d22..71dce5a78f 100644
--- a/Content.Client/Station/StationSpawningSystem.cs
+++ b/Content.Client/Station/StationSpawningSystem.cs
@@ -2,7 +2,4 @@
namespace Content.Client.Station;
-public sealed class StationSpawningSystem : SharedStationSpawningSystem
-{
-
-}
+public sealed class StationSpawningSystem : SharedStationSpawningSystem;
diff --git a/Content.Client/StatusIcon/StatusIconOverlay.cs b/Content.Client/StatusIcon/StatusIconOverlay.cs
index f8381afdbe..372bd04f57 100644
--- a/Content.Client/StatusIcon/StatusIconOverlay.cs
+++ b/Content.Client/StatusIcon/StatusIconOverlay.cs
@@ -39,8 +39,8 @@ protected override void Draw(in OverlayDrawArgs args)
var eyeRot = args.Viewport.Eye?.Rotation ?? default;
var xformQuery = _entity.GetEntityQuery();
- var scaleMatrix = Matrix3.CreateScale(new Vector2(1, 1));
- var rotationMatrix = Matrix3.CreateRotation(-eyeRot);
+ var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(1, 1));
+ var rotationMatrix = Matrix3Helpers.CreateRotation(-eyeRot);
var query = _entity.AllEntityQueryEnumerator();
while (query.MoveNext(out var uid, out var comp, out var sprite, out var xform, out var meta))
@@ -59,9 +59,9 @@ protected override void Draw(in OverlayDrawArgs args)
if (icons.Count == 0)
continue;
- var worldMatrix = Matrix3.CreateTranslation(worldPos);
- Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld);
- Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty);
+ var worldMatrix = Matrix3Helpers.CreateTranslation(worldPos);
+ var scaledWorld = Matrix3x2.Multiply(scaleMatrix, worldMatrix);
+ var matty = Matrix3x2.Multiply(rotationMatrix, scaledWorld);
handle.SetTransform(matty);
var countL = 0;
diff --git a/Content.Client/Storage/Components/StorageContainerVisualsComponent.cs b/Content.Client/Storage/Components/StorageContainerVisualsComponent.cs
index 9f07867da8..9ef6c65e89 100644
--- a/Content.Client/Storage/Components/StorageContainerVisualsComponent.cs
+++ b/Content.Client/Storage/Components/StorageContainerVisualsComponent.cs
@@ -1,4 +1,5 @@
using Content.Client.Chemistry.Visualizers;
+using Content.Shared.Chemistry.Components;
namespace Content.Client.Storage.Components;
diff --git a/Content.Client/Storage/StorageBoundUserInterface.cs b/Content.Client/Storage/StorageBoundUserInterface.cs
index f7fdbb8367..899df30f7f 100644
--- a/Content.Client/Storage/StorageBoundUserInterface.cs
+++ b/Content.Client/Storage/StorageBoundUserInterface.cs
@@ -17,6 +17,14 @@ public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKe
_storage = _entManager.System();
}
+ protected override void Open()
+ {
+ base.Open();
+
+ if (_entManager.TryGetComponent(Owner, out var comp))
+ _storage.OpenStorageWindow((Owner, comp));
+ }
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
@@ -25,16 +33,5 @@ protected override void Dispose(bool disposing)
_storage.CloseStorageWindow(Owner);
}
-
- protected override void ReceiveMessage(BoundUserInterfaceMessage message)
- {
- base.ReceiveMessage(message);
-
- if (message is StorageModifyWindowMessage)
- {
- if (_entManager.TryGetComponent(Owner, out var comp))
- _storage.OpenStorageWindow((Owner, comp));
- }
- }
}
diff --git a/Content.Client/Storage/Systems/StorageSystem.cs b/Content.Client/Storage/Systems/StorageSystem.cs
index ce0a6bf1ca..b80a855f98 100644
--- a/Content.Client/Storage/Systems/StorageSystem.cs
+++ b/Content.Client/Storage/Systems/StorageSystem.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System.Linq;
+using System.Numerics;
using Content.Client.Animations;
using Content.Shared.Hands;
using Content.Shared.Storage;
@@ -26,7 +27,7 @@ public override void Initialize()
SubscribeLocalEvent(OnShutdown);
SubscribeNetworkEvent(HandlePickupAnimation);
- SubscribeNetworkEvent(HandleAnimatingInsertingEntities);
+ SubscribeAllEvent(HandleAnimatingInsertingEntities);
}
public override void UpdateUI(Entity entity)
@@ -111,7 +112,7 @@ private void CloseStorageBoundUserInterface(Entity enti
if (!Resolve(entity, ref entity.Comp, false))
return;
- if (entity.Comp.OpenInterfaces.GetValueOrDefault(StorageComponent.StorageUiKey.Key) is not { } bui)
+ if (entity.Comp.ClientOpenInterfaces.GetValueOrDefault(StorageComponent.StorageUiKey.Key) is not { } bui)
return;
bui.Close();
@@ -149,7 +150,7 @@ public void PickupAnimation(EntityUid item, EntityCoordinates initialCoords, Ent
}
var finalMapPos = finalCoords.ToMapPos(EntityManager, TransformSystem);
- var finalPos = TransformSystem.GetInvWorldMatrix(initialCoords.EntityId).Transform(finalMapPos);
+ var finalPos = Vector2.Transform(finalMapPos, TransformSystem.GetInvWorldMatrix(initialCoords.EntityId));
_entityPickupAnimation.AnimateEntityPickup(item, initialCoords, finalPos, initialAngle);
}
diff --git a/Content.Client/Store/Ui/StoreMenu.xaml.cs b/Content.Client/Store/Ui/StoreMenu.xaml.cs
index b7a2c285fe..7eb597f2f3 100644
--- a/Content.Client/Store/Ui/StoreMenu.xaml.cs
+++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs
@@ -3,6 +3,7 @@
using Content.Client.Message;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
+using Content.Client.Stylesheets;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -147,6 +148,10 @@ private void AddListingGui(ListingData listing)
}
var newListing = new StoreListingControl(listing, GetListingPriceString(listing), hasBalance, texture);
+
+ if (listing.DiscountValue > 0)
+ newListing.StoreItemBuyButton.AddStyleClass(StyleNano.ButtonColorDangerDefault.ToString());
+
newListing.StoreItemBuyButton.OnButtonDown += args
=> OnListingButtonPressed?.Invoke(args, listing);
diff --git a/Content.Client/Strip/StrippableSystem.cs b/Content.Client/Strip/StrippableSystem.cs
index c5083d2204..23f38e9d51 100644
--- a/Content.Client/Strip/StrippableSystem.cs
+++ b/Content.Client/Strip/StrippableSystem.cs
@@ -35,7 +35,7 @@ public void UpdateUi(EntityUid uid, StrippableComponent? component = null, Entit
if (!TryComp(uid, out UserInterfaceComponent? uiComp))
return;
- foreach (var ui in uiComp.OpenInterfaces.Values)
+ foreach (var ui in uiComp.ClientOpenInterfaces.Values)
{
if (ui is StrippableBoundUserInterface stripUi)
stripUi.DirtyMenu();
diff --git a/Content.Client/Stylesheets/StyleBase.cs b/Content.Client/Stylesheets/StyleBase.cs
index 048d5602a2..638ef84c8d 100644
--- a/Content.Client/Stylesheets/StyleBase.cs
+++ b/Content.Client/Stylesheets/StyleBase.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using Content.Client.Resources;
+using Content.Client.UserInterface.Controls;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
@@ -25,6 +26,7 @@ public abstract class StyleBase
public const string ButtonSquare = "ButtonSquare";
public const string ButtonCaution = "Caution";
+ public const string ButtonDanger = "Danger";
public const int DefaultGrabberSize = 10;
diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs
index a10e3eb592..b2d2bf3b48 100644
--- a/Content.Client/Stylesheets/StyleNano.cs
+++ b/Content.Client/Stylesheets/StyleNano.cs
@@ -98,12 +98,17 @@ public sealed class StyleNano : StyleBase
public static readonly Color ButtonColorHovered = Color.FromHex("#575b61");
public static readonly Color ButtonColorHoveredRed = Color.FromHex("#DF6B6B");
public static readonly Color ButtonColorPressed = Color.FromHex("#3e6c45");
- public static readonly Color ButtonColorDisabled = Color.FromHex("#303133");
+ public static readonly Color ButtonColorDisabled = Color.FromHex("#292929");
- public static readonly Color ButtonColorCautionDefault = Color.FromHex("#ab3232");
- public static readonly Color ButtonColorCautionHovered = Color.FromHex("#cf2f2f");
- public static readonly Color ButtonColorCautionPressed = Color.FromHex("#3e6c45");
- public static readonly Color ButtonColorCautionDisabled = Color.FromHex("#602a2a");
+ public static readonly Color ButtonColorCautionDefault = Color.FromHex("#8F6A33");
+ public static readonly Color ButtonColorCautionHovered = Color.FromHex("#C0934E");
+ public static readonly Color ButtonColorCautionPressed = Color.FromHex("#E49F35");
+ public static readonly Color ButtonColorCautionDisabled = Color.FromHex("#28251F");
+
+ public static readonly Color ButtonColorDangerDefault = Color.FromHex("#7B2D2D");
+ public static readonly Color ButtonColorDangerHovered = Color.FromHex("#BD524B");
+ public static readonly Color ButtonColorDangerPressed = Color.FromHex("#C12525");
+ public static readonly Color ButtonColorDangerDisabled = Color.FromHex("#2F2020");
public static readonly Color ButtonColorGoodDefault = Color.FromHex("#3E6C45");
public static readonly Color ButtonColorGoodHovered = Color.FromHex("#31843E");
@@ -136,6 +141,8 @@ public sealed class StyleNano : StyleBase
public const string StyleClassPowerStateGood = "PowerStateGood";
public const string StyleClassItemStatus = "ItemStatus";
+ public const string StyleClassItemStatusNotHeld = "ItemStatusNotHeld";
+ public static readonly Color ItemStatusNotHeldColor = Color.Gray;
//Background
public const string StyleClassBackgroundBaseDark = "PanelBackgroundBaseDark";
@@ -660,22 +667,39 @@ public StyleNano(IResourceCache resCache) : base(resCache)
.Pseudo(ContainerButton.StylePseudoClassDisabled)
.Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDisabled),
+ // Colors for the danger buttons.
+ Element().Class(ContainerButton.StyleClassButton).Class(ButtonDanger)
+ .Pseudo(ContainerButton.StylePseudoClassNormal)
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerDefault),
+
+ Element().Class(ContainerButton.StyleClassButton).Class(ButtonDanger)
+ .Pseudo(ContainerButton.StylePseudoClassHover)
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerHovered),
+
+ Element().Class(ContainerButton.StyleClassButton).Class(ButtonDanger)
+ .Pseudo(ContainerButton.StylePseudoClassPressed)
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerPressed),
+
+ Element().Class(ContainerButton.StyleClassButton).Class(ButtonDanger)
+ .Pseudo(ContainerButton.StylePseudoClassDisabled)
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerDisabled),
+
// Colors for confirm buttons confirm states.
Element()
.Pseudo(ConfirmButton.ConfirmPrefix + ContainerButton.StylePseudoClassNormal)
- .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDefault),
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerDefault),
Element()
.Pseudo(ConfirmButton.ConfirmPrefix + ContainerButton.StylePseudoClassHover)
- .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionHovered),
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerHovered),
Element()
.Pseudo(ConfirmButton.ConfirmPrefix + ContainerButton.StylePseudoClassPressed)
- .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionPressed),
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerPressed),
Element()
.Pseudo(ConfirmButton.ConfirmPrefix + ContainerButton.StylePseudoClassDisabled)
- .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDisabled),
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerDisabled),
new StyleRule(new SelectorChild(
new SelectorElement(typeof(Button), null, null, new[] {ContainerButton.StylePseudoClassDisabled}),
@@ -734,19 +758,19 @@ public StyleNano(IResourceCache resCache) : base(resCache)
Element().Class(ConfirmationMenuElement.StyleClassConfirmationContextMenuButton)
.Pseudo(ContainerButton.StylePseudoClassNormal)
- .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDefault),
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerDefault),
Element().Class(ConfirmationMenuElement.StyleClassConfirmationContextMenuButton)
.Pseudo(ContainerButton.StylePseudoClassHover)
- .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionHovered),
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerHovered),
Element().Class(ConfirmationMenuElement.StyleClassConfirmationContextMenuButton)
.Pseudo(ContainerButton.StylePseudoClassPressed)
- .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionPressed),
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerPressed),
Element().Class(ConfirmationMenuElement.StyleClassConfirmationContextMenuButton)
.Pseudo(ContainerButton.StylePseudoClassDisabled)
- .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDisabled),
+ .Prop(Control.StylePropertyModulateSelf, ButtonColorDangerDisabled),
// Examine buttons
Element().Class(ExamineButton.StyleClassExamineButton)
@@ -1234,6 +1258,16 @@ public StyleNano(IResourceCache resCache) : base(resCache)
new StyleProperty("font", notoSans10),
}),
+ Element()
+ .Class(StyleClassItemStatusNotHeld)
+ .Prop("font", notoSansItalic10)
+ .Prop("font-color", ItemStatusNotHeldColor),
+
+ Element()
+ .Class(StyleClassItemStatus)
+ .Prop(nameof(RichTextLabel.LineHeightScale), 0.7f)
+ .Prop(nameof(Control.Margin), new Thickness(0, 0, 0, -6)),
+
// Slider
new StyleRule(SelectorElement.Type(typeof(Slider)), new []
{
@@ -1377,6 +1411,17 @@ public StyleNano(IResourceCache resCache) : base(resCache)
Element().Class("WindowHeadingBackgroundLight")
.Prop("panel", new StyleBoxTexture(BaseButtonOpenLeft) { Padding = default }),
+ // Window Header Help Button
+ Element().Class(FancyWindow.StyleClassWindowHelpButton)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Nano/help.png"))
+ .Prop(Control.StylePropertyModulateSelf, Color.FromHex("#4B596A")),
+
+ Element().Class(FancyWindow.StyleClassWindowHelpButton).Pseudo(ContainerButton.StylePseudoClassHover)
+ .Prop(Control.StylePropertyModulateSelf, Color.FromHex("#7F3636")),
+
+ Element().Class(FancyWindow.StyleClassWindowHelpButton).Pseudo(ContainerButton.StylePseudoClassPressed)
+ .Prop(Control.StylePropertyModulateSelf, Color.FromHex("#753131")),
+
//The lengths you have to go through to change a background color smh
Element().Class("PanelBackgroundBaseDark")
.Prop("panel", new StyleBoxTexture(BaseButtonOpenBoth) { Padding = default })
@@ -1576,6 +1621,60 @@ public StyleNano(IResourceCache resCache) : base(resCache)
{
BackgroundColor = FancyTreeSelectedRowColor,
}),
+ // Shitmed Edit Start
+ Element().Class("TargetDollButtonHead")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/head_hover.png")),
+
+ Element().Class("TargetDollButtonChest")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/torso_hover.png")),
+
+ Element().Class("TargetDollButtonGroin")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/groin_hover.png")),
+
+ Element().Class("TargetDollButtonLeftArm")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/leftarm_hover.png")),
+
+ Element().Class("TargetDollButtonLeftHand")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/lefthand_hover.png")),
+
+ Element().Class("TargetDollButtonRightArm")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/rightarm_hover.png")),
+
+ Element().Class("TargetDollButtonRightHand")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/righthand_hover.png")),
+
+ Element().Class("TargetDollButtonLeftLeg")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/leftleg_hover.png")),
+
+ Element().Class("TargetDollButtonLeftFoot")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/leftfoot_hover.png")),
+
+ Element().Class("TargetDollButtonRightLeg")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/rightleg_hover.png")),
+
+ Element().Class("TargetDollButtonRightFoot")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/rightfoot_hover.png")),
+
+ Element().Class("TargetDollButtonEyes")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/eyes_hover.png")),
+
+ Element().Class("TargetDollButtonMouth")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/mouth_hover.png")),
+ // Shitmed Edit End
+
}).ToList());
}
}
diff --git a/Content.Client/Stylesheets/StyleSpace.cs b/Content.Client/Stylesheets/StyleSpace.cs
index 3bb4e986af..84f82ea038 100644
--- a/Content.Client/Stylesheets/StyleSpace.cs
+++ b/Content.Client/Stylesheets/StyleSpace.cs
@@ -131,19 +131,19 @@ public StyleSpace(IResourceCache resCache) : base(resCache)
.Prop(Control.StylePropertyModulateSelf, ButtonColorDisabled),
// Colors for the caution buttons.
- Element().Class(ContainerButton.StyleClassButton).Class(ButtonCaution)
+ Element().Class(ContainerButton.StyleClassButton).Class(ButtonDanger)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDefault),
- Element().Class(ContainerButton.StyleClassButton).Class(ButtonCaution)
+ Element().Class(ContainerButton.StyleClassButton).Class(ButtonDanger)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, ButtonColorCautionHovered),
- Element().Class(ContainerButton.StyleClassButton).Class(ButtonCaution)
+ Element().Class(ContainerButton.StyleClassButton).Class(ButtonDanger)
.Pseudo(ContainerButton.StylePseudoClassPressed)
.Prop(Control.StylePropertyModulateSelf, ButtonColorCautionPressed),
- Element().Class(ContainerButton.StyleClassButton).Class(ButtonCaution)
+ Element().Class(ContainerButton.StyleClassButton).Class(ButtonDanger)
.Pseudo(ContainerButton.StylePseudoClassDisabled)
.Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDisabled),
diff --git a/Content.Client/Targeting/TargetingSystem.cs b/Content.Client/Targeting/TargetingSystem.cs
new file mode 100644
index 0000000000..2c92d53ae1
--- /dev/null
+++ b/Content.Client/Targeting/TargetingSystem.cs
@@ -0,0 +1,102 @@
+using Content.Shared.Input;
+using Content.Shared.Targeting;
+using Content.Shared.Targeting.Events;
+using Robust.Client.Player;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Player;
+
+namespace Content.Client.Targeting;
+public sealed class TargetingSystem : SharedTargetingSystem
+{
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+
+ public event Action? TargetingStartup;
+ public event Action? TargetingShutdown;
+ public event Action? TargetChange;
+ public event Action? PartStatusStartup;
+ public event Action? PartStatusUpdate;
+ public event Action? PartStatusShutdown;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(HandlePlayerAttached);
+ SubscribeLocalEvent(HandlePlayerDetached);
+ SubscribeLocalEvent(OnTargetingStartup);
+ SubscribeLocalEvent(OnTargetingShutdown);
+ SubscribeNetworkEvent(OnTargetIntegrityChange);
+
+ CommandBinds.Builder
+ .Bind(ContentKeyFunctions.TargetHead,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.Head)))
+ .Bind(ContentKeyFunctions.TargetTorso,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.Torso)))
+ .Bind(ContentKeyFunctions.TargetLeftArm,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.LeftArm)))
+/* .Bind(ContentKeyFunctions.TargetLeftHand,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.LeftHand))) SOON :TM: */
+ .Bind(ContentKeyFunctions.TargetRightArm,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.RightArm)))
+/* .Bind(ContentKeyFunctions.TargetRightHand,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.RightHand)))*/
+ .Bind(ContentKeyFunctions.TargetLeftLeg,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.LeftLeg)))
+/* .Bind(ContentKeyFunctions.TargetLeftFoot,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.LeftFoot)))*/
+ .Bind(ContentKeyFunctions.TargetRightLeg,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.RightLeg)))
+/* .Bind(ContentKeyFunctions.TargetRightFoot,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.RightFoot)))*/
+ .Register();
+ }
+
+ private void HandlePlayerAttached(EntityUid uid, TargetingComponent component, LocalPlayerAttachedEvent args)
+ {
+ TargetingStartup?.Invoke(component);
+ PartStatusStartup?.Invoke(component);
+ }
+
+ private void HandlePlayerDetached(EntityUid uid, TargetingComponent component, LocalPlayerDetachedEvent args)
+ {
+ TargetingShutdown?.Invoke();
+ PartStatusShutdown?.Invoke();
+ }
+
+ private void OnTargetingStartup(EntityUid uid, TargetingComponent component, ComponentStartup args)
+ {
+ if (_playerManager.LocalEntity != uid)
+ return;
+
+ TargetingStartup?.Invoke(component);
+ PartStatusStartup?.Invoke(component);
+ }
+
+ private void OnTargetingShutdown(EntityUid uid, TargetingComponent component, ComponentShutdown args)
+ {
+ if (_playerManager.LocalEntity != uid)
+ return;
+
+ TargetingShutdown?.Invoke();
+ PartStatusShutdown?.Invoke();
+ }
+
+ private void OnTargetIntegrityChange(TargetIntegrityChangeEvent args)
+ {
+ if (!TryGetEntity(args.Uid, out var uid)
+ || !_playerManager.LocalEntity.Equals(uid)
+ || !TryComp(uid, out TargetingComponent? component)
+ || !args.RefreshUi)
+ return;
+
+ PartStatusUpdate?.Invoke(component);
+ }
+
+ private void HandleTargetChange(ICommonSession? session, TargetBodyPart target)
+ {
+ if (session == null
+ || session.AttachedEntity is not { } uid
+ || !TryComp(uid, out var targeting))
+ return;
+
+ TargetChange?.Invoke(target);
+ }
+}
diff --git a/Content.Client/Telescope/TelescopeSystem.cs b/Content.Client/Telescope/TelescopeSystem.cs
new file mode 100644
index 0000000000..ac2270aa97
--- /dev/null
+++ b/Content.Client/Telescope/TelescopeSystem.cs
@@ -0,0 +1,128 @@
+using System.Numerics;
+using Content.Client.Viewport;
+using Content.Shared.CCVar;
+using Content.Shared.Telescope;
+using Content.Shared.Input;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Shared.Configuration;
+using Robust.Shared.Input;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Telescope;
+
+public sealed class TelescopeSystem : SharedTelescopeSystem
+{
+ [Dependency] private readonly InputSystem _inputSystem = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IInputManager _input = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ private ScalingViewport? _viewport;
+ private bool _holdLookUp;
+ private bool _toggled;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _cfg.OnValueChanged(CCVars.HoldLookUp,
+ val =>
+ {
+ var input = val ? null : InputCmdHandler.FromDelegate(_ => _toggled = !_toggled);
+ _input.SetInputCommand(ContentKeyFunctions.LookUp, input);
+ _holdLookUp = val;
+ _toggled = false;
+ },
+ true);
+ }
+
+ public override void FrameUpdate(float frameTime)
+ {
+ base.FrameUpdate(frameTime);
+
+ if (_timing.ApplyingState
+ || !_timing.IsFirstTimePredicted
+ || !_input.MouseScreenPosition.IsValid)
+ return;
+
+ var player = _player.LocalEntity;
+
+ var telescope = GetRightTelescope(player);
+
+ if (telescope == null)
+ {
+ _toggled = false;
+ return;
+ }
+
+ if (!TryComp(player, out var eye))
+ return;
+
+ var offset = Vector2.Zero;
+
+ if (_holdLookUp)
+ {
+ if (_inputSystem.CmdStates.GetState(ContentKeyFunctions.LookUp) != BoundKeyState.Down)
+ {
+ RaiseEvent(offset);
+ return;
+ }
+ }
+ else if (!_toggled)
+ {
+ RaiseEvent(offset);
+ return;
+ }
+
+ var mousePos = _input.MouseScreenPosition;
+
+ if (_uiManager.MouseGetControl(mousePos) as ScalingViewport is { } viewport)
+ _viewport = viewport;
+
+ if (_viewport == null)
+ return;
+
+ var centerPos = _eyeManager.WorldToScreen(eye.Eye.Position.Position + eye.Offset);
+
+ var diff = mousePos.Position - centerPos;
+ var len = diff.Length();
+
+ var size = _viewport.PixelSize;
+
+ var maxLength = Math.Min(size.X, size.Y) * 0.4f;
+ var minLength = maxLength * 0.2f;
+
+ if (len > maxLength)
+ {
+ diff *= maxLength / len;
+ len = maxLength;
+ }
+
+ var divisor = maxLength * telescope.Divisor;
+
+ if (len > minLength)
+ {
+ diff -= diff * minLength / len;
+ offset = new Vector2(diff.X / divisor, -diff.Y / divisor);
+ offset = new Angle(-eye.Rotation.Theta).RotateVec(offset);
+ }
+
+ RaiseEvent(offset);
+ }
+
+ private void RaiseEvent(Vector2 offset)
+ {
+ RaisePredictiveEvent(new EyeOffsetChangedEvent
+ {
+ Offset = offset
+ });
+ }
+}
diff --git a/Content.Client/Tips/TippyUI.xaml b/Content.Client/Tips/TippyUI.xaml
new file mode 100644
index 0000000000..a86e05aadd
--- /dev/null
+++ b/Content.Client/Tips/TippyUI.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Tips/TippyUI.xaml.cs b/Content.Client/Tips/TippyUI.xaml.cs
new file mode 100644
index 0000000000..de3eaf4f51
--- /dev/null
+++ b/Content.Client/Tips/TippyUI.xaml.cs
@@ -0,0 +1,54 @@
+using Content.Client.Paper;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Tips;
+
+[GenerateTypedNameReferences]
+public sealed partial class TippyUI : UIWidget
+{
+ public TippyState State = TippyState.Hidden;
+ public bool ModifyLayers = true;
+
+ public TippyUI()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void InitLabel(PaperVisualsComponent? visuals, IResourceCache resCache)
+ {
+ if (visuals == null)
+ return;
+
+ Label.ModulateSelfOverride = visuals.FontAccentColor;
+
+ if (visuals.BackgroundImagePath == null)
+ return;
+
+ LabelPanel.ModulateSelfOverride = visuals.BackgroundModulate;
+ var backgroundImage = resCache.GetResource(visuals.BackgroundImagePath);
+ var backgroundImageMode = visuals.BackgroundImageTile ? StyleBoxTexture.StretchMode.Tile : StyleBoxTexture.StretchMode.Stretch;
+ var backgroundPatchMargin = visuals.BackgroundPatchMargin;
+ LabelPanel.PanelOverride = new StyleBoxTexture
+ {
+ Texture = backgroundImage,
+ TextureScale = visuals.BackgroundScale,
+ Mode = backgroundImageMode,
+ PatchMarginLeft = backgroundPatchMargin.Left,
+ PatchMarginBottom = backgroundPatchMargin.Bottom,
+ PatchMarginRight = backgroundPatchMargin.Right,
+ PatchMarginTop = backgroundPatchMargin.Top
+ };
+ }
+
+ public enum TippyState : byte
+ {
+ Hidden,
+ Revealing,
+ Speaking,
+ Hiding,
+ }
+}
diff --git a/Content.Client/Tips/TippyUIController.cs b/Content.Client/Tips/TippyUIController.cs
new file mode 100644
index 0000000000..2cc694d97d
--- /dev/null
+++ b/Content.Client/Tips/TippyUIController.cs
@@ -0,0 +1,241 @@
+using Content.Client.Gameplay;
+using System.Numerics;
+using Content.Client.Message;
+using Content.Client.Paper;
+using Content.Shared.CCVar;
+using Content.Shared.Movement.Components;
+using Content.Shared.Tips;
+using Robust.Client.GameObjects;
+using Robust.Client.ResourceManagement;
+using Robust.Client.State;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.Audio;
+using Robust.Shared.Configuration;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using static Content.Client.Tips.TippyUI;
+
+namespace Content.Client.Tips;
+
+public sealed class TippyUIController : UIController
+{
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly IResourceCache _resCache = default!;
+ [UISystemDependency] private readonly AudioSystem _audio = default!;
+
+ public const float Padding = 50;
+ public static Angle WaddleRotation = Angle.FromDegrees(10);
+
+ private EntityUid _entity;
+ private float _secondsUntilNextState;
+ private int _previousStep = 0;
+ private TippyEvent? _currentMessage;
+ private readonly Queue _queuedMessages = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ UIManager.OnScreenChanged += OnScreenChanged;
+ SubscribeNetworkEvent(OnTippyEvent);
+ }
+
+ private void OnTippyEvent(TippyEvent msg, EntitySessionEventArgs args)
+ {
+ _queuedMessages.Enqueue(msg);
+ }
+
+ public override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ var screen = UIManager.ActiveScreen;
+ if (screen == null)
+ {
+ _queuedMessages.Clear();
+ return;
+ }
+
+ var tippy = screen.GetOrAddWidget();
+ _secondsUntilNextState -= args.DeltaSeconds;
+
+ if (_secondsUntilNextState <= 0)
+ NextState(tippy);
+ else
+ {
+ var pos = UpdatePosition(tippy, screen.Size, args); ;
+ LayoutContainer.SetPosition(tippy, pos);
+ }
+ }
+
+ private Vector2 UpdatePosition(TippyUI tippy, Vector2 screenSize, FrameEventArgs args)
+ {
+ if (_currentMessage == null)
+ return default;
+
+ var slideTime = _currentMessage.SlideTime;
+
+ var offset = tippy.State switch
+ {
+ TippyState.Hidden => 0,
+ TippyState.Revealing => Math.Clamp(1 - _secondsUntilNextState / slideTime, 0, 1),
+ TippyState.Hiding => Math.Clamp(_secondsUntilNextState / slideTime, 0, 1),
+ _ => 1,
+ };
+
+ var waddle = _currentMessage.WaddleInterval;
+
+ if (_currentMessage == null
+ || waddle <= 0
+ || tippy.State == TippyState.Hidden
+ || tippy.State == TippyState.Speaking
+ || !EntityManager.TryGetComponent(_entity, out SpriteComponent? sprite))
+ {
+ return new Vector2(screenSize.X - offset * (tippy.DesiredSize.X + Padding), (screenSize.Y - tippy.DesiredSize.Y) / 2);
+ }
+
+ var numSteps = (int) Math.Ceiling(slideTime / waddle);
+ var curStep = (int) Math.Floor(numSteps * offset);
+ var stepSize = (tippy.DesiredSize.X + Padding) / numSteps;
+
+ if (curStep != _previousStep)
+ {
+ _previousStep = curStep;
+ sprite.Rotation = sprite.Rotation > 0
+ ? -WaddleRotation
+ : WaddleRotation;
+
+ if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step))
+ {
+ var audioParams = step.FootstepSoundCollection.Params
+ .AddVolume(-7f)
+ .WithVariation(0.1f);
+ _audio.PlayGlobal(step.FootstepSoundCollection, EntityUid.Invalid, audioParams);
+ }
+ }
+
+ return new Vector2(screenSize.X - stepSize * curStep, (screenSize.Y - tippy.DesiredSize.Y) / 2);
+ }
+
+ private void NextState(TippyUI tippy)
+ {
+ SpriteComponent? sprite;
+ switch (tippy.State)
+ {
+ case TippyState.Hidden:
+ if (!_queuedMessages.TryDequeue(out var next))
+ return;
+
+ if (next.Proto != null)
+ {
+ _entity = EntityManager.SpawnEntity(next.Proto, MapCoordinates.Nullspace);
+ tippy.ModifyLayers = false;
+ }
+ else
+ {
+ _entity = EntityManager.SpawnEntity(_cfg.GetCVar(CCVars.TippyEntity), MapCoordinates.Nullspace);
+ tippy.ModifyLayers = true;
+ }
+ if (!EntityManager.TryGetComponent(_entity, out sprite))
+ return;
+ if (!EntityManager.HasComponent(_entity))
+ {
+ var paper = EntityManager.AddComponent(_entity);
+ paper.BackgroundImagePath = "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png";
+ paper.BackgroundPatchMargin = new(16f, 16f, 16f, 16f);
+ paper.BackgroundModulate = new(255, 255, 204);
+ paper.FontAccentColor = new(0, 0, 0);
+ }
+ tippy.InitLabel(EntityManager.GetComponentOrNull(_entity), _resCache);
+
+ var scale = sprite.Scale;
+ if (tippy.ModifyLayers)
+ {
+ sprite.Scale = Vector2.One;
+ }
+ else
+ {
+ sprite.Scale = new Vector2(3, 3);
+ }
+ tippy.Entity.SetEntity(_entity);
+ tippy.Entity.Scale = scale;
+
+ _currentMessage = next;
+ _secondsUntilNextState = next.SlideTime;
+ tippy.State = TippyState.Revealing;
+ _previousStep = 0;
+ if (tippy.ModifyLayers)
+ {
+ sprite.LayerSetAnimationTime("revealing", 0);
+ sprite.LayerSetVisible("revealing", true);
+ sprite.LayerSetVisible("speaking", false);
+ sprite.LayerSetVisible("hiding", false);
+ }
+ sprite.Rotation = 0;
+ tippy.Label.SetMarkupPermissive(_currentMessage.Msg);
+ tippy.Label.Visible = false;
+ tippy.LabelPanel.Visible = false;
+ tippy.Visible = true;
+ sprite.Visible = true;
+ break;
+
+ case TippyState.Revealing:
+ tippy.State = TippyState.Speaking;
+ if (!EntityManager.TryGetComponent(_entity, out sprite))
+ return;
+ sprite.Rotation = 0;
+ _previousStep = 0;
+ if (tippy.ModifyLayers)
+ {
+ sprite.LayerSetAnimationTime("speaking", 0);
+ sprite.LayerSetVisible("revealing", false);
+ sprite.LayerSetVisible("speaking", true);
+ sprite.LayerSetVisible("hiding", false);
+ }
+ tippy.Label.Visible = true;
+ tippy.LabelPanel.Visible = true;
+ tippy.InvalidateArrange();
+ tippy.InvalidateMeasure();
+ if (_currentMessage != null)
+ _secondsUntilNextState = _currentMessage.SpeakTime;
+
+ break;
+
+ case TippyState.Speaking:
+ tippy.State = TippyState.Hiding;
+ if (!EntityManager.TryGetComponent(_entity, out sprite))
+ return;
+ if (tippy.ModifyLayers)
+ {
+ sprite.LayerSetAnimationTime("hiding", 0);
+ sprite.LayerSetVisible("revealing", false);
+ sprite.LayerSetVisible("speaking", false);
+ sprite.LayerSetVisible("hiding", true);
+ }
+ tippy.LabelPanel.Visible = false;
+ if (_currentMessage != null)
+ _secondsUntilNextState = _currentMessage.SlideTime;
+ break;
+
+ default: // finished hiding
+
+ EntityManager.DeleteEntity(_entity);
+ _entity = default;
+ tippy.Visible = false;
+ _currentMessage = null;
+ _secondsUntilNextState = 0;
+ tippy.State = TippyState.Hidden;
+ break;
+ }
+ }
+
+ private void OnScreenChanged((UIScreen? Old, UIScreen? New) ev)
+ {
+ ev.Old?.RemoveWidget();
+ _currentMessage = null;
+ EntityManager.DeleteEntity(_entity);
+ }
+}
diff --git a/Content.Client/Tools/Components/WelderComponent.cs b/Content.Client/Tools/Components/WelderComponent.cs
deleted file mode 100644
index a83a78a5a4..0000000000
--- a/Content.Client/Tools/Components/WelderComponent.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Client.Tools.UI;
-using Content.Shared.Tools.Components;
-
-namespace Content.Client.Tools.Components
-{
- [RegisterComponent, Access(typeof(ToolSystem), typeof(WelderStatusControl))]
- public sealed partial class WelderComponent : SharedWelderComponent
- {
- [ViewVariables(VVAccess.ReadWrite)]
- public bool UiUpdateNeeded { get; set; }
-
- [ViewVariables]
- public float FuelCapacity { get; set; }
-
- [ViewVariables]
- public float Fuel { get; set; }
- }
-}
diff --git a/Content.Client/Tools/ToolSystem.cs b/Content.Client/Tools/ToolSystem.cs
index 6811d58460..2207242918 100644
--- a/Content.Client/Tools/ToolSystem.cs
+++ b/Content.Client/Tools/ToolSystem.cs
@@ -1,10 +1,8 @@
using Content.Client.Items;
using Content.Client.Tools.Components;
using Content.Client.Tools.UI;
-using Content.Shared.Item;
using Content.Shared.Tools.Components;
using Robust.Client.GameObjects;
-using Robust.Shared.GameStates;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
namespace Content.Client.Tools
@@ -15,8 +13,7 @@ public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnWelderHandleState);
- Subs.ItemStatus(ent => new WelderStatusControl(ent));
+ Subs.ItemStatus(ent => new WelderStatusControl(ent, EntityManager, this));
Subs.ItemStatus(ent => new MultipleToolStatusControl(ent));
}
@@ -42,20 +39,5 @@ public override void SetMultipleTool(EntityUid uid,
sprite.LayerSetSprite(0, current.Sprite);
}
}
-
- private void OnWelderHandleState(EntityUid uid, WelderComponent welder, ref ComponentHandleState args)
- {
- if (args.Current is not WelderComponentState state)
- return;
-
- welder.FuelCapacity = state.FuelCapacity;
- welder.Fuel = state.Fuel;
- welder.UiUpdateNeeded = true;
- }
-
- protected override bool IsWelder(EntityUid uid)
- {
- return HasComp(uid);
- }
}
}
diff --git a/Content.Client/Tools/UI/WelderStatusControl.cs b/Content.Client/Tools/UI/WelderStatusControl.cs
index af81a28f62..3d44d6fa84 100644
--- a/Content.Client/Tools/UI/WelderStatusControl.cs
+++ b/Content.Client/Tools/UI/WelderStatusControl.cs
@@ -1,62 +1,45 @@
+using Content.Client.Items.UI;
using Content.Client.Message;
using Content.Client.Stylesheets;
-using Content.Client.Tools.Components;
-using Content.Shared.Item;
-using Robust.Client.UserInterface;
+using Content.Shared.FixedPoint;
+using Content.Shared.Tools.Components;
+using Content.Shared.Tools.Systems;
using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Timing;
-using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent;
namespace Content.Client.Tools.UI;
-public sealed class WelderStatusControl : Control
+public sealed class WelderStatusControl : PollingItemStatusControl
{
- [Dependency] private readonly IEntityManager _entMan = default!;
-
- private readonly WelderComponent _parent;
- private readonly ItemToggleComponent? _toggleComponent;
+ private readonly Entity _parent;
+ private readonly IEntityManager _entityManager;
+ private readonly SharedToolSystem _toolSystem;
private readonly RichTextLabel _label;
- public WelderStatusControl(Entity parent)
+ public WelderStatusControl(Entity parent, IEntityManager entityManager, SharedToolSystem toolSystem)
{
_parent = parent;
- _entMan = IoCManager.Resolve();
- if (_entMan.TryGetComponent(parent, out var itemToggle))
- _toggleComponent = itemToggle;
+ _entityManager = entityManager;
+ _toolSystem = toolSystem;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
UpdateDraw();
}
- ///
- protected override void FrameUpdate(FrameEventArgs args)
+ protected override Data PollData()
{
- base.FrameUpdate(args);
-
- if (!_parent.UiUpdateNeeded)
- {
- return;
- }
- Update();
+ var (fuel, capacity) = _toolSystem.GetWelderFuelAndCapacity(_parent, _parent.Comp);
+ return new Data(fuel, capacity, _parent.Comp.Enabled);
}
- public void Update()
+ protected override void Update(in Data data)
{
- _parent.UiUpdateNeeded = false;
-
- var fuelCap = _parent.FuelCapacity;
- var fuel = _parent.Fuel;
- var lit = false;
- if (_toggleComponent != null)
- {
- lit = _toggleComponent.Activated;
- }
-
_label.SetMarkup(Loc.GetString("welder-component-on-examine-detailed-message",
- ("colorName", fuel < fuelCap / 4f ? "darkorange" : "orange"),
- ("fuelLeft", Math.Round(fuel, 1)),
- ("fuelCapacity", fuelCap),
- ("status", Loc.GetString(lit ? "welder-component-on-examine-welder-lit-message" : "welder-component-on-examine-welder-not-lit-message"))));
+ ("colorName", data.Fuel < data.FuelCapacity / 4f ? "darkorange" : "orange"),
+ ("fuelLeft", data.Fuel),
+ ("fuelCapacity", data.FuelCapacity),
+ ("status", Loc.GetString(data.Lit ? "welder-component-on-examine-welder-lit-message" : "welder-component-on-examine-welder-not-lit-message"))));
}
+
+ public record struct Data(FixedPoint2 Fuel, FixedPoint2 FuelCapacity, bool Lit);
}
diff --git a/Content.Client/UserInterface/Controls/ClipControl.cs b/Content.Client/UserInterface/Controls/ClipControl.cs
new file mode 100644
index 0000000000..1fca3c0f47
--- /dev/null
+++ b/Content.Client/UserInterface/Controls/ClipControl.cs
@@ -0,0 +1,55 @@
+using System.Numerics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.UserInterface.Controls;
+
+///
+/// Pretends to child controls that there's infinite space.
+/// This can be used to make something like a clip instead of wrapping.
+///
+public sealed class ClipControl : Control
+{
+ private bool _clipHorizontal = true;
+ private bool _clipVertical = true;
+
+ public bool ClipHorizontal
+ {
+ get => _clipHorizontal;
+ set
+ {
+ _clipHorizontal = value;
+ InvalidateMeasure();
+ }
+ }
+
+ public bool ClipVertical
+ {
+ get => _clipVertical;
+ set
+ {
+ _clipVertical = value;
+ InvalidateMeasure();
+ }
+ }
+
+ protected override Vector2 MeasureOverride(Vector2 availableSize)
+ {
+ if (ClipHorizontal)
+ availableSize = availableSize with { X = float.PositiveInfinity };
+ if (ClipVertical)
+ availableSize = availableSize with { Y = float.PositiveInfinity };
+
+ return base.MeasureOverride(availableSize);
+ }
+
+ protected override Vector2 ArrangeOverride(Vector2 finalSize)
+ {
+ foreach (var child in Children)
+ {
+ child.Arrange(UIBox2.FromDimensions(Vector2.Zero, child.DesiredSize));
+ }
+
+ return finalSize;
+ }
+}
diff --git a/Content.Client/UserInterface/Controls/DirectionIcon.cs b/Content.Client/UserInterface/Controls/DirectionIcon.cs
index a6cc428091..c8fd63b43c 100644
--- a/Content.Client/UserInterface/Controls/DirectionIcon.cs
+++ b/Content.Client/UserInterface/Controls/DirectionIcon.cs
@@ -65,7 +65,7 @@ protected override void Draw(DrawingHandleScreen handle)
if (_rotation != null)
{
var offset = (-_rotation.Value).RotateVec(Size * UIScale / 2) - Size * UIScale / 2;
- handle.SetTransform(Matrix3.CreateTransform(GlobalPixelPosition - offset, -_rotation.Value));
+ handle.SetTransform(Matrix3Helpers.CreateTransform(GlobalPixelPosition - offset, -_rotation.Value));
}
base.Draw(handle);
diff --git a/Content.Client/UserInterface/Controls/FancyWindow.xaml b/Content.Client/UserInterface/Controls/FancyWindow.xaml
index d076a552bf..84d0499b3a 100644
--- a/Content.Client/UserInterface/Controls/FancyWindow.xaml
+++ b/Content.Client/UserInterface/Controls/FancyWindow.xaml
@@ -11,6 +11,7 @@
+
diff --git a/Content.Client/UserInterface/Controls/FancyWindow.xaml.cs b/Content.Client/UserInterface/Controls/FancyWindow.xaml.cs
index 8cdfe57dba..5912687fc3 100644
--- a/Content.Client/UserInterface/Controls/FancyWindow.xaml.cs
+++ b/Content.Client/UserInterface/Controls/FancyWindow.xaml.cs
@@ -1,7 +1,10 @@
using System.Numerics;
+using Content.Client.Guidebook;
+using Content.Client.Guidebook.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls
{
@@ -9,13 +12,17 @@ namespace Content.Client.UserInterface.Controls
[Virtual]
public partial class FancyWindow : BaseWindow
{
+ [Dependency] private readonly IEntitySystemManager _sysMan = default!;
+ private GuidebookSystem? _guidebookSystem;
private const int DRAG_MARGIN_SIZE = 7;
+ public const string StyleClassWindowHelpButton = "windowHelpButton";
public FancyWindow()
{
RobustXamlLoader.Load(this);
CloseButton.OnPressed += _ => Close();
+ HelpButton.OnPressed += _ => Help();
XamlChildren = ContentsContainer.Children;
}
@@ -25,6 +32,26 @@ public string? Title
set => WindowTitle.Text = value;
}
+ private List? _helpGuidebookIds;
+ public List? HelpGuidebookIds
+ {
+ get => _helpGuidebookIds;
+ set
+ {
+ _helpGuidebookIds = value;
+ HelpButton.Disabled = _helpGuidebookIds == null;
+ HelpButton.Visible = !HelpButton.Disabled;
+ }
+ }
+
+ public void Help()
+ {
+ if (HelpGuidebookIds is null)
+ return;
+ _guidebookSystem ??= _sysMan.GetEntitySystem();
+ _guidebookSystem.OpenHelp(HelpGuidebookIds);
+ }
+
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
var mode = DragMode.Move;
diff --git a/Content.Client/UserInterface/Controls/MainViewport.cs b/Content.Client/UserInterface/Controls/MainViewport.cs
index e334f61572..721d750115 100644
--- a/Content.Client/UserInterface/Controls/MainViewport.cs
+++ b/Content.Client/UserInterface/Controls/MainViewport.cs
@@ -51,6 +51,7 @@ public void UpdateCfg()
var stretch = _cfg.GetCVar(CCVars.ViewportStretch);
var renderScaleUp = _cfg.GetCVar(CCVars.ViewportScaleRender);
var fixedFactor = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
+ var verticalFit = _cfg.GetCVar(CCVars.ViewportVerticalFit);
if (stretch)
{
@@ -60,6 +61,7 @@ public void UpdateCfg()
// Did not find a snap, enable stretching.
Viewport.FixedStretchSize = null;
Viewport.StretchMode = ScalingViewportStretchMode.Bilinear;
+ Viewport.IgnoreDimension = verticalFit ? ScalingViewportIgnoreDimension.Horizontal : ScalingViewportIgnoreDimension.None;
if (renderScaleUp)
{
@@ -104,6 +106,8 @@ public void UpdateCfg()
// where we are clipping the viewport to make it fit.
var cfgToleranceClip = _cfg.GetCVar(CCVars.ViewportSnapToleranceClip);
+ var cfgVerticalFit = _cfg.GetCVar(CCVars.ViewportVerticalFit);
+
// Calculate if the viewport, when rendered at an integer scale,
// is close enough to the control size to enable "snapping" to NN,
// potentially cutting a tiny bit off/leaving a margin.
@@ -123,7 +127,8 @@ public void UpdateCfg()
// The rule for which snap fits is that at LEAST one axis needs to be in the tolerance size wise.
// One axis MAY be larger but not smaller than tolerance.
// Obviously if it's too small it's bad, and if it's too big on both axis we should stretch up.
- if (Fits(dx) && Fits(dy) || Fits(dx) && Larger(dy) || Larger(dx) && Fits(dy))
+ // Additionally, if the viewport's supposed to be vertically fit, then the horizontal scale should just be ignored where appropriate.
+ if ((Fits(dx) || cfgVerticalFit) && Fits(dy) || !cfgVerticalFit && Fits(dx) && Larger(dy) || Larger(dx) && Fits(dy))
{
// Found snap that fits.
return i;
diff --git a/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs b/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
index f6b0929f3b..a10155f3e8 100644
--- a/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
+++ b/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
@@ -169,7 +169,7 @@ protected Vector2 InverseMapPosition(Vector2 value)
var inversePos = (value - MidPointVector) / MinimapScale;
inversePos = inversePos with { Y = -inversePos.Y };
- inversePos = Matrix3.CreateTransform(Offset, Angle.Zero).Transform(inversePos);
+ inversePos = Vector2.Transform(inversePos, Matrix3Helpers.CreateTransform(Offset, Angle.Zero));
return inversePos;
}
diff --git a/Content.Client/UserInterface/Controls/NeoTabContainer.Props.xaml.cs b/Content.Client/UserInterface/Controls/NeoTabContainer.Props.xaml.cs
new file mode 100644
index 0000000000..318dd06014
--- /dev/null
+++ b/Content.Client/UserInterface/Controls/NeoTabContainer.Props.xaml.cs
@@ -0,0 +1,163 @@
+using static Robust.Client.UserInterface.Controls.BoxContainer.LayoutOrientation;
+using static Robust.Shared.Maths.Direction;
+
+namespace Content.Client.UserInterface.Controls;
+
+public sealed partial class NeoTabContainer
+{
+ // Too many computed properties...
+
+ ///
+ /// Where to place the tabs in relation to the contents
+ ///
+ /// If , the tabs will be above the contents
+ ///
+ private Direction _tabLocation = North;
+ public Direction TabLocation
+ {
+ get => _tabLocation;
+ set => LayoutChanged(value);
+ }
+
+ /// If the 's horizontal scroll is enabled
+ private bool _hScrollEnabled;
+ ///
+ public bool HScrollEnabled
+ {
+ get => _hScrollEnabled;
+ set => ScrollingChanged(value, _vScrollEnabled);
+ }
+
+ /// If the 's vertical scroll is enabled
+ private bool _vScrollEnabled;
+ ///
+ public bool VScrollEnabled
+ {
+ get => _vScrollEnabled;
+ set => ScrollingChanged(_hScrollEnabled, value);
+ }
+
+ /// The margin around the whole UI element
+ private Thickness _containerMargin = new(0);
+ ///
+ public Thickness ContainerMargin
+ {
+ get => _containerMargin;
+ set => ContainerMarginChanged(value);
+ }
+
+ /// The margin around the separator between the tabs and contents
+ public Thickness SeparatorMargin
+ {
+ get => Separator.Margin;
+ set => Separator.Margin = value;
+ }
+
+ private bool _firstTabOpenBoth;
+ public bool FirstTabOpenBoth
+ {
+ get => _firstTabOpenBoth;
+ set => TabStyleChanged(value, LastTabOpenBoth);
+ }
+
+ private bool _lastTabOpenBoth;
+ public bool LastTabOpenBoth
+ {
+ get => _lastTabOpenBoth;
+ set => TabStyleChanged(FirstTabOpenBoth, value);
+ }
+
+
+ /// Changes the layout of the tabs and contents based on the value
+ /// See
+ private void LayoutChanged(Direction direction)
+ {
+ _tabLocation = direction;
+
+ LayoutOrientation DirectionalOrientation(Direction direction, LayoutOrientation north, LayoutOrientation south,
+ LayoutOrientation east, LayoutOrientation west)
+ {
+ return direction switch
+ {
+ North => north,
+ South => south,
+ East => east,
+ West => west,
+ _ => Vertical,
+ };
+ }
+ TabContainer.Orientation = DirectionalOrientation(direction, Horizontal, Horizontal, Vertical, Vertical);
+ Container.Orientation = DirectionalOrientation(direction, Vertical, Vertical, Horizontal, Horizontal);
+
+ var containerMargin = direction switch
+ {
+ North => new Thickness(_containerMargin.Left, 0, _containerMargin.Right, _containerMargin.Bottom),
+ South => new Thickness(_containerMargin.Left, _containerMargin.Top, _containerMargin.Right, 0),
+ East => new Thickness(_containerMargin.Left, _containerMargin.Top, 0, _containerMargin.Bottom),
+ West => new Thickness(0, _containerMargin.Top, _containerMargin.Right, _containerMargin.Bottom),
+ _ => _containerMargin,
+ };
+ TabScrollContainer.Margin = containerMargin;
+ ContentScrollContainer.Margin = containerMargin;
+
+ bool DirectionalBool(Direction direction, bool north, bool south, bool east, bool west)
+ {
+ return direction switch
+ {
+ North => north,
+ South => south,
+ East => east,
+ West => west,
+ _ => false,
+ };
+ }
+ TabScrollContainer.HorizontalExpand = DirectionalBool(direction, true, true, false, false);
+ TabScrollContainer.VerticalExpand = DirectionalBool(direction, false, false, true, true);
+ TabScrollContainer.HScrollEnabled = DirectionalBool(direction, true, true, false, false);
+ TabScrollContainer.VScrollEnabled = DirectionalBool(direction, false, false, true, true);
+
+
+ // Move the TabScrollContainer, Separator, and ContentScrollContainer to the correct positions
+ TabScrollContainer.Orphan();
+ Separator.Orphan();
+ ContentScrollContainer.Orphan();
+
+ if (direction is North or West)
+ {
+ Container.AddChild(TabScrollContainer);
+ Container.AddChild(Separator);
+ Container.AddChild(ContentScrollContainer);
+ }
+ else
+ {
+ Container.AddChild(ContentScrollContainer);
+ Container.AddChild(Separator);
+ Container.AddChild(TabScrollContainer);
+ }
+
+ UpdateTabMerging();
+ }
+
+ private void ScrollingChanged(bool hScroll, bool vScroll)
+ {
+ _hScrollEnabled = hScroll;
+ _vScrollEnabled = vScroll;
+
+ ContentScrollContainer.HScrollEnabled = hScroll;
+ ContentScrollContainer.VScrollEnabled = vScroll;
+ }
+
+ private void ContainerMarginChanged(Thickness value)
+ {
+ _containerMargin = value;
+ LayoutChanged(TabLocation);
+ }
+
+ private void TabStyleChanged(bool firstTabOpenBoth, bool lastTabOpenBoth)
+ {
+ _firstTabOpenBoth = firstTabOpenBoth;
+ _lastTabOpenBoth = lastTabOpenBoth;
+
+ UpdateTabMerging();
+ }
+}
diff --git a/Content.Client/UserInterface/Controls/NeoTabContainer.xaml b/Content.Client/UserInterface/Controls/NeoTabContainer.xaml
new file mode 100644
index 0000000000..21ee79a6ca
--- /dev/null
+++ b/Content.Client/UserInterface/Controls/NeoTabContainer.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Controls/NeoTabContainer.xaml.cs b/Content.Client/UserInterface/Controls/NeoTabContainer.xaml.cs
new file mode 100644
index 0000000000..fc148cc634
--- /dev/null
+++ b/Content.Client/UserInterface/Controls/NeoTabContainer.xaml.cs
@@ -0,0 +1,299 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using static Content.Client.Stylesheets.StyleBase;
+using static Robust.Shared.Maths.Direction;
+
+namespace Content.Client.UserInterface.Controls;
+
+///
+/// A simple yet good-looking tab container using normal UI elements with multiple styles
+///
+/// Because nobody else could do it better.
+///
+[GenerateTypedNameReferences]
+public sealed partial class NeoTabContainer : BoxContainer
+{
+ private readonly Dictionary _tabs = new();
+ private readonly List _controls = new();
+ private readonly ButtonGroup _tabGroup = new(false);
+
+ /// All children within the
+ public OrderedChildCollection Contents => ContentContainer.Children;
+ /// All children within the that are visible
+ public List VisibleContents => Contents.Where(c => c == CurrentControl).ToList();
+
+ /// All children within the
+ public OrderedChildCollection Tabs => TabContainer.Children;
+ /// All children within the that are visible
+ public List VisibleTabs => Tabs.Where(c => c.Visible).ToList();
+
+ public Control? CurrentControl { get; private set; }
+ public int? CurrentTab => _controls.FirstOrDefault(control => control == CurrentControl) switch
+ {
+ { } control => _controls.IndexOf(control),
+ _ => null,
+ };
+
+
+ ///
+ public NeoTabContainer()
+ {
+ RobustXamlLoader.Load(this);
+
+ LayoutChanged(TabLocation);
+ ScrollingChanged(HScrollEnabled, VScrollEnabled);
+ }
+
+ protected override void ChildRemoved(Control child)
+ {
+ if (_tabs.Remove(child, out var button))
+ button.Dispose();
+
+ // Set the current tab to a different control
+ if (CurrentControl == child)
+ {
+ var previous = _controls.IndexOf(child) - 1;
+
+ if (previous > -1)
+ SelectTab(_controls[previous]);
+ else
+ CurrentControl = null;
+ }
+
+ _controls.Remove(child);
+ base.ChildRemoved(child);
+ UpdateTabMerging();
+ }
+
+ // A fun display of every location for the tabs if you want it
+ // private TimeSpan _lastLayoutChange = TimeSpan.Zero;
+ // private TimeSpan _nextLayoutChange = TimeSpan.Zero;
+ // protected override void FrameUpdate(FrameEventArgs args)
+ // {
+ // base.FrameUpdate(args);
+ //
+ // _lastLayoutChange += TimeSpan.FromSeconds(args.DeltaSeconds);
+ // // Change the layout every second such that the tabs go in a circle
+ // if (_lastLayoutChange.TotalSeconds < _nextLayoutChange.TotalSeconds)
+ // return;
+ //
+ // _lastLayoutChange = _nextLayoutChange;
+ // _nextLayoutChange = _lastLayoutChange + TimeSpan.FromSeconds(2);
+ //
+ // LayoutChanged(TabLocation switch
+ // {
+ // North => East,
+ // East => South,
+ // South => West,
+ // West => North,
+ // _ => North,
+ // });
+ // }
+
+
+ ///
+ /// Adds a tab to this container
+ ///
+ /// The tab contents
+ /// The title of the tab
+ /// Whether the tabs should fix their styling automatically. Useful if you're doing tons of updates at once
+ /// The index of the new tab
+ public int AddTab(Control control, string? title, bool updateTabMerging = true)
+ {
+ var button = new Button
+ {
+ Group = _tabGroup,
+ MinHeight = 32,
+ MaxHeight = 32,
+ HorizontalExpand = true,
+ };
+ button.OnPressed += _ => SelectTab(control);
+ if (!string.IsNullOrEmpty(title))
+ button.Text = title;
+
+ TabContainer.AddChild(button);
+ ContentContainer.AddChild(control);
+ _controls.Add(control);
+ _tabs.Add(control, button);
+
+ // Show it if it has content
+ if (ContentContainer.ChildCount > 1)
+ control.Visible = false;
+ else
+ // Select it if it's the only tab
+ SelectTab(control);
+
+ if (updateTabMerging)
+ UpdateTabMerging();
+ return ChildCount - 1;
+ }
+
+ ///
+ /// Removes/Disposes the tab associated with the given index
+ ///
+ /// The index of the tab to remove
+ /// Whether the tabs should fix their styling automatically. Useful if you're doing tons of updates at once
+ /// True if the tab was removed, false otherwise
+ public bool RemoveTab(int index, bool updateTabMerging = true)
+ {
+ if (index < 0 || index >= _controls.Count)
+ return false;
+
+ var control = _controls[index];
+ RemoveTab(control, updateTabMerging);
+ return true;
+ }
+
+ ///
+ /// Removes/Disposes the tab associated with the given control
+ ///
+ /// The control to remove
+ /// Whether the tabs should fix their styling automatically. Useful if you're doing tons of updates at once
+ /// True if the tab was removed, false otherwise
+ public bool RemoveTab(Control control, bool updateTabMerging = true)
+ {
+ if (!_tabs.TryGetValue(control, out var button))
+ return false;
+
+ button.Dispose();
+ control.Dispose();
+ if (updateTabMerging)
+ UpdateTabMerging();
+ return true;
+ }
+
+
+ /// Sets the title of the tab associated with the given index
+ public void SetTabTitle(int index, string title)
+ {
+ if (index < 0 || index >= _controls.Count)
+ return;
+
+ var control = _controls[index];
+ SetTabTitle(control, title);
+ }
+
+ /// Sets the title of the tab associated with the given control
+ public void SetTabTitle(Control control, string title)
+ {
+ if (!_tabs.TryGetValue(control, out var button))
+ return;
+
+ if (button is Button b)
+ b.Text = title;
+ }
+
+ /// Shows or hides the tab associated with the given index
+ public void SetTabVisible(int index, bool visible)
+ {
+ if (index < 0 || index >= _controls.Count)
+ return;
+
+ var control = _controls[index];
+ SetTabVisible(control, visible);
+ }
+
+ /// Shows or hides the tab associated with the given control
+ public void SetTabVisible(Control control, bool visible)
+ {
+ if (!_tabs.TryGetValue(control, out var button))
+ return;
+
+ button.Visible = visible;
+ UpdateTabMerging();
+ }
+
+ /// Selects the tab associated with the control
+ public void SelectTab(Control control)
+ {
+ if (CurrentControl != null)
+ CurrentControl.Visible = false;
+
+ var button = _tabs[control];
+ button.Pressed = true;
+ control.Visible = true;
+ CurrentControl = control;
+ }
+
+ /// Sets the style of every visible tab's Button to be Open to Right, Both, or Left depending on position
+ public void UpdateTabMerging()
+ {
+ var visibleTabs = VisibleTabs;
+
+ if (visibleTabs.Count == 0)
+ return;
+
+ if (visibleTabs.Count == 1)
+ {
+ var button = visibleTabs[0];
+ button.RemoveStyleClass(ButtonOpenRight);
+ button.RemoveStyleClass(ButtonOpenBoth);
+ button.RemoveStyleClass(ButtonOpenLeft);
+
+ if (FirstTabOpenBoth)
+ button.AddStyleClass(ButtonOpenBoth);
+
+ return;
+ }
+
+ string GetDirection(Direction direction, int position)
+ {
+ return position switch
+ {
+ // First
+ 0 => direction switch
+ {
+ North => ButtonOpenRight,
+ South => ButtonOpenRight,
+ East => ButtonOpenLeft,
+ West => ButtonOpenLeft,
+ _ => ButtonOpenRight,
+ },
+ // Middle
+ 1 => ButtonOpenBoth,
+ // Last
+ 2 => direction switch
+ {
+ North => ButtonOpenLeft,
+ South => ButtonOpenLeft,
+ East => ButtonOpenRight,
+ West => ButtonOpenRight,
+ _ => ButtonOpenLeft,
+ },
+ _ => ButtonOpenBoth,
+ };
+ }
+
+ for (var i = 0; i < visibleTabs.Count; i++)
+ {
+ var button = visibleTabs[i];
+ button.RemoveStyleClass(ButtonOpenRight);
+ button.RemoveStyleClass(ButtonOpenBoth);
+ button.RemoveStyleClass(ButtonOpenLeft);
+
+ if (FirstTabOpenBoth && i == 0)
+ {
+ button.AddStyleClass(ButtonOpenBoth);
+ continue;
+ }
+ if (LastTabOpenBoth && i == visibleTabs.Count - 1)
+ {
+ button.AddStyleClass(ButtonOpenBoth);
+ continue;
+ }
+
+ var position = i switch
+ {
+ 0 => 0,
+ _ when i == visibleTabs.Count - 1 => 2,
+ _ => 1,
+ };
+ button.AddStyleClass(GetDirection(TabLocation, position));
+ }
+ }
+}
diff --git a/Content.Client/UserInterface/Controls/SlotButton.cs b/Content.Client/UserInterface/Controls/SlotButton.cs
index 4ec6861606..c33782371f 100644
--- a/Content.Client/UserInterface/Controls/SlotButton.cs
+++ b/Content.Client/UserInterface/Controls/SlotButton.cs
@@ -9,6 +9,7 @@ public SlotButton() { }
public SlotButton(SlotData slotData)
{
ButtonTexturePath = slotData.TextureName;
+ FullButtonTexturePath = slotData.FullTextureName;
Blocked = slotData.Blocked;
Highlight = slotData.Highlighted;
StorageTexturePath = "Slots/back";
diff --git a/Content.Client/UserInterface/Controls/SlotControl.cs b/Content.Client/UserInterface/Controls/SlotControl.cs
index 9b94f8a0c8..a684bb05ef 100644
--- a/Content.Client/UserInterface/Controls/SlotControl.cs
+++ b/Content.Client/UserInterface/Controls/SlotControl.cs
@@ -15,11 +15,12 @@ public abstract class SlotControl : Control, IEntityControl
public TextureRect ButtonRect { get; }
public TextureRect BlockedRect { get; }
public TextureRect HighlightRect { get; }
- public SpriteView SpriteView { get; }
public SpriteView HoverSpriteView { get; }
public TextureButton StorageButton { get; }
public CooldownGraphic CooldownDisplay { get; }
+ private SpriteView SpriteView { get; }
+
public EntityUid? Entity => SpriteView.Entity;
private bool _slotNameSet;
@@ -68,7 +69,18 @@ public string? ButtonTexturePath
set
{
_buttonTexturePath = value;
- ButtonRect.Texture = Theme.ResolveTextureOrNull(_buttonTexturePath)?.Texture;
+ UpdateButtonTexture();
+ }
+ }
+
+ private string? _fullButtonTexturePath;
+ public string? FullButtonTexturePath
+ {
+ get => _fullButtonTexturePath;
+ set
+ {
+ _fullButtonTexturePath = value;
+ UpdateButtonTexture();
}
}
@@ -197,6 +209,21 @@ public void ClearHover()
HoverSpriteView.SetEntity(null);
}
+ public void SetEntity(EntityUid? ent)
+ {
+ SpriteView.SetEntity(ent);
+ UpdateButtonTexture();
+ }
+
+ private void UpdateButtonTexture()
+ {
+ var fullTexture = Theme.ResolveTextureOrNull(_fullButtonTexturePath);
+ var texture = Entity.HasValue && fullTexture != null
+ ? fullTexture.Texture
+ : Theme.ResolveTextureOrNull(_buttonTexturePath)?.Texture;
+ ButtonRect.Texture = texture;
+ }
+
private void OnButtonPressed(GUIBoundKeyEventArgs args)
{
Pressed?.Invoke(args, this);
@@ -229,8 +256,8 @@ protected override void OnThemeUpdated()
base.OnThemeUpdated();
StorageButton.TextureNormal = Theme.ResolveTextureOrNull(_storageTexturePath)?.Texture;
- ButtonRect.Texture = Theme.ResolveTextureOrNull(_buttonTexturePath)?.Texture;
HighlightRect.Texture = Theme.ResolveTextureOrNull(_highlightTexturePath)?.Texture;
+ UpdateButtonTexture();
}
EntityUid? IEntityControl.UiEntity => Entity;
diff --git a/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml b/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml
index 4ba820b339..0596009ed1 100644
--- a/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml
+++ b/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml
@@ -9,6 +9,7 @@
xmlns:hotbar="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:inventory="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Widgets"
+ xmlns:targeting="clr-namespace:Content.Client.UserInterface.Systems.Targeting.Widgets"
Name="DefaultHud"
VerticalExpand="False"
VerticalAlignment="Bottom"
@@ -28,6 +29,7 @@
+
diff --git a/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml.cs b/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml.cs
index c45ec9d4a0..d6bc0f97cc 100644
--- a/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml.cs
+++ b/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml.cs
@@ -22,6 +22,7 @@ public OverlayChatGameScreen()
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
SetAnchorAndMarginPreset(Chat, LayoutPreset.TopRight, margin: 10);
SetAnchorAndMarginPreset(Alerts, LayoutPreset.TopRight, margin: 10);
+ SetAnchorAndMarginPreset(Targeting, LayoutPreset.BottomRight, margin: 5);
Chat.OnResized += ChatOnResized;
Chat.OnChatResizeFinish += ChatOnResizeFinish;
diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml
index 7f1d1bcd5b..c60b6c44dd 100644
--- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml
+++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml
@@ -10,6 +10,7 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:inventory="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Widgets"
+ xmlns:targeting="clr-namespace:Content.Client.UserInterface.Systems.Targeting.Widgets"
Name="SeparatedChatHud"
VerticalExpand="False"
VerticalAlignment="Bottom"
@@ -20,6 +21,7 @@
+
diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs
index 45a29e03f1..5c612587ed 100644
--- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs
+++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs
@@ -23,6 +23,7 @@ public SeparatedChatGameScreen()
SetAnchorAndMarginPreset(Ghost, LayoutPreset.BottomWide, margin: 80);
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
SetAnchorAndMarginPreset(Alerts, LayoutPreset.CenterRight, margin: 10);
+ SetAnchorAndMarginPreset(Targeting, LayoutPreset.BottomRight, margin: 5);
ScreenContainer.OnSplitResizeFinished += () =>
OnChatResized?.Invoke(new Vector2(ScreenContainer.SplitFraction, 0));
diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
index 09663ba82c..69ac3ab023 100644
--- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
+++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
@@ -15,6 +15,7 @@
using Content.Shared.Input;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
+using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
@@ -42,6 +43,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged position && position >= 0)
+ _actions.RemoveAt(position);
}
}
else if (button.TryReplaceWith(actionId.Value, _actionsSystem) &&
@@ -539,24 +548,23 @@ private void SetAction(ActionButton button, EntityUid? actionId, bool updateSlot
private void DragAction()
{
- EntityUid? swapAction = null;
- if (UIManager.CurrentlyHovered is ActionButton button)
+ if (_menuDragHelper.Dragged is not {ActionId: {} action} dragged)
{
- if (!_menuDragHelper.IsDragging || _menuDragHelper.Dragged?.ActionId is not { } type)
- {
- _menuDragHelper.EndDrag();
- return;
- }
-
- swapAction = button.ActionId;
- SetAction(button, type, false);
+ _menuDragHelper.EndDrag();
+ return;
}
- if (_menuDragHelper.Dragged is {Parent: ActionButtonContainer} old)
+ EntityUid? swapAction = null;
+ var currentlyHovered = UIManager.MouseGetControl(_input.MouseScreenPosition);
+ if (currentlyHovered is ActionButton button)
{
- SetAction(old, swapAction, false);
+ swapAction = button.ActionId;
+ SetAction(button, action, false);
}
+ if (dragged.Parent is ActionButtonContainer)
+ SetAction(dragged, swapAction, false);
+
if (_actionsSystem != null)
_container?.SetActionData(_actionsSystem, _actions.ToArray());
@@ -610,27 +618,27 @@ private void OnWindowActionFocusExisted(ActionButton button)
private void OnActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
{
- if (args.Function == EngineKeyFunctions.UIClick)
+ if (args.Function == EngineKeyFunctions.UIRightClick)
{
- if (button.ActionId == null)
- {
- var ev = new FillActionSlotEvent();
- EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
- if (ev.Action != null)
- SetAction(button, ev.Action);
- }
- else
- {
- _menuDragHelper.MouseDown(button);
- }
-
+ SetAction(button, null);
args.Handle();
+ return;
}
- else if (args.Function == EngineKeyFunctions.UIRightClick)
+
+ if (args.Function != EngineKeyFunctions.UIClick)
+ return;
+
+ args.Handle();
+ if (button.ActionId != null)
{
- SetAction(button, null);
- args.Handle();
+ _menuDragHelper.MouseDown(button);
+ return;
}
+
+ var ev = new FillActionSlotEvent();
+ EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
+ if (ev.Action != null)
+ SetAction(button, ev.Action);
}
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
@@ -638,33 +646,31 @@ private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
if (args.Function != EngineKeyFunctions.UIClick || _actionsSystem == null)
return;
- //todo: make dragging onto the same spot NOT trigger again
- if (UIManager.CurrentlyHovered == button)
- {
- _menuDragHelper.EndDrag();
+ args.Handle();
- if (_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
- {
- if (baseAction is BaseTargetActionComponent action)
- {
- // for target actions, we go into "select target" mode, we don't
- // message the server until we actually pick our target.
-
- // if we're clicking the same thing we're already targeting for, then we simply cancel
- // targeting
- ToggleTargeting(button.ActionId.Value, action);
- return;
- }
-
- _actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
- }
- }
- else
+ if (_menuDragHelper.IsDragging)
{
DragAction();
+ return;
}
- args.Handle();
+ _menuDragHelper.EndDrag();
+
+ if (!_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
+ return;
+
+ if (baseAction is not BaseTargetActionComponent action)
+ {
+ _actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
+ return;
+ }
+
+ // for target actions, we go into "select target" mode, we don't
+ // message the server until we actually pick our target.
+
+ // if we're clicking the same thing we're already targeting for, then we simply cancel
+ // targeting
+ ToggleTargeting(button.ActionId.Value, action);
}
private bool OnMenuBeginDrag()
diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
index 2372d98f8d..6be41af0d8 100644
--- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
+++ b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
@@ -152,16 +152,8 @@ public ActionButton(IEntityManager entities, SpriteSystem? spriteSys = null, Act
OnThemeUpdated();
- OnKeyBindDown += args =>
- {
- Depress(args, true);
- OnPressed(args);
- };
- OnKeyBindUp += args =>
- {
- Depress(args, false);
- OnUnpressed(args);
- };
+ OnKeyBindDown += OnPressed;
+ OnKeyBindUp += OnUnpressed;
TooltipSupplier = SupplyTooltip;
}
@@ -175,11 +167,23 @@ protected override void OnThemeUpdated()
private void OnPressed(GUIBoundKeyEventArgs args)
{
+ if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.UIRightClick)
+ return;
+
+ if (args.Function == EngineKeyFunctions.UIRightClick)
+ Depress(args, true);
+
ActionPressed?.Invoke(args, this);
}
private void OnUnpressed(GUIBoundKeyEventArgs args)
{
+ if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.UIRightClick)
+ return;
+
+ if (args.Function == EngineKeyFunctions.UIRightClick)
+ Depress(args, false);
+
ActionUnpressed?.Invoke(args, this);
}
@@ -281,10 +285,19 @@ public void UpdateIcons()
_controller ??= UserInterfaceManager.GetUIController();
_spriteSys ??= _entities.System();
- if ((_controller.SelectingTargetFor == ActionId || _action.Toggled) && _action.IconOn != null)
- SetActionIcon(_spriteSys.Frame0(_action.IconOn));
+ if ((_controller.SelectingTargetFor == ActionId || _action.Toggled))
+ {
+ if (_action.IconOn != null)
+ SetActionIcon(_spriteSys.Frame0(_action.IconOn));
+
+ if (_action.BackgroundOn != null)
+ _buttonBackgroundTexture = _spriteSys.Frame0(_action.BackgroundOn);
+ }
else
+ {
SetActionIcon(_action.Icon != null ? _spriteSys.Frame0(_action.Icon) : null);
+ _buttonBackgroundTexture = Theme.ResolveTexture("SlotBackground");
+ }
}
public void UpdateBackground()
@@ -378,12 +391,6 @@ public void Depress(GUIBoundKeyEventArgs args, bool depress)
if (_action is not {Enabled: true})
return;
- if (_depressed && !depress)
- {
- // fire the action
- OnUnpressed(args);
- }
-
_depressed = depress;
DrawModeChanged();
}
diff --git a/Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs b/Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs
index 9423f7288d..6327757dec 100644
--- a/Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs
+++ b/Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs
@@ -3,7 +3,6 @@
using Content.Client.Cooldown;
using Content.Shared.Alert;
using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
@@ -118,7 +117,8 @@ protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
- _entityManager.DeleteEntity(_spriteViewEntity);
+ if (!_entityManager.Deleted(_spriteViewEntity))
+ _entityManager.QueueDeleteEntity(_spriteViewEntity);
}
}
diff --git a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml
index 9a273d2ed1..e52e2175b2 100644
--- a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml
+++ b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml
@@ -1,7 +1,11 @@
-
-
-
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
index 189de50407..a1a494c47b 100644
--- a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
@@ -97,7 +97,8 @@ private void SyncUpdateControls(AlertsSystem alertsSystem, AlertOrderPrototype?
}
else
{
- if (existingAlertControl != null) AlertContainer.Children.Remove(existingAlertControl);
+ if (existingAlertControl != null)
+ AlertContainer.Children.Remove(existingAlertControl);
// this is a new alert + alert key or just a different alert with the same
// key, create the control and add it in the appropriate order
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
index bc79283d76..b1d8b01050 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
@@ -550,7 +550,7 @@ private void UpdateChannelPermissions()
}
// only admins can see / filter asay
- if (_admin.HasFlag(AdminFlags.Admin))
+ if (_admin.HasFlag(AdminFlags.Adminchat))
{
FilterableChannels |= ChatChannel.Admin;
FilterableChannels |= ChatChannel.AdminAlert;
diff --git a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs
new file mode 100644
index 0000000000..7b86859a1a
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs
@@ -0,0 +1,125 @@
+using Content.Client.Chat.UI;
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Chat;
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Input;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.UserInterface.Systems.Emotes;
+
+[UsedImplicitly]
+public sealed class EmotesUIController : UIController, IOnStateChanged
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+
+ private MenuButton? EmotesButton => UIManager.GetActiveUIWidgetOrNull()?.EmotesButton;
+ private EmotesMenu? _menu;
+
+ public void OnStateEntered(GameplayState state)
+ {
+ CommandBinds.Builder
+ .Bind(ContentKeyFunctions.OpenEmotesMenu,
+ InputCmdHandler.FromDelegate(_ => ToggleEmotesMenu(false)))
+ .Register();
+ }
+
+ public void OnStateExited(GameplayState state)
+ {
+ CommandBinds.Unregister();
+ }
+
+ private void ToggleEmotesMenu(bool centered)
+ {
+ if (_menu == null)
+ {
+ // setup window
+ _menu = UIManager.CreateWindow();
+ _menu.OnClose += OnWindowClosed;
+ _menu.OnOpen += OnWindowOpen;
+ _menu.OnPlayEmote += OnPlayEmote;
+
+ if (EmotesButton != null)
+ EmotesButton.SetClickPressed(true);
+
+ if (centered)
+ {
+ _menu.OpenCentered();
+ }
+ else
+ {
+ // Open the menu, centered on the mouse
+ var vpSize = _displayManager.ScreenSize;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ }
+ }
+ else
+ {
+ _menu.OnClose -= OnWindowClosed;
+ _menu.OnOpen -= OnWindowOpen;
+ _menu.OnPlayEmote -= OnPlayEmote;
+
+ if (EmotesButton != null)
+ EmotesButton.SetClickPressed(false);
+
+ CloseMenu();
+ }
+ }
+
+ public void UnloadButton()
+ {
+ if (EmotesButton == null)
+ return;
+
+ EmotesButton.OnPressed -= ActionButtonPressed;
+ }
+
+ public void LoadButton()
+ {
+ if (EmotesButton == null)
+ return;
+
+ EmotesButton.OnPressed += ActionButtonPressed;
+ }
+
+ private void ActionButtonPressed(BaseButton.ButtonEventArgs args)
+ {
+ ToggleEmotesMenu(true);
+ }
+
+ private void OnWindowClosed()
+ {
+ if (EmotesButton != null)
+ EmotesButton.Pressed = false;
+
+ CloseMenu();
+ }
+
+ private void OnWindowOpen()
+ {
+ if (EmotesButton != null)
+ EmotesButton.Pressed = true;
+ }
+
+ private void CloseMenu()
+ {
+ if (_menu == null)
+ return;
+
+ _menu.Dispose();
+ _menu = null;
+ }
+
+ private void OnPlayEmote(ProtoId protoId)
+ {
+ _entityManager.RaisePredictiveEvent(new PlayEmoteMessage(protoId));
+ }
+}
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/GhostTargetWindow.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/GhostTargetWindow.xaml
index 86fd09b2c8..32cab732d1 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/GhostTargetWindow.xaml
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/GhostTargetWindow.xaml
@@ -1,5 +1,6 @@
+
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/GhostTargetWindow.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/GhostTargetWindow.xaml.cs
index 3c17697c25..9a20226fdf 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/GhostTargetWindow.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/GhostTargetWindow.xaml.cs
@@ -15,11 +15,14 @@ public sealed partial class GhostTargetWindow : DefaultWindow
private string _searchText = string.Empty;
public event Action? WarpClicked;
+ public event Action? OnGhostnadoClicked;
public GhostTargetWindow()
{
RobustXamlLoader.Load(this);
SearchBar.OnTextChanged += OnSearchTextChanged;
+
+ GhostnadoButton.OnPressed += _ => OnGhostnadoClicked?.Invoke();
}
public void UpdateWarps(IEnumerable warps)
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml
index 92e38e35e0..ffde5d69f7 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml
@@ -5,7 +5,7 @@
Text="{Loc 'ghost-roles-window-request-role-button'}"
StyleClasses="OpenRight"
HorizontalAlignment="Left"
- SetWidth="150"/>
+ SetWidth="300"/>
"ghost-roles-window-request-role-button",
+ GhostRoleKind.RaffleReady => "ghost-roles-window-join-raffle-button",
+ GhostRoleKind.RaffleInProgress => "ghost-roles-window-raffle-in-progress-button",
+ GhostRoleKind.RaffleJoined => "ghost-roles-window-leave-raffle-button",
+ _ => throw new ArgumentOutOfRangeException(nameof(_ghostRoleKind),
+ $"Unknown {nameof(GhostRoleKind)} '{_ghostRoleKind}'")
+ };
+
+ if (IsActiveRaffle(_ghostRoleKind))
+ {
+ var timeLeft = _timing.CurTime <= _raffleEndTime
+ ? _raffleEndTime - _timing.CurTime
+ : TimeSpan.Zero;
+
+ var timeString = $"{timeLeft.Minutes:0}:{timeLeft.Seconds:00}";
+ RequestButton.Text = Loc.GetString(messageId, ("time", timeString), ("players", _playerCount));
+ }
+ else
+ {
+ RequestButton.Text = Loc.GetString(messageId);
+ }
+ }
+
+ private static bool IsActiveRaffle(GhostRoleKind kind)
+ {
+ return kind is GhostRoleKind.RaffleInProgress or GhostRoleKind.RaffleJoined;
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+ if (IsActiveRaffle(_ghostRoleKind))
+ {
+ UpdateRequestButton();
+ }
+ }
}
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs
index d6a53adff2..fc53cc72ae 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs
@@ -26,7 +26,7 @@ public GhostRolesEntry(string name, string description, bool hasAccess, Formatte
foreach (var role in roles)
{
- var button = new GhostRoleEntryButtons();
+ var button = new GhostRoleEntryButtons(role);
button.RequestButton.OnPressed += _ => OnRoleSelected?.Invoke(role);
button.FollowButton.OnPressed += _ => OnRoleFollow?.Invoke(role);
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs
index f03289782a..1fb8174a1c 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs
@@ -1,7 +1,8 @@
using System.Linq;
using Content.Client.Eui;
+using Content.Client.Lobby;
using Content.Client.Players.PlayTimeTracking;
-using Content.Client.Preferences;
+using Content.Shared.Clothing.Loadouts.Prototypes;
using Content.Shared.Customization.Systems;
using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
@@ -10,7 +11,6 @@
using Robust.Client.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
{
@@ -25,13 +25,24 @@ public GhostRolesEui()
{
_window = new GhostRolesWindow();
- _window.OnRoleRequested += info =>
+ _window.OnRoleRequestButtonClicked += info =>
{
- if (_windowRules != null)
- _windowRules.Close();
+ _windowRules?.Close();
+
+ if (info.Kind == GhostRoleKind.RaffleJoined)
+ {
+ SendMessage(new LeaveGhostRoleRaffleMessage(info.Identifier));
+ return;
+ }
+
_windowRules = new GhostRoleRulesWindow(info.Rules, _ =>
{
- SendMessage(new GhostRoleTakeoverRequestMessage(info.Identifier));
+ SendMessage(new RequestGhostRoleMessage(info.Identifier));
+
+ // if raffle role, close rules window on request, otherwise do
+ // old behavior of waiting for the server to close it
+ if (info.Kind != GhostRoleKind.FirstComeFirstServe)
+ _windowRules?.Close();
});
_windowRulesId = info.Identifier;
_windowRules.OnClose += () =>
@@ -43,7 +54,7 @@ public GhostRolesEui()
_window.OnRoleFollow += info =>
{
- SendMessage(new GhostRoleFollowRequestMessage(info.Identifier));
+ SendMessage(new FollowGhostRoleMessage(info.Identifier));
};
_window.OnClose += () =>
@@ -69,7 +80,8 @@ public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
- if (state is not GhostRolesEuiState ghostState) return;
+ if (state is not GhostRolesEuiState ghostState)
+ return;
_window.ClearEntries();
var entityManager = IoCManager.Resolve();
@@ -91,15 +103,16 @@ public override void HandleState(EuiStateBase state)
var hasAccess = true;
if (!characterReqs.CheckRequirementsValid(
- group.Key.Requirements ?? new(),
- new(),
- (HumanoidCharacterProfile) (prefs.Preferences?.SelectedCharacter ?? HumanoidCharacterProfile.DefaultWithSpecies()),
- requirementsManager.GetRawPlayTimeTrackers(),
- requirementsManager.IsWhitelisted(),
- entityManager,
- protoMan,
- configManager,
- out var reasons))
+ group.Key.Requirements ?? new(),
+ new(),
+ (HumanoidCharacterProfile) (prefs.Preferences?.SelectedCharacter ?? HumanoidCharacterProfile.DefaultWithSpecies()),
+ requirementsManager.GetRawPlayTimeTrackers(),
+ requirementsManager.IsWhitelisted(),
+ new LoadoutPrototype(), // idk
+ entityManager,
+ protoMan,
+ configManager,
+ out var reasons))
hasAccess = false;
_window.AddEntry(name, description, hasAccess, characterReqs.GetRequirementsText(reasons), group, spriteSystem);
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml
index c91269063e..ea16a6f18a 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml
@@ -1,7 +1,7 @@
+ MinSize="490 400"
+ SetSize="490 500">
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml.cs
index 547d990e76..2e7c99641b 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml.cs
@@ -9,7 +9,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
[GenerateTypedNameReferences]
public sealed partial class GhostRolesWindow : DefaultWindow
{
- public event Action? OnRoleRequested;
+ public event Action? OnRoleRequestButtonClicked;
public event Action? OnRoleFollow;
public void ClearEntries()
@@ -23,7 +23,7 @@ public void AddEntry(string name, string description, bool hasAccess, FormattedM
NoRolesMessage.Visible = false;
var entry = new GhostRolesEntry(name, description, hasAccess, reason, roles, spriteSystem);
- entry.OnRoleSelected += OnRoleRequested;
+ entry.OnRoleSelected += OnRoleRequestButtonClicked;
entry.OnRoleFollow += OnRoleFollow;
EntryContainer.AddChild(entry);
}
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleEui.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleEui.cs
index 5c5e31de03..1e24d4c84c 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleEui.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleEui.cs
@@ -1,4 +1,5 @@
using Content.Client.Eui;
+using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
using JetBrains.Annotations;
@@ -41,7 +42,7 @@ public override void Opened()
_window.OpenCentered();
}
- private void OnMake(NetEntity entity, string name, string description, string rules, bool makeSentient)
+ private void OnMake(NetEntity entity, string name, string description, string rules, bool makeSentient, GhostRoleRaffleSettings? raffleSettings)
{
var session = _playerManager.LocalSession;
if (session == null)
@@ -49,12 +50,22 @@ private void OnMake(NetEntity entity, string name, string description, string ru
return;
}
+ var command = raffleSettings is not null ? "makeghostroleraffled" : "makeghostrole";
+
var makeGhostRoleCommand =
- $"makeghostrole " +
+ $"{command} " +
$"\"{CommandParsing.Escape(entity.ToString())}\" " +
$"\"{CommandParsing.Escape(name)}\" " +
- $"\"{CommandParsing.Escape(description)}\" " +
- $"\"{CommandParsing.Escape(rules)}\"";
+ $"\"{CommandParsing.Escape(description)}\" ";
+
+ if (raffleSettings is not null)
+ {
+ makeGhostRoleCommand += $"{raffleSettings.InitialDuration} " +
+ $"{raffleSettings.JoinExtendsDurationBy} " +
+ $"{raffleSettings.MaxDuration} ";
+ }
+
+ makeGhostRoleCommand += $"\"{CommandParsing.Escape(rules)}\"";
_consoleHost.ExecuteCommand(session, makeGhostRoleCommand);
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml
index 1d4033eed3..ff8e56f8fe 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml
@@ -22,6 +22,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml.cs
index 0839510970..6711d76b10 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml.cs
@@ -1,7 +1,12 @@
-using System.Numerics;
+using System.Linq;
+using System.Numerics;
+using Content.Server.Ghost.Roles.Raffles;
+using Content.Shared.Ghost.Roles.Raffles;
using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
@@ -9,10 +14,20 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
[GenerateTypedNameReferences]
public sealed partial class MakeGhostRoleWindow : DefaultWindow
{
- public delegate void MakeRole(NetEntity uid, string name, string description, string rules, bool makeSentient);
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ private readonly List _rafflePrototypes = [];
+
+ private const int RaffleDontRaffleId = -1;
+ private const int RaffleCustomRaffleId = -2;
+ private int _raffleSettingId = RaffleDontRaffleId;
+
+ private NetEntity? Entity { get; set; }
+
+ public event MakeRole? OnMake;
public MakeGhostRoleWindow()
{
+ IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
MakeSentientLabel.MinSize = new Vector2(150, 0);
@@ -23,13 +38,87 @@ public MakeGhostRoleWindow()
RoleDescription.MinSize = new Vector2(300, 0);
RoleRulesLabel.MinSize = new Vector2(150, 0);
RoleRules.MinSize = new Vector2(300, 0);
+ RaffleLabel.MinSize = new Vector2(150, 0);
+ RaffleButton.MinSize = new Vector2(300, 0);
+ RaffleInitialDurationLabel.MinSize = new Vector2(150, 0);
+ RaffleInitialDuration.MinSize = new Vector2(300, 0);
+ RaffleJoinExtendsDurationByLabel.MinSize = new Vector2(150, 0);
+ RaffleJoinExtendsDurationBy.MinSize = new Vector2(270, 0);
+ RaffleMaxDurationLabel.MinSize = new Vector2(150, 0);
+ RaffleMaxDuration.MinSize = new Vector2(270, 0);
+
+ RaffleInitialDuration.OverrideValue(30);
+ RaffleJoinExtendsDurationBy.OverrideValue(5);
+ RaffleMaxDuration.OverrideValue(60);
+
+ RaffleInitialDuration.SetButtons(new List { -30, -10 }, new List { 10, 30 });
+ RaffleJoinExtendsDurationBy.SetButtons(new List { -10, -5 }, new List { 5, 10 });
+ RaffleMaxDuration.SetButtons(new List { -30, -10 }, new List { 10, 30 });
+
+ RaffleInitialDuration.IsValid = duration => duration > 0;
+ RaffleJoinExtendsDurationBy.IsValid = duration => duration >= 0;
+ RaffleMaxDuration.IsValid = duration => duration > 0;
+
+ RaffleInitialDuration.ValueChanged += OnRaffleDurationChanged;
+ RaffleJoinExtendsDurationBy.ValueChanged += OnRaffleDurationChanged;
+ RaffleMaxDuration.ValueChanged += OnRaffleDurationChanged;
+
- MakeButton.OnPressed += OnPressed;
+ RaffleButton.AddItem("Don't raffle", RaffleDontRaffleId);
+ RaffleButton.AddItem("Custom settings", RaffleCustomRaffleId);
+
+ var raffleProtos =
+ _prototypeManager.EnumeratePrototypes();
+
+ var idx = 0;
+ foreach (var raffleProto in raffleProtos)
+ {
+ _rafflePrototypes.Add(raffleProto);
+ var s = raffleProto.Settings;
+ var label =
+ $"{raffleProto.ID} (initial {s.InitialDuration}s, max {s.MaxDuration}s, join adds {s.JoinExtendsDurationBy}s)";
+ RaffleButton.AddItem(label, idx++);
+ }
+
+ MakeButton.OnPressed += OnMakeButtonPressed;
+ RaffleButton.OnItemSelected += OnRaffleButtonItemSelected;
}
- private NetEntity? Entity { get; set; }
+ private void OnRaffleDurationChanged(ValueChangedEventArgs args)
+ {
+ ValidateRaffleDurations();
+ }
- public event MakeRole? OnMake;
+ private void ValidateRaffleDurations()
+ {
+ if (RaffleInitialDuration.Value > RaffleMaxDuration.Value)
+ {
+ MakeButton.Disabled = true;
+ MakeButton.ToolTip = "The initial duration must not exceed the maximum duration.";
+ }
+ else
+ {
+ MakeButton.Disabled = false;
+ MakeButton.ToolTip = null;
+ }
+ }
+
+ private void OnRaffleButtonItemSelected(OptionButton.ItemSelectedEventArgs args)
+ {
+ _raffleSettingId = args.Id;
+ args.Button.SelectId(args.Id);
+ if (args.Id != RaffleCustomRaffleId)
+ {
+ RaffleCustomSettingsContainer.Visible = false;
+ MakeButton.ToolTip = null;
+ MakeButton.Disabled = false;
+ }
+ else
+ {
+ RaffleCustomSettingsContainer.Visible = true;
+ ValidateRaffleDurations();
+ }
+ }
public void SetEntity(IEntityManager entManager, NetEntity entity)
{
@@ -38,14 +127,32 @@ public void SetEntity(IEntityManager entManager, NetEntity entity)
RoleEntity.Text = $"{entity}";
}
- private void OnPressed(ButtonEventArgs args)
+ private void OnMakeButtonPressed(ButtonEventArgs args)
{
if (Entity == null)
{
return;
}
- OnMake?.Invoke(Entity.Value, RoleName.Text, RoleDescription.Text, RoleRules.Text, MakeSentientCheckbox.Pressed);
+ GhostRoleRaffleSettings? raffleSettings = null;
+
+ if (_raffleSettingId == RaffleCustomRaffleId)
+ {
+ raffleSettings = new GhostRoleRaffleSettings()
+ {
+ InitialDuration = (uint) RaffleInitialDuration.Value,
+ JoinExtendsDurationBy = (uint) RaffleJoinExtendsDurationBy.Value,
+ MaxDuration = (uint) RaffleMaxDuration.Value
+ };
+ }
+ else if (_raffleSettingId != RaffleDontRaffleId)
+ {
+ raffleSettings = _rafflePrototypes[_raffleSettingId].Settings;
+ }
+
+ OnMake?.Invoke(Entity.Value, RoleName.Text, RoleDescription.Text, RoleRules.Text, MakeSentientCheckbox.Pressed, raffleSettings);
}
+
+ public delegate void MakeRole(NetEntity uid, string name, string description, string rules, bool makeSentient, GhostRoleRaffleSettings? settings);
}
}
diff --git a/Content.Client/UserInterface/Systems/Ghost/GhostUIController.cs b/Content.Client/UserInterface/Systems/Ghost/GhostUIController.cs
index 12d6c65953..3c6f992ad0 100644
--- a/Content.Client/UserInterface/Systems/Ghost/GhostUIController.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/GhostUIController.cs
@@ -111,6 +111,12 @@ private void OnWarpClicked(NetEntity player)
_net.SendSystemNetworkMessage(msg);
}
+ private void OnGhostnadoClicked()
+ {
+ var msg = new GhostnadoRequestEvent();
+ _net.SendSystemNetworkMessage(msg);
+ }
+
public void LoadGui()
{
if (Gui == null)
@@ -120,6 +126,8 @@ public void LoadGui()
Gui.ReturnToBodyPressed += ReturnToBody;
Gui.GhostRolesPressed += GhostRolesPressed;
Gui.TargetWindow.WarpClicked += OnWarpClicked;
+ Gui.TargetWindow.OnGhostnadoClicked += OnGhostnadoClicked;
+ Gui.ReturnToRoundPressed += ReturnToRound;
UpdateGui();
}
@@ -133,6 +141,7 @@ public void UnloadGui()
Gui.ReturnToBodyPressed -= ReturnToBody;
Gui.GhostRolesPressed -= GhostRolesPressed;
Gui.TargetWindow.WarpClicked -= OnWarpClicked;
+ Gui.ReturnToRoundPressed -= ReturnToRound;
Gui.Hide();
}
@@ -142,6 +151,11 @@ private void ReturnToBody()
_system?.ReturnToBody();
}
+ private void ReturnToRound()
+ {
+ _system?.ReturnToRound();
+ }
+
private void RequestWarps()
{
_system?.RequestWarps();
diff --git a/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml b/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml
index 0f65debb4e..5e9349bb43 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml
+++ b/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml
@@ -5,5 +5,6 @@
+
diff --git a/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs
index 0f64e8a275..fc22b20904 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs
@@ -14,6 +14,7 @@ public sealed partial class GhostGui : UIWidget
public event Action? RequestWarpsPressed;
public event Action? ReturnToBodyPressed;
public event Action? GhostRolesPressed;
+ public event Action? ReturnToRoundPressed;
public GhostGui()
{
@@ -26,6 +27,7 @@ public GhostGui()
GhostWarpButton.OnPressed += _ => RequestWarpsPressed?.Invoke();
ReturnToBodyButton.OnPressed += _ => ReturnToBodyPressed?.Invoke();
GhostRolesButton.OnPressed += _ => GhostRolesPressed?.Invoke();
+ ReturnToRound.OnPressed += _ => ReturnToRoundPressed?.Invoke();
}
public void Hide()
@@ -43,11 +45,11 @@ public void Update(int? roles, bool? canReturnToBody)
GhostRolesButton.Text = Loc.GetString("ghost-gui-ghost-roles-button", ("count", roles));
if (roles > 0)
{
- GhostRolesButton.StyleClasses.Add(StyleBase.ButtonCaution);
+ GhostRolesButton.StyleClasses.Add(StyleBase.ButtonDanger);
}
else
{
- GhostRolesButton.StyleClasses.Remove(StyleBase.ButtonCaution);
+ GhostRolesButton.StyleClasses.Remove(StyleBase.ButtonDanger);
}
}
diff --git a/Content.Client/UserInterface/Systems/Hands/Controls/HandButton.cs b/Content.Client/UserInterface/Systems/Hands/Controls/HandButton.cs
index 574e0c4707..d5794f7195 100644
--- a/Content.Client/UserInterface/Systems/Hands/Controls/HandButton.cs
+++ b/Content.Client/UserInterface/Systems/Hands/Controls/HandButton.cs
@@ -5,8 +5,11 @@ namespace Content.Client.UserInterface.Systems.Hands.Controls;
public sealed class HandButton : SlotControl
{
+ public HandLocation HandLocation { get; }
+
public HandButton(string handName, HandLocation handLocation)
{
+ HandLocation = handLocation;
Name = "hand_" + handName;
SlotName = handName;
SetBackground(handLocation);
diff --git a/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs b/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
index 1bcdd89af6..9ee429ba7e 100644
--- a/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
+++ b/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
@@ -22,12 +22,22 @@ public sealed class HandsUIController : UIController, IOnStateEntered _handsContainers = new();
private readonly Dictionary _handContainerIndices = new();
private readonly Dictionary _handLookup = new();
private HandsComponent? _playerHandsComponent;
private HandButton? _activeHand = null;
+
+ // We only have two item status controls (left and right hand),
+ // but we may have more than two hands.
+ // We handle this by having the item status be the *last active* hand of that side.
+ // These variables store which that is.
+ // ("middle" hands are hardcoded as right, whatever)
+ private HandButton? _statusHandLeft;
+ private HandButton? _statusHandRight;
+
private int _backupSuffix = 0; //this is used when autogenerating container names if they don't have names
private HotbarGui? HandsGui => UIManager.GetActiveUIWidgetOrNull();
@@ -120,12 +130,12 @@ private void LoadPlayerHands(HandsComponent handsComp)
if (_entities.TryGetComponent(hand.HeldEntity, out VirtualItemComponent? virt))
{
- handButton.SpriteView.SetEntity(virt.BlockingEntity);
+ handButton.SetEntity(virt.BlockingEntity);
handButton.Blocked = true;
}
else
{
- handButton.SpriteView.SetEntity(hand.HeldEntity);
+ handButton.SetEntity(hand.HeldEntity);
handButton.Blocked = false;
}
}
@@ -171,17 +181,16 @@ private void OnItemAdded(string name, EntityUid entity)
if (_entities.TryGetComponent(entity, out VirtualItemComponent? virt))
{
- hand.SpriteView.SetEntity(virt.BlockingEntity);
+ hand.SetEntity(virt.BlockingEntity);
hand.Blocked = true;
}
else
{
- hand.SpriteView.SetEntity(entity);
+ hand.SetEntity(entity);
hand.Blocked = false;
}
- if (_playerHandsComponent?.ActiveHand?.Name == name)
- HandsGui?.UpdatePanelEntity(entity);
+ UpdateHandStatus(hand, entity);
}
private void OnItemRemoved(string name, EntityUid entity)
@@ -190,9 +199,8 @@ private void OnItemRemoved(string name, EntityUid entity)
if (hand == null)
return;
- hand.SpriteView.SetEntity(null);
- if (_playerHandsComponent?.ActiveHand?.Name == name)
- HandsGui?.UpdatePanelEntity(null);
+ hand.SetEntity(null);
+ UpdateHandStatus(hand, null);
}
private HandsContainer GetFirstAvailableContainer()
@@ -232,7 +240,6 @@ private void SetActiveHand(string? handName)
if (_activeHand != null)
_activeHand.Highlight = false;
- HandsGui?.UpdatePanelEntity(null);
return;
}
@@ -250,7 +257,20 @@ private void SetActiveHand(string? handName)
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand(playerEntity, handName, out var hand, _playerHandsComponent))
{
- HandsGui.UpdatePanelEntity(hand.HeldEntity);
+ var foldedLocation = hand.Location.GetUILocation();
+ if (foldedLocation == HandUILocation.Left)
+ {
+ _statusHandLeft = handControl;
+ HandsGui.UpdatePanelEntityLeft(hand.HeldEntity);
+ }
+ else
+ {
+ // Middle or right
+ _statusHandRight = handControl;
+ HandsGui.UpdatePanelEntityRight(hand.HeldEntity);
+ }
+
+ HandsGui.SetHighlightHand(foldedLocation);
}
}
@@ -278,6 +298,16 @@ private HandButton AddHand(string handName, HandLocation location)
GetFirstAvailableContainer().AddButton(button);
}
+ // If we don't have a status for this hand type yet, set it.
+ // This means we have status filled by default in most scenarios,
+ // otherwise the user'd need to switch hands to "activate" the hands the first time.
+ if (location.GetUILocation() == HandUILocation.Left)
+ _statusHandLeft ??= button;
+ else
+ _statusHandRight ??= button;
+
+ UpdateVisibleStatusPanels();
+
return button;
}
@@ -336,11 +366,37 @@ private bool RemoveHand(string handName, out HandButton? handButton)
handContainer.RemoveButton(handButton);
}
+ if (_statusHandLeft == handButton)
+ _statusHandLeft = null;
+ if (_statusHandRight == handButton)
+ _statusHandRight = null;
+
_handLookup.Remove(handName);
handButton.Dispose();
+ UpdateVisibleStatusPanels();
return true;
}
+ private void UpdateVisibleStatusPanels()
+ {
+ var leftVisible = false;
+ var rightVisible = false;
+
+ foreach (var hand in _handLookup.Values)
+ {
+ if (hand.HandLocation.GetUILocation() == HandUILocation.Left)
+ {
+ leftVisible = true;
+ }
+ else
+ {
+ rightVisible = true;
+ }
+ }
+
+ HandsGui?.UpdateStatusVisibility(leftVisible, rightVisible);
+ }
+
public string RegisterHandContainer(HandsContainer handContainer)
{
var name = "HandContainer_" + _backupSuffix;
@@ -395,16 +451,25 @@ public override void FrameUpdate(FrameEventArgs args)
foreach (var hand in container.GetButtons())
{
- if (!_entities.TryGetComponent(hand.Entity, out UseDelayComponent? useDelay) ||
- useDelay is not { DelayStartTime: var start, DelayEndTime: var end })
+ if (!_entities.TryGetComponent(hand.Entity, out UseDelayComponent? useDelay))
{
hand.CooldownDisplay.Visible = false;
continue;
}
+ var delay = _useDelay.GetLastEndingDelay((hand.Entity.Value, useDelay));
hand.CooldownDisplay.Visible = true;
- hand.CooldownDisplay.FromTime(start, end);
+ hand.CooldownDisplay.FromTime(delay.StartTime, delay.EndTime);
}
}
}
+
+ private void UpdateHandStatus(HandButton hand, EntityUid? entity)
+ {
+ if (hand == _statusHandLeft)
+ HandsGui?.UpdatePanelEntityLeft(entity);
+
+ if (hand == _statusHandRight)
+ HandsGui?.UpdatePanelEntityRight(entity);
+ }
}
diff --git a/Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs b/Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs
index daececcd35..2f266cfdd6 100644
--- a/Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs
+++ b/Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs
@@ -31,7 +31,7 @@ private void OnScreenLoad()
ReloadHotbar();
}
- public void Setup(HandsContainer handsContainer, ItemStatusPanel handStatus, StorageContainer storageContainer)
+ public void Setup(HandsContainer handsContainer, StorageContainer storageContainer)
{
_inventory = UIManager.GetUIController();
_hands = UIManager.GetUIController();
diff --git a/Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml b/Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml
index 0e9f0c77f9..00ba1878b4 100644
--- a/Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml
+++ b/Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml
@@ -10,21 +10,14 @@
Orientation="Vertical"
HorizontalAlignment="Center">
-
-
-
-
-
-
+
+
+
+
+ ColumnLimit="6"/>
+
();
- hotbarController.Setup(HandContainer, StatusPanel, StoragePanel);
+ hotbarController.Setup(HandContainer, StoragePanel);
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
}
- public void UpdatePanelEntity(EntityUid? entity)
+ public void UpdatePanelEntityLeft(EntityUid? entity)
{
- StatusPanel.Update(entity);
- if (entity == null)
- {
- StatusPanel.Visible = false;
- return;
- }
+ StatusPanelLeft.Update(entity);
+ }
+
+ public void UpdatePanelEntityRight(EntityUid? entity)
+ {
+ StatusPanelRight.Update(entity);
+ }
- StatusPanel.Visible = true;
+ public void SetHighlightHand(HandUILocation? hand)
+ {
+ StatusPanelLeft.UpdateHighlight(hand is HandUILocation.Left);
+ StatusPanelRight.UpdateHighlight(hand is HandUILocation.Right);
+ }
+
+ public void UpdateStatusVisibility(bool left, bool right)
+ {
+ StatusPanelLeft.Visible = left;
+ StatusPanelRight.Visible = right;
}
}
diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml
index d469e6ced0..3b1257b44c 100644
--- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml
+++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml
@@ -3,22 +3,27 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
VerticalAlignment="Bottom"
- HorizontalAlignment="Center"
- MinSize="150 0">
-
+ HorizontalAlignment="Center">
+
+ PatchMarginBottom="4"
+ PatchMarginTop="6"
+ TextureScale="2 2"
+ Mode="Tile"/>
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs
index 90ae571711..95951fa1b0 100644
--- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs
@@ -1,70 +1,92 @@
using Content.Client.Items;
-using Content.Client.Resources;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory.VirtualItem;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
-using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
-using static Content.Client.IoC.StaticIoC;
namespace Content.Client.UserInterface.Systems.Inventory.Controls;
[GenerateTypedNameReferences]
-public sealed partial class ItemStatusPanel : BoxContainer
+public sealed partial class ItemStatusPanel : Control
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] private EntityUid? _entity;
+ // Tracked so we can re-run SetSide() if the theme changes.
+ private HandUILocation _side;
+
public ItemStatusPanel()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
-
- SetSide(HandLocation.Middle);
}
- public void SetSide(HandLocation location)
+ public void SetSide(HandUILocation location)
{
- string texture;
+ // AN IMPORTANT REMINDER ABOUT THIS CODE:
+ // In the UI, the RIGHT hand is on the LEFT on the screen.
+ // So that a character facing DOWN matches the hand positions.
+
+ Texture? texture;
+ Texture? textureHighlight;
StyleBox.Margin cutOut;
StyleBox.Margin flat;
- Label.AlignMode textAlign;
+ Thickness contentMargin;
switch (location)
{
- case HandLocation.Left:
- texture = "/Textures/Interface/Nano/item_status_right.svg.96dpi.png";
- cutOut = StyleBox.Margin.Left | StyleBox.Margin.Top;
- flat = StyleBox.Margin.Right | StyleBox.Margin.Bottom;
- textAlign = Label.AlignMode.Right;
- break;
- case HandLocation.Middle:
- texture = "/Textures/Interface/Nano/item_status_middle.svg.96dpi.png";
- cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top;
- flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom;
- textAlign = Label.AlignMode.Left;
+ case HandUILocation.Right:
+ texture = Theme.ResolveTexture("item_status_right");
+ textureHighlight = Theme.ResolveTexture("item_status_right_highlight");
+ cutOut = StyleBox.Margin.Left;
+ flat = StyleBox.Margin.Right;
+ contentMargin = MarginFromThemeColor("_itemstatus_content_margin_right");
break;
- case HandLocation.Right:
- texture = "/Textures/Interface/Nano/item_status_left.svg.96dpi.png";
- cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top;
- flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom;
- textAlign = Label.AlignMode.Left;
+ case HandUILocation.Left:
+ texture = Theme.ResolveTexture("item_status_left");
+ textureHighlight = Theme.ResolveTexture("item_status_left_highlight");
+ cutOut = StyleBox.Margin.Right;
+ flat = StyleBox.Margin.Left;
+ contentMargin = MarginFromThemeColor("_itemstatus_content_margin_left");
break;
default:
throw new ArgumentOutOfRangeException(nameof(location), location, null);
}
+ Contents.Margin = contentMargin;
+
var panel = (StyleBoxTexture) Panel.PanelOverride!;
- panel.Texture = ResC.GetTexture(texture);
- panel.SetPatchMargin(flat, 2);
- panel.SetPatchMargin(cutOut, 13);
+ panel.Texture = texture;
+ panel.SetPatchMargin(flat, 4);
+ panel.SetPatchMargin(cutOut, 7);
+
+ var panelHighlight = (StyleBoxTexture) HighlightPanel.PanelOverride!;
+ panelHighlight.Texture = textureHighlight;
+ panelHighlight.SetPatchMargin(flat, 4);
+ panelHighlight.SetPatchMargin(cutOut, 7);
+
+ _side = location;
+ }
- ItemNameLabel.Align = textAlign;
+ private Thickness MarginFromThemeColor(string itemName)
+ {
+ // This is the worst thing I've ever programmed
+ // (can you tell I'm a graphics programmer?)
+ // (the margin needs to change depending on the UI theme, so we use a fake color entry to store the value)
+
+ var color = Theme.ResolveColorOrSpecified(itemName);
+ return new Thickness(color.RByte, color.GByte, color.BByte, color.AByte);
+ }
+
+ protected override void OnThemeUpdated()
+ {
+ SetSide(_side);
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -75,11 +97,14 @@ protected override void FrameUpdate(FrameEventArgs args)
public void Update(EntityUid? entity)
{
+ ItemNameLabel.Visible = entity != null;
+ NoItemLabel.Visible = entity == null;
+
if (entity == null)
{
+ ItemNameLabel.Text = "";
ClearOldStatus();
_entity = null;
- Panel.Visible = false;
return;
}
@@ -90,8 +115,11 @@ public void Update(EntityUid? entity)
UpdateItemName();
}
+ }
- Panel.Visible = true;
+ public void UpdateHighlight(bool highlight)
+ {
+ HighlightPanel.Visible = highlight;
}
private void UpdateItemName()
diff --git a/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs b/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
index ad007be5fb..347a5023b9 100644
--- a/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
+++ b/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
@@ -134,10 +134,9 @@ private void UpdateInventoryHotbar(InventorySlotsComponent? clientInv)
_inventoryHotbar?.ClearButtons();
return;
}
-
foreach (var (_, data) in clientInv.SlotData)
{
- if (!data.ShowInWindow || !_slotGroups.TryGetValue(data.SlotGroup, out var container))
+ if (!data.ShowInWindow || data.SlotDef.Disabled || !_slotGroups.TryGetValue(data.SlotGroup, out var container))
continue;
if (!container.TryGetButton(data.SlotName, out var button))
@@ -210,7 +209,6 @@ private void UpdateStrippingWindow(InventorySlotsComponent? clientInv)
{
if (!data.ShowInWindow)
continue;
-
if (!_strippingWindow!.InventoryButtons.TryGetButton(data.SlotName, out var button))
{
button = CreateSlotButton(data);
@@ -417,7 +415,7 @@ private void SpriteUpdated(SlotSpriteUpdate update)
if (_strippingWindow?.InventoryButtons.GetButton(update.Name) is { } inventoryButton)
{
- inventoryButton.SpriteView.SetEntity(entity);
+ inventoryButton.SetEntity(entity);
inventoryButton.StorageButton.Visible = showStorage;
}
@@ -426,12 +424,12 @@ private void SpriteUpdated(SlotSpriteUpdate update)
if (_entities.TryGetComponent(entity, out VirtualItemComponent? virtb))
{
- button.SpriteView.SetEntity(virtb.BlockingEntity);
+ button.SetEntity(virtb.BlockingEntity);
button.Blocked = true;
}
else
{
- button.SpriteView.SetEntity(entity);
+ button.SetEntity(entity);
button.Blocked = false;
button.StorageButton.Visible = showStorage;
}
diff --git a/Content.Client/UserInterface/Systems/Language/LanguageMenuUIController.cs b/Content.Client/UserInterface/Systems/Language/LanguageMenuUIController.cs
index f36521ce81..e351a16bfb 100644
--- a/Content.Client/UserInterface/Systems/Language/LanguageMenuUIController.cs
+++ b/Content.Client/UserInterface/Systems/Language/LanguageMenuUIController.cs
@@ -2,7 +2,6 @@
using Content.Client.Gameplay;
using Content.Client.UserInterface.Controls;
using Content.Shared.Input;
-using Content.Shared.Language.Events;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
@@ -18,12 +17,6 @@ public sealed class LanguageMenuUIController : UIController, IOnStateEntered UIManager.GetActiveUIWidgetOrNull()?.LanguageButton;
- public override void Initialize()
- {
- SubscribeNetworkEvent((LanguagesUpdatedMessage message, EntitySessionEventArgs _) =>
- LanguageWindow?.UpdateState(message.CurrentLanguage, message.Spoken));
- }
-
public void OnStateEntered(GameplayState state)
{
DebugTools.Assert(LanguageWindow == null);
@@ -31,6 +24,17 @@ public void OnStateEntered(GameplayState state)
LanguageWindow = UIManager.CreateWindow();
LayoutContainer.SetAnchorPreset(LanguageWindow, LayoutContainer.LayoutPreset.CenterTop);
+ LanguageWindow.OnClose += () =>
+ {
+ if (LanguageButton != null)
+ LanguageButton.Pressed = false;
+ };
+ LanguageWindow.OnOpen += () =>
+ {
+ if (LanguageButton != null)
+ LanguageButton.Pressed = true;
+ };
+
CommandBinds.Builder.Bind(ContentKeyFunctions.OpenLanguageMenu,
InputCmdHandler.FromDelegate(_ => ToggleWindow())).Register();
}
@@ -60,12 +64,6 @@ public void LoadButton()
return;
LanguageButton.OnPressed += LanguageButtonPressed;
-
- if (LanguageWindow == null)
- return;
-
- LanguageWindow.OnClose += () => LanguageButton.Pressed = false;
- LanguageWindow.OnOpen += () => LanguageButton.Pressed = true;
}
private void LanguageButtonPressed(ButtonEventArgs args)
diff --git a/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs b/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs
index 156fa63884..6f9545288f 100644
--- a/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs
+++ b/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs
@@ -3,6 +3,7 @@
using Content.Client.UserInterface.Systems.Bwoink;
using Content.Client.UserInterface.Systems.Character;
using Content.Client.UserInterface.Systems.Crafting;
+using Content.Client.UserInterface.Systems.Emotes;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Client.UserInterface.Systems.Guidebook;
@@ -23,6 +24,7 @@ public sealed class GameTopMenuBarUIController : UIController
[Dependency] private readonly ActionUIController _action = default!;
[Dependency] private readonly SandboxUIController _sandbox = default!;
[Dependency] private readonly GuidebookUIController _guidebook = default!;
+ [Dependency] private readonly EmotesUIController _emotes = default!;
[Dependency] private readonly LanguageMenuUIController _language = default!;
private GameTopMenuBar? GameTopMenuBar => UIManager.GetActiveUIWidgetOrNull();
@@ -46,6 +48,7 @@ public void UnloadButtons()
_ahelp.UnloadButton();
_action.UnloadButton();
_sandbox.UnloadButton();
+ _emotes.UnloadButton();
_language.UnloadButton();
}
@@ -59,6 +62,7 @@ public void LoadButtons()
_ahelp.LoadButton();
_action.LoadButton();
_sandbox.LoadButton();
+ _emotes.LoadButton();
_language.LoadButton();
}
}
diff --git a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
index a76943ace8..c7cf1b3f3d 100644
--- a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
+++ b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
@@ -43,6 +43,16 @@
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
+
, IOnSystemChanged
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IEntityNetworkManager _net = default!;
+ private SpriteSystem _spriteSystem = default!;
+ private TargetingComponent? _targetingComponent;
+ private PartStatusControl? PartStatusControl => UIManager.GetActiveUIWidgetOrNull();
+
+ public void OnSystemLoaded(TargetingSystem system)
+ {
+ system.PartStatusStartup += AddPartStatusControl;
+ system.PartStatusShutdown += RemovePartStatusControl;
+ system.PartStatusUpdate += UpdatePartStatusControl;
+ }
+
+ public void OnSystemUnloaded(TargetingSystem system)
+ {
+ system.PartStatusStartup -= AddPartStatusControl;
+ system.PartStatusShutdown -= RemovePartStatusControl;
+ system.PartStatusUpdate -= UpdatePartStatusControl;
+ }
+
+ public void OnStateEntered(GameplayState state)
+ {
+ if (PartStatusControl != null)
+ {
+ PartStatusControl.SetVisible(_targetingComponent != null);
+
+ if (_targetingComponent != null)
+ PartStatusControl.SetTextures(_targetingComponent.BodyStatus);
+ }
+ }
+
+ public void AddPartStatusControl(TargetingComponent component)
+ {
+ _targetingComponent = component;
+
+ if (PartStatusControl != null)
+ {
+ PartStatusControl.SetVisible(_targetingComponent != null);
+
+ if (_targetingComponent != null)
+ PartStatusControl.SetTextures(_targetingComponent.BodyStatus);
+ }
+
+ }
+
+ public void RemovePartStatusControl()
+ {
+ if (PartStatusControl != null)
+ PartStatusControl.SetVisible(false);
+
+ _targetingComponent = null;
+ }
+
+ public void UpdatePartStatusControl(TargetingComponent component)
+ {
+ if (PartStatusControl != null && _targetingComponent != null)
+ PartStatusControl.SetTextures(_targetingComponent.BodyStatus);
+ }
+
+ public Texture GetTexture(SpriteSpecifier specifier)
+ {
+ if (_spriteSystem == null)
+ _spriteSystem = _entManager.System();
+
+ return _spriteSystem.Frame0(specifier);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml b/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml
new file mode 100644
index 0000000000..f37d7e1cbf
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml.cs b/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml.cs
new file mode 100644
index 0000000000..9c13e50ed1
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml.cs
@@ -0,0 +1,50 @@
+using Content.Client.UserInterface.Systems.PartStatus;
+using Content.Shared.Targeting;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+using System.Linq;
+
+namespace Content.Client.UserInterface.Systems.PartStatus.Widgets;
+
+[GenerateTypedNameReferences]
+public sealed partial class PartStatusControl : UIWidget
+{
+ private readonly Dictionary _partStatusControls;
+ private readonly PartStatusUIController _controller;
+ public PartStatusControl()
+ {
+ RobustXamlLoader.Load(this);
+
+ _controller = UserInterfaceManager.GetUIController();
+ _partStatusControls = new Dictionary
+ {
+ { TargetBodyPart.Head, DollHead },
+ { TargetBodyPart.Torso, DollTorso },
+ { TargetBodyPart.Groin, DollGroin },
+ { TargetBodyPart.LeftArm, DollLeftArm },
+ { TargetBodyPart.LeftHand, DollLeftHand },
+ { TargetBodyPart.RightArm, DollRightArm },
+ { TargetBodyPart.RightHand, DollRightHand },
+ { TargetBodyPart.LeftLeg, DollLeftLeg },
+ { TargetBodyPart.LeftFoot, DollLeftFoot },
+ { TargetBodyPart.RightLeg, DollRightLeg },
+ { TargetBodyPart.RightFoot, DollRightFoot }
+ };
+ }
+
+ public void SetTextures(Dictionary state)
+ {
+ foreach (var (bodyPart, integrity) in state)
+ {
+ string enumName = Enum.GetName(typeof(TargetBodyPart), bodyPart) ?? "Unknown";
+ int enumValue = (int) integrity;
+ var texture = new SpriteSpecifier.Rsi(new ResPath($"/Textures/Interface/Targeting/Status/{enumName.ToLowerInvariant()}.rsi"), $"{enumName.ToLowerInvariant()}_{enumValue}");
+ _partStatusControls[bodyPart].Texture = _controller.GetTexture(texture);
+ }
+ }
+
+ public void SetVisible(bool visible) => this.Visible = visible;
+
+}
diff --git a/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs b/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs
index e39ac5d322..a9d7e09826 100644
--- a/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs
+++ b/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs
@@ -254,7 +254,7 @@ public void BuildItemPieces()
//todo. at some point, we may want to only rebuild the pieces that have actually received new data.
- _pieceGrid.Children.Clear();
+ _pieceGrid.RemoveAllChildren();
_pieceGrid.Rows = boundingGrid.Height + 1;
_pieceGrid.Columns = boundingGrid.Width + 1;
for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++)
@@ -275,18 +275,29 @@ public void BuildItemPieces()
if (_entity.TryGetComponent(itemEnt, out var itemEntComponent))
{
- var gridPiece = new ItemGridPiece((itemEnt, itemEntComponent), itemPos, _entity)
+ ItemGridPiece gridPiece;
+
+ if (_storageController.CurrentlyDragging?.Entity is { } dragging
+ && dragging == itemEnt)
+ {
+ _storageController.CurrentlyDragging.Orphan();
+ gridPiece = _storageController.CurrentlyDragging;
+ }
+ else
{
- MinSize = size,
- Marked = Array.IndexOf(containedEntities, itemEnt) switch
+ gridPiece = new ItemGridPiece((itemEnt, itemEntComponent), itemPos, _entity)
{
- 0 => ItemGridPieceMarks.First,
- 1 => ItemGridPieceMarks.Second,
- _ => null,
- }
- };
- gridPiece.OnPiecePressed += OnPiecePressed;
- gridPiece.OnPieceUnpressed += OnPieceUnpressed;
+ MinSize = size,
+ Marked = Array.IndexOf(containedEntities, itemEnt) switch
+ {
+ 0 => ItemGridPieceMarks.First,
+ 1 => ItemGridPieceMarks.Second,
+ _ => null,
+ }
+ };
+ gridPiece.OnPiecePressed += OnPiecePressed;
+ gridPiece.OnPieceUnpressed += OnPieceUnpressed;
+ }
control.AddChild(gridPiece);
}
diff --git a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
index b865b54dd0..97c9d8b795 100644
--- a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
+++ b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
@@ -262,7 +262,7 @@ private void OnPiecePressed(GUIBoundKeyEventArgs args, ItemGridPiece control)
}
else if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
{
- _entity.EntityNetManager?.SendSystemNetworkMessage(
+ _entity.RaisePredictiveEvent(
new InteractInventorySlotEvent(_entity.GetNetEntity(control.Entity), altInteract: false));
args.Handle();
}
@@ -314,15 +314,16 @@ private void OnPieceUnpressed(GUIBoundKeyEventArgs args, ItemGridPiece control)
_entity.GetNetEntity(storageEnt)));
}
+ _menuDragHelper.EndDrag();
_container?.BuildItemPieces();
}
else //if we just clicked, then take it out of the bag.
{
+ _menuDragHelper.EndDrag();
_entity.RaisePredictiveEvent(new StorageInteractWithItemEvent(
_entity.GetNetEntity(control.Entity),
_entity.GetNetEntity(storageEnt)));
}
- _menuDragHelper.EndDrag();
args.Handle();
}
diff --git a/Content.Client/UserInterface/Systems/Targeting/TargetingUIController.cs b/Content.Client/UserInterface/Systems/Targeting/TargetingUIController.cs
new file mode 100644
index 0000000000..d430adfd9c
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/Targeting/TargetingUIController.cs
@@ -0,0 +1,82 @@
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Systems.Targeting.Widgets;
+using Content.Shared.Targeting;
+using Content.Client.Targeting;
+using Content.Shared.Targeting.Events;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Client.Player;
+
+namespace Content.Client.UserInterface.Systems.Targeting;
+
+public sealed class TargetingUIController : UIController, IOnStateEntered, IOnSystemChanged
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IEntityNetworkManager _net = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+
+ private TargetingComponent? _targetingComponent;
+ private TargetingControl? TargetingControl => UIManager.GetActiveUIWidgetOrNull();
+
+ public void OnSystemLoaded(TargetingSystem system)
+ {
+ system.TargetingStartup += AddTargetingControl;
+ system.TargetingShutdown += RemoveTargetingControl;
+ system.TargetChange += CycleTarget;
+ }
+
+ public void OnSystemUnloaded(TargetingSystem system)
+ {
+ system.TargetingStartup -= AddTargetingControl;
+ system.TargetingShutdown -= RemoveTargetingControl;
+ system.TargetChange -= CycleTarget;
+ }
+
+ public void OnStateEntered(GameplayState state)
+ {
+ if (TargetingControl == null)
+ return;
+
+ TargetingControl.SetTargetDollVisible(_targetingComponent != null);
+
+ if (_targetingComponent != null)
+ TargetingControl.SetBodyPartsVisible(_targetingComponent.Target);
+ }
+
+ public void AddTargetingControl(TargetingComponent component)
+ {
+ _targetingComponent = component;
+
+ if (TargetingControl != null)
+ {
+ TargetingControl.SetTargetDollVisible(_targetingComponent != null);
+
+ if (_targetingComponent != null)
+ TargetingControl.SetBodyPartsVisible(_targetingComponent.Target);
+ }
+
+ }
+
+ public void RemoveTargetingControl()
+ {
+ if (TargetingControl != null)
+ TargetingControl.SetTargetDollVisible(false);
+
+ _targetingComponent = null;
+ }
+
+ public void CycleTarget(TargetBodyPart bodyPart)
+ {
+ if (_playerManager.LocalEntity is not { } user
+ || _entManager.GetComponent(user) is not { } targetingComponent
+ || TargetingControl == null)
+ return;
+
+ var player = _entManager.GetNetEntity(user);
+ if (bodyPart != targetingComponent.Target)
+ {
+ var msg = new TargetChangeEvent(player, bodyPart);
+ _net.SendSystemNetworkMessage(msg);
+ TargetingControl?.SetBodyPartsVisible(bodyPart);
+ }
+ }
+}
diff --git a/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml b/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml
new file mode 100644
index 0000000000..1489628d93
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml.cs b/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml.cs
new file mode 100644
index 0000000000..07af0e8092
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml.cs
@@ -0,0 +1,58 @@
+using System.Linq;
+using Content.Shared.Targeting;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.UserInterface.Systems.Targeting.Widgets;
+
+[GenerateTypedNameReferences]
+public sealed partial class TargetingControl : UIWidget
+{
+ private readonly TargetingUIController _controller;
+ private readonly Dictionary _bodyPartControls;
+
+ public TargetingControl()
+ {
+ RobustXamlLoader.Load(this);
+ _controller = UserInterfaceManager.GetUIController();
+
+ _bodyPartControls = new Dictionary
+ {
+ // TODO: ADD EYE AND MOUTH TARGETING
+ { TargetBodyPart.Head, HeadButton },
+ { TargetBodyPart.Torso, ChestButton },
+ { TargetBodyPart.Groin, GroinButton },
+ { TargetBodyPart.LeftArm, LeftArmButton },
+ { TargetBodyPart.LeftHand, LeftHandButton },
+ { TargetBodyPart.RightArm, RightArmButton },
+ { TargetBodyPart.RightHand, RightHandButton },
+ { TargetBodyPart.LeftLeg, LeftLegButton },
+ { TargetBodyPart.LeftFoot, LeftFootButton },
+ { TargetBodyPart.RightLeg, RightLegButton },
+ { TargetBodyPart.RightFoot, RightFootButton },
+ };
+
+ foreach (var bodyPartButton in _bodyPartControls)
+ {
+ bodyPartButton.Value.MouseFilter = MouseFilterMode.Stop;
+ bodyPartButton.Value.OnPressed += _ => SetActiveBodyPart(bodyPartButton.Key);
+
+ TargetDoll.Texture = Theme.ResolveTexture("target_doll");
+ }
+ }
+
+ private void SetActiveBodyPart(TargetBodyPart bodyPart) => _controller.CycleTarget(bodyPart);
+
+ public void SetBodyPartsVisible(TargetBodyPart bodyPart)
+ {
+ foreach (var bodyPartButton in _bodyPartControls)
+ bodyPartButton.Value.Children.First().Visible = bodyPartButton.Key == bodyPart;
+ }
+
+ protected override void OnThemeUpdated() => TargetDoll.Texture = Theme.ResolveTexture("target_doll");
+
+ public void SetTargetDollVisible(bool visible) => Visible = visible;
+
+}
diff --git a/Content.Client/UserInterface/Systems/Viewport/ViewportUIController.cs b/Content.Client/UserInterface/Systems/Viewport/ViewportUIController.cs
index d117043f42..59747b1b0f 100644
--- a/Content.Client/UserInterface/Systems/Viewport/ViewportUIController.cs
+++ b/Content.Client/UserInterface/Systems/Viewport/ViewportUIController.cs
@@ -25,6 +25,7 @@ public override void Initialize()
_configurationManager.OnValueChanged(CCVars.ViewportMinimumWidth, _ => UpdateViewportRatio());
_configurationManager.OnValueChanged(CCVars.ViewportMaximumWidth, _ => UpdateViewportRatio());
_configurationManager.OnValueChanged(CCVars.ViewportWidth, _ => UpdateViewportRatio());
+ _configurationManager.OnValueChanged(CCVars.ViewportVerticalFit, _ => UpdateViewportRatio());
var gameplayStateLoad = UIManager.GetUIController();
gameplayStateLoad.OnScreenLoad += OnScreenLoad;
@@ -45,13 +46,19 @@ private void UpdateViewportRatio()
var min = _configurationManager.GetCVar(CCVars.ViewportMinimumWidth);
var max = _configurationManager.GetCVar(CCVars.ViewportMaximumWidth);
var width = _configurationManager.GetCVar(CCVars.ViewportWidth);
+ var verticalfit = _configurationManager.GetCVar(CCVars.ViewportVerticalFit) && _configurationManager.GetCVar(CCVars.ViewportStretch);
- if (width < min || width > max)
+ if (verticalfit)
+ {
+ width = max;
+ }
+ else if (width < min || width > max)
{
width = CCVars.ViewportWidth.DefaultValue;
}
Viewport.Viewport.ViewportSize = (EyeManager.PixelsPerMeter * width, EyeManager.PixelsPerMeter * ViewportHeight);
+ Viewport.UpdateCfg();
}
public void ReloadViewport()
diff --git a/Content.Client/Verbs/UI/VerbMenuUIController.cs b/Content.Client/Verbs/UI/VerbMenuUIController.cs
index 21eb6becd6..c3fc8c8356 100644
--- a/Content.Client/Verbs/UI/VerbMenuUIController.cs
+++ b/Content.Client/Verbs/UI/VerbMenuUIController.cs
@@ -31,6 +31,7 @@ public sealed class VerbMenuUIController : UIController, IOnStateEntered CurrentVerbs = new();
+ public List ExtraCategories = new();
///
/// Separate from , since we can open a verb menu as a submenu
@@ -91,19 +92,12 @@ public void OpenVerbMenu(NetEntity target, bool force = false, ContextMenuPopup?
menu.MenuBody.DisposeAllChildren();
CurrentTarget = target;
- CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, force);
+ CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, out ExtraCategories, force);
OpenMenu = menu;
// Fill in client-side verbs.
FillVerbPopup(menu);
- // Add indicator that some verbs may be missing.
- // I long for the day when verbs will all be predicted and this becomes unnecessary.
- if (!target.IsClientSide())
- {
- _context.AddElement(menu, new ContextMenuElement(Loc.GetString("verb-system-waiting-on-server-text")));
- }
-
// if popup isn't null (ie we are opening out of an entity menu element),
// assume that that is going to handle opening the submenu properly
if (popup != null)
@@ -128,11 +122,19 @@ private void FillVerbPopup(ContextMenuPopup popup)
var element = new VerbMenuElement(verb);
_context.AddElement(popup, element);
}
-
else if (listedCategories.Add(verb.Category.Text))
AddVerbCategory(verb.Category, popup);
}
+ if (ExtraCategories != null)
+ {
+ foreach (var category in ExtraCategories)
+ {
+ if (listedCategories.Add(category.Text))
+ AddVerbCategory(category, popup);
+ }
+ }
+
popup.InvalidateMeasure();
}
@@ -153,10 +155,11 @@ public void AddVerbCategory(VerbCategory category, ContextMenuPopup popup)
}
}
- if (verbsInCategory.Count == 0)
+ if (verbsInCategory.Count == 0 && !ExtraCategories.Contains(category))
return;
- var element = new VerbMenuElement(category, verbsInCategory[0].TextStyleClass);
+ var style = verbsInCategory.FirstOrDefault()?.TextStyleClass ?? Verb.DefaultTextStyleClass;
+ var element = new VerbMenuElement(category, style);
_context.AddElement(popup, element);
// Create the pop-up that appears when hovering over this element
diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs
index 77f46a3fc9..49a3785eb2 100644
--- a/Content.Client/Verbs/VerbSystem.cs
+++ b/Content.Client/Verbs/VerbSystem.cs
@@ -165,29 +165,23 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true
return true;
}
- ///
- /// Asks the server to send back a list of server-side verbs, for the given verb type.
- ///
- public SortedSet GetVerbs(EntityUid target, EntityUid user, Type type, bool force = false)
- {
- return GetVerbs(GetNetEntity(target), user, new List() { type }, force);
- }
-
///
/// Ask the server to send back a list of server-side verbs, and for now return an incomplete list of verbs
/// (only those defined locally).
///
- public SortedSet GetVerbs(NetEntity target, EntityUid user, List verbTypes,
- bool force = false)
+ public SortedSet GetVerbs(NetEntity target, EntityUid user, List verbTypes, out List extraCategories, bool force = false)
{
if (!target.IsClientSide())
RaiseNetworkEvent(new RequestServerVerbsEvent(target, verbTypes, adminRequest: force));
// Some admin menu interactions will try get verbs for entities that have not yet been sent to the player.
if (!TryGetEntity(target, out var local))
+ {
+ extraCategories = new();
return new();
+ }
- return GetLocalVerbs(local.Value, user, verbTypes, force);
+ return GetLocalVerbs(local.Value, user, verbTypes, out extraCategories, force);
}
diff --git a/Content.Client/Viewport/ScalingViewport.cs b/Content.Client/Viewport/ScalingViewport.cs
index 9271e010f3..ccd9636d77 100644
--- a/Content.Client/Viewport/ScalingViewport.cs
+++ b/Content.Client/Viewport/ScalingViewport.cs
@@ -32,6 +32,7 @@ public sealed class ScalingViewport : Control, IViewportControl
private int _curRenderScale;
private ScalingViewportStretchMode _stretchMode = ScalingViewportStretchMode.Bilinear;
private ScalingViewportRenderScaleMode _renderScaleMode = ScalingViewportRenderScaleMode.Fixed;
+ private ScalingViewportIgnoreDimension _ignoreDimension = ScalingViewportIgnoreDimension.None;
private int _fixedRenderScale = 1;
private readonly List> _queuedScreenshots = new();
@@ -106,6 +107,17 @@ public int FixedRenderScale
}
}
+ [ViewVariables(VVAccess.ReadWrite)]
+ public ScalingViewportIgnoreDimension IgnoreDimension
+ {
+ get => _ignoreDimension;
+ set
+ {
+ _ignoreDimension = value;
+ InvalidateViewport();
+ }
+ }
+
public ScalingViewport()
{
IoCManager.InjectDependencies(this);
@@ -178,7 +190,19 @@ private UIBox2i GetDrawBox()
if (FixedStretchSize == null)
{
var (ratioX, ratioY) = ourSize / vpSize;
- var ratio = Math.Min(ratioX, ratioY);
+ var ratio = 1f;
+ switch (_ignoreDimension)
+ {
+ case ScalingViewportIgnoreDimension.None:
+ ratio = Math.Min(ratioX, ratioY);
+ break;
+ case ScalingViewportIgnoreDimension.Vertical:
+ ratio = ratioX;
+ break;
+ case ScalingViewportIgnoreDimension.Horizontal:
+ ratio = ratioY;
+ break;
+ }
var size = vpSize * ratio;
// Size
@@ -253,8 +277,8 @@ public MapCoordinates ScreenToMap(Vector2 coords)
EnsureViewportCreated();
- var matrix = Matrix3.Invert(GetLocalToScreenMatrix());
- coords = matrix.Transform(coords);
+ Matrix3x2.Invert(GetLocalToScreenMatrix(), out var matrix);
+ coords = Vector2.Transform(coords, matrix);
return _viewport!.LocalToWorld(coords);
}
@@ -267,8 +291,8 @@ public MapCoordinates PixelToMap(Vector2 coords)
EnsureViewportCreated();
- var matrix = Matrix3.Invert(GetLocalToScreenMatrix());
- coords = matrix.Transform(coords);
+ Matrix3x2.Invert(GetLocalToScreenMatrix(), out var matrix);
+ coords = Vector2.Transform(coords, matrix);
var ev = new PixelToMapEvent(coords, this, _viewport!);
_entityManager.EventBus.RaiseEvent(EventSource.Local, ref ev);
@@ -287,16 +311,16 @@ public Vector2 WorldToScreen(Vector2 map)
var matrix = GetLocalToScreenMatrix();
- return matrix.Transform(vpLocal);
+ return Vector2.Transform(vpLocal, matrix);
}
- public Matrix3 GetWorldToScreenMatrix()
+ public Matrix3x2 GetWorldToScreenMatrix()
{
EnsureViewportCreated();
return _viewport!.GetWorldToLocalMatrix() * GetLocalToScreenMatrix();
}
- public Matrix3 GetLocalToScreenMatrix()
+ public Matrix3x2 GetLocalToScreenMatrix()
{
EnsureViewportCreated();
@@ -305,9 +329,9 @@ public Matrix3 GetLocalToScreenMatrix()
if (scaleFactor.X == 0 || scaleFactor.Y == 0)
// Basically a nonsense scenario, at least make sure to return something that can be inverted.
- return Matrix3.Identity;
+ return Matrix3x2.Identity;
- return Matrix3.CreateTransform(GlobalPixelPosition + drawBox.TopLeft, 0, scaleFactor);
+ return Matrix3Helpers.CreateTransform(GlobalPixelPosition + drawBox.TopLeft, 0, scaleFactor);
}
private void EnsureViewportCreated()
@@ -357,4 +381,25 @@ public enum ScalingViewportRenderScaleMode
///
CeilInt
}
+
+ ///
+ /// If the viewport is allowed to freely scale, this determines which dimensions should be ignored while fitting the viewport
+ ///
+ public enum ScalingViewportIgnoreDimension
+ {
+ ///
+ /// The viewport won't ignore any dimension.
+ ///
+ None = 0,
+
+ ///
+ /// The viewport will ignore the horizontal dimension, and will exclusively consider the vertical dimension for scaling.
+ ///
+ Horizontal,
+
+ ///
+ /// The viewport will ignore the vertical dimension, and will exclusively consider the horizontal dimension for scaling.
+ ///
+ Vertical
+ }
}
diff --git a/Content.Client/Voting/VoteManager.cs b/Content.Client/Voting/VoteManager.cs
index 63c706c86b..8ade25056d 100644
--- a/Content.Client/Voting/VoteManager.cs
+++ b/Content.Client/Voting/VoteManager.cs
@@ -6,12 +6,15 @@
using Robust.Client.Audio;
using Robust.Client.Console;
using Robust.Client.GameObjects;
+using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Player;
using Robust.Shared.Audio;
+using Robust.Shared.Audio.Sources;
+using Robust.Shared.ContentPack;
namespace Content.Client.Voting
@@ -31,16 +34,20 @@ public interface IVoteManager
public sealed class VoteManager : IVoteManager
{
+ [Dependency] private readonly IAudioManager _audio = default!;
+ [Dependency] private readonly IBaseClient _client = default!;
+ [Dependency] private readonly IClientConsoleHost _console = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly IClientConsoleHost _console = default!;
- [Dependency] private readonly IBaseClient _client = default!;
+ [Dependency] private readonly IResourceCache _res = default!;
private readonly Dictionary _standardVoteTimeouts = new();
private readonly Dictionary _votes = new();
private readonly Dictionary _votePopups = new();
private Control? _popupContainer;
+ private IAudioSource? _voteSource;
+
public bool CanCallVote { get; private set; }
public event Action? CanCallVoteChanged;
@@ -49,6 +56,12 @@ public sealed class VoteManager : IVoteManager
public void Initialize()
{
+ const string sound = "/Audio/Effects/voteding.ogg";
+ _voteSource = _audio.CreateAudioSource(_res.GetResource(sound));
+
+ if (_voteSource != null)
+ _voteSource.Global = true;
+
_netManager.RegisterNetMessage(ReceiveVoteData);
_netManager.RegisterNetMessage(ReceiveVoteCanCall);
@@ -125,9 +138,8 @@ private void ReceiveVoteData(MsgVoteData message)
return;
}
+ _voteSource?.Restart();
@new = true;
- IoCManager.Resolve().GetEntitySystem()
- .PlayGlobal("/Audio/Effects/voteding.ogg", Filter.Local(), false);
// Refresh
var container = _popupContainer;
diff --git a/Content.Client/Weapons/Melee/Components/WeaponArcVisualsComponent.cs b/Content.Client/Weapons/Melee/Components/WeaponArcVisualsComponent.cs
index d0f95f7069..6d3d4d40d4 100644
--- a/Content.Client/Weapons/Melee/Components/WeaponArcVisualsComponent.cs
+++ b/Content.Client/Weapons/Melee/Components/WeaponArcVisualsComponent.cs
@@ -6,6 +6,8 @@ namespace Content.Client.Weapons.Melee.Components;
[RegisterComponent]
public sealed partial class WeaponArcVisualsComponent : Component
{
+ public EntityUid? User;
+
[DataField("animation")]
public WeaponArcAnimation Animation = WeaponArcAnimation.None;
diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
index 0f2f98e764..3e1a4b1906 100644
--- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
+++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using Content.Client.Animations;
using Content.Client.Weapons.Melee.Components;
using Content.Shared.Weapons.Melee;
using Robust.Client.Animations;
@@ -55,32 +56,33 @@ public override void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vect
if (meleeWeaponComponent.SwingLeft)
angle *= -1;
}
- sprite.NoRotation = true;
sprite.Rotation = localPos.ToWorldAngle();
var distance = Math.Clamp(localPos.Length() / 2f, 0.2f, 1f);
var xform = _xformQuery.GetComponent(animationUid);
+ TrackUserComponent track;
switch (arcComponent.Animation)
{
case WeaponArcAnimation.Slash:
+ track = EnsureComp(animationUid);
+ track.User = user;
_animation.Play(animationUid, GetSlashAnimation(sprite, angle, spriteRotation), SlashAnimationKey);
- TransformSystem.SetParent(animationUid, xform, user, userXform);
if (arcComponent.Fadeout)
_animation.Play(animationUid, GetFadeAnimation(sprite, 0.065f, 0.065f + 0.05f), FadeAnimationKey);
break;
case WeaponArcAnimation.Thrust:
+ track = EnsureComp(animationUid);
+ track.User = user;
_animation.Play(animationUid, GetThrustAnimation(sprite, distance, spriteRotation), ThrustAnimationKey);
- TransformSystem.SetParent(animationUid, xform, user, userXform);
if (arcComponent.Fadeout)
_animation.Play(animationUid, GetFadeAnimation(sprite, 0.05f, 0.15f), FadeAnimationKey);
break;
case WeaponArcAnimation.None:
var (mapPos, mapRot) = TransformSystem.GetWorldPositionRotation(userXform);
- TransformSystem.AttachToGridOrMap(animationUid, xform);
var worldPos = mapPos + (mapRot - userXform.LocalRotation).RotateVec(localPos);
- var newLocalPos = TransformSystem.GetInvWorldMatrix(xform.ParentUid).Transform(worldPos);
- TransformSystem.SetLocalPositionNoLerp(xform, newLocalPos);
+ var newLocalPos = Vector2.Transform(worldPos, TransformSystem.GetInvWorldMatrix(xform.ParentUid));
+ TransformSystem.SetLocalPositionNoLerp(animationUid, newLocalPos, xform);
if (arcComponent.Fadeout)
_animation.Play(animationUid, GetFadeAnimation(sprite, 0f, 0.15f), FadeAnimationKey);
break;
@@ -98,7 +100,6 @@ private Animation GetSlashAnimation(SpriteComponent sprite, Angle arc, Angle spr
var endRotationOffset = endRotation.RotateVec(new Vector2(0f, -1f));
startRotation += spriteRotation;
endRotation += spriteRotation;
- sprite.NoRotation = true;
return new Animation()
{
@@ -205,4 +206,27 @@ private Animation GetLungeAnimation(Vector2 direction)
}
};
}
+
+ ///
+ /// Updates the effect positions to follow the user
+ ///
+ private void UpdateEffects()
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var arcComponent, out var xform))
+ {
+ if (arcComponent.User == null)
+ continue;
+
+ Vector2 targetPos = TransformSystem.GetWorldPosition(arcComponent.User.Value);
+
+ if (arcComponent.Offset != Vector2.Zero)
+ {
+ var entRotation = TransformSystem.GetWorldRotation(xform);
+ targetPos += entRotation.RotateVec(arcComponent.Offset);
+ }
+
+ TransformSystem.SetWorldPosition(xform, targetPos);
+ }
+ }
}
diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
index 98528c691d..1d72f16706 100644
--- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
+++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
@@ -42,6 +42,12 @@ public override void Initialize()
UpdatesOutsidePrediction = true;
}
+ public override void FrameUpdate(float frameTime)
+ {
+ base.FrameUpdate(frameTime);
+ UpdateEffects();
+ }
+
public override void Update(float frameTime)
{
base.Update(frameTime);
diff --git a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
new file mode 100644
index 0000000000..e6cb596b94
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
@@ -0,0 +1,252 @@
+using System.Numerics;
+using Content.Client.Resources;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Weapons.Ranged.ItemStatus;
+
+public abstract class BaseBulletRenderer : Control
+{
+ private int _capacity;
+ private LayoutParameters _params;
+
+ public int Rows { get; set; } = 2;
+ public int Count { get; set; }
+
+ public int Capacity
+ {
+ get => _capacity;
+ set
+ {
+ if (_capacity == value)
+ return;
+
+ _capacity = value;
+ InvalidateMeasure();
+ }
+ }
+
+ protected LayoutParameters Parameters
+ {
+ get => _params;
+ set
+ {
+ _params = value;
+ InvalidateMeasure();
+ }
+ }
+
+ protected override Vector2 MeasureOverride(Vector2 availableSize)
+ {
+ var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X));
+
+ var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows);
+
+ var height = _params.ItemHeight * rows + (_params.VerticalSeparation * rows - 1);
+ var width = RowWidth(countPerRow);
+
+ return new Vector2(width, height);
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ // Scale rendering in this control by UIScale.
+ var currentTransform = handle.GetTransform();
+ handle.SetTransform(Matrix3Helpers.CreateScale(new Vector2(UIScale)) * currentTransform);
+
+ var countPerRow = CountPerRow(Size.X);
+
+ var pos = new Vector2();
+
+ var spent = Capacity - Count;
+
+ var bulletsDone = 0;
+
+ // Draw by rows, bottom to top.
+ for (var row = 0; row < Rows; row++)
+ {
+ var altColor = false;
+
+ var thisRowCount = Math.Min(countPerRow, Capacity - bulletsDone);
+ if (thisRowCount <= 0)
+ break;
+
+ // Handle MinCountPerRow
+ // We only do this if:
+ // 1. The next row would have less than MinCountPerRow bullets.
+ // 2. The next row is actually visible (we aren't the last row).
+ // 3. MinCountPerRow is actually smaller than the count per row (avoid degenerate cases).
+ // 4. There's enough bullets that at least one will end up on the next row.
+ var nextRowCount = Capacity - bulletsDone - thisRowCount;
+ if (nextRowCount < _params.MinCountPerRow && row != Rows - 1 && _params.MinCountPerRow < countPerRow && nextRowCount > 0)
+ thisRowCount -= _params.MinCountPerRow - nextRowCount;
+
+ // Account for row width to right-align.
+ var rowWidth = RowWidth(thisRowCount);
+ pos.X += Size.X - rowWidth;
+
+ // Draw row left to right (so overlapping works)
+ for (var bullet = 0; bullet < thisRowCount; bullet++)
+ {
+ var absIdx = Capacity - bulletsDone - thisRowCount + bullet;
+
+ var renderPos = pos;
+ renderPos.Y = Size.Y - renderPos.Y - _params.ItemHeight;
+
+ DrawItem(handle, renderPos, absIdx < spent, altColor);
+
+ pos.X += _params.ItemSeparation;
+ altColor ^= true;
+ }
+
+ bulletsDone += thisRowCount;
+ pos.X = 0;
+ pos.Y += _params.ItemHeight + _params.VerticalSeparation;
+ }
+ }
+
+ protected abstract void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor);
+
+ private int CountPerRow(float width)
+ {
+ return (int) ((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
+ }
+
+ private int RowWidth(int count)
+ {
+ return (count - 1) * _params.ItemSeparation + _params.ItemWidth;
+ }
+
+ protected struct LayoutParameters
+ {
+ public int ItemHeight;
+ public int ItemSeparation;
+ public int ItemWidth;
+ public int VerticalSeparation;
+
+ ///
+ /// Try to ensure there's at least this many bullets on one row.
+ ///
+ ///
+ /// For example, if there are two rows and the second row has only two bullets,
+ /// we "steal" some bullets from the row below it to make it look nicer.
+ ///
+ public int MinCountPerRow;
+ }
+}
+
+///
+/// Renders one or more rows of bullets for item status.
+///
+///
+/// This is a custom control to allow complex responsive layout logic.
+///
+public sealed class BulletRender : BaseBulletRenderer
+{
+ public const int MinCountPerRow = 7;
+
+ public const int BulletHeight = 12;
+ public const int VerticalSeparation = 2;
+
+ private static readonly LayoutParameters LayoutNormal = new LayoutParameters
+ {
+ ItemHeight = BulletHeight,
+ ItemSeparation = 3,
+ ItemWidth = 5,
+ VerticalSeparation = VerticalSeparation,
+ MinCountPerRow = MinCountPerRow
+ };
+
+ private static readonly LayoutParameters LayoutTiny = new LayoutParameters
+ {
+ ItemHeight = BulletHeight,
+ ItemSeparation = 2,
+ ItemWidth = 2,
+ VerticalSeparation = VerticalSeparation,
+ MinCountPerRow = MinCountPerRow
+ };
+
+ private static readonly Color ColorA = Color.FromHex("#b68f0e");
+ private static readonly Color ColorB = Color.FromHex("#d7df60");
+ private static readonly Color ColorGoneA = Color.FromHex("#000000");
+ private static readonly Color ColorGoneB = Color.FromHex("#222222");
+
+ private readonly Texture _bulletTiny;
+ private readonly Texture _bulletNormal;
+
+ private BulletType _type = BulletType.Normal;
+
+ public BulletType Type
+ {
+ get => _type;
+ set
+ {
+ if (_type == value)
+ return;
+
+ Parameters = _type switch
+ {
+ BulletType.Normal => LayoutNormal,
+ BulletType.Tiny => LayoutTiny,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ _type = value;
+ }
+ }
+
+ public BulletRender()
+ {
+ var resC = IoCManager.Resolve();
+ _bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png");
+ _bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png");
+ Parameters = LayoutNormal;
+ }
+
+ protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor)
+ {
+ Color color;
+ if (spent)
+ color = altColor ? ColorGoneA : ColorGoneB;
+ else
+ color = altColor ? ColorA : ColorB;
+
+ var texture = _type == BulletType.Tiny ? _bulletTiny : _bulletNormal;
+ handle.DrawTexture(texture, renderPos, color);
+ }
+
+ public enum BulletType
+ {
+ Normal,
+ Tiny
+ }
+}
+
+public sealed class BatteryBulletRenderer : BaseBulletRenderer
+{
+ private static readonly Color ItemColor = Color.FromHex("#E00000");
+ private static readonly Color ItemColorGone = Color.Black;
+
+ private const int SizeH = 10;
+ private const int SizeV = 10;
+ private const int Separation = 4;
+
+ public BatteryBulletRenderer()
+ {
+ Parameters = new LayoutParameters
+ {
+ ItemWidth = SizeH,
+ ItemHeight = SizeV,
+ ItemSeparation = SizeH + Separation,
+ MinCountPerRow = 3,
+ VerticalSeparation = Separation
+ };
+ }
+
+ protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor)
+ {
+ var color = spent ? ItemColorGone : ItemColor;
+ handle.DrawRect(UIBox2.FromDimensions(renderPos, new Vector2(SizeH, SizeV)), color);
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
index 32343af56f..84eaa9af1b 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
@@ -4,11 +4,11 @@
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Client.Weapons.Ranged.Components;
+using Content.Client.Weapons.Ranged.ItemStatus;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Graphics;
namespace Content.Client.Weapons.Ranged.Systems;
@@ -91,122 +91,32 @@ public sealed class UpdateAmmoCounterEvent : HandledEntityEventArgs
private sealed class DefaultStatusControl : Control
{
- private readonly BoxContainer _bulletsListTop;
- private readonly BoxContainer _bulletsListBottom;
+ private readonly BulletRender _bulletRender;
public DefaultStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
- VerticalAlignment = Control.VAlignment.Center;
- AddChild(new BoxContainer
+ VerticalAlignment = VAlignment.Center;
+ AddChild(_bulletRender = new BulletRender
{
- Orientation = BoxContainer.LayoutOrientation.Vertical,
- HorizontalExpand = true,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0,
- Children =
- {
- (_bulletsListTop = new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- SeparationOverride = 0
- }),
- new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- Children =
- {
- new Control
- {
- HorizontalExpand = true,
- Children =
- {
- (_bulletsListBottom = new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0
- }),
- }
- },
- }
- }
- }
+ HorizontalAlignment = HAlignment.Right,
+ VerticalAlignment = VAlignment.Bottom
});
}
public void Update(int count, int capacity)
{
- _bulletsListTop.RemoveAllChildren();
- _bulletsListBottom.RemoveAllChildren();
-
- string texturePath;
- if (capacity <= 20)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
- }
- else if (capacity <= 30)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
- }
- else
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
- }
-
- var texture = StaticIoC.ResC.GetTexture(texturePath);
-
- const int tinyMaxRow = 60;
+ _bulletRender.Count = count;
+ _bulletRender.Capacity = capacity;
- if (capacity > tinyMaxRow)
- {
- FillBulletRow(_bulletsListBottom, Math.Min(tinyMaxRow, count), tinyMaxRow, texture);
- FillBulletRow(_bulletsListTop, Math.Max(0, count - tinyMaxRow), capacity - tinyMaxRow, texture);
- }
- else
- {
- FillBulletRow(_bulletsListBottom, count, capacity, texture);
- }
- }
-
- private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
- {
- var colorA = Color.FromHex("#b68f0e");
- var colorB = Color.FromHex("#d7df60");
- var colorGoneA = Color.FromHex("#000000");
- var colorGoneB = Color.FromHex("#222222");
-
- var altColor = false;
-
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorGoneA : colorGoneB
- });
-
- altColor ^= true;
- }
-
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorA : colorB
- });
-
- altColor ^= true;
- }
+ _bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
}
}
public sealed class BoxesStatusControl : Control
{
- private readonly BoxContainer _bulletsList;
+ private readonly BatteryBulletRenderer _bullets;
private readonly Label _ammoCount;
public BoxesStatusControl()
@@ -218,27 +128,18 @@ public BoxesStatusControl()
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
- HorizontalExpand = true,
Children =
{
- new Control
+ (_bullets = new BatteryBulletRenderer
{
- HorizontalExpand = true,
- Children =
- {
- (_bulletsList = new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 4
- }),
- }
- },
- new Control() { MinSize = new Vector2(5, 0) },
+ Margin = new Thickness(0, 0, 5, 0),
+ HorizontalExpand = true
+ }),
(_ammoCount = new Label
{
StyleClasses = { StyleNano.StyleClassItemStatus },
HorizontalAlignment = HAlignment.Right,
+ VerticalAlignment = VAlignment.Bottom
}),
}
});
@@ -246,52 +147,18 @@ public BoxesStatusControl()
public void Update(int count, int max)
{
- _bulletsList.RemoveAllChildren();
-
_ammoCount.Visible = true;
_ammoCount.Text = $"x{count:00}";
- max = Math.Min(max, 8);
- FillBulletRow(_bulletsList, count, max);
- }
-
- private static void FillBulletRow(Control container, int count, int capacity)
- {
- var colorGone = Color.FromHex("#000000");
- var color = Color.FromHex("#E00000");
-
- // Draw the empty ones
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = colorGone,
- },
- MinSize = new Vector2(10, 15),
- });
- }
- // Draw the full ones, but limit the count to the capacity
- count = Math.Min(count, capacity);
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = color,
- },
- MinSize = new Vector2(10, 15),
- });
- }
+ _bullets.Capacity = max;
+ _bullets.Count = count;
}
}
private sealed class ChamberMagazineStatusControl : Control
{
- private readonly BoxContainer _bulletsList;
+ private readonly BulletRender _bulletRender;
private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel;
private readonly Label _ammoCount;
@@ -308,23 +175,16 @@ public ChamberMagazineStatusControl()
HorizontalExpand = true,
Children =
{
- (_chamberedBullet = new TextureRect
- {
- Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered_rotated.png"),
- VerticalAlignment = VAlignment.Center,
- HorizontalAlignment = HAlignment.Right,
- }),
- new Control() { MinSize = new Vector2(5,0) },
new Control
{
HorizontalExpand = true,
+ Margin = new Thickness(0, 0, 5, 0),
Children =
{
- (_bulletsList = new BoxContainer
+ (_bulletRender = new BulletRender
{
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0
+ HorizontalAlignment = HAlignment.Right,
+ VerticalAlignment = VAlignment.Bottom
}),
(_noMagazineLabel = new Label
{
@@ -333,12 +193,25 @@ public ChamberMagazineStatusControl()
})
}
},
- new Control() { MinSize = new Vector2(5,0) },
- (_ammoCount = new Label
+ new BoxContainer
{
- StyleClasses = {StyleNano.StyleClassItemStatus},
- HorizontalAlignment = HAlignment.Right,
- }),
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ VerticalAlignment = VAlignment.Bottom,
+ Margin = new Thickness(0, 0, 0, 2),
+ Children =
+ {
+ (_ammoCount = new Label
+ {
+ StyleClasses = {StyleNano.StyleClassItemStatus},
+ HorizontalAlignment = HAlignment.Right,
+ }),
+ (_chamberedBullet = new TextureRect
+ {
+ Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered.png"),
+ HorizontalAlignment = HAlignment.Left,
+ }),
+ }
+ }
}
});
}
@@ -348,61 +221,24 @@ public void Update(bool chambered, bool magazine, int count, int capacity)
_chamberedBullet.ModulateSelfOverride =
chambered ? Color.FromHex("#d7df60") : Color.Black;
- _bulletsList.RemoveAllChildren();
-
if (!magazine)
{
+ _bulletRender.Visible = false;
_noMagazineLabel.Visible = true;
_ammoCount.Visible = false;
return;
}
+ _bulletRender.Visible = true;
_noMagazineLabel.Visible = false;
_ammoCount.Visible = true;
- var texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
- var texture = StaticIoC.ResC.GetTexture(texturePath);
+ _bulletRender.Count = count;
+ _bulletRender.Capacity = capacity;
- _ammoCount.Text = $"x{count:00}";
- capacity = Math.Min(capacity, 20);
- FillBulletRow(_bulletsList, count, capacity, texture);
- }
-
- private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
- {
- var colorA = Color.FromHex("#b68f0e");
- var colorB = Color.FromHex("#d7df60");
- var colorGoneA = Color.FromHex("#000000");
- var colorGoneB = Color.FromHex("#222222");
+ _bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
- var altColor = false;
-
- // Draw the empty ones
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorGoneA : colorGoneB,
- Stretch = TextureRect.StretchMode.KeepCentered
- });
-
- altColor ^= true;
- }
-
- // Draw the full ones, but limit the count to the capacity
- count = Math.Min(count, capacity);
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorA : colorB,
- Stretch = TextureRect.StretchMode.KeepCentered
- });
-
- altColor ^= true;
- }
+ _ammoCount.Text = $"x{count:00}";
}
public void PlayAlarmAnimation(Animation animation)
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
index 57d200d96b..4a7711032e 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using Content.Client.Animations;
using Content.Client.Gameplay;
using Content.Client.Items;
using Content.Client.Weapons.Ranged.Components;
@@ -17,6 +18,7 @@
using Robust.Shared.Animations;
using Robust.Shared.Input;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
@@ -26,6 +28,7 @@ namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
+ [Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
@@ -33,7 +36,7 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
[Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
- [Dependency] private readonly IComponentFactory _factory = default!;
+ [Dependency] private readonly SharedMapSystem _maps = default!;
[ValidatePrototypeId]
public const string HitscanProto = "HitscanEffect";
@@ -126,7 +129,7 @@ private void OnHitscan(HitscanEvent ev)
}
};
- _animPlayer.Play(ent, null, anim, "hitscan-effect");
+ _animPlayer.Play(ent, anim, "hitscan-effect");
}
}
@@ -197,6 +200,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid?
// to just delete the spawned entities. This is for programmer sanity despite the wasted perf.
// This also means any ammo specific stuff can be grabbed as necessary.
var direction = fromCoordinates.ToMapPos(EntityManager, TransformSystem) - toCoordinates.ToMapPos(EntityManager, TransformSystem);
+ var worldAngle = direction.ToAngle().Opposite();
foreach (var (ent, shootable) in ammo)
{
@@ -216,7 +220,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid?
if (!cartridge.Spent)
{
SetCartridgeSpent(ent!.Value, cartridge, true);
- MuzzleFlash(gunUid, cartridge, user);
+ MuzzleFlash(gunUid, cartridge, worldAngle, user);
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
Recoil(user, direction, gun.CameraRecoilScalarModified);
// TODO: Can't predict entity deletions.
@@ -234,7 +238,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid?
break;
case AmmoComponent newAmmo:
- MuzzleFlash(gunUid, newAmmo, user);
+ MuzzleFlash(gunUid, newAmmo, worldAngle, user);
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
Recoil(user, direction, gun.CameraRecoilScalarModified);
if (IsClientSide(ent!.Value))
@@ -266,33 +270,41 @@ protected override void Popup(string message, EntityUid? uid, EntityUid? user)
PopupSystem.PopupEntity(message, uid.Value, user.Value);
}
- protected override void CreateEffect(EntityUid uid, MuzzleFlashEvent message, EntityUid? user = null)
+ protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null)
{
if (!Timing.IsFirstTimePredicted)
return;
+ var gunXform = Transform(gunUid);
+ var gridUid = gunXform.GridUid;
EntityCoordinates coordinates;
- if (message.MatchRotation)
- coordinates = new EntityCoordinates(uid, Vector2.Zero);
- else if (TryComp(uid, out var xform))
- coordinates = xform.Coordinates;
+ if (TryComp(gridUid, out MapGridComponent? mapGrid))
+ {
+ coordinates = new EntityCoordinates(gridUid.Value, _maps.LocalToGrid(gridUid.Value, mapGrid, gunXform.Coordinates));
+ }
+ else if (gunXform.MapUid != null)
+ {
+ coordinates = new EntityCoordinates(gunXform.MapUid.Value, TransformSystem.GetWorldPosition(gunXform));
+ }
else
+ {
return;
-
- if (!coordinates.IsValid(EntityManager))
- return;
+ }
var ent = Spawn(message.Prototype, coordinates);
+ TransformSystem.SetWorldRotationNoLerp(ent, message.Angle);
- var effectXform = Transform(ent);
- TransformSystem.SetLocalPositionRotation(effectXform,
- effectXform.LocalPosition + new Vector2(0f, -0.5f),
- effectXform.LocalRotation - MathF.PI / 2);
+ if (user != null)
+ {
+ var track = EnsureComp(ent);
+ track.User = user;
+ track.Offset = Vector2.UnitX / 2f;
+ }
var lifetime = 0.4f;
- if (TryComp(uid, out var despawn))
+ if (TryComp(gunUid, out var despawn))
{
lifetime = despawn.Lifetime;
}
@@ -317,18 +329,17 @@ protected override void CreateEffect(EntityUid uid, MuzzleFlashEvent message, En
};
_animPlayer.Play(ent, anim, "muzzle-flash");
- if (!TryComp(uid, out PointLightComponent? light))
+ if (!TryComp(gunUid, out PointLightComponent? light))
{
light = (PointLightComponent) _factory.GetComponent(typeof(PointLightComponent));
- light.Owner = uid;
light.NetSyncEnabled = false;
- AddComp(uid, light);
+ AddComp(gunUid, light);
}
- Lights.SetEnabled(uid, true, light);
- Lights.SetRadius(uid, 2f, light);
- Lights.SetColor(uid, Color.FromHex("#cc8e2b"), light);
- Lights.SetEnergy(uid, 5f, light);
+ Lights.SetEnabled(gunUid, true, light);
+ Lights.SetRadius(gunUid, 2f, light);
+ Lights.SetColor(gunUid, Color.FromHex("#cc8e2b"), light);
+ Lights.SetEnergy(gunUid, 5f, light);
var animTwo = new Animation()
{
@@ -360,9 +371,9 @@ protected override void CreateEffect(EntityUid uid, MuzzleFlashEvent message, En
}
};
- var uidPlayer = EnsureComp(uid);
+ var uidPlayer = EnsureComp(gunUid);
- _animPlayer.Stop(uid, uidPlayer, "muzzle-flash-light");
- _animPlayer.Play(uid, uidPlayer, animTwo,"muzzle-flash-light");
+ _animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light");
+ _animPlayer.Play((gunUid, uidPlayer), animTwo,"muzzle-flash-light");
}
}
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
index 143f01b6b7..2538caf6eb 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
@@ -39,6 +39,14 @@ protected override void Open()
{
SendMessage(new AnalysisConsoleExtractButtonPressedMessage());
};
+ _consoleMenu.OnUpBiasButtonPressed += () =>
+ {
+ SendMessage(new AnalysisConsoleBiasButtonPressedMessage(false));
+ };
+ _consoleMenu.OnDownBiasButtonPressed += () =>
+ {
+ SendMessage(new AnalysisConsoleBiasButtonPressedMessage(true));
+ };
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -47,7 +55,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
switch (state)
{
- case AnalysisConsoleScanUpdateState msg:
+ case AnalysisConsoleUpdateState msg:
_consoleMenu?.SetButtonsDisabled(msg);
_consoleMenu?.UpdateInformationDisplay(msg);
_consoleMenu?.UpdateProgressBar(msg);
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
index ed4008004f..29f4a54847 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
@@ -1,29 +1,45 @@
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
+ Title="{Loc 'analysis-console-menu-title'}"
+ MinSize="620 280"
+ SetSize="620 280">
-
+
+ Text="{Loc 'analysis-console-server-list-button'}">
+ Text="{Loc 'analysis-console-scan-button'}"
+ ToolTip="{Loc 'analysis-console-scan-tooltip-info'}">
+ Text="{Loc 'analysis-console-print-button'}"
+ ToolTip="{Loc 'analysis-console-print-tooltip-info'}">
+
+
+
+
+
+
+
+ Text="{Loc 'analysis-console-extract-button'}"
+ ToolTip="{Loc 'analysis-console-extract-button-info'}">
@@ -36,13 +52,13 @@
-
+
-
+
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
index 90732f814f..2890bb3dbf 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
@@ -3,6 +3,7 @@
using Content.Shared.Xenoarchaeology.Equipment;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -19,6 +20,8 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
public event Action? OnScanButtonPressed;
public event Action? OnPrintButtonPressed;
public event Action? OnExtractButtonPressed;
+ public event Action? OnUpBiasButtonPressed;
+ public event Action? OnDownBiasButtonPressed;
// For rendering the progress bar, updated from BUI state
private TimeSpan? _startTime;
@@ -36,6 +39,12 @@ public AnalysisConsoleMenu()
ScanButton.OnPressed += _ => OnScanButtonPressed?.Invoke();
PrintButton.OnPressed += _ => OnPrintButtonPressed?.Invoke();
ExtractButton.OnPressed += _ => OnExtractButtonPressed?.Invoke();
+ UpBiasButton.OnPressed += _ => OnUpBiasButtonPressed?.Invoke();
+ DownBiasButton.OnPressed += _ => OnDownBiasButtonPressed?.Invoke();
+
+ var buttonGroup = new ButtonGroup(false);
+ UpBiasButton.Group = buttonGroup;
+ DownBiasButton.Group = buttonGroup;
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -60,25 +69,55 @@ protected override void FrameUpdate(FrameEventArgs args)
ProgressBar.Value = Math.Clamp(1.0f - (float) remaining.Divide(total), 0.0f, 1.0f);
}
- public void SetButtonsDisabled(AnalysisConsoleScanUpdateState state)
+ public void SetButtonsDisabled(AnalysisConsoleUpdateState state)
{
ScanButton.Disabled = !state.CanScan;
PrintButton.Disabled = !state.CanPrint;
+ if (state.IsTraversalDown)
+ DownBiasButton.Pressed = true;
+ else
+ UpBiasButton.Pressed = true;
- var disabled = !state.ServerConnected || !state.CanScan || state.PointAmount <= 0;
-
- ExtractButton.Disabled = disabled;
+ ExtractButton.Disabled = false;
+ if (!state.ServerConnected)
+ {
+ ExtractButton.Disabled = true;
+ ExtractButton.ToolTip = Loc.GetString("analysis-console-no-server-connected");
+ }
+ else if (!state.CanScan)
+ {
+ ExtractButton.Disabled = true;
+
+ // CanScan can be false if either there's no analyzer connected or if there's
+ // no entity on the scanner. The `Information` text will always tell the user
+ // of the former case, but in the latter, it'll only show a message if a scan
+ // has never been performed, so add a tooltip to indicate that the artifact
+ // is gone.
+ if (state.AnalyzerConnected)
+ {
+ ExtractButton.ToolTip = Loc.GetString("analysis-console-no-artifact-placed");
+ }
+ else
+ {
+ ExtractButton.ToolTip = null;
+ }
+ }
+ else if (state.PointAmount <= 0)
+ {
+ ExtractButton.Disabled = true;
+ ExtractButton.ToolTip = Loc.GetString("analysis-console-no-points-to-extract");
+ }
- if (disabled)
+ if (ExtractButton.Disabled)
{
ExtractButton.RemoveStyleClass("ButtonColorGreen");
}
else
{
ExtractButton.AddStyleClass("ButtonColorGreen");
+ ExtractButton.ToolTip = null;
}
}
-
private void UpdateArtifactIcon(EntityUid? uid)
{
if (uid == null)
@@ -91,7 +130,7 @@ private void UpdateArtifactIcon(EntityUid? uid)
ArtifactDisplay.SetEntity(uid);
}
- public void UpdateInformationDisplay(AnalysisConsoleScanUpdateState state)
+ public void UpdateInformationDisplay(AnalysisConsoleUpdateState state)
{
var message = new FormattedMessage();
@@ -129,7 +168,7 @@ public void UpdateInformationDisplay(AnalysisConsoleScanUpdateState state)
Information.SetMessage(message);
}
- public void UpdateProgressBar(AnalysisConsoleScanUpdateState state)
+ public void UpdateProgressBar(AnalysisConsoleUpdateState state)
{
ProgressBar.Visible = state.Scanning;
ProgressLabel.Visible = state.Scanning;
diff --git a/Content.Client/Xenonids/UI/XenoChoiceControl.xaml b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml
new file mode 100644
index 0000000000..1257fbc54b
--- /dev/null
+++ b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs
new file mode 100644
index 0000000000..ae451fffe0
--- /dev/null
+++ b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs
@@ -0,0 +1,26 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Xenonids.UI;
+
+[GenerateTypedNameReferences]
+[Virtual]
+public partial class XenoChoiceControl : Control
+{
+ public XenoChoiceControl() => RobustXamlLoader.Load(this);
+
+ public void Set(string name, Texture? texture)
+ {
+ NameLabel.SetMessage(name);
+ Texture.Texture = texture;
+ }
+
+ public void Set(FormattedMessage msg, Texture? texture)
+ {
+ NameLabel.SetMessage(msg);
+ Texture.Texture = texture;
+ }
+}
diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
index 0ea6d3e2dc..f46b83165f 100644
--- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
@@ -2,6 +2,9 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Content.Server.Preferences.Managers;
+using Content.Shared.Preferences;
+using Content.Shared.Roles;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -128,4 +131,29 @@ public List GetPrototypesWithComponent(
return list;
}
+
+ ///
+ /// Helper method for enabling or disabling a antag role
+ ///
+ public async Task SetAntagPref(ProtoId id, bool value)
+ {
+ var prefMan = Server.ResolveDependency();
+
+ var prefs = prefMan.GetPreferences(Client.User!.Value);
+ // what even is the point of ICharacterProfile if we always cast it to HumanoidCharacterProfile to make it usable?
+ var profile = (HumanoidCharacterProfile) prefs.SelectedCharacter;
+
+ Assert.That(profile.AntagPreferences.Any(preference => preference == id), Is.EqualTo(!value));
+ var newProfile = profile.WithAntagPreference(id, value);
+
+ await Server.WaitPost(() =>
+ {
+ prefMan.SetProfile(Client.User.Value, prefs.SelectedCharacterIndex, newProfile).Wait();
+ });
+
+ // And why the fuck does it always create a new preference and profile object instead of just reusing them?
+ var newPrefs = prefMan.GetPreferences(Client.User.Value);
+ var newProf = (HumanoidCharacterProfile) newPrefs.SelectedCharacter;
+ Assert.That(newProf.AntagPreferences.Any(preference => preference == id), Is.EqualTo(value));
+ }
}
diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
index 52fdf600bb..c0f4b3b745 100644
--- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
@@ -131,7 +131,7 @@ public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
// Move to pre-round lobby. Required to toggle dummy ticker on and off
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
- await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting server.");
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
Assert.That(gameTicker.DummyTicker, Is.False);
Server.CfgMan.SetCVar(CCVars.GameLobbyEnabled, true);
await Server.WaitPost(() => gameTicker.RestartRound());
@@ -146,6 +146,7 @@ public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
// Restart server.
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting server again");
+ await Server.WaitPost(() => Server.EntMan.FlushEntities());
await Server.WaitPost(() => gameTicker.RestartRound());
await RunTicksSync(1);
diff --git a/Content.IntegrationTests/Pair/TestPair.Timing.cs b/Content.IntegrationTests/Pair/TestPair.Timing.cs
index 3487ea6801..e0859660d4 100644
--- a/Content.IntegrationTests/Pair/TestPair.Timing.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Timing.cs
@@ -1,5 +1,4 @@
#nullable enable
-using Robust.Shared.Timing;
namespace Content.IntegrationTests.Pair;
@@ -19,6 +18,22 @@ public async Task RunTicksSync(int ticks)
}
}
+ ///
+ /// Convert a time interval to some number of ticks.
+ ///
+ public int SecondsToTicks(float seconds)
+ {
+ return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
+ }
+
+ ///
+ /// Run the server & client in sync for some amount of time
+ ///
+ public async Task RunSeconds(float seconds)
+ {
+ await RunTicksSync(SecondsToTicks(seconds));
+ }
+
///
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
///
@@ -59,4 +74,4 @@ public async Task SyncTicks(int targetDelta = 1)
delta = cTick - sTick;
Assert.That(delta, Is.EqualTo(targetDelta));
}
-}
\ No newline at end of file
+}
diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs
index 327ec627f5..5acd9d502c 100644
--- a/Content.IntegrationTests/PoolManager.Cvars.cs
+++ b/Content.IntegrationTests/PoolManager.Cvars.cs
@@ -21,7 +21,9 @@ private static readonly (string cvar, string value)[] TestCvars =
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
+ (CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
+ (CCVars.PreloadGrids.Name, "false"),
(CCVars.ArrivalsShuttles.Name, "false"),
(CCVars.EmergencyShuttleEnabled.Name, "false"),
(CCVars.ProcgenPreload.Name, "false"),
@@ -32,6 +34,7 @@ private static readonly (string cvar, string value)[] TestCvars =
(CCVars.GameLobbyEnabled.Name, "false"),
(CCVars.ConfigPresetDevelopment.Name, "false"),
(CCVars.AdminLogsEnabled.Name, "false"),
+ (CCVars.AutosaveEnabled.Name, "false"),
(CVars.NetBufferSize.Name, "0")
};
diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs
index b544fe2854..3f489de649 100644
--- a/Content.IntegrationTests/PoolManager.cs
+++ b/Content.IntegrationTests/PoolManager.cs
@@ -68,11 +68,11 @@ public static partial class PoolManager
options.BeforeStart += () =>
{
+ // Server-only systems (i.e., systems that subscribe to events with server-only components)
var entSysMan = IoCManager.Resolve();
- entSysMan.LoadExtraSystemType();
- entSysMan.LoadExtraSystemType();
entSysMan.LoadExtraSystemType();
entSysMan.LoadExtraSystemType();
+
IoCManager.Resolve().GetSawmill("loc").Level = LogLevel.Error;
IoCManager.Resolve()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
index f52f820a4c..cd95a85f20 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
@@ -57,4 +57,3 @@ public async Task ChangeMachine()
AssertPrototype("Autolathe");
}
}
-
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
index 16744d83dc..c40b8ed286 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
@@ -19,36 +19,45 @@ public sealed class DamageableTest
# Define some damage groups
- type: damageType
id: TestDamage1
+ name: damage-type-blunt
- type: damageType
id: TestDamage2a
+ name: damage-type-blunt
- type: damageType
id: TestDamage2b
+ name: damage-type-blunt
- type: damageType
id: TestDamage3a
+ name: damage-type-blunt
- type: damageType
id: TestDamage3b
+ name: damage-type-blunt
- type: damageType
id: TestDamage3c
+ name: damage-type-blunt
# Define damage Groups with 1,2,3 damage types
- type: damageGroup
id: TestGroup1
+ name: damage-group-brute
damageTypes:
- TestDamage1
- type: damageGroup
id: TestGroup2
+ name: damage-group-brute
damageTypes:
- TestDamage2a
- TestDamage2b
- type: damageGroup
id: TestGroup3
+ name: damage-group-brute
damageTypes:
- TestDamage3a
- TestDamage3b
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleTestPrototypes.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleTestPrototypes.cs
index 12292f4652..7ff9242398 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleTestPrototypes.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleTestPrototypes.cs
@@ -12,24 +12,31 @@ public static class DestructibleTestPrototypes
public const string DamagePrototypes = $@"
- type: damageType
id: TestBlunt
+ name: damage-type-blunt
- type: damageType
id: TestSlash
+ name: damage-type-slash
- type: damageType
id: TestPiercing
+ name: damage-type-piercing
- type: damageType
id: TestHeat
+ name: damage-type-heat
- type: damageType
id: TestShock
+ name: damage-type-shock
- type: damageType
id: TestCold
+ name: damage-type-cold
- type: damageGroup
id: TestBrute
+ name: damage-group-brute
damageTypes:
- TestBlunt
- TestSlash
@@ -37,6 +44,7 @@ public static class DestructibleTestPrototypes
- type: damageGroup
id: TestBurn
+ name: damage-group-burn
damageTypes:
- TestHeat
- TestShock
diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
index d47eb13273..0ebd17d887 100644
--- a/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
+++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
@@ -3,8 +3,6 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.IntegrationTests.Tests.Weldable;
using Content.Shared.Tools.Components;
-using Content.Server.Tools.Components;
-using Content.Shared.DoAfter;
namespace Content.IntegrationTests.Tests.DoAfter;
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index d3b1fb4722..926374cf05 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -19,6 +19,8 @@ namespace Content.IntegrationTests.Tests
[TestOf(typeof(EntityUid))]
public sealed class EntityTest
{
+ private static readonly ProtoId SpawnerCategory = "Spawner";
+
[Test]
public async Task SpawnAndDeleteAllEntitiesOnDifferentMaps()
{
@@ -234,14 +236,6 @@ public async Task SpawnAndDeleteEntityCountTest()
"StationEvent",
"TimedDespawn",
- // Spawner entities
- "DragonRift",
- "RandomHumanoidSpawner",
- "RandomSpawner",
- "ConditionalSpawner",
- "GhostRoleMobSpawner",
- "NukeOperativeSpawner",
- "TimedSpawner",
// makes an announcement on mapInit.
"AnnounceOnSpawn",
};
@@ -253,6 +247,7 @@ public async Task SpawnAndDeleteEntityCountTest()
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => !excluded.Any(p.Components.ContainsKey))
+ .Where(p => p.Categories.All(x => x.ID != SpawnerCategory))
.Select(p => p.ID)
.ToList();
@@ -350,8 +345,12 @@ public async Task AllComponentsOneToOneDeleteTest()
"DebrisFeaturePlacerController", // Above.
"LoadedChunk", // Worldgen chunk loading malding.
"BiomeSelection", // Whaddya know, requires config.
+ "ActivatableUI", // Requires enum key
};
+ // TODO TESTS
+ // auto ignore any components that have a "required" data field.
+
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.ResolveDependency();
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
index 1da77ac558..b0aceacc03 100644
--- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
@@ -84,11 +84,13 @@ static AlertsUI FindAlertsUI(Control control)
return null;
}
- // we should be seeing 3 alerts - our health, and the 2 debug alerts, in a specific order.
- Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(3));
+ // This originally was hardcoded to expect a player character to have a Human Healthbar.
+ // It is no longer hardcoded to demand that.
+ // We should be seeing 2 alerts - the 2 debug alerts, in a specific order.
+ Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(2));
var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c);
var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray();
- var expectedIDs = new[] { AlertType.HumanHealth, AlertType.Debug1, AlertType.Debug2 };
+ var expectedIDs = new[] { AlertType.Debug1, AlertType.Debug2 };
Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
});
@@ -101,11 +103,11 @@ await server.WaitAssertion(() =>
await client.WaitAssertion(() =>
{
- // we should be seeing 2 alerts now because one was cleared
- Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(2));
+ // We should be seeing 1 alert now because one was cleared
+ Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(1));
var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c);
var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray();
- var expectedIDs = new[] { AlertType.HumanHealth, AlertType.Debug2 };
+ var expectedIDs = new[] { AlertType.Debug2 };
Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
});
diff --git a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs
new file mode 100644
index 0000000000..662ea3b974
--- /dev/null
+++ b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs
@@ -0,0 +1,76 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.Antag;
+using Content.Server.Antag.Components;
+using Content.Server.GameTicking;
+using Content.Shared.GameTicking;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+
+namespace Content.IntegrationTests.Tests.GameRules;
+
+// Once upon a time, players in the lobby weren't ever considered eligible for antag roles.
+// Lets not let that happen again.
+[TestFixture]
+public sealed class AntagPreferenceTest
+{
+ [Test]
+ public async Task TestLobbyPlayersValid()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings
+ {
+ DummyTicker = false,
+ Connected = true,
+ InLobby = true
+ });
+
+ var server = pair.Server;
+ var client = pair.Client;
+ var ticker = server.System();
+ var sys = server.System();
+
+ // Initially in the lobby
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
+ Assert.That(client.AttachedEntity, Is.Null);
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
+
+ EntityUid uid = default;
+ await server.WaitPost(() => uid = server.EntMan.Spawn("Traitor"));
+ var rule = new Entity(uid, server.EntMan.GetComponent(uid));
+ var def = rule.Comp.Definitions.Single();
+
+ // IsSessionValid & IsEntityValid are preference agnostic and should always be true for players in the lobby.
+ // Though maybe that will change in the future, but then GetPlayerPool() needs to be updated to reflect that.
+ Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
+ Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
+
+ // By default, traitor/antag preferences are disabled, so the pool should be empty.
+ var sessions = new List{pair.Player!};
+ var pool = sys.GetPlayerPool(rule, sessions, def);
+ Assert.That(pool.Count, Is.EqualTo(0));
+
+ // Opt into the traitor role.
+ await pair.SetAntagPref("Traitor", true);
+
+ Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
+ Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
+ pool = sys.GetPlayerPool(rule, sessions, def);
+ Assert.That(pool.Count, Is.EqualTo(1));
+ pool.TryPickAndTake(pair.Server.ResolveDependency(), out var picked);
+ Assert.That(picked, Is.EqualTo(pair.Player));
+ Assert.That(sessions.Count, Is.EqualTo(1));
+
+ // opt back out
+ await pair.SetAntagPref("Traitor", false);
+
+ Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
+ Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
+ pool = sys.GetPlayerPool(rule, sessions, def);
+ Assert.That(pool.Count, Is.EqualTo(0));
+
+ await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
new file mode 100644
index 0000000000..d0e0255ae7
--- /dev/null
+++ b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
@@ -0,0 +1,152 @@
+// #nullable enable
+// using Content.Server.GameTicking;
+// using Content.Server.GameTicking.Components;
+// using Content.Server.GameTicking.Presets;
+// using Content.Shared.CCVar;
+// using Content.Shared.GameTicking;
+// using Robust.Shared.GameObjects;
+
+// namespace Content.IntegrationTests.Tests.GameRules;
+
+// [TestFixture]
+// public sealed class FailAndStartPresetTest
+// {
+// [TestPrototypes]
+// private const string Prototypes = @"
+// - type: gamePreset
+// id: TestPreset
+// alias:
+// - nukeops
+// name: Test Preset
+// description: """"
+// showInVote: false
+// rules:
+// - TestRule
+
+// - type: gamePreset
+// id: TestPresetTenPlayers
+// alias:
+// - nukeops
+// name: Test Preset 10 players
+// description: """"
+// showInVote: false
+// rules:
+// - TestRuleTenPlayers
+
+// - type: entity
+// id: TestRule
+// parent: BaseGameRule
+// noSpawn: true
+// components:
+// - type: GameRule
+// minPlayers: 0
+// - type: TestRule
+
+// - type: entity
+// id: TestRuleTenPlayers
+// parent: BaseGameRule
+// noSpawn: true
+// components:
+// - type: GameRule
+// minPlayers: 10
+// - type: TestRule
+// ";
+
+// ///