Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Samuka-C <47865393+Samuka-C@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: Partmedia <kevinz5000@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Victor Shen <71985089+Vexerot@users.noreply.github.com>
Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: Kirus59 <145689588+Kirus59@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Stomf <5dorkydorks@gmail.com>
Co-authored-by: drakewill-CRL <46307022+drakewill-CRL@users.noreply.github.com>
Co-authored-by: PraxisMapper <praxismapper@gmail.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com>
Co-authored-by: abadaba695 <spacestation13thingy@gmail.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: Thinbug <101073555+Thinbug0@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: ActiveMammmoth <140334666+ActiveMammmoth@users.noreply.github.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: Gentleman-Bird <dcgreen406@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: BIGZi0348 <svalker0348@gmail.com>
Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com>
Co-authored-by: imatsoup <93290208+imatsoup@users.noreply.github.com>
Co-authored-by: Matthew Herber <32679887+happyrobot33@users.noreply.github.com>
Co-authored-by: Ertanic <36124833+Ertanic@users.noreply.github.com>
Co-authored-by: MissKay1994 <15877268+MissKay1994@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: eoineoineoin <helloworld@eoinrul.es>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: nikitosych <boriszyn@gmail.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: YoungThug <ramialanbagy@gmail.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: Vladislav Suchkov <20380250+murolem@users.noreply.github.com>
Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: Unkn0wn_Gh0st <shadowstalkermll@gmail.com>
Co-authored-by: 3nderall <101940324+3nderall@users.noreply.github.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
Co-authored-by: J <billsmith116@gmail.com>
Co-authored-by: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: youtissoum <51883137+youtissoum@users.noreply.github.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: Spanky <scott@wearejacob.com>
Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: brainfood1183 <113240905+brainfood1183@users.noreply.github.com>
Co-authored-by: Deerstop <edainturner@gmail.com>
Co-authored-by: B_Kirill <153602297+B-Kirill@users.noreply.github.com>
Co-authored-by: archee1 <archee3@hotmail.co.uk>
Co-authored-by: Cojoke <83733158+Cojoke-dot@users.noreply.github.com>
Co-authored-by: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Co-authored-by: poklj <compgeek223@gmail.com>
Co-authored-by: Krunklehorn <42424291+Krunklehorn@users.noreply.github.com>
Co-authored-by: OnyxTheBrave <131422822+OnyxTheBrave@users.noreply.github.com>
Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Zalycon <84675130+Zalycon@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: Dmitry <57028746+dimm00n@users.noreply.github.com>
This commit is contained in:
Dmitry
2025-05-24 15:00:17 +07:00
committed by GitHub
parent b41feb23e9
commit 25c5e59248
3429 changed files with 267307 additions and 99006 deletions

View File

@@ -127,6 +127,7 @@ csharp_indent_braces = false
#csharp_indent_case_contents_when_block = true #csharp_indent_case_contents_when_block = true
#csharp_indent_labels = one_less_than_current #csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true csharp_indent_switch_labels = true
xmldoc_indent_text = zeroindent
# Space preferences # Space preferences
csharp_space_after_cast = false csharp_space_after_cast = false

6
.github/CODEOWNERS vendored
View File

@@ -8,8 +8,8 @@
*.ftl @ficcialfaint *.ftl @ficcialfaint
# Map files # Map files
/Resources/Prototypes/Maps/** @Ko4ergaPunk /Resources/Prototypes/Maps/** @Ko4ergaPunk
/Resources/Maps/** @Ko4ergaPunk /Resources/Maps/** @Ko4ergaPunk
# Sprites # Sprites
/Resources/Textures/** @SonicHDC /Resources/Textures/** @SonicHDC

View File

@@ -2,6 +2,7 @@
concurrency: concurrency:
group: publish-testing group: publish-testing
cancel-in-progress: true
on: on:
workflow_dispatch: workflow_dispatch:

View File

@@ -2,6 +2,7 @@ name: Publish
concurrency: concurrency:
group: publish group: publish
cancel-in-progress: true
on: on:
workflow_dispatch: workflow_dispatch:
@@ -66,12 +67,14 @@ jobs:
FORK_ID: ${{ vars.FORK_ID }} FORK_ID: ${{ vars.FORK_ID }}
# - name: Publish changelog (Discord) # - name: Publish changelog (Discord)
# continue-on-error: true
# run: Tools/actions_changelogs_since_last_run.py # run: Tools/actions_changelogs_since_last_run.py
# env: # env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }} # DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
# - name: Publish changelog (RSS) # - name: Publish changelog (RSS)
# continue-on-error: true
# run: Tools/actions_changelog_rss.py # run: Tools/actions_changelog_rss.py
# env: # env:
# CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }} # CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}

44
.vscode/tasks.json vendored
View File

@@ -32,6 +32,50 @@
"/consoleloggerparameters:'ForceNoAlign;NoSummary'" "/consoleloggerparameters:'ForceNoAlign;NoSummary'"
], ],
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "test",
"command": "dotnet",
"type": "shell",
"args": [
"test",
"--no-build",
"--configuration",
"DebugOpt",
"Content.Tests/Content.Tests.csproj",
"--",
"NUnit.ConsoleOut=0"
],
"group": {
"kind": "test"
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "integration-test",
"command": "dotnet",
"type": "shell",
"args": [
"test",
"--no-build",
"--configuration",
"DebugOpt",
"Content.IntegrationTests/Content.IntegrationTests.csproj",
"--",
"NUnit.ConsoleOut=0",
"NUnit.MapWarningTo=Failed.ConsoleOut=0",
"NUnit.MapWarningTo=Failed"
],
"group": {
"kind": "test"
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
} }
] ]
} }

View File

@@ -5,6 +5,7 @@ import subprocess
import sys import sys
import os import os
import shutil import shutil
import time
from pathlib import Path from pathlib import Path
from typing import List from typing import List
@@ -104,7 +105,21 @@ def reset_solution():
with SOLUTION_PATH.open("w") as f: with SOLUTION_PATH.open("w") as f:
f.write(content) f.write(content)
def check_for_zip_download():
# Check if .git exists,
cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if not os.path.isdir(os.path.join(cur_dir, ".git")):
print("It appears that you downloaded this repository directly from GitHub. (Using the .zip download option) \n"
"When downloading straight from GitHub, it leaves out important information that git needs to function. "
"Such as information to download the engine or even the ability to even be able to create contributions. \n"
"Please read and follow https://docs.spacestation14.com/en/general-development/setup/setting-up-a-development-environment.html \n"
"If you just want a Sandbox Server, you are following the wrong guide! You can download a premade server following the instructions here:"
"https://docs.spacestation14.com/en/general-development/setup/server-hosting-tutorial.html \n"
"Closing automatically in 30 seconds.")
time.sleep(30)
exit(1)
if __name__ == '__main__': if __name__ == '__main__':
check_for_zip_download()
install_hooks() install_hooks()
update_submodules() update_submodules()

View File

@@ -7,6 +7,7 @@ using Content.IntegrationTests;
using Content.IntegrationTests.Pair; using Content.IntegrationTests.Pair;
using Content.Server.Mind; using Content.Server.Mind;
using Content.Server.Warps; using Content.Server.Warps;
using Content.Shared.Warps;
using Robust.Shared; using Robust.Shared;
using Robust.Shared.Analyzers; using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization; using Robust.Shared.EntitySerialization;

View File

@@ -174,7 +174,8 @@ namespace Content.Client.Access.UI
new List<ProtoId<AccessLevelPrototype>>()); new List<ProtoId<AccessLevelPrototype>>());
var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype); var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
// If the job index is < 0 that means they don't have a job registered in the station records. // If the job index is < 0 that means they don't have a job registered in the station records
// or the IdCardComponent's JobPrototype field.
// For example, a new ID from a box would have no job index. // For example, a new ID from a box would have no job index.
if (jobIndex < 0) if (jobIndex < 0)
{ {

View File

@@ -1,6 +1,7 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Content.Shared.Actions; using Content.Shared.Actions;
using Content.Shared.Charges.Systems;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.ContentPack; using Robust.Shared.ContentPack;
@@ -22,6 +23,7 @@ namespace Content.Client.Actions
{ {
public delegate void OnActionReplaced(EntityUid actionId); public delegate void OnActionReplaced(EntityUid actionId);
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceManager _resources = default!; [Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly ISerializationManager _serialization = default!;
@@ -51,29 +53,6 @@ namespace Content.Client.Actions
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState); SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
} }
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
while (worldActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
while (instantActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
while (entityActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
}
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args) private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
{ {
if (args.Current is not InstantActionComponentState state) if (args.Current is not InstantActionComponentState state)
@@ -127,9 +106,6 @@ namespace Content.Client.Actions
component.Toggled = state.Toggled; component.Toggled = state.Toggled;
component.Cooldown = state.Cooldown; component.Cooldown = state.Cooldown;
component.UseDelay = state.UseDelay; component.UseDelay = state.UseDelay;
component.Charges = state.Charges;
component.MaxCharges = state.MaxCharges;
component.RenewCharges = state.RenewCharges;
component.Container = EnsureEntity<T>(state.Container, uid); component.Container = EnsureEntity<T>(state.Container, uid);
component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid); component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
component.CheckCanInteract = state.CheckCanInteract; component.CheckCanInteract = state.CheckCanInteract;
@@ -152,7 +128,8 @@ namespace Content.Client.Actions
if (!ResolveActionData(actionId, ref action)) if (!ResolveActionData(actionId, ref action))
return; return;
action.IconColor = action.Charges < 1 ? action.DisabledIconColor : action.OriginalIconColor; // TODO: Decouple this.
action.IconColor = _sharedCharges.GetCurrentCharges(actionId.Value) == 0 ? action.DisabledIconColor : action.OriginalIconColor;
base.UpdateAction(actionId, action); base.UpdateAction(actionId, action);
if (_playerManager.LocalEntity != action.AttachedEntity) if (_playerManager.LocalEntity != action.AttachedEntity)
@@ -225,6 +202,7 @@ namespace Content.Client.Actions
return; return;
OnActionAdded?.Invoke(actionId); OnActionAdded?.Invoke(actionId);
ActionsUpdated?.Invoke();
} }
protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action) protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
@@ -233,6 +211,7 @@ namespace Content.Client.Actions
return; return;
OnActionRemoved?.Invoke(actionId); OnActionRemoved?.Invoke(actionId);
ActionsUpdated?.Invoke();
} }
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions() public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()

View File

@@ -6,6 +6,7 @@ using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Ghost; using Content.Shared.Ghost;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Roles;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
@@ -22,10 +23,11 @@ internal sealed class AdminNameOverlay : Overlay
private readonly IEyeManager _eyeManager; private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup; private readonly EntityLookupSystem _entityLookup;
private readonly IUserInterfaceManager _userInterfaceManager; private readonly IUserInterfaceManager _userInterfaceManager;
private readonly SharedRoleSystem _roles;
private readonly Font _font; private readonly Font _font;
private readonly Font _fontBold; private readonly Font _fontBold;
private bool _overlayClassic; private AdminOverlayAntagFormat _overlayFormat;
private bool _overlaySymbols; private AdminOverlayAntagSymbolStyle _overlaySymbolStyle;
private bool _overlayPlaytime; private bool _overlayPlaytime;
private bool _overlayStartingJob; private bool _overlayStartingJob;
private float _ghostFadeDistance; private float _ghostFadeDistance;
@@ -33,9 +35,10 @@ internal sealed class AdminNameOverlay : Overlay
private int _overlayStackMax; private int _overlayStackMax;
private float _overlayMergeDistance; private float _overlayMergeDistance;
//TODO make this adjustable via GUI //TODO make this adjustable via GUI?
private readonly ProtoId<RoleTypePrototype>[] _filter = private readonly ProtoId<RoleTypePrototype>[] _filter =
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"]; ["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic"); private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
public AdminNameOverlay( public AdminNameOverlay(
@@ -45,20 +48,22 @@ internal sealed class AdminNameOverlay : Overlay
IResourceCache resourceCache, IResourceCache resourceCache,
EntityLookupSystem entityLookup, EntityLookupSystem entityLookup,
IUserInterfaceManager userInterfaceManager, IUserInterfaceManager userInterfaceManager,
IConfigurationManager config) IConfigurationManager config,
SharedRoleSystem roles)
{ {
_system = system; _system = system;
_entityManager = entityManager; _entityManager = entityManager;
_eyeManager = eyeManager; _eyeManager = eyeManager;
_entityLookup = entityLookup; _entityLookup = entityLookup;
_userInterfaceManager = userInterfaceManager; _userInterfaceManager = userInterfaceManager;
_roles = roles;
ZIndex = 200; ZIndex = 200;
// Setting these to a specific ttf would break the antag symbols // Setting these to a specific ttf would break the antag symbols
_font = resourceCache.NotoStack(); _font = resourceCache.NotoStack();
_fontBold = resourceCache.NotoStack(variation: "Bold"); _fontBold = resourceCache.NotoStack(variation: "Bold");
config.OnValueChanged(CCVars.AdminOverlayClassic, (show) => { _overlayClassic = show; }, true); config.OnValueChanged(CCVars.AdminOverlayAntagFormat, (show) => { _overlayFormat = UpdateOverlayFormat(show); }, true);
config.OnValueChanged(CCVars.AdminOverlaySymbols, (show) => { _overlaySymbols = show; }, true); config.OnValueChanged(CCVars.AdminOverlaySymbolStyle, (show) => { _overlaySymbolStyle = UpdateOverlaySymbolStyle(show); }, true);
config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true); config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayStartingJob, (show) => { _overlayStartingJob = show; }, true); config.OnValueChanged(CCVars.AdminOverlayStartingJob, (show) => { _overlayStartingJob = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayGhostHideDistance, (f) => { _ghostHideDistance = f; }, true); config.OnValueChanged(CCVars.AdminOverlayGhostHideDistance, (f) => { _ghostHideDistance = f; }, true);
@@ -67,6 +72,22 @@ internal sealed class AdminNameOverlay : Overlay
config.OnValueChanged(CCVars.AdminOverlayMergeDistance, (f) => { _overlayMergeDistance = f; }, true); config.OnValueChanged(CCVars.AdminOverlayMergeDistance, (f) => { _overlayMergeDistance = f; }, true);
} }
private AdminOverlayAntagFormat UpdateOverlayFormat(string formatString)
{
if (!Enum.TryParse<AdminOverlayAntagFormat>(formatString, out var format))
format = AdminOverlayAntagFormat.Binary;
return format;
}
private AdminOverlayAntagSymbolStyle UpdateOverlaySymbolStyle(string symbolString)
{
if (!Enum.TryParse<AdminOverlayAntagSymbolStyle>(symbolString, out var symbolStyle))
symbolStyle = AdminOverlayAntagSymbolStyle.Off;
return symbolStyle;
}
public override OverlaySpace Space => OverlaySpace.ScreenSpace; public override OverlaySpace Space => OverlaySpace.ScreenSpace;
protected override void Draw(in OverlayDrawArgs args) protected override void Draw(in OverlayDrawArgs args)
@@ -183,34 +204,56 @@ internal sealed class AdminNameOverlay : Overlay
currentOffset += lineoffset; currentOffset += lineoffset;
} }
// Classic Antag Label // Determine antag symbol
if (_overlayClassic && playerInfo.Antag) string? symbol;
switch (_overlaySymbolStyle)
{ {
var symbol = _overlaySymbols ? Loc.GetString("player-tab-antag-prefix") : string.Empty; case AdminOverlayAntagSymbolStyle.Specific:
var label = _overlaySymbols symbol = playerInfo.RoleProto.Symbol;
? Loc.GetString("player-tab-character-name-antag-symbol", break;
("symbol", symbol), case AdminOverlayAntagSymbolStyle.Basic:
("name", _antagLabelClassic)) symbol = Loc.GetString("player-tab-antag-prefix");
: _antagLabelClassic; break;
color = Color.OrangeRed; default:
color.A = alpha; case AdminOverlayAntagSymbolStyle.Off:
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color); symbol = string.Empty;
currentOffset += lineoffset; break;
} }
// Role Type
else if (!_overlayClassic && _filter.Contains(playerInfo.RoleProto)) // Determine antag/role type name
string? text;
switch (_overlayFormat)
{ {
var symbol = _overlaySymbols && playerInfo.Antag ? playerInfo.RoleProto.Symbol : string.Empty; case AdminOverlayAntagFormat.Roletype:
var role = Loc.GetString(playerInfo.RoleProto.Name).ToUpper(); color = playerInfo.RoleProto.Color;
var label = _overlaySymbols symbol = _filter.Contains(playerInfo.RoleProto) ? symbol : string.Empty;
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", role)) text = _filter.Contains(playerInfo.RoleProto)
: role; ? Loc.GetString(playerInfo.RoleProto.Name).ToUpper()
color = playerInfo.RoleProto.Color; : string.Empty;
color.A = alpha; break;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color); case AdminOverlayAntagFormat.Subtype:
currentOffset += lineoffset; color = playerInfo.RoleProto.Color;
symbol = _filter.Contains(playerInfo.RoleProto) ? symbol : string.Empty;
text = _filter.Contains(playerInfo.RoleProto)
? _roles.GetRoleSubtypeLabel(playerInfo.RoleProto.Name, playerInfo.Subtype).ToUpper()
: string.Empty;
break;
default:
case AdminOverlayAntagFormat.Binary:
color = Color.OrangeRed;
symbol = playerInfo.Antag ? symbol : string.Empty;
text = playerInfo.Antag ? _antagLabelClassic : string.Empty;
break;
} }
// Draw antag label
color.A = alpha;
var label = !string.IsNullOrEmpty(symbol)
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", text))
: text;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
//Save the coordinates and size of the text block, for stack merge check //Save the coordinates and size of the text block, for stack merge check
drawnOverlays.Add((screenCoordinatesCenter, currentOffset)); drawnOverlays.Add((screenCoordinatesCenter, currentOffset));
} }

View File

@@ -0,0 +1,15 @@
namespace Content.Client.Administration;
public enum AdminOverlayAntagFormat
{
Binary,
Roletype,
Subtype
}
public enum AdminOverlayAntagSymbolStyle
{
Off,
Basic,
Specific
}

View File

@@ -1,4 +1,5 @@
using Content.Client.Administration.Managers; using Content.Client.Administration.Managers;
using Content.Shared.Roles;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
@@ -15,6 +16,7 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly EntityLookupSystem _entityLookup = default!; [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
private AdminNameOverlay _adminNameOverlay = default!; private AdminNameOverlay _adminNameOverlay = default!;
@@ -30,7 +32,8 @@ namespace Content.Client.Administration.Systems
_resourceCache, _resourceCache,
_entityLookup, _entityLookup,
_userInterfaceManager, _userInterfaceManager,
_configurationManager); _configurationManager,
_roles);
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated; _adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
} }
@@ -46,7 +49,8 @@ namespace Content.Client.Administration.Systems
public void AdminOverlayOn() public void AdminOverlayOn()
{ {
if (_overlayManager.HasOverlay<AdminNameOverlay>()) return; if (_overlayManager.HasOverlay<AdminNameOverlay>())
return;
_overlayManager.AddOverlay(_adminNameOverlay); _overlayManager.AddOverlay(_adminNameOverlay);
OverlayEnabled?.Invoke(); OverlayEnabled?.Invoke();
} }

View File

@@ -32,7 +32,7 @@ namespace Content.Client.Administration.Systems
var verb = new VvVerb() var verb = new VvVerb()
{ {
Text = Loc.GetString("view-variables"), Text = Loc.GetString("view-variables"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
Act = () => _clientConsoleHost.ExecuteCommand($"vv {GetNetEntity(args.Target)}"), Act = () => _clientConsoleHost.ExecuteCommand($"vv {GetNetEntity(args.Target)}"),
ClientExclusive = true // opening VV window is client-side. Don't ask server to run this verb. ClientExclusive = true // opening VV window is client-side. Don't ask server to run this verb.
}; };

View File

@@ -1,59 +0,0 @@
using System.Threading;
using Content.Client.Stylesheets;
using Robust.Client.UserInterface.Controls;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.Administration.UI;
public static class AdminUIHelpers
{
private static void ResetButton(Button button, ConfirmationData data)
{
data.Cancellation.Cancel();
button.ModulateSelfOverride = null;
button.Text = data.OriginalText;
}
public static bool RemoveConfirm(Button button, Dictionary<Button, ConfirmationData> confirmations)
{
if (confirmations.Remove(button, out var data))
{
ResetButton(button, data);
return true;
}
return false;
}
public static void RemoveAllConfirms(Dictionary<Button, ConfirmationData> confirmations)
{
foreach (var (button, confirmation) in confirmations)
{
ResetButton(button, confirmation);
}
confirmations.Clear();
}
public static bool TryConfirm(Button button, Dictionary<Button, ConfirmationData> confirmations)
{
if (RemoveConfirm(button, confirmations))
return true;
var data = new ConfirmationData(new CancellationTokenSource(), button.Text);
confirmations[button] = data;
Timer.Spawn(TimeSpan.FromSeconds(5), () =>
{
confirmations.Remove(button);
button.ModulateSelfOverride = null;
button.Text = data.OriginalText;
}, data.Cancellation.Token);
button.ModulateSelfOverride = StyleNano.ButtonColorCautionDefault;
button.Text = Loc.GetString("admin-player-actions-confirm");
return false;
}
}
public readonly record struct ConfirmationData(CancellationTokenSource Cancellation, string? OriginalText);

View File

@@ -1,6 +1,7 @@
<Control <Control
xmlns="https://spacestation14.io" xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"> xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<PanelContainer StyleClasses="BackgroundDark"> <PanelContainer StyleClasses="BackgroundDark">
<SplitContainer Orientation="Vertical" ResizeMode="NotResizable"> <SplitContainer Orientation="Vertical" ResizeMode="NotResizable">
<SplitContainer Orientation="Horizontal" VerticalExpand="True"> <SplitContainer Orientation="Horizontal" VerticalExpand="True">
@@ -18,9 +19,9 @@
<Control HorizontalExpand="True" /> <Control HorizontalExpand="True" />
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" /> <Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" /> <Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" StyleClasses="OpenBoth" /> <controls:ConfirmButton Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" /> <Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" StyleClasses="OpenBoth" /> <controls:ConfirmButton Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" /> <Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
</BoxContainer> </BoxContainer>
</SplitContainer> </SplitContainer>

View File

@@ -29,7 +29,6 @@ namespace Content.Client.Administration.UI.Bwoink
public AdminAHelpUIHandler AHelpHelper = default!; public AdminAHelpUIHandler AHelpHelper = default!;
private PlayerInfo? _currentPlayer; private PlayerInfo? _currentPlayer;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public BwoinkControl() public BwoinkControl()
{ {
@@ -178,11 +177,6 @@ namespace Content.Client.Administration.UI.Bwoink
Kick.OnPressed += _ => Kick.OnPressed += _ =>
{ {
if (!AdminUIHelpers.TryConfirm(Kick, _confirmations))
{
return;
}
// TODO: Reason field // TODO: Reason field
if (_currentPlayer is not null) if (_currentPlayer is not null)
_console.ExecuteCommand($"kick \"{_currentPlayer.Username}\""); _console.ExecuteCommand($"kick \"{_currentPlayer.Username}\"");
@@ -196,11 +190,6 @@ namespace Content.Client.Administration.UI.Bwoink
Respawn.OnPressed += _ => Respawn.OnPressed += _ =>
{ {
if (!AdminUIHelpers.TryConfirm(Respawn, _confirmations))
{
return;
}
if (_currentPlayer is not null) if (_currentPlayer is not null)
_console.ExecuteCommand($"respawn \"{_currentPlayer.Username}\""); _console.ExecuteCommand($"respawn \"{_currentPlayer.Username}\"");
}; };

View File

@@ -1,5 +1,6 @@
<Popup xmlns="https://spacestation14.io" <Popup xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"> xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<PanelContainer> <PanelContainer>
<PanelContainer.PanelOverride> <PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BorderThickness="2" BorderColor="#18181B" BackgroundColor="#25252a"/> <gfx:StyleBoxFlat BorderThickness="2" BorderColor="#18181B" BackgroundColor="#25252a"/>
@@ -19,7 +20,7 @@
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal">
<Button Name="EditButton" Text="{Loc admin-notes-edit}"/> <Button Name="EditButton" Text="{Loc admin-notes-edit}"/>
<Control HorizontalExpand="True"/> <Control HorizontalExpand="True"/>
<Button Name="DeleteButton" Text="{Loc admin-notes-delete}" HorizontalAlignment="Right"/> <controls:ConfirmButton Name="DeleteButton" Text="{Loc admin-notes-delete}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" HorizontalAlignment="Right"/>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</PanelContainer> </PanelContainer>

View File

@@ -19,12 +19,13 @@
<Label Name="SharedConnections"/> <Label Name="SharedConnections"/>
<BoxContainer Align="Center"> <BoxContainer Align="Center">
<GridContainer Rows="5"> <GridContainer Rows="6">
<Button Name="NotesButton" Text="{Loc player-panel-show-notes}" SetWidth="136" Disabled="True"/> <Button Name="NotesButton" Text="{Loc player-panel-show-notes}" SetWidth="136" Disabled="True"/>
<Button Name="AhelpButton" Text="{Loc player-panel-help}" Disabled="True"/> <Button Name="AhelpButton" Text="{Loc player-panel-help}" Disabled="True"/>
<Button Name="FreezeButton" Text = "{Loc player-panel-freeze}" Disabled="True"/> <Button Name="FreezeButton" Text = "{Loc player-panel-freeze}" Disabled="True"/>
<controls:ConfirmButton Name="KickButton" Text="{Loc player-panel-kick}" Disabled="True"/> <controls:ConfirmButton Name="KickButton" Text="{Loc player-panel-kick}" Disabled="True"/>
<controls:ConfirmButton Name="DeleteButton" Text="{Loc player-panel-delete}" Disabled="True"/> <controls:ConfirmButton Name="DeleteButton" Text="{Loc player-panel-delete}" Disabled="True"/>
<Button Name="FollowButton" Text="{Loc player-panel-follow}"/>
<Button Name="ShowBansButton" Text="{Loc player-panel-show-bans}" SetWidth="136" Disabled="True"/> <Button Name="ShowBansButton" Text="{Loc player-panel-show-bans}" SetWidth="136" Disabled="True"/>
<Button Name="LogsButton" Text="{Loc player-panel-logs}" Disabled="True"/> <Button Name="LogsButton" Text="{Loc player-panel-logs}" Disabled="True"/>
<Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/> <Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/>

View File

@@ -2,7 +2,6 @@ using Content.Client.Administration.Managers;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -21,6 +20,7 @@ public sealed partial class PlayerPanel : FancyWindow
public event Action<string?>? OnKick; public event Action<string?>? OnKick;
public event Action<NetUserId?>? OnOpenBanPanel; public event Action<NetUserId?>? OnOpenBanPanel;
public event Action<NetUserId?, bool>? OnWhitelistToggle; public event Action<NetUserId?, bool>? OnWhitelistToggle;
public event Action? OnFollow;
public event Action? OnFreezeAndMuteToggle; public event Action? OnFreezeAndMuteToggle;
public event Action? OnFreeze; public event Action? OnFreeze;
public event Action? OnLogs; public event Action? OnLogs;
@@ -36,7 +36,7 @@ public sealed partial class PlayerPanel : FancyWindow
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
_adminManager = adminManager; _adminManager = adminManager;
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(PlayerName.Text ?? ""); UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(TargetUsername ?? "");
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer); BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername); KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer); NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
@@ -47,6 +47,7 @@ public sealed partial class PlayerPanel : FancyWindow
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted); OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
SetWhitelisted(!_isWhitelisted); SetWhitelisted(!_isWhitelisted);
}; };
FollowButton.OnPressed += _ => OnFollow?.Invoke();
FreezeButton.OnPressed += _ => OnFreeze?.Invoke(); FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke(); FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
LogsButton.OnPressed += _ => OnLogs?.Invoke(); LogsButton.OnPressed += _ => OnLogs?.Invoke();

View File

@@ -38,6 +38,7 @@ public sealed class PlayerPanelEui : BaseEui
PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage()); PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage());
PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage()); PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage()); PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage());
PlayerPanel.OnFollow += () => SendMessage(new PlayerPanelFollowMessage());
PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage()); PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage());
} }

View File

@@ -16,6 +16,7 @@
<cc:UICommandButton Command="callshuttle" Text="{Loc admin-player-actions-window-shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/> <cc:UICommandButton Command="callshuttle" Text="{Loc admin-player-actions-window-shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
<cc:CommandButton Command="adminlogs" Text="{Loc admin-player-actions-window-admin-logs}"/> <cc:CommandButton Command="adminlogs" Text="{Loc admin-player-actions-window-admin-logs}"/>
<cc:CommandButton Command="faxui" Text="{Loc admin-player-actions-window-admin-fax}"/> <cc:CommandButton Command="faxui" Text="{Loc admin-player-actions-window-admin-fax}"/>
<cc:CommandButton Command="achatwindow" Text="{Loc admin-player-actions-window-admin-chat}"/>
</GridContainer> </GridContainer>
</BoxContainer> </BoxContainer>
</Control> </Control>

View File

@@ -1,6 +1,7 @@
<DefaultWindow <DefaultWindow
xmlns="https://spacestation14.io" xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc admin-player-actions-window-title}" MinSize="425 272"> Title="{Loc admin-player-actions-window-title}" MinSize="425 272">
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal">
@@ -10,9 +11,9 @@
</BoxContainer> </BoxContainer>
<cc:PlayerListControl Name="PlayerList" VerticalExpand="True" /> <cc:PlayerListControl Name="PlayerList" VerticalExpand="True" />
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal">
<Button Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" Disabled="True"/> <controls:ConfirmButton Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
<Button Name="SubmitAHelpButton" Text="{Loc admin-player-actions-ahelp}" Disabled="True"/> <Button Name="SubmitAHelpButton" Text="{Loc admin-player-actions-ahelp}" Disabled="True"/>
<Button Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" Disabled="True"/> <controls:ConfirmButton Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</DefaultWindow> </DefaultWindow>

View File

@@ -14,7 +14,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
public sealed partial class PlayerActionsWindow : DefaultWindow public sealed partial class PlayerActionsWindow : DefaultWindow
{ {
private PlayerInfo? _selectedPlayer; private PlayerInfo? _selectedPlayer;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public PlayerActionsWindow() public PlayerActionsWindow()
{ {
@@ -28,9 +27,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
private void OnListOnOnSelectionChanged(PlayerInfo? obj) private void OnListOnOnSelectionChanged(PlayerInfo? obj)
{ {
if (_selectedPlayer != obj)
AdminUIHelpers.RemoveAllConfirms(_confirmations);
_selectedPlayer = obj; _selectedPlayer = obj;
var disableButtons = _selectedPlayer == null; var disableButtons = _selectedPlayer == null;
SubmitKickButton.Disabled = disableButtons; SubmitKickButton.Disabled = disableButtons;
@@ -43,9 +39,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
if (_selectedPlayer == null) if (_selectedPlayer == null)
return; return;
if (!AdminUIHelpers.TryConfirm(SubmitKickButton, _confirmations))
return;
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand( IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
$"kick \"{_selectedPlayer.Username}\" \"{CommandParsing.Escape(ReasonLine.Text)}\""); $"kick \"{_selectedPlayer.Username}\" \"{CommandParsing.Escape(ReasonLine.Text)}\"");
} }
@@ -64,9 +57,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
if (_selectedPlayer == null) if (_selectedPlayer == null)
return; return;
if (!AdminUIHelpers.TryConfirm(SubmitRespawnButton, _confirmations))
return;
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand( IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
$"respawn \"{_selectedPlayer.Username}\""); $"respawn \"{_selectedPlayer.Username}\"");
} }

View File

@@ -13,7 +13,6 @@ public sealed partial class ObjectsTabEntry : PanelContainer
public Action<NetEntity>? OnTeleport; public Action<NetEntity>? OnTeleport;
public Action<NetEntity>? OnDelete; public Action<NetEntity>? OnDelete;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public ObjectsTabEntry(IClientAdminManager manager, string name, NetEntity nent, StyleBox styleBox) public ObjectsTabEntry(IClientAdminManager manager, string name, NetEntity nent, StyleBox styleBox)
{ {
@@ -28,13 +27,6 @@ public sealed partial class ObjectsTabEntry : PanelContainer
DeleteButton.Disabled = !manager.CanCommand("delete"); DeleteButton.Disabled = !manager.CanCommand("delete");
TeleportButton.OnPressed += _ => OnTeleport?.Invoke(nent); TeleportButton.OnPressed += _ => OnTeleport?.Invoke(nent);
DeleteButton.OnPressed += _ => DeleteButton.OnPressed += _ => OnDelete?.Invoke(nent);
{
if (!AdminUIHelpers.TryConfirm(DeleteButton, _confirmations))
{
return;
}
OnDelete?.Invoke(nent);
};
} }
} }

View File

@@ -32,6 +32,10 @@ public sealed partial class PlayerTab : Control
private bool _ascending = true; private bool _ascending = true;
private bool _showDisconnected; private bool _showDisconnected;
private AdminPlayerTabColorOption _playerTabColorSetting;
private AdminPlayerTabRoleTypeOption _playerTabRoleSetting;
private AdminPlayerTabSymbolOption _playerTabSymbolSetting;
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown; public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
public PlayerTab() public PlayerTab()
@@ -44,9 +48,10 @@ public sealed partial class PlayerTab : Control
_adminSystem.OverlayEnabled += OverlayEnabled; _adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled; _adminSystem.OverlayDisabled += OverlayDisabled;
_config.OnValueChanged(CCVars.AdminPlayerlistSeparateSymbols, PlayerListSettingsChanged); _config.OnValueChanged(CCVars.AdminPlayerTabRoleSetting, RoleSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerlistHighlightedCharacterColor, PlayerListSettingsChanged); _config.OnValueChanged(CCVars.AdminPlayerTabColorSetting, ColorSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerlistRoleTypeColor, PlayerListSettingsChanged); _config.OnValueChanged(CCVars.AdminPlayerTabSymbolSetting, SymbolSettingChanged, true);
OverlayButton.OnPressed += OverlayButtonPressed; OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed; ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
@@ -113,8 +118,27 @@ public sealed partial class PlayerTab : Control
#region ListContainer #region ListContainer
private void PlayerListSettingsChanged(bool _) private void RoleSettingChanged(string s)
{ {
if (!Enum.TryParse<AdminPlayerTabRoleTypeOption>(s, out var format))
format = AdminPlayerTabRoleTypeOption.Subtype;
_playerTabRoleSetting = format;
RefreshPlayerList(_adminSystem.PlayerList);
}
private void ColorSettingChanged(string s)
{
if (!Enum.TryParse<AdminPlayerTabColorOption>(s, out var format))
format = AdminPlayerTabColorOption.Both;
_playerTabColorSetting = format;
RefreshPlayerList(_adminSystem.PlayerList);
}
private void SymbolSettingChanged(string s)
{
if (!Enum.TryParse<AdminPlayerTabSymbolOption>(s, out var format))
format = AdminPlayerTabSymbolOption.Specific;
_playerTabSymbolSetting = format;
RefreshPlayerList(_adminSystem.PlayerList); RefreshPlayerList(_adminSystem.PlayerList);
} }
@@ -140,7 +164,12 @@ public sealed partial class PlayerTab : Control
if (data is not PlayerListData { Info: var player}) if (data is not PlayerListData { Info: var player})
return; return;
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor)); var entry = new PlayerTabEntry(
player,
new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor),
_playerTabColorSetting,
_playerTabRoleSetting,
_playerTabSymbolSetting);
button.AddChild(entry); button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}"; button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
} }

View File

@@ -1,42 +1,99 @@
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.Roles;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
namespace Content.Client.Administration.UI.Tabs.PlayerTab; namespace Content.Client.Administration.UI.Tabs.PlayerTab;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class PlayerTabEntry : PanelContainer public sealed partial class PlayerTabEntry : PanelContainer
{ {
public NetEntity? PlayerEntity; [Dependency] private readonly IEntityManager _entMan = default!;
public PlayerTabEntry(PlayerInfo player, StyleBoxFlat styleBoxFlat) public PlayerTabEntry(
PlayerInfo player,
StyleBoxFlat styleBoxFlat,
AdminPlayerTabColorOption colorOption,
AdminPlayerTabRoleTypeOption roleSetting,
AdminPlayerTabSymbolOption symbolSetting)
{ {
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
var config = IoCManager.Resolve<IConfigurationManager>(); var roles = _entMan.System<SharedRoleSystem>();
UsernameLabel.Text = player.Username; UsernameLabel.Text = player.Username;
if (!player.Connected) if (!player.Connected)
UsernameLabel.StyleClasses.Add("Disabled"); UsernameLabel.StyleClasses.Add("Disabled");
JobLabel.Text = player.StartingJob; JobLabel.Text = player.StartingJob;
var separateAntagSymbols = config.GetCVar(CCVars.AdminPlayerlistSeparateSymbols);
var genericAntagSymbol = player.Antag ? Loc.GetString("player-tab-antag-prefix") : string.Empty; var colorAntags = false;
var roleSymbol = player.Antag ? player.RoleProto.Symbol : string.Empty; var colorRoles = false;
var symbol = separateAntagSymbols ? roleSymbol : genericAntagSymbol; switch (colorOption)
{
case AdminPlayerTabColorOption.Off:
break;
case AdminPlayerTabColorOption.Character:
colorAntags = true;
break;
case AdminPlayerTabColorOption.Roletype:
colorRoles = true;
break;
default:
case AdminPlayerTabColorOption.Both:
colorAntags = true;
colorRoles = true;
break;
}
var symbol = string.Empty;
switch (symbolSetting)
{
case AdminPlayerTabSymbolOption.Off:
break;
case AdminPlayerTabSymbolOption.Basic:
symbol = player.Antag ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
break;
default:
case AdminPlayerTabSymbolOption.Specific:
symbol = player.Antag ? player.RoleProto.Symbol : string.Empty;
break;
}
CharacterLabel.Text = Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", player.CharacterName)); CharacterLabel.Text = Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", player.CharacterName));
if (player.Antag && config.GetCVar(CCVars.AdminPlayerlistHighlightedCharacterColor)) if (player.Antag && colorAntags)
CharacterLabel.FontColorOverride = player.RoleProto.Color; CharacterLabel.FontColorOverride = player.RoleProto.Color;
if (player.IdentityName != player.CharacterName) if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]"; CharacterLabel.Text += $" [{player.IdentityName}]";
RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
if (config.GetCVar(CCVars.AdminPlayerlistRoleTypeColor)) var roletype = RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
var subtype = roles.GetRoleSubtypeLabel(player.RoleProto.Name, player.Subtype);
switch (roleSetting)
{
case AdminPlayerTabRoleTypeOption.RoleTypeSubtype:
RoleTypeLabel.Text = roletype != subtype
? roletype + " - " +subtype
: roletype;
break;
case AdminPlayerTabRoleTypeOption.SubtypeRoleType:
RoleTypeLabel.Text = roletype != subtype
? subtype + " - " + roletype
: roletype;
break;
case AdminPlayerTabRoleTypeOption.RoleType:
RoleTypeLabel.Text = roletype;
break;
default:
case AdminPlayerTabRoleTypeOption.Subtype:
RoleTypeLabel.Text = subtype;
break;
}
if (colorRoles)
RoleTypeLabel.FontColorOverride = player.RoleProto.Color; RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
BackgroundColorPanel.PanelOverride = styleBoxFlat; BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = player.PlaytimeString; OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity;
} }
} }

View File

@@ -0,0 +1,24 @@
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
public enum AdminPlayerTabColorOption
{
Off,
Character,
Roletype,
Both
}
public enum AdminPlayerTabRoleTypeOption
{
RoleType,
Subtype,
RoleTypeSubtype,
SubtypeRoleType
}
public enum AdminPlayerTabSymbolOption
{
Off,
Basic,
Specific
}

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Alert; using Content.Shared.Alert;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -15,6 +16,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
public event EventHandler? ClearAlerts; public event EventHandler? ClearAlerts;
public event EventHandler<IReadOnlyDictionary<AlertKey, AlertState>>? SyncAlerts; public event EventHandler<IReadOnlyDictionary<AlertKey, AlertState>>? SyncAlerts;
@@ -27,6 +29,12 @@ public sealed class ClientAlertsSystem : AlertsSystem
SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached); SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState); SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState);
} }
protected override void HandledAlert()
{
_ui.ClickSound();
}
protected override void LoadPrototypes() protected override void LoadPrototypes()
{ {
base.LoadPrototypes(); base.LoadPrototypes();
@@ -52,8 +60,24 @@ public sealed class ClientAlertsSystem : AlertsSystem
if (args.Current is not AlertComponentState cast) if (args.Current is not AlertComponentState cast)
return; return;
// Save all client-sided alerts to later put back in
var clientAlerts = new Dictionary<AlertKey, AlertState>();
foreach (var alert in alerts.Comp.Alerts)
{
if (alert.Key.AlertType != null && TryGet(alert.Key.AlertType.Value, out var alertProto))
{
if (alertProto.ClientHandled)
clientAlerts[alert.Key] = alert.Value;
}
}
alerts.Comp.Alerts = new(cast.Alerts); alerts.Comp.Alerts = new(cast.Alerts);
foreach (var alert in clientAlerts)
{
alerts.Comp.Alerts[alert.Key] = alert.Value;
}
UpdateHud(alerts); UpdateHud(alerts);
} }

View File

@@ -0,0 +1,29 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
namespace Content.Client.Atmos.EntitySystems;
public sealed class GasTankSystem : SharedGasTankSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasTankComponent, AfterAutoHandleStateEvent>(OnGasTankState);
}
private void OnGasTankState(Entity<GasTankComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
{
bui.Update<GasTankBoundUserInterfaceState>();
}
}
public override void UpdateUserInterface(Entity<GasTankComponent> ent)
{
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
{
bui.Update<GasTankBoundUserInterfaceState>();
}
}
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Binary.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Atmos.Piping.Binary.Systems;
public sealed class GasVolumePumpSystem : SharedGasVolumePumpSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasVolumePumpComponent, AfterAutoHandleStateEvent>(OnPumpState);
}
protected override void UpdateUi(Entity<GasVolumePumpComponent> entity)
{
if (_ui.TryGetOpenUi(entity.Owner, GasVolumePumpUiKey.Key, out var bui))
{
bui.Update();
}
}
private void OnPumpState(Entity<GasVolumePumpComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
}

View File

@@ -0,0 +1,32 @@
using Content.Client.Atmos.UI;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Piping.Unary.Systems;
using Content.Shared.NodeContainer;
namespace Content.Client.Atmos.Piping.Unary.Systems;
public sealed class GasCanisterSystem : SharedGasCanisterSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasCanisterComponent, AfterAutoHandleStateEvent>(OnGasState);
}
private void OnGasState(Entity<GasCanisterComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(ent.Owner, GasCanisterUiKey.Key, out var bui))
{
bui.Update<GasCanisterBoundUserInterfaceState>();
}
}
protected override void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null)
{
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(uid, GasCanisterUiKey.Key, out var bui))
{
bui.Update<GasCanisterBoundUserInterfaceState>();
}
}
}

View File

@@ -1,4 +1,7 @@
using Content.Shared.Atmos.Piping.Binary.Components; using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.IdentityManagement;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
@@ -32,22 +35,22 @@ namespace Content.Client.Atmos.UI
private void OnTankEjectPressed() private void OnTankEjectPressed()
{ {
SendMessage(new GasCanisterHoldingTankEjectMessage()); SendPredictedMessage(new GasCanisterHoldingTankEjectMessage());
} }
private void OnReleasePressureSet(float value) private void OnReleasePressureSet(float value)
{ {
SendMessage(new GasCanisterChangeReleasePressureMessage(value)); SendPredictedMessage(new GasCanisterChangeReleasePressureMessage(value));
} }
private void OnReleaseValveOpenPressed() private void OnReleaseValveOpenPressed()
{ {
SendMessage(new GasCanisterChangeReleaseValveMessage(true)); SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(true));
} }
private void OnReleaseValveClosePressed() private void OnReleaseValveClosePressed()
{ {
SendMessage(new GasCanisterChangeReleaseValveMessage(false)); SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(false));
} }
/// <summary> /// <summary>
@@ -57,17 +60,21 @@ namespace Content.Client.Atmos.UI
protected override void UpdateState(BoundUserInterfaceState state) protected override void UpdateState(BoundUserInterfaceState state)
{ {
base.UpdateState(state); base.UpdateState(state);
if (_window == null || state is not GasCanisterBoundUserInterfaceState cast) if (_window == null || state is not GasCanisterBoundUserInterfaceState cast || !EntMan.TryGetComponent(Owner, out GasCanisterComponent? component))
return; return;
_window.SetCanisterLabel(cast.CanisterLabel); var canisterLabel = Identity.Name(Owner, EntMan);
var tankLabel = component.GasTankSlot.Item != null ? Identity.Name(component.GasTankSlot.Item.Value, EntMan) : null;
_window.SetCanisterLabel(canisterLabel);
_window.SetCanisterPressure(cast.CanisterPressure); _window.SetCanisterPressure(cast.CanisterPressure);
_window.SetPortStatus(cast.PortStatus); _window.SetPortStatus(cast.PortStatus);
_window.SetTankLabel(cast.TankLabel);
_window.SetTankLabel(tankLabel);
_window.SetTankPressure(cast.TankPressure); _window.SetTankPressure(cast.TankPressure);
_window.SetReleasePressureRange(cast.ReleasePressureMin, cast.ReleasePressureMax); _window.SetReleasePressureRange(component.MinReleasePressure, component.MaxReleasePressure);
_window.SetReleasePressure(cast.ReleasePressure); _window.SetReleasePressure(component.ReleasePressure);
_window.SetReleaseValve(cast.ReleaseValve); _window.SetReleaseValve(component.ReleaseValve);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)

View File

@@ -13,9 +13,6 @@ namespace Content.Client.Atmos.UI;
[UsedImplicitly] [UsedImplicitly]
public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{ {
[ViewVariables]
private const float MaxPressure = Atmospherics.MaxOutputPressure;
[ViewVariables] [ViewVariables]
private GasPressurePumpWindow? _window; private GasPressurePumpWindow? _window;

View File

@@ -1,8 +1,7 @@
using Content.Shared.Atmos; using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Binary.Components; using Content.Shared.IdentityManagement;
using Content.Shared.Localizations; using Content.Shared.Localizations;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
namespace Content.Client.Atmos.UI namespace Content.Client.Atmos.UI
@@ -14,7 +13,7 @@ namespace Content.Client.Atmos.UI
public sealed class GasVolumePumpBoundUserInterface : BoundUserInterface public sealed class GasVolumePumpBoundUserInterface : BoundUserInterface
{ {
[ViewVariables] [ViewVariables]
private const float MaxTransferRate = Atmospherics.MaxTransferRate; private float _maxTransferRate;
[ViewVariables] [ViewVariables]
private GasVolumePumpWindow? _window; private GasVolumePumpWindow? _window;
@@ -29,38 +28,41 @@ namespace Content.Client.Atmos.UI
_window = this.CreateWindow<GasVolumePumpWindow>(); _window = this.CreateWindow<GasVolumePumpWindow>();
if (EntMan.TryGetComponent(Owner, out GasVolumePumpComponent? pump))
{
_maxTransferRate = pump.MaxTransferRate;
}
_window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed; _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
_window.PumpTransferRateChanged += OnPumpTransferRatePressed; _window.PumpTransferRateChanged += OnPumpTransferRatePressed;
Update();
} }
private void OnToggleStatusButtonPressed() private void OnToggleStatusButtonPressed()
{ {
if (_window is null) return; if (_window is null) return;
SendMessage(new GasVolumePumpToggleStatusMessage(_window.PumpStatus));
SendPredictedMessage(new GasVolumePumpToggleStatusMessage(_window.PumpStatus));
} }
private void OnPumpTransferRatePressed(string value) private void OnPumpTransferRatePressed(string value)
{ {
var rate = UserInputParser.TryFloat(value, out var parsed) ? parsed : 0f; var rate = UserInputParser.TryFloat(value, out var parsed) ? parsed : 0f;
if (rate > MaxTransferRate) rate = Math.Clamp(rate, 0f, _maxTransferRate);
rate = MaxTransferRate;
SendMessage(new GasVolumePumpChangeTransferRateMessage(rate)); SendPredictedMessage(new GasVolumePumpChangeTransferRateMessage(rate));
} }
/// <summary> public override void Update()
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
{ {
base.UpdateState(state); base.Update();
if (_window == null || state is not GasVolumePumpBoundUserInterfaceState cast)
if (_window is null || !EntMan.TryGetComponent(Owner, out GasVolumePumpComponent? pump))
return; return;
_window.Title = cast.PumpLabel; _window.Title = Identity.Name(Owner, EntMan);
_window.SetPumpStatus(cast.Enabled); _window.SetPumpStatus(pump.Enabled);
_window.SetTransferRate(cast.TransferRate); _window.SetTransferRate(pump.TransferRate);
} }
} }
} }

View File

@@ -1,5 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="200 120" Title="Volume Pump"> MinSize="200 120" Title="Volume Pump">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10"> <BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True"> <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
@@ -19,4 +20,4 @@
<Button Name="SetTransferRateButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" HorizontalAlignment="Right" Disabled="True"/> <Button Name="SetTransferRateButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" HorizontalAlignment="Right" Disabled="True"/>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</DefaultWindow> </controls:FancyWindow>

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Content.Client.Atmos.EntitySystems; using Content.Client.Atmos.EntitySystems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Prototypes; using Content.Shared.Atmos.Prototypes;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
@@ -16,7 +17,7 @@ namespace Content.Client.Atmos.UI
/// Client-side UI used to control a gas volume pump. /// Client-side UI used to control a gas volume pump.
/// </summary> /// </summary>
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class GasVolumePumpWindow : DefaultWindow public sealed partial class GasVolumePumpWindow : FancyWindow
{ {
public bool PumpStatus = true; public bool PumpStatus = true;

View File

@@ -0,0 +1,24 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
namespace Content.Client.Body.Systems;
public sealed class InternalsSystem : SharedInternalsSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InternalsComponent, AfterAutoHandleStateEvent>(OnInternalsAfterState);
}
private void OnInternalsAfterState(Entity<InternalsComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.GasTankEntity != null && _ui.TryGetOpenUi(ent.Comp.GasTankEntity.Value, SharedGasTankUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Cargo; using Content.Shared.Cargo;
using Content.Client.Cargo.UI; using Content.Client.Cargo.UI;
using Content.Shared.Cargo.BUI; using Content.Shared.Cargo.BUI;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Events; using Content.Shared.Cargo.Events;
using Content.Shared.Cargo.Prototypes; using Content.Shared.Cargo.Prototypes;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
@@ -14,6 +15,8 @@ namespace Content.Client.Cargo.BUI
{ {
public sealed class CargoOrderConsoleBoundUserInterface : BoundUserInterface public sealed class CargoOrderConsoleBoundUserInterface : BoundUserInterface
{ {
private readonly SharedCargoSystem _cargoSystem;
[ViewVariables] [ViewVariables]
private CargoConsoleMenu? _menu; private CargoConsoleMenu? _menu;
@@ -43,6 +46,7 @@ namespace Content.Client.Cargo.BUI
public CargoOrderConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) public CargoOrderConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{ {
_cargoSystem = EntMan.System<SharedCargoSystem>();
} }
protected override void Open() protected override void Open()
@@ -57,7 +61,7 @@ namespace Content.Client.Cargo.BUI
string orderRequester; string orderRequester;
if (EntMan.TryGetComponent<MetaDataComponent>(localPlayer, out var metadata)) if (EntMan.EntityExists(localPlayer))
orderRequester = Identity.Name(localPlayer.Value, EntMan); orderRequester = Identity.Name(localPlayer.Value, EntMan);
else else
orderRequester = string.Empty; orderRequester = string.Empty;
@@ -96,41 +100,54 @@ namespace Content.Client.Cargo.BUI
} }
}; };
_menu.OnAccountAction += (account, amount) =>
{
SendMessage(new CargoConsoleWithdrawFundsMessage(account, amount));
};
_menu.OnToggleUnboundedLimit += _ =>
{
SendMessage(new CargoConsoleToggleLimitMessage());
};
_menu.OpenCentered(); _menu.OpenCentered();
} }
private void Populate(List<CargoOrderData> orders) private void Populate(List<CargoOrderData> orders)
{ {
if (_menu == null) return; if (_menu == null)
return;
_menu.PopulateProducts(); _menu.PopulateProducts();
_menu.PopulateCategories(); _menu.PopulateCategories();
_menu.PopulateOrders(orders); _menu.PopulateOrders(orders);
_menu.PopulateAccountActions();
} }
protected override void UpdateState(BoundUserInterfaceState state) protected override void UpdateState(BoundUserInterfaceState state)
{ {
base.UpdateState(state); base.UpdateState(state);
if (state is not CargoConsoleInterfaceState cState) if (state is not CargoConsoleInterfaceState cState || !EntMan.TryGetComponent<CargoOrderConsoleComponent>(Owner, out var orderConsole))
return; return;
var station = EntMan.GetEntity(cState.Station);
OrderCapacity = cState.Capacity; OrderCapacity = cState.Capacity;
OrderCount = cState.Count; OrderCount = cState.Count;
BankBalance = cState.Balance; BankBalance = _cargoSystem.GetBalanceFromAccount(station, orderConsole.Account);
AccountName = cState.Name; AccountName = cState.Name;
_menu?.UpdateStation(station);
Populate(cState.Orders); Populate(cState.Orders);
_menu?.UpdateCargoCapacity(OrderCount, OrderCapacity);
_menu?.UpdateBankData(AccountName, BankBalance);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
if (!disposing) return; if (!disposing)
return;
_menu?.Dispose(); _menu?.Dispose();
_orderMenu?.Dispose(); _orderMenu?.Dispose();
@@ -170,8 +187,6 @@ namespace Content.Client.Cargo.BUI
return; return;
SendMessage(new CargoConsoleApproveOrderMessage(row.Order.OrderId)); SendMessage(new CargoConsoleApproveOrderMessage(row.Order.OrderId));
// Most of the UI isn't predicted anyway so.
// _menu?.UpdateCargoCapacity(OrderCount + row.Order.Amount, OrderCapacity);
} }
} }
} }

View File

@@ -0,0 +1,35 @@
using Content.Client.Cargo.UI;
using Content.Shared.Cargo.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Cargo.BUI;
[UsedImplicitly]
public sealed class FundingAllocationConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private FundingAllocationMenu? _menu;
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<FundingAllocationMenu>();
_menu.OnSavePressed += (dicts, primary, lockbox) =>
{
SendMessage(new SetFundingAllocationBuiMessage(dicts, primary, lockbox));
};
}
protected override void UpdateState(BoundUserInterfaceState message)
{
base.UpdateState(message);
if (message is not FundingAllocationConsoleBuiState state)
return;
_menu?.Update(state);
}
}

View File

@@ -75,10 +75,9 @@ public sealed partial class CargoSystem
switch (state) switch (state)
{ {
case CargoTelepadState.Teleporting: case CargoTelepadState.Teleporting:
if (_player.HasRunningAnimation(uid, TelepadBeamKey)) _player.Stop((uid, player), TelepadIdleKey);
return; if (!_player.HasRunningAnimation(uid, TelepadBeamKey))
_player.Stop(uid, player, TelepadIdleKey); _player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
break; break;
case CargoTelepadState.Unpowered: case CargoTelepadState.Unpowered:
sprite.LayerSetVisible(CargoTelepadLayers.Beam, false); sprite.LayerSetVisible(CargoTelepadLayers.Beam, false);

View File

@@ -3,66 +3,84 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="600 600" SetSize="600 600"
MinSize="600 600"> MinSize="600 600">
<BoxContainer Orientation="Vertical" Margin="5 0 5 0"> <BoxContainer Orientation="Vertical" Margin="15 5 15 10">
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'cargo-console-menu-account-name-label'}" <Label Text="{Loc 'cargo-console-menu-account-name-label'}"
StyleClasses="LabelKeyText" /> StyleClasses="LabelKeyText" />
<Label Name="AccountNameLabel" <RichTextLabel Name="AccountNameLabel"
Text="{Loc 'cargo-console-menu-account-name-none-text'}" /> Text="{Loc 'cargo-console-menu-account-name-none-text'}" />
</BoxContainer> </BoxContainer>
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'cargo-console-menu-points-label'}" <Label Text="{Loc 'cargo-console-menu-points-label'}"
StyleClasses="LabelKeyText" /> StyleClasses="LabelKeyText" />
<Label Name="PointsLabel" <RichTextLabel Name="PointsLabel"
Text="$0" /> Text="$0" />
</BoxContainer> </BoxContainer>
<BoxContainer Orientation="Horizontal"> <Control MinHeight="10"/>
<Label Text="{Loc 'cargo-console-menu-order-capacity-label'}" <TabContainer Name="TabContainer" VerticalExpand="True">
StyleClasses="LabelKeyText" /> <BoxContainer Orientation="Vertical" VerticalExpand="True">
<Label Name="ShuttleCapacityLabel" <BoxContainer Orientation="Horizontal">
Text="0/20" /> <OptionButton Name="Categories"
</BoxContainer> Prefix="{Loc 'cargo-console-menu-categories-label'}"
<BoxContainer Orientation="Horizontal"> HorizontalExpand="True" />
<OptionButton Name="Categories" <LineEdit Name="SearchBar"
Prefix="{Loc 'cargo-console-menu-categories-label'}" PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
HorizontalExpand="True" /> HorizontalExpand="True" />
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
HorizontalExpand="True" />
</BoxContainer>
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
SizeFlagsStretchRatio="6">
<BoxContainer Name="Products"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Products get added here by code -->
</BoxContainer>
</ScrollContainer>
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="6">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000" />
</PanelContainer.PanelOverride>
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'cargo-console-menu-requests-label'}" />
<BoxContainer Name="Requests"
Orientation="Vertical"
VerticalExpand="True">
<!-- Requests are added here by code -->
</BoxContainer>
<Label Text="{Loc 'cargo-console-menu-orders-label'}" />
<BoxContainer Name="Orders"
Orientation="Vertical"
StyleClasses="transparentItemList"
VerticalExpand="True">
<!-- Orders are added here by code -->
</BoxContainer>
</BoxContainer> </BoxContainer>
</ScrollContainer> <Control MinHeight="5"/>
</PanelContainer> <ScrollContainer HorizontalExpand="True"
<TextureButton VerticalExpand="True" /> VerticalExpand="True"
SizeFlagsStretchRatio="2">
<BoxContainer Name="Products"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Products get added here by code -->
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5" Name="OrdersSpacer"/>
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="1"
Name="Orders">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000" />
</PanelContainer.PanelOverride>
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical" Margin="5">
<Label Text="{Loc 'cargo-console-menu-requests-label'}" />
<BoxContainer Name="Requests"
Orientation="Vertical"
VerticalExpand="True">
<!-- Requests are added here by code -->
</BoxContainer>
</BoxContainer>
</ScrollContainer>
</PanelContainer>
</BoxContainer>
<!-- Funds tab -->
<BoxContainer Orientation="Vertical" Margin="15">
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
<RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
<OptionButton Name="ActionOptions"/>
</BoxContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
<SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
</BoxContainer>
<Control MinHeight="15"/>
<BoxContainer HorizontalAlignment="Center">
<Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
</BoxContainer>
<Control VerticalExpand="True"/>
<BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
<Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
</BoxContainer>
</BoxContainer>
</TabContainer>
</BoxContainer> </BoxContainer>
</controls:FancyWindow> </controls:FancyWindow>

View File

@@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using Content.Client.Cargo.Systems;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Shared.Cargo; using Content.Shared.Cargo;
using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Components;
@@ -8,6 +9,7 @@ using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using static Robust.Client.UserInterface.Controls.BaseButton; using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Cargo.UI namespace Content.Client.Cargo.UI
@@ -15,30 +17,83 @@ namespace Content.Client.Cargo.UI
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class CargoConsoleMenu : FancyWindow public sealed partial class CargoConsoleMenu : FancyWindow
{ {
private IEntityManager _entityManager; [Dependency] private readonly IGameTiming _timing = default!;
private IPrototypeManager _protoManager;
private SpriteSystem _spriteSystem; private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _protoManager;
private readonly CargoSystem _cargoSystem;
private readonly SpriteSystem _spriteSystem;
private EntityUid _owner; private EntityUid _owner;
private EntityUid? _station;
private readonly EntityQuery<CargoOrderConsoleComponent> _orderConsoleQuery;
private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
public event Action<ButtonEventArgs>? OnItemSelected; public event Action<ButtonEventArgs>? OnItemSelected;
public event Action<ButtonEventArgs>? OnOrderApproved; public event Action<ButtonEventArgs>? OnOrderApproved;
public event Action<ButtonEventArgs>? OnOrderCanceled; public event Action<ButtonEventArgs>? OnOrderCanceled;
public event Action<ProtoId<CargoAccountPrototype>?, int>? OnAccountAction;
public event Action<ButtonEventArgs>? OnToggleUnboundedLimit;
private readonly List<string> _categoryStrings = new(); private readonly List<string> _categoryStrings = new();
private string? _category; private string? _category;
public CargoConsoleMenu(EntityUid owner, IEntityManager entMan, IPrototypeManager protoManager, SpriteSystem spriteSystem) public CargoConsoleMenu(EntityUid owner, IEntityManager entMan, IPrototypeManager protoManager, SpriteSystem spriteSystem)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_entityManager = entMan; _entityManager = entMan;
_protoManager = protoManager; _protoManager = protoManager;
_cargoSystem = entMan.System<CargoSystem>();
_spriteSystem = spriteSystem; _spriteSystem = spriteSystem;
_owner = owner; _owner = owner;
Title = Loc.GetString("cargo-console-menu-title"); _orderConsoleQuery = _entityManager.GetEntityQuery<CargoOrderConsoleComponent>();
_bankQuery = _entityManager.GetEntityQuery<StationBankAccountComponent>();
Title = entMan.GetComponent<MetaDataComponent>(owner).EntityName;
SearchBar.OnTextChanged += OnSearchBarTextChanged; SearchBar.OnTextChanged += OnSearchBarTextChanged;
Categories.OnItemSelected += OnCategoryItemSelected; Categories.OnItemSelected += OnCategoryItemSelected;
if (entMan.TryGetComponent<CargoOrderConsoleComponent>(owner, out var orderConsole))
{
var accountProto = _protoManager.Index(orderConsole.Account);
AccountNameLabel.Text = Loc.GetString("cargo-console-menu-account-name-format",
("color", accountProto.Color),
("name", Loc.GetString(accountProto.Name)),
("code", Loc.GetString(accountProto.Code)));
}
TabContainer.SetTabTitle(0, Loc.GetString("cargo-console-menu-tab-title-orders"));
TabContainer.SetTabTitle(1, Loc.GetString("cargo-console-menu-tab-title-funds"));
ActionOptions.OnItemSelected += idx =>
{
ActionOptions.SelectId(idx.Id);
};
TransferSpinBox.IsValid = val =>
{
if (!_entityManager.TryGetComponent<CargoOrderConsoleComponent>(owner, out var console) ||
!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
return true;
return val >= 0 && val <= (int) (console.TransferLimit * bank.Accounts[console.Account]);
};
AccountActionButton.OnPressed += _ =>
{
var account = (ProtoId<CargoAccountPrototype>?) ActionOptions.SelectedMetadata;
OnAccountAction?.Invoke(account, TransferSpinBox.Value);
};
AccountLimitToggleButton.OnPressed += a =>
{
OnToggleUnboundedLimit?.Invoke(a);
};
} }
private void OnCategoryItemSelected(OptionButton.ItemSelectedEventArgs args) private void OnCategoryItemSelected(OptionButton.ItemSelectedEventArgs args)
@@ -144,13 +199,16 @@ namespace Content.Client.Cargo.UI
/// </summary> /// </summary>
public void PopulateOrders(IEnumerable<CargoOrderData> orders) public void PopulateOrders(IEnumerable<CargoOrderData> orders)
{ {
Orders.DisposeAllChildren();
Requests.DisposeAllChildren(); Requests.DisposeAllChildren();
foreach (var order in orders) foreach (var order in orders)
{ {
if (order.Approved)
continue;
var product = _protoManager.Index<EntityPrototype>(order.ProductId); var product = _protoManager.Index<EntityPrototype>(order.ProductId);
var productName = product.Name; var productName = product.Name;
var account = _protoManager.Index(order.Account);
var row = new CargoOrderRow var row = new CargoOrderRow
{ {
@@ -162,37 +220,74 @@ namespace Content.Client.Cargo.UI
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text", "cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
("productName", productName), ("productName", productName),
("orderAmount", order.OrderQuantity), ("orderAmount", order.OrderQuantity),
("orderRequester", order.Requester)) ("orderRequester", order.Requester),
("accountColor", account.Color),
("account", Loc.GetString(account.Code)))
}, },
Description = {Text = Loc.GetString("cargo-console-menu-order-reason-description", Description =
("reason", order.Reason))} {
Text = Loc.GetString("cargo-console-menu-order-reason-description",
("reason", order.Reason))
}
}; };
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); }; row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
if (order.Approved)
{ // TODO: Disable based on access.
row.Approve.Visible = false; row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
row.Cancel.Visible = false; Requests.AddChild(row);
Orders.AddChild(row);
}
else
{
// TODO: Disable based on access.
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
Requests.AddChild(row);
}
} }
} }
public void UpdateCargoCapacity(int count, int capacity) public void PopulateAccountActions()
{ {
// TODO: Rename + Loc. if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank) ||
ShuttleCapacityLabel.Text = $"{count}/{capacity}"; !_entityManager.TryGetComponent<CargoOrderConsoleComponent>(_owner, out var console))
return;
var i = 0;
ActionOptions.Clear();
ActionOptions.AddItem(Loc.GetString("cargo-console-menu-account-action-option-withdraw"), i);
i++;
foreach (var account in bank.Accounts.Keys)
{
if (account == console.Account)
continue;
var accountProto = _protoManager.Index(account);
ActionOptions.AddItem(Loc.GetString("cargo-console-menu-account-action-option-transfer",
("code", Loc.GetString(accountProto.Code))),
i);
ActionOptions.SetItemMetadata(i, account);
i++;
}
} }
public void UpdateBankData(string name, int points) public void UpdateStation(EntityUid station)
{ {
AccountNameLabel.Text = name; _station = station;
PointsLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", points.ToString())); }
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_bankQuery.TryComp(_station, out var bankAccount) ||
!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
{
return;
}
var balance = _cargoSystem.GetBalanceFromAccount((_station.Value, bankAccount), orderConsole.Account);
PointsLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", balance));
TransferLimitLabel.Text = Loc.GetString("cargo-console-menu-account-action-transfer-limit",
("limit", (int) (balance * orderConsole.TransferLimit)));
UnlimitedNotifier.Visible = orderConsole.TransferUnbounded;
AccountActionButton.Disabled = TransferSpinBox.Value <= 0 ||
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
_timing.CurTime < orderConsole.NextAccountActionTime;
OrdersSpacer.Visible = !orderConsole.SlipPrinter;
Orders.Visible = !orderConsole.SlipPrinter;
} }
} }
} }

View File

@@ -1,19 +1,20 @@
<PanelContainer xmlns="https://spacestation14.io" <PanelContainer xmlns="https://spacestation14.io"
HorizontalExpand="True"> HorizontalExpand="True"
Margin="0 1">
<BoxContainer Orientation="Horizontal" <BoxContainer Orientation="Horizontal"
HorizontalExpand="True"> HorizontalExpand="True">
<TextureRect Name="Icon" <TextureRect Name="Icon"
Access="Public" Access="Public"
MinSize="32 32" MinSize="32 32"
RectClipContent="True" /> RectClipContent="True" />
<Control MinWidth="5"/>
<BoxContainer Orientation="Vertical" <BoxContainer Orientation="Vertical"
HorizontalExpand="True" HorizontalExpand="True"
VerticalExpand="True"> VerticalExpand="True">
<Label Name="ProductName" <RichTextLabel Name="ProductName"
Access="Public" Access="Public"
HorizontalExpand="True" HorizontalExpand="True"
StyleClasses="LabelSubText" StyleClasses="LabelSubText" />
ClipText="True" />
<Label Name="Description" <Label Name="Description"
Access="Public" Access="Public"
HorizontalExpand="True" HorizontalExpand="True"
@@ -23,10 +24,10 @@
<Button Name="Approve" <Button Name="Approve"
Access="Public" Access="Public"
Text="{Loc 'cargo-console-menu-cargo-order-row-approve-button'}" Text="{Loc 'cargo-console-menu-cargo-order-row-approve-button'}"
StyleClasses="LabelSubText" /> StyleClasses="OpenRight" />
<Button Name="Cancel" <Button Name="Cancel"
Access="Public" Access="Public"
Text="{Loc 'cargo-console-menu-cargo-order-row-cancel-button'}" Text="{Loc 'cargo-console-menu-cargo-order-row-cancel-button'}"
StyleClasses="LabelSubText" /> StyleClasses="OpenLeft" />
</BoxContainer> </BoxContainer>
</PanelContainer> </PanelContainer>

View File

@@ -4,7 +4,8 @@
ToolTip="" ToolTip=""
Access="Public" Access="Public"
HorizontalExpand="True" HorizontalExpand="True"
VerticalExpand="True" /> VerticalExpand="True"
StyleClasses="OpenBoth"/>
<BoxContainer Orientation="Horizontal" <BoxContainer Orientation="Horizontal"
HorizontalExpand="True"> HorizontalExpand="True">
<TextureRect Name="Icon" <TextureRect Name="Icon"
@@ -18,7 +19,8 @@
<Label Name="PointCost" <Label Name="PointCost"
Access="Public" Access="Public"
MinSize="52 32" MinSize="52 32"
Align="Right" /> Align="Right"
Margin="0 0 5 0"/>
</PanelContainer> </PanelContainer>
</BoxContainer> </BoxContainer>
</PanelContainer> </PanelContainer>

View File

@@ -36,6 +36,7 @@ namespace Content.Client.Cargo.UI
{ {
var product = protoManager.Index<EntityPrototype>(order.ProductId); var product = protoManager.Index<EntityPrototype>(order.ProductId);
var productName = product.Name; var productName = product.Name;
var account = protoManager.Index(order.Account);
var row = new CargoOrderRow var row = new CargoOrderRow
{ {
@@ -47,7 +48,9 @@ namespace Content.Client.Cargo.UI
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text", "cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
("productName", productName), ("productName", productName),
("orderAmount", order.OrderQuantity - order.NumDispatched), ("orderAmount", order.OrderQuantity - order.NumDispatched),
("orderRequester", order.Requester)) ("orderRequester", order.Requester),
("accountColor", account.Color),
("account", Loc.GetString(account.Code)))
}, },
Description = {Text = Loc.GetString("cargo-console-menu-order-reason-description", Description = {Text = Loc.GetString("cargo-console-menu-order-reason-description",
("reason", order.Reason))} ("reason", order.Reason))}

View File

@@ -0,0 +1,32 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'cargo-funding-alloc-console-menu-title'}">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="10 5 10 10">
<controls:TableContainer Columns="2" HorizontalExpand="True" VerticalExpand="True">
<RichTextLabel Name="PrimaryCutLabel" Text="{Loc 'cargo-funding-alloc-console-label-primary-cut'}"/>
<SpinBox Name="PrimaryCut"/>
<RichTextLabel Name="LockboxCutLabel" Text="{Loc 'cargo-funding-alloc-console-label-lockbox-cut'}"/>
<SpinBox Name="LockboxCut"/>
</controls:TableContainer>
<Label Name="HelpLabel" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="0 10"/>
<PanelContainer VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Top" MaxHeight="250">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E"/>
</PanelContainer.PanelOverride>
<controls:TableContainer Name="EntriesContainer" Columns="4" HorizontalExpand="True" VerticalExpand="True" Margin="5 0">
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-account'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-code'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-balance'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-cut'}" HorizontalAlignment="Center"/>
</controls:TableContainer>
</PanelContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0">
<Button Name="SaveButton" Text="{Loc 'cargo-funding-alloc-console-button-save'}" Disabled="True"/>
<RichTextLabel Name="SaveAlertLabel" HorizontalExpand="True" HorizontalAlignment="Right" Visible="False"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,229 @@
using System.Collections.Generic;
using System.Linq;
using Content.Client.Message;
using Content.Client.UserInterface.Controls;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Cargo.UI;
[GenerateTypedNameReferences]
public sealed partial class FundingAllocationMenu : FancyWindow
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
public event Action<Dictionary<ProtoId<CargoAccountPrototype>, int>, double, double>? OnSavePressed;
private EntityUid? _station;
private bool _allowPrimaryAccountAllocation;
private bool _allowPrimaryCutAdjustment;
private bool _lockboxCutEnabled;
private double _primaryCut;
private double _lockboxCut;
private readonly HashSet<Control> _addedControls = new();
private readonly List<SpinBox> _spinBoxes = new();
private readonly Dictionary<ProtoId<CargoAccountPrototype>, RichTextLabel> _balanceLabels = new();
public FundingAllocationMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_bankQuery = _entityManager.GetEntityQuery<StationBankAccountComponent>();
PrimaryCut.ValueChanged += args =>
{
_primaryCut = (double)args.Value / 100.0;
UpdateButtonDisabled();
};
LockboxCut.ValueChanged += args =>
{
_lockboxCut = 1.0 - (double)args.Value / 100.0;
UpdateButtonDisabled();
};
SaveButton.OnPressed += _ =>
{
if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
return;
var accounts = EditableAccounts(bank).OrderBy(p => p.Key).Select(p => p.Key).ToList();
var dicts = new Dictionary<ProtoId<CargoAccountPrototype>, int>();
for (var i = 0; i< accounts.Count; i++)
{
dicts.Add(accounts[i], _spinBoxes[i].Value);
}
OnSavePressed?.Invoke(dicts, _primaryCut, _lockboxCut);
SaveButton.Disabled = true;
};
_cfg.OnValueChanged(CCVars.AllowPrimaryAccountAllocation, enabled => { _allowPrimaryAccountAllocation = enabled; }, true);
_cfg.OnValueChanged(CCVars.AllowPrimaryCutAdjustment, enabled => { _allowPrimaryCutAdjustment = enabled; }, true);
_cfg.OnValueChanged(CCVars.LockboxCutEnabled, enabled => { _lockboxCutEnabled = enabled; }, true);
BuildEntries();
}
private IEnumerable<KeyValuePair<ProtoId<CargoAccountPrototype>, int>> EditableAccounts(StationBankAccountComponent bank)
{
foreach (var kvp in bank.Accounts)
{
if (_allowPrimaryAccountAllocation || kvp.Key != bank.PrimaryAccount)
{
yield return kvp;
}
}
}
private void BuildEntries()
{
if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
return;
if (_allowPrimaryCutAdjustment)
{
HelpLabel.Text = Loc.GetString("cargo-funding-alloc-console-label-help-adjustible");
}
else
{
HelpLabel.Text = Loc.GetString("cargo-funding-alloc-console-label-help-non-adjustible",
("percent", (int) (bank.PrimaryCut * 100)));
}
foreach (var ctrl in _addedControls)
{
ctrl.Orphan();
}
_addedControls.Clear();
_spinBoxes.Clear();
_balanceLabels.Clear();
_primaryCut = bank.PrimaryCut;
_lockboxCut = bank.LockboxCut;
LockboxCut.OverrideValue(100 - (int)(_lockboxCut * 100));
PrimaryCut.OverrideValue((int)(_primaryCut * 100));
LockboxCut.IsValid = val => val is >= 0 and <= 100;
PrimaryCut.IsValid = val => val is >= 0 and <= 100;
LockboxCut.Visible = _lockboxCutEnabled;
LockboxCutLabel.Visible = _lockboxCutEnabled;
PrimaryCut.Visible = _allowPrimaryCutAdjustment;
PrimaryCutLabel.Visible = _allowPrimaryCutAdjustment;
var accounts = EditableAccounts(bank).OrderBy(p => p.Key);
foreach (var (account, balance) in accounts)
{
var accountProto = _prototypeManager.Index(account);
var accountNameLabel = new RichTextLabel
{
Modulate = accountProto.Color,
Margin = new Thickness(0, 0, 10, 0)
};
accountNameLabel.SetMarkup($"[bold]{Loc.GetString(accountProto.Name)}[/bold]");
EntriesContainer.AddChild(accountNameLabel);
var codeLabel = new RichTextLabel
{
Text = $"[font=\"Monospace\"]{Loc.GetString(accountProto.Code)}[/font]",
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(5, 0),
};
EntriesContainer.AddChild(codeLabel);
var balanceLabel = new RichTextLabel
{
Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", balance)),
HorizontalExpand = true,
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(5, 0),
};
EntriesContainer.AddChild(balanceLabel);
var box = new SpinBox
{
HorizontalAlignment = HAlignment.Center,
HorizontalExpand = true,
Value = (int) (bank.RevenueDistribution[account] * 100),
IsValid = val => val is >= 0 and <= 100,
};
box.ValueChanged += _ => UpdateButtonDisabled();
EntriesContainer.AddChild(box);
_spinBoxes.Add(box);
_balanceLabels.Add(account, balanceLabel);
_addedControls.Add(accountNameLabel);
_addedControls.Add(codeLabel);
_addedControls.Add(balanceLabel);
_addedControls.Add(box);
}
}
private void UpdateButtonDisabled()
{
if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
return;
var sum = _spinBoxes.Sum(s => s.Value);
var incorrectSum = sum != 100;
var differs = false;
var accounts = EditableAccounts(bank).OrderBy(p => p.Key).Select(p => p.Key).ToList();
for (var i = 0; i < accounts.Count; i++)
{
var percent = _spinBoxes[i].Value;
if (percent != (int) Math.Round(bank.RevenueDistribution[accounts[i]] * 100))
{
differs = true;
break;
}
}
differs = differs || _primaryCut != bank.PrimaryCut || _lockboxCut != bank.LockboxCut;
SaveButton.Disabled = !differs || incorrectSum;
var diff = sum - 100;
SaveAlertLabel.Visible = incorrectSum;
SaveAlertLabel.SetMarkup(Loc.GetString("cargo-funding-alloc-console-label-save-fail",
("pos", Math.Sign(diff)),
("val", Math.Abs(diff))));
}
public void Update(FundingAllocationConsoleBuiState state)
{
_station = _entityManager.GetEntity(state.Station);
BuildEntries();
UpdateButtonDisabled();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_bankQuery.TryComp(_station, out var bank))
return;
foreach (var (account, label) in _balanceLabels)
{
label.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", bank.Accounts[account]));
}
}
}

View File

@@ -90,10 +90,12 @@ public sealed partial class NanoTaskItemPopup : DefaultWindow
{ {
if (item is NanoTaskItem task) if (item is NanoTaskItem task)
{ {
var button = task.Priority switch { var button = task.Priority switch
{
NanoTaskPriority.High => HighButton, NanoTaskPriority.High => HighButton,
NanoTaskPriority.Medium => MediumButton, NanoTaskPriority.Medium => MediumButton,
NanoTaskPriority.Low => LowButton, NanoTaskPriority.Low => LowButton,
_ => throw new ArgumentException("Invalid priority"),
}; };
button.Pressed = true; button.Pressed = true;
DescriptionInput.Text = task.Description; DescriptionInput.Text = task.Description;

View File

@@ -38,10 +38,12 @@ public sealed partial class NanoTaskUiFragment : BoxContainer
foreach (var task in tasks) foreach (var task in tasks)
{ {
var container = task.Data.Priority switch { var container = task.Data.Priority switch
{
NanoTaskPriority.High => HighContainer, NanoTaskPriority.High => HighContainer,
NanoTaskPriority.Medium => MediumContainer, NanoTaskPriority.Medium => MediumContainer,
NanoTaskPriority.Low => LowContainer, NanoTaskPriority.Low => LowContainer,
_ => throw new ArgumentException("Invalid priority"),
}; };
var control = new NanoTaskItemControl(task); var control = new NanoTaskItemControl(task);
container.AddChild(control); container.AddChild(control);

View File

@@ -0,0 +1,52 @@
using Content.Client.Actions;
using Content.Shared.Actions;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
namespace Content.Client.Charges;
public sealed class ChargesSystem : SharedChargesSystem
{
[Dependency] private readonly ActionsSystem _actions = default!;
private Dictionary<EntityUid, int> _lastCharges = new();
private Dictionary<EntityUid, int> _tempLastCharges = new();
public override void Update(float frameTime)
{
// Technically this should probably be in frameupdate but no one will ever notice a tick of delay on this.
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
// Update recharging actions. Server doesn't actually care about this and it's a waste of performance, actions are immediate.
var query = AllEntityQuery<AutoRechargeComponent, LimitedChargesComponent>();
while (query.MoveNext(out var uid, out var recharge, out var charges))
{
BaseActionComponent? actionComp = null;
if (!_actions.ResolveActionData(uid, ref actionComp, logError: false))
continue;
var current = GetCurrentCharges((uid, charges, recharge));
if (!_lastCharges.TryGetValue(uid, out var last) || current != last)
{
_actions.UpdateAction(uid, actionComp);
}
_tempLastCharges[uid] = current;
}
_lastCharges.Clear();
foreach (var (uid, value) in _tempLastCharges)
{
_lastCharges[uid] = value;
}
_tempLastCharges.Clear();
}
}

View File

@@ -1,5 +0,0 @@
using Content.Shared.Charges.Systems;
namespace Content.Client.Charges.Systems;
public sealed class ChargesSystem : SharedChargesSystem { }

View File

@@ -16,7 +16,7 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
private readonly TimeSpan _typingTimeout = TimeSpan.FromSeconds(2); private readonly TimeSpan _typingTimeout = TimeSpan.FromSeconds(2);
private TimeSpan _lastTextChange; private TimeSpan _lastTextChange;
private bool _isClientTyping; private bool _isClientTyping;
private bool _isClientChatFocused; // Corvax-TypingIndicator private bool _isClientChatFocused;
public override void Initialize() public override void Initialize()
{ {
@@ -32,10 +32,8 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
return; return;
// client typed something - show typing indicator // client typed something - show typing indicator
// Corvax-TypingIndicator-Start
_isClientTyping = true; _isClientTyping = true;
ClientUpdateTyping(); ClientUpdateTyping();
// Corvax-TypingIndicator-End
_lastTextChange = _time.CurTime; _lastTextChange = _time.CurTime;
} }
@@ -46,14 +44,10 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
return; return;
// client submitted text - hide typing indicator // client submitted text - hide typing indicator
// Corvax-TypingIndicator-Start
_isClientTyping = false; _isClientTyping = false;
_isClientChatFocused = false;
ClientUpdateTyping(); ClientUpdateTyping();
// Corvax-TypingIndicator-End
} }
// Corvax-TypingIndicator-Start
public void ClientChangedChatFocus(bool isFocused) public void ClientChangedChatFocus(bool isFocused)
{ {
// don't update it if player don't want to show typing // don't update it if player don't want to show typing
@@ -64,7 +58,6 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
_isClientChatFocused = isFocused; _isClientChatFocused = isFocused;
ClientUpdateTyping(); ClientUpdateTyping();
} }
// Corvax-TypingIndicator-End
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
@@ -76,34 +69,25 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
var dif = _time.CurTime - _lastTextChange; var dif = _time.CurTime - _lastTextChange;
if (dif > _typingTimeout) if (dif > _typingTimeout)
{ {
// client didn't typed anything for a long time - hide indicator // client didn't typed anything for a long time - change indicator
// Corvax-TypingIndicator-Start
_isClientTyping = false; _isClientTyping = false;
ClientUpdateTyping(); ClientUpdateTyping();
// Corvax-TypingIndicator-End
} }
} }
} }
private void ClientUpdateTyping() // Corvax-TypingIndicator private void ClientUpdateTyping()
{ {
// Corvax-TypingIndicator-Start // check if player controls any pawn
// if (_isClientTyping == isClientTyping)
// return;
// Corvax-TypingIndicator-End
// check if player controls any entity.
if (_playerManager.LocalEntity == null) if (_playerManager.LocalEntity == null)
return; return;
// Corvax-TypingIndicator-Start
// _isClientTyping = isClientTyping;
var state = TypingIndicatorState.None; var state = TypingIndicatorState.None;
if (_isClientChatFocused) if (_isClientChatFocused)
state = _isClientTyping ? TypingIndicatorState.Typing : TypingIndicatorState.Idle; state = _isClientTyping ? TypingIndicatorState.Typing : TypingIndicatorState.Idle;
// Corvax-TypingIndicator-End
// send a networked event to server // send a networked event to server
RaisePredictiveEvent(new TypingChangedEvent(state)); // Corvax-TypingIndicator RaisePredictiveEvent(new TypingChangedEvent(state));
} }
private void OnShowTypingChanged(bool showTyping) private void OnShowTypingChanged(bool showTyping)
@@ -111,10 +95,8 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
// hide typing indicator immediately if player don't want to show it anymore // hide typing indicator immediately if player don't want to show it anymore
if (!showTyping) if (!showTyping)
{ {
// Corvax-TypingIndicator-Start
_isClientTyping = false; _isClientTyping = false;
ClientUpdateTyping(); ClientUpdateTyping();
// Corvax-TypingIndicator-End
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using Content.Shared.Chat.TypingIndicator; using Content.Shared.Chat.TypingIndicator;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -35,7 +35,6 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
return; return;
} }
//AppearanceSystem.TryGetData<bool>(uid, TypingIndicatorVisuals.IsTyping, out var isTyping, args.Component); // Corvax-TypingIndicator
var layerExists = args.Sprite.LayerMapTryGet(TypingIndicatorLayers.Base, out var layer); var layerExists = args.Sprite.LayerMapTryGet(TypingIndicatorLayers.Base, out var layer);
if (!layerExists) if (!layerExists)
layer = args.Sprite.LayerMapReserveBlank(TypingIndicatorLayers.Base); layer = args.Sprite.LayerMapReserveBlank(TypingIndicatorLayers.Base);
@@ -44,8 +43,7 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
args.Sprite.LayerSetState(layer, proto.TypingState); args.Sprite.LayerSetState(layer, proto.TypingState);
args.Sprite.LayerSetShader(layer, proto.Shader); args.Sprite.LayerSetShader(layer, proto.Shader);
args.Sprite.LayerSetOffset(layer, proto.Offset); args.Sprite.LayerSetOffset(layer, proto.Offset);
// args.Sprite.LayerSetVisible(layer, isTyping); // Corvax-TypingIndicator
// Corvax-TypingIndicator-Start
AppearanceSystem.TryGetData<TypingIndicatorState>(uid, TypingIndicatorVisuals.State, out var state); AppearanceSystem.TryGetData<TypingIndicatorState>(uid, TypingIndicatorVisuals.State, out var state);
args.Sprite.LayerSetVisible(layer, state != TypingIndicatorState.None); args.Sprite.LayerSetVisible(layer, state != TypingIndicatorState.None);
switch (state) switch (state)
@@ -57,6 +55,5 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
args.Sprite.LayerSetState(layer, proto.TypingState); args.Sprite.LayerSetState(layer, proto.TypingState);
break; break;
} }
// Corvax-TypingIndicator-End
} }
} }

View File

@@ -116,7 +116,10 @@ namespace Content.Client.Chemistry.UI
("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth), ("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth),
("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth), ("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth),
("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth), ("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth),
("15", ChemMasterReagentAmount.U15, StyleBase.ButtonOpenBoth),
("20", ChemMasterReagentAmount.U20, StyleBase.ButtonOpenBoth),
("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth), ("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth),
("30", ChemMasterReagentAmount.U30, StyleBase.ButtonOpenBoth),
("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth), ("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth),
("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth), ("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth),
(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft), (Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft),

View File

@@ -44,7 +44,7 @@ public sealed class HyposprayStatusControl : Control
PrevMaxVolume = solution.MaxVolume; PrevMaxVolume = solution.MaxVolume;
PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs; PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs;
var modeStringLocalized = Loc.GetString(_parent.Comp.OnlyAffectsMobs switch var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch
{ {
false => "hypospray-all-mode-text", false => "hypospray-all-mode-text",
true => "hypospray-mobs-only-mode-text", true => "hypospray-mobs-only-mode-text",

View File

@@ -1,4 +1,5 @@
using Content.Client.Guidebook.Components; using Content.Client.Guidebook.Components;
using Content.Client.UserInterface.Controls;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -31,8 +32,7 @@ namespace Content.Client.Chemistry.UI
// Setup window layout/elements // Setup window layout/elements
_window = this.CreateWindow<ReagentDispenserWindow>(); _window = this.CreateWindow<ReagentDispenserWindow>();
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName; _window.SetInfoFromEntity(EntMan, Owner);
_window.HelpGuidebookIds = EntMan.GetComponent<GuideHelpComponent>(Owner).Guides;
// Setup static button actions. // Setup static button actions.
_window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedReagentDispenser.OutputSlotName)); _window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedReagentDispenser.OutputSlotName));

View File

@@ -334,8 +334,11 @@ public sealed class ClientClothingSystem : ClothingSystem
if (layerData.State is not null && inventory.SpeciesId is not null && layerData.State.EndsWith(inventory.SpeciesId)) if (layerData.State is not null && inventory.SpeciesId is not null && layerData.State.EndsWith(inventory.SpeciesId))
continue; continue;
if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers)) if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, out var displacementKey))
{
revealedLayers.Add(displacementKey);
index++; index++;
}
} }
} }

View File

@@ -0,0 +1,25 @@
using Content.Client.Configurable.UI;
using Content.Shared.Configurable;
namespace Content.Client.Configurable;
public sealed class ConfigurationSystem : SharedConfigurationSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ConfigurationComponent, AfterAutoHandleStateEvent>(OnConfigurationState);
}
private void OnConfigurationState(Entity<ConfigurationComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_uiSystem.TryGetOpenUi<ConfigurationBoundUserInterface>(ent.Owner,
ConfigurationComponent.ConfigurationUiKey.Key,
out var bui))
{
bui.Refresh(ent);
}
}
}

View File

@@ -1,6 +1,8 @@
using System.Text.RegularExpressions; using System.Numerics;
using Robust.Client.GameObjects; using System.Text.RegularExpressions;
using Content.Shared.Configurable;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Shared.Configurable.ConfigurationComponent; using static Content.Shared.Configurable.ConfigurationComponent;
namespace Content.Client.Configurable.UI namespace Content.Client.Configurable.UI
@@ -19,16 +21,53 @@ namespace Content.Client.Configurable.UI
base.Open(); base.Open();
_menu = this.CreateWindow<ConfigurationMenu>(); _menu = this.CreateWindow<ConfigurationMenu>();
_menu.OnConfiguration += SendConfiguration; _menu.OnConfiguration += SendConfiguration;
if (EntMan.TryGetComponent(Owner, out ConfigurationComponent? component))
Refresh((Owner, component));
} }
protected override void UpdateState(BoundUserInterfaceState state) public void Refresh(Entity<ConfigurationComponent> entity)
{ {
base.UpdateState(state); if (_menu == null)
if (state is not ConfigurationBoundUserInterfaceState configurationState)
return; return;
_menu?.Populate(configurationState); _menu.Column.Children.Clear();
_menu.Inputs.Clear();
foreach (var field in entity.Comp.Config)
{
var label = new Label
{
Margin = new Thickness(0, 0, 8, 0),
Name = field.Key,
Text = field.Key + ":",
VerticalAlignment = Control.VAlignment.Center,
HorizontalExpand = true,
SizeFlagsStretchRatio = .2f,
MinSize = new Vector2(60, 0)
};
var input = new LineEdit
{
Name = field.Key + "-input",
Text = field.Value ?? "",
IsValid = _menu.Validate,
HorizontalExpand = true,
SizeFlagsStretchRatio = .8f
};
_menu.Inputs.Add((field.Key, input));
var row = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal
};
ConfigurationMenu.CopyProperties(_menu.Row, row);
row.AddChild(label);
row.AddChild(input);
_menu.Column.AddChild(row);
}
} }
protected override void ReceiveMessage(BoundUserInterfaceMessage message) protected override void ReceiveMessage(BoundUserInterfaceMessage message)

View File

@@ -1,12 +1,8 @@
using System.Collections.Generic; using System.Numerics;
using System.Numerics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using static Content.Shared.Configurable.ConfigurationComponent;
using static Robust.Client.UserInterface.Controls.BaseButton; using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.BoxContainer; using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -14,10 +10,10 @@ namespace Content.Client.Configurable.UI
{ {
public sealed class ConfigurationMenu : DefaultWindow public sealed class ConfigurationMenu : DefaultWindow
{ {
private readonly BoxContainer _column; public readonly BoxContainer Column;
private readonly BoxContainer _row; public readonly BoxContainer Row;
private readonly List<(string name, LineEdit input)> _inputs; public readonly List<(string name, LineEdit input)> Inputs;
[ViewVariables] [ViewVariables]
public Regex? Validation { get; internal set; } public Regex? Validation { get; internal set; }
@@ -28,7 +24,7 @@ namespace Content.Client.Configurable.UI
{ {
MinSize = SetSize = new Vector2(300, 250); MinSize = SetSize = new Vector2(300, 250);
_inputs = new List<(string name, LineEdit input)>(); Inputs = new List<(string name, LineEdit input)>();
Title = Loc.GetString("configuration-menu-device-title"); Title = Loc.GetString("configuration-menu-device-title");
@@ -39,14 +35,14 @@ namespace Content.Client.Configurable.UI
HorizontalExpand = true HorizontalExpand = true
}; };
_column = new BoxContainer Column = new BoxContainer
{ {
Orientation = LayoutOrientation.Vertical, Orientation = LayoutOrientation.Vertical,
Margin = new Thickness(8), Margin = new Thickness(8),
SeparationOverride = 16, SeparationOverride = 16,
}; };
_row = new BoxContainer Row = new BoxContainer
{ {
Orientation = LayoutOrientation.Horizontal, Orientation = LayoutOrientation.Horizontal,
SeparationOverride = 16, SeparationOverride = 16,
@@ -69,61 +65,20 @@ namespace Content.Client.Configurable.UI
ModulateSelfOverride = Color.FromHex("#202025") ModulateSelfOverride = Color.FromHex("#202025")
}; };
outerColumn.AddChild(_column); outerColumn.AddChild(Column);
baseContainer.AddChild(outerColumn); baseContainer.AddChild(outerColumn);
baseContainer.AddChild(confirmButton); baseContainer.AddChild(confirmButton);
Contents.AddChild(baseContainer); Contents.AddChild(baseContainer);
} }
public void Populate(ConfigurationBoundUserInterfaceState state)
{
_column.Children.Clear();
_inputs.Clear();
foreach (var field in state.Config)
{
var label = new Label
{
Margin = new Thickness(0, 0, 8, 0),
Name = field.Key,
Text = field.Key + ":",
VerticalAlignment = VAlignment.Center,
HorizontalExpand = true,
SizeFlagsStretchRatio = .2f,
MinSize = new Vector2(60, 0)
};
var input = new LineEdit
{
Name = field.Key + "-input",
Text = field.Value ?? "",
IsValid = Validate,
HorizontalExpand = true,
SizeFlagsStretchRatio = .8f
};
_inputs.Add((field.Key, input));
var row = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal
};
CopyProperties(_row, row);
row.AddChild(label);
row.AddChild(input);
_column.AddChild(row);
}
}
private void OnConfirm(ButtonEventArgs args) private void OnConfirm(ButtonEventArgs args)
{ {
var config = GenerateDictionary(_inputs, "Text"); var config = GenerateDictionary(Inputs, "Text");
OnConfiguration?.Invoke(config); OnConfiguration?.Invoke(config);
Close(); Close();
} }
private bool Validate(string value) public bool Validate(string value)
{ {
return Validation?.IsMatch(value) != false; return Validation?.IsMatch(value) != false;
} }
@@ -140,7 +95,7 @@ namespace Content.Client.Configurable.UI
return dictionary; return dictionary;
} }
private static void CopyProperties<T>(T from, T to) where T : Control public static void CopyProperties<T>(T from, T to) where T : Control
{ {
foreach (var property in from.AllAttachedProperties) foreach (var property in from.AllAttachedProperties)
{ {

View File

@@ -1,8 +1,10 @@
using System.Linq; using System.Linq;
using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.Placement; using Robust.Client.Placement;
using Robust.Client.Utility; using Robust.Client.ResourceManagement;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Construction namespace Content.Client.Construction
{ {
@@ -45,7 +47,14 @@ namespace Content.Client.Construction
public override void StartHijack(PlacementManager manager) public override void StartHijack(PlacementManager manager)
{ {
base.StartHijack(manager); base.StartHijack(manager);
manager.CurrentTextures = _prototype?.Layers.Select(sprite => sprite.DirFrame0()).ToList();
if (_prototype is null || !_constructionSystem.TryGetRecipePrototype(_prototype.ID, out var targetProtoId))
return;
if (!IoCManager.Resolve<IPrototypeManager>().TryIndex(targetProtoId, out EntityPrototype? proto))
return;
manager.CurrentTextures = SpriteComponent.GetPrototypeTextures(proto, IoCManager.Resolve<IResourceCache>()).ToList();
} }
} }
} }

View File

@@ -1,11 +1,10 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client.Popups; using Content.Client.Popups;
using Content.Shared.Construction; using Content.Shared.Construction;
using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Prototypes;
using Content.Shared.Construction.Steps;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Wall; using Content.Shared.Wall;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
@@ -15,6 +14,7 @@ using Robust.Shared.Input.Binding;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Construction namespace Content.Client.Construction
{ {
@@ -25,7 +25,6 @@ namespace Content.Client.Construction
public sealed class ConstructionSystem : SharedConstructionSystem public sealed class ConstructionSystem : SharedConstructionSystem
{ {
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!; [Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -33,6 +32,8 @@ namespace Content.Client.Construction
private readonly Dictionary<int, EntityUid> _ghosts = new(); private readonly Dictionary<int, EntityUid> _ghosts = new();
private readonly Dictionary<string, ConstructionGuide> _guideCache = new(); private readonly Dictionary<string, ConstructionGuide> _guideCache = new();
private readonly Dictionary<string, string> _recipesMetadataCache = [];
public bool CraftingEnabled { get; private set; } public bool CraftingEnabled { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
@@ -40,6 +41,8 @@ namespace Content.Client.Construction
{ {
base.Initialize(); base.Initialize();
WarmupRecipesCache();
UpdatesOutsidePrediction = true; UpdatesOutsidePrediction = true;
SubscribeLocalEvent<LocalPlayerAttachedEvent>(HandlePlayerAttached); SubscribeLocalEvent<LocalPlayerAttachedEvent>(HandlePlayerAttached);
SubscribeNetworkEvent<AckStructureConstructionMessage>(HandleAckStructure); SubscribeNetworkEvent<AckStructureConstructionMessage>(HandleAckStructure);
@@ -63,6 +66,77 @@ namespace Content.Client.Construction
ClearGhost(component.GhostId); ClearGhost(component.GhostId);
} }
public bool TryGetRecipePrototype(string constructionProtoId, [NotNullWhen(true)] out string? targetProtoId)
{
if (_recipesMetadataCache.TryGetValue(constructionProtoId, out targetProtoId))
return true;
targetProtoId = null;
return false;
}
private void WarmupRecipesCache()
{
foreach (var constructionProto in PrototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (!PrototypeManager.TryIndex(constructionProto.Graph, out var graphProto))
continue;
if (constructionProto.TargetNode is not { } targetNodeId)
continue;
if (!graphProto.Nodes.TryGetValue(targetNodeId, out var targetNode))
continue;
// Recursion is for wimps.
var stack = new Stack<ConstructionGraphNode>();
stack.Push(targetNode);
do
{
var node = stack.Pop();
// I never realized if this uid affects anything...
// EntityUid? userUid = args.SenderSession.State.ControlledEntity.HasValue
// ? GetEntity(args.SenderSession.State.ControlledEntity.Value)
// : null;
// We try to get the id of the target prototype, if it fails, we try going through the edges.
if (node.Entity.GetId(null, null, new(EntityManager)) is not { } entityId)
{
// If the stack is not empty, there is a high probability that the loop will go to infinity.
if (stack.Count == 0)
{
foreach (var edge in node.Edges)
{
if (graphProto.Nodes.TryGetValue(edge.Target, out var graphNode))
stack.Push(graphNode);
}
}
continue;
}
// If we got the id of the prototype, we exit the “recursion” by clearing the stack.
stack.Clear();
if (!PrototypeManager.TryIndex(constructionProto.ID, out ConstructionPrototype? recipe))
continue;
if (!PrototypeManager.TryIndex(entityId, out var proto))
continue;
var name = recipe.SetName.HasValue ? Loc.GetString(recipe.SetName) : proto.Name;
var desc = recipe.SetDescription.HasValue ? Loc.GetString(recipe.SetDescription) : proto.Description;
recipe.Name = name;
recipe.Description = desc;
_recipesMetadataCache.Add(constructionProto.ID, entityId);
} while (stack.Count > 0);
}
}
private void OnConstructionGuideReceived(ResponseConstructionGuide ev) private void OnConstructionGuideReceived(ResponseConstructionGuide ev)
{ {
_guideCache[ev.ConstructionId] = ev.Guide; _guideCache[ev.ConstructionId] = ev.Guide;
@@ -88,7 +162,7 @@ namespace Content.Client.Construction
private void HandleConstructionGhostExamined(EntityUid uid, ConstructionGhostComponent component, ExaminedEvent args) private void HandleConstructionGhostExamined(EntityUid uid, ConstructionGhostComponent component, ExaminedEvent args)
{ {
if (component.Prototype == null) if (component.Prototype?.Name is null)
return; return;
using (args.PushGroup(nameof(ConstructionGhostComponent))) using (args.PushGroup(nameof(ConstructionGhostComponent)))
@@ -97,7 +171,7 @@ namespace Content.Client.Construction
"construction-ghost-examine-message", "construction-ghost-examine-message",
("name", component.Prototype.Name))); ("name", component.Prototype.Name)));
if (!_prototypeManager.TryIndex(component.Prototype.Graph, out ConstructionGraphPrototype? graph)) if (!PrototypeManager.TryIndex(component.Prototype.Graph, out var graph))
return; return;
var startNode = graph.Nodes[component.Prototype.StartNode]; var startNode = graph.Nodes[component.Prototype.StartNode];
@@ -198,6 +272,9 @@ namespace Content.Client.Construction
return false; return false;
} }
if (!TryGetRecipePrototype(prototype.ID, out var targetProtoId) || !PrototypeManager.TryIndex(targetProtoId, out EntityPrototype? targetProto))
return false;
if (GhostPresent(loc)) if (GhostPresent(loc))
return false; return false;
@@ -214,16 +291,43 @@ namespace Content.Client.Construction
comp.GhostId = ghost.GetHashCode(); comp.GhostId = ghost.GetHashCode();
EntityManager.GetComponent<TransformComponent>(ghost.Value).LocalRotation = dir.ToAngle(); EntityManager.GetComponent<TransformComponent>(ghost.Value).LocalRotation = dir.ToAngle();
_ghosts.Add(comp.GhostId, ghost.Value); _ghosts.Add(comp.GhostId, ghost.Value);
var sprite = EntityManager.GetComponent<SpriteComponent>(ghost.Value); var sprite = EntityManager.GetComponent<SpriteComponent>(ghost.Value);
sprite.Color = new Color(48, 255, 48, 128); sprite.Color = new Color(48, 255, 48, 128);
for (int i = 0; i < prototype.Layers.Count; i++) if (targetProto.TryGetComponent(out IconComponent? icon, EntityManager.ComponentFactory))
{ {
sprite.AddBlankLayer(i); // There is no way to actually check if this already exists, so we blindly insert a new one sprite.AddBlankLayer(0);
sprite.LayerSetSprite(i, prototype.Layers[i]); sprite.LayerSetSprite(0, icon.Icon);
sprite.LayerSetShader(i, "unshaded"); sprite.LayerSetShader(0, "unshaded");
sprite.LayerSetVisible(i, true); sprite.LayerSetVisible(0, true);
} }
else if (targetProto.Components.TryGetValue("Sprite", out _))
{
var dummy = EntityManager.SpawnEntity(targetProtoId, MapCoordinates.Nullspace);
var targetSprite = EntityManager.EnsureComponent<SpriteComponent>(dummy);
EntityManager.System<AppearanceSystem>().OnChangeData(dummy, targetSprite);
for (var i = 0; i < targetSprite.AllLayers.Count(); i++)
{
if (!targetSprite[i].Visible || !targetSprite[i].RsiState.IsValid)
continue;
var rsi = targetSprite[i].Rsi ?? targetSprite.BaseRSI;
if (rsi is null || !rsi.TryGetState(targetSprite[i].RsiState, out var state) ||
state.StateId.Name is null)
continue;
sprite.AddBlankLayer(i);
sprite.LayerSetSprite(i, new SpriteSpecifier.Rsi(rsi.Path, state.StateId.Name));
sprite.LayerSetShader(i, "unshaded");
sprite.LayerSetVisible(i, true);
}
EntityManager.DeleteEntity(dummy);
}
else
return false;
if (prototype.CanBuildInImpassable) if (prototype.CanBuildInImpassable)
EnsureComp<WallMountComponent>(ghost.Value).Arc = new(Math.Tau); EnsureComp<WallMountComponent>(ghost.Value).Arc = new(Math.Tau);

View File

@@ -1,11 +1,12 @@
<DefaultWindow xmlns="https://spacestation14.io"> <DefaultWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True"> <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" MinWidth="243" Margin="0 0 5 0"> <BoxContainer Orientation="Vertical" MinWidth="243" Margin="0 0 5 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5"> <BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5">
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True"/> <LineEdit Name="SearchBar" PlaceHolder="{Loc 'construction-menu-search'}" HorizontalExpand="True"/>
<OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/> <OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/>
</BoxContainer> </BoxContainer>
<ItemList Name="Recipes" Access="Public" SelectMode="Single" VerticalExpand="True"/> <controls:ListContainer Name="Recipes" Access="Public" Group="True" Toggle="True" VerticalExpand="True" />
<ScrollContainer Name="RecipesGridScrollContainer" VerticalExpand="True" Access="Public" Visible="False"> <ScrollContainer Name="RecipesGridScrollContainer" VerticalExpand="True" Access="Public" Visible="False">
<GridContainer Name="RecipesGrid" Columns="5" Access="Public"/> <GridContainer Name="RecipesGrid" Columns="5" Access="Public"/>
</ScrollContainer> </ScrollContainer>
@@ -18,7 +19,7 @@
<Control> <Control>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 5"> <BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 5">
<BoxContainer Orientation="Horizontal" Align="Center"> <BoxContainer Orientation="Horizontal" Align="Center">
<TextureRect Name="TargetTexture" HorizontalAlignment="Right" Stretch="Keep" Margin="0 0 10 0"/> <EntityPrototypeView Name="TargetTexture" HorizontalAlignment="Right" Stretch="Fill" Margin="0 0 10 0"/>
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<RichTextLabel Name="TargetName"/> <RichTextLabel Name="TargetName"/>
<RichTextLabel Name="TargetDesc"/> <RichTextLabel Name="TargetDesc"/>

View File

@@ -1,14 +1,11 @@
using System;
using System.Numerics; using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Construction.Prototypes;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Graphics; using Robust.Shared.Prototypes;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Content.Client.Construction.UI namespace Content.Client.Construction.UI
{ {
@@ -28,7 +25,7 @@ namespace Content.Client.Construction.UI
bool GridViewButtonPressed { get; set; } bool GridViewButtonPressed { get; set; }
bool BuildButtonPressed { get; set; } bool BuildButtonPressed { get; set; }
ItemList Recipes { get; } ListContainer Recipes { get; }
ItemList RecipeStepList { get; } ItemList RecipeStepList { get; }
@@ -36,14 +33,14 @@ namespace Content.Client.Construction.UI
GridContainer RecipesGrid { get; } GridContainer RecipesGrid { get; }
event EventHandler<(string search, string catagory)> PopulateRecipes; event EventHandler<(string search, string catagory)> PopulateRecipes;
event EventHandler<ItemList.Item?> RecipeSelected; event EventHandler<ConstructionMenu.ConstructionMenuListData?> RecipeSelected;
event EventHandler RecipeFavorited; event EventHandler RecipeFavorited;
event EventHandler<bool> BuildButtonToggled; event EventHandler<bool> BuildButtonToggled;
event EventHandler<bool> EraseButtonToggled; event EventHandler<bool> EraseButtonToggled;
event EventHandler ClearAllGhosts; event EventHandler ClearAllGhosts;
void ClearRecipeInfo(); void ClearRecipeInfo();
void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem, bool isFavorite); void SetRecipeInfo(string name, string description, EntityPrototype? targetPrototype, bool isItem, bool isFavorite);
void ResetPlacement(); void ResetPlacement();
#region Window Control #region Window Control
@@ -94,8 +91,36 @@ namespace Content.Client.Construction.UI
Title = Loc.GetString("construction-menu-title"); Title = Loc.GetString("construction-menu-title");
BuildButton.Text = Loc.GetString("construction-menu-place-ghost"); BuildButton.Text = Loc.GetString("construction-menu-place-ghost");
Recipes.OnItemSelected += obj => RecipeSelected?.Invoke(this, obj.ItemList[obj.ItemIndex]); Recipes.ItemPressed += (_, data) => RecipeSelected?.Invoke(this, data as ConstructionMenuListData);
Recipes.OnItemDeselected += _ => RecipeSelected?.Invoke(this, null); Recipes.NoItemSelected += () => RecipeSelected?.Invoke(this, null);
Recipes.GenerateItem += (data, button) =>
{
if (data is not ConstructionMenuListData (var prototype, var targetPrototype))
return;
var entProtoView = new EntityPrototypeView()
{
SetSize = new(32f),
Stretch = SpriteView.StretchMode.Fill,
Scale = new(2),
Margin = new(0, 2),
};
entProtoView.SetPrototype(targetPrototype);
var label = new Label()
{
Text = prototype.Name,
Margin = new(5, 0),
};
var box = new BoxContainer();
box.AddChild(entProtoView);
box.AddChild(label);
button.AddChild(box);
button.ToolTip = prototype.Description;
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
};
SearchBar.OnTextChanged += _ => SearchBar.OnTextChanged += _ =>
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId])); PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId]));
@@ -121,7 +146,7 @@ namespace Content.Client.Construction.UI
public event EventHandler? ClearAllGhosts; public event EventHandler? ClearAllGhosts;
public event EventHandler<(string search, string catagory)>? PopulateRecipes; public event EventHandler<(string search, string catagory)>? PopulateRecipes;
public event EventHandler<ItemList.Item?>? RecipeSelected; public event EventHandler<ConstructionMenuListData?>? RecipeSelected;
public event EventHandler? RecipeFavorited; public event EventHandler? RecipeFavorited;
public event EventHandler<bool>? BuildButtonToggled; public event EventHandler<bool>? BuildButtonToggled;
public event EventHandler<bool>? EraseButtonToggled; public event EventHandler<bool>? EraseButtonToggled;
@@ -133,13 +158,17 @@ namespace Content.Client.Construction.UI
} }
public void SetRecipeInfo( public void SetRecipeInfo(
string name, string description, Texture iconTexture, bool isItem, bool isFavorite) string name,
string description,
EntityPrototype? targetPrototype,
bool isItem,
bool isFavorite)
{ {
BuildButton.Disabled = false; BuildButton.Disabled = false;
BuildButton.Text = Loc.GetString(isItem ? "construction-menu-place-ghost" : "construction-menu-craft"); BuildButton.Text = Loc.GetString(isItem ? "construction-menu-place-ghost" : "construction-menu-craft");
TargetName.SetMessage(name); TargetName.SetMessage(name);
TargetDesc.SetMessage(description); TargetDesc.SetMessage(description);
TargetTexture.Texture = iconTexture; TargetTexture.SetPrototype(targetPrototype?.ID);
FavoriteButton.Visible = true; FavoriteButton.Visible = true;
FavoriteButton.Text = Loc.GetString( FavoriteButton.Text = Loc.GetString(
isFavorite ? "construction-add-favorite-button" : "construction-remove-from-favorite-button"); isFavorite ? "construction-add-favorite-button" : "construction-remove-from-favorite-button");
@@ -150,9 +179,11 @@ namespace Content.Client.Construction.UI
BuildButton.Disabled = true; BuildButton.Disabled = true;
TargetName.SetMessage(string.Empty); TargetName.SetMessage(string.Empty);
TargetDesc.SetMessage(string.Empty); TargetDesc.SetMessage(string.Empty);
TargetTexture.Texture = null; TargetTexture.SetPrototype(null);
FavoriteButton.Visible = false; FavoriteButton.Visible = false;
RecipeStepList.Clear(); RecipeStepList.Clear();
} }
public sealed record ConstructionMenuListData(ConstructionPrototype Prototype, EntityPrototype TargetPrototype) : ListData;
} }
} }

View File

@@ -10,10 +10,8 @@ using Robust.Client.Placement;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Construction.UI namespace Content.Client.Construction.UI
{ {
@@ -30,18 +28,20 @@ namespace Content.Client.Construction.UI
[Dependency] private readonly IPlacementManager _placementManager = default!; [Dependency] private readonly IPlacementManager _placementManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!; [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
private readonly SpriteSystem _spriteSystem;
private readonly IConstructionMenuView _constructionView; private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem; private readonly EntityWhitelistSystem _whitelistSystem;
private readonly SpriteSystem _spriteSystem;
private ConstructionSystem? _constructionSystem; private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected; private ConstructionPrototype? _selected;
private List<ConstructionPrototype> _favoritedRecipes = []; private List<ConstructionPrototype> _favoritedRecipes = [];
private Dictionary<string, TextureButton> _recipeButtons = new(); private Dictionary<string, ContainerButton> _recipeButtons = new();
private string _selectedCategory = string.Empty; private string _selectedCategory = string.Empty;
private string _favoriteCatName = "construction-category-favorites";
private string _forAllCategoryName = "construction-category-all"; private const string FavoriteCatName = "construction-category-favorites";
private const string ForAllCategoryName = "construction-category-all";
private bool CraftingAvailable private bool CraftingAvailable
{ {
get => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Visible; get => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Visible;
@@ -98,15 +98,18 @@ namespace Content.Client.Construction.UI
_placementManager.PlacementChanged += OnPlacementChanged; _placementManager.PlacementChanged += OnPlacementChanged;
_constructionView.OnClose += () => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false; _constructionView.OnClose +=
() => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false;
_constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts(); _constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts();
_constructionView.PopulateRecipes += OnViewPopulateRecipes; _constructionView.PopulateRecipes += OnViewPopulateRecipes;
_constructionView.RecipeSelected += OnViewRecipeSelected; _constructionView.RecipeSelected += OnViewRecipeSelected;
_constructionView.BuildButtonToggled += (_, b) => BuildButtonToggled(b); _constructionView.BuildButtonToggled += (_, b) => BuildButtonToggled(b);
_constructionView.EraseButtonToggled += (_, b) => _constructionView.EraseButtonToggled += (_, b) =>
{ {
if (_constructionSystem is null) return; if (_constructionSystem is null)
if (b) _placementManager.Clear(); return;
if (b)
_placementManager.Clear();
_placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_constructionSystem, null)); _placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_constructionSystem, null));
_constructionView.EraseButtonPressed = b; _constructionView.EraseButtonPressed = b;
}; };
@@ -117,7 +120,7 @@ namespace Content.Client.Construction.UI
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty)); OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
} }
public void OnHudCraftingButtonToggled(ButtonToggledEventArgs args) public void OnHudCraftingButtonToggled(BaseButton.ButtonToggledEventArgs args)
{ {
WindowOpen = args.Pressed; WindowOpen = args.Pressed;
} }
@@ -139,7 +142,7 @@ namespace Content.Client.Construction.UI
_constructionView.ResetPlacement(); _constructionView.ResetPlacement();
} }
private void OnViewRecipeSelected(object? sender, ItemList.Item? item) private void OnViewRecipeSelected(object? sender, ConstructionMenu.ConstructionMenuListData? item)
{ {
if (item is null) if (item is null)
{ {
@@ -148,12 +151,15 @@ namespace Content.Client.Construction.UI
return; return;
} }
_selected = (ConstructionPrototype) item.Metadata!; _selected = item.Prototype;
if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
if (_placementManager is { IsActive: true, Eraser: false })
UpdateGhostPlacement();
PopulateInfo(_selected); PopulateInfo(_selected);
} }
private void OnGridViewRecipeSelected(object? sender, ConstructionPrototype? recipe) private void OnGridViewRecipeSelected(object? _, ConstructionPrototype? recipe)
{ {
if (recipe is null) if (recipe is null)
{ {
@@ -163,62 +169,21 @@ namespace Content.Client.Construction.UI
} }
_selected = recipe; _selected = recipe;
if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
if (_placementManager is { IsActive: true, Eraser: false })
UpdateGhostPlacement();
PopulateInfo(_selected); PopulateInfo(_selected);
} }
private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args) private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args)
{ {
var (search, category) = args; if (_constructionSystem is null)
return;
var recipes = new List<ConstructionPrototype>(); var actualRecipes = GetAndSortRecipes(args);
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
if (isEmptyCategory)
_selectedCategory = string.Empty;
else
_selectedCategory = category;
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (recipe.Hide)
continue;
if (_playerManager.LocalSession == null
|| _playerManager.LocalEntity == null
|| _whitelistSystem.IsWhitelistFail(recipe.EntityWhitelist, _playerManager.LocalEntity.Value))
continue;
if (!string.IsNullOrEmpty(search))
{
if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant()))
continue;
}
if (!isEmptyCategory)
{
if (category == _favoriteCatName)
{
if (!_favoritedRecipes.Contains(recipe))
{
continue;
}
}
else if (recipe.Category != category)
{
continue;
}
}
recipes.Add(recipe);
}
recipes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.InvariantCulture));
var recipesList = _constructionView.Recipes; var recipesList = _constructionView.Recipes;
recipesList.Clear();
var recipesGrid = _constructionView.RecipesGrid; var recipesGrid = _constructionView.RecipesGrid;
recipesGrid.RemoveAllChildren(); recipesGrid.RemoveAllChildren();
@@ -227,60 +192,120 @@ namespace Content.Client.Construction.UI
if (_constructionView.GridViewButtonPressed) if (_constructionView.GridViewButtonPressed)
{ {
foreach (var recipe in recipes) recipesList.PopulateList([]);
{ PopulateGrid(recipesGrid, actualRecipes);
var itemButton = new TextureButton
{
TextureNormal = _spriteSystem.Frame0(recipe.Icon),
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.Name,
ToolTip = recipe.Name,
Scale = new Vector2(1.35f),
ToggleMode = true,
};
var itemButtonPanelContainer = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
Children = { itemButton },
};
itemButton.OnToggled += buttonToggledEventArgs =>
{
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
if (buttonToggledEventArgs.Pressed &&
_selected != null &&
_recipeButtons.TryGetValue(_selected.Name, out var oldButton))
{
oldButton.Pressed = false;
SelectGridButton(oldButton, false);
}
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe : null);
};
recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Name] = itemButton;
var isCurrentButtonSelected = _selected == recipe;
itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected);
}
} }
else else
{ {
foreach (var recipe in recipes) recipesList.PopulateList(actualRecipes);
{
recipesList.Add(GetItem(recipe, recipesList));
}
} }
} }
private void SelectGridButton(TextureButton button, bool select) private void PopulateGrid(GridContainer recipesGrid,
IEnumerable<ConstructionMenu.ConstructionMenuListData> actualRecipes)
{
foreach (var recipe in actualRecipes)
{
var protoView = new EntityPrototypeView()
{
Scale = new Vector2(1.2f),
};
protoView.SetPrototype(recipe.TargetPrototype);
var itemButton = new ContainerButton()
{
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.TargetPrototype.Name,
ToolTip = recipe.TargetPrototype.Name,
ToggleMode = true,
Children = { protoView },
};
var itemButtonPanelContainer = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
Children = { itemButton },
};
itemButton.OnToggled += buttonToggledEventArgs =>
{
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
if (buttonToggledEventArgs.Pressed &&
_selected != null &&
_recipeButtons.TryGetValue(_selected.Name!, out var oldButton))
{
oldButton.Pressed = false;
SelectGridButton(oldButton, false);
}
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe.Prototype : null);
};
recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Prototype.Name!] = itemButton;
var isCurrentButtonSelected = _selected == recipe.Prototype;
itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected);
}
}
private List<ConstructionMenu.ConstructionMenuListData> GetAndSortRecipes((string, string) args)
{
var recipes = new List<ConstructionMenu.ConstructionMenuListData>();
var (search, category) = args;
var isEmptyCategory = string.IsNullOrEmpty(category) || category == ForAllCategoryName;
_selectedCategory = isEmptyCategory ? string.Empty : category;
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (recipe.Hide)
continue;
if (_playerManager.LocalSession == null
|| _playerManager.LocalEntity == null
|| _whitelistSystem.IsWhitelistFail(recipe.EntityWhitelist, _playerManager.LocalEntity.Value))
continue;
if (!string.IsNullOrEmpty(search) && (recipe.Name is { } name &&
!name.Contains(search.Trim(),
StringComparison.InvariantCultureIgnoreCase)))
continue;
if (!isEmptyCategory)
{
if ((category != FavoriteCatName || !_favoritedRecipes.Contains(recipe)) &&
recipe.Category != category)
continue;
}
if (!_constructionSystem!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
{
Logger.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
recipe.ID,
nameof(ConstructionPrototype));
continue;
}
if (!_prototypeManager.TryIndex(targetProtoId, out EntityPrototype? proto))
continue;
recipes.Add(new(recipe, proto));
}
recipes.Sort(
(a, b) => string.Compare(a.Prototype.Name, b.Prototype.Name, StringComparison.InvariantCulture));
return recipes;
}
private void SelectGridButton(BaseButton button, bool select)
{ {
if (button.Parent is not PanelContainer buttonPanel) if (button.Parent is not PanelContainer buttonPanel)
return; return;
button.Modulate = select ? Color.Green : Color.White; button.Modulate = select ? Color.Green : Color.Transparent;
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent; var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor }; buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
} }
@@ -302,12 +327,12 @@ namespace Content.Client.Construction.UI
// hard-coded to show all recipes // hard-coded to show all recipes
var idx = 0; var idx = 0;
categoriesArray[idx++] = _forAllCategoryName; categoriesArray[idx++] = ForAllCategoryName;
// hard-coded to show favorites if it need // hard-coded to show favorites if it need
if (isFavorites) if (isFavorites)
{ {
categoriesArray[idx++] = _favoriteCatName; categoriesArray[idx++] = FavoriteCatName;
} }
var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString); var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString);
@@ -325,18 +350,31 @@ namespace Content.Client.Construction.UI
if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i]) if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i])
_constructionView.OptionCategories.SelectId(i); _constructionView.OptionCategories.SelectId(i);
} }
_constructionView.Categories = categoriesArray; _constructionView.Categories = categoriesArray;
} }
private void PopulateInfo(ConstructionPrototype prototype) private void PopulateInfo(ConstructionPrototype? prototype)
{ {
if (_constructionSystem is null)
return;
_constructionView.ClearRecipeInfo(); _constructionView.ClearRecipeInfo();
if (prototype is null)
return;
if (!_constructionSystem.TryGetRecipePrototype(prototype.ID, out var targetProtoId))
return;
if (!_prototypeManager.TryIndex(targetProtoId, out EntityPrototype? proto))
return;
_constructionView.SetRecipeInfo( _constructionView.SetRecipeInfo(
prototype.Name, prototype.Description, _spriteSystem.Frame0(prototype.Icon), prototype.Name!,
prototype.Description!,
proto,
prototype.Type != ConstructionType.Item, prototype.Type != ConstructionType.Item,
!_favoritedRecipes.Contains(prototype)); !_favoritedRecipes.Contains(prototype));
@@ -349,16 +387,17 @@ namespace Content.Client.Construction.UI
if (_constructionSystem?.GetGuide(prototype) is not { } guide) if (_constructionSystem?.GetGuide(prototype) is not { } guide)
return; return;
foreach (var entry in guide.Entries) foreach (var entry in guide.Entries)
{ {
var text = entry.Arguments != null var text = entry.Arguments != null
? Loc.GetString(entry.Localization, entry.Arguments) : Loc.GetString(entry.Localization); ? Loc.GetString(entry.Localization, entry.Arguments)
: Loc.GetString(entry.Localization);
if (entry.EntryNumber is { } number) if (entry.EntryNumber is { } number)
{ {
text = Loc.GetString("construction-presenter-step-wrapper", text = Loc.GetString("construction-presenter-step-wrapper",
("step-number", number), ("text", text)); ("step-number", number),
("text", text));
} }
// The padding needs to be applied regardless of text length... (See PadLeft documentation) // The padding needs to be applied regardless of text length... (See PadLeft documentation)
@@ -369,23 +408,12 @@ namespace Content.Client.Construction.UI
} }
} }
private ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
{
return new(itemList)
{
Metadata = recipe,
Text = recipe.Name,
Icon = _spriteSystem.Frame0(recipe.Icon),
TooltipEnabled = true,
TooltipText = recipe.Description,
};
}
private void BuildButtonToggled(bool pressed) private void BuildButtonToggled(bool pressed)
{ {
if (pressed) if (pressed)
{ {
if (_selected == null) return; if (_selected == null)
return;
// not bound to a construction system // not bound to a construction system
if (_constructionSystem is null) if (_constructionSystem is null)
@@ -402,10 +430,11 @@ namespace Content.Client.Construction.UI
} }
_placementManager.BeginPlacing(new PlacementInformation _placementManager.BeginPlacing(new PlacementInformation
{ {
IsTile = false, IsTile = false,
PlacementOption = _selected.PlacementMode PlacementOption = _selected.PlacementMode
}, new ConstructionPlacementHijack(_constructionSystem, _selected)); },
new ConstructionPlacementHijack(_constructionSystem, _selected));
UpdateGhostPlacement(); UpdateGhostPlacement();
} }
@@ -429,38 +458,39 @@ namespace Content.Client.Construction.UI
var constructSystem = _systemManager.GetEntitySystem<ConstructionSystem>(); var constructSystem = _systemManager.GetEntitySystem<ConstructionSystem>();
_placementManager.BeginPlacing(new PlacementInformation() _placementManager.BeginPlacing(new PlacementInformation()
{ {
IsTile = false, IsTile = false,
PlacementOption = _selected.PlacementMode, PlacementOption = _selected.PlacementMode,
}, new ConstructionPlacementHijack(constructSystem, _selected)); },
new ConstructionPlacementHijack(constructSystem, _selected));
_constructionView.BuildButtonPressed = true; _constructionView.BuildButtonPressed = true;
} }
private void OnSystemLoaded(object? sender, SystemChangedArgs args) private void OnSystemLoaded(object? sender, SystemChangedArgs args)
{ {
if (args.System is ConstructionSystem system) SystemBindingChanged(system); if (args.System is ConstructionSystem system)
SystemBindingChanged(system);
} }
private void OnSystemUnloaded(object? sender, SystemChangedArgs args) private void OnSystemUnloaded(object? sender, SystemChangedArgs args)
{ {
if (args.System is ConstructionSystem) SystemBindingChanged(null); if (args.System is ConstructionSystem)
SystemBindingChanged(null);
} }
private void OnViewFavoriteRecipe() private void OnViewFavoriteRecipe()
{ {
if (_selected is not ConstructionPrototype recipe) if (_selected is null)
return; return;
if (!_favoritedRecipes.Remove(_selected)) if (!_favoritedRecipes.Remove(_selected))
_favoritedRecipes.Add(_selected); _favoritedRecipes.Add(_selected);
if (_selectedCategory == _favoriteCatName) if (_selectedCategory == FavoriteCatName)
{ {
if (_favoritedRecipes.Count > 0) OnViewPopulateRecipes(_constructionView,
OnViewPopulateRecipes(_constructionView, (string.Empty, _favoriteCatName)); _favoritedRecipes.Count > 0 ? (string.Empty, FavoriteCatName) : (string.Empty, string.Empty));
else
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
} }
PopulateInfo(_selected); PopulateInfo(_selected);
@@ -492,6 +522,9 @@ namespace Content.Client.Construction.UI
private void BindToSystem(ConstructionSystem system) private void BindToSystem(ConstructionSystem system)
{ {
_constructionSystem = system; _constructionSystem = system;
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
system.ToggleCraftingWindow += SystemOnToggleMenu; system.ToggleCraftingWindow += SystemOnToggleMenu;
system.FlipConstructionPrototype += SystemFlipConstructionPrototype; system.FlipConstructionPrototype += SystemFlipConstructionPrototype;
system.CraftingAvailabilityChanged += SystemCraftingAvailabilityChanged; system.CraftingAvailabilityChanged += SystemCraftingAvailabilityChanged;
@@ -533,7 +566,8 @@ namespace Content.Client.Construction.UI
if (IsAtFront) if (IsAtFront)
{ {
WindowOpen = false; WindowOpen = false;
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.SetClickPressed(false); // This does not call CraftingButtonToggled _uiManager.GetActiveUIWidget<GameTopMenuBar>()
.CraftingButton.SetClickPressed(false); // This does not call CraftingButtonToggled
} }
else else
_constructionView.MoveToFront(); _constructionView.MoveToFront();
@@ -541,7 +575,8 @@ namespace Content.Client.Construction.UI
else else
{ {
WindowOpen = true; WindowOpen = true;
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.SetClickPressed(true); // This does not call CraftingButtonToggled _uiManager.GetActiveUIWidget<GameTopMenuBar>()
.CraftingButton.SetClickPressed(true); // This does not call CraftingButtonToggled
} }
} }

View File

@@ -1,7 +1,7 @@
<DefaultWindow xmlns="https://spacestation14.io" <DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'credits-window-title'}" Title="{Loc 'credits-window-title'}"
SetSize="650 650" > SetSize="650 650">
<TabContainer> <TabContainer Name="MasterTabContainer">
<ScrollContainer Name="Ss14ContributorsTab" <ScrollContainer Name="Ss14ContributorsTab"
HScrollEnabled="False"> HScrollEnabled="False">
<BoxContainer Name="Ss14ContributorsContainer" <BoxContainer Name="Ss14ContributorsContainer"
@@ -26,5 +26,11 @@
<!-- Licenses get added here by code --> <!-- Licenses get added here by code -->
</BoxContainer> </BoxContainer>
</ScrollContainer> </ScrollContainer>
<ScrollContainer Name="AttributionsTab"
HScrollEnabled="False">
<BoxContainer Name="AttributionsContainer"
Orientation="Vertical"
Margin="2 2 0 0" />
</ScrollContainer>
</TabContainer> </TabContainer>
</DefaultWindow> </DefaultWindow>

View File

@@ -1,185 +1,382 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Credits; using Robust.Client.Credits;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.ContentPack; using Robust.Shared.ContentPack;
using Robust.Shared.IoC; using Robust.Shared.Prototypes;
using Robust.Shared.Localization; using Robust.Shared.Serialization.Manager;
using Robust.Shared.Maths; using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel; using YamlDotNet.RepresentationModel;
using static Robust.Client.UserInterface.Controls.BoxContainer; using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Credits namespace Content.Client.Credits;
{
[GenerateTypedNameReferences]
public sealed partial class CreditsWindow : DefaultWindow
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private static readonly Dictionary<string, int> PatronTierPriority = new() [GenerateTypedNameReferences]
public sealed partial class CreditsWindow : DefaultWindow
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private static readonly Dictionary<string, int> PatronTierPriority = new()
{
["Nuclear Operative"] = 1,
["Syndicate Agent"] = 2,
["Revolutionary"] = 3,
};
private readonly List<FormattedMessage> _attributions = [];
private readonly ISawmill _sawmill = Logger.GetSawmill("Credits");
private const int AttributionsSourcesPerPage = 50;
public CreditsWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
TabContainer.SetTabTitle(Ss14ContributorsTab, Loc.GetString("credits-window-ss14contributorslist-tab"));
TabContainer.SetTabTitle(PatronsTab, Loc.GetString("credits-window-patrons-tab"));
TabContainer.SetTabTitle(LicensesTab, Loc.GetString("credits-window-licenses-tab"));
TabContainer.SetTabTitle(AttributionsTab, Loc.GetString("credits-window-attributions-tab"));
_protoManager.PrototypesReloaded += _ =>
{ {
["Nuclear Operative"] = 1, _attributions.Clear();
["Syndicate Agent"] = 2,
["Revolutionary"] = 3
}; };
public CreditsWindow() MasterTabContainer.OnTabChanged += OnTabChanged;
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
TabContainer.SetTabTitle(Ss14ContributorsTab, Loc.GetString("credits-window-ss14contributorslist-tab")); PopulateContributors(Ss14ContributorsContainer);
TabContainer.SetTabTitle(PatronsTab, Loc.GetString("credits-window-patrons-tab")); }
TabContainer.SetTabTitle(LicensesTab, Loc.GetString("credits-window-licenses-tab"));
/// <summary>
/// Only populates the tab when they are selected, which reduces lagspike when not looking at attributions.
/// </summary>
private void OnTabChanged(int tab)
{
if (tab == Ss14ContributorsTab.GetPositionInParent())
PopulateContributors(Ss14ContributorsContainer); PopulateContributors(Ss14ContributorsContainer);
else if (tab == PatronsTab.GetPositionInParent())
PopulatePatrons(PatronsContainer); PopulatePatrons(PatronsContainer);
else if (tab == LicensesTab.GetPositionInParent())
PopulateLicenses(LicensesContainer); PopulateLicenses(LicensesContainer);
else if (tab == AttributionsTab.GetPositionInParent())
PopulateAttributions(AttributionsContainer, 0);
}
private async void PopulateAttributions(BoxContainer attributionsContainer, int count)
{
attributionsContainer.DisposeAllChildren();
if (_attributions.Count == 0)
{
var rsi = await CollectRSiAttributions();
var rga = await CollectRgaAttributions();
_attributions.AddRange(rsi);
_attributions.AddRange(rga);
} }
private void PopulateLicenses(BoxContainer licensesContainer) foreach (var message in _attributions.Skip(count).Take(AttributionsSourcesPerPage))
{ {
foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name)) var rich = new RichTextLabel();
{ rich.SetMessage(message);
licensesContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = entry.Name}); attributionsContainer.AddChild(rich);
}
// We split these line by line because otherwise var container = new BoxContainer { Orientation = LayoutOrientation.Horizontal };
// the LGPL causes Clyde to go out of bounds in the rendering code.
foreach (var line in entry.License.Split("\n")) var previousPageButton = new Button { Text = "Previous Page" };
previousPageButton.OnPressed +=
_ => PopulateAttributions(attributionsContainer, count - AttributionsSourcesPerPage);
var nextPageButton = new Button { Text = "Next Page" };
nextPageButton.OnPressed +=
_ => PopulateAttributions(attributionsContainer, count + AttributionsSourcesPerPage);
if (count - AttributionsSourcesPerPage >= 0)
container.AddChild(previousPageButton);
if (count + AttributionsSourcesPerPage < _attributions.Count)
container.AddChild(nextPageButton);
attributionsContainer.AddChild(container);
}
private Task<List<FormattedMessage>> CollectRSiAttributions()
{
return Task.Run(() =>
{
var rsiStreams = _resourceManager.ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"));
var attrs = new List<FormattedMessage>();
foreach (var stream in rsiStreams)
{
try
{ {
licensesContainer.AddChild(new Label {Text = line, FontColorOverride = new Color(200, 200, 200)}); var m = new FormattedMessage();
var yamlStream = _resourceManager.ContentFileReadYaml(stream);
if (yamlStream.Documents[0].RootNode.ToDataNode() is not MappingDataNode map)
throw new Exception("meta.json is not a mapping.");
if (!map.TryGet("copyright", out var copyrightNode))
throw new Exception("Missing the copyright field.");
if (!map.TryGet("states", out var statesNode))
throw new Exception("Missing the states field.");
if (statesNode is not SequenceDataNode states)
throw new Exception("Missing a list of states.");
var copyright = copyrightNode.ToString();
var files = states.Select(n => (MappingDataNode)n)
.Select(n => n.Get("name") + ".png");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-directory",
("directory", stream.Directory.ToString())));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-files",
("files", string.Join(", ", files))));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-copyright",
("copyright", copyright)));
m.AddText("\n");
attrs.Add(m);
}
catch (Exception e)
{
var m = new FormattedMessage();
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-failed",
("file", stream.ToString())));
m.AddText("\n");
_sawmill.Error($"{stream.ToString()}\n{e}");
attrs.Add(m);
} }
} }
}
private void PopulatePatrons(BoxContainer patronsContainer) return attrs;
});
}
private Task<List<FormattedMessage>> CollectRgaAttributions()
{
return Task.Run(() =>
{ {
var patrons = LoadPatrons(); var rgaStreams = _resourceManager.ContentFindFiles("/")
.Where(p => p.Filename == "attributions.yml");
// Do not show "become a patron" button on Steam builds var attrs = new List<FormattedMessage>();
// since Patreon violates Valve's rules about alternative storefronts.
var linkPatreon = _cfg.GetCVar(CCVars.InfoLinksPatreon); foreach (var stream in rgaStreams)
if (!_cfg.GetCVar(CCVars.BrandingSteam) && linkPatreon != "")
{ {
Button patronButton; try
patronsContainer.AddChild(patronButton = new Button
{ {
Text = Loc.GetString("credits-window-become-patron-button"), var yamlStream = _resourceManager.ContentFileReadYaml(stream);
HorizontalAlignment = HAlignment.Center
});
patronButton.OnPressed += if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
_ => IoCManager.Resolve<IUriOpener>().OpenUri(linkPatreon); throw new Exception("Attributions file is not a list of attributions.");
foreach (var attribution in sequence.Sequence)
{
var m = new FormattedMessage();
if (attribution is not MappingDataNode map)
throw new Exception("Attribution is not a mapping.");
if (!map.TryGet("files", out var filesNode))
throw new Exception("Attribution does not list files.");
if (!map.TryGet("copyright", out var copyrightNode))
throw new Exception("Attribution does not copyright.");
if (!map.TryGet("license", out var licenseNode))
throw new Exception("Attribution does not identify a license.");
if (!map.TryGet("source", out var sourceNode))
throw new Exception("Attribution does not identify a source.");
var files = _serialization.Read<string[]>(filesNode, notNullableOverride: true);
var copyright = copyrightNode.ToString();
var license = licenseNode.ToString();
var source = sourceNode.ToString();
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-directory",
("directory", stream.Directory.ToString())));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-files",
("files", string.Join(", ", files))));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-copyright",
("copyright", copyright)));
m.AddText("\n");
m.AddMarkupPermissive(
_loc.GetString("credits-window-attributions-license", ("license", license)));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-source", ("source", source)));
m.AddText("\n");
attrs.Add(m);
}
}
catch (Exception e)
{
var m = new FormattedMessage();
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-failed",
("file", stream.ToString())));
m.AddText("\n");
_sawmill.Error($"{stream.ToString()}\n{e}");
attrs.Add(m);
}
} }
var first = true; return attrs;
foreach (var tier in patrons.GroupBy(p => p.Tier).OrderBy(p => PatronTierPriority[p.Key])) });
{ }
if (!first)
{
patronsContainer.AddChild(new Control {MinSize = new Vector2(0, 10)});
}
first = false; private void PopulateLicenses(BoxContainer licensesContainer)
patronsContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = $"{tier.Key}"}); {
foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name))
var msg = string.Join(", ", tier.OrderBy(p => p.Name).Select(p => p.Name));
var label = new RichTextLabel();
label.SetMessage(msg);
patronsContainer.AddChild(label);
}
}
private IEnumerable<PatronEntry> LoadPatrons()
{ {
var yamlStream = _resourceManager.ContentFileReadYaml(new ("/Credits/Patrons.yml")); licensesContainer.AddChild(new Label
var sequence = (YamlSequenceNode) yamlStream.Documents[0].RootNode; { StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = entry.Name });
return sequence // We split these line by line because otherwise
.Cast<YamlMappingNode>() // the LGPL causes Clyde to go out of bounds in the rendering code.
.Select(m => new PatronEntry(m["Name"].AsString(), m["Tier"].AsString())); foreach (var line in entry.License.Split("\n"))
}
private void PopulateContributors(BoxContainer ss14ContributorsContainer)
{
Button contributeButton;
ss14ContributorsContainer.AddChild(new BoxContainer
{ {
Orientation = LayoutOrientation.Horizontal, licensesContainer.AddChild(new Label { Text = line, FontColorOverride = new Color(200, 200, 200) });
HorizontalAlignment = HAlignment.Center,
SeparationOverride = 20,
Children =
{
new Label {Text = Loc.GetString("credits-window-contributor-encouragement-label") },
(contributeButton = new Button {Text = Loc.GetString("credits-window-contribute-button")})
}
});
var first = true;
void AddSection(string title, string path, bool markup = false)
{
if (!first)
{
ss14ContributorsContainer.AddChild(new Control {MinSize = new Vector2(0, 10)});
}
first = false;
ss14ContributorsContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = title});
var label = new RichTextLabel();
var text = _resourceManager.ContentFileReadAllText($"/Credits/{path}");
if (markup)
{
label.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
}
else
{
label.SetMessage(text);
}
ss14ContributorsContainer.AddChild(label);
}
AddSection(Loc.GetString("credits-window-contributors-section-title"), "GitHub.txt");
AddSection(Loc.GetString("credits-window-codebases-section-title"), "SpaceStation13.txt");
AddSection(Loc.GetString("credits-window-tts-title"), "TTS.txt"); // Corvax-TTS
AddSection(Loc.GetString("credits-window-original-remake-team-section-title"), "OriginalRemake.txt");
AddSection(Loc.GetString("credits-window-special-thanks-section-title"), "SpecialThanks.txt", true);
var linkGithub = _cfg.GetCVar(CCVars.InfoLinksGithub);
contributeButton.OnPressed += _ =>
IoCManager.Resolve<IUriOpener>().OpenUri(linkGithub);
if (linkGithub == "")
contributeButton.Visible = false;
}
private sealed class PatronEntry
{
public string Name { get; }
public string Tier { get; }
public PatronEntry(string name, string tier)
{
Name = name;
Tier = tier;
} }
} }
} }
private void PopulatePatrons(BoxContainer patronsContainer)
{
var patrons = LoadPatrons();
// Do not show "become a patron" button on Steam builds
// since Patreon violates Valve's rules about alternative storefronts.
var linkPatreon = _cfg.GetCVar(CCVars.InfoLinksPatreon);
if (!_cfg.GetCVar(CCVars.BrandingSteam) && linkPatreon != "")
{
Button patronButton;
patronsContainer.AddChild(patronButton = new Button
{
Text = Loc.GetString("credits-window-become-patron-button"),
HorizontalAlignment = HAlignment.Center,
});
patronButton.OnPressed +=
_ => IoCManager.Resolve<IUriOpener>().OpenUri(linkPatreon);
}
var first = true;
foreach (var tier in patrons.GroupBy(p => p.Tier).OrderBy(p => PatronTierPriority[p.Key]))
{
if (!first)
patronsContainer.AddChild(new Control { MinSize = new Vector2(0, 10) });
first = false;
patronsContainer.AddChild(new Label
{ StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = $"{tier.Key}" });
var msg = string.Join(", ", tier.OrderBy(p => p.Name).Select(p => p.Name));
var label = new RichTextLabel();
label.SetMessage(msg);
patronsContainer.AddChild(label);
}
}
private IEnumerable<PatronEntry> LoadPatrons()
{
var yamlStream = _resourceManager.ContentFileReadYaml(new ResPath("/Credits/Patrons.yml"));
var sequence = (YamlSequenceNode)yamlStream.Documents[0].RootNode;
return sequence
.Cast<YamlMappingNode>()
.Select(m => new PatronEntry(m["Name"].AsString(), m["Tier"].AsString()));
}
private void PopulateContributors(BoxContainer ss14ContributorsContainer)
{
Button contributeButton;
ss14ContributorsContainer.AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = HAlignment.Center,
SeparationOverride = 20,
Children =
{
new Label { Text = Loc.GetString("credits-window-contributor-encouragement-label") },
(contributeButton = new Button { Text = Loc.GetString("credits-window-contribute-button") }),
},
});
var first = true;
void AddSection(string title, string path, bool markup = false)
{
if (!first)
ss14ContributorsContainer.AddChild(new Control { MinSize = new Vector2(0, 10) });
first = false;
ss14ContributorsContainer.AddChild(new Label
{ StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = title });
var label = new RichTextLabel();
var text = _resourceManager.ContentFileReadAllText($"/Credits/{path}");
if (markup)
label.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
else
label.SetMessage(text);
ss14ContributorsContainer.AddChild(label);
}
AddSection(Loc.GetString("credits-window-tts-title"), "TTS.txt"); // Corvax-TTS
AddSection(Loc.GetString("credits-window-contributors-section-title"), "GitHub.txt");
AddSection(Loc.GetString("credits-window-codebases-section-title"), "SpaceStation13.txt");
AddSection(Loc.GetString("credits-window-original-remake-team-section-title"), "OriginalRemake.txt");
AddSection(Loc.GetString("credits-window-special-thanks-section-title"), "SpecialThanks.txt", true);
var linkGithub = _cfg.GetCVar(CCVars.InfoLinksGithub);
contributeButton.OnPressed += _ =>
IoCManager.Resolve<IUriOpener>().OpenUri(linkGithub);
if (linkGithub == "")
contributeButton.Visible = false;
}
private sealed class PatronEntry
{
public string Name { get; }
public string Tier { get; }
public PatronEntry(string name, string tier)
{
Name = name;
Tier = tier;
}
}
} }

View File

@@ -213,52 +213,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
return; return;
} }
var entries = listing.ToList(); var entries = listing.Select(i => new ItemList.Item(RecordListing) {
entries.Sort((a, b) => string.Compare(a.Value, b.Value, StringComparison.Ordinal)); Text = i.Value,
// `entries` now contains the definitive list of items which should be in Metadata = i.Key
// our list of records and is in the order we want to present those items. }).ToList();
entries.Sort((a, b) => string.Compare(a.Text, b.Text, StringComparison.Ordinal));
// Walk through the existing items in RecordListing and in the updated listing RecordListing.SetItems(entries, (a,b) => string.Compare(a.Text, b.Text));
// in parallel to synchronize the items in RecordListing with `entries`.
int i = RecordListing.Count - 1;
int j = entries.Count - 1;
while (i >= 0 && j >= 0)
{
var strcmp = string.Compare(RecordListing[i].Text, entries[j].Value, StringComparison.Ordinal);
if (strcmp == 0)
{
// This item exists in both RecordListing and `entries`. Nothing to do.
i--;
j--;
}
else if (strcmp > 0)
{
// Item exists in RecordListing, but not in `entries`. Remove it.
RecordListing.RemoveAt(i);
i--;
}
else if (strcmp < 0)
{
// A new entry which doesn't exist in RecordListing. Create it.
RecordListing.Insert(i + 1, new ItemList.Item(RecordListing){Text = entries[j].Value, Metadata = entries[j].Key});
j--;
}
}
// Any remaining items in RecordListing don't exist in `entries`, so remove them
while (i >= 0)
{
RecordListing.RemoveAt(i);
i--;
}
// And finally, any remaining items in `entries`, don't exist in RecordListing. Create them.
while (j >= 0)
{
RecordListing.Insert(0, new ItemList.Item(RecordListing){ Text = entries[j].Value, Metadata = entries[j].Key });
j--;
}
} }
private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord) private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
{ {
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/job_icons.rsi"), "Unknown"); var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/job_icons.rsi"), "Unknown");

View File

@@ -0,0 +1,7 @@
using Content.Shared.Damage.Systems;
namespace Content.Client.Damage.Systems;
public sealed partial class StaminaSystem : SharedStaminaSystem
{
}

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Decals
[Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteSystem _sprites = default!; [Dependency] private readonly SpriteSystem _sprites = default!;
private DecalOverlay _overlay = default!; private DecalOverlay? _overlay;
private HashSet<uint> _removedUids = new(); private HashSet<uint> _removedUids = new();
private readonly List<Vector2i> _removedChunks = new(); private readonly List<Vector2i> _removedChunks = new();
@@ -31,6 +31,9 @@ namespace Content.Client.Decals
public void ToggleOverlay() public void ToggleOverlay()
{ {
if (_overlay == null)
return;
if (_overlayManager.HasOverlay<DecalOverlay>()) if (_overlayManager.HasOverlay<DecalOverlay>())
{ {
_overlayManager.RemoveOverlay(_overlay); _overlayManager.RemoveOverlay(_overlay);
@@ -44,6 +47,10 @@ namespace Content.Client.Decals
public override void Shutdown() public override void Shutdown()
{ {
base.Shutdown(); base.Shutdown();
if (_overlay == null)
return;
_overlayManager.RemoveOverlay(_overlay); _overlayManager.RemoveOverlay(_overlay);
} }

View File

@@ -43,3 +43,9 @@ public enum DeliveryVisualLayers : byte
Breakage, Breakage,
Trash, Trash,
} }
public enum DeliverySpawnerVisualLayers : byte
{
Contents,
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.DeviceNetwork.Systems;
namespace Content.Client.DeviceNetwork.Systems;
public sealed class DeviceNetworkSystem : SharedDeviceNetworkSystem
{
}

View File

@@ -9,22 +9,36 @@ public sealed class DisplacementMapSystem : EntitySystem
{ {
[Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly ISerializationManager _serialization = default!;
public bool TryAddDisplacement(DisplacementData data, SpriteComponent sprite, int index, string key, HashSet<string> revealedLayers) /// <summary>
/// Attempting to apply a displacement map to a specific layer of SpriteComponent
/// </summary>
/// <param name="data">Information package for applying the displacement map</param>
/// <param name="sprite">SpriteComponent</param>
/// <param name="index">Index of the layer where the new map layer will be added</param>
/// <param name="key">Unique layer key, which will determine which layer to apply displacement map to</param>
/// <param name="displacementKey">The key of the new displacement map layer added by this function.</param>
/// <returns></returns>
public bool TryAddDisplacement(DisplacementData data,
SpriteComponent sprite,
int index,
object key,
out string displacementKey)
{ {
displacementKey = $"{key}-displacement";
if (key.ToString() is null)
return false;
if (data.ShaderOverride != null) if (data.ShaderOverride != null)
sprite.LayerSetShader(index, data.ShaderOverride); sprite.LayerSetShader(index, data.ShaderOverride);
var displacementKey = $"{key}-displacement"; if (sprite.LayerMapTryGet(displacementKey, out var oldIndex))
if (!revealedLayers.Add(displacementKey)) sprite.RemoveLayer(oldIndex);
{
Log.Warning($"Duplicate key for DISPLACEMENT: {displacementKey}.");
return false;
}
//allows you not to write it every time in the YML //allows you not to write it every time in the YML
foreach (var pair in data.SizeMaps) foreach (var pair in data.SizeMaps)
{ {
pair.Value.CopyToShaderParameters??= new() pair.Value.CopyToShaderParameters ??= new()
{ {
LayerKey = "dummy", LayerKey = "dummy",
ParameterTexture = "displacementMap", ParameterTexture = "displacementMap",
@@ -45,21 +59,22 @@ public sealed class DisplacementMapSystem : EntitySystem
if (actualRSI is not null) if (actualRSI is not null)
{ {
if (actualRSI.Size.X != actualRSI.Size.Y) if (actualRSI.Size.X != actualRSI.Size.Y)
Log.Warning($"DISPLACEMENT: {displacementKey} has a resolution that is not 1:1, things can look crooked"); {
Log.Warning(
$"DISPLACEMENT: {displacementKey} has a resolution that is not 1:1, things can look crooked");
}
var layerSize = actualRSI.Size.X; var layerSize = actualRSI.Size.X;
if (data.SizeMaps.ContainsKey(layerSize)) if (data.SizeMaps.TryGetValue(layerSize, out var map))
displacementDataLayer = data.SizeMaps[layerSize]; displacementDataLayer = map;
} }
var displacementLayer = _serialization.CreateCopy(displacementDataLayer, notNullableOverride: true); var displacementLayer = _serialization.CreateCopy(displacementDataLayer, notNullableOverride: true);
displacementLayer.CopyToShaderParameters!.LayerKey = key; displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString() ?? "this is impossible";
sprite.AddLayer(displacementLayer, index); sprite.AddLayer(displacementLayer, index);
sprite.LayerMapSet(displacementKey, index); sprite.LayerMapSet(displacementKey, index);
revealedLayers.Add(displacementKey);
return true; return true;
} }
} }

View File

@@ -1,9 +0,0 @@
using Content.Shared.Disposal.Components;
namespace Content.Client.Disposal;
[RegisterComponent]
public sealed partial class DisposalUnitComponent : SharedDisposalUnitComponent
{
}

View File

@@ -0,0 +1,80 @@
using Content.Client.Disposal.Unit;
using Content.Client.Power.EntitySystems;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using System.Linq;
namespace Content.Client.Disposal.Mailing;
public sealed class MailingUnitBoundUserInterface : BoundUserInterface
{
[ViewVariables]
public MailingUnitWindow? MailingUnitWindow;
public MailingUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(DisposalUnitComponent.UiButton button)
{
SendMessage(new DisposalUnitComponent.UiButtonPressedMessage(button));
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
// the pressure lerp up.
}
private void TargetSelected(ItemList.ItemListSelectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
SendMessage(new TargetSelectedMessage(item.Text));
}
protected override void Open()
{
base.Open();
MailingUnitWindow = this.CreateWindow<MailingUnitWindow>();
MailingUnitWindow.OpenCenteredRight();
MailingUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Eject);
MailingUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Engage);
MailingUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Power);
MailingUnitWindow.TargetListContainer.OnItemSelected += TargetSelected;
if (EntMan.TryGetComponent(Owner, out MailingUnitComponent? component))
Refresh((Owner, component));
}
public void Refresh(Entity<MailingUnitComponent> entity)
{
if (MailingUnitWindow == null)
return;
// TODO: This should be decoupled from disposals
if (EntMan.TryGetComponent(entity.Owner, out DisposalUnitComponent? disposals))
{
var disposalSystem = EntMan.System<DisposalUnitSystem>();
var disposalState = disposalSystem.GetState(Owner, disposals);
var fullPressure = disposalSystem.EstimatedFullPressure(Owner, disposals);
MailingUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
MailingUnitWindow.FullPressure = fullPressure;
MailingUnitWindow.PressureBar.UpdatePressure(fullPressure);
MailingUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
MailingUnitWindow.Engage.Pressed = disposals.Engaged;
}
MailingUnitWindow.Title = Loc.GetString("ui-mailing-unit-window-title", ("tag", entity.Comp.Tag ?? " "));
//UnitTag.Text = state.Tag;
MailingUnitWindow.Target.Text = entity.Comp.Target;
var entries = entity.Comp.TargetList.Select(target => new ItemList.Item(MailingUnitWindow.TargetListContainer) {
Text = target,
Selected = target == entity.Comp.Target
}).ToList();
MailingUnitWindow.TargetListContainer.SetItems(entries);
}
}

View File

@@ -0,0 +1,22 @@
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Mailing;
namespace Content.Client.Disposal.Mailing;
public sealed class MailingUnitSystem : SharedMailingUnitSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MailingUnitComponent, AfterAutoHandleStateEvent>(OnMailingState);
}
private void OnMailingState(Entity<MailingUnitComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UserInterfaceSystem.TryGetOpenUi<MailingUnitBoundUserInterface>(ent.Owner, MailingUnitUiKey.Key, out var bui))
{
bui.Refresh(ent);
}
}
}

View File

@@ -1,12 +1,14 @@
<DefaultWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Disposal.UI" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="300 400" xmlns:disposal="clr-namespace:Content.Client.Disposal"
MinSize="300 400"
SetSize="300 400" SetSize="300 400"
Resizable="False"> Resizable="False">
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" SeparationOverride="8"> <BoxContainer Orientation="Horizontal" SeparationOverride="8">
<Label Text="{Loc 'ui-mailing-unit-target-label'}" /> <Label Text="{Loc 'ui-mailing-unit-target-label'}" />
<Label Name="Target" <Label Name="Target"
Access="Public"
Text="" /> Text="" />
</BoxContainer> </BoxContainer>
<ItemList Name="TargetListContainer" <ItemList Name="TargetListContainer"
@@ -18,14 +20,15 @@
</ItemList> </ItemList>
<BoxContainer Orientation="Horizontal" SeparationOverride="4"> <BoxContainer Orientation="Horizontal" SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" /> <Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState" <Label Name="UnitState" Access="Public"
Text="{Loc 'ui-disposal-unit-label-status'}" /> Text="{Loc 'ui-disposal-unit-label-status'}" />
</BoxContainer> </BoxContainer>
<Control MinSize="0 5" /> <Control MinSize="0 5" />
<BoxContainer Orientation="Horizontal" <BoxContainer Orientation="Horizontal"
SeparationOverride="4"> SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" /> <Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<ui:PressureBar Name="PressureBar" <disposal:PressureBar Name="PressureBar"
Access="Public"
MinSize="190 20" MinSize="190 20"
HorizontalAlignment="Right" HorizontalAlignment="Right"
MinValue="0" MinValue="0"
@@ -50,4 +53,4 @@
StyleClasses="OpenLeft" /> StyleClasses="OpenLeft" />
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</DefaultWindow> </controls:FancyWindow>

View File

@@ -0,0 +1,27 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.Mailing
{
/// <summary>
/// Client-side UI used to control a <see cref="Shared.Disposal.Components.MailingUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class MailingUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public MailingUnitWindow()
{
RobustXamlLoader.Load(this);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,9 +1,10 @@
using Content.Shared.Disposal; using Content.Shared.Disposal;
using Content.Shared.Disposal.Unit;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Client.Disposal.UI; namespace Content.Client.Disposal;
public sealed class PressureBar : ProgressBar public sealed class PressureBar : ProgressBar
{ {

View File

@@ -1,187 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Content.Shared.DragDrop;
using Content.Shared.Emag.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Physics.Events;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.Systems;
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
private const string AnimationKey = "disposal_unit_animation";
private const string DefaultFlushState = "disposal-flush";
private const string DefaultChargeState = "disposal-charging";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<DisposalUnitComponent, PreventCollideEvent>(OnPreventCollide);
SubscribeLocalEvent<DisposalUnitComponent, CanDropTargetEvent>(OnCanDragDropOn);
SubscribeLocalEvent<DisposalUnitComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<DisposalUnitComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DisposalUnitComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref ComponentHandleState args)
{
if (args.Current is not DisposalUnitComponentState state)
return;
component.FlushSound = state.FlushSound;
component.State = state.State;
component.NextPressurized = state.NextPressurized;
component.AutomaticEngageTime = state.AutomaticEngageTime;
component.NextFlush = state.NextFlush;
component.Powered = state.Powered;
component.Engaged = state.Engaged;
component.RecentlyEjected.Clear();
component.RecentlyEjected.AddRange(EnsureEntityList<DisposalUnitComponent>(state.RecentlyEjected, uid));
}
public override bool HasDisposals(EntityUid? uid)
{
return HasComp<DisposalUnitComponent>(uid);
}
public override bool ResolveDisposals(EntityUid uid, [NotNullWhen(true)] ref SharedDisposalUnitComponent? component)
{
if (component != null)
return true;
TryComp<DisposalUnitComponent>(uid, out var storage);
component = storage;
return component != null;
}
public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null)
{
return;
}
private void OnComponentInit(EntityUid uid, SharedDisposalUnitComponent sharedDisposalUnit, ComponentInit args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite) || !TryComp<AppearanceComponent>(uid, out var appearance))
return;
UpdateState(uid, sharedDisposalUnit, sprite, appearance);
}
private void OnAppearanceChange(EntityUid uid, SharedDisposalUnitComponent unit, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
UpdateState(uid, unit, args.Sprite, args.Component);
}
/// <summary>
/// Update visuals and tick animation
/// </summary>
private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!_appearanceSystem.TryGetData<VisualState>(uid, Visuals.VisualState, out var state, appearance))
return;
sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == VisualState.UnAnchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state is VisualState.OverlayFlushing or VisualState.OverlayCharging);
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
? sprite.LayerGetState(chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == VisualState.OverlayFlushing)
{
if (!_animationSystem.HasRunningAnimation(uid, AnimationKey))
{
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer)
? sprite.LayerGetState(flushLayer)
: new RSI.StateId(DefaultFlushState);
// Setup the flush animation to play
var anim = new Animation
{
Length = unit.FlushDelay,
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
KeyFrames =
{
// Play the flush animation
new AnimationTrackSpriteFlick.KeyFrame(flushState, 0),
// Return to base state (though, depending on how the unit is
// configured we might get an appearance change event telling
// us to go to charging state)
new AnimationTrackSpriteFlick.KeyFrame(chargingState, (float) unit.FlushDelay.TotalSeconds)
}
},
}
};
if (unit.FlushSound != null)
{
anim.AnimationTracks.Add(
new AnimationTrackPlaySound
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(unit.FlushSound), 0)
}
});
}
_animationSystem.Play(uid, anim, AnimationKey);
}
}
else if (state == VisualState.OverlayCharging)
sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, chargingState);
else
_animationSystem.Stop(uid, AnimationKey);
if (!_appearanceSystem.TryGetData<HandleState>(uid, Visuals.Handle, out var handleState, appearance))
handleState = HandleState.Normal;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal);
if (!_appearanceSystem.TryGetData<LightStates>(uid, Visuals.Light, out var lightState, appearance))
lightState = LightStates.Off;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging,
(lightState & LightStates.Charging) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayReady,
(lightState & LightStates.Ready) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFull,
(lightState & LightStates.Full) != 0);
}
}
public enum DisposalUnitVisualLayers : byte
{
Unanchored,
Base,
BaseCharging,
OverlayFlush,
OverlayCharging,
OverlayReady,
OverlayFull,
OverlayEngaged
}

View File

@@ -1,9 +1,8 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent; using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
namespace Content.Client.Disposal.UI namespace Content.Client.Disposal.Tube
{ {
/// <summary> /// <summary>
/// Initializes a <see cref="DisposalRouterWindow"/> and updates it when new server messages are received. /// Initializes a <see cref="DisposalRouterWindow"/> and updates it when new server messages are received.

View File

@@ -1,11 +1,10 @@
using Content.Shared.Disposal.Components; using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent; using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
namespace Content.Client.Disposal.UI namespace Content.Client.Disposal.Tube
{ {
/// <summary> /// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalRouterComponent"/> /// Client-side UI used to control a <see cref="SharedDisposalRouterComponent"/>

View File

@@ -1,9 +1,8 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent; using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Client.Disposal.UI namespace Content.Client.Disposal.Tube
{ {
/// <summary> /// <summary>
/// Initializes a <see cref="DisposalTaggerWindow"/> and updates it when new server messages are received. /// Initializes a <see cref="DisposalTaggerWindow"/> and updates it when new server messages are received.

View File

@@ -1,11 +1,10 @@
using Content.Shared.Disposal.Components; using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent; using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Client.Disposal.UI namespace Content.Client.Disposal.Tube
{ {
/// <summary> /// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalTaggerComponent"/> /// Client-side UI used to control a <see cref="SharedDisposalTaggerComponent"/>

View File

@@ -0,0 +1,8 @@
using Content.Shared.Disposal.Unit;
namespace Content.Client.Disposal.Tube;
public sealed class DisposalTubeSystem : SharedDisposalTubeSystem
{
}

View File

@@ -1,103 +0,0 @@
using Content.Client.Disposal.Systems;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Initializes a <see cref="MailingUnitWindow"/> or a <see cref="DisposalUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
{
// What are you doing here
[ViewVariables]
public MailingUnitWindow? MailingUnitWindow;
[ViewVariables]
public DisposalUnitWindow? DisposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(UiButton button)
{
SendMessage(new UiButtonPressedMessage(button));
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
// the pressure lerp up.
}
private void TargetSelected(ItemList.ItemListSelectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
SendMessage(new TargetSelectedMessage(item.Text));
}
protected override void Open()
{
base.Open();
if (UiKey is MailingUnitUiKey)
{
MailingUnitWindow = new MailingUnitWindow();
MailingUnitWindow.OpenCenteredRight();
MailingUnitWindow.OnClose += Close;
MailingUnitWindow.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
MailingUnitWindow.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
MailingUnitWindow.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
MailingUnitWindow.TargetListContainer.OnItemSelected += TargetSelected;
}
else if (UiKey is DisposalUnitUiKey)
{
DisposalUnitWindow = new DisposalUnitWindow();
DisposalUnitWindow.OpenCenteredRight();
DisposalUnitWindow.OnClose += Close;
DisposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
DisposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
DisposalUnitWindow.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
}
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not MailingUnitBoundUserInterfaceState && state is not DisposalUnitBoundUserInterfaceState)
{
return;
}
switch (state)
{
case MailingUnitBoundUserInterfaceState mailingUnitState:
MailingUnitWindow?.UpdateState(mailingUnitState);
break;
case DisposalUnitBoundUserInterfaceState disposalUnitState:
DisposalUnitWindow?.UpdateState(disposalUnitState);
break;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
MailingUnitWindow?.Dispose();
DisposalUnitWindow?.Dispose();
}
}
}

View File

@@ -1,43 +0,0 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalUnitWindow : DefaultWindow
{
public TimeSpan FullPressure;
public DisposalUnitWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
/// <summary>
/// Update the interface state for the disposals window.
/// </summary>
/// <returns>true if we should stop updating every frame.</returns>
public void UpdateState(DisposalUnitBoundUserInterfaceState state)
{
Title = state.UnitName;
UnitState.Text = state.UnitState;
Power.Pressed = state.Powered;
Engage.Pressed = state.Engaged;
FullPressure = state.FullPressureTime;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,55 +0,0 @@
using Content.Shared.Disposal;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Client-side UI used to control a <see cref="MailingUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class MailingUnitWindow : DefaultWindow
{
public TimeSpan FullPressure;
public MailingUnitWindow()
{
RobustXamlLoader.Load(this);
}
/// <summary>
/// Update the interface state for the disposals window.
/// </summary>
/// <returns>true if we should stop updating every frame.</returns>
public bool UpdateState(MailingUnitBoundUserInterfaceState state)
{
var disposalState = state.DisposalState;
Title = Loc.GetString("ui-mailing-unit-window-title", ("tag", state.Tag ?? " "));
UnitState.Text = disposalState.UnitState;
FullPressure = disposalState.FullPressureTime;
var pressureReached = PressureBar.UpdatePressure(disposalState.FullPressureTime);
Power.Pressed = disposalState.Powered;
Engage.Pressed = disposalState.Engaged;
//UnitTag.Text = state.Tag;
Target.Text = state.Target;
TargetListContainer.Clear();
foreach (var target in state.TargetList)
{
TargetListContainer.AddItem(target);
}
return !disposalState.Powered || pressureReached;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -0,0 +1,63 @@
using Content.Client.Disposal.Mailing;
using Content.Client.Power.EntitySystems;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Disposal.Unit
{
/// <summary>
/// Initializes a <see cref="MailingUnitWindow"/> or a <see cref="_disposalUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
{
[ViewVariables] private DisposalUnitWindow? _disposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(DisposalUnitComponent.UiButton button)
{
SendPredictedMessage(new DisposalUnitComponent.UiButtonPressedMessage(button));
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
// the pressure lerp up.
}
protected override void Open()
{
base.Open();
_disposalUnitWindow = this.CreateWindow<DisposalUnitWindow>();
_disposalUnitWindow.OpenCenteredRight();
_disposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Eject);
_disposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Engage);
_disposalUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Power);
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
Refresh((Owner, component));
}
}
public void Refresh(Entity<DisposalUnitComponent> entity)
{
if (_disposalUnitWindow == null)
return;
var disposalSystem = EntMan.System<DisposalUnitSystem>();
_disposalUnitWindow.Title = EntMan.GetComponent<MetaDataComponent>(entity.Owner).EntityName;
var state = disposalSystem.GetState(entity.Owner, entity.Comp);
_disposalUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{state}");
_disposalUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
_disposalUnitWindow.Engage.Pressed = entity.Comp.Engaged;
_disposalUnitWindow.FullPressure = disposalSystem.EstimatedFullPressure(entity.Owner, entity.Comp);
}
}
}

View File

@@ -0,0 +1,151 @@
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Unit;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Systems;
namespace Content.Client.Disposal.Unit;
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
private const string AnimationKey = "disposal_unit_animation";
private const string DefaultFlushState = "disposal-flush";
private const string DefaultChargeState = "disposal-charging";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, AfterAutoHandleStateEvent>(OnHandleState);
SubscribeLocalEvent<DisposalUnitComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateUI((uid, component));
}
protected override void UpdateUI(Entity<DisposalUnitComponent> entity)
{
if (_uiSystem.TryGetOpenUi<DisposalUnitBoundUserInterface>(entity.Owner, DisposalUnitComponent.DisposalUnitUiKey.Key, out var bui))
{
bui.Refresh(entity);
}
}
protected override void OnDisposalInit(Entity<DisposalUnitComponent> ent, ref ComponentInit args)
{
base.OnDisposalInit(ent, ref args);
if (!TryComp<SpriteComponent>(ent, out var sprite) || !TryComp<AppearanceComponent>(ent, out var appearance))
return;
UpdateState(ent, sprite, appearance);
}
private void OnAppearanceChange(Entity<DisposalUnitComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
UpdateState(ent, args.Sprite, args.Component);
}
/// <summary>
/// Update visuals and tick animation
/// </summary>
private void UpdateState(Entity<DisposalUnitComponent> ent, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.VisualState>(ent, DisposalUnitComponent.Visuals.VisualState, out var state, appearance))
return;
sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == DisposalUnitComponent.VisualState.UnAnchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == DisposalUnitComponent.VisualState.Anchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state == DisposalUnitComponent.VisualState.OverlayFlushing);
sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseCharging, state == DisposalUnitComponent.VisualState.OverlayCharging);
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
? sprite.LayerGetState(chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == DisposalUnitComponent.VisualState.OverlayFlushing)
{
if (!_animationSystem.HasRunningAnimation(ent, AnimationKey))
{
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer)
? sprite.LayerGetState(flushLayer)
: new RSI.StateId(DefaultFlushState);
// Setup the flush animation to play
var anim = new Animation
{
Length = ent.Comp.FlushDelay,
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
KeyFrames =
{
// Play the flush animation
new AnimationTrackSpriteFlick.KeyFrame(flushState, 0),
}
},
}
};
if (ent.Comp.FlushSound != null)
{
anim.AnimationTracks.Add(
new AnimationTrackPlaySound
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0)
}
});
}
_animationSystem.Play(ent, anim, AnimationKey);
}
}
else
_animationSystem.Stop(ent.Owner, AnimationKey);
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.HandleState>(ent, DisposalUnitComponent.Visuals.Handle, out var handleState, appearance))
handleState = DisposalUnitComponent.HandleState.Normal;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != DisposalUnitComponent.HandleState.Normal);
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.LightStates>(ent, DisposalUnitComponent.Visuals.Light, out var lightState, appearance))
lightState = DisposalUnitComponent.LightStates.Off;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging,
(lightState & DisposalUnitComponent.LightStates.Charging) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayReady,
(lightState & DisposalUnitComponent.LightStates.Ready) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFull,
(lightState & DisposalUnitComponent.LightStates.Full) != 0);
}
}
public enum DisposalUnitVisualLayers : byte
{
Unanchored,
Base,
BaseCharging,
OverlayFlush,
OverlayCharging,
OverlayReady,
OverlayFull,
OverlayEngaged
}

View File

@@ -1,20 +1,21 @@
<DefaultWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Disposal.UI" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="300 140" xmlns:disposal="clr-namespace:Content.Client.Disposal"
SetSize="300 140" MinSize="300 140"
SetSize="300 160"
Resizable="False"> Resizable="False">
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical" Margin="10">
<BoxContainer Orientation="Horizontal" <BoxContainer Orientation="Horizontal"
SeparationOverride="4"> SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" /> <Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState" <Label Name="UnitState" Access="Public"
Text="{Loc 'ui-disposal-unit-label-status'}" /> Text="{Loc 'ui-disposal-unit-label-status'}" />
</BoxContainer> </BoxContainer>
<Control MinSize="0 5" /> <Control MinSize="0 5" />
<BoxContainer Orientation="Horizontal" <BoxContainer Orientation="Horizontal"
SeparationOverride="4"> SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" /> <Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<ui:PressureBar Name="PressureBar" <disposal:PressureBar Name="PressureBar"
MinSize="190 20" MinSize="190 20"
HorizontalAlignment="Right" HorizontalAlignment="Right"
MinValue="0" MinValue="0"
@@ -23,7 +24,7 @@
Value="0.5" /> Value="0.5" />
</BoxContainer> </BoxContainer>
<Control MinSize="0 10" /> <Control MinSize="0 10" />
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="Engage" <Button Name="Engage"
Access="Public" Access="Public"
Text="{Loc 'ui-disposal-unit-button-flush'}" Text="{Loc 'ui-disposal-unit-button-flush'}"
@@ -39,4 +40,4 @@
StyleClasses="OpenLeft" /> StyleClasses="OpenLeft" />
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</DefaultWindow> </controls:FancyWindow>

View File

@@ -0,0 +1,28 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.Unit
{
/// <summary>
/// Client-side UI used to control a <see cref="Shared.Disposal.Components.DisposalUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public DisposalUnitWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems; using Content.Shared.Doors.Systems;
using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
namespace Content.Client.Doors; namespace Content.Client.Doors;
@@ -14,6 +15,30 @@ public sealed class FirelockSystem : SharedFirelockSystem
SubscribeLocalEvent<FirelockComponent, AppearanceChangeEvent>(OnAppearanceChange); SubscribeLocalEvent<FirelockComponent, AppearanceChangeEvent>(OnAppearanceChange);
} }
protected override void OnComponentStartup(Entity<FirelockComponent> ent, ref ComponentStartup args)
{
base.OnComponentStartup(ent, ref args);
if(!TryComp<DoorComponent>(ent.Owner, out var door))
return;
door.ClosedSpriteStates.Add((DoorVisualLayers.BaseUnlit, ent.Comp.WarningLightSpriteState));
door.OpenSpriteStates.Add((DoorVisualLayers.BaseUnlit, ent.Comp.WarningLightSpriteState));
((Animation)door.OpeningAnimation).AnimationTracks.Add(new AnimationTrackSpriteFlick()
{
LayerKey = DoorVisualLayers.BaseUnlit,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.OpeningLightSpriteState, 0f) },
}
);
((Animation)door.ClosingAnimation).AnimationTracks.Add(new AnimationTrackSpriteFlick()
{
LayerKey = DoorVisualLayers.BaseUnlit,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.ClosingLightSpriteState, 0f) },
}
);
}
private void OnAppearanceChange(EntityUid uid, FirelockComponent comp, ref AppearanceChangeEvent args) private void OnAppearanceChange(EntityUid uid, FirelockComponent comp, ref AppearanceChangeEvent args)
{ {
if (args.Sprite == null) if (args.Sprite == null)

View File

@@ -0,0 +1,75 @@
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Examine;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Doors;
/// <inheritdoc/>
public sealed class TurnstileSystem : SharedTurnstileSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
private static EntProtoId _examineArrow = "TurnstileArrow";
private const string AnimationKey = "Turnstile";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TurnstileComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<TurnstileComponent, ExaminedEvent>(OnExamined);
}
private void OnAnimationCompleted(Entity<TurnstileComponent> ent, ref AnimationCompletedEvent args)
{
if (args.Key != AnimationKey)
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
sprite.LayerSetState(TurnstileVisualLayers.Base, new RSI.StateId(ent.Comp.DefaultState));
}
private void OnExamined(Entity<TurnstileComponent> ent, ref ExaminedEvent args)
{
Spawn(_examineArrow, new EntityCoordinates(ent, 0, 0));
}
protected override void PlayAnimation(EntityUid uid, string stateId)
{
if (!TryComp<AnimationPlayerComponent>(uid, out var animation) || !TryComp<SpriteComponent>(uid, out var sprite))
return;
var ent = (uid, animation);
if (_animationPlayer.HasRunningAnimation(animation, AnimationKey))
_animationPlayer.Stop(ent, AnimationKey);
if (sprite.BaseRSI == null || !sprite.BaseRSI.TryGetState(stateId, out var state))
return;
var animLength = state.AnimationLength;
var anim = new Animation
{
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = TurnstileVisualLayers.Base,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(state.StateId, 0f),
},
},
},
Length = TimeSpan.FromSeconds(animLength),
};
_animationPlayer.Play(ent, anim, AnimationKey);
}
}

View File

@@ -21,6 +21,7 @@ using Content.Client.Replay;
using Content.Client.Screenshot; using Content.Client.Screenshot;
using Content.Client.Singularity; using Content.Client.Singularity;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.UserInterface;
using Content.Client.Viewport; using Content.Client.Viewport;
using Content.Client.Voting; using Content.Client.Voting;
using Content.Shared.Ame.Components; using Content.Shared.Ame.Components;
@@ -74,6 +75,7 @@ namespace Content.Client.Entry
[Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!; [Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!; [Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
public override void Init() public override void Init()
{ {
@@ -227,6 +229,15 @@ namespace Content.Client.Entry
{ {
_debugMonitorManager.FrameUpdate(); _debugMonitorManager.FrameUpdate();
} }
if (level == ModUpdateLevel.PreEngine)
{
if (_baseClient.RunLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame)
{
var updateSystem = _entitySystemManager.GetEntitySystem<BuiPreTickUpdateSystem>();
updateSystem.RunUpdates();
}
}
} }
} }
} }

View File

@@ -32,10 +32,11 @@ namespace Content.Client.Examine
[Dependency] private readonly VerbSystem _verbSystem = default!; [Dependency] private readonly VerbSystem _verbSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!; [Dependency] private readonly SpriteSystem _sprite = default!;
private List<Verb> _verbList = new();
public const string StyleClassEntityTooltip = "entity-tooltip"; public const string StyleClassEntityTooltip = "entity-tooltip";
private EntityUid _examinedEntity; private EntityUid _examinedEntity;
private EntityUid _lastExaminedEntity;
private Popup? _examineTooltipOpen; private Popup? _examineTooltipOpen;
private ScreenCoordinates _popupPos; private ScreenCoordinates _popupPos;
private CancellationTokenSource? _requestCancelTokenSource; private CancellationTokenSource? _requestCancelTokenSource;
@@ -158,13 +159,13 @@ namespace Content.Client.Examine
var entity = GetEntity(ev.EntityUid); var entity = GetEntity(ev.EntityUid);
OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget); OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget);
UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs); UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs, getVerbs: false);
} }
public override void SendExamineTooltip(EntityUid player, EntityUid target, FormattedMessage message, bool getVerbs, bool centerAtCursor) public override void SendExamineTooltip(EntityUid player, EntityUid target, FormattedMessage message, bool getVerbs, bool centerAtCursor)
{ {
OpenTooltip(player, target, centerAtCursor, false); OpenTooltip(player, target, centerAtCursor);
UpdateTooltipInfo(player, target, message); UpdateTooltipInfo(player, target, message, getVerbs: getVerbs);
} }
/// <summary> /// <summary>
@@ -259,7 +260,7 @@ namespace Content.Client.Examine
/// <summary> /// <summary>
/// Fills the examine tooltip with a message and buttons if applicable. /// Fills the examine tooltip with a message and buttons if applicable.
/// </summary> /// </summary>
public void UpdateTooltipInfo(EntityUid player, EntityUid target, FormattedMessage message, List<Verb>? verbs=null) public void UpdateTooltipInfo(EntityUid player, EntityUid target, FormattedMessage message, List<Verb>? verbs=null, bool getVerbs = true)
{ {
var vBox = _examineTooltipOpen?.GetChild(0).GetChild(0); var vBox = _examineTooltipOpen?.GetChild(0).GetChild(0);
if (vBox == null) if (vBox == null)
@@ -283,9 +284,29 @@ namespace Content.Client.Examine
break; break;
} }
verbs ??= new List<Verb>();
var totalVerbs = _verbSystem.GetLocalVerbs(target, player, typeof(ExamineVerb)); var totalVerbs = _verbSystem.GetLocalVerbs(target, player, typeof(ExamineVerb));
totalVerbs.UnionWith(verbs);
// We still need client-exclusive verbs even when the server sends its data in so if that's the case
// we remove any non-client-exclusive verbs.
if (!getVerbs)
{
_verbList.AddRange(totalVerbs);
foreach (var verb in _verbList)
{
if (!verb.ClientExclusive)
{
totalVerbs.Remove(verb);
}
}
_verbList.Clear();
}
if (verbs != null)
{
totalVerbs.UnionWith(verbs);
}
AddVerbsToTooltip(totalVerbs); AddVerbsToTooltip(totalVerbs);
} }
@@ -394,15 +415,14 @@ namespace Content.Client.Examine
if (!IsClientSide(entity)) if (!IsClientSide(entity))
{ {
// Ask server for extra examine info. // Ask server for extra examine info.
if (entity != _lastExaminedEntity) unchecked
{
_idCounter += 1; _idCounter += 1;
if (_idCounter == int.MaxValue) }
_idCounter = 0;
RaiseNetworkEvent(new ExamineSystemMessages.RequestExamineInfoMessage(GetNetEntity(entity), _idCounter, true)); RaiseNetworkEvent(new ExamineSystemMessages.RequestExamineInfoMessage(GetNetEntity(entity), _idCounter, true));
} }
RaiseLocalEvent(entity, new ClientExaminedEvent(entity, playerEnt.Value)); RaiseLocalEvent(entity, new ClientExaminedEvent(entity, playerEnt.Value));
_lastExaminedEntity = entity;
} }
private void CloseTooltip() private void CloseTooltip()

View File

@@ -93,7 +93,7 @@ public sealed partial class TriggerSystem
break; break;
case ProximityTriggerVisuals.Active: case ProximityTriggerVisuals.Active:
if (_player.HasRunningAnimation(uid, player, AnimKey)) return; if (_player.HasRunningAnimation(uid, player, AnimKey)) return;
_player.Play(uid, player, _flasherAnimation, AnimKey); _player.Play((uid, player), _flasherAnimation, AnimKey);
break; break;
case ProximityTriggerVisuals.Off: case ProximityTriggerVisuals.Off:
default: default:

View File

@@ -34,7 +34,7 @@ public sealed class EyeLerpingSystem : EntitySystem
SubscribeLocalEvent<LerpingEyeComponent, LocalPlayerDetachedEvent>(OnDetached); SubscribeLocalEvent<LerpingEyeComponent, LocalPlayerDetachedEvent>(OnDetached);
UpdatesAfter.Add(typeof(TransformSystem)); UpdatesAfter.Add(typeof(TransformSystem));
UpdatesAfter.Add(typeof(PhysicsSystem)); UpdatesAfter.Add(typeof(Robust.Client.Physics.PhysicsSystem));
UpdatesBefore.Add(typeof(SharedEyeSystem)); UpdatesBefore.Add(typeof(SharedEyeSystem));
UpdatesOutsidePrediction = true; UpdatesOutsidePrediction = true;
} }

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