diff --git a/.editorconfig b/.editorconfig index 58d0d332bbe6..a5dfab07a503 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,5 @@ root = true + [*] charset = utf-8 @@ -9,9 +10,10 @@ indent_style = space tab_width = 4 # New line preferences -end_of_line = crlf:suggestion +#end_of_line = crlf insert_final_newline = true trim_trailing_whitespace = true +max_line_length = 120 #### .NET Coding Conventions #### @@ -104,7 +106,6 @@ csharp_preferred_modifier_order = public, private, protected, internal, new, abs # 'using' directive preferences csharp_using_directive_placement = outside_namespace:silent -csharp_style_namespace_declarations = file_scoped:suggestion #### C# Formatting Rules #### @@ -278,7 +279,7 @@ dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case dotnet_naming_style.t_upper_camel_case_style.required_prefix = T dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case -dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected dotnet_naming_symbols.constants_symbols.applicable_kinds = field dotnet_naming_symbols.constants_symbols.required_modifiers = const @@ -317,27 +318,32 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field -dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly dotnet_naming_symbols.property_symbols.applicable_accessibilities = * dotnet_naming_symbols.property_symbols.applicable_kinds = property -dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field -dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field -dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly +dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = * -dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace,class,struct,enum,delegate +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace, class, struct, enum, delegate dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter # ReSharper properties resharper_braces_for_ifelse = required_for_multiline +resharper_csharp_wrap_arguments_style = chop_if_long +resharper_csharp_wrap_parameters_style = chop_if_long resharper_keep_existing_attribute_arrangement = true +resharper_wrap_chained_binary_patterns = chop_if_long +resharper_wrap_chained_method_calls = chop_if_long +resharper_csharp_trailing_comma_in_multiline_lists = true [*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}] indent_size = 2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 69c5f0138ff5..da9d4d693a81 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,7 @@ /Content.*/GameTicking/ @moonheart08 @EmoGarbage404 /Resources/ServerInfo/ @moonheart08 @Chief-Engineer /Resources/ServerInfo/Guidebook/ @moonheart08 @EmoGarbage404 +/Resources/ServerInfo/Guidebook/ServerRules/ @Chief-Engineer /Resources/engineCommandPerms.yml @moonheart08 @Chief-Engineer /Resources/clientCommandPerms.yml @moonheart08 @Chief-Engineer @@ -23,11 +24,12 @@ /Resources/Prototypes/Body/ @DrSmugleaf # suffering /Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf /Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf +/Resources/Prototypes/Guidebook/rules.yml @Chief-Engineer /Content.*/Body/ @DrSmugleaf /Content.YAMLLinter @DrSmugleaf /Content.Shared/Damage/ @DrSmugleaf -/Content.*/Anomaly/ @EmoGarbage404 +/Content.*/Anomaly/ @EmoGarbage404 @TheShuEd /Content.*/Lathe/ @EmoGarbage404 /Content.*/Materials/ @EmoGarbage404 /Content.*/Mech/ @EmoGarbage404 @@ -35,7 +37,7 @@ /Content.*/Stack/ @EmoGarbage404 /Content.*/Xenoarchaeology/ @EmoGarbage404 /Content.*/Zombies/ @EmoGarbage404 -/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @EmoGarbage404 +/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @EmoGarbage404 @TheShuEd /Resources/Prototypes/Research/ @EmoGarbage404 /Content.*/Forensics/ @ficcialfaint diff --git a/.github/labeler.yml b/.github/labeler.yml index b088f229434c..886ce89708c8 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,12 +1,18 @@ "Changes: Sprites": -- '**/*.rsi/*.png' +- changed-files: + - any-glob-to-any-file: '**/*.rsi/*.png' "Changes: Map": -- 'Resources/Maps/*.yml' -- 'Resources/Prototypes/Maps/*.yml' +- changed-files: + - any-glob-to-any-file: + - 'Resources/Maps/*.yml' + - 'Resources/Prototypes/Maps/*.yml' "Changes: UI": -- '**/*.xaml*' +- changed-files: + - any-glob-to-any-file: '**/*.xaml*' "No C#": -- all: ["!**/*.cs"] +- changed-files: + # Equiv to any-glob-to-all as long as this has one matcher. If ALL changed files are not C# files, then apply label. + - all-globs-to-all-files: "!**/*.cs" diff --git a/.github/workflows/conflict-labeler.yml b/.github/workflows/conflict-labeler.yml index a78716bde685..1e2125c30a23 100644 --- a/.github/workflows/conflict-labeler.yml +++ b/.github/workflows/conflict-labeler.yml @@ -1,18 +1,20 @@ name: Check Merge Conflicts on: - push: - branches: - - master pull_request_target: + types: + - opened + - synchronize + - reopened + - ready_for_review jobs: Label: - if: github.actor != 'PJBot' + if: ( github.event.pull_request.draft == false ) && ( github.actor != 'PJBot' ) runs-on: ubuntu-latest steps: - name: Check for Merge Conflicts - uses: ike709/actions-label-merge-conflict@9eefdd17e10566023c46d2dc6dc04fcb8ec76142 + uses: eps1lon/actions-label-merge-conflict@v3.0.0 with: dirtyLabel: "Merge Conflict" repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/labeler-pr.yml b/.github/workflows/labeler-pr.yml index 711eb0ccac01..42ed10098120 100644 --- a/.github/workflows/labeler-pr.yml +++ b/.github/workflows/labeler-pr.yml @@ -6,8 +6,9 @@ on: jobs: labeler: if: github.actor != 'PJBot' + permissions: + contents: read + pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v3 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + - uses: actions/labeler@v5 diff --git a/.github/workflows/rsi-diff.yml b/.github/workflows/rsi-diff.yml index 1f122526d738..98cc97e9221d 100644 --- a/.github/workflows/rsi-diff.yml +++ b/.github/workflows/rsi-diff.yml @@ -15,9 +15,12 @@ jobs: - name: Get changed files id: files - uses: Ana06/get-changed-files@v1.2 + uses: Ana06/get-changed-files@v2.3.0 with: format: 'space-delimited' + filter: | + **.rsi + **.png - name: Diff changed RSIs id: diff diff --git a/Content.Benchmarks/ComponentQueryBenchmark.cs b/Content.Benchmarks/ComponentQueryBenchmark.cs new file mode 100644 index 000000000000..11c7ab9d5f59 --- /dev/null +++ b/Content.Benchmarks/ComponentQueryBenchmark.cs @@ -0,0 +1,273 @@ +#nullable enable +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using Content.IntegrationTests; +using Content.IntegrationTests.Pair; +using Content.Shared.Clothing.Components; +using Content.Shared.Doors.Components; +using Content.Shared.Item; +using Robust.Server.GameObjects; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Random; + +namespace Content.Benchmarks; + +/// +/// Benchmarks for comparing the speed of various component fetching/lookup related methods, including directed event +/// subscriptions +/// +[Virtual] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class ComponentQueryBenchmark +{ + public const string Map = "Maps/atlas.yml"; + + private TestPair _pair = default!; + private IEntityManager _entMan = default!; + private MapId _mapId = new(10); + private EntityQuery _itemQuery; + private EntityQuery _clothingQuery; + private EntityQuery _mapQuery; + private EntityUid[] _items = default!; + + [GlobalSetup] + public void Setup() + { + ProgramShared.PathOffset = "../../../../"; + PoolManager.Startup(typeof(QueryBenchSystem).Assembly); + + _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _entMan = _pair.Server.ResolveDependency(); + + _itemQuery = _entMan.GetEntityQuery(); + _clothingQuery = _entMan.GetEntityQuery(); + _mapQuery = _entMan.GetEntityQuery(); + + _pair.Server.ResolveDependency().SetSeed(42); + _pair.Server.WaitPost(() => + { + var success = _entMan.System().TryLoad(_mapId, Map, out _); + if (!success) + throw new Exception("Map load failed"); + _pair.Server.MapMan.DoMapInitialize(_mapId); + }).GetAwaiter().GetResult(); + + _items = new EntityUid[_entMan.Count()]; + var i = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out _)) + { + _items[i++] = uid; + } + } + + [GlobalCleanup] + public async Task Cleanup() + { + await _pair.DisposeAsync(); + PoolManager.Shutdown(); + } + + #region TryComp + + /// + /// Baseline TryComp benchmark. When the benchmark was created, around 40% of the items were clothing. + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("TryComp")] + public int TryComp() + { + var hashCode = 0; + foreach (var uid in _items) + { + if (_clothingQuery.TryGetComponent(uid, out var clothing)) + hashCode = HashCode.Combine(hashCode, clothing.GetHashCode()); + } + return hashCode; + } + + /// + /// Variant of that is meant to always fail to get a component. + /// + [Benchmark] + [BenchmarkCategory("TryComp")] + public int TryCompFail() + { + var hashCode = 0; + foreach (var uid in _items) + { + if (_mapQuery.TryGetComponent(uid, out var map)) + hashCode = HashCode.Combine(hashCode, map.GetHashCode()); + } + return hashCode; + } + + /// + /// Variant of that is meant to always succeed getting a component. + /// + [Benchmark] + [BenchmarkCategory("TryComp")] + public int TryCompSucceed() + { + var hashCode = 0; + foreach (var uid in _items) + { + if (_itemQuery.TryGetComponent(uid, out var item)) + hashCode = HashCode.Combine(hashCode, item.GetHashCode()); + } + return hashCode; + } + + /// + /// Variant of that uses `Resolve()` to try get the component. + /// + [Benchmark] + [BenchmarkCategory("TryComp")] + public int Resolve() + { + var hashCode = 0; + foreach (var uid in _items) + { + DoResolve(uid, ref hashCode); + } + return hashCode; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DoResolve(EntityUid uid, ref int hash, ClothingComponent? clothing = null) + { + if (_clothingQuery.Resolve(uid, ref clothing, false)) + hash = HashCode.Combine(hash, clothing.GetHashCode()); + } + + #endregion + + #region Enumeration + + [Benchmark] + [BenchmarkCategory("Item Enumerator")] + public int SingleItemEnumerator() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out var item)) + { + hashCode = HashCode.Combine(hashCode, item.GetHashCode()); + } + + return hashCode; + } + + [Benchmark] + [BenchmarkCategory("Item Enumerator")] + public int DoubleItemEnumerator() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out _, out var item)) + { + hashCode = HashCode.Combine(hashCode, item.GetHashCode()); + } + + return hashCode; + } + + [Benchmark] + [BenchmarkCategory("Item Enumerator")] + public int TripleItemEnumerator() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out _, out _, out var xform)) + { + hashCode = HashCode.Combine(hashCode, xform.GetHashCode()); + } + + return hashCode; + } + + [Benchmark] + [BenchmarkCategory("Airlock Enumerator")] + public int SingleAirlockEnumerator() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out var airlock)) + { + hashCode = HashCode.Combine(hashCode, airlock.GetHashCode()); + } + + return hashCode; + } + + [Benchmark] + [BenchmarkCategory("Airlock Enumerator")] + public int DoubleAirlockEnumerator() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out _, out var door)) + { + hashCode = HashCode.Combine(hashCode, door.GetHashCode()); + } + + return hashCode; + } + + [Benchmark] + [BenchmarkCategory("Airlock Enumerator")] + public int TripleAirlockEnumerator() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out _, out _, out var xform)) + { + hashCode = HashCode.Combine(hashCode, xform.GetHashCode()); + } + + return hashCode; + } + + #endregion + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Events")] + public int StructEvents() + { + var ev = new QueryBenchEvent(); + foreach (var uid in _items) + { + _entMan.EventBus.RaiseLocalEvent(uid, ref ev); + } + + return ev.HashCode; + } +} + +[ByRefEvent] +public struct QueryBenchEvent +{ + public int HashCode; +} + +public sealed class QueryBenchSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnEvent); + } + + private void OnEvent(EntityUid uid, ClothingComponent component, ref QueryBenchEvent args) + { + args.HashCode = HashCode.Combine(args.HashCode, component.GetHashCode()); + } +} diff --git a/Content.Benchmarks/EntityManagerGetAllComponents.cs b/Content.Benchmarks/EntityManagerGetAllComponents.cs index 0b9683a4abb6..8e02b8d71de6 100644 --- a/Content.Benchmarks/EntityManagerGetAllComponents.cs +++ b/Content.Benchmarks/EntityManagerGetAllComponents.cs @@ -47,6 +47,7 @@ public void Setup() var componentFactory = new Mock(); componentFactory.Setup(p => p.GetComponent()).Returns(new DummyComponent()); + componentFactory.Setup(m => m.GetIndex(typeof(DummyComponent))).Returns(CompIdx.Index()); componentFactory.Setup(p => p.GetRegistration(It.IsAny())).Returns(dummyReg); componentFactory.Setup(p => p.GetAllRegistrations()).Returns(new[] { dummyReg }); componentFactory.Setup(p => p.GetAllRefTypes()).Returns(new[] { CompIdx.Index() }); diff --git a/Content.Benchmarks/EntityQueryBenchmark.cs b/Content.Benchmarks/EntityQueryBenchmark.cs deleted file mode 100644 index cef6a5e35c58..000000000000 --- a/Content.Benchmarks/EntityQueryBenchmark.cs +++ /dev/null @@ -1,137 +0,0 @@ -#nullable enable -using System; -using System.Threading.Tasks; -using BenchmarkDotNet.Attributes; -using Content.IntegrationTests; -using Content.IntegrationTests.Pair; -using Content.Shared.Clothing.Components; -using Content.Shared.Item; -using Robust.Server.GameObjects; -using Robust.Shared; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Random; - -namespace Content.Benchmarks; - -[Virtual] -public class EntityQueryBenchmark -{ - public const string Map = "Maps/atlas.yml"; - - private TestPair _pair = default!; - private IEntityManager _entMan = default!; - private MapId _mapId = new MapId(10); - private EntityQuery _clothingQuery; - - [GlobalSetup] - public void Setup() - { - ProgramShared.PathOffset = "../../../../"; - PoolManager.Startup(null); - - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); - _entMan = _pair.Server.ResolveDependency(); - - _pair.Server.ResolveDependency().SetSeed(42); - _pair.Server.WaitPost(() => - { - var success = _entMan.System().TryLoad(_mapId, Map, out _); - if (!success) - throw new Exception("Map load failed"); - _pair.Server.MapMan.DoMapInitialize(_mapId); - }).GetAwaiter().GetResult(); - - _clothingQuery = _entMan.GetEntityQuery(); - - // Apparently ~40% of entities are items, and 1 in 6 of those are clothing. - /* - var entCount = _entMan.EntityCount; - var itemCount = _entMan.Count(); - var clothingCount = _entMan.Count(); - var itemRatio = (float) itemCount / entCount; - var clothingRatio = (float) clothingCount / entCount; - Console.WriteLine($"Entities: {entCount}. Items: {itemRatio:P2}. Clothing: {clothingRatio:P2}."); - */ - } - - [GlobalCleanup] - public async Task Cleanup() - { - await _pair.DisposeAsync(); - PoolManager.Shutdown(); - } - - [Benchmark] - public int HasComponent() - { - var hashCode = 0; - var enumerator = _entMan.AllEntityQueryEnumerator(); - while (enumerator.MoveNext(out var uid, out var _)) - { - if (_entMan.HasComponent(uid)) - hashCode = HashCode.Combine(hashCode, uid.Id); - } - - return hashCode; - } - - [Benchmark] - public int HasComponentQuery() - { - var hashCode = 0; - var enumerator = _entMan.AllEntityQueryEnumerator(); - while (enumerator.MoveNext(out var uid, out var _)) - { - if (_clothingQuery.HasComponent(uid)) - hashCode = HashCode.Combine(hashCode, uid.Id); - } - - return hashCode; - } - - [Benchmark] - public int TryGetComponent() - { - var hashCode = 0; - var enumerator = _entMan.AllEntityQueryEnumerator(); - while (enumerator.MoveNext(out var uid, out var _)) - { - if (_entMan.TryGetComponent(uid, out ClothingComponent? clothing)) - hashCode = HashCode.Combine(hashCode, clothing.GetHashCode()); - } - - return hashCode; - } - - [Benchmark] - public int TryGetComponentQuery() - { - var hashCode = 0; - var enumerator = _entMan.AllEntityQueryEnumerator(); - while (enumerator.MoveNext(out var uid, out var _)) - { - if (_clothingQuery.TryGetComponent(uid, out var clothing)) - hashCode = HashCode.Combine(hashCode, clothing.GetHashCode()); - } - - return hashCode; - } - - /// - /// Enumerate all entities with both an item and clothing component. - /// - [Benchmark] - public int Enumerator() - { - var hashCode = 0; - var enumerator = _entMan.AllEntityQueryEnumerator(); - while (enumerator.MoveNext(out var _, out var clothing)) - { - hashCode = HashCode.Combine(hashCode, clothing.GetHashCode()); - } - - return hashCode; - } -} diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs index ce95e9a158db..a3319e3067ff 100644 --- a/Content.Benchmarks/MapLoadBenchmark.cs +++ b/Content.Benchmarks/MapLoadBenchmark.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -26,7 +26,7 @@ public class MapLoadBenchmark public void Setup() { ProgramShared.PathOffset = "../../../../"; - PoolManager.Startup(null); + PoolManager.Startup(); _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); var server = _pair.Server; @@ -46,7 +46,7 @@ public async Task Cleanup() PoolManager.Shutdown(); } - public static readonly string[] MapsSource = { "Empty", "Box", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry" }; + public static readonly string[] MapsSource = { "Empty", "Box", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry", "Oasis" }; [ParamsSource(nameof(MapsSource))] public string Map; diff --git a/Content.Benchmarks/PvsBenchmark.cs b/Content.Benchmarks/PvsBenchmark.cs index c7f22bdb0cdb..fa7f9d454226 100644 --- a/Content.Benchmarks/PvsBenchmark.cs +++ b/Content.Benchmarks/PvsBenchmark.cs @@ -1,19 +1,17 @@ #nullable enable using System; -using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; using Content.IntegrationTests.Pair; +using Content.Server.Mind; using Content.Server.Warps; using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.Analyzers; -using Robust.Shared.Enums; using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; using Robust.Shared.Map; -using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Random; @@ -49,7 +47,7 @@ public void Setup() #if !DEBUG ProgramShared.PathOffset = "../../../../"; #endif - PoolManager.Startup(null); + PoolManager.Startup(); _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); _entMan = _pair.Server.ResolveDependency(); @@ -58,15 +56,20 @@ public void Setup() _pair.Server.CfgMan.SetCVar(CVars.NetPvsAsync, false); _sys = _entMan.System(); + SetupAsync().Wait(); + } + + private async Task SetupAsync() + { // Spawn the map _pair.Server.ResolveDependency().SetSeed(42); - _pair.Server.WaitPost(() => + await _pair.Server.WaitPost(() => { var success = _entMan.System().TryLoad(_mapId, Map, out _); if (!success) throw new Exception("Map load failed"); _pair.Server.MapMan.DoMapInitialize(_mapId); - }).Wait(); + }); // Get list of ghost warp positions _spawns = _entMan.AllComponentsList() @@ -76,17 +79,19 @@ public void Setup() Array.Resize(ref _players, PlayerCount); - // Spawn "Players". - _pair.Server.WaitPost(() => + // Spawn "Players" + _players = await _pair.Server.AddDummySessions(PlayerCount); + await _pair.Server.WaitPost(() => { + var mind = _pair.Server.System(); for (var i = 0; i < PlayerCount; i++) { var pos = _spawns[i % _spawns.Length]; var uid =_entMan.SpawnEntity("MobHuman", pos); _pair.Server.ConsoleHost.ExecuteCommand($"setoutfit {_entMan.GetNetEntity(uid)} CaptainGear"); - _players[i] = new DummySession{AttachedEntity = uid}; + mind.ControlMob(_players[i].UserId, uid); } - }).Wait(); + }); // Repeatedly move players around so that they "explore" the map and see lots of entities. // This will populate their PVS data with out-of-view entities. @@ -168,20 +173,4 @@ public void CycleTick() }).Wait(); _pair.Server.PvsTick(_players); } - - private sealed class DummySession : ICommonSession - { - public SessionStatus Status => SessionStatus.InGame; - public EntityUid? AttachedEntity {get; set; } - public NetUserId UserId => default; - public string Name => string.Empty; - public short Ping => default; - public INetChannel Channel { get; set; } = default!; - public LoginType AuthType => default; - public HashSet ViewSubscriptions { get; } = new(); - public DateTime ConnectedTime { get; set; } - public SessionState State => default!; - public SessionData Data => default!; - public bool ClientSide { get; set; } - } } diff --git a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs index 8512107b69de..0638d945aa5c 100644 --- a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs +++ b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs @@ -32,7 +32,7 @@ public class SpawnEquipDeleteBenchmark public async Task SetupAsync() { ProgramShared.PathOffset = "../../../../"; - PoolManager.Startup(null); + PoolManager.Startup(); _pair = await PoolManager.GetServerClient(); var server = _pair.Server; diff --git a/Content.Client/Access/IdCardSystem.cs b/Content.Client/Access/IdCardSystem.cs index fcf2bf57de38..e0c02976f7b3 100644 --- a/Content.Client/Access/IdCardSystem.cs +++ b/Content.Client/Access/IdCardSystem.cs @@ -2,6 +2,4 @@ namespace Content.Client.Access; -public sealed class IdCardSystem : SharedIdCardSystem -{ -} +public sealed class IdCardSystem : SharedIdCardSystem; diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs index 73f18aec8d6b..761f52988a9d 100644 --- a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs +++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs @@ -1,5 +1,7 @@ using Content.Shared.Access.Systems; +using Content.Shared.StatusIcon; using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; namespace Content.Client.Access.UI { @@ -40,9 +42,9 @@ private void OnJobChanged(string newJob) SendMessage(new AgentIDCardJobChangedMessage(newJob)); } - public void OnJobIconChanged(string newJobIcon) + public void OnJobIconChanged(ProtoId newJobIconId) { - SendMessage(new AgentIDCardJobIconChangedMessage(newJobIcon)); + SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId)); } /// @@ -57,7 +59,7 @@ protected override void UpdateState(BoundUserInterfaceState state) _window.SetCurrentName(cast.CurrentName); _window.SetCurrentJob(cast.CurrentJob); - _window.SetAllowedIcons(cast.Icons); + _window.SetAllowedIcons(cast.Icons, cast.CurrentJobIconId); } protected override void Dispose(bool disposing) diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs index beca0c41ba93..6d0b2a184f49 100644 --- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs +++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs @@ -38,7 +38,7 @@ public AgentIDCardWindow(AgentIDCardBoundUserInterface bui) JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text); } - public void SetAllowedIcons(HashSet icons) + public void SetAllowedIcons(HashSet> icons, string currentJobIconId) { IconGrid.DisposeAllChildren(); @@ -46,10 +46,8 @@ public void SetAllowedIcons(HashSet icons) var i = 0; foreach (var jobIconId in icons) { - if (!_prototypeManager.TryIndex(jobIconId, out var jobIcon)) - { + if (!_prototypeManager.TryIndex(jobIconId, out var jobIcon)) continue; - } String styleBase = StyleBase.ButtonOpenBoth; var modulo = i % JobIconColumnCount; @@ -77,8 +75,12 @@ public void SetAllowedIcons(HashSet icons) }; jobIconButton.AddChild(jobIconTexture); - jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIcon.ID); + jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIconId); IconGrid.AddChild(jobIconButton); + + if (jobIconId.Equals(currentJobIconId)) + jobIconButton.Pressed = true; + i++; } } diff --git a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs index 5b7011c195ab..a321b4121e58 100644 --- a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs +++ b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs @@ -1,6 +1,5 @@ using Content.Shared.Access; using Content.Shared.Access.Components; -using Content.Shared.Access; using Content.Shared.Access.Systems; using Content.Shared.Containers.ItemSlots; using Content.Shared.CrewManifest; diff --git a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs index 298912e7d536..82f6ebd8b59c 100644 --- a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs +++ b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs @@ -27,6 +27,9 @@ public sealed partial class IdCardConsoleWindow : DefaultWindow private string? _lastJobTitle; private string? _lastJobProto; + // The job that will be picked if the ID doesn't have a job on the station. + private static ProtoId _defaultJob = "Passenger"; + public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager, List> accessLevels) { @@ -65,7 +68,6 @@ public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeMana } JobPresetOptionButton.OnItemSelected += SelectJobPreset; - _accessButtons.Populate(accessLevels, prototypeManager); AccessLevelControlContainer.AddChild(_accessButtons); @@ -172,11 +174,15 @@ public void UpdateState(IdCardConsoleBoundUserInterfaceState state) new List>()); var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype); - if (jobIndex >= 0) + // If the job index is < 0 that means they don't have a job registered in the station records. + // For example, a new ID from a box would have no job index. + if (jobIndex < 0) { - JobPresetOptionButton.SelectId(jobIndex); + jobIndex = _jobPrototypeIds.IndexOf(_defaultJob); } + JobPresetOptionButton.SelectId(jobIndex); + _lastFullName = state.TargetIdFullName; _lastJobTitle = state.TargetIdJobTitle; _lastJobProto = state.TargetIdJobPrototype; diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 5ff003452ab7..aff6c1ff7be6 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -247,7 +247,10 @@ public void TriggerAction(EntityUid actionId, BaseActionComponent action) if (action.ClientExclusive) { if (instantAction.Event != null) + { instantAction.Event.Performer = user; + instantAction.Event.Action = actionId; + } PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime); } diff --git a/Content.Client/Administration/Components/HeadstandComponent.cs b/Content.Client/Administration/Components/HeadstandComponent.cs index d95e74576bfd..a4e3bfc5aaf4 100644 --- a/Content.Client/Administration/Components/HeadstandComponent.cs +++ b/Content.Client/Administration/Components/HeadstandComponent.cs @@ -3,7 +3,7 @@ namespace Content.Client.Administration.Components; -[RegisterComponent, NetworkedComponent] +[RegisterComponent] public sealed partial class HeadstandComponent : SharedHeadstandComponent { diff --git a/Content.Client/Administration/Components/KillSignComponent.cs b/Content.Client/Administration/Components/KillSignComponent.cs index 1cf47b93ff50..91c44ef3f275 100644 --- a/Content.Client/Administration/Components/KillSignComponent.cs +++ b/Content.Client/Administration/Components/KillSignComponent.cs @@ -3,6 +3,5 @@ namespace Content.Client.Administration.Components; -[NetworkedComponent, RegisterComponent] -public sealed partial class KillSignComponent : SharedKillSignComponent -{ } +[RegisterComponent] +public sealed partial class KillSignComponent : SharedKillSignComponent; diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index fdd62fb6a2dd..0f740c810459 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -126,12 +126,15 @@ void IPostInjectInit.PostInject() public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false) { - return uid == _player.LocalEntity ? _adminData : null; + if (uid == _player.LocalEntity && (_adminData?.Active ?? includeDeAdmin)) + return _adminData; + + return null; } public AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false) { - if (_player.LocalUser == session.UserId) + if (_player.LocalUser == session.UserId && (_adminData?.Active ?? includeDeAdmin)) return _adminData; return null; diff --git a/Content.Client/Administration/Systems/AdminFrozenSystem.cs b/Content.Client/Administration/Systems/AdminFrozenSystem.cs new file mode 100644 index 000000000000..885585f985ce --- /dev/null +++ b/Content.Client/Administration/Systems/AdminFrozenSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Administration; + +namespace Content.Client.Administration.Systems; + +public sealed class AdminFrozenSystem : SharedAdminFrozenSystem +{ +} diff --git a/Content.Client/Administration/Systems/AdminVerbSystem.cs b/Content.Client/Administration/Systems/AdminVerbSystem.cs index e0f84bc4f039..dced59bbf2e0 100644 --- a/Content.Client/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Client/Administration/Systems/AdminVerbSystem.cs @@ -1,3 +1,6 @@ +using Content.Shared.Administration; +using Content.Shared.Administration.Managers; +using Content.Shared.Mind.Components; using Content.Shared.Verbs; using Robust.Client.Console; using Robust.Shared.Utility; @@ -11,10 +14,12 @@ sealed class AdminVerbSystem : EntitySystem { [Dependency] private readonly IClientConGroupController _clientConGroupController = default!; [Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!; + [Dependency] private readonly ISharedAdminManager _admin = default!; public override void Initialize() { SubscribeLocalEvent>(AddAdminVerbs); + } private void AddAdminVerbs(GetVerbsEvent args) @@ -33,6 +38,24 @@ private void AddAdminVerbs(GetVerbsEvent args) }; args.Verbs.Add(verb); } + + if (!_admin.IsAdmin(args.User)) + return; + + if (_admin.HasAdminFlag(args.User, AdminFlags.Admin)) + args.ExtraCategories.Add(VerbCategory.Admin); + + if (_admin.HasAdminFlag(args.User, AdminFlags.Fun) && HasComp(args.Target)) + args.ExtraCategories.Add(VerbCategory.Antag); + + if (_admin.HasAdminFlag(args.User, AdminFlags.Debug)) + args.ExtraCategories.Add(VerbCategory.Debug); + + if (_admin.HasAdminFlag(args.User, AdminFlags.Fun)) + args.ExtraCategories.Add(VerbCategory.Smite); + + if (_admin.HasAdminFlag(args.User, AdminFlags.Admin)) + args.ExtraCategories.Add(VerbCategory.Tricks); } } } diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml b/Content.Client/Administration/UI/AdminMenuWindow.xaml index 311d67b826c7..d3d3df02d936 100644 --- a/Content.Client/Administration/UI/AdminMenuWindow.xaml +++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml @@ -6,7 +6,8 @@ xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs" xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab" xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab" - xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab"> + xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab" + xmlns:baby="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"> @@ -14,6 +15,7 @@ + diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs index c3ea67a3edb9..f3aa2572f2f9 100644 --- a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs +++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs @@ -21,8 +21,12 @@ public AdminMenuWindow() MasterTabContainer.SetTabTitle(3, Loc.GetString("admin-menu-round-tab")); MasterTabContainer.SetTabTitle(4, Loc.GetString("admin-menu-server-tab")); MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-panic-bunker-tab")); - MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-players-tab")); - MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-objects-tab")); + /* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-baby-jail-tab")); + MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-players-tab")); + MasterTabContainer.SetTabTitle(8, Loc.GetString("admin-menu-objects-tab")); } protected override void Dispose(bool disposing) diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs index 1f32640f7ddf..588d62e56036 100644 --- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs +++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs @@ -3,6 +3,7 @@ using System.Net.Sockets; using Content.Client.Administration.UI.CustomControls; using Content.Shared.Administration; +using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.Roles; using Robust.Client.AutoGenerated; @@ -11,6 +12,7 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -32,8 +34,11 @@ public sealed partial class BanPanel : DefaultWindow // This is less efficient than just holding a reference to the root control and enumerating children, but you // have to know how the controls are nested, which makes the code more complicated. private readonly List _roleCheckboxes = new(); + private readonly ISawmill _banpanelSawmill; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly ILogManager _logManager = default!; private enum TabNumbers { @@ -65,6 +70,7 @@ public BanPanel() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + _banpanelSawmill = _logManager.GetSawmill("admin.banpanel"); PlayerList.OnSelectionChanged += OnPlayerSelectionChanged; PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged(); PlayerCheckbox.OnPressed += _ => @@ -104,6 +110,11 @@ public BanPanel() }; SubmitButton.OnPressed += SubmitButtonOnOnPressed; + IpCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanIpBanDefault); + HwidCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanHwidBanDefault); + LastConnCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanUseLastDetails); + EraseCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanErasePlayer); + SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-none"), (int) NoteSeverity.None); SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-low"), (int) NoteSeverity.Minor); SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-medium"), (int) NoteSeverity.Medium); @@ -136,7 +147,7 @@ public BanPanel() var prototypeManager = IoCManager.Resolve(); foreach (var proto in prototypeManager.EnumeratePrototypes()) { - CreateRoleGroup(proto.ID, proto.Roles, proto.Color); + CreateRoleGroup(proto.ID, proto.Roles.Select(p => p.Id), proto.Color); } CreateRoleGroup("Antagonist", prototypeManager.EnumeratePrototypes().Select(p => p.ID), Color.Red); @@ -175,6 +186,39 @@ private void CreateRoleGroup(string roleName, IEnumerable roleList, Colo c.Pressed = args.Pressed; } } + + if (args.Pressed) + { + if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity)) + { + _banpanelSawmill + .Warning("Departmental role ban severity could not be parsed from config!"); + return; + } + SeverityOption.SelectId((int) newSeverity); + } + else + { + foreach (var childContainer in RolesContainer.Children) + { + if (childContainer is Container) + { + foreach (var child in childContainer.Children) + { + if (child is CheckBox { Pressed: true }) + return; + } + } + } + + if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity)) + { + _banpanelSawmill + .Warning("Role ban severity could not be parsed from config!"); + return; + } + SeverityOption.SelectId((int) newSeverity); + } }; outerContainer.AddChild(innerContainer); foreach (var role in roleList) @@ -353,6 +397,35 @@ private void OnTypeChanged() { TypeOption.ModulateSelfOverride = null; Tabs.SetTabVisible((int) TabNumbers.Roles, TypeOption.SelectedId == (int) Types.Role); + NoteSeverity? newSeverity = null; + switch (TypeOption.SelectedId) + { + case (int)Types.Server: + if (Enum.TryParse(_cfg.GetCVar(CCVars.ServerBanDefaultSeverity), true, out NoteSeverity serverSeverity)) + newSeverity = serverSeverity; + else + { + _banpanelSawmill + .Warning("Server ban severity could not be parsed from config!"); + } + + break; + case (int) Types.Role: + + if (Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity roleSeverity)) + { + newSeverity = roleSeverity; + } + else + { + _banpanelSawmill + .Warning("Role ban severity could not be parsed from config!"); + } + break; + } + + if (newSeverity != null) + SeverityOption.SelectId((int) newSeverity.Value); } private void UpdateSubmitEnabled() diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs index af977f763c6c..ddd66623bd4e 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs +++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs @@ -75,7 +75,7 @@ public BwoinkControl() if (info.Antag && info.ActiveThisRound) sb.Append(new Rune(0x1F5E1)); // 🗡 - if (info.OverallPlaytime <= TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.NewPlayerThreshold))) + if (info.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold))) sb.Append(new Rune(0x23F2)); // ⏲ sb.AppendFormat("\"{0}\"", text); @@ -226,7 +226,7 @@ private string FormatTabTitle(ItemList.Item li, PlayerInfo? pl = default) if (pl.Antag) sb.Append(new Rune(0x1F5E1)); // 🗡 - if (pl.OverallPlaytime <= TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.NewPlayerThreshold))) + if (pl.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold))) sb.Append(new Rune(0x23F2)); // ⏲ sb.AppendFormat("\"{0}\"", pl.CharacterName); @@ -243,9 +243,9 @@ private void SwitchToChannel(NetUserId? ch) { UpdateButtons(); + AHelpHelper.HideAllPanels(); if (ch != null) { - AHelpHelper.HideAllPanels(); var panel = AHelpHelper.EnsurePanel(ch.Value); panel.Visible = true; } diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs index f8d06f758f40..30f9d24df1df 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs +++ b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs @@ -16,14 +16,17 @@ public BwoinkWindow() Bwoink.ChannelSelector.OnSelectionChanged += sel => { - if (sel is not null) + if (sel is null) { - Title = $"{sel.CharacterName} / {sel.Username}"; + Title = Loc.GetString("bwoink-title-none-selected"); + return; + } + + Title = $"{sel.CharacterName} / {sel.Username}"; - if (sel.OverallPlaytime != null) - { - Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}"; - } + if (sel.OverallPlaytime != null) + { + Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}"; } }; diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index fdf935d7c048..12522d552d71 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -20,7 +20,7 @@ public sealed partial class PlayerListControl : BoxContainer private List _playerList = new(); private readonly List _sortedPlayerList = new(); - public event Action? OnSelectionChanged; + public event Action? OnSelectionChanged; public IReadOnlyList PlayerInfo => _playerList; public Func? OverrideText; @@ -41,12 +41,19 @@ public PlayerListControl() PlayerListContainer.ItemPressed += PlayerListItemPressed; PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown; PlayerListContainer.GenerateItem += GenerateButton; + PlayerListContainer.NoItemSelected += PlayerListNoItemSelected; PopulateList(_adminSystem.PlayerList); FilterLineEdit.OnTextChanged += _ => FilterList(); _adminSystem.PlayerListChanged += PopulateList; BackgroundPanel.PanelOverride = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 40)}; } + private void PlayerListNoItemSelected() + { + _selectedPlayer = null; + OnSelectionChanged?.Invoke(null); + } + private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data) { if (args == null || data is not PlayerListData {Info: var selectedPlayer}) diff --git a/Content.Client/Administration/UI/SpawnExplosion/ExplosionDebugOverlay.cs b/Content.Client/Administration/UI/SpawnExplosion/ExplosionDebugOverlay.cs index eede3a6217f0..d60094ad8978 100644 --- a/Content.Client/Administration/UI/SpawnExplosion/ExplosionDebugOverlay.cs +++ b/Content.Client/Administration/UI/SpawnExplosion/ExplosionDebugOverlay.cs @@ -25,7 +25,7 @@ public sealed class ExplosionDebugOverlay : Overlay public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace; - public Matrix3 SpaceMatrix; + public Matrix3x2 SpaceMatrix; public MapId Map; private readonly Font _font; @@ -78,7 +78,8 @@ private void DrawScreen(OverlayDrawArgs args) if (SpaceTiles == null) return; - gridBounds = Matrix3.Invert(SpaceMatrix).TransformBox(args.WorldBounds); + Matrix3x2.Invert(SpaceMatrix, out var invSpace); + gridBounds = invSpace.TransformBox(args.WorldBounds); DrawText(handle, gridBounds, SpaceMatrix, SpaceTiles, SpaceTileSize); } @@ -86,7 +87,7 @@ private void DrawScreen(OverlayDrawArgs args) private void DrawText( DrawingHandleScreen handle, Box2 gridBounds, - Matrix3 transform, + Matrix3x2 transform, Dictionary> tileSets, ushort tileSize) { @@ -103,7 +104,7 @@ private void DrawText( if (!gridBounds.Contains(centre)) continue; - var worldCenter = transform.Transform(centre); + var worldCenter = Vector2.Transform(centre, transform); var screenCenter = _eyeManager.WorldToScreen(worldCenter); @@ -119,7 +120,7 @@ private void DrawText( if (tileSets.TryGetValue(0, out var set)) { var epicenter = set.First(); - var worldCenter = transform.Transform((epicenter + Vector2Helpers.Half) * tileSize); + var worldCenter = Vector2.Transform((epicenter + Vector2Helpers.Half) * tileSize, transform); var screenCenter = _eyeManager.WorldToScreen(worldCenter) + new Vector2(-24, -24); var text = $"{Intensity[0]:F2}\nΣ={TotalIntensity:F1}\nΔ={Slope:F1}"; handle.DrawString(_font, screenCenter, text); @@ -148,11 +149,12 @@ private void DrawWorld(in OverlayDrawArgs args) if (SpaceTiles == null) return; - gridBounds = Matrix3.Invert(SpaceMatrix).TransformBox(args.WorldBounds).Enlarged(2); + Matrix3x2.Invert(SpaceMatrix, out var invSpace); + gridBounds = invSpace.TransformBox(args.WorldBounds).Enlarged(2); handle.SetTransform(SpaceMatrix); DrawTiles(handle, gridBounds, SpaceTiles, SpaceTileSize); - handle.SetTransform(Matrix3.Identity); + handle.SetTransform(Matrix3x2.Identity); } private void DrawTiles( diff --git a/Content.Client/Administration/UI/SpawnExplosion/SpawnExplosionWindow.xaml.cs b/Content.Client/Administration/UI/SpawnExplosion/SpawnExplosionWindow.xaml.cs index 5f187cad794e..b0d8a946ec5f 100644 --- a/Content.Client/Administration/UI/SpawnExplosion/SpawnExplosionWindow.xaml.cs +++ b/Content.Client/Administration/UI/SpawnExplosion/SpawnExplosionWindow.xaml.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; using Robust.Client.AutoGenerated; using Robust.Client.Console; +using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; @@ -22,7 +23,7 @@ public sealed partial class SpawnExplosionWindow : DefaultWindow [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IEntityManager _entMan = default!; - + private readonly SharedTransformSystem _transform = default!; private readonly SpawnExplosionEui _eui; private List _mapData = new(); @@ -37,6 +38,7 @@ public SpawnExplosionWindow(SpawnExplosionEui eui) { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + _transform = _entMan.System(); _eui = eui; ExplosionOption.OnItemSelected += ExplosionSelected; @@ -104,7 +106,7 @@ private void SetLocation() _pausePreview = true; MapOptions.Select(_mapData.IndexOf(transform.MapID)); - (MapX.Value, MapY.Value) = transform.MapPosition.Position; + (MapX.Value, MapY.Value) = _transform.GetMapCoordinates(_playerManager.LocalEntity!.Value, xform: transform).Position; _pausePreview = false; UpdatePreview(); diff --git a/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml new file mode 100644 index 000000000000..b8034faf52ab --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml @@ -0,0 +1,6 @@ + + + diff --git a/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml.cs b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml.cs new file mode 100644 index 000000000000..9e1d53818f24 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailStatusWindow.xaml.cs @@ -0,0 +1,21 @@ +using Content.Client.Message; +using Content.Client.UserInterface.Controls; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Administration.UI.Tabs.BabyJailTab; + +/* + * TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + +[GenerateTypedNameReferences] +public sealed partial class BabyJailStatusWindow : FancyWindow +{ + public BabyJailStatusWindow() + { + RobustXamlLoader.Load(this); + MessageLabel.SetMarkup(Loc.GetString("admin-ui-baby-jail-is-enabled")); + } +} diff --git a/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml new file mode 100644 index 000000000000..dd770c2be53c --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml.cs b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml.cs new file mode 100644 index 000000000000..aa9d6ced9517 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/BabyJailTab/BabyJailTab.xaml.cs @@ -0,0 +1,75 @@ +using Content.Shared.Administration.Events; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Console; + +/* + * TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ + +namespace Content.Client.Administration.UI.Tabs.BabyJailTab; + +[GenerateTypedNameReferences] +public sealed partial class BabyJailTab : Control +{ + [Dependency] private readonly IConsoleHost _console = default!; + + private string _maxAccountAge; + private string _maxOverallMinutes; + + public BabyJailTab() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + MaxAccountAge.OnTextEntered += args => SendMaxAccountAge(args.Text); + MaxAccountAge.OnFocusExit += args => SendMaxAccountAge(args.Text); + _maxAccountAge = MaxAccountAge.Text; + + MaxOverallMinutes.OnTextEntered += args => SendMaxOverallMinutes(args.Text); + MaxOverallMinutes.OnFocusExit += args => SendMaxOverallMinutes(args.Text); + _maxOverallMinutes = MaxOverallMinutes.Text; + } + + private void SendMaxAccountAge(string text) + { + if (string.IsNullOrWhiteSpace(text) || + text == _maxAccountAge || + !int.TryParse(text, out var minutes)) + { + return; + } + + _console.ExecuteCommand($"babyjail_max_account_age {minutes}"); + } + + private void SendMaxOverallMinutes(string text) + { + if (string.IsNullOrWhiteSpace(text) || + text == _maxOverallMinutes || + !int.TryParse(text, out var minutes)) + { + return; + } + + _console.ExecuteCommand($"babyjail_max_overall_minutes {minutes}"); + } + + public void UpdateStatus(BabyJailStatus status) + { + EnabledButton.Pressed = status.Enabled; + EnabledButton.Text = Loc.GetString(status.Enabled + ? "admin-ui-baby-jail-enabled" + : "admin-ui-baby-jail-disabled" + ); + EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null; + ShowReasonButton.Pressed = status.ShowReason; + + MaxAccountAge.Text = status.MaxAccountAgeMinutes.ToString(); + _maxAccountAge = MaxAccountAge.Text; + + MaxOverallMinutes.Text = status.MaxOverallMinutes.ToString(); + _maxOverallMinutes = MaxOverallMinutes.Text; + } +} diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml index fb68e6c79089..ea89916ba8c2 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml @@ -1,15 +1,21 @@  + xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" + xmlns:ot="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab" + xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"> + + - - - - + + + + + diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs index a5c300843658..c8606ca80d5e 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs @@ -1,5 +1,7 @@ using Content.Client.Station; +using Content.Client.UserInterface.Controls; using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map.Components; @@ -10,20 +12,20 @@ namespace Content.Client.Administration.UI.Tabs.ObjectsTab; [GenerateTypedNameReferences] public sealed partial class ObjectsTab : Control { - [Dependency] private readonly EntityManager _entityManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IGameTiming _timing = default!; private readonly List _objects = new(); - private List _selections = new(); + private readonly List _selections = new(); + private bool _ascending = false; // Set to false for descending order by default + private ObjectsTabHeader.Header _headerClicked = ObjectsTabHeader.Header.ObjectName; + private readonly Color _altColor = Color.FromHex("#292B38"); + private readonly Color _defaultColor = Color.FromHex("#2F2F3B"); - public event Action? OnEntryKeyBindDown; + public event Action? OnEntryKeyBindDown; - // Listen I could either have like 4 different event subscribers (for map / grid / station changes) and manage their lifetimes in AdminUIController - // OR - // I can do this. - private TimeSpan _updateFrequency = TimeSpan.FromSeconds(2); - - private TimeSpan _nextUpdate = TimeSpan.FromSeconds(2); + private readonly TimeSpan _updateFrequency = TimeSpan.FromSeconds(2); + private TimeSpan _nextUpdate; public ObjectsTab() { @@ -42,6 +44,30 @@ public ObjectsTab() ObjectTypeOptions.AddItem(Enum.GetName((ObjectsTabSelection)type)!); } + ListHeader.OnHeaderClicked += HeaderClicked; + SearchList.SearchBar = SearchLineEdit; + SearchList.GenerateItem += GenerateButton; + SearchList.DataFilterCondition += DataFilterCondition; + + RefreshObjectList(); + // Set initial selection and refresh the list to apply the initial sort order + var defaultSelection = ObjectsTabSelection.Grids; + ObjectTypeOptions.SelectId((int)defaultSelection); // Set the default selection + RefreshObjectList(defaultSelection); // Refresh the list with the default selection + + // Initialize the next update time + _nextUpdate = TimeSpan.Zero; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (_timing.CurTime < _nextUpdate) + return; + + _nextUpdate = _timing.CurTime + _updateFrequency; + RefreshObjectList(); } @@ -81,32 +107,70 @@ private void RefreshObjectList(ObjectsTabSelection selection) throw new ArgumentOutOfRangeException(nameof(selection), selection, null); } - foreach (var control in _objects) + entities.Sort((a, b) => { - ObjectList.RemoveChild(control); - } + var valueA = GetComparableValue(a, _headerClicked); + var valueB = GetComparableValue(b, _headerClicked); + return _ascending ? Comparer.Default.Compare(valueA, valueB) : Comparer.Default.Compare(valueB, valueA); + }); - _objects.Clear(); - - foreach (var (name, nent) in entities) + var listData = new List(); + for (int index = 0; index < entities.Count; index++) { - var ctrl = new ObjectsTabEntry(name, nent); - _objects.Add(ctrl); - ObjectList.AddChild(ctrl); - ctrl.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(ctrl, args); + var info = entities[index]; + listData.Add(new ObjectsListData(info, $"{info.Name} {info.Entity}", index % 2 == 0 ? _altColor : _defaultColor)); } + + SearchList.PopulateList(listData); } - protected override void FrameUpdate(FrameEventArgs args) + private void GenerateButton(ListData data, ListContainerButton button) { - base.FrameUpdate(args); - - if (_timing.CurTime < _nextUpdate) + if (data is not ObjectsListData { Info: var info, BackgroundColor: var backgroundColor }) return; - // I do not care for precision. - _nextUpdate = _timing.CurTime + _updateFrequency; + var entry = new ObjectsTabEntry(info.Name, info.Entity, new StyleBoxFlat { BackgroundColor = backgroundColor }); + button.ToolTip = $"{info.Name}, {info.Entity}"; + + button.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(args, data); + button.AddChild(entry); + } + + private bool DataFilterCondition(string filter, ListData listData) + { + if (listData is not ObjectsListData { FilteringString: var filteringString }) + return false; + + // If the filter is empty, do not filter out any entries + if (string.IsNullOrEmpty(filter)) + return true; + + return filteringString.Contains(filter, StringComparison.CurrentCultureIgnoreCase); + } + + private object GetComparableValue((string Name, NetEntity Entity) entity, ObjectsTabHeader.Header header) + { + return header switch + { + ObjectsTabHeader.Header.ObjectName => entity.Name, + ObjectsTabHeader.Header.EntityID => entity.Entity.ToString(), + _ => entity.Name + }; + } + + private void HeaderClicked(ObjectsTabHeader.Header header) + { + if (_headerClicked == header) + { + _ascending = !_ascending; + } + else + { + _headerClicked = header; + _ascending = true; + } + ListHeader.UpdateHeaderSymbols(_headerClicked, _ascending); RefreshObjectList(); } @@ -118,3 +182,4 @@ private enum ObjectsTabSelection } } +public record ObjectsListData((string Name, NetEntity Entity) Info, string FilteringString, Color BackgroundColor) : ListData; diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml index 0f6975e3656d..83c4cc5697f7 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml @@ -1,6 +1,6 @@ - - + @@ -14,4 +14,4 @@ HorizontalExpand="True" ClipText="True"/> - + diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs index c9b2cd8b572d..aab06c6ccd0f 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs @@ -1,19 +1,21 @@ using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; namespace Content.Client.Administration.UI.Tabs.ObjectsTab; [GenerateTypedNameReferences] -public sealed partial class ObjectsTabEntry : ContainerButton +public sealed partial class ObjectsTabEntry : PanelContainer { public NetEntity AssocEntity; - public ObjectsTabEntry(string name, NetEntity nent) + public ObjectsTabEntry(string name, NetEntity nent, StyleBox styleBox) { RobustXamlLoader.Load(this); AssocEntity = nent; EIDLabel.Text = nent.ToString(); NameLabel.Text = name; + BackgroundColorPanel.PanelOverride = styleBox; } } diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml new file mode 100644 index 000000000000..71a1f5c7bc64 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml @@ -0,0 +1,21 @@ + + + + + diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs new file mode 100644 index 000000000000..3a91b5b94836 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs @@ -0,0 +1,86 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Input; + +namespace Content.Client.Administration.UI.Tabs.ObjectsTab +{ + [GenerateTypedNameReferences] + public sealed partial class ObjectsTabHeader : Control + { + public event Action
? OnHeaderClicked; + + private const string ArrowUp = "↑"; + private const string ArrowDown = "↓"; + + public ObjectsTabHeader() + { + RobustXamlLoader.Load(this); + + ObjectNameLabel.OnKeyBindDown += ObjectNameClicked; + EntityIDLabel.OnKeyBindDown += EntityIDClicked; + } + + public Label GetHeader(Header header) + { + return header switch + { + Header.ObjectName => ObjectNameLabel, + Header.EntityID => EntityIDLabel, + _ => throw new ArgumentOutOfRangeException(nameof(header), header, null) + }; + } + + public void ResetHeaderText() + { + ObjectNameLabel.Text = Loc.GetString("object-tab-object-name"); + EntityIDLabel.Text = Loc.GetString("object-tab-entity-id"); + } + + public void UpdateHeaderSymbols(Header headerClicked, bool ascending) + { + ResetHeaderText(); + var arrow = ascending ? ArrowUp : ArrowDown; + GetHeader(headerClicked).Text += $" {arrow}"; + } + + private void HeaderClicked(GUIBoundKeyEventArgs args, Header header) + { + if (args.Function != EngineKeyFunctions.UIClick) + { + return; + } + + OnHeaderClicked?.Invoke(header); + args.Handle(); + } + + private void ObjectNameClicked(GUIBoundKeyEventArgs args) + { + HeaderClicked(args, Header.ObjectName); + } + + private void EntityIDClicked(GUIBoundKeyEventArgs args) + { + HeaderClicked(args, Header.EntityID); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + ObjectNameLabel.OnKeyBindDown -= ObjectNameClicked; + EntityIDLabel.OnKeyBindDown -= EntityIDClicked; + } + } + + public enum Header + { + ObjectName, + EntityID + } + } +} diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml index 89827d064246..ee7ba4d34ff6 100644 --- a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml +++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml @@ -31,12 +31,12 @@ - diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs index f6212cd5ee10..c3bcf3ffa09f 100644 --- a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs @@ -12,7 +12,7 @@ public sealed partial class PanicBunkerTab : Control [Dependency] private readonly IConsoleHost _console = default!; private string _minAccountAge; - private string _minOverallHours; + private string _minOverallMinutes; public PanicBunkerTab() { @@ -25,9 +25,9 @@ public PanicBunkerTab() MinAccountAge.OnFocusExit += args => SendMinAccountAge(args.Text); _minAccountAge = MinAccountAge.Text; - MinOverallHours.OnTextEntered += args => SendMinOverallHours(args.Text); - MinOverallHours.OnFocusExit += args => SendMinOverallHours(args.Text); - _minOverallHours = MinOverallHours.Text; + MinOverallMinutes.OnTextEntered += args => SendMinOverallMinutes(args.Text); + MinOverallMinutes.OnFocusExit += args => SendMinOverallMinutes(args.Text); + _minOverallMinutes = MinOverallMinutes.Text; } private void SendMinAccountAge(string text) @@ -42,16 +42,16 @@ private void SendMinAccountAge(string text) _console.ExecuteCommand($"panicbunker_min_account_age {minutes}"); } - private void SendMinOverallHours(string text) + private void SendMinOverallMinutes(string text) { if (string.IsNullOrWhiteSpace(text) || - text == _minOverallHours || - !int.TryParse(text, out var hours)) + text == _minOverallMinutes || + !int.TryParse(text, out var minutes)) { return; } - _console.ExecuteCommand($"panicbunker_min_overall_hours {hours}"); + _console.ExecuteCommand($"panicbunker_min_overall_minutes {minutes}"); } public void UpdateStatus(PanicBunkerStatus status) @@ -68,10 +68,10 @@ public void UpdateStatus(PanicBunkerStatus status) CountDeadminnedButton.Pressed = status.CountDeadminnedAdmins; ShowReasonButton.Pressed = status.ShowReason; - MinAccountAge.Text = status.MinAccountAgeHours.ToString(); + MinAccountAge.Text = status.MinAccountAgeMinutes.ToString(); _minAccountAge = MinAccountAge.Text; - MinOverallHours.Text = status.MinOverallHours.ToString(); - _minOverallHours = MinOverallHours.Text; + MinOverallMinutes.Text = status.MinOverallMinutes.ToString(); + _minOverallMinutes = MinOverallMinutes.Text; } } diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml index 3071bf8358b3..25a96df1d377 100644 --- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml +++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml @@ -1,21 +1,19 @@  + xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" + xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"> - + diff --git a/Content.Client/Chemistry/UI/ReagentCardControl.xaml.cs b/Content.Client/Chemistry/UI/ReagentCardControl.xaml.cs new file mode 100644 index 000000000000..f7cadcc264d0 --- /dev/null +++ b/Content.Client/Chemistry/UI/ReagentCardControl.xaml.cs @@ -0,0 +1,32 @@ +using Content.Shared.Chemistry; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Chemistry.UI; + +[GenerateTypedNameReferences] +public sealed partial class ReagentCardControl : Control +{ + public string StorageSlotId { get; } + public Action? OnPressed; + public Action? OnEjectButtonPressed; + + public ReagentCardControl(ReagentInventoryItem item) + { + RobustXamlLoader.Load(this); + + StorageSlotId = item.StorageSlotId; + ColorPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = item.ReagentColor }; + ReagentNameLabel.Text = item.ReagentLabel; + FillLabel.Text = Loc.GetString("reagent-dispenser-window-quantity-label-text", ("quantity", item.Quantity));; + EjectButtonIcon.Text = Loc.GetString("reagent-dispenser-window-eject-container-button"); + + if (item.Quantity == 0.0) + MainButton.Disabled = true; + + MainButton.OnPressed += args => OnPressed?.Invoke(StorageSlotId); + EjectButton.OnPressed += args => OnEjectButtonPressed?.Invoke(StorageSlotId); + } +} diff --git a/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs b/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs index 8244e3e6edb3..99e5a3d39536 100644 --- a/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs @@ -1,3 +1,4 @@ +using Content.Client.Guidebook.Components; using Content.Shared.Chemistry; using Content.Shared.Containers.ItemSlots; using JetBrains.Annotations; @@ -34,6 +35,7 @@ protected override void Open() _window = new() { Title = EntMan.GetComponent(Owner).EntityName, + HelpGuidebookIds = EntMan.GetComponent(Owner).Guides }; _window.OpenCentered(); @@ -42,38 +44,11 @@ protected override void Open() // Setup static button actions. _window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedReagentDispenser.OutputSlotName)); _window.ClearButton.OnPressed += _ => SendMessage(new ReagentDispenserClearContainerSolutionMessage()); - _window.DispenseButton1.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U1)); - _window.DispenseButton5.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U5)); - _window.DispenseButton10.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U10)); - _window.DispenseButton15.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U15)); - _window.DispenseButton20.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U20)); - _window.DispenseButton25.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U25)); - _window.DispenseButton30.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U30)); - _window.DispenseButton50.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U50)); - _window.DispenseButton100.OnPressed += _ => SendMessage(new ReagentDispenserSetDispenseAmountMessage(ReagentDispenserDispenseAmount.U100)); - // Setup reagent button actions. - _window.OnDispenseReagentButtonPressed += (args, button) => SendMessage(new ReagentDispenserDispenseReagentMessage(button.ReagentId)); - _window.OnDispenseReagentButtonMouseEntered += (args, button) => - { - if (_lastState is not null) - _window.UpdateContainerInfo(_lastState); - }; - _window.OnDispenseReagentButtonMouseExited += (args, button) => - { - if (_lastState is not null) - _window.UpdateContainerInfo(_lastState); - }; + _window.AmountGrid.OnButtonPressed += s => SendMessage(new ReagentDispenserSetDispenseAmountMessage(s)); - _window.OnEjectJugButtonPressed += (args, button) => SendMessage(new ItemSlotButtonPressedEvent(button.ReagentId)); - _window.OnEjectJugButtonMouseEntered += (args, button) => { - if (_lastState is not null) - _window.UpdateContainerInfo(_lastState); - }; - _window.OnEjectJugButtonMouseExited += (args, button) => { - if (_lastState is not null) - _window.UpdateContainerInfo(_lastState); - }; + _window.OnDispenseReagentButtonPressed += (id) => SendMessage(new ReagentDispenserDispenseReagentMessage(id)); + _window.OnEjectJugButtonPressed += (id) => SendMessage(new ItemSlotButtonPressedEvent(id)); } /// diff --git a/Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml b/Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml index 3b812ba56b21..9da340f8a76e 100644 --- a/Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml +++ b/Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml @@ -1,53 +1,78 @@ - - - - [GenerateTypedNameReferences] - public sealed partial class ReagentDispenserWindow : DefaultWindow + public sealed partial class ReagentDispenserWindow : FancyWindow { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; - public event Action? OnDispenseReagentButtonPressed; - public event Action? OnDispenseReagentButtonMouseEntered; - public event Action? OnDispenseReagentButtonMouseExited; - - public event Action? OnEjectJugButtonPressed; - public event Action? OnEjectJugButtonMouseEntered; - public event Action? OnEjectJugButtonMouseExited; + public event Action? OnDispenseReagentButtonPressed; + public event Action? OnEjectJugButtonPressed; /// /// Create and initialize the dispenser UI client-side. Creates the basic layout, @@ -35,44 +29,27 @@ public ReagentDispenserWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - - var dispenseAmountGroup = new ButtonGroup(); - DispenseButton1.Group = dispenseAmountGroup; - DispenseButton5.Group = dispenseAmountGroup; - DispenseButton10.Group = dispenseAmountGroup; - DispenseButton15.Group = dispenseAmountGroup; - DispenseButton20.Group = dispenseAmountGroup; - DispenseButton25.Group = dispenseAmountGroup; - DispenseButton30.Group = dispenseAmountGroup; - DispenseButton50.Group = dispenseAmountGroup; - DispenseButton100.Group = dispenseAmountGroup; } /// /// Update the button grid of reagents which can be dispensed. /// /// Reagents which can be dispensed by this dispenser - public void UpdateReagentsList(List>> inventory) + public void UpdateReagentsList(List inventory) { - if (ChemicalList == null) + if (ReagentList == null) return; - ChemicalList.Children.Clear(); + ReagentList.Children.Clear(); //Sort inventory by reagentLabel - inventory.Sort((x, y) => x.Value.Key.CompareTo(y.Value.Key)); + inventory.Sort((x, y) => x.ReagentLabel.CompareTo(y.ReagentLabel)); - foreach (KeyValuePair> entry in inventory) + foreach (var item in inventory) { - var button = new DispenseReagentButton(entry.Key, entry.Value.Key, entry.Value.Value); - button.OnPressed += args => OnDispenseReagentButtonPressed?.Invoke(args, button); - button.OnMouseEntered += args => OnDispenseReagentButtonMouseEntered?.Invoke(args, button); - button.OnMouseExited += args => OnDispenseReagentButtonMouseExited?.Invoke(args, button); - ChemicalList.AddChild(button); - var ejectButton = new EjectJugButton(entry.Key); - ejectButton.OnPressed += args => OnEjectJugButtonPressed?.Invoke(args, ejectButton); - ejectButton.OnMouseEntered += args => OnEjectJugButtonMouseEntered?.Invoke(args, ejectButton); - ejectButton.OnMouseExited += args => OnEjectJugButtonMouseExited?.Invoke(args, ejectButton); - ChemicalList.AddChild(ejectButton); + var card = new ReagentCardControl(item); + card.OnPressed += OnDispenseReagentButtonPressed; + card.OnEjectButtonPressed += OnEjectJugButtonPressed; + ReagentList.Children.Add(card); } } @@ -93,36 +70,7 @@ public void UpdateState(BoundUserInterfaceState state) ClearButton.Disabled = castState.OutputContainer is null; EjectButton.Disabled = castState.OutputContainer is null; - switch (castState.SelectedDispenseAmount) - { - case ReagentDispenserDispenseAmount.U1: - DispenseButton1.Pressed = true; - break; - case ReagentDispenserDispenseAmount.U5: - DispenseButton5.Pressed = true; - break; - case ReagentDispenserDispenseAmount.U10: - DispenseButton10.Pressed = true; - break; - case ReagentDispenserDispenseAmount.U15: - DispenseButton15.Pressed = true; - break; - case ReagentDispenserDispenseAmount.U20: - DispenseButton20.Pressed = true; - break; - case ReagentDispenserDispenseAmount.U25: - DispenseButton25.Pressed = true; - break; - case ReagentDispenserDispenseAmount.U30: - DispenseButton30.Pressed = true; - break; - case ReagentDispenserDispenseAmount.U50: - DispenseButton50.Pressed = true; - break; - case ReagentDispenserDispenseAmount.U100: - DispenseButton100.Pressed = true; - break; - } + AmountGrid.Selected = ((int)castState.SelectedDispenseAmount).ToString(); } /// @@ -137,23 +85,15 @@ public void UpdateContainerInfo(ReagentDispenserBoundUserInterfaceState state) if (state.OutputContainer is null) { + ContainerInfoName.Text = ""; + ContainerInfoFill.Text = ""; ContainerInfo.Children.Add(new Label { Text = Loc.GetString("reagent-dispenser-window-no-container-loaded-text") }); return; } - ContainerInfo.Children.Add(new BoxContainer // Name of the container and its fill status (Ex: 44/100u) - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - new Label {Text = $"{state.OutputContainer.DisplayName}: "}, - new Label - { - Text = $"{state.OutputContainer.CurrentVolume}/{state.OutputContainer.MaxVolume}", - StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} - } - } - }); + // Set Name of the container and its fill status (Ex: 44/100u) + ContainerInfoName.Text = state.OutputContainer.DisplayName; + ContainerInfoFill.Text = state.OutputContainer.CurrentVolume + "/" + state.OutputContainer.MaxVolume; foreach (var (reagent, quantity) in state.OutputContainer.Reagents!) { @@ -181,28 +121,4 @@ public void UpdateContainerInfo(ReagentDispenserBoundUserInterfaceState state) } } } - - public sealed class DispenseReagentButton : Button - { - public string ReagentId { get; } - - public DispenseReagentButton(string reagentId, string text, string amount) - { - AddStyleClass("OpenRight"); - ReagentId = reagentId; - Text = text + " " + amount; - } - } - - public sealed class EjectJugButton : Button - { - public string ReagentId { get; } - - public EjectJugButton(string reagentId) - { - AddStyleClass("OpenLeft"); - ReagentId = reagentId; - Text = "⏏"; - } - } } diff --git a/Content.Client/Chemistry/UI/SolutionStatusControl.cs b/Content.Client/Chemistry/UI/SolutionStatusControl.cs new file mode 100644 index 000000000000..1a33ffb0e146 --- /dev/null +++ b/Content.Client/Chemistry/UI/SolutionStatusControl.cs @@ -0,0 +1,59 @@ +using Content.Client.Chemistry.Components; +using Content.Client.Chemistry.EntitySystems; +using Content.Client.Items.UI; +using Content.Client.Message; +using Content.Client.Stylesheets; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.FixedPoint; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.Chemistry.UI; + +/// +/// Displays basic solution information for . +/// +/// +public sealed class SolutionStatusControl : PollingItemStatusControl +{ + private readonly Entity _parent; + private readonly IEntityManager _entityManager; + private readonly SharedSolutionContainerSystem _solutionContainers; + private readonly RichTextLabel _label; + + public SolutionStatusControl( + Entity parent, + IEntityManager entityManager, + SharedSolutionContainerSystem solutionContainers) + { + _parent = parent; + _entityManager = entityManager; + _solutionContainers = solutionContainers; + _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; + AddChild(_label); + } + + protected override Data PollData() + { + if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.Solution, out _, out var solution)) + return default; + + FixedPoint2? transferAmount = null; + if (_entityManager.TryGetComponent(_parent.Owner, out SolutionTransferComponent? transfer)) + transferAmount = transfer.TransferAmount; + + return new Data(solution.Volume, solution.MaxVolume, transferAmount); + } + + protected override void Update(in Data data) + { + var markup = Loc.GetString("solution-status-volume", + ("currentVolume", data.Volume), + ("maxVolume", data.MaxVolume)); + if (data.TransferVolume is { } transferVolume) + markup += "\n" + Loc.GetString("solution-status-transfer", ("volume", transferVolume)); + _label.SetMarkup(markup); + } + + public readonly record struct Data(FixedPoint2 Volume, FixedPoint2 MaxVolume, FixedPoint2? TransferVolume); +} diff --git a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs index f1e8e8d7aa44..17b88fb5a8f5 100644 --- a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs +++ b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Hands; +using Content.Shared.Item; using Content.Shared.Rounding; using Robust.Client.GameObjects; using Robust.Shared.Prototypes; @@ -150,6 +151,9 @@ private void OnGetHeldVisuals(EntityUid uid, SolutionContainerVisualsComponent c if (!TryComp(uid, out AppearanceComponent? appearance)) return; + if (!TryComp(uid, out var item)) + return; + if (!AppearanceSystem.TryGetData(uid, SolutionContainerVisuals.FillFraction, out var fraction, appearance)) return; @@ -159,7 +163,8 @@ private void OnGetHeldVisuals(EntityUid uid, SolutionContainerVisualsComponent c { var layer = new PrototypeLayerData(); - var key = "inhand-" + args.Location.ToString().ToLowerInvariant() + component.InHandsFillBaseName + closestFillSprite; + var heldPrefix = item.HeldPrefix == null ? "inhand-" : $"{item.HeldPrefix}-inhand-"; + var key = heldPrefix + args.Location.ToString().ToLowerInvariant() + component.InHandsFillBaseName + closestFillSprite; layer.State = key; diff --git a/Content.Client/Clickable/ClickableComponent.cs b/Content.Client/Clickable/ClickableComponent.cs index cfbd1a99d69b..6f75df46830e 100644 --- a/Content.Client/Clickable/ClickableComponent.cs +++ b/Content.Client/Clickable/ClickableComponent.cs @@ -38,9 +38,9 @@ public bool CheckClick(SpriteComponent sprite, TransformComponent transform, Ent renderOrder = sprite.RenderOrder; var (spritePos, spriteRot) = transform.GetWorldPositionRotation(xformQuery); var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation); - bottom = Matrix3.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom; + bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom; - var invSpriteMatrix = Matrix3.Invert(sprite.GetLocalMatrix()); + Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix); // This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites. var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive(); @@ -48,8 +48,8 @@ public bool CheckClick(SpriteComponent sprite, TransformComponent transform, Ent Angle cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero; // First we get `localPos`, the clicked location in the sprite-coordinate frame. - var entityXform = Matrix3.CreateInverseTransform(transform.WorldPosition, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping); - var localPos = invSpriteMatrix.Transform(entityXform.Transform(worldPos)); + var entityXform = Matrix3Helpers.CreateInverseTransform(transform.WorldPosition, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping); + var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix); // Check explicitly defined click-able bounds if (CheckDirBound(sprite, relativeRotation, localPos)) @@ -79,8 +79,8 @@ public bool CheckClick(SpriteComponent sprite, TransformComponent transform, Ent // convert to layer-local coordinates layer.GetLayerDrawMatrix(dir, out var matrix); - var inverseMatrix = Matrix3.Invert(matrix); - var layerLocal = inverseMatrix.Transform(localPos); + Matrix3x2.Invert(matrix, out var inverseMatrix); + var layerLocal = Vector2.Transform(localPos, inverseMatrix); // Convert to image coordinates var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.Size / 2f); diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index 7e78ac7d707e..1c0d831226db 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Numerics; using Content.Client.Inventory; using Content.Shared.Clothing; using Content.Shared.Clothing.Components; @@ -11,6 +12,7 @@ using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Utility; using static Robust.Client.GameObjects.SpriteComponent; @@ -46,6 +48,7 @@ public sealed class ClientClothingSystem : ClothingSystem }; [Dependency] private readonly IResourceCache _cache = default!; + [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; public override void Initialize() @@ -111,6 +114,7 @@ private void OnGetVisuals(EntityUid uid, ClothingComponent item, GetEquipmentVis i++; } + item.MappedLayer = key; args.Layers.Add((key, layer)); } } @@ -151,13 +155,9 @@ private bool TryGetDefaultVisuals(EntityUid uid, ClothingComponent clothing, str // species specific if (speciesId != null && rsi.TryGetState($"{state}-{speciesId}", out _)) - { state = $"{state}-{speciesId}"; - } else if (!rsi.TryGetState(state, out _)) - { return false; - } var layer = new PrototypeLayerData(); layer.RsiPath = rsi.Path.ToString(); @@ -265,6 +265,7 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot // temporary, until layer draw depths get added. Basically: a layer with the key "slot" is being used as a // bookmark to determine where in the list of layers we should insert the clothing layers. bool slotLayerExists = sprite.LayerMapTryGet(slot, out var index); + var displacementData = inventory.Displacements.GetValueOrDefault(slot); // add the new layers foreach (var (key, layerData) in ev.Layers) @@ -284,6 +285,8 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot if (layerData.Color != null) sprite.LayerSetColor(key, layerData.Color.Value); + if (layerData.Scale != null) + sprite.LayerSetScale(key, layerData.Scale.Value); } else index = sprite.LayerMapReserveBlank(key); @@ -308,6 +311,28 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot sprite.LayerSetData(index, layerData); layer.Offset += slotDef.Offset; + + if (displacementData != null) + { + if (displacementData.ShaderOverride != null) + sprite.LayerSetShader(index, displacementData.ShaderOverride); + + var displacementKey = $"{key}-displacement"; + if (!revealedLayers.Add(displacementKey)) + { + Log.Warning($"Duplicate key for clothing visuals DISPLACEMENT: {displacementKey}."); + continue; + } + + var displacementLayer = _serialization.CreateCopy(displacementData.Layer, notNullableOverride: true); + displacementLayer.CopyToShaderParameters!.LayerKey = key; + + // Add before main layer for this item. + sprite.AddLayer(displacementLayer, index); + sprite.LayerMapSet(displacementKey, index); + + revealedLayers.Add(displacementKey); + } } RaiseLocalEvent(equipment, new EquipmentVisualsUpdatedEvent(equipee, slot, revealedLayers), true); diff --git a/Content.Client/Clothing/FlippableClothingVisualizerSystem.cs b/Content.Client/Clothing/FlippableClothingVisualizerSystem.cs new file mode 100644 index 000000000000..2c3afb0324fd --- /dev/null +++ b/Content.Client/Clothing/FlippableClothingVisualizerSystem.cs @@ -0,0 +1,48 @@ +using Content.Shared.Clothing; +using Content.Shared.Clothing.Components; +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Foldable; +using Content.Shared.Item; +using Robust.Client.GameObjects; + +namespace Content.Client.Clothing; + +public sealed class FlippableClothingVisualizerSystem : VisualizerSystem +{ + [Dependency] private readonly SharedItemSystem _itemSys = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetVisuals, after: [typeof(ClothingSystem)]); + SubscribeLocalEvent(OnFolded); + } + + private void OnFolded(Entity ent, ref FoldedEvent args) + { + _itemSys.VisualsChanged(ent); + } + + private void OnGetVisuals(Entity ent, ref GetEquipmentVisualsEvent args) + { + if (!TryComp(ent, out SpriteComponent? sprite) || + !TryComp(ent, out ClothingComponent? clothing)) + return; + + if (clothing.MappedLayer == null || + !AppearanceSystem.TryGetData(ent, FoldableSystem.FoldedVisuals.State, out var folding) || + !sprite.LayerMapTryGet(folding ? ent.Comp.FoldingLayer : ent.Comp.UnfoldingLayer, out var idx)) + return; + + // add each layer to the visuals + var spriteLayer = sprite[idx]; + foreach (var layer in args.Layers) + { + if (layer.Item1 != clothing.MappedLayer) + continue; + + layer.Item2.Scale = spriteLayer.Scale; + } + } +} diff --git a/Content.Client/Clothing/FlippableClothingVisualsComponent.cs b/Content.Client/Clothing/FlippableClothingVisualsComponent.cs new file mode 100644 index 000000000000..33d622b8b52f --- /dev/null +++ b/Content.Client/Clothing/FlippableClothingVisualsComponent.cs @@ -0,0 +1,16 @@ +namespace Content.Client.Clothing; + +/// +/// Communicates folded layers data (currently only Scale to handle flipping) +/// to the wearer clothing sprite layer +/// +[RegisterComponent] +[Access(typeof(FlippableClothingVisualizerSystem))] +public sealed partial class FlippableClothingVisualsComponent : Component +{ + [DataField] + public string FoldingLayer = "foldedLayer"; + + [DataField] + public string UnfoldingLayer = "unfoldedLayer"; +} diff --git a/Content.Client/Clothing/MagbootsSystem.cs b/Content.Client/Clothing/MagbootsSystem.cs deleted file mode 100644 index a3d39eafded8..000000000000 --- a/Content.Client/Clothing/MagbootsSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Content.Shared.Clothing; - -namespace Content.Client.Clothing; - -public sealed class MagbootsSystem : SharedMagbootsSystem -{ - -} diff --git a/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs b/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs new file mode 100644 index 000000000000..c04cf0a60bac --- /dev/null +++ b/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs @@ -0,0 +1,19 @@ +using Content.Shared.Clothing.Components; +using Robust.Client.Physics; + +namespace Content.Client.Clothing.Systems; + +public sealed partial class PilotedByClothingSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUpdatePredicted); + } + + private void OnUpdatePredicted(Entity entity, ref UpdateIsPredictedEvent args) + { + args.BlockPrediction = true; + } +} diff --git a/Content.Client/Clothing/Systems/WaddleClothingSystem.cs b/Content.Client/Clothing/Systems/WaddleClothingSystem.cs deleted file mode 100644 index b8ac3c207bf2..000000000000 --- a/Content.Client/Clothing/Systems/WaddleClothingSystem.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Shared.Clothing.Components; -using Content.Shared.Movement.Components; -using Content.Shared.Inventory.Events; - -namespace Content.Client.Clothing.Systems; - -public sealed class WaddleClothingSystem : EntitySystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnGotEquipped); - SubscribeLocalEvent(OnGotUnequipped); - } - - private void OnGotEquipped(EntityUid entity, WaddleWhenWornComponent comp, GotEquippedEvent args) - { - var waddleAnimComp = EnsureComp(args.Equipee); - - waddleAnimComp.AnimationLength = comp.AnimationLength; - waddleAnimComp.HopIntensity = comp.HopIntensity; - waddleAnimComp.RunAnimationLengthMultiplier = comp.RunAnimationLengthMultiplier; - waddleAnimComp.TumbleIntensity = comp.TumbleIntensity; - } - - private void OnGotUnequipped(EntityUid entity, WaddleWhenWornComponent comp, GotUnequippedEvent args) - { - RemComp(args.Equipee); - } -} diff --git a/Content.Client/Commands/ShowHealthBarsCommand.cs b/Content.Client/Commands/ShowHealthBarsCommand.cs index bd3e21718f0a..0811f9666378 100644 --- a/Content.Client/Commands/ShowHealthBarsCommand.cs +++ b/Content.Client/Commands/ShowHealthBarsCommand.cs @@ -35,6 +35,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) var showHealthBarsComponent = new ShowHealthBarsComponent { DamageContainers = args.ToList(), + HealthStatusIcon = null, NetSyncEnabled = false }; diff --git a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs index 90643e45cf7f..4d8dd86a4dcc 100644 --- a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs +++ b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs @@ -1,7 +1,9 @@ using Content.Client.UserInterface.Controls; using System.Threading; +using Content.Shared.CCVar; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Configuration; using Robust.Shared.Utility; using Timer = Robust.Shared.Timing.Timer; @@ -13,6 +15,8 @@ public sealed partial class CommunicationsConsoleMenu : FancyWindow private CommunicationsConsoleBoundUserInterface Owner { get; set; } private readonly CancellationTokenSource _timerCancelTokenSource = new(); + [Dependency] private readonly IConfigurationManager _cfg = default!; + public CommunicationsConsoleMenu(CommunicationsConsoleBoundUserInterface owner) { IoCManager.InjectDependencies(this); @@ -23,6 +27,22 @@ public CommunicationsConsoleMenu(CommunicationsConsoleBoundUserInterface owner) var loc = IoCManager.Resolve(); MessageInput.Placeholder = new Rope.Leaf(loc.GetString("comms-console-menu-announcement-placeholder")); + var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength); + MessageInput.OnTextChanged += (args) => + { + if (args.Control.TextLength > maxAnnounceLength) + { + AnnounceButton.Disabled = true; + AnnounceButton.ToolTip = Loc.GetString("comms-console-message-too-long"); + } + else + { + AnnounceButton.Disabled = !owner.CanAnnounce; + AnnounceButton.ToolTip = null; + + } + }; + AnnounceButton.OnPressed += (_) => Owner.AnnounceButtonPressed(Rope.Collapse(MessageInput.TextRope)); AnnounceButton.Disabled = !owner.CanAnnounce; diff --git a/Content.Client/Construction/ConstructionSystem.cs b/Content.Client/Construction/ConstructionSystem.cs index 66000a8457dc..453658bebf93 100644 --- a/Content.Client/Construction/ConstructionSystem.cs +++ b/Content.Client/Construction/ConstructionSystem.cs @@ -27,6 +27,7 @@ public sealed class ConstructionSystem : SharedConstructionSystem [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + [Dependency] private readonly ExamineSystemShared _examineSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; @@ -195,9 +196,8 @@ public bool TrySpawnGhost( if (GhostPresent(loc)) return false; - // This InRangeUnobstructed should probably be replaced with "is there something blocking us in that tile?" var predicate = GetPredicate(prototype.CanBuildInImpassable, loc.ToMap(EntityManager, _transformSystem)); - if (!_interactionSystem.InRangeUnobstructed(user, loc, 20f, predicate: predicate)) + if (!_examineSystem.InRangeUnOccluded(user, loc, 20f, predicate: predicate)) return false; if (!CheckConstructionConditions(prototype, loc, dir, user, showPopup: true)) diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index 9a0943617665..0c7912e0bcd5 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -2,6 +2,7 @@ using Content.Client.UserInterface.Systems.MenuBar.Widgets; using Content.Shared.Construction.Prototypes; using Content.Shared.Tag; +using Content.Shared.Whitelist; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Placement; @@ -23,6 +24,7 @@ namespace Content.Client.Construction.UI /// internal sealed class ConstructionMenuPresenter : IDisposable { + [Dependency] private readonly EntityManager _entManager = default!; [Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPlacementManager _placementManager = default!; @@ -30,6 +32,7 @@ internal sealed class ConstructionMenuPresenter : IDisposable [Dependency] private readonly IPlayerManager _playerManager = default!; private readonly IConstructionMenuView _constructionView; + private readonly EntityWhitelistSystem _whitelistSystem; private ConstructionSystem? _constructionSystem; private ConstructionPrototype? _selected; @@ -78,6 +81,7 @@ public ConstructionMenuPresenter() // This is a lot easier than a factory IoCManager.InjectDependencies(this); _constructionView = new ConstructionMenu(); + _whitelistSystem = _entManager.System(); // This is required so that if we load after the system is initialized, we can bind to it immediately if (_systemManager.TryGetEntitySystem(out var constructionSystem)) @@ -157,7 +161,7 @@ private void OnViewPopulateRecipes(object? sender, (string search, string catago if (_playerManager.LocalSession == null || _playerManager.LocalEntity == null - || (recipe.EntityWhitelist != null && !recipe.EntityWhitelist.IsValid(_playerManager.LocalEntity.Value))) + || _whitelistSystem.IsWhitelistFail(recipe.EntityWhitelist, _playerManager.LocalEntity.Value)) continue; if (!string.IsNullOrEmpty(search)) diff --git a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml index eec5c229cb53..322e4e66a9a9 100644 --- a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml +++ b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml @@ -8,7 +8,7 @@ - + diff --git a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs index ad19bc30f42f..9f3d5695bb60 100644 --- a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs +++ b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs @@ -23,7 +23,6 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow private readonly ItemSlotsSystem _itemSlots; private readonly FlatpackSystem _flatpack; private readonly MaterialStorageSystem _materialStorage; - private readonly SpriteSystem _spriteSystem; private readonly EntityUid _owner; @@ -31,7 +30,6 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow public const string NoBoardEffectId = "FlatpackerNoBoardEffect"; private EntityUid? _currentBoard = EntityUid.Invalid; - private EntityUid? _machinePreview; public event Action? PackButtonPressed; @@ -43,7 +41,6 @@ public FlatpackCreatorMenu(EntityUid uid) _itemSlots = _entityManager.System(); _flatpack = _entityManager.System(); _materialStorage = _entityManager.System(); - _spriteSystem = _entityManager.System(); _owner = uid; @@ -57,17 +54,10 @@ protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); - if (_machinePreview is not { } && _entityManager.Deleted(_machinePreview)) - { - _machinePreview = null; - MachineSprite.SetEntity(_machinePreview); - } - if (!_entityManager.TryGetComponent(_owner, out var flatpacker) || !_itemSlots.TryGetSlot(_owner, flatpacker.SlotId, out var itemSlot)) return; - MachineBoardComponent? machineBoardComp = null; if (flatpacker.Packing) { PackButton.Disabled = true; @@ -75,11 +65,10 @@ protected override void FrameUpdate(FrameEventArgs args) else if (_currentBoard != null) { Dictionary cost; - if (_entityManager.TryGetComponent(_currentBoard, out machineBoardComp) && - machineBoardComp.Prototype is not null) + if (_entityManager.TryGetComponent(_currentBoard, out var machineBoardComp)) cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp)); else - cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker)); + cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null); PackButton.Disabled = !_materialStorage.CanChangeMaterialAmount(_owner, cost); } @@ -87,9 +76,6 @@ protected override void FrameUpdate(FrameEventArgs args) if (_currentBoard == itemSlot.Item) return; - if (_machinePreview != null) - _entityManager.DeleteEntity(_machinePreview); - _currentBoard = itemSlot.Item; CostHeaderLabel.Visible = _currentBoard != null; InsertLabel.Visible = _currentBoard == null; @@ -99,35 +85,32 @@ protected override void FrameUpdate(FrameEventArgs args) string? prototype = null; Dictionary? cost = null; - if (machineBoardComp != null || _entityManager.TryGetComponent(_currentBoard, out machineBoardComp)) + if (_entityManager.TryGetComponent(_currentBoard, out var newMachineBoardComp)) { - prototype = machineBoardComp.Prototype; - cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp)); + prototype = newMachineBoardComp.Prototype; + cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, newMachineBoardComp)); } else if (_entityManager.TryGetComponent(_currentBoard, out var computerBoard)) { prototype = computerBoard.Prototype; - cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker)); + cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null); } if (prototype is not null && cost is not null) { var proto = _prototypeManager.Index(prototype); - _machinePreview = _entityManager.Spawn(proto.ID); - _spriteSystem.ForceUpdate(_machinePreview.Value); + MachineSprite.SetPrototype(prototype); MachineNameLabel.SetMessage(proto.Name); CostLabel.SetMarkup(GetCostString(cost)); } } else { - _machinePreview = _entityManager.Spawn(NoBoardEffectId); + MachineSprite.SetPrototype(NoBoardEffectId); CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label")); MachineNameLabel.SetMessage(" "); PackButton.Disabled = true; } - - MachineSprite.SetEntity(_machinePreview); } private string GetCostString(Dictionary costs) @@ -149,7 +132,7 @@ private string GetCostString(Dictionary costs) ("amount", amountText), ("material", Loc.GetString(matProto.Name))); - msg.AddMarkup(text); + msg.TryAddMarkup(text, out _); if (i != orderedCosts.Length - 1) msg.PushNewline(); @@ -157,12 +140,4 @@ private string GetCostString(Dictionary costs) return msg.ToMarkup(); } - - public override void Close() - { - base.Close(); - - _entityManager.DeleteEntity(_machinePreview); - _machinePreview = null; - } } diff --git a/Content.Client/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Client/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index 2a34376f6a69..4bf9d4c3e1c1 100644 --- a/Content.Client/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Client/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -2,6 +2,4 @@ namespace Content.Client.CriminalRecords.Systems; -public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleSystem -{ -} +public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleSystem; diff --git a/Content.Client/CriminalRecords/Systems/CriminalRecordsHackerSystem.cs b/Content.Client/CriminalRecords/Systems/CriminalRecordsHackerSystem.cs new file mode 100644 index 000000000000..c895a00c8873 --- /dev/null +++ b/Content.Client/CriminalRecords/Systems/CriminalRecordsHackerSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.CriminalRecords.Systems; + +namespace Content.Client.CriminalRecords.Systems; + +public sealed class CriminalRecordsHackerSystem : SharedCriminalRecordsHackerSystem; diff --git a/Content.Client/CriminalRecords/Systems/CriminalRecordsSystem.cs b/Content.Client/CriminalRecords/Systems/CriminalRecordsSystem.cs new file mode 100644 index 000000000000..c0b98d7ce311 --- /dev/null +++ b/Content.Client/CriminalRecords/Systems/CriminalRecordsSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.CriminalRecords.Systems; + +namespace Content.Client.CriminalRecords.Systems; + +public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem; diff --git a/Content.Client/Decals/DecalSystem.cs b/Content.Client/Decals/DecalSystem.cs index 901ab270fb59..41e5f39c2867 100644 --- a/Content.Client/Decals/DecalSystem.cs +++ b/Content.Client/Decals/DecalSystem.cs @@ -56,34 +56,43 @@ protected override void OnDecalRemoved(EntityUid gridId, uint decalId, DecalGrid private void OnHandleState(EntityUid gridUid, DecalGridComponent gridComp, ref ComponentHandleState args) { - if (args.Current is not DecalGridState state) - return; - // is this a delta or full state? _removedChunks.Clear(); + Dictionary modifiedChunks; - if (!state.FullState) + switch (args.Current) { - foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) + case DecalGridDeltaState delta: { - if (!state.AllChunks!.Contains(key)) - _removedChunks.Add(key); + modifiedChunks = delta.ModifiedChunks; + foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) + { + if (!delta.AllChunks.Contains(key)) + _removedChunks.Add(key); + } + + break; } - } - else - { - foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) + case DecalGridState state: { - if (!state.Chunks.ContainsKey(key)) - _removedChunks.Add(key); + modifiedChunks = state.Chunks; + foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) + { + if (!state.Chunks.ContainsKey(key)) + _removedChunks.Add(key); + } + + break; } + default: + return; } if (_removedChunks.Count > 0) RemoveChunks(gridUid, gridComp, _removedChunks); - if (state.Chunks.Count > 0) - UpdateChunks(gridUid, gridComp, state.Chunks); + if (modifiedChunks.Count > 0) + UpdateChunks(gridUid, gridComp, modifiedChunks); } private void OnChunkUpdate(DecalChunkUpdateEvent ev) diff --git a/Content.Client/Decals/Overlays/DecalOverlay.cs b/Content.Client/Decals/Overlays/DecalOverlay.cs index d9904ae80b5e..0de3301e5891 100644 --- a/Content.Client/Decals/Overlays/DecalOverlay.cs +++ b/Content.Client/Decals/Overlays/DecalOverlay.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Shared.Decals; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -113,7 +114,7 @@ protected override void Draw(in OverlayDrawArgs args) handle.DrawTexture(cache.Texture, decal.Coordinates, angle, decal.Color); } - handle.SetTransform(Matrix3.Identity); + handle.SetTransform(Matrix3x2.Identity); } } } diff --git a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs index be277448eda2..845bd7c03d27 100644 --- a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs +++ b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs @@ -51,7 +51,7 @@ protected override void Draw(in OverlayDrawArgs args) var handle = args.WorldHandle; handle.SetTransform(worldMatrix); - var localPos = invMatrix.Transform(mousePos.Position); + var localPos = Vector2.Transform(mousePos.Position, invMatrix); if (snap) { @@ -63,6 +63,6 @@ protected override void Draw(in OverlayDrawArgs args) var box = new Box2Rotated(aabb, rotation, localPos); handle.DrawTextureRect(_sprite.Frame0(decal.Sprite), box, color); - handle.SetTransform(Matrix3.Identity); + handle.SetTransform(Matrix3x2.Identity); } } diff --git a/Content.Client/Decals/ToggleDecalCommand.cs b/Content.Client/Decals/ToggleDecalCommand.cs index 9f0851f08060..025ed1299d1f 100644 --- a/Content.Client/Decals/ToggleDecalCommand.cs +++ b/Content.Client/Decals/ToggleDecalCommand.cs @@ -5,11 +5,13 @@ namespace Content.Client.Decals; public sealed class ToggleDecalCommand : IConsoleCommand { + [Dependency] private readonly IEntityManager _e = default!; + public string Command => "toggledecals"; public string Description => "Toggles decaloverlay"; public string Help => $"{Command}"; public void Execute(IConsoleShell shell, string argStr, string[] args) { - EntitySystem.Get().ToggleOverlay(); + _e.System().ToggleOverlay(); } } diff --git a/Content.Client/Decals/UI/DecalPlacerWindow.xaml.cs b/Content.Client/Decals/UI/DecalPlacerWindow.xaml.cs index 1be175108079..21b816515a48 100644 --- a/Content.Client/Decals/UI/DecalPlacerWindow.xaml.cs +++ b/Content.Client/Decals/UI/DecalPlacerWindow.xaml.cs @@ -16,6 +16,7 @@ namespace Content.Client.Decals.UI; public sealed partial class DecalPlacerWindow : DefaultWindow { [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IEntityManager _e = default!; private readonly DecalPlacementSystem _decalPlacementSystem; @@ -39,7 +40,7 @@ public DecalPlacerWindow() RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - _decalPlacementSystem = EntitySystem.Get(); + _decalPlacementSystem = _e.System(); // This needs to be done in C# so we can have custom stuff passed in the constructor // and thus have a proper step size diff --git a/Content.Client/DeviceNetwork/JammerSystem.cs b/Content.Client/DeviceNetwork/JammerSystem.cs new file mode 100644 index 000000000000..c7dbf8c8fece --- /dev/null +++ b/Content.Client/DeviceNetwork/JammerSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Radio.EntitySystems; + +namespace Content.Client.DeviceNetwork; + +public sealed class JammerSystem : SharedJammerSystem +{ + +} diff --git a/Content.Client/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs b/Content.Client/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs new file mode 100644 index 000000000000..39e03a17daee --- /dev/null +++ b/Content.Client/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.DeviceNetwork.Systems; + +namespace Content.Client.DeviceNetwork.Systems; + +/// +public sealed class DeviceNetworkJammerSystem : SharedDeviceNetworkJammerSystem; diff --git a/Content.Client/Disposal/Systems/DisposalUnitSystem.cs b/Content.Client/Disposal/Systems/DisposalUnitSystem.cs index b9e4a3866045..2fe56fcce990 100644 --- a/Content.Client/Disposal/Systems/DisposalUnitSystem.cs +++ b/Content.Client/Disposal/Systems/DisposalUnitSystem.cs @@ -153,7 +153,7 @@ private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, Sprite } } else if (state == VisualState.OverlayCharging) - sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, new RSI.StateId("disposal-charging")); + sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, chargingState); else _animationSystem.Stop(uid, AnimationKey); diff --git a/Content.Client/DoAfter/DoAfterOverlay.cs b/Content.Client/DoAfter/DoAfterOverlay.cs index 2e23dd44caca..dfbbf1089172 100644 --- a/Content.Client/DoAfter/DoAfterOverlay.cs +++ b/Content.Client/DoAfter/DoAfterOverlay.cs @@ -21,7 +21,7 @@ public sealed class DoAfterOverlay : Overlay private readonly ProgressColorSystem _progressColor; private readonly Texture _barTexture; - private readonly ShaderInstance _shader; + private readonly ShaderInstance _unshadedShader; /// /// Flash time for cancelled DoAfters @@ -45,7 +45,7 @@ public DoAfterOverlay(IEntityManager entManager, IPrototypeManager protoManager, var sprite = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/progress_bar.rsi"), "icon"); _barTexture = _entManager.EntitySysManager.GetEntitySystem().Frame0(sprite); - _shader = protoManager.Index("unshaded").Instance(); + _unshadedShader = protoManager.Index("unshaded").Instance(); } protected override void Draw(in OverlayDrawArgs args) @@ -56,9 +56,8 @@ protected override void Draw(in OverlayDrawArgs args) // If you use the display UI scale then need to set max(1f, displayscale) because 0 is valid. const float scale = 1f; - var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale)); - var rotationMatrix = Matrix3.CreateRotation(-rotation); - handle.UseShader(_shader); + var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale)); + var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation); var curTime = _timing.CurTime; @@ -79,15 +78,22 @@ protected override void Draw(in OverlayDrawArgs args) if (!bounds.Contains(worldPosition)) continue; + // shades the do-after bar if the do-after bar belongs to other players + // does not shade do-afters belonging to the local player + if (uid != localEnt) + handle.UseShader(null); + else + handle.UseShader(_unshadedShader); + // If the entity is paused, we will draw the do-after as it was when the entity got paused. var meta = metaQuery.GetComponent(uid); var time = meta.EntityPaused ? curTime - _meta.GetPauseTime(uid, meta) : curTime; - var worldMatrix = Matrix3.CreateTranslation(worldPosition); - Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld); - Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty); + var worldMatrix = Matrix3Helpers.CreateTranslation(worldPosition); + var scaledWorld = Matrix3x2.Multiply(scaleMatrix, worldMatrix); + var matty = Matrix3x2.Multiply(rotationMatrix, scaledWorld); handle.SetTransform(matty); var offset = 0f; @@ -145,7 +151,7 @@ protected override void Draw(in OverlayDrawArgs args) } handle.UseShader(null); - handle.SetTransform(Matrix3.Identity); + handle.SetTransform(Matrix3x2.Identity); } public Color GetProgressColor(float progress, float alpha = 1f) diff --git a/Content.Client/Doors/FirelockSystem.cs b/Content.Client/Doors/FirelockSystem.cs index cfd84a471336..ad869391f4b8 100644 --- a/Content.Client/Doors/FirelockSystem.cs +++ b/Content.Client/Doors/FirelockSystem.cs @@ -1,9 +1,10 @@ using Content.Shared.Doors.Components; +using Content.Shared.Doors.Systems; using Robust.Client.GameObjects; namespace Content.Client.Doors; -public sealed class FirelockSystem : EntitySystem +public sealed class FirelockSystem : SharedFirelockSystem { [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; @@ -24,15 +25,12 @@ private void OnAppearanceChange(EntityUid uid, FirelockComponent comp, ref Appea if (!_appearanceSystem.TryGetData(uid, DoorVisuals.State, out var state, args.Component)) state = DoorState.Closed; - if (_appearanceSystem.TryGetData(uid, DoorVisuals.Powered, out var powered, args.Component) && powered) - { - boltedVisible = _appearanceSystem.TryGetData(uid, DoorVisuals.BoltLights, out var lights, args.Component) && lights; - unlitVisible = - state == DoorState.Closing - || state == DoorState.Opening - || state == DoorState.Denying - || (_appearanceSystem.TryGetData(uid, DoorVisuals.ClosedLights, out var closedLights, args.Component) && closedLights); - } + boltedVisible = _appearanceSystem.TryGetData(uid, DoorVisuals.BoltLights, out var lights, args.Component) && lights; + unlitVisible = + state == DoorState.Closing + || state == DoorState.Opening + || state == DoorState.Denying + || (_appearanceSystem.TryGetData(uid, DoorVisuals.ClosedLights, out var closedLights, args.Component) && closedLights); args.Sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, unlitVisible && !boltedVisible); args.Sprite.LayerSetVisible(DoorVisualLayers.BaseBolted, boltedVisible); diff --git a/Content.Client/Drugs/RainbowOverlay.cs b/Content.Client/Drugs/RainbowOverlay.cs index e62b0dfa66cd..fb48c9101096 100644 --- a/Content.Client/Drugs/RainbowOverlay.cs +++ b/Content.Client/Drugs/RainbowOverlay.cs @@ -1,7 +1,9 @@ +using Content.Shared.CCVar; using Content.Shared.Drugs; using Content.Shared.StatusEffect; using Robust.Client.Graphics; using Robust.Client.Player; +using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -10,6 +12,7 @@ namespace Content.Client.Drugs; public sealed class RainbowOverlay : Overlay { + [Dependency] private readonly IConfigurationManager _config = default!; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; @@ -75,6 +78,10 @@ protected override bool BeforeDraw(in OverlayDrawArgs args) protected override void Draw(in OverlayDrawArgs args) { + // TODO disable only the motion part or ike's idea (single static frame of the overlay) + if (_config.GetCVar(CCVars.ReducedMotion)) + return; + if (ScreenTexture == null) return; diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 47f11ee16164..dd7e781f0bb4 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -2,18 +2,16 @@ using Content.Client.Changelog; using Content.Client.Chat.Managers; using Content.Client.Eui; -using Content.Client.Flash; using Content.Client.Fullscreen; using Content.Client.GhostKick; using Content.Client.Guidebook; -using Content.Client.Info; using Content.Client.Input; using Content.Client.IoC; using Content.Client.Launcher; +using Content.Client.Lobby; using Content.Client.MainMenu; using Content.Client.Parallax.Managers; using Content.Client.Players.PlayTimeTracking; -using Content.Client.Preferences; using Content.Client.Radiation.Overlays; using Content.Client.Replay; using Content.Client.Screenshot; @@ -52,7 +50,6 @@ public sealed class EntryPoint : GameClient [Dependency] private readonly IScreenshotHook _screenshotHook = default!; [Dependency] private readonly FullscreenHook _fullscreenHook = default!; [Dependency] private readonly ChangelogManager _changelogManager = default!; - [Dependency] private readonly RulesManager _rulesManager = default!; [Dependency] private readonly ViewportManager _viewportManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; @@ -118,13 +115,13 @@ public override void Init() _prototypeManager.RegisterIgnore("wireLayout"); _prototypeManager.RegisterIgnore("alertLevels"); _prototypeManager.RegisterIgnore("nukeopsRole"); + _prototypeManager.RegisterIgnore("ghostRoleRaffleDecider"); _componentFactory.GenerateNetIds(); _adminManager.Initialize(); _screenshotHook.Initialize(); _fullscreenHook.Initialize(); _changelogManager.Initialize(); - _rulesManager.Initialize(); _viewportManager.Initialize(); _ghostKick.Initialize(); _extendedDisconnectInformation.Initialize(); @@ -151,7 +148,6 @@ public override void PostInit() _parallaxManager.LoadDefaultParallax(); _overlayManager.AddOverlay(new SingularityOverlay()); - _overlayManager.AddOverlay(new FlashOverlay()); _overlayManager.AddOverlay(new RadiationPulseOverlay()); _chatManager.Initialize(); _clientPreferencesManager.Initialize(); diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs index 45db4efa53ca..b476971a13ac 100644 --- a/Content.Client/Examine/ExamineSystem.cs +++ b/Content.Client/Examine/ExamineSystem.cs @@ -239,8 +239,8 @@ public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCurso if (knowTarget) { - var itemName = FormattedMessage.RemoveMarkup(Identity.Name(target, EntityManager, player)); - var labelMessage = FormattedMessage.FromMarkup($"[bold]{itemName}[/bold]"); + var itemName = FormattedMessage.EscapeText(Identity.Name(target, EntityManager, player)); + var labelMessage = FormattedMessage.FromMarkupPermissive($"[bold]{itemName}[/bold]"); var label = new RichTextLabel(); label.SetMessage(labelMessage); hBox.AddChild(label); @@ -248,7 +248,7 @@ public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCurso else { var label = new RichTextLabel(); - label.SetMessage(FormattedMessage.FromMarkup("[bold]???[/bold]")); + label.SetMessage(FormattedMessage.FromMarkupOrThrow("[bold]???[/bold]")); hBox.AddChild(label); } diff --git a/Content.Client/Explosion/ExplosionOverlay.cs b/Content.Client/Explosion/ExplosionOverlay.cs index 2d8c15f1b9f9..8cf7447a5d8e 100644 --- a/Content.Client/Explosion/ExplosionOverlay.cs +++ b/Content.Client/Explosion/ExplosionOverlay.cs @@ -48,7 +48,7 @@ protected override void Draw(in OverlayDrawArgs args) DrawExplosion(drawHandle, args.WorldBounds, visuals, index, xforms, textures); } - drawHandle.SetTransform(Matrix3.Identity); + drawHandle.SetTransform(Matrix3x2.Identity); drawHandle.UseShader(null); } @@ -78,7 +78,8 @@ private void DrawExplosion( if (visuals.SpaceTiles == null) return; - gridBounds = Matrix3.Invert(visuals.SpaceMatrix).TransformBox(worldBounds).Enlarged(2); + Matrix3x2.Invert(visuals.SpaceMatrix, out var invSpace); + gridBounds = invSpace.TransformBox(worldBounds).Enlarged(2); drawHandle.SetTransform(visuals.SpaceMatrix); DrawTiles(drawHandle, gridBounds, index, visuals.SpaceTiles, visuals, visuals.SpaceTileSize, textures); diff --git a/Content.Client/Extinguisher/FireExtinguisherComponent.cs b/Content.Client/Extinguisher/FireExtinguisherComponent.cs index 126c172924b0..324b05a93d49 100644 --- a/Content.Client/Extinguisher/FireExtinguisherComponent.cs +++ b/Content.Client/Extinguisher/FireExtinguisherComponent.cs @@ -3,7 +3,5 @@ namespace Content.Client.Extinguisher; -[NetworkedComponent, RegisterComponent] -public sealed partial class FireExtinguisherComponent : SharedFireExtinguisherComponent -{ -} +[RegisterComponent] +public sealed partial class FireExtinguisherComponent : SharedFireExtinguisherComponent; diff --git a/Content.Client/Eye/EyeLerpingSystem.cs b/Content.Client/Eye/EyeLerpingSystem.cs index 78e1b851fcbd..ac32299dca7d 100644 --- a/Content.Client/Eye/EyeLerpingSystem.cs +++ b/Content.Client/Eye/EyeLerpingSystem.cs @@ -86,7 +86,7 @@ public void RemoveEye(EntityUid uid) private void HandleMapChange(EntityUid uid, LerpingEyeComponent component, ref EntParentChangedMessage args) { // Is this actually a map change? If yes, stop any lerps - if (args.OldMapId != args.Transform.MapID) + if (args.OldMapId != args.Transform.MapUid) component.LastRotation = GetRotation(uid, args.Transform); } diff --git a/Content.Client/Fax/System/FaxVisualsSystem.cs b/Content.Client/Fax/System/FaxVisualsSystem.cs new file mode 100644 index 000000000000..892aec1d9549 --- /dev/null +++ b/Content.Client/Fax/System/FaxVisualsSystem.cs @@ -0,0 +1,48 @@ +using Robust.Client.GameObjects; +using Content.Shared.Fax.Components; +using Content.Shared.Fax; +using Robust.Client.Animations; + +namespace Content.Client.Fax.System; + +/// +/// Visualizer for the fax machine which displays the correct sprite based on the inserted entity. +/// +public sealed class FaxVisualsSystem : EntitySystem +{ + [Dependency] private readonly AnimationPlayerSystem _player = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAppearanceChanged); + } + + private void OnAppearanceChanged(EntityUid uid, FaxMachineComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + return; + + if (_appearance.TryGetData(uid, FaxMachineVisuals.VisualState, out FaxMachineVisualState visuals) && visuals == FaxMachineVisualState.Inserting) + { + _player.Play(uid, new Animation() + { + Length = TimeSpan.FromSeconds(2.4), + AnimationTracks = + { + new AnimationTrackSpriteFlick() + { + LayerKey = FaxMachineVisuals.VisualState, + KeyFrames = + { + new AnimationTrackSpriteFlick.KeyFrame(component.InsertingState, 0f), + new AnimationTrackSpriteFlick.KeyFrame("icon", 2.4f), + } + } + } + }, "faxecute"); + } + } +} diff --git a/Content.Client/Fax/UI/FaxBoundUi.cs b/Content.Client/Fax/UI/FaxBoundUi.cs index 9b57595d7b49..a95066a3b58f 100644 --- a/Content.Client/Fax/UI/FaxBoundUi.cs +++ b/Content.Client/Fax/UI/FaxBoundUi.cs @@ -40,7 +40,7 @@ private async void OnFileButtonPressed() { if (_dialogIsOpen) return; - + _dialogIsOpen = true; var filters = new FileDialogFilters(new FileDialogFilters.Group("txt")); await using var file = await _fileDialogManager.OpenFile(filters); @@ -52,8 +52,27 @@ private async void OnFileButtonPressed() } using var reader = new StreamReader(file); + + var firstLine = await reader.ReadLineAsync(); + string? label = null; var content = await reader.ReadToEndAsync(); - SendMessage(new FaxFileMessage(content[..Math.Min(content.Length, FaxFileMessageValidation.MaxContentSize)], _window.OfficePaper)); + + if (firstLine is { }) + { + if (firstLine.StartsWith('#')) + { + label = firstLine[1..].Trim(); + } + else + { + content = firstLine + "\n" + content; + } + } + + SendMessage(new FaxFileMessage( + label?[..Math.Min(label.Length, FaxFileMessageValidation.MaxLabelSize)], + content[..Math.Min(content.Length, FaxFileMessageValidation.MaxContentSize)], + _window.OfficePaper)); } private void OnSendButtonPressed() diff --git a/Content.Client/Flash/FlashOverlay.cs b/Content.Client/Flash/FlashOverlay.cs index fe9c888227ec..9ea00275e842 100644 --- a/Content.Client/Flash/FlashOverlay.cs +++ b/Content.Client/Flash/FlashOverlay.cs @@ -1,12 +1,11 @@ -using System.Numerics; +using Content.Shared.Flash; +using Content.Shared.Flash.Components; +using Content.Shared.StatusEffect; using Content.Client.Viewport; using Robust.Client.Graphics; using Robust.Client.State; using Robust.Client.Player; using Robust.Shared.Enums; -using Robust.Shared.Graphics; -using Robust.Shared.IoC; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using SixLabors.ImageSharp.PixelFormats; @@ -17,66 +16,87 @@ public sealed class FlashOverlay : Overlay { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IClyde _displayManager = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + private readonly StatusEffectsSystem _statusSys; public override OverlaySpace Space => OverlaySpace.WorldSpace; private readonly ShaderInstance _shader; - private double _startTime = -1; - private double _lastsFor = 1; - private Texture? _screenshotTexture; + public float PercentComplete = 0.0f; + public Texture? ScreenshotTexture; public FlashOverlay() { IoCManager.InjectDependencies(this); - _shader = _prototypeManager.Index("FlashedEffect").Instance().Duplicate(); + _shader = _prototypeManager.Index("FlashedEffect").InstanceUnique(); + _statusSys = _entityManager.System(); } - public void ReceiveFlash(double duration) + protected override void FrameUpdate(FrameEventArgs args) + { + var playerEntity = _playerManager.LocalEntity; + + if (playerEntity == null) + return; + + if (!_entityManager.HasComponent(playerEntity) + || !_entityManager.TryGetComponent(playerEntity, out var status)) + return; + + if (!_statusSys.TryGetTime(playerEntity.Value, SharedFlashSystem.FlashedKey, out var time, status)) + return; + + var curTime = _timing.CurTime; + var lastsFor = (float) (time.Value.Item2 - time.Value.Item1).TotalSeconds; + var timeDone = (float) (curTime - time.Value.Item1).TotalSeconds; + + PercentComplete = timeDone / lastsFor; + } + + public void ReceiveFlash() { if (_stateManager.CurrentState is IMainViewportState state) { + // take a screenshot + // note that the callback takes a while and ScreenshotTexture will be null the first few Draws state.Viewport.Viewport.Screenshot(image => { var rgba32Image = image.CloneAs(SixLabors.ImageSharp.Configuration.Default); - _screenshotTexture = _displayManager.LoadTextureFromImage(rgba32Image); + ScreenshotTexture = _displayManager.LoadTextureFromImage(rgba32Image); }); } - - _startTime = _gameTiming.CurTime.TotalSeconds; - _lastsFor = duration; } - protected override void Draw(in OverlayDrawArgs args) + protected override bool BeforeDraw(in OverlayDrawArgs args) { if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp)) - return; - + return false; if (args.Viewport.Eye != eyeComp.Eye) - return; + return false; + + return PercentComplete < 1.0f; + } - var percentComplete = (float) ((_gameTiming.CurTime.TotalSeconds - _startTime) / _lastsFor); - if (percentComplete >= 1.0f) + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenshotTexture == null) return; var worldHandle = args.WorldHandle; + _shader.SetParameter("percentComplete", PercentComplete); worldHandle.UseShader(_shader); - _shader.SetParameter("percentComplete", percentComplete); - - if (_screenshotTexture != null) - { - worldHandle.DrawTextureRectRegion(_screenshotTexture, args.WorldBounds); - } - + worldHandle.DrawTextureRectRegion(ScreenshotTexture, args.WorldBounds); worldHandle.UseShader(null); } protected override void DisposeBehavior() { base.DisposeBehavior(); - _screenshotTexture = null; + ScreenshotTexture = null; + PercentComplete = 1.0f; } } } diff --git a/Content.Client/Flash/FlashSystem.cs b/Content.Client/Flash/FlashSystem.cs index ad8f8b0b82bf..9a0579f6aa38 100644 --- a/Content.Client/Flash/FlashSystem.cs +++ b/Content.Client/Flash/FlashSystem.cs @@ -1,62 +1,67 @@ using Content.Shared.Flash; +using Content.Shared.Flash.Components; +using Content.Shared.StatusEffect; using Robust.Client.Graphics; using Robust.Client.Player; -using Robust.Shared.GameStates; -using Robust.Shared.Timing; +using Robust.Shared.Player; -namespace Content.Client.Flash +namespace Content.Client.Flash; + +public sealed class FlashSystem : SharedFlashSystem { - public sealed class FlashSystem : SharedFlashSystem + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + private FlashOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnStatusAdded); + + _overlay = new(); + } + + private void OnPlayerAttached(EntityUid uid, FlashedComponent component, LocalPlayerAttachedEvent args) { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IOverlayManager _overlayManager = default!; + _overlayMan.AddOverlay(_overlay); + } - public override void Initialize() + private void OnPlayerDetached(EntityUid uid, FlashedComponent component, LocalPlayerDetachedEvent args) + { + _overlay.PercentComplete = 1.0f; + _overlay.ScreenshotTexture = null; + _overlayMan.RemoveOverlay(_overlay); + } + + private void OnInit(EntityUid uid, FlashedComponent component, ComponentInit args) + { + if (_player.LocalEntity == uid) { - base.Initialize(); + _overlayMan.AddOverlay(_overlay); + } + } - SubscribeLocalEvent(OnFlashableHandleState); + private void OnShutdown(EntityUid uid, FlashedComponent component, ComponentShutdown args) + { + if (_player.LocalEntity == uid) + { + _overlay.PercentComplete = 1.0f; + _overlay.ScreenshotTexture = null; + _overlayMan.RemoveOverlay(_overlay); } + } - private void OnFlashableHandleState(EntityUid uid, FlashableComponent component, ref ComponentHandleState args) + private void OnStatusAdded(EntityUid uid, FlashedComponent component, StatusEffectAddedEvent args) + { + if (_player.LocalEntity == uid && args.Key == FlashedKey) { - if (args.Current is not FlashableComponentState state) - return; - - // Yes, this code is awful. I'm just porting it to an entity system so don't blame me. - if (_playerManager.LocalEntity != uid) - { - return; - } - - if (state.Time == default) - { - return; - } - - // Few things here: - // 1. If a shorter duration flash is applied then don't do anything - // 2. If the client-side time is later than when the flash should've ended don't do anything - var currentTime = _gameTiming.CurTime.TotalSeconds; - var newEndTime = state.Time.TotalSeconds + state.Duration; - var currentEndTime = component.LastFlash.TotalSeconds + component.Duration; - - if (currentEndTime > newEndTime) - { - return; - } - - if (currentTime > newEndTime) - { - return; - } - - component.LastFlash = state.Time; - component.Duration = state.Duration; - - var overlay = _overlayManager.GetOverlay(); - overlay.ReceiveFlash(component.Duration); + _overlay.ReceiveFlash(); } } } diff --git a/Content.Client/FlavorText/FlavorText.xaml.cs b/Content.Client/FlavorText/FlavorText.xaml.cs index ffcf653f119a..91b59046a477 100644 --- a/Content.Client/FlavorText/FlavorText.xaml.cs +++ b/Content.Client/FlavorText/FlavorText.xaml.cs @@ -17,7 +17,7 @@ public FlavorText() var loc = IoCManager.Resolve(); CFlavorTextInput.Placeholder = new Rope.Leaf(loc.GetString("flavor-text-placeholder")); - CFlavorTextInput.OnKeyBindDown += _ => FlavorTextChanged(); + CFlavorTextInput.OnTextChanged += _ => FlavorTextChanged(); } public void FlavorTextChanged() diff --git a/Content.Client/Fluids/PuddleOverlay.cs b/Content.Client/Fluids/PuddleOverlay.cs index ac6661cfdd83..a8c1d355105b 100644 --- a/Content.Client/Fluids/PuddleOverlay.cs +++ b/Content.Client/Fluids/PuddleOverlay.cs @@ -1,4 +1,5 @@ -using Content.Shared.FixedPoint; +using System.Numerics; +using Content.Shared.FixedPoint; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Shared.Enums; @@ -73,7 +74,7 @@ private void DrawWorld(in OverlayDrawArgs args) } } - drawHandle.SetTransform(Matrix3.Identity); + drawHandle.SetTransform(Matrix3x2.Identity); } private void DrawScreen(in OverlayDrawArgs args) @@ -99,7 +100,7 @@ private void DrawScreen(in OverlayDrawArgs args) if (!gridBounds.Contains(centre)) continue; - var screenCenter = _eyeManager.WorldToScreen(matrix.Transform(centre)); + var screenCenter = _eyeManager.WorldToScreen(Vector2.Transform(centre, matrix)); drawHandle.DrawString(_font, screenCenter, debugOverlayData.CurrentVolume.ToString(), Color.White); } diff --git a/Content.Client/GPS/UI/HandheldGpsStatusControl.cs b/Content.Client/GPS/UI/HandheldGpsStatusControl.cs index de6a1031bad0..7dcf3f29c519 100644 --- a/Content.Client/GPS/UI/HandheldGpsStatusControl.cs +++ b/Content.Client/GPS/UI/HandheldGpsStatusControl.cs @@ -1,6 +1,7 @@ using Content.Client.GPS.Components; using Content.Client.Message; using Content.Client.Stylesheets; +using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Timing; @@ -13,11 +14,13 @@ public sealed class HandheldGpsStatusControl : Control private readonly RichTextLabel _label; private float _updateDif; private readonly IEntityManager _entMan; + private readonly SharedTransformSystem _transform; public HandheldGpsStatusControl(Entity parent) { _parent = parent; _entMan = IoCManager.Resolve(); + _transform = _entMan.System(); _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; AddChild(_label); UpdateGpsDetails(); @@ -41,7 +44,7 @@ private void UpdateGpsDetails() var posText = "Error"; if (_entMan.TryGetComponent(_parent, out TransformComponent? transComp)) { - var pos = transComp.MapPosition; + var pos = _transform.GetMapCoordinates(_parent.Owner, xform: transComp); var x = (int) pos.X; var y = (int) pos.Y; posText = $"({x}, {y})"; diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs index df709e944461..fcf5ae91a497 100644 --- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs +++ b/Content.Client/GameTicking/Managers/ClientGameTicker.cs @@ -1,12 +1,15 @@ +using Content.Client.Administration.Managers; using Content.Client.Gameplay; using Content.Client.Lobby; using Content.Client.RoundEnd; using Content.Shared.GameTicking; using Content.Shared.GameWindow; +using Content.Shared.Roles; using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.State; -using Robust.Shared.Utility; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; namespace Content.Client.GameTicking.Managers { @@ -14,17 +17,13 @@ namespace Content.Client.GameTicking.Managers public sealed class ClientGameTicker : SharedGameTicker { [Dependency] private readonly IStateManager _stateManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IClientAdminManager _admin = default!; + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; - [ViewVariables] private bool _initialized; - private Dictionary> _jobsAvailable = new(); + private Dictionary, int?>> _jobsAvailable = new(); private Dictionary _stationNames = new(); - /// - /// The current round-end window. Could be used to support re-opening the window after closing it. - /// - private RoundEndSummaryWindow? _window; - [ViewVariables] public bool AreWeReady { get; private set; } [ViewVariables] public bool IsGameStarted { get; private set; } [ViewVariables] public string? RestartSound { get; private set; } @@ -34,18 +33,16 @@ public sealed class ClientGameTicker : SharedGameTicker [ViewVariables] public TimeSpan StartTime { get; private set; } [ViewVariables] public new bool Paused { get; private set; } - [ViewVariables] public IReadOnlyDictionary> JobsAvailable => _jobsAvailable; + [ViewVariables] public IReadOnlyDictionary, int?>> JobsAvailable => _jobsAvailable; [ViewVariables] public IReadOnlyDictionary StationNames => _stationNames; public event Action? InfoBlobUpdated; public event Action? LobbyStatusUpdated; public event Action? LobbyLateJoinStatusUpdated; - public event Action>>? LobbyJobsAvailableUpdated; + public event Action, int?>>>? LobbyJobsAvailableUpdated; public override void Initialize() { - DebugTools.Assert(!_initialized); - SubscribeNetworkEvent(JoinLobby); SubscribeNetworkEvent(JoinGame); SubscribeNetworkEvent(ConnectionStatus); @@ -53,14 +50,33 @@ public override void Initialize() SubscribeNetworkEvent(LobbyInfo); SubscribeNetworkEvent(LobbyCountdown); SubscribeNetworkEvent(RoundEnd); - SubscribeNetworkEvent(msg => - { - IoCManager.Resolve().RequestWindowAttention(); - }); + SubscribeNetworkEvent(OnAttentionRequest); SubscribeNetworkEvent(LateJoinStatus); SubscribeNetworkEvent(UpdateJobsAvailable); - _initialized = true; + _admin.AdminStatusUpdated += OnAdminUpdated; + OnAdminUpdated(); + } + + public override void Shutdown() + { + _admin.AdminStatusUpdated -= OnAdminUpdated; + base.Shutdown(); + } + + private void OnAdminUpdated() + { + // Hide some map/grid related logs from clients. This is to try prevent some easy metagaming by just + // reading the console. E.g., logs like this one could leak the nuke station/grid: + // > Grid NT-Arrivals 1101 (122/n25896) changed parent. Old parent: map 10 (121/n25895). New parent: FTL (123/n26470) +#if !DEBUG + EntityManager.System().Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning; +#endif + } + + private void OnAttentionRequest(RequestWindowAttentionEvent ev) + { + _clyde.RequestWindowAttention(); } private void LateJoinStatus(TickerLateJoinStatusEvent message) @@ -132,12 +148,7 @@ private void RoundEnd(RoundEndMessageEvent message) // Force an update in the event of this song being the same as the last. RestartSound = message.RestartSound; - // Don't open duplicate windows (mainly for replays). - if (_window?.RoundId == message.RoundId) - return; - - //This is not ideal at all, but I don't see an immediately better fit anywhere else. - _window = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText, message.RoundDuration, message.RoundId, message.AllPlayersEndInfo, _entityManager); + _userInterfaceManager.GetUIController().OpenRoundEndSummaryWindow(message); } } } diff --git a/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs new file mode 100644 index 000000000000..480da6ad8d9b --- /dev/null +++ b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs @@ -0,0 +1,26 @@ +using Robust.Shared.Console; + +namespace Content.Client.Ghost.Commands; + +public sealed class ToggleGhostVisibilityCommand : IConsoleCommand +{ + [Dependency] private readonly IEntitySystemManager _entSysMan = default!; + + public string Command => "toggleghostvisibility"; + public string Description => "Toggles ghost visibility on the client."; + public string Help => "toggleghostvisibility [bool]"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var ghostSystem = _entSysMan.GetEntitySystem(); + + if (args.Length != 0 && bool.TryParse(args[0], out var visibility)) + { + ghostSystem.ToggleGhostVisibility(visibility); + } + else + { + ghostSystem.ToggleGhostVisibility(); + } + } +} diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index c42e7cd0e0c5..94872a58ef97 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -3,7 +3,6 @@ using Content.Shared.Ghost; using Robust.Client.Console; using Robust.Client.GameObjects; -using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Player; @@ -177,9 +176,9 @@ public void OpenGhostRoles() _console.RemoteExecuteCommand(null, "ghostroles"); } - public void ToggleGhostVisibility() + public void ToggleGhostVisibility(bool? visibility = null) { - GhostVisibility = !GhostVisibility; + GhostVisibility = visibility ?? !GhostVisibility; } } } diff --git a/Content.Client/Gravity/GravitySystem.Shake.cs b/Content.Client/Gravity/GravitySystem.Shake.cs index c4356588d355..9b9918ca3e74 100644 --- a/Content.Client/Gravity/GravitySystem.Shake.cs +++ b/Content.Client/Gravity/GravitySystem.Shake.cs @@ -25,7 +25,7 @@ private void OnShakeInit(EntityUid uid, GravityShakeComponent component, Compone { var localPlayer = _playerManager.LocalEntity; - if (!TryComp(localPlayer, out var xform) || + if (!TryComp(localPlayer, out TransformComponent? xform) || xform.GridUid != uid && xform.MapUid != uid) { return; @@ -46,7 +46,7 @@ protected override void ShakeGrid(EntityUid uid, GravityComponent? gravity = nul var localPlayer = _playerManager.LocalEntity; - if (!TryComp(localPlayer, out var xform)) + if (!TryComp(localPlayer, out TransformComponent? xform)) return; if (xform.GridUid != uid || diff --git a/Content.Client/Guidebook/Components/GuideHelpComponent.cs b/Content.Client/Guidebook/Components/GuideHelpComponent.cs index db19bb9dcc89..bb1d30bbc165 100644 --- a/Content.Client/Guidebook/Components/GuideHelpComponent.cs +++ b/Content.Client/Guidebook/Components/GuideHelpComponent.cs @@ -1,4 +1,5 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Content.Shared.Guidebook; +using Robust.Shared.Prototypes; namespace Content.Client.Guidebook.Components; @@ -13,9 +14,8 @@ public sealed partial class GuideHelpComponent : Component /// What guides to include show when opening the guidebook. The first entry will be used to select the currently /// selected guidebook. /// - [DataField("guides", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - [ViewVariables] - public List Guides = new(); + [DataField(required: true)] + public List> Guides = new(); /// /// Whether or not to automatically include the children of the given guides. diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml index f46e319abeb5..73a17e9bcc9e 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml +++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml @@ -47,6 +47,17 @@ + + + + + + + + diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs index e2b09386dfbb..87931bf84559 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs @@ -4,6 +4,7 @@ using Content.Client.Guidebook.Richtext; using Content.Client.Message; using Content.Client.UserInterface.ControlExtensions; +using Content.Shared.Body.Prototypes; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using JetBrains.Annotations; @@ -128,7 +129,7 @@ private void GenerateControl(ReagentPrototype reagent) var groupLabel = new RichTextLabel(); groupLabel.SetMarkup(Loc.GetString("guidebook-reagent-effects-metabolism-group-rate", - ("group", group), ("rate", effect.MetabolismRate))); + ("group", _prototype.Index(group).LocalizedName), ("rate", effect.MetabolismRate))); var descriptionLabel = new RichTextLabel { Margin = new Thickness(25, 0, 10, 0) @@ -156,6 +157,39 @@ private void GenerateControl(ReagentPrototype reagent) } #endregion + #region PlantMetabolisms + if (_chemistryGuideData.ReagentGuideRegistry.TryGetValue(reagent.ID, out var guideEntryRegistryPlant) && + guideEntryRegistryPlant.PlantMetabolisms != null && + guideEntryRegistryPlant.PlantMetabolisms.Count > 0) + { + PlantMetabolismsDescriptionContainer.Children.Clear(); + var metabolismLabel = new RichTextLabel(); + metabolismLabel.SetMarkup(Loc.GetString("guidebook-reagent-plant-metabolisms-rate")); + var descriptionLabel = new RichTextLabel + { + Margin = new Thickness(25, 0, 10, 0) + }; + var descMsg = new FormattedMessage(); + var descriptionsCount = guideEntryRegistryPlant.PlantMetabolisms.Count; + var i = 0; + foreach (var effectString in guideEntryRegistryPlant.PlantMetabolisms) + { + descMsg.AddMarkup(effectString); + i++; + if (i < descriptionsCount) + descMsg.PushNewline(); + } + descriptionLabel.SetMessage(descMsg); + + PlantMetabolismsDescriptionContainer.AddChild(metabolismLabel); + PlantMetabolismsDescriptionContainer.AddChild(descriptionLabel); + } + else + { + PlantMetabolismsContainer.Visible = false; + } + #endregion + GenerateSources(reagent); FormattedMessage description = new(); diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml index 8dbfde3c4754..69534af7f6a3 100644 --- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml +++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml @@ -2,7 +2,7 @@ xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" xmlns:fancyTree="clr-namespace:Content.Client.UserInterface.Controls.FancyTree" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" - SetSize="750 700" + SetSize="900 700" MinSize="100 200" Resizable="True" Title="{Loc 'guidebook-window-title'}"> @@ -18,12 +18,16 @@ Name="SearchBar" PlaceHolder="{Loc 'guidebook-filter-placeholder-text'}" HorizontalExpand="True" - Margin="0 5 10 5"> + Margin="0 5 10 5"> + + public sealed partial class DocumentParsingManager { + [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] private readonly IResourceManager _resourceManager = default!; [Dependency] private readonly ISandboxHelper _sandboxHelper = default!; private readonly Dictionary> _tagControlParsers = new(); @@ -37,6 +42,21 @@ public void Initialize() ControlParser = SkipWhitespaces.Then(_controlParser.Many()); } + public bool TryAddMarkup(Control control, ProtoId entryId, bool log = true) + { + if (!_prototype.TryIndex(entryId, out var entry)) + return false; + + using var file = _resourceManager.ContentFileReadText(entry.Text); + return TryAddMarkup(control, file.ReadToEnd(), log); + } + + public bool TryAddMarkup(Control control, GuideEntry entry, bool log = true) + { + using var file = _resourceManager.ContentFileReadText(entry.Text); + return TryAddMarkup(control, file.ReadToEnd(), log); + } + public bool TryAddMarkup(Control control, string text, bool log = true) { try diff --git a/Content.Client/Guidebook/GuideEntry.cs b/Content.Client/Guidebook/GuideEntry.cs deleted file mode 100644 index b3c004267db2..000000000000 --- a/Content.Client/Guidebook/GuideEntry.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; -using Robust.Shared.Utility; - -namespace Content.Client.Guidebook; - -[Virtual] -public class GuideEntry -{ - /// - /// The file containing the contents of this guide. - /// - [DataField("text", required: true)] public ResPath Text = default!; - - /// - /// The unique id for this guide. - /// - [IdDataField] - public string Id = default!; - - /// - /// The name of this guide. This gets localized. - /// - [DataField("name", required: true)] public string Name = default!; - - /// - /// The "children" of this guide for when guides are shown in a tree / table of contents. - /// - [DataField("children", customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List Children = new(); - - /// - /// Enable filtering of items. - /// - [DataField("filterEnabled")] public bool FilterEnabled = default!; - - /// - /// Priority for sorting top-level guides when shown in a tree / table of contents. - /// If the guide is the child of some other guide, the order simply determined by the order of children in . - /// - [DataField("priority")] public int Priority = 0; -} - -[Prototype("guideEntry")] -public sealed class GuideEntryPrototype : GuideEntry, IPrototype -{ - public string ID => Id; -} diff --git a/Content.Client/Guidebook/GuidebookSystem.cs b/Content.Client/Guidebook/GuidebookSystem.cs index cb13d4ca6e5b..675a025d7a86 100644 --- a/Content.Client/Guidebook/GuidebookSystem.cs +++ b/Content.Client/Guidebook/GuidebookSystem.cs @@ -2,6 +2,7 @@ using Content.Client.Guidebook.Components; using Content.Client.Light; using Content.Client.Verbs; +using Content.Shared.Guidebook; using Content.Shared.Interaction; using Content.Shared.Light.Components; using Content.Shared.Speech; @@ -13,6 +14,7 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -31,7 +33,12 @@ public sealed class GuidebookSystem : EntitySystem [Dependency] private readonly SharedPointLightSystem _pointLightSystem = default!; [Dependency] private readonly TagSystem _tags = default!; - public event Action, List?, string?, bool, string?>? OnGuidebookOpen; + public event Action>, + List>?, + ProtoId?, + bool, + ProtoId?>? OnGuidebookOpen; + public const string GuideEmbedTag = "GuideEmbeded"; private EntityUid _defaultUser; @@ -80,6 +87,11 @@ private void OnGetVerbs(EntityUid uid, GuideHelpComponent component, GetVerbsEve }); } + public void OpenHelp(List> guides) + { + OnGuidebookOpen?.Invoke(guides, null, null, true, guides[0]); + } + private void OnInteract(EntityUid uid, GuideHelpComponent component, ActivateInWorldEvent args) { if (!_timing.IsFirstTimePredicted) @@ -143,7 +155,7 @@ private void OnGuidebookControlsTestInteractHand(EntityUid uid, GuidebookControl public void FakeClientActivateInWorld(EntityUid activated) { - var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated); + var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated, true); RaiseLocalEvent(activated, activateMsg); } diff --git a/Content.Client/Guidebook/Richtext/Box.cs b/Content.Client/Guidebook/Richtext/Box.cs index ecf6cb21f70a..6e18ad9c5754 100644 --- a/Content.Client/Guidebook/Richtext/Box.cs +++ b/Content.Client/Guidebook/Richtext/Box.cs @@ -11,6 +11,9 @@ public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out HorizontalExpand = true; control = this; + if (args.TryGetValue("Margin", out var margin)) + Margin = new Thickness(float.Parse(margin)); + if (args.TryGetValue("Orientation", out var orientation)) Orientation = Enum.Parse(orientation); else diff --git a/Content.Client/Guidebook/Richtext/ColorBox.cs b/Content.Client/Guidebook/Richtext/ColorBox.cs new file mode 100644 index 000000000000..84de300d6e01 --- /dev/null +++ b/Content.Client/Guidebook/Richtext/ColorBox.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.Guidebook.Richtext; + +[UsedImplicitly] +public sealed class ColorBox : PanelContainer, IDocumentTag +{ + public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) + { + HorizontalExpand = true; + VerticalExpand = true; + control = this; + + if (args.TryGetValue("Margin", out var margin)) + Margin = new Thickness(float.Parse(margin)); + + if (args.TryGetValue("HorizontalAlignment", out var halign)) + HorizontalAlignment = Enum.Parse(halign); + else + HorizontalAlignment = HAlignment.Stretch; + + if (args.TryGetValue("VerticalAlignment", out var valign)) + VerticalAlignment = Enum.Parse(valign); + else + VerticalAlignment = VAlignment.Stretch; + + var styleBox = new StyleBoxFlat(); + if (args.TryGetValue("Color", out var color)) + styleBox.BackgroundColor = Color.FromHex(color); + + if (args.TryGetValue("OutlineThickness", out var outlineThickness)) + styleBox.BorderThickness = new Thickness(float.Parse(outlineThickness)); + else + styleBox.BorderThickness = new Thickness(1); + + if (args.TryGetValue("OutlineColor", out var outlineColor)) + styleBox.BorderColor = Color.FromHex(outlineColor); + else + styleBox.BorderColor = Color.White; + + PanelOverride = styleBox; + + return true; + } +} diff --git a/Content.Client/Guidebook/Richtext/Table.cs b/Content.Client/Guidebook/Richtext/Table.cs new file mode 100644 index 000000000000..b6923c3698ea --- /dev/null +++ b/Content.Client/Guidebook/Richtext/Table.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Client.UserInterface.Controls; +using JetBrains.Annotations; +using Robust.Client.UserInterface; + +namespace Content.Client.Guidebook.Richtext; + +[UsedImplicitly] +public sealed class Table : TableContainer, IDocumentTag +{ + public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) + { + HorizontalExpand = true; + control = this; + + if (!args.TryGetValue("Columns", out var columns) || !int.TryParse(columns, out var columnsCount)) + { + Logger.Error("Guidebook tag \"Table\" does not specify required property \"Columns.\""); + control = null; + return false; + } + + Columns = columnsCount; + + return true; + } +} diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs index 0cb3ad144d70..fcf6d4551fdd 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs @@ -139,7 +139,7 @@ private void DrawDiagnosticGroups( var groupTitleText = $"{Loc.GetString( "health-analyzer-window-damage-group-text", - ("damageGroup", Loc.GetString("health-analyzer-window-damage-group-" + damageGroupId)), + ("damageGroup", _prototypes.Index(damageGroupId).LocalizedName), ("amount", damageAmount) )}"; @@ -170,7 +170,7 @@ private void DrawDiagnosticGroups( var damageString = Loc.GetString( "health-analyzer-window-damage-type-text", - ("damageType", Loc.GetString("health-analyzer-window-damage-type-" + type)), + ("damageType", _prototypes.Index(type).LocalizedName), ("amount", typeAmount) ); diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 5bae35da5ba2..6eb5dd9ec987 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -108,8 +108,11 @@ private void SetLayerData( /// This should not be used if the entity is owned by the server. The server will otherwise /// override this with the appearance data it sends over. /// - public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null) + public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null) { + if (profile == null) + return; + if (!Resolve(uid, ref humanoid)) { return; diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs index f3f0cdea7d75..e2ffabd17d9e 100644 --- a/Content.Client/Implants/UI/ImplanterStatusControl.cs +++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs @@ -1,5 +1,6 @@ using Content.Client.Message; using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; using Content.Shared.Implants.Components; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -17,7 +18,7 @@ public ImplanterStatusControl(ImplanterComponent parent) _parent = parent; _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; _label.MaxWidth = 350; - AddChild(_label); + AddChild(new ClipControl { Children = { _label } }); Update(); } @@ -42,17 +43,12 @@ private void Update() _ => Loc.GetString("injector-invalid-injector-toggle-mode") }; - var (implantName, implantDescription) = _parent.ImplanterSlot.HasItem switch - { - false => (Loc.GetString("implanter-empty-text"), ""), - true => (_parent.ImplantData.Item1, _parent.ImplantData.Item2), - }; - + var implantName = _parent.ImplanterSlot.HasItem + ? _parent.ImplantData.Item1 + : Loc.GetString("implanter-empty-text"); _label.SetMarkup(Loc.GetString("implanter-label", ("implantName", implantName), - ("implantDescription", implantDescription), - ("modeString", modeStringLocalized), - ("lineBreak", "\n"))); + ("modeString", modeStringLocalized))); } } diff --git a/Content.Client/Info/InfoSystem.cs b/Content.Client/Info/InfoSystem.cs deleted file mode 100644 index b69799498187..000000000000 --- a/Content.Client/Info/InfoSystem.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Shared.Info; -using Robust.Shared.Log; - -namespace Content.Client.Info; - -public sealed class InfoSystem : EntitySystem -{ - public RulesMessage Rules = new RulesMessage("Server Rules", "The server did not send any rules."); - [Dependency] private readonly RulesManager _rules = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeNetworkEvent(OnRulesReceived); - Log.Debug("Requested server info."); - RaiseNetworkEvent(new RequestRulesMessage()); - } - - private void OnRulesReceived(RulesMessage message, EntitySessionEventArgs eventArgs) - { - Log.Debug("Received server rules."); - Rules = message; - _rules.UpdateRules(); - } -} diff --git a/Content.Client/Info/RulesAndInfoWindow.cs b/Content.Client/Info/RulesAndInfoWindow.cs index 7a763a1d6f4b..b9131dcb3c07 100644 --- a/Content.Client/Info/RulesAndInfoWindow.cs +++ b/Content.Client/Info/RulesAndInfoWindow.cs @@ -1,10 +1,8 @@ using System.Numerics; using Content.Client.UserInterface.Systems.EscapeMenu; -using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; -using Robust.Shared.Configuration; using Robust.Shared.ContentPack; namespace Content.Client.Info @@ -12,7 +10,6 @@ namespace Content.Client.Info public sealed class RulesAndInfoWindow : DefaultWindow { [Dependency] private readonly IResourceManager _resourceManager = default!; - [Dependency] private readonly RulesManager _rules = default!; public RulesAndInfoWindow() { @@ -22,8 +19,14 @@ public RulesAndInfoWindow() var rootContainer = new TabContainer(); - var rulesList = new Info(); - var tutorialList = new Info(); + var rulesList = new RulesControl + { + Margin = new Thickness(10) + }; + var tutorialList = new Info + { + Margin = new Thickness(10) + }; rootContainer.AddChild(rulesList); rootContainer.AddChild(tutorialList); @@ -31,7 +34,6 @@ public RulesAndInfoWindow() TabContainer.SetTabTitle(rulesList, Loc.GetString("ui-info-tab-rules")); TabContainer.SetTabTitle(tutorialList, Loc.GetString("ui-info-tab-tutorial")); - AddSection(rulesList, _rules.RulesSection()); PopulateTutorial(tutorialList); Contents.AddChild(rootContainer); diff --git a/Content.Client/Info/RulesControl.xaml b/Content.Client/Info/RulesControl.xaml index 3b2476468846..04fa7191234e 100644 --- a/Content.Client/Info/RulesControl.xaml +++ b/Content.Client/Info/RulesControl.xaml @@ -1,6 +1,18 @@ - + + + + + + + diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml.cs b/Content.Client/Lathe/UI/RecipeControl.xaml.cs index bf85ff7d938c..db428d3cf0e9 100644 --- a/Content.Client/Lathe/UI/RecipeControl.xaml.cs +++ b/Content.Client/Lathe/UI/RecipeControl.xaml.cs @@ -2,8 +2,9 @@ using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Graphics; +using Robust.Shared.Prototypes; namespace Content.Client.Lathe.UI; @@ -13,12 +14,13 @@ public sealed partial class RecipeControl : Control public Action? OnButtonPressed; public Func TooltipTextSupplier; - public RecipeControl(LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, Texture? texture = null) + public RecipeControl(LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, EntityPrototype? entityPrototype = null) { RobustXamlLoader.Load(this); RecipeName.Text = recipe.Name; - RecipeTexture.Texture = texture; + if (entityPrototype != null) + RecipePrototype.SetPrototype(entityPrototype); Button.Disabled = !canProduce; TooltipTextSupplier = tooltipTextSupplier; Button.TooltipSupplier = SupplyTooltip; diff --git a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs index a2a7fb2531c9..807740673010 100644 --- a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs +++ b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs @@ -52,7 +52,7 @@ protected override void OnAppearanceChange(EntityUid uid, ExpendableLightCompone case ExpendableLightState.Lit: _audioSystem.Stop(comp.PlayingStream); comp.PlayingStream = _audioSystem.PlayPvs( - comp.LoopedSound, uid, SharedExpendableLightComponent.LoopedSoundParams)?.Entity; + comp.LoopedSound, uid)?.Entity; if (args.Sprite.LayerMapTryGet(ExpendableLightVisualLayers.Overlay, out var layerIdx, true)) { diff --git a/Content.Client/Light/RgbLightControllerSystem.cs b/Content.Client/Light/RgbLightControllerSystem.cs index 7d55bcebf113..85b611483061 100644 --- a/Content.Client/Light/RgbLightControllerSystem.cs +++ b/Content.Client/Light/RgbLightControllerSystem.cs @@ -207,8 +207,11 @@ public override void FrameUpdate(float frameTime) public static Color GetCurrentRgbColor(TimeSpan curTime, TimeSpan offset, Entity rgb) { + var delta = (float)(curTime - offset).TotalSeconds; + var entOffset = Math.Abs(rgb.Owner.Id * 0.09817f); + var hue = (delta * rgb.Comp.CycleRate + entOffset) % 1; return Color.FromHsv(new Vector4( - (float) (((curTime.TotalSeconds - offset.TotalSeconds) * rgb.Comp.CycleRate + Math.Abs(rgb.Owner.Id * 0.1)) % 1), + MathF.Abs(hue), 1.0f, 1.0f, 1.0f diff --git a/Content.Client/Preferences/ClientPreferencesManager.cs b/Content.Client/Lobby/ClientPreferencesManager.cs similarity index 97% rename from Content.Client/Preferences/ClientPreferencesManager.cs rename to Content.Client/Lobby/ClientPreferencesManager.cs index 89cee7bf79b9..3f01e1a8f67a 100644 --- a/Content.Client/Preferences/ClientPreferencesManager.cs +++ b/Content.Client/Lobby/ClientPreferencesManager.cs @@ -2,12 +2,10 @@ using Content.Shared.Preferences; using Robust.Client; using Robust.Client.Player; -using Robust.Shared.Configuration; using Robust.Shared.Network; -using Robust.Shared.Prototypes; using Robust.Shared.Utility; -namespace Content.Client.Preferences +namespace Content.Client.Lobby { /// /// Receives and from the server during the initial diff --git a/Content.Client/Preferences/IClientPreferencesManager.cs b/Content.Client/Lobby/IClientPreferencesManager.cs similarity index 92% rename from Content.Client/Preferences/IClientPreferencesManager.cs rename to Content.Client/Lobby/IClientPreferencesManager.cs index e55d6b600cad..45a770b1621f 100644 --- a/Content.Client/Preferences/IClientPreferencesManager.cs +++ b/Content.Client/Lobby/IClientPreferencesManager.cs @@ -1,7 +1,6 @@ -using System; using Content.Shared.Preferences; -namespace Content.Client.Preferences +namespace Content.Client.Lobby { public interface IClientPreferencesManager { diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs index 98c109afde18..1aabc4ff381e 100644 --- a/Content.Client/Lobby/LobbyState.cs +++ b/Content.Client/Lobby/LobbyState.cs @@ -3,8 +3,6 @@ using Content.Client.LateJoin; using Content.Client.Lobby.UI; using Content.Client.Message; -using Content.Client.Preferences; -using Content.Client.Preferences.UI; using Content.Client.UserInterface.Systems.Chat; using Content.Client.Voting; using Robust.Client; @@ -12,8 +10,6 @@ using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; -using Robust.Shared.Configuration; -using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -25,20 +21,15 @@ public sealed class LobbyState : Robust.Client.State.State [Dependency] private readonly IClientConsoleHost _consoleHost = default!; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; - [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IVoteManager _voteManager = default!; - [Dependency] private readonly IConfigurationManager _configurationManager = default!; - - [ViewVariables] private CharacterSetupGui? _characterSetup; private ClientGameTicker _gameTicker = default!; private ContentAudioSystem _contentAudioSystem = default!; protected override Type? LinkedScreenType { get; } = typeof(LobbyGui); - private LobbyGui? _lobby; + public LobbyGui? Lobby; protected override void Startup() { @@ -47,39 +38,23 @@ protected override void Startup() return; } - _lobby = (LobbyGui) _userInterfaceManager.ActiveScreen; + Lobby = (LobbyGui) _userInterfaceManager.ActiveScreen; var chatController = _userInterfaceManager.GetUIController(); _gameTicker = _entityManager.System(); _contentAudioSystem = _entityManager.System(); _contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo; - _characterSetup = new CharacterSetupGui(_entityManager, _resourceCache, _preferencesManager, - _prototypeManager, _configurationManager); - LayoutContainer.SetAnchorPreset(_characterSetup, LayoutContainer.LayoutPreset.Wide); - _lobby.CharacterSetupState.AddChild(_characterSetup); chatController.SetMainChat(true); - _voteManager.SetPopupContainer(_lobby.VoteContainer); - - _characterSetup.CloseButton.OnPressed += _ => - { - _lobby.SwitchState(LobbyGui.LobbyGuiState.Default); - }; - - _characterSetup.SaveButton.OnPressed += _ => - { - _characterSetup.Save(); - _userInterfaceManager.GetUIController().UpdateCharacterUI(); - }; - - LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide); - _lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you... + _voteManager.SetPopupContainer(Lobby.VoteContainer); + LayoutContainer.SetAnchorPreset(Lobby, LayoutContainer.LayoutPreset.Wide); + Lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you... UpdateLobbyUi(); - _lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed; - _lobby.ReadyButton.OnPressed += OnReadyPressed; - _lobby.ReadyButton.OnToggled += OnReadyToggled; + Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed; + Lobby.ReadyButton.OnPressed += OnReadyPressed; + Lobby.ReadyButton.OnToggled += OnReadyToggled; _gameTicker.InfoBlobUpdated += UpdateLobbyUi; _gameTicker.LobbyStatusUpdated += LobbyStatusUpdated; @@ -97,20 +72,23 @@ protected override void Shutdown() _voteManager.ClearPopupContainer(); - _lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed; - _lobby!.ReadyButton.OnPressed -= OnReadyPressed; - _lobby!.ReadyButton.OnToggled -= OnReadyToggled; + Lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed; + Lobby!.ReadyButton.OnPressed -= OnReadyPressed; + Lobby!.ReadyButton.OnToggled -= OnReadyToggled; - _lobby = null; + Lobby = null; + } - _characterSetup?.Dispose(); - _characterSetup = null; + public void SwitchState(LobbyGui.LobbyGuiState state) + { + // Yeah I hate this but LobbyState contains all the badness for now. + Lobby?.SwitchState(state); } private void OnSetupPressed(BaseButton.ButtonEventArgs args) { SetReady(false); - _lobby!.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup); + Lobby?.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup); } private void OnReadyPressed(BaseButton.ButtonEventArgs args) @@ -132,13 +110,13 @@ public override void FrameUpdate(FrameEventArgs e) { if (_gameTicker.IsGameStarted) { - _lobby!.StartTime.Text = string.Empty; + Lobby!.StartTime.Text = string.Empty; var roundTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan); - _lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-time", ("hours", roundTime.Hours), ("minutes", roundTime.Minutes)); + Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-time", ("hours", roundTime.Hours), ("minutes", roundTime.Minutes)); return; } - _lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started"); + Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started"); string text; if (_gameTicker.Paused) @@ -147,7 +125,7 @@ public override void FrameUpdate(FrameEventArgs e) } else if (_gameTicker.StartTime < _gameTiming.CurTime) { - _lobby!.StartTime.Text = Loc.GetString("lobby-state-soon"); + Lobby!.StartTime.Text = Loc.GetString("lobby-state-soon"); return; } else @@ -164,7 +142,7 @@ public override void FrameUpdate(FrameEventArgs e) } } - _lobby!.StartTime.Text = Loc.GetString("lobby-state-round-start-countdown-text", ("timeLeft", text)); + Lobby!.StartTime.Text = Loc.GetString("lobby-state-round-start-countdown-text", ("timeLeft", text)); } private void LobbyStatusUpdated() @@ -175,31 +153,31 @@ private void LobbyStatusUpdated() private void LobbyLateJoinStatusUpdated() { - _lobby!.ReadyButton.Disabled = _gameTicker.DisallowedLateJoin; + Lobby!.ReadyButton.Disabled = _gameTicker.DisallowedLateJoin; } private void UpdateLobbyUi() { if (_gameTicker.IsGameStarted) { - _lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state"); - _lobby!.ReadyButton.ToggleMode = false; - _lobby!.ReadyButton.Pressed = false; - _lobby!.ObserveButton.Disabled = false; + Lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state"); + Lobby!.ReadyButton.ToggleMode = false; + Lobby!.ReadyButton.Pressed = false; + Lobby!.ObserveButton.Disabled = false; } else { - _lobby!.StartTime.Text = string.Empty; - _lobby!.ReadyButton.Text = Loc.GetString(_lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready"); - _lobby!.ReadyButton.ToggleMode = true; - _lobby!.ReadyButton.Disabled = false; - _lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady; - _lobby!.ObserveButton.Disabled = true; + Lobby!.StartTime.Text = string.Empty; + Lobby!.ReadyButton.Text = Loc.GetString(Lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready"); + Lobby!.ReadyButton.ToggleMode = true; + Lobby!.ReadyButton.Disabled = false; + Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady; + Lobby!.ObserveButton.Disabled = true; } if (_gameTicker.ServerInfoBlob != null) { - _lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob); + Lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob); } } @@ -207,7 +185,7 @@ private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev) { if (ev.SoundtrackFilename == null) { - _lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text")); + Lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text")); } else if ( ev.SoundtrackFilename != null @@ -228,7 +206,7 @@ private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev) ("songTitle", title), ("songArtist", artist)); - _lobby!.LobbySong.SetMarkup(markup); + Lobby!.LobbySong.SetMarkup(markup); } } @@ -236,11 +214,11 @@ private void UpdateLobbyBackground() { if (_gameTicker.LobbyBackground != null) { - _lobby!.Background.Texture = _resourceCache.GetResource(_gameTicker.LobbyBackground ); + Lobby!.Background.Texture = _resourceCache.GetResource(_gameTicker.LobbyBackground ); } else { - _lobby!.Background.Texture = null; + Lobby!.Background.Texture = null; } } diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs index 19f43e05752b..e4a13ed8c6b7 100644 --- a/Content.Client/Lobby/LobbyUIController.cs +++ b/Content.Client/Lobby/LobbyUIController.cs @@ -1,128 +1,300 @@ using System.Linq; +using Content.Client.Guidebook; using Content.Client.Humanoid; using Content.Client.Inventory; using Content.Client.Lobby.UI; -using Content.Client.Preferences; +using Content.Client.Players.PlayTimeTracking; using Content.Client.Station; +using Content.Shared.CCVar; using Content.Shared.Clothing; using Content.Shared.GameTicking; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; -using Content.Shared.Preferences.Loadouts.Effects; using Content.Shared.Roles; +using Content.Shared.Traits; +using Robust.Client.Player; +using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; +using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Client.Lobby; public sealed class LobbyUIController : UIController, IOnStateEntered, IOnStateExited { [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!; - [Dependency] private readonly IStateManager _stateManager = default!; + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly IFileDialogManager _dialogManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly IStateManager _stateManager = default!; + [Dependency] private readonly JobRequirementsManager _requirements = default!; + [Dependency] private readonly MarkingManager _markings = default!; [UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!; [UISystemDependency] private readonly ClientInventorySystem _inventory = default!; [UISystemDependency] private readonly StationSpawningSystem _spawn = default!; + [UISystemDependency] private readonly GuidebookSystem _guide = default!; - private LobbyCharacterPreviewPanel? _previewPanel; - - /* - * Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor - * that is shared too. - */ + private CharacterSetupGui? _characterSetup; + private HumanoidProfileEditor? _profileEditor; /// - /// Preview dummy for role gear. + /// This is the characher preview panel in the chat. This should only update if their character updates. /// - private EntityUid? _previewDummy; + private LobbyCharacterPreviewPanel? PreviewPanel => GetLobbyPreview(); /// - /// If we currently have a loadout selected. + /// This is the modified profile currently being edited. /// - private JobPrototype? _dummyJob; + private HumanoidCharacterProfile? EditedProfile => _profileEditor?.Profile; - // TODO: Load the species directly and don't update entity ever. - public event Action? PreviewDummyUpdated; + private int? EditedSlot => _profileEditor?.CharacterSlot; public override void Initialize() { base.Initialize(); + _prototypeManager.PrototypesReloaded += OnProtoReload; _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded; + _requirements.Updated += OnRequirementsUpdated; + + _configurationManager.OnValueChanged(CCVars.FlavorText, args => + { + _profileEditor?.RefreshFlavorText(); + }); + + _configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor()); + + _configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor()); + } + + private LobbyCharacterPreviewPanel? GetLobbyPreview() + { + if (_stateManager.CurrentState is LobbyState lobby) + { + return lobby.Lobby?.CharacterPreview; + } + + return null; + } + + private void OnRequirementsUpdated() + { + if (_profileEditor != null) + { + _profileEditor.RefreshAntags(); + _profileEditor.RefreshJobs(); + } + } + + private void OnProtoReload(PrototypesReloadedEventArgs obj) + { + if (_profileEditor != null) + { + if (obj.WasModified()) + { + _profileEditor.RefreshAntags(); + } + + if (obj.WasModified() || + obj.WasModified()) + { + _profileEditor.RefreshJobs(); + } + + if (obj.WasModified() || + obj.WasModified() || + obj.WasModified()) + { + _profileEditor.RefreshLoadouts(); + } + + if (obj.WasModified()) + { + _profileEditor.RefreshSpecies(); + } + + if (obj.WasModified()) + { + _profileEditor.RefreshTraits(); + } + } } private void PreferencesDataLoaded() { - UpdateCharacterUI(); + PreviewPanel?.SetLoaded(true); + + if (_stateManager.CurrentState is not LobbyState) + return; + + ReloadCharacterSetup(); } public void OnStateEntered(LobbyState state) { + PreviewPanel?.SetLoaded(_preferencesManager.ServerDataLoaded); + ReloadCharacterSetup(); } public void OnStateExited(LobbyState state) { - EntityManager.DeleteEntity(_previewDummy); - _previewDummy = null; - } + PreviewPanel?.SetLoaded(false); + _profileEditor?.Dispose(); + _characterSetup?.Dispose(); - public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel) - { - _previewPanel = panel; - UpdateCharacterUI(); + _characterSetup = null; + _profileEditor = null; } - public void SetDummyJob(JobPrototype? job) + /// + /// Reloads every single character setup control. + /// + public void ReloadCharacterSetup() { - _dummyJob = job; - UpdateCharacterUI(); + RefreshLobbyPreview(); + var (characterGui, profileEditor) = EnsureGui(); + characterGui.ReloadCharacterPickers(); + profileEditor.SetProfile( + (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, + _preferencesManager.Preferences?.SelectedCharacterIndex); } - public void UpdateCharacterUI() + /// + /// Refreshes the character preview in the lobby chat. + /// + private void RefreshLobbyPreview() { - // Test moment - if (_stateManager.CurrentState is not LobbyState) + if (PreviewPanel == null) return; - if (!_preferencesManager.ServerDataLoaded) + // Get selected character, load it, then set it + var character = _preferencesManager.Preferences?.SelectedCharacter; + + if (character is not HumanoidCharacterProfile humanoid) { - _previewPanel?.SetLoaded(false); + PreviewPanel.SetSprite(EntityUid.Invalid); + PreviewPanel.SetSummaryText(string.Empty); return; } - _previewPanel?.SetLoaded(true); + var dummy = LoadProfileEntity(humanoid, null, true); + PreviewPanel.SetSprite(dummy); + PreviewPanel.SetSummaryText(humanoid.Summary); + } - if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter) + private void RefreshProfileEditor() + { + _profileEditor?.RefreshAntags(); + _profileEditor?.RefreshJobs(); + _profileEditor?.RefreshLoadouts(); + } + + private void SaveProfile() + { + DebugTools.Assert(EditedProfile != null); + + if (EditedProfile == null || EditedSlot == null) + return; + + var selected = _preferencesManager.Preferences?.SelectedCharacterIndex; + + if (selected == null) + return; + + _preferencesManager.UpdateCharacter(EditedProfile, EditedSlot.Value); + ReloadCharacterSetup(); + } + + private (CharacterSetupGui, HumanoidProfileEditor) EnsureGui() + { + if (_characterSetup != null && _profileEditor != null) { - _previewPanel?.SetSummaryText(string.Empty); + _characterSetup.Visible = true; + _profileEditor.Visible = true; + return (_characterSetup, _profileEditor); } - else + + _profileEditor = new HumanoidProfileEditor( + _preferencesManager, + _configurationManager, + EntityManager, + _dialogManager, + _logManager, + _playerManager, + _prototypeManager, + _requirements, + _markings); + + _profileEditor.OnOpenGuidebook += _guide.OpenHelp; + + _characterSetup = new CharacterSetupGui(EntityManager, _prototypeManager, _resourceCache, _preferencesManager, _profileEditor); + + _characterSetup.CloseButton.OnPressed += _ => + { + // Reset sliders etc. + _profileEditor.SetProfile(null, null); + _profileEditor.Visible = false; + + if (_stateManager.CurrentState is LobbyState lobbyGui) + { + lobbyGui.SwitchState(LobbyGui.LobbyGuiState.Default); + } + }; + + _profileEditor.Save += SaveProfile; + + _characterSetup.SelectCharacter += args => + { + _preferencesManager.SelectCharacter(args); + ReloadCharacterSetup(); + }; + + _characterSetup.DeleteCharacter += args => { - EntityManager.DeleteEntity(_previewDummy); - _previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace); - _previewPanel?.SetSprite(_previewDummy.Value); - _previewPanel?.SetSummaryText(selectedCharacter.Summary); - _humanoid.LoadProfile(_previewDummy.Value, selectedCharacter); + _preferencesManager.DeleteCharacter(args); - GiveDummyJobClothesLoadout(_previewDummy.Value, selectedCharacter); - PreviewDummyUpdated?.Invoke(_previewDummy.Value); + // Reload everything + if (EditedSlot == args) + { + ReloadCharacterSetup(); + } + else + { + // Only need to reload character pickers + _characterSetup?.ReloadCharacterPickers(); + } + }; + + if (_stateManager.CurrentState is LobbyState lobby) + { + lobby.Lobby?.CharacterSetupState.AddChild(_characterSetup); } + + return (_characterSetup, _profileEditor); } + #region Helpers + /// /// Applies the highest priority job's clothes to the dummy. /// - public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile) + public void GiveDummyJobClothesLoadout(EntityUid dummy, JobPrototype? jobProto, HumanoidCharacterProfile profile) { - var job = _dummyJob ?? GetPreferredJob(profile); + var job = jobProto ?? GetPreferredJob(profile); GiveDummyJobClothes(dummy, profile, job); if (_prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID))) { - var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), EntityManager, _prototypeManager); + var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), _playerManager.LocalSession, profile.Species, EntityManager, _prototypeManager); GiveDummyLoadout(dummy, loadout); } } @@ -134,7 +306,7 @@ public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile) { var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key; // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?) - return _prototypeManager.Index(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob); + return _prototypeManager.Index(highPriorityJob.Id ?? SharedGameTicker.FallbackOverflowJob); } public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout) @@ -216,8 +388,39 @@ public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profil } } - public EntityUid? GetPreviewDummy() + /// + /// Loads the profile onto a dummy entity. + /// + public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, JobPrototype? job, bool jobClothes) { - return _previewDummy; + EntityUid dummyEnt; + + if (humanoid is not null) + { + var dummy = _prototypeManager.Index(humanoid.Species).DollPrototype; + dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace); + } + else + { + dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace); + } + + _humanoid.LoadProfile(dummyEnt, humanoid); + + if (humanoid != null && jobClothes) + { + job ??= GetPreferredJob(humanoid); + GiveDummyJobClothes(dummyEnt, humanoid, job); + + if (_prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID))) + { + var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), _playerManager.LocalSession, humanoid.Species, EntityManager, _prototypeManager); + GiveDummyLoadout(dummyEnt, loadout); + } + } + + return dummyEnt; } + + #endregion } diff --git a/Content.Client/Lobby/UI/CharacterPickerButton.xaml b/Content.Client/Lobby/UI/CharacterPickerButton.xaml new file mode 100644 index 000000000000..af1e640aadb6 --- /dev/null +++ b/Content.Client/Lobby/UI/CharacterPickerButton.xaml @@ -0,0 +1,22 @@ + + + + + [GenerateTypedNameReferences] + public sealed partial class CharacterSetupGui : Control + { + private readonly IClientPreferencesManager _preferencesManager; + private readonly IEntityManager _entManager; + private readonly IPrototypeManager _protomanager; + + private readonly Button _createNewCharacterButton; + + public event Action? SelectCharacter; + public event Action? DeleteCharacter; + + public CharacterSetupGui( + IEntityManager entManager, + IPrototypeManager protoManager, + IResourceCache resourceCache, + IClientPreferencesManager preferencesManager, + HumanoidProfileEditor profileEditor) + { + RobustXamlLoader.Load(this); + _preferencesManager = preferencesManager; + _entManager = entManager; + _protomanager = protoManager; + + var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); + var back = new StyleBoxTexture + { + Texture = panelTex, + Modulate = new Color(37, 37, 42) + }; + back.SetPatchMargin(StyleBox.Margin.All, 10); + + BackgroundPanel.PanelOverride = back; + + _createNewCharacterButton = new Button + { + Text = Loc.GetString("character-setup-gui-create-new-character-button"), + }; + + _createNewCharacterButton.OnPressed += args => + { + preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random()); + ReloadCharacterPickers(); + args.Event.Handle(); + }; + + CharEditor.AddChild(profileEditor); + RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open(); + + StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered(); + } + + /// + /// Disposes and reloads all character picker buttons from the preferences data. + /// + public void ReloadCharacterPickers() + { + _createNewCharacterButton.Orphan(); + Characters.DisposeAllChildren(); + + var numberOfFullSlots = 0; + var characterButtonsGroup = new ButtonGroup(); + + if (!_preferencesManager.ServerDataLoaded) + { + return; + } + + _createNewCharacterButton.ToolTip = + Loc.GetString("character-setup-gui-create-new-character-button-tooltip", + ("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots)); + + var selectedSlot = _preferencesManager.Preferences?.SelectedCharacterIndex; + + foreach (var (slot, character) in _preferencesManager.Preferences!.Characters) + { + numberOfFullSlots++; + var characterPickerButton = new CharacterPickerButton(_entManager, + _protomanager, + characterButtonsGroup, + character, + slot == selectedSlot); + + Characters.AddChild(characterPickerButton); + + characterPickerButton.OnPressed += args => + { + SelectCharacter?.Invoke(slot); + }; + + characterPickerButton.OnDeletePressed += () => + { + DeleteCharacter?.Invoke(slot); + }; + } + + _createNewCharacterButton.Disabled = numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots; + Characters.AddChild(_createNewCharacterButton); + } + } +} diff --git a/Content.Client/Preferences/UI/HighlightedContainer.xaml b/Content.Client/Lobby/UI/HighlightedContainer.xaml similarity index 100% rename from Content.Client/Preferences/UI/HighlightedContainer.xaml rename to Content.Client/Lobby/UI/HighlightedContainer.xaml diff --git a/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs b/Content.Client/Lobby/UI/HighlightedContainer.xaml.cs similarity index 88% rename from Content.Client/Preferences/UI/HighlightedContainer.xaml.cs rename to Content.Client/Lobby/UI/HighlightedContainer.xaml.cs index 68294d0f0596..084c1c370982 100644 --- a/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs +++ b/Content.Client/Lobby/UI/HighlightedContainer.xaml.cs @@ -2,7 +2,7 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; -namespace Content.Client.Preferences.UI; +namespace Content.Client.Lobby.UI; [GenerateTypedNameReferences] public sealed partial class HighlightedContainer : PanelContainer diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml new file mode 100644 index 000000000000..03a205e94a80 --- /dev/null +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + [DataField("screenColors")] [ViewVariables(VVAccess.ReadWrite)] - public Color[] ScreenColors = new Color[(byte)ApcChargeState.NumStates]{Color.FromHex("#d1332e"), Color.FromHex("#2e8ad1"), Color.FromHex("#3db83b"), Color.FromHex("#ffac1c")}; + public Color[] ScreenColors = new Color[(byte)ApcChargeState.NumStates]{Color.FromHex("#d1332e"), Color.FromHex("#dcdc28"), Color.FromHex("#82ff4c"), Color.FromHex("#ffac1c")}; /// /// The sprite state of the unlit overlay used for the APC screen when the APC has been emagged. diff --git a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs new file mode 100644 index 000000000000..5a082485a5ac --- /dev/null +++ b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs @@ -0,0 +1,27 @@ +using Content.Client.Power.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; +using Content.Shared.UserInterface; +using Content.Shared.Wires; + +namespace Content.Client.Power; + +public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequiresPowerSystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + + protected override void OnActivate(Entity ent, ref ActivatableUIOpenAttemptEvent args) + { + if (args.Cancelled || this.IsPowered(ent.Owner, EntityManager)) + { + return; + } + + if (TryComp(ent.Owner, out var panel) && panel.Open) + return; + + _popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User); + args.Cancel(); + } +} diff --git a/Content.Client/Power/Components/ApcPowerReceiverComponent.cs b/Content.Client/Power/Components/ApcPowerReceiverComponent.cs new file mode 100644 index 000000000000..fbebcb7cf83a --- /dev/null +++ b/Content.Client/Power/Components/ApcPowerReceiverComponent.cs @@ -0,0 +1,8 @@ +using Content.Shared.Power.Components; + +namespace Content.Client.Power.Components; + +[RegisterComponent] +public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent +{ +} diff --git a/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs b/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs new file mode 100644 index 000000000000..4d56592c2322 --- /dev/null +++ b/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs @@ -0,0 +1,23 @@ +using Content.Client.Power.Components; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Client.Power.EntitySystems; + +public sealed class PowerReceiverSystem : SharedPowerReceiverSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentHandleState args) + { + if (args.Current is not ApcPowerReceiverComponentState state) + return; + + component.Powered = state.Powered; + } +} diff --git a/Content.Client/Power/EntitySystems/StaticPowerSystem.cs b/Content.Client/Power/EntitySystems/StaticPowerSystem.cs new file mode 100644 index 000000000000..2ca945cbbd8c --- /dev/null +++ b/Content.Client/Power/EntitySystems/StaticPowerSystem.cs @@ -0,0 +1,16 @@ +using Content.Client.Power.Components; + +namespace Content.Client.Power.EntitySystems; + +public static class StaticPowerSystem +{ + // Using this makes the call shorter. + // ReSharper disable once UnusedParameter.Global + public static bool IsPowered(this EntitySystem system, EntityUid uid, IEntityManager entManager, ApcPowerReceiverComponent? receiver = null) + { + if (receiver == null && !entManager.TryGetComponent(uid, out receiver)) + return false; + + return receiver.Powered; + } +} diff --git a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs index 902d6bb7e609..d5057416cf84 100644 --- a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs +++ b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs @@ -5,6 +5,7 @@ using Robust.Shared.Collections; using Robust.Shared.Map.Components; using System.Numerics; +using static Content.Shared.Power.SharedPowerMonitoringConsoleSystem; namespace Content.Client.Power; @@ -23,8 +24,13 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks; public List HiddenLineGroups = new(); - public Dictionary>? PowerCableNetwork; - public Dictionary>? FocusCableNetwork; + public List PowerCableNetwork = new(); + public List FocusCableNetwork = new(); + + private Dictionary[] _horizLines = [new(), new(), new()]; + private Dictionary[] _horizLinesReversed = [new(), new(), new()]; + private Dictionary[] _vertLines = [new(), new(), new()]; + private Dictionary[] _vertLinesReversed = [new(), new(), new()]; private MapGridComponent? _grid; @@ -48,15 +54,15 @@ protected override void UpdateNavMap() if (!_entManager.TryGetComponent(Owner, out var cableNetworks)) return; - if (!_entManager.TryGetComponent(MapUid, out _grid)) - return; - - PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks, _grid); - FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks, _grid); + PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks); + FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks); } public void DrawAllCableNetworks(DrawingHandleScreen handle) { + if (!_entManager.TryGetComponent(MapUid, out _grid)) + return; + // Draw full cable network if (PowerCableNetwork != null && PowerCableNetwork.Count > 0) { @@ -69,36 +75,29 @@ public void DrawAllCableNetworks(DrawingHandleScreen handle) DrawCableNetwork(handle, FocusCableNetwork, Color.White); } - public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary> fullCableNetwork, Color modulator) + public void DrawCableNetwork(DrawingHandleScreen handle, List fullCableNetwork, Color modulator) { + if (!_entManager.TryGetComponent(MapUid, out _grid)) + return; + var offset = GetOffset(); - var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset); + offset = offset with { Y = -offset.Y }; if (WorldRange / WorldMaxRange > 0.5f) { var cableNetworks = new ValueList[3]; - foreach ((var chunk, var chunkedLines) in fullCableNetwork) + foreach (var line in fullCableNetwork) { - var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize; - - if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right) + if (HiddenLineGroups.Contains(line.Group)) continue; - if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top) - continue; - - foreach (var chunkedLine in chunkedLines) - { - if (HiddenLineGroups.Contains(chunkedLine.Group)) - continue; - - var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y)); - var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y)); + var cableOffset = _powerCableOffsets[(int) line.Group]; + var start = ScalePosition(line.Origin + cableOffset - offset); + var end = ScalePosition(line.Terminus + cableOffset - offset); - cableNetworks[(int) chunkedLine.Group].Add(start); - cableNetworks[(int) chunkedLine.Group].Add(end); - } + cableNetworks[(int) line.Group].Add(start); + cableNetworks[(int) line.Group].Add(end); } for (int cableNetworkIdx = 0; cableNetworkIdx < cableNetworks.Length; cableNetworkIdx++) @@ -124,48 +123,39 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary[3]; - foreach ((var chunk, var chunkedLines) in fullCableNetwork) + foreach (var line in fullCableNetwork) { - var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize; - - if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right) + if (HiddenLineGroups.Contains(line.Group)) continue; - if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top) - continue; - - foreach (var chunkedLine in chunkedLines) - { - if (HiddenLineGroups.Contains(chunkedLine.Group)) - continue; - - var leftTop = ScalePosition(new Vector2 - (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f, - Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f) - - new Vector2(offset.X, -offset.Y)); - - var rightTop = ScalePosition(new Vector2 - (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f, - Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f) - - new Vector2(offset.X, -offset.Y)); - - var leftBottom = ScalePosition(new Vector2 - (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f, - Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f) - - new Vector2(offset.X, -offset.Y)); - - var rightBottom = ScalePosition(new Vector2 - (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f, - Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f) - - new Vector2(offset.X, -offset.Y)); - - cableVertexUVs[(int) chunkedLine.Group].Add(leftBottom); - cableVertexUVs[(int) chunkedLine.Group].Add(leftTop); - cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom); - cableVertexUVs[(int) chunkedLine.Group].Add(leftTop); - cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom); - cableVertexUVs[(int) chunkedLine.Group].Add(rightTop); - } + var cableOffset = _powerCableOffsets[(int) line.Group]; + + var leftTop = ScalePosition(new Vector2 + (Math.Min(line.Origin.X, line.Terminus.X) - 0.1f, + Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f) + + cableOffset - offset); + + var rightTop = ScalePosition(new Vector2 + (Math.Max(line.Origin.X, line.Terminus.X) + 0.1f, + Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f) + + cableOffset - offset); + + var leftBottom = ScalePosition(new Vector2 + (Math.Min(line.Origin.X, line.Terminus.X) - 0.1f, + Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f) + + cableOffset - offset); + + var rightBottom = ScalePosition(new Vector2 + (Math.Max(line.Origin.X, line.Terminus.X) + 0.1f, + Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f) + + cableOffset - offset); + + cableVertexUVs[(int) line.Group].Add(leftBottom); + cableVertexUVs[(int) line.Group].Add(leftTop); + cableVertexUVs[(int) line.Group].Add(rightBottom); + cableVertexUVs[(int) line.Group].Add(leftTop); + cableVertexUVs[(int) line.Group].Add(rightBottom); + cableVertexUVs[(int) line.Group].Add(rightTop); } for (int cableNetworkIdx = 0; cableNetworkIdx < cableVertexUVs.Length; cableNetworkIdx++) @@ -188,34 +178,43 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary>? GetDecodedPowerCableChunks(Dictionary? chunks, MapGridComponent? grid) + public List GetDecodedPowerCableChunks(Dictionary? chunks) { - if (chunks == null || grid == null) - return null; + var decodedOutput = new List(); - var decodedOutput = new Dictionary>(); + if (!_entManager.TryGetComponent(MapUid, out _grid)) + return decodedOutput; - foreach ((var chunkOrigin, var chunk) in chunks) - { - var list = new List(); + if (chunks == null) + return decodedOutput; + + Array.ForEach(_horizLines, x=> x.Clear()); + Array.ForEach(_horizLinesReversed, x=> x.Clear()); + Array.ForEach(_vertLines, x=> x.Clear()); + Array.ForEach(_vertLinesReversed, x=> x.Clear()); - for (int cableIdx = 0; cableIdx < chunk.PowerCableData.Length; cableIdx++) + foreach (var (chunkOrigin, chunk) in chunks) + { + for (var cableIdx = 0; cableIdx < 3; cableIdx++) { - var chunkMask = chunk.PowerCableData[cableIdx]; + var horizLines = _horizLines[cableIdx]; + var horizLinesReversed = _horizLinesReversed[cableIdx]; + var vertLines = _vertLines[cableIdx]; + var vertLinesReversed = _vertLinesReversed[cableIdx]; - Vector2 offset = _powerCableOffsets[cableIdx]; + var chunkMask = chunk.PowerCableData[cableIdx]; - for (var chunkIdx = 0; chunkIdx < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; chunkIdx++) + for (var chunkIdx = 0; chunkIdx < ChunkSize * ChunkSize; chunkIdx++) { - var value = (int) Math.Pow(2, chunkIdx); + var value = 1 << chunkIdx; var mask = chunkMask & value; if (mask == 0x0) continue; - var relativeTile = SharedNavMapSystem.GetTile(mask); - var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize; - var position = new Vector2(tile.X, -tile.Y); + var relativeTile = GetTileFromIndex(chunkIdx); + var tile = (chunk.Origin * ChunkSize + relativeTile) * _grid.TileSize; + tile = tile with { Y = -tile.Y }; PowerCableChunk neighborChunk; bool neighbor; @@ -223,56 +222,65 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary 0) - decodedOutput.Add(chunkOrigin, list); + for (var index = 0; index < _vertLines.Length; index++) + { + var vertLines = _vertLines[index]; + foreach (var (origin, terminal) in vertLines) + { + decodedOutput.Add(new PowerMonitoringConsoleLine(origin + gridOffset, terminal + gridOffset, + (PowerMonitoringConsoleLineGroup) index)); + } } return decodedOutput; diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs index 25a586a75de8..74752ddc5343 100644 --- a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs @@ -102,7 +102,8 @@ public void UpdateWindowEntryButton(NetEntity netEntity, PowerMonitoringButton b button.ToolTip = Loc.GetString(name); // Update power value - button.PowerValue.Text = Loc.GetString("power-monitoring-window-value", ("value", entry.PowerValue)); + // Don't use SI prefixes, just give the number in W, so that it is readily apparent which consumer is using a lot of power. + button.PowerValue.Text = Loc.GetString("power-monitoring-window-button-value", ("value", Math.Round(entry.PowerValue).ToString("N0"))); } private void UpdateEntrySourcesOrLoads(BoxContainer masterContainer, BoxContainer currentContainer, PowerMonitoringConsoleEntry[]? entries, SpriteSpecifier.Texture icon) @@ -480,7 +481,8 @@ public PowerMonitoringButton() PowerValue = new Label() { HorizontalAlignment = HAlignment.Right, - SetWidth = 72f, + Align = Label.AlignMode.Right, + SetWidth = 80f, Margin = new Thickness(10, 0, 0, 0), ClipText = true, }; diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.cs index edc0eaa18a85..81fe1f4d0478 100644 --- a/Content.Client/Power/PowerMonitoringWindow.xaml.cs +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.cs @@ -170,9 +170,6 @@ public void ShowEntites NavMap.TrackedEntities[mon.Value] = blip; } - // Update nav map - NavMap.ForceNavMapUpdate(); - // If the entry group doesn't match the current tab, the data is out dated, do not use it if (allEntries.Length > 0 && allEntries[0].Group != GetCurrentPowerMonitoringConsoleGroup()) return; diff --git a/Content.Client/Preferences/UI/AntagPreferenceSelector.cs b/Content.Client/Preferences/UI/AntagPreferenceSelector.cs deleted file mode 100644 index 654c393b267a..000000000000 --- a/Content.Client/Preferences/UI/AntagPreferenceSelector.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Content.Client.Players.PlayTimeTracking; -using Content.Shared.Roles; -using Robust.Client.UserInterface.Controls; - -namespace Content.Client.Preferences.UI; - -public sealed class AntagPreferenceSelector : RequirementsSelector -{ - // 0 is yes and 1 is no - public bool Preference - { - get => Options.SelectedValue == 0; - set => Options.Select((value && !Disabled) ? 0 : 1); - } - - public event Action? PreferenceChanged; - - public AntagPreferenceSelector(AntagPrototype proto, ButtonGroup btnGroup) - : base(proto, btnGroup) - { - Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference); - - var items = new[] - { - ("humanoid-profile-editor-antag-preference-yes-button", 0), - ("humanoid-profile-editor-antag-preference-no-button", 1) - }; - var title = Loc.GetString(proto.Name); - var description = Loc.GetString(proto.Objective); - // Not supported yet get fucked. - Setup(null, items, title, 250, description); - - // immediately lock requirements if they arent met. - // another function checks Disabled after creating the selector so this has to be done now - var requirements = IoCManager.Resolve(); - if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason)) - { - LockRequirements(reason); - } - } -} diff --git a/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs b/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs deleted file mode 100644 index 0dcd5e6ad30d..000000000000 --- a/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Client.Humanoid; -using Content.Client.Info; -using Content.Client.Info.PlaytimeStats; -using Content.Client.Lobby; -using Content.Client.Resources; -using Content.Client.Stylesheets; -using Content.Shared.Clothing; -using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Prototypes; -using Content.Shared.Preferences; -using Content.Shared.Preferences.Loadouts; -using Content.Shared.Roles; -using Robust.Client.AutoGenerated; -using Robust.Client.Graphics; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Configuration; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using static Robust.Client.UserInterface.Controls.BoxContainer; -using Direction = Robust.Shared.Maths.Direction; - -namespace Content.Client.Preferences.UI -{ - [GenerateTypedNameReferences] - public sealed partial class CharacterSetupGui : Control - { - private readonly IClientPreferencesManager _preferencesManager; - private readonly IEntityManager _entityManager; - private readonly IPrototypeManager _prototypeManager; - private readonly Button _createNewCharacterButton; - private readonly HumanoidProfileEditor _humanoidProfileEditor; - - public CharacterSetupGui( - IEntityManager entityManager, - IResourceCache resourceCache, - IClientPreferencesManager preferencesManager, - IPrototypeManager prototypeManager, - IConfigurationManager configurationManager) - { - RobustXamlLoader.Load(this); - _entityManager = entityManager; - _prototypeManager = prototypeManager; - _preferencesManager = preferencesManager; - - var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); - var back = new StyleBoxTexture - { - Texture = panelTex, - Modulate = new Color(37, 37, 42) - }; - back.SetPatchMargin(StyleBox.Margin.All, 10); - - BackgroundPanel.PanelOverride = back; - - _createNewCharacterButton = new Button - { - Text = Loc.GetString("character-setup-gui-create-new-character-button"), - }; - _createNewCharacterButton.OnPressed += args => - { - preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random()); - UpdateUI(); - args.Event.Handle(); - }; - - _humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager); - _humanoidProfileEditor.OnProfileChanged += ProfileChanged; - CharEditor.AddChild(_humanoidProfileEditor); - - UpdateUI(); - - RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open(); - - StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered(); - preferencesManager.OnServerDataLoaded += UpdateUI; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _preferencesManager.OnServerDataLoaded -= UpdateUI; - } - - public void Save() => _humanoidProfileEditor.Save(); - - private void ProfileChanged(ICharacterProfile profile, int profileSlot) - { - _humanoidProfileEditor.UpdateControls(); - UpdateUI(); - } - - private void UpdateUI() - { - UserInterfaceManager.GetUIController().UpdateCharacterUI(); - var numberOfFullSlots = 0; - var characterButtonsGroup = new ButtonGroup(); - Characters.RemoveAllChildren(); - - if (!_preferencesManager.ServerDataLoaded) - { - return; - } - - _createNewCharacterButton.ToolTip = - Loc.GetString("character-setup-gui-create-new-character-button-tooltip", - ("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots)); - - foreach (var (slot, character) in _preferencesManager.Preferences!.Characters) - { - numberOfFullSlots++; - var characterPickerButton = new CharacterPickerButton(_entityManager, - _preferencesManager, - _prototypeManager, - characterButtonsGroup, - character); - Characters.AddChild(characterPickerButton); - - var characterIndexCopy = slot; - characterPickerButton.OnPressed += args => - { - _humanoidProfileEditor.Profile = (HumanoidCharacterProfile)character; - _humanoidProfileEditor.CharacterSlot = characterIndexCopy; - _humanoidProfileEditor.UpdateControls(); - _preferencesManager.SelectCharacter(character); - UpdateUI(); - args.Event.Handle(); - }; - } - - _createNewCharacterButton.Disabled = - numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots; - Characters.AddChild(_createNewCharacterButton); - // TODO: Move this shit to the Lobby UI controller - } - - /// - /// Shows individual characters on the side of the character GUI. - /// - private sealed class CharacterPickerButton : ContainerButton - { - private EntityUid _previewDummy; - - public CharacterPickerButton( - IEntityManager entityManager, - IClientPreferencesManager preferencesManager, - IPrototypeManager prototypeManager, - ButtonGroup group, - ICharacterProfile profile) - { - AddStyleClass(StyleClassButton); - ToggleMode = true; - Group = group; - - var humanoid = profile as HumanoidCharacterProfile; - if (humanoid is not null) - { - var dummy = prototypeManager.Index(humanoid.Species).DollPrototype; - _previewDummy = entityManager.SpawnEntity(dummy, MapCoordinates.Nullspace); - } - else - { - _previewDummy = entityManager.SpawnEntity(prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace); - } - - EntitySystem.Get().LoadProfile(_previewDummy, (HumanoidCharacterProfile)profile); - - if (humanoid != null) - { - var controller = UserInterfaceManager.GetUIController(); - var job = controller.GetPreferredJob(humanoid); - controller.GiveDummyJobClothes(_previewDummy, humanoid, job); - - if (prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID))) - { - var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), entityManager, prototypeManager); - controller.GiveDummyLoadout(_previewDummy, loadout); - } - } - - var isSelectedCharacter = profile == preferencesManager.Preferences?.SelectedCharacter; - - if (isSelectedCharacter) - Pressed = true; - - var view = new SpriteView - { - Scale = new Vector2(2, 2), - OverrideDirection = Direction.South - }; - view.SetEntity(_previewDummy); - - var description = profile.Name; - - var highPriorityJob = humanoid?.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key; - if (highPriorityJob != null) - { - var jobName = IoCManager.Resolve().Index(highPriorityJob).LocalizedName; - description = $"{description}\n{jobName}"; - } - - var descriptionLabel = new Label - { - Text = description, - ClipText = true, - HorizontalExpand = true - }; - var deleteButton = new Button - { - Text = Loc.GetString("character-setup-gui-character-picker-button-delete-button"), - Visible = !isSelectedCharacter, - }; - var confirmDeleteButton = new Button - { - Text = Loc.GetString("character-setup-gui-character-picker-button-confirm-delete-button"), - Visible = false, - }; - confirmDeleteButton.ModulateSelfOverride = StyleNano.ButtonColorCautionDefault; - confirmDeleteButton.OnPressed += _ => - { - Parent?.RemoveChild(this); - Parent?.RemoveChild(confirmDeleteButton); - preferencesManager.DeleteCharacter(profile); - }; - deleteButton.OnPressed += _ => - { - - deleteButton.Visible = false; - confirmDeleteButton.Visible = true; - - }; - - var internalHBox = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalExpand = true, - SeparationOverride = 0, - Children = - { - view, - descriptionLabel, - deleteButton, - confirmDeleteButton - } - }; - - AddChild(internalHBox); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - IoCManager.Resolve().DeleteEntity(_previewDummy); - _previewDummy = default; - } - } - } -} diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs deleted file mode 100644 index 750006bf7a98..000000000000 --- a/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Content.Shared.Preferences; -using Robust.Shared.Prototypes; - -namespace Content.Client.Preferences.UI -{ - public sealed partial class HumanoidProfileEditor - { - private void RandomizeEverything() - { - Profile = HumanoidCharacterProfile.Random(); - UpdateControls(); - IsDirty = true; - } - - private void RandomizeName() - { - if (Profile == null) return; - var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender); - SetName(name); - UpdateNameEdit(); - } - } -} diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml deleted file mode 100644 index 5926aee8987e..000000000000 --- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - public AlignRCDConstruction(PlacementManager pMan) : base(pMan) { - var dependencies = IoCManager.Instance!; - _entityManager = dependencies.Resolve(); - _mapManager = dependencies.Resolve(); - _playerManager = dependencies.Resolve(); - _stateManager = dependencies.Resolve(); - + IoCManager.InjectDependencies(this); _mapSystem = _entityManager.System(); _rcdSystem = _entityManager.System(); _transformSystem = _entityManager.System(); diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs index 51ec66ea4446..3eb0397a6908 100644 --- a/Content.Client/RCD/RCDMenu.xaml.cs +++ b/Content.Client/RCD/RCDMenu.xaml.cs @@ -68,7 +68,7 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) tooltip = Loc.GetString(entProto.Name); } - tooltip = char.ToUpper(tooltip[0]) + tooltip.Remove(0, 1); + tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1)); var button = new RCDMenuButton() { @@ -119,6 +119,12 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) SendRCDSystemMessageAction += bui.SendRCDSystemMessage; } + private static string OopsConcat(string a, string b) + { + // This exists to prevent Roslyn being clever and compiling something that fails sandbox checks. + return a + b; + } + private void AddRCDMenuButtonOnClickActions(Control control) { var radialContainer = control as RadialContainer; diff --git a/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs b/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs index 9012767ef3f3..8d5607af2d06 100644 --- a/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs +++ b/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs @@ -1,9 +1,11 @@ using System.Numerics; using Content.Shared.Radiation.Components; +using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.Enums; using Robust.Shared.Graphics; using Robust.Shared.Map; +using Robust.Shared.Physics; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -14,6 +16,7 @@ public sealed class RadiationPulseOverlay : Overlay [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + private TransformSystem? _transform; private const float MaxDist = 15.0f; @@ -72,6 +75,8 @@ protected override void Draw(in OverlayDrawArgs args) //Queries all pulses on the map and either adds or removes them from the list of rendered pulses based on whether they should be drawn (in range? on the same z-level/map? pulse entity still exists?) private void RadiationQuery(IEye? currentEye) { + _transform ??= _entityManager.System(); + if (currentEye == null) { _pulses.Clear(); @@ -91,7 +96,7 @@ private void RadiationQuery(IEye? currentEye) ( _baseShader.Duplicate(), new RadiationShaderInstance( - _entityManager.GetComponent(pulseEntity).MapPosition, + _transform.GetMapCoordinates(pulseEntity), pulse.VisualRange, pulse.StartTime, pulse.VisualDuration @@ -109,7 +114,7 @@ private void RadiationQuery(IEye? currentEye) _entityManager.TryGetComponent(pulseEntity, out RadiationPulseComponent? pulse)) { var shaderInstance = _pulses[pulseEntity]; - shaderInstance.instance.CurrentMapCoords = _entityManager.GetComponent(pulseEntity).MapPosition; + shaderInstance.instance.CurrentMapCoords = _transform.GetMapCoordinates(pulseEntity); shaderInstance.instance.Range = pulse.VisualRange; } else { _pulses[pulseEntity].shd.Dispose(); diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Blockers.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Blockers.cs index 2fa862f3df7f..99d85350b5e2 100644 --- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Blockers.cs +++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Blockers.cs @@ -17,7 +17,7 @@ private void InitializeBlockers() SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); - SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnInteractAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); @@ -27,6 +27,11 @@ private void InitializeBlockers() SubscribeLocalEvent(OnPullAttempt); } + private void OnInteractAttempt(Entity ent, ref InteractionAttemptEvent args) + { + args.Cancelled = true; + } + private void OnAttempt(EntityUid uid, ReplaySpectatorComponent component, CancellableEntityEventArgs args) { args.Cancel(); diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs index 2ee7e30ec9a2..d00e319eed91 100644 --- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs +++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs @@ -195,9 +195,16 @@ private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component, if (uid != _player.LocalEntity) return; - if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace) + if (args.Transform.MapUid != null || args.OldMapId == null) return; + if (_spectatorData != null) + { + // Currently scrubbing/setting the replay tick + // the observer will get respawned once the state was applied + return; + } + // The entity being spectated from was moved to null-space. // This was probably because they were spectating some entity in a client-side replay that left PVS range. // Simple respawn the ghost. diff --git a/Content.Client/Revenant/RevenantSystem.cs b/Content.Client/Revenant/RevenantSystem.cs index 49d29d8a5f4c..e050fe35aa2c 100644 --- a/Content.Client/Revenant/RevenantSystem.cs +++ b/Content.Client/Revenant/RevenantSystem.cs @@ -1,5 +1,4 @@ using Content.Client.Alerts; -using Content.Shared.Alert; using Content.Shared.Revenant; using Content.Shared.Revenant.Components; using Robust.Client.GameObjects; @@ -42,7 +41,7 @@ private void OnAppearanceChange(EntityUid uid, RevenantComponent component, ref private void OnUpdateAlert(Entity ent, ref UpdateAlertSpriteEvent args) { - if (args.Alert.AlertType != AlertType.Essence) + if (args.Alert.ID != ent.Comp.EssenceAlert) return; var sprite = args.SpriteViewEnt.Comp; diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs index 682c73f93e78..8e7e687fa8fc 100644 --- a/Content.Client/Revolutionary/RevolutionarySystem.cs +++ b/Content.Client/Revolutionary/RevolutionarySystem.cs @@ -1,44 +1,37 @@ -using Content.Shared.Antag; using Content.Shared.Revolutionary.Components; -using Content.Shared.Ghost; +using Content.Shared.Revolutionary; using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; namespace Content.Client.Revolutionary; /// /// Used for the client to get status icons from other revs. /// -public sealed class RevolutionarySystem : EntitySystem +public sealed class RevolutionarySystem : SharedRevolutionarySystem { + [Dependency] private readonly IPrototypeManager _prototype = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnCanShowRevIcon); - SubscribeLocalEvent(OnCanShowRevIcon); + SubscribeLocalEvent(GetRevIcon); + SubscribeLocalEvent(GetHeadRevIcon); } - /// - /// Determine whether a client should display the rev icon. - /// - private void OnCanShowRevIcon(EntityUid uid, T comp, ref CanDisplayStatusIconsEvent args) where T : IAntagStatusIconComponent + private void GetRevIcon(Entity ent, ref GetStatusIconsEvent args) { - args.Cancelled = !CanDisplayIcon(args.User, comp.IconVisibleToGhost); + if (HasComp(ent)) + return; + + if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); } - /// - /// The criteria that determine whether a client should see Rev/Head rev icons. - /// - private bool CanDisplayIcon(EntityUid? uid, bool visibleToGhost) + private void GetHeadRevIcon(Entity ent, ref GetStatusIconsEvent args) { - if (HasComp(uid) || HasComp(uid)) - return true; - - if (visibleToGhost && HasComp(uid)) - return true; - - return HasComp(uid); + if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); } - } diff --git a/Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs b/Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs new file mode 100644 index 000000000000..0219c965cde7 --- /dev/null +++ b/Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Robotics.Systems; + +namespace Content.Client.Robotics.Systems; + +public sealed class RoboticsConsoleSystem : SharedRoboticsConsoleSystem +{ +} diff --git a/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs b/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs new file mode 100644 index 000000000000..6185979eee6d --- /dev/null +++ b/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs @@ -0,0 +1,50 @@ +using Content.Shared.Robotics; +using Robust.Client.GameObjects; + +namespace Content.Client.Robotics.UI; + +public sealed class RoboticsConsoleBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + public RoboticsConsoleWindow _window = default!; + + public RoboticsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _window = new RoboticsConsoleWindow(Owner); + _window.OnDisablePressed += address => + { + SendMessage(new RoboticsConsoleDisableMessage(address)); + }; + _window.OnDestroyPressed += address => + { + SendMessage(new RoboticsConsoleDestroyMessage(address)); + }; + _window.OnClose += Close; + + _window.OpenCentered(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (state is not RoboticsConsoleState cast) + return; + + _window?.UpdateState(cast); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _window?.Dispose(); + } +} diff --git a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml new file mode 100644 index 000000000000..a3b397879002 --- /dev/null +++ b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml @@ -0,0 +1,40 @@ + + + + + + + + + +