Merge branch 'master' into euphoriacardport

Signed-off-by: freezer dog <iamabanana372456@gmail.com>
This commit is contained in:
freezer dog 2026-06-29 16:57:09 -04:00 committed by GitHub
commit 70482199d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3004 changed files with 85883 additions and 89528 deletions

View File

@ -32,6 +32,7 @@ jobs:
run: |
cd RobustToolbox/
git submodule update --init --recursive
rm Robust.Shared.Tests/Networking/NetEncryptionDoSTest.cs # DeltaV - work around bug in the backport
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
@ -45,13 +46,23 @@ jobs:
run: dotnet build --configuration DebugOpt --no-restore /m
- name: Run Content.Tests
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
shell: pwsh
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0 NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results"
- name: Run Content.IntegrationTests
shell: pwsh
run: |
$env:DOTNET_gcServer=1
dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results"
- name: Archive NUnit3 test results.
if: always()
uses: actions/upload-artifact@v4
with:
name: nunit3-results-${{ matrix.os }}
path: test_results/*
retention-days: 7
compression-level: 9
ci-success:
name: Build & Test Debug
needs:

View File

@ -29,6 +29,7 @@ jobs:
run: |
cd RobustToolbox
git fetch --depth=1
rm Robust.Shared.Tests/Networking/NetEncryptionDoSTest.cs # DeltaV - work around bug in the backport
- name: Install dependencies
run: dotnet restore

View File

@ -48,10 +48,11 @@ jobs:
cd RobustToolbox/
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 10.0.x
# ubuntu-latest has .NET 10
# - name: Setup .NET Core
# uses: actions/setup-dotnet@v4.1.0
# with:
# dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
@ -60,7 +61,13 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
run: dotnet run --project Content.Packaging server --log-build --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
run: dotnet run --project Content.Packaging client --log-build --no-wipe-release
- uses: actions/upload-artifact@v4
with:
name: binlogs
path: release/*.binlog
retention-days: 7

View File

@ -23,6 +23,7 @@ jobs:
run: |
cd RobustToolbox/
git submodule update --init --recursive
rm Robust.Shared.Tests/Networking/NetEncryptionDoSTest.cs # DeltaV - work around bug in the backport
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:

View File

@ -269,7 +269,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | [[File:SL_BreakingAndEntering.png]]
! style="border: 1px solid black;" | {{anchor|Breaking and Entering}}Breaking and Entering
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To break and enter into a high security area where one is not authorised nor invited, with intent to commit a crime within.
| style="border: 1px solid black;" | To break and enter into an area where one is not authorised nor invited.
|-
| style="border: 1px solid black;" | 203
| style="border: 1px solid black;" | [[File:SL_Rioting.png]]

View File

@ -73,28 +73,88 @@ If you are adding a lot of C# code, then take advantage of partial classes. Put
Otherwise, **add comments on or around any changed lines.**
A comment on a new imported namespace:
### Single-Line Changes
Format should look like this.
```cs
using Content.Server.Psionics.Glimmer; // DeltaV
/* Importing Namespaces - Include optional comment if its not obvious what its being used for. */
using Content.Server._DV.Psionics.Glimmer; // DeltaV
using Content.Shared.Damage.Systems; // DeltaV - Addition of HandHeldArmor
/* Changing an upstream line - Same line as the change */
if (!TryComp<EyeComponent>(ent, out var eye) || _disabled) // DeltaV - check if disabled
/* Adding - Either same line or above the line. */
EnsureComp<PotentialPsionicComponent>(entity); // Deltav - Psionics
/* "Deleting" - Don't actually delete, just comment out and say why. This only applies to upstream code. */
// args.StatusIcons.Add(_prototype.Index(component.Icon)); // DeltaV - commented out. status icon now added above
```
A pair of comments enclosing a block of added code:
> * Its pretty obvious in the example above that importing `Content.Server._DV.Psionics.Glimmer` means we'll be interacting with glimmer so putting `// DeltaV - Add Glimmer` is needlessly redundant.
> * It's not as obvious what the `Content.Shared.Damage.Systems` namespace is used for, since its so broad, so adding a comment what feature is using it helps.
> * Actual code changes should almost always include the comment after ``// DeltaV`.
### Multi-Line Changes
Depending on how much you are editing, putting a comment on EACH line may be excessive, so if you have a larger block of code you are changing, denote it like so:
```cs
private EntityUid Slice(...)
// BEGIN DeltaV - Remove innate radio and radios from pockets
for (var i = 1; i <= 4; i++) // Arachnids have 4 pockets
{
...
_transform.SetLocalRotation(sliceUid, 0);
// DeltaV - start of deep frier stuff
var slicedEv = new FoodSlicedEvent(user, uid, sliceUid);
RaiseLocalEvent(uid, ref slicedEv);
// DeltaV - end of deep frier stuff
...
if (_inventory.TryGetSlotEntity(target, $"pocket{i}", out var headset) && HasComp<HeadsetComponent>(headset))
_inventory.TryUnequip(target, $"pocket{i}", true, true);
}
RemComp<ActiveRadioComponent>(target); // If the zombie has an innate radio, get rid of it.
// END DeltaV
```
> * Denoting these with a BEGIN and END clearly shows they are block of code without having to read the entire comment. This makes it easier to tell when you're dealing with single-line comments versus a block with merging in conflicts.
> * Case and order of the first two words is less of a concern. `// DeltaV Begin` or `// Begin DeltaV` will work fine too.
> * Try to make your blocks as small as possible, but use your discretion.
> * If you deleting multiple lines, use line comments (``//``) if its a few lines but if its a larger block (like commenting out an entire function), it is preferable to use block comments (`/* */`).
#### Soft Exceptions to the Multi-Line "Rules"
Some multi-line changes can use a single-line comment in certain scenarios. But if you are UNSURE, just use `// BEGIN DeltaV` and `// END DeltaV` comments like the previous section does and it'll be fine.
I'll give some examples.
```cs
/* This change comments out 3 lines but only needs a single line comment because commenting out the if statement implies that its logic will be commented out too. */
// if (obj.WasModified<TraitPrototype>()) // DeltaV - Refreshed in TraitsTab
// {
// _profileEditor.RefreshTraits();
// }
/* Same principle here. This adds two lines but the if statement implies the next line so commenting both lines isn't really needed. */
if (_flight.IsFlying(entity.Owner)) // DeltaV - Harpy Flight
return true;
```
### New Methods or Component Variables
Sometimes, you'll need to implement a whole new method or component variable and instead of wrapping it in `// BEGIN DeltaV` and `// END DeltaV`, you can just denote that it's a DeltaV function in the summary block before the function. This denotes the WHOLE function as a DeltaV addition.
```cs
/* New Method Example */
/// <summary>
/// DeltaV - Handle revealing ninja if cloaked when attacked by a hitscan attack.
/// </summary>
private void OnNinjaAttacked(Entity<SpaceNinjaComponent> ent, ref DamageChangedEvent args)
{
...
}
/* New Component Variable Example */
/// <summary>
/// DeltaV - If disabled the action will not disable when no charges remain. Use if you want to handle no charges differently.
/// </summary>
[DataField]
public bool DisableWhenEmpty = true;
```
In short:
* Use `// BEGIN DeltaV` and `// END Delta` to denote a *block* of changes.
* Keep blocks as small as possible.
* Use `// DeltaV` on or before the line if its not a block of changes.
* Use exceptions when they make sense.
### Changing Upstream Localization Fluent .ftl files
**Move all changed locale strings to a new DeltaV file** - use a `.ftl` file in the `_DV` folder. Comment out the old strings in the upstream file, and explain that they were moved.
@ -104,10 +164,10 @@ Example:
Commented out old string in `Resources\Locale\en-US\xenoarchaeology\artifact-analyzer.ftl`
```
# DeltaV - moved to _DV file
#analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
#}[/color][/font]
# analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
# }[/color][/font]
```
The new version of the string in `Resources\Locale\en-US\_DV\xenoarchaeology\artifact-analyzer.ftl`

View File

@ -1,5 +1,6 @@
#nullable enable
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@ -44,7 +45,7 @@ public class ComponentQueryBenchmark
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(QueryBenchSystem).Assembly);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency<IEntityManager>();
_itemQuery = _entMan.GetEntityQuery<ItemComponent>();

View File

@ -7,6 +7,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
<Nullable>disable</Nullable>
<DefineConstants>$(DefineConstants);ALLOW_BAD_PRACTICES</DefineConstants>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>

View File

@ -1,11 +1,11 @@
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.CCVar;
using Robust.Shared;
using Robust.Shared.Analyzers;
@ -69,7 +69,7 @@ public class DeltaPressureBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
@ -69,7 +70,7 @@ public class DestructibleBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
_entMan = server.ResolveDependency<IEntityManager>();

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
@ -60,7 +61,7 @@ public class DeviceNetworkingBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(DeviceNetworkingBenchmark).Assembly);
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
await server.WaitPost(() =>

View File

@ -1,3 +1,4 @@
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
@ -51,7 +52,7 @@ public class GasReactionBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
// Create test map and grid

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
@ -27,7 +28,7 @@ public class HeatCapacityBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
await _pair.Connect();
_cEntMan = _pair.Client.ResolveDependency<IEntityManager>();
_sEntMan = _pair.Server.ResolveDependency<IEntityManager>();

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@ -29,7 +30,7 @@ public class MapLoadBenchmark
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
var server = _pair.Server;
Paths = server.ResolveDependency<IPrototypeManager>()

View File

@ -1,5 +1,6 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@ -50,7 +51,7 @@ public class PvsBenchmark
#endif
PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency<IEntityManager>();
_pair.Server.CfgMan.SetCVar(CVars.NetPVS, true);
_pair.Server.CfgMan.SetCVar(CVars.ThreadParallelCount, 0);

View File

@ -1,4 +1,5 @@
#nullable enable
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@ -21,7 +22,7 @@ public class RaiseEventBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(BenchSystem).Assembly);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
var entMan = _pair.Server.EntMan;
var fact = _pair.Server.ResolveDependency<IComponentFactory>();
var bus = (EntityEventBus)entMan.EventBus;

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
@ -36,7 +37,7 @@ public class SpawnEquipDeleteBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
var mapData = await _pair.CreateTestMap();

View File

@ -0,0 +1,39 @@
using Content.Client.Overlays;
using Content.Shared.Access.Systems;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Access.Systems;
public sealed class JobStatusSystem : SharedJobStatusSystem
{
[Dependency] private readonly ShowJobIconsSystem _showJobIcons = default!;
[Dependency] private readonly ShowCrewIconsSystem _showCrewIcons = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private static readonly ProtoId<SecurityIconPrototype> CrewBorderIcon = "CrewBorderIcon";
private static readonly ProtoId<SecurityIconPrototype> CrewUncertainBorderIcon = "CrewUncertainBorderIcon";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<JobStatusComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
// show the status icons if the player has the correponding HUDs
private void OnGetStatusIconsEvent(Entity<JobStatusComponent> ent, ref GetStatusIconsEvent ev)
{
if (_showJobIcons.IsActive && ent.Comp.JobStatusIcon != null)
ev.StatusIcons.Add(_prototype.Index(ent.Comp.JobStatusIcon));
if (_showCrewIcons.IsActive)
{
if (_showCrewIcons.UncertainCrewBorder)
ev.StatusIcons.Add(_prototype.Index(CrewUncertainBorderIcon));
else if (ent.Comp.IsCrew)
ev.StatusIcons.Add(_prototype.Index(CrewBorderIcon));
}
}
}

View File

@ -2,11 +2,9 @@ using System.Linq; // DeltaV
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
using Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
@ -15,21 +13,13 @@ namespace Content.Client.Access.UI
public sealed class IdCardConsoleBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
private readonly SharedIdCardConsoleSystem _idCardConsoleSystem = default!;
private IdCardConsoleWindow? _window;
// CCVar.
private int _maxNameLength;
private int _maxIdJobLength;
public IdCardConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_idCardConsoleSystem = EntMan.System<SharedIdCardConsoleSystem>();
_maxNameLength =_cfgManager.GetCVar(CCVars.MaxNameLength);
_maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength);
}
protected override void Open()
@ -79,12 +69,6 @@ namespace Content.Client.Access.UI
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, ProtoId<JobPrototype> newJobPrototype)
{
if (newFullName.Length > _maxNameLength)
newFullName = newFullName[.._maxNameLength];
if (newJobTitle.Length > _maxIdJobLength)
newJobTitle = newJobTitle[.._maxIdJobLength];
SendMessage(new WriteToTargetIdMessage(
newFullName,
newJobTitle,

View File

@ -23,10 +23,6 @@ namespace Content.Client.Access.UI
private readonly IdCardConsoleBoundUserInterface _owner;
// CCVar.
private int _maxNameLength;
private int _maxIdJobLength;
private AccessLevelControl _accessButtons = new();
private readonly List<string> _jobPrototypeIds = new();
@ -46,11 +42,8 @@ namespace Content.Client.Access.UI
_owner = owner;
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
_maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength);
FullNameLineEdit.OnTextEntered += _ => SubmitData();
FullNameLineEdit.IsValid = s => s.Length <= _maxNameLength;
FullNameLineEdit.IsValid = s => s.Length <= _cfgManager.GetCVar(CCVars.MaxNameLength);
FullNameLineEdit.OnTextChanged += _ =>
{
FullNameSaveButton.Disabled = FullNameSaveButton.Text == _lastFullName;
@ -58,7 +51,7 @@ namespace Content.Client.Access.UI
FullNameSaveButton.OnPressed += _ => SubmitData();
JobTitleLineEdit.OnTextEntered += _ => SubmitData();
JobTitleLineEdit.IsValid = s => s.Length <= _maxIdJobLength;
JobTitleLineEdit.IsValid = s => s.Length <= _cfgManager.GetCVar(CCVars.MaxIdJobLength);
JobTitleLineEdit.OnTextChanged += _ =>
{
JobTitleSaveButton.Disabled = JobTitleLineEdit.Text == _lastJobTitle;

View File

@ -237,11 +237,12 @@ internal sealed class AdminNameOverlay : Overlay
}
// DeltaV - SSD Time START
if (_entityManager.TryGetComponent<SSDIndicatorComponent>(entity, out var ssdIndicator) && ssdIndicator.IsSSD)
if (_entityManager.TryGetComponent<SSDIndicatorComponent>(entity, out var ssdIndicator)
&& ssdIndicator.SsdSince is {} ssdSince)
{
color = Color.MediumPurple;
color.A = alpha;
var ssdText = Loc.GetString("admin-overlay-ssd-time", ("time", (_timing.CurTime - ssdIndicator.SsdSince).ToString("%hh':'mm':'ss")));
var ssdText = Loc.GetString("admin-overlay-ssd-time", ("time", (_timing.CurTime - ssdSince).ToString("%hh':'mm':'ss")));
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, ssdText, uiScale, color);
currentOffset += lineoffset;
}

View File

@ -1,4 +1,5 @@
using System.Numerics;
using System.Linq;
using System.Numerics;
using Content.Client.Administration.UI.BanList.Bans;
using Content.Client.Administration.UI.BanList.RoleBans;
using Content.Client.Eui;
@ -73,7 +74,7 @@ public sealed class BanListEui : BaseEui
return date.ToString("MM/dd/yyyy h:mm tt");
}
public static void SetData<T>(IBanListLine<T> line, SharedServerBan ban) where T : SharedServerBan
public static void SetData<T>(IBanListLine<T> line, SharedBan ban) where T : SharedBan
{
line.Reason.Text = ban.Reason;
line.BanTime.Text = FormatDate(ban.BanTime);
@ -94,20 +95,20 @@ public sealed class BanListEui : BaseEui
line.BanningAdmin.Text = ban.BanningAdminName;
}
private void OnLineIdsClicked<T>(IBanListLine<T> line) where T : SharedServerBan
private void OnLineIdsClicked<T>(IBanListLine<T> line) where T : SharedBan
{
_popup?.Close();
_popup = null;
var ban = line.Ban;
var id = ban.Id == null ? string.Empty : Loc.GetString("ban-list-id", ("id", ban.Id.Value));
var ip = ban.Address == null
var ip = ban.Addresses.Length == 0
? string.Empty
: Loc.GetString("ban-list-ip", ("ip", ban.Address.Value.address));
var hwid = ban.HWId == null ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", ban.HWId));
var guid = ban.UserId == null
: Loc.GetString("ban-list-ip", ("ip", string.Join(',', ban.Addresses.Select(a => a.address))));
var hwid = ban.HWIds.Length == 0 ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", string.Join(',', ban.HWIds)));
var guid = ban.UserIds.Length == 0
? string.Empty
: Loc.GetString("ban-list-guid", ("guid", ban.UserId.Value.ToString()));
: Loc.GetString("ban-list-guid", ("guid", string.Join(',', ban.UserIds)));
_popup = new BanListIdsPopup(id, ip, hwid, guid);

View File

@ -16,7 +16,7 @@ public sealed partial class BanListControl : Control
RobustXamlLoader.Load(this);
}
public void SetBans(List<SharedServerBan> bans)
public void SetBans(List<SharedBan> bans)
{
for (var i = Bans.ChildCount - 1; i >= 1; i--)
{

View File

@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Administration.UI.BanList.Bans;
[GenerateTypedNameReferences]
public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedServerBan>
public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedBan>
{
public SharedServerBan Ban { get; }
public SharedBan Ban { get; }
public event Action<BanListLine>? IdsClicked;
public BanListLine(SharedServerBan ban)
public BanListLine(SharedBan ban)
{
RobustXamlLoader.Load(this);

View File

@ -3,7 +3,7 @@ using Robust.Client.UserInterface.Controls;
namespace Content.Client.Administration.UI.BanList;
public interface IBanListLine<T> where T : SharedServerBan
public interface IBanListLine<T> where T : SharedBan
{
T Ban { get; }
Label Reason { get; }

View File

@ -16,7 +16,7 @@ public sealed partial class RoleBanListControl : Control
RobustXamlLoader.Load(this);
}
public void SetRoleBans(List<SharedServerRoleBan> bans)
public void SetRoleBans(List<SharedBan> bans)
{
for (var i = RoleBans.ChildCount - 1; i >= 1; i--)
{

View File

@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Administration.UI.BanList.RoleBans;
[GenerateTypedNameReferences]
public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedServerRoleBan>
public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedBan>
{
public SharedServerRoleBan Ban { get; }
public SharedBan Ban { get; }
public event Action<RoleBanListLine>? IdsClicked;
public RoleBanListLine(SharedServerRoleBan ban)
public RoleBanListLine(SharedBan ban)
{
RobustXamlLoader.Load(this);
@ -21,7 +21,7 @@ public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedS
IdsHidden.OnPressed += IdsPressed;
BanListEui.SetData(this, ban);
Role.Text = ban.Role;
Role.Text = string.Join(", ", ban.Roles ?? []);
}
private void IdsPressed(ButtonEventArgs buttonEventArgs)

View File

@ -70,7 +70,7 @@ public sealed partial class AdminNotesLine : BoxContainer
TimeLabel.Text = Note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
ServerLabel.Text = Note.ServerName ?? "Unknown";
RoundLabel.Text = Note.Round == null ? "Unknown round" : "Round " + Note.Round;
RoundLabel.Text = Note.Rounds.Length == 0 ? "Unknown round" : "Round " + string.Join(',', Note.Rounds);
AdminLabel.Text = Note.CreatedByName;
PlaytimeLabel.Text = $"{Note.PlaytimeAtNote.TotalHours: 0.0}h";
@ -143,7 +143,12 @@ public sealed partial class AdminNotesLine : BoxContainer
private string FormatRoleBanMessage()
{
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} ");
var rolesText = string.Join(
", ",
// Explicit cast here to avoid sandbox violation.
(IEnumerable<BanRoleDef>?)Note.BannedRoles ?? [new BanRoleDef("what", "You should not be seeing this")]);
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {rolesText} ");
return FormatBanMessageCommon(banMessage);
}

View File

@ -32,9 +32,9 @@ public sealed partial class AdminNotesLinePopup : Popup
IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id));
TypeLabel.Text = Loc.GetString("admin-notes-type", ("type", note.NoteType));
SeverityLabel.Text = Loc.GetString("admin-notes-severity", ("severity", note.NoteSeverity ?? NoteSeverity.None));
RoundIdLabel.Text = note.Round == null
RoundIdLabel.Text = note.Rounds.Length == 0
? Loc.GetString("admin-notes-round-id-unknown")
: Loc.GetString("admin-notes-round-id", ("id", note.Round));
: Loc.GetString("admin-notes-round-id", ("id", string.Join(',', note.Rounds)));
CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName));
CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")));
EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName));

View File

@ -25,8 +25,8 @@ public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
var index = _sprite.LayerMapReserve((ent.Owner, sprite), ent.Comp.LayerMap);
if (TryComp<HumanoidAppearanceComponent>(ent, out var humanoidAppearance) &&
ent.Comp.SpeciesSprites.TryGetValue(humanoidAppearance.Species, out var speciesSprite))
if (TryComp<HumanoidProfileComponent>(ent, out var humanoid) &&
ent.Comp.SpeciesSprites.TryGetValue(humanoid.Species, out var speciesSprite))
{
_sprite.LayerSetSprite((ent.Owner, sprite), index, speciesSprite);
}

View File

@ -1,4 +1,4 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'anomaly-generator-ui-title'}"
@ -32,7 +32,7 @@
</BoxContainer>
<!--Sprite View-->
<PanelContainer Margin="12 0 0 0"
StyleClasses="Inset"
StyleClasses="BackgroundPanelDark"
VerticalAlignment="Center">
<SpriteView Name="EntityView"
SetSize="96 96"

View File

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

View File

@ -1,6 +1,7 @@
using Content.Client.Stylesheets;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Monitor;
using Content.Shared.FixedPoint;
using Content.Shared.Temperature;
@ -22,6 +23,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
private readonly IEntityManager _entManager;
private readonly IResourceCache _cache;
private readonly SharedAtmosphereSystem _atmosphere;
private Dictionary<AtmosAlarmType, string> _alarmStrings = new Dictionary<AtmosAlarmType, string>()
{
@ -37,6 +39,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
_entManager = IoCManager.Resolve<IEntityManager>();
_cache = IoCManager.Resolve<IResourceCache>();
_atmosphere = _entManager.System<SharedAtmosphereSystem>();
NetEntity = uid;
Coordinates = coordinates;
@ -149,7 +152,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
{
FixedPoint2 gasPercent = percent * 100f;
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
var gasAbbreviation = Loc.GetString(_atmosphere.GetGas(gas).Abbreviation);
var gasLabel = new Label()
{

View File

@ -1,6 +1,7 @@
using Content.Client.Stylesheets;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
@ -19,12 +20,14 @@ public sealed partial class AtmosMonitoringEntryContainer : BoxContainer
private readonly IEntityManager _entManager;
private readonly IResourceCache _cache;
private readonly SharedAtmosphereSystem _atmosphere;
public AtmosMonitoringEntryContainer(AtmosMonitoringConsoleEntry data)
{
RobustXamlLoader.Load(this);
_entManager = IoCManager.Resolve<IEntityManager>();
_cache = IoCManager.Resolve<IResourceCache>();
_atmosphere = _entManager.System<SharedAtmosphereSystem>();
Data = data;
@ -132,7 +135,7 @@ public sealed partial class AtmosMonitoringEntryContainer : BoxContainer
var gasPercent = (FixedPoint2)0f;
gasPercent = percent * 100f;
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
var gasAbbreviation = Loc.GetString(_atmosphere.GetGas(gas).Abbreviation);
var gasLabel = new Label()
{

View File

@ -10,7 +10,9 @@ namespace Content.Client.Atmos.EntitySystems
[UsedImplicitly]
internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem
{
public readonly Dictionary<EntityUid, AtmosDebugOverlayMessage> TileData = new();
[Dependency] private readonly IOverlayManager _overlayManager = default!;
public readonly Dictionary<EntityUid, AtmosDebugOverlayMessage> TileData = [];
// Configuration set by debug commands and used by AtmosDebugOverlay {
/// <summary>Value source for display</summary>
@ -25,6 +27,8 @@ namespace Content.Client.Atmos.EntitySystems
public bool CfgCBM = false;
// }
private AtmosDebugOverlay? _overlay;
public override void Initialize()
{
base.Initialize();
@ -34,10 +38,6 @@ namespace Content.Client.Atmos.EntitySystems
SubscribeNetworkEvent<AtmosDebugOverlayDisableMessage>(HandleAtmosDebugOverlayDisableMessage);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if(!overlayManager.HasOverlay<AtmosDebugOverlay>())
overlayManager.AddOverlay(new AtmosDebugOverlay(this));
}
private void OnGridRemoved(GridRemovalEvent ev)
@ -51,19 +51,25 @@ namespace Content.Client.Atmos.EntitySystems
private void HandleAtmosDebugOverlayMessage(AtmosDebugOverlayMessage message)
{
TileData[GetEntity(message.GridId)] = message;
if (_overlay is not null)
return;
_overlay = new AtmosDebugOverlay(this);
_overlayManager.AddOverlay(_overlay);
}
private void HandleAtmosDebugOverlayDisableMessage(AtmosDebugOverlayDisableMessage ev)
{
TileData.Clear();
RemoveOverlay();
}
public override void Shutdown()
{
base.Shutdown();
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (overlayManager.HasOverlay<AtmosDebugOverlay>())
overlayManager.RemoveOverlay<AtmosDebugOverlay>();
RemoveOverlay();
}
public void Reset(RoundRestartCleanupEvent ev)
@ -75,6 +81,15 @@ namespace Content.Client.Atmos.EntitySystems
{
return TileData.ContainsKey(gridId);
}
private void RemoveOverlay()
{
if (_overlay is null)
return;
_overlayManager.RemoveOverlay(_overlay);
_overlay = null;
}
}
internal enum AtmosDebugOverlayMode : byte

View File

@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Reactions;
namespace Content.Client.Atmos.EntitySystems;
@ -13,6 +14,29 @@ public sealed partial class AtmosphereSystem
implementation.
*/
/// <inheritdoc/>
/// <remarks>No-op on client as reactions aren't entirely in shared.
/// Don't call it. Smile.</remarks>
public override ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder)
{
// Reactions don't work on client so don't even try.
throw new NotImplementedException();
}
public override bool IsMixtureFuel(GasMixture mixture, float epsilon = Atmospherics.Epsilon)
{
var tmp = new float[Atmospherics.AdjustedNumberOfGases];
NumericsHelpers.Multiply(mixture.Moles, GasFuelMask, tmp);
return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
}
public override bool IsMixtureOxidizer(GasMixture mixture, float epsilon = Atmospherics.Epsilon)
{
var tmp = new float[Atmospherics.AdjustedNumberOfGases];
NumericsHelpers.Multiply(mixture.Moles, GasOxidizerMask, tmp);
return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override float GetHeatCapacityCalculation(float[] moles, bool space)
{
@ -27,7 +51,7 @@ public sealed partial class AtmosphereSystem
// though this isnt the hottest code path so it should be fine
// the gc can eat a little as a treat
var tmp = new float[moles.Length];
NumericsHelpers.Multiply(moles, GasSpecificHeats, tmp);
NumericsHelpers.Multiply(moles, GasMolarHeatCapacities, tmp);
// Adjust heat capacity by speedup, because this is primarily what
// determines how quickly gases heat up/cool.
return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity);

View File

@ -0,0 +1,30 @@
using Content.Client.Atmos.Overlays;
using JetBrains.Annotations;
using Robust.Client.Graphics;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// System responsible for rendering atmos fire animations using <see cref="GasTileFireOverlay"/>.
/// </summary>
[UsedImplicitly]
public sealed class GasTileFireOverlaySystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private GasTileFireOverlay _fireOverlay = default!;
public override void Initialize()
{
base.Initialize();
_fireOverlay = new GasTileFireOverlay();
_overlayMan.AddOverlay(_fireOverlay);
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay<GasTileFireOverlay>();
}
}

View File

@ -0,0 +1,30 @@
using Content.Client.Atmos.Overlays;
using JetBrains.Annotations;
using Robust.Client.Graphics;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// System responsible for rendering heat distortion using <see cref="GasTileHeatBlurOverlay"/>.
/// </summary>
[UsedImplicitly]
public sealed class GasTileHeatBlurOverlaySystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private GasTileHeatBlurOverlay _gasTileHeatBlurOverlay = default!;
public override void Initialize()
{
base.Initialize();
_gasTileHeatBlurOverlay = new GasTileHeatBlurOverlay();
_overlayMan.AddOverlay(_gasTileHeatBlurOverlay);
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay<GasTileHeatBlurOverlay>();
}
}

View File

@ -1,106 +1,85 @@
using Content.Client.Atmos.Overlays;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameStates;
namespace Content.Client.Atmos.EntitySystems
namespace Content.Client.Atmos.EntitySystems;
[UsedImplicitly]
public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
[UsedImplicitly]
public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
public override void Initialize()
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly SpriteSystem _spriteSys = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
base.Initialize();
SubscribeNetworkEvent<GasOverlayUpdateEvent>(HandleGasOverlayUpdate);
SubscribeLocalEvent<GasTileOverlayComponent, ComponentHandleState>(OnHandleState);
}
private GasTileOverlay _overlay = default!;
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
{
Dictionary<Vector2i, GasOverlayChunk> modifiedChunks;
public override void Initialize()
switch (args.Current)
{
base.Initialize();
SubscribeNetworkEvent<GasOverlayUpdateEvent>(HandleGasOverlayUpdate);
SubscribeLocalEvent<GasTileOverlayComponent, ComponentHandleState>(OnHandleState);
_overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
_overlayMan.AddOverlay(_overlay);
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay<GasTileOverlay>();
}
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
{
Dictionary<Vector2i, GasOverlayChunk> modifiedChunks;
switch (args.Current)
// is this a delta or full state?
case GasTileOverlayDeltaState delta:
{
// is this a delta or full state?
case GasTileOverlayDeltaState delta:
modifiedChunks = delta.ModifiedChunks;
foreach (var index in comp.Chunks.Keys)
{
modifiedChunks = delta.ModifiedChunks;
foreach (var index in comp.Chunks.Keys)
{
if (!delta.AllChunks.Contains(index))
comp.Chunks.Remove(index);
}
break;
if (!delta.AllChunks.Contains(index))
comp.Chunks.Remove(index);
}
case GasTileOverlayState state:
{
modifiedChunks = state.Chunks;
foreach (var index in comp.Chunks.Keys)
{
if (!state.Chunks.ContainsKey(index))
comp.Chunks.Remove(index);
}
break;
}
default:
return;
break;
}
foreach (var (index, data) in modifiedChunks)
case GasTileOverlayState state:
{
comp.Chunks[index] = data;
modifiedChunks = state.Chunks;
foreach (var index in comp.Chunks.Keys)
{
if (!state.Chunks.ContainsKey(index))
comp.Chunks.Remove(index);
}
break;
}
default:
return;
}
foreach (var (index, data) in modifiedChunks)
{
comp.Chunks[index] = data;
}
}
private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev)
{
foreach (var (nent, removedIndicies) in ev.RemovedChunks)
{
var grid = GetEntity(nent);
if (!TryComp(grid, out GasTileOverlayComponent? comp))
continue;
foreach (var index in removedIndicies)
{
comp.Chunks.Remove(index);
}
}
private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev)
foreach (var (nent, gridData) in ev.UpdatedChunks)
{
foreach (var (nent, removedIndicies) in ev.RemovedChunks)
var grid = GetEntity(nent);
if (!TryComp(grid, out GasTileOverlayComponent? comp))
continue;
foreach (var chunkData in gridData)
{
var grid = GetEntity(nent);
if (!TryComp(grid, out GasTileOverlayComponent? comp))
continue;
foreach (var index in removedIndicies)
{
comp.Chunks.Remove(index);
}
}
foreach (var (nent, gridData) in ev.UpdatedChunks)
{
var grid = GetEntity(nent);
if (!TryComp(grid, out GasTileOverlayComponent? comp))
continue;
foreach (var chunkData in gridData)
{
comp.Chunks[chunkData.Index] = chunkData;
}
comp.Chunks[chunkData.Index] = chunkData;
}
}
}

View File

@ -0,0 +1,31 @@
using Content.Client.Atmos.Overlays;
using JetBrains.Annotations;
using Robust.Client.Graphics;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// System responsible for rendering visible atmos gasses (like plasma for example) using <see cref="GasTileVisibleGasOverlay"/>.
/// </summary>
[UsedImplicitly]
public sealed class GasTileVisibleGasOverlaySystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private GasTileVisibleGasOverlay _visibleGasOverlay = default!;
public override void Initialize()
{
base.Initialize();
_visibleGasOverlay = new GasTileVisibleGasOverlay();
_overlayMan.AddOverlay(_visibleGasOverlay);
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay<GasTileVisibleGasOverlay>();
}
}

View File

@ -7,7 +7,7 @@
<!-- Status (pressure, temperature, alarm state, device total, address, etc) -->
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2">
<!-- Left column (view of entity) -->
<PanelContainer Margin="2 0 6 0" StyleClasses="Inset" VerticalAlignment="Center" VerticalExpand="True">
<PanelContainer Margin="2 0 6 0" StyleClasses="BackgroundPanelDark" VerticalAlignment="Center" VerticalExpand="True">
<SpriteView Name="EntityView" OverrideDirection="South" Scale="2 2" SetSize="64 64"/>
</PanelContainer>
<!-- Center column (pressure, temperature, alarm state) -->

View File

@ -0,0 +1,253 @@
using Content.Client.Atmos.EntitySystems;
using Content.Client.Graphics;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using System.Numerics;
namespace Content.Client.Atmos.Overlays;
/// <summary>
/// Renders a thermal heatmap overlay for gas tiles, used for equipment like thermal glasses.
/// /// </summary>
public sealed class GasTileDangerousTemperatureOverlay : Overlay
{
public override bool RequestScreenTexture { get; set; } = false;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
private GasTileOverlaySystem? _gasTileOverlay;
private readonly SharedTransformSystem _xformSys;
private EntityQuery<GasTileOverlayComponent> _overlayQuery;
private readonly OverlayResourceCache<CachedResources> _resources = new();
private List<Entity<MapGridComponent>> _grids = new();
// Cache used to transform ThermalByte into Color for overlay
private readonly Color[] _colorCache = new Color[256];
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
public GasTileDangerousTemperatureOverlay()
{
IoCManager.InjectDependencies(this);
_xformSys = _entManager.System<SharedTransformSystem>();
_overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
for (byte i = 0; i <= ThermalByte.TempResolution; i++)
{
_colorCache[i] = PreCalculateColor(i);
}
_colorCache[ThermalByte.StateVacuum] = Color.Teal;
_colorCache[ThermalByte.StateVacuum].A = 0.6f;
_colorCache[ThermalByte.AtmosImpossible] = Color.Transparent;
#if DEBUG // This shouldn't happend so tell me if you see this LimeGreen on the screen
_colorCache[ThermalByte.ReservedFuture0] = Color.LimeGreen;
_colorCache[ThermalByte.ReservedFuture1] = Color.LimeGreen;
_colorCache[ThermalByte.ReservedFuture2] = Color.LimeGreen;
#else
_colorCache[ThermalByte.ReservedFuture0] = Color.Transparent;
_colorCache[ThermalByte.ReservedFuture1] = Color.Transparent;
_colorCache[ThermalByte.ReservedFuture2] = Color.Transparent;
#endif
}
/// <summary>
/// Used for Calculating onscreen color from ThermalByte core value
/// /// </summary>
private static Color PreCalculateColor(byte byteTemp)
{
// Color Thresholds in Kelvin
// -150 C
const float deepFreezeK = 123.15f;
// -50 C
const float freezeStartK = 223.15f;
// 0 C
const float waterFreezeK = 273.15f;
// 50 C
const float heatStartK = 323.15f;
// 100 C
const float waterBoilK = 373.15f;
// 300 C
const float superHeatK = 573.15f;
var tempK = byteTemp * ThermalByte.TempDegreeResolution;
// Neutral Zone Check (0C to 50C)
// If between 273.15K and 323.15K, it's transparent.
if (tempK >= waterFreezeK && tempK < heatStartK)
{
return Color.Transparent;
}
Color resultingColor;
switch (tempK)
{
case < deepFreezeK:
resultingColor = Color.FromHex("#330066");
resultingColor.A = 0.7f;
break;
case < freezeStartK:
// Interpolate Deep Purple -> Blue
// Range: 123.15 to 223.15 (Span: 100)
resultingColor = Color.InterpolateBetween(
Color.FromHex("#330066"),
Color.Blue,
(tempK - deepFreezeK) * 0.01f);
resultingColor.A = 0.6f;
break;
case < waterFreezeK:
// Interpolate Blue -> Transparent
// Range: 223.15 to 273.15 (Span: 50)
resultingColor = Color.InterpolateBetween(
new Color(Color.Blue.R, Color.Blue.G, Color.Blue.B, 0.6f),
new Color(Color.Blue.R, Color.Blue.G, Color.Blue.B, 0.2f),
(tempK - freezeStartK) * 0.02f);
break;
case < waterBoilK:
// Interpolate Transparent -> Yellow
// Range: 323.15 to 373.15 (Span: 50)
resultingColor = Color.InterpolateBetween(
new Color(Color.Yellow.R, Color.Yellow.G, Color.Yellow.B, 0.2f),
new Color(Color.Yellow.R, Color.Yellow.G, Color.Yellow.B, 0.6f),
(tempK - heatStartK) * 0.02f);
break;
case < superHeatK:
// Interpolate Yellow -> Red
// Range: 373.15 to 573.15 (Span: 200)
resultingColor = Color.InterpolateBetween(
Color.Yellow,
Color.Red,
(tempK - waterBoilK) * 0.005f);
resultingColor.A = 0.6f;
break;
default:
resultingColor = Color.DarkRed;
resultingColor.A = 0.7f;
break;
}
return resultingColor;
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return false;
_gasTileOverlay ??= _entManager.System<GasTileOverlaySystem>();
if (_gasTileOverlay == null)
return false;
var target = args.Viewport.RenderTarget;
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (res.TemperatureTarget is null || res.TemperatureTarget.Texture.Size != target.Size)
{
res.TemperatureTarget?.Dispose();
res.TemperatureTarget = _clyde.CreateRenderTarget(
target.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: nameof(GasTileDangerousTemperatureOverlay));
}
var drawHandle = args.WorldHandle;
var worldBounds = args.WorldBounds;
var worldAABB = args.WorldAABB;
var mapId = args.MapId;
var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
drawHandle.RenderInRenderTarget(res.TemperatureTarget,
() =>
{
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref _grids);
foreach (var grid in _grids)
{
if (!_overlayQuery.TryGetComponent(grid.Owner, out var comp))
continue;
var gridTileSizeVec = grid.Comp.TileSizeVector;
var gridTileCenterVec = grid.Comp.TileSizeHalfVector;
var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner);
var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
drawHandle.SetTransform(gridEntToViewportLocal);
var worldToGridLocal = _xformSys.GetInvWorldMatrix(grid.Owner);
var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
var localBounds = new Box2i(
(int)MathF.Floor(floatBounds.Left),
(int)MathF.Floor(floatBounds.Bottom),
(int)MathF.Ceiling(floatBounds.Right),
(int)MathF.Ceiling(floatBounds.Top));
foreach (var chunk in comp.Chunks.Values)
{
var enumerator = new GasChunkEnumerator(chunk);
while (enumerator.MoveNext(out var tileGas))
{
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
if (!localBounds.Contains(tilePosition))
continue;
var gasColor = _colorCache[tileGas.ByteGasTemperature.Value];
if (gasColor.A <= 0f)
continue;
drawHandle.DrawRect(
Box2.CenteredAround(tilePosition + gridTileCenterVec, gridTileSizeVec),
gasColor
);
}
}
}
},
new Color(0, 0, 0, 0));
drawHandle.SetTransform(Matrix3x2.Identity);
return true;
}
protected override void Draw(in OverlayDrawArgs args)
{
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (res.TemperatureTarget != null)
args.WorldHandle.DrawTextureRect(res.TemperatureTarget.Texture, args.WorldBounds);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
protected override void DisposeBehavior()
{
_resources.Dispose();
base.DisposeBehavior();
}
private sealed class CachedResources : IDisposable
{
public IRenderTexture? TemperatureTarget;
public void Dispose()
{
TemperatureTarget?.Dispose();
}
}
}

View File

@ -0,0 +1,172 @@
using Content.Client.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Species;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Numerics;
namespace Content.Client.Atmos.Overlays;
/// <summary>
/// Overlay responsible for rendering atmos fire animation.
/// </summary>
public sealed class GasTileFireOverlay : Overlay
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private readonly SharedTransformSystem _xformSys;
private readonly SharedMapSystem _mapSystem = default!;
private readonly ShaderInstance _shader;
private readonly float[] _timer;
private readonly float[][] _frameDelays;
private readonly int[] _frameCounter;
// TODO combine textures into a single texture atlas.
private readonly Texture[][] _frames;
private const int FireStates = 3;
private const string FireRsiPath = "/Textures/Effects/fire.rsi";
public const int GasOverlayZIndex = (int)Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else
public GasTileFireOverlay()
{
IoCManager.InjectDependencies(this);
_xformSys = _entManager.System<SharedTransformSystem>();
_mapSystem = _entManager.System<SharedMapSystem>();
_shader = _protoMan.Index(UnshadedShader).Instance();
ZIndex = GasOverlayZIndex;
_timer = new float[FireStates];
_frameDelays = new float[FireStates][];
_frameCounter = new int[FireStates];
_frames = new Texture[FireStates][];
var fire = _resourceCache.GetResource<RSIResource>(FireRsiPath).RSI;
for (var i = 0; i < FireStates; i++)
{
if (!fire.TryGetState((i + 1).ToString(), out var state))
throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
_frames[i] = state.GetFrames(RsiDirection.South);
_frameDelays[i] = state.GetDelays();
_frameCounter[i] = 0;
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
for (var i = 0; i < FireStates; i++)
{
var delays = _frameDelays[i];
if (delays.Length == 0)
continue;
var frameCount = _frameCounter[i];
_timer[i] += args.DeltaSeconds;
var time = delays[frameCount];
if (_timer[i] < time) continue;
_timer[i] -= time;
_frameCounter[i] = (frameCount + 1) % _frames[i].Length;
}
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return;
var drawHandle = args.WorldHandle;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
var gridState = (args.WorldBounds,
args.WorldHandle,
_frames,
_frameCounter,
_shader,
overlayQuery,
xformQuery,
_xformSys);
var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
if (args.Space != OverlaySpace.WorldSpaceEntities)
return;
// TODO: WorldBounds callback.
_mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState,
static (EntityUid uid, MapGridComponent grid,
ref (Box2Rotated WorldBounds,
DrawingHandleWorld drawHandle,
Texture[][] frames,
int[] frameCounter,
ShaderInstance shader,
EntityQuery<GasTileOverlayComponent> overlayQuery,
EntityQuery<TransformComponent> xformQuery,
SharedTransformSystem xformSys) state) =>
{
if (!state.overlayQuery.TryGetComponent(uid, out var comp) ||
!state.xformQuery.TryGetComponent(uid, out var gridXform))
{
return true;
}
var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform);
state.drawHandle.SetTransform(worldMatrix);
var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.TileSize);
var localBounds = new Box2i(
(int)MathF.Floor(floatBounds.Left),
(int)MathF.Floor(floatBounds.Bottom),
(int)MathF.Ceiling(floatBounds.Right),
(int)MathF.Ceiling(floatBounds.Top));
// Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are
// ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls
// by chunk, even though its currently slower.
state.drawHandle.UseShader(state.shader);
foreach (var chunk in comp.Chunks.Values)
{
var enumerator = new GasChunkEnumerator(chunk);
while (enumerator.MoveNext(out var gas))
{
if (gas.FireState == 0)
continue;
var index = chunk.Origin + (enumerator.X, enumerator.Y);
if (!localBounds.Contains(index))
continue;
var fireState = gas.FireState - 1;
var texture = state.frames[fireState][state.frameCounter[fireState]];
state.drawHandle.DrawTexture(texture, index);
}
}
return true;
});
drawHandle.UseShader(null);
drawHandle.SetTransform(Matrix3x2.Identity);
}
}

View File

@ -0,0 +1,263 @@
using Content.Client.Atmos.EntitySystems;
using Content.Client.Graphics;
using Content.Client.Resources;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using System.Numerics;
using Color = Robust.Shared.Maths.Color;
using Texture = Robust.Client.Graphics.Texture;
namespace Content.Client.Atmos.Overlays;
/// <summary>
/// Overlay responsible for rendering heat distortion shader.
/// </summary>
public sealed class GasTileHeatBlurOverlay : Overlay
{
public override bool RequestScreenTexture { get; set; } = true;
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private static readonly ProtoId<ShaderPrototype> HeatOverlayShader = "HeatBlur";
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private readonly SharedTransformSystem _xformSys;
private readonly ShaderInstance _shader;
private readonly Texture _noiseTexture;
private readonly Texture _heatGradientTexture;
private List<Entity<MapGridComponent>> _intersectingGrids = new();
private readonly OverlayResourceCache<CachedResources> _resources = new();
// Overlay settings
private const float
ShaderSpilling = 2.5f; // for example 4f - spills shader one tile from hotspot, 2.5f - spills it half tile
private const float ShaderStrength = 0.04f; // Makes waves stronger
private const float ShaderScale = 1f; // Makes more waves
private const float ShaderSpeed = 0.4f; // Makes waves run faster
// Overlay settings for reduced motion setting
private const float ShaderStrengthForReducedMotion = 0.01f;
private const float ShaderScaleReducedMotion = 0.5f;
private const float ShaderSpeedReducedMotion = 0.25f;
private const int MinDistortionTemp = 300; // Distortion starts to show up at this temperature in Kelvins
private const int MaxDistortionTemp = 2000; // Maximum distortion strength at this temperature in Kelvins
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public GasTileHeatBlurOverlay()
{
IoCManager.InjectDependencies(this);
_xformSys = _entManager.System<SharedTransformSystem>();
_noiseTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/perlin_noise.png");
_heatGradientTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/soft_circle.png");
_shader = _proto.Index(HeatOverlayShader).InstanceUnique();
_configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true);
}
private void SetReducedMotion(bool reducedMotion)
{
_shader.SetParameter("strength_scale", reducedMotion ? ShaderStrengthForReducedMotion : ShaderStrength);
_shader.SetParameter("spatial_scale", reducedMotion ? ShaderScaleReducedMotion : ShaderScale);
_shader.SetParameter("speed_scale", reducedMotion ? ShaderSpeedReducedMotion : ShaderSpeed);
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return false;
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
var target = args.Viewport.RenderTarget;
// Probably the resolution of the game window changed, remake the textures.
if (res.HeatTarget?.Texture.Size != target.Size)
{
res.HeatTarget?.Dispose();
res.HeatTarget = _clyde.CreateRenderTarget(
target.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: nameof(GasTileHeatBlurOverlaySystem));
}
if (res.HeatBlurTarget?.Texture.Size != target.Size)
{
res.HeatBlurTarget?.Dispose();
res.HeatBlurTarget = _clyde.CreateRenderTarget(
target.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: $"{nameof(GasTileHeatBlurOverlaySystem)}-blur");
}
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
var mapId = args.MapId;
var worldAABB = args.WorldAABB;
var worldBounds = args.WorldBounds;
var worldHandle = args.WorldHandle;
var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
// If there is no distortion after checking all visible tiles, we can bail early
var anyDistortion = false;
// We're rendering in the context of the heat target texture, which will encode data as to where and how strong
// the heat distortion will be
args.WorldHandle.RenderInRenderTarget(res.HeatTarget,
() =>
{
_intersectingGrids.Clear();
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids);
foreach (var grid in _intersectingGrids)
{
if (!overlayQuery.TryGetComponent(grid.Owner, out var comp))
continue;
var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner);
var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt))
continue;
var uvToUi = Matrix3Helpers.CreateScale(res.HeatTarget.Size.X, -res.HeatTarget.Size.Y);
var uvToGridEnt = uvToUi * viewportLocalToGridEnt;
// Because we want the actual distortion to be calculated based on the grid coordinates*, we need
// to pass a matrix transformation to go from the viewport coordinates to grid coordinates.
// * (why? because otherwise the effect would shimmer like crazy as you moved around, think
// moving a piece of warped glass above a picture instead of placing the warped glass on the
// paper and moving them together)
_shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt);
// Draw commands (like DrawRect) will be using grid coordinates from here
worldHandle.SetTransform(gridEntToViewportLocal);
// We only care about tiles that fit in these bounds
var worldToGridLocal = _xformSys.GetInvWorldMatrix(grid.Owner);
var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
var localBounds = new Box2i(
(int)MathF.Floor(floatBounds.Left),
(int)MathF.Floor(floatBounds.Bottom),
(int)MathF.Ceiling(floatBounds.Right),
(int)MathF.Ceiling(floatBounds.Top));
// for each tile and its gas --->
foreach (var chunk in comp.Chunks.Values)
{
var enumerator = new GasChunkEnumerator(chunk);
while (enumerator.MoveNext(out var tileGas))
{
// Check and make sure the tile is within the viewport/screen
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
if (!localBounds.Contains(tilePosition))
continue;
// Get the distortion strength from the temperature and bail if it's not hot enough
var strength = GetHeatDistortionStrength(tileGas.ByteGasTemperature);
if (strength <= 0f)
continue;
anyDistortion = true;
// Encode the strength in the red channel
// alpha set to 1 as tile is active
worldHandle.DrawTextureRect(
_heatGradientTexture,
Box2.CenteredAround(tilePosition + grid.Comp.TileSizeHalfVector,
grid.Comp.TileSizeVector * ShaderSpilling),
new Color(strength, 0f, 0f));
}
}
}
},
// This clears the buffer to all zero first...
new Color(0, 0, 0, 0));
// no distortion, no need to render
if (!anyDistortion)
{
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
return false;
}
return true;
}
protected override void Draw(in OverlayDrawArgs args)
{
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (ScreenTexture is null || res.HeatTarget is null || res.HeatBlurTarget is null)
return;
_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_shader.SetParameter("NOISE_TEXTURE", _noiseTexture);
args.WorldHandle.UseShader(_shader);
args.WorldHandle.DrawTextureRect(res.HeatTarget.Texture, args.WorldBounds);
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
protected override void DisposeBehavior()
{
_resources.Dispose();
_configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion);
base.DisposeBehavior();
}
/// <summary>
/// Gets the strength of the heat distortion effect based on the temperature of the tile.
/// The strength is a value between 0 and 1, where 0 means no distortion and 1 means maximum distortion.
/// </summary>
/// <param name="temp">The temperature of the tile.</param>
/// <returns>The strength of the heat distortion effect.</returns>
/// <seealso cref="ThermalByte"/>
private static float GetHeatDistortionStrength(ThermalByte temp)
{
if (!temp.TryGetTemperature(out var kelvinTemp))
{
return 0f;
}
var strength = (kelvinTemp - MinDistortionTemp) / (MaxDistortionTemp - MinDistortionTemp);
return MathHelper.Clamp01(strength);
}
internal sealed class CachedResources : IDisposable
{
public IRenderTexture? HeatTarget;
public IRenderTexture? HeatBlurTarget;
public void Dispose()
{
HeatTarget?.Dispose();
HeatBlurTarget?.Dispose();
}
}
}

View File

@ -1,302 +0,0 @@
using System.Numerics;
using Content.Client.Atmos.Components;
using Content.Client.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Atmos.Overlays
{
public sealed class GasTileOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
private readonly SharedAtmosphereSystem _atmosphereSystem;
private readonly SharedMapSystem _mapSystem;
private readonly SharedTransformSystem _xformSys;
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
private readonly ShaderInstance _shader;
// Gas overlays
private readonly float[] _timer;
private readonly float[][] _frameDelays;
private readonly int[] _frameCounter;
// TODO combine textures into a single texture atlas.
private readonly Texture[][] _frames;
// Fire overlays
private const int FireStates = 3;
private const string FireRsiPath = "/Textures/Effects/fire.rsi";
private readonly float[] _fireTimer = new float[FireStates];
private readonly float[][] _fireFrameDelays = new float[FireStates][];
private readonly int[] _fireFrameCounter = new int[FireStates];
private readonly Texture[][] _fireFrames = new Texture[FireStates][];
private int _gasCount;
public const int GasOverlayZIndex = (int) Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else
public GasTileOverlay(GasTileOverlaySystem system, IEntityManager entManager, IResourceCache resourceCache, IPrototypeManager protoMan, SpriteSystem spriteSys, SharedTransformSystem xformSys)
{
_entManager = entManager;
_mapManager = IoCManager.Resolve<IMapManager>();
_atmosphereSystem = entManager.System<SharedAtmosphereSystem>();
_mapSystem = entManager.System<SharedMapSystem>();
_xformSys = xformSys;
_shader = protoMan.Index(UnshadedShader).Instance();
ZIndex = GasOverlayZIndex;
_gasCount = system.VisibleGasId.Length;
_timer = new float[_gasCount];
_frameDelays = new float[_gasCount][];
_frameCounter = new int[_gasCount];
_frames = new Texture[_gasCount][];
for (var i = 0; i < _gasCount; i++)
{
var gasPrototype = _atmosphereSystem.GetGas(system.VisibleGasId[i]);
SpriteSpecifier overlay;
if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState))
overlay = new SpriteSpecifier.Rsi(new (gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState);
else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture))
overlay = new SpriteSpecifier.Texture(new (gasPrototype.GasOverlayTexture));
else
continue;
switch (overlay)
{
case SpriteSpecifier.Rsi animated:
var rsi = resourceCache.GetResource<RSIResource>(animated.RsiPath).RSI;
var stateId = animated.RsiState;
if (!rsi.TryGetState(stateId, out var state))
continue;
_frames[i] = state.GetFrames(RsiDirection.South);
_frameDelays[i] = state.GetDelays();
_frameCounter[i] = 0;
break;
case SpriteSpecifier.Texture texture:
_frames[i] = new[] { spriteSys.Frame0(texture) };
_frameDelays[i] = Array.Empty<float>();
break;
}
}
var fire = resourceCache.GetResource<RSIResource>(FireRsiPath).RSI;
for (var i = 0; i < FireStates; i++)
{
if (!fire.TryGetState((i + 1).ToString(), out var state))
throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
_fireFrames[i] = state.GetFrames(RsiDirection.South);
_fireFrameDelays[i] = state.GetDelays();
_fireFrameCounter[i] = 0;
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
for (var i = 0; i < _gasCount; i++)
{
var delays = _frameDelays[i];
if (delays.Length == 0)
continue;
var frameCount = _frameCounter[i];
_timer[i] += args.DeltaSeconds;
var time = delays[frameCount];
if (_timer[i] < time)
continue;
_timer[i] -= time;
_frameCounter[i] = (frameCount + 1) % _frames[i].Length;
}
for (var i = 0; i < FireStates; i++)
{
var delays = _fireFrameDelays[i];
if (delays.Length == 0)
continue;
var frameCount = _fireFrameCounter[i];
_fireTimer[i] += args.DeltaSeconds;
var time = delays[frameCount];
if (_fireTimer[i] < time) continue;
_fireTimer[i] -= time;
_fireFrameCounter[i] = (frameCount + 1) % _fireFrames[i].Length;
}
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return;
var drawHandle = args.WorldHandle;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
var gridState = (args.WorldBounds,
args.WorldHandle,
_gasCount,
_frames,
_frameCounter,
_fireFrames,
_fireFrameCounter,
_shader,
overlayQuery,
xformQuery,
_xformSys);
var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
if (_entManager.TryGetComponent<MapAtmosphereComponent>(mapUid, out var atmos))
DrawMapOverlay(drawHandle, args, mapUid, atmos);
if (args.Space != OverlaySpace.WorldSpaceEntities)
return;
// TODO: WorldBounds callback.
_mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState,
static (EntityUid uid, MapGridComponent grid,
ref (Box2Rotated WorldBounds,
DrawingHandleWorld drawHandle,
int gasCount,
Texture[][] frames,
int[] frameCounter,
Texture[][] fireFrames,
int[] fireFrameCounter,
ShaderInstance shader,
EntityQuery<GasTileOverlayComponent> overlayQuery,
EntityQuery<TransformComponent> xformQuery,
SharedTransformSystem xformSys) state) =>
{
if (!state.overlayQuery.TryGetComponent(uid, out var comp) ||
!state.xformQuery.TryGetComponent(uid, out var gridXform))
{
return true;
}
var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform);
state.drawHandle.SetTransform(worldMatrix);
var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.TileSize);
var localBounds = new Box2i(
(int) MathF.Floor(floatBounds.Left),
(int) MathF.Floor(floatBounds.Bottom),
(int) MathF.Ceiling(floatBounds.Right),
(int) MathF.Ceiling(floatBounds.Top));
// Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are
// ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls
// by chunk, even though its currently slower.
state.drawHandle.UseShader(null);
foreach (var chunk in comp.Chunks.Values)
{
var enumerator = new GasChunkEnumerator(chunk);
while (enumerator.MoveNext(out var gas))
{
if (gas.Opacity == null!)
continue;
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
if (!localBounds.Contains(tilePosition))
continue;
for (var i = 0; i < state.gasCount; i++)
{
var opacity = gas.Opacity[i];
if (opacity > 0)
state.drawHandle.DrawTexture(state.frames[i][state.frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
}
}
}
// And again for fire, with the unshaded shader
state.drawHandle.UseShader(state.shader);
foreach (var chunk in comp.Chunks.Values)
{
var enumerator = new GasChunkEnumerator(chunk);
while (enumerator.MoveNext(out var gas))
{
if (gas.FireState == 0)
continue;
var index = chunk.Origin + (enumerator.X, enumerator.Y);
if (!localBounds.Contains(index))
continue;
var fireState = gas.FireState - 1;
var texture = state.fireFrames[fireState][state.fireFrameCounter[fireState]];
state.drawHandle.DrawTexture(texture, index);
}
}
return true;
});
drawHandle.UseShader(null);
drawHandle.SetTransform(Matrix3x2.Identity);
}
private void DrawMapOverlay(
DrawingHandleWorld handle,
OverlayDrawArgs args,
EntityUid map,
MapAtmosphereComponent atmos)
{
var mapGrid = _entManager.HasComponent<MapGridComponent>(map);
// map-grid atmospheres get drawn above grids
if (mapGrid && args.Space != OverlaySpace.WorldSpaceEntities)
return;
// Normal map atmospheres get drawn below grids
if (!mapGrid && args.Space != OverlaySpace.WorldSpaceBelowWorld)
return;
var bottomLeft = args.WorldAABB.BottomLeft.Floored();
var topRight = args.WorldAABB.TopRight.Ceiled();
for (var x = bottomLeft.X; x <= topRight.X; x++)
{
for (var y = bottomLeft.Y; y <= topRight.Y; y++)
{
var tilePosition = new Vector2(x, y);
for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++)
{
var opacity = atmos.OverlayData.Opacity[i];
if (opacity > 0)
handle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
}
}
}
}
}
}

View File

@ -0,0 +1,248 @@
using Content.Client.Atmos.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Numerics;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Atmos.Overlays;
/// <summary>
/// Overlay responsible for rendering visible atmos gasses (like plasma for example) usin.
/// </summary>
public sealed class GasTileVisibleGasOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private readonly SharedAtmosphereSystem _atmosphereSystem;
private readonly SharedMapSystem _mapSystem;
private readonly SharedTransformSystem _xformSys;
private readonly SharedGasTileOverlaySystem _gasTileOverlaySystem;
private readonly SpriteSystem _spriteSystem;
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
private readonly ShaderInstance _shader;
// Gas overlays
private readonly float[] _timer;
private readonly float[][] _frameDelays;
private readonly int[] _frameCounter;
// TODO combine textures into a single texture atlas.
private readonly Texture[][] _frames;
private readonly int _gasCount;
public const int GasOverlayZIndex = (int)DrawDepth.Gasses; // Under ghosts and fire, above mostly everything else
public GasTileVisibleGasOverlay()
{
IoCManager.InjectDependencies(this);
_atmosphereSystem = _entManager.System<SharedAtmosphereSystem>();
_mapSystem = _entManager.System<SharedMapSystem>();
_xformSys = _entManager.System<SharedTransformSystem>();
_gasTileOverlaySystem = _entManager.System<SharedGasTileOverlaySystem>();
_spriteSystem = _entManager.System<SpriteSystem>();
_shader = _protoManager.Index(UnshadedShader).Instance();
ZIndex = GasOverlayZIndex;
_gasCount = _gasTileOverlaySystem.VisibleGasId.Length;
_timer = new float[_gasCount];
_frameDelays = new float[_gasCount][];
_frameCounter = new int[_gasCount];
_frames = new Texture[_gasCount][];
for (var i = 0; i < _gasCount; i++)
{
var gasPrototype = _atmosphereSystem.GetGas(_gasTileOverlaySystem.VisibleGasId[i]);
switch (gasPrototype.GasOverlaySprite)
{
case SpriteSpecifier.Rsi animated:
var rsi = _resourceCache.GetResource<RSIResource>(animated.RsiPath).RSI;
var stateId = animated.RsiState;
if (!rsi.TryGetState(stateId, out var state))
continue;
_frames[i] = state.GetFrames(RsiDirection.South);
_frameDelays[i] = state.GetDelays();
_frameCounter[i] = 0;
break;
case SpriteSpecifier.Texture texture:
_frames[i] = new[] { _spriteSystem.Frame0(texture) };
_frameDelays[i] = Array.Empty<float>();
break;
}
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
for (var i = 0; i < _gasCount; i++)
{
var delays = _frameDelays[i];
if (delays.Length == 0)
continue;
var frameCount = _frameCounter[i];
_timer[i] += args.DeltaSeconds;
var time = delays[frameCount];
if (_timer[i] < time)
continue;
_timer[i] -= time;
_frameCounter[i] = (frameCount + 1) % _frames[i].Length;
}
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return;
var drawHandle = args.WorldHandle;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
var gridState = (args.WorldBounds,
args.WorldHandle,
_gasCount,
_frames,
_frameCounter,
_shader,
overlayQuery,
xformQuery,
_xformSys);
var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
if (_entManager.TryGetComponent<MapAtmosphereComponent>(mapUid, out var atmos))
DrawMapOverlay(drawHandle, args, mapUid, atmos);
if (args.Space != OverlaySpace.WorldSpaceEntities)
return;
// TODO: WorldBounds callback.
_mapManager.FindGridsIntersecting(args.MapId,
args.WorldAABB,
ref gridState,
static (EntityUid uid,
MapGridComponent grid,
ref (Box2Rotated WorldBounds,
DrawingHandleWorld drawHandle,
int gasCount,
Texture[][] frames,
int[] frameCounter,
ShaderInstance shader,
EntityQuery<GasTileOverlayComponent> overlayQuery,
EntityQuery<TransformComponent> xformQuery,
SharedTransformSystem xformSys) state) =>
{
if (!state.overlayQuery.TryGetComponent(uid, out var comp) ||
!state.xformQuery.TryGetComponent(uid, out var gridXform))
{
return true;
}
var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform);
state.drawHandle.SetTransform(worldMatrix);
var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.TileSize);
var localBounds = new Box2i(
(int)MathF.Floor(floatBounds.Left),
(int)MathF.Floor(floatBounds.Bottom),
(int)MathF.Ceiling(floatBounds.Right),
(int)MathF.Ceiling(floatBounds.Top));
// Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are
// ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls
// by chunk, even though its currently slower.
state.drawHandle.UseShader(null);
foreach (var chunk in comp.Chunks.Values)
{
var enumerator = new GasChunkEnumerator(chunk);
while (enumerator.MoveNext(out var gas))
{
if (gas.Opacity == null!)
continue;
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
if (!localBounds.Contains(tilePosition))
continue;
for (var i = 0; i < state.gasCount; i++)
{
var opacity = gas.Opacity[i];
if (opacity > 0)
{
state.drawHandle.DrawTexture(state.frames[i][state.frameCounter[i]],
tilePosition,
Color.White.WithAlpha(opacity));
}
}
}
}
return true;
});
drawHandle.UseShader(null);
drawHandle.SetTransform(Matrix3x2.Identity);
}
private void DrawMapOverlay(
DrawingHandleWorld handle,
OverlayDrawArgs args,
EntityUid map,
MapAtmosphereComponent atmos)
{
var mapGrid = _entManager.HasComponent<MapGridComponent>(map);
// map-grid atmospheres get drawn above grids
if (mapGrid && args.Space != OverlaySpace.WorldSpaceEntities)
return;
// Normal map atmospheres get drawn below grids
if (!mapGrid && args.Space != OverlaySpace.WorldSpaceBelowWorld)
return;
var bottomLeft = args.WorldAABB.BottomLeft.Floored();
var topRight = args.WorldAABB.TopRight.Ceiled();
for (var x = bottomLeft.X; x <= topRight.X; x++)
{
for (var y = bottomLeft.Y; y <= topRight.Y; y++)
{
var tilePosition = new Vector2(x, y);
for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++)
{
var opacity = atmos.OverlayData.Opacity[i];
if (opacity > 0)
handle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
}
}
}
}
}

View File

@ -1,41 +1,33 @@
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
using Content.Shared.Atmos.Components;
namespace Content.Client.Atmos.UI
namespace Content.Client.Atmos.UI;
public sealed class GasAnalyzerBoundUserInterface : BoundUserInterface
{
public sealed class GasAnalyzerBoundUserInterface : BoundUserInterface
[ViewVariables]
private GasAnalyzerWindow? _window;
public GasAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
[ViewVariables]
private GasAnalyzerWindow? _window;
}
public GasAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
protected override void Open()
{
base.Open();
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
_window.OnClose += Close;
}
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
_window.OnClose += Close;
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (_window == null)
return;
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (_window == null)
return;
if (message is not GasAnalyzerUserMessage cast)
return;
_window.Populate(cast);
}
if (message is not GasAnalyzerUserMessage cast)
return;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_window?.Dispose();
}
_window.Populate(cast);
}
}

View File

@ -1,6 +1,8 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Temperature;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@ -8,7 +10,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Atmos.UI
@ -16,25 +17,17 @@ namespace Content.Client.Atmos.UI
[GenerateTypedNameReferences]
public sealed partial class GasAnalyzerWindow : DefaultWindow
{
private readonly SharedAtmosphereSystem _atmosphere;
private NetEntity _currentEntity = NetEntity.Invalid;
public GasAnalyzerWindow()
{
RobustXamlLoader.Load(this);
_atmosphere = IoCManager.Resolve<IEntityManager>().System<SharedAtmosphereSystem>();
}
public void Populate(GasAnalyzerUserMessage msg)
{
if (msg.Error != null)
{
CTopBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-error-text", ("errorText", msg.Error)),
FontColorOverride = Color.Red
});
return;
}
if (msg.NodeGasMixes.Length == 0)
{
CTopBox.AddChild(new Label
@ -329,31 +322,31 @@ namespace Content.Client.Atmos.UI
for (var j = 0; j < gasMix.Gases.Length; j++)
{
var gas = gasMix.Gases[j];
var color = Color.FromHex($"#{gas.Color}", Color.White);
var gasEntry = gasMix.Gases[j];
var gasProto = _atmosphere.GetGas(gasEntry.Gas);
// Add to the table
tableKey.AddChild(new Label
{
Text = Loc.GetString(gas.Name)
Text = Loc.GetString(gasProto.Name)
});
tableVal.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-molarity-text",
("mol", $"{gas.Amount:0.00}")),
("mol", $"{gasEntry.Amount:0.00}")),
Align = Label.AlignMode.Right,
});
tablePercent.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-percentage-text",
("percentage", $"{(gas.Amount / totalGasAmount * 100):0.0}")),
("percentage", $"{(gasEntry.Amount / totalGasAmount * 100):0.0}")),
Align = Label.AlignMode.Right
});
// Add to the gas bar //TODO: highlight the currently hover one
gasBar.AddEntry(gas.Amount, color, tooltip: Loc.GetString("gas-analyzer-window-molarity-percentage-text",
("gasName", gas.Name),
("amount", $"{gas.Amount:0.##}"),
("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")));
gasBar.AddEntry(gasEntry.Amount, gasProto.Color, tooltip: Loc.GetString("gas-analyzer-window-molarity-percentage-text",
("gasName", Loc.GetString(gasProto.Name)),
("amount", $"{gasEntry.Amount:0.##}"),
("percentage", $"{(gasEntry.Amount / totalGasAmount * 100):0.#}")));
}
dataContainer.AddChild(gasBar);

View File

@ -37,10 +37,9 @@ namespace Content.Client.Atmos.UI
_window.SelectGasPressed += OnSelectGasPressed;
}
private void OnToggleStatusButtonPressed()
private void OnToggleStatusButtonPressed(bool status)
{
if (_window is null) return;
SendMessage(new GasFilterToggleStatusMessage(_window.FilterStatus));
SendMessage(new GasFilterToggleStatusMessage(status));
}
private void OnFilterTransferRatePressed(string value)

View File

@ -1,11 +1,12 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="480 400" Title="Filter">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc comp-gas-filter-ui-filter-status}"/>
<Button Name="ToggleStatusButton"/>
</BoxContainer>
<controls:SwitchButton
Name="ToggleStatusButton"
HorizontalAlignment="Left"
Pressed="True" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc comp-gas-filter-ui-filter-transfer-rate}"/>

View File

@ -18,11 +18,10 @@ namespace Content.Client.Atmos.UI
{
private readonly ButtonGroup _buttonGroup = new();
public bool FilterStatus = true;
public string? SelectedGas;
public string? CurrentGasId;
public event Action? ToggleStatusButtonPressed;
public event Action<bool>? ToggleStatusButtonPressed;
public event Action<string>? FilterTransferRateChanged;
public event Action? SelectGasPressed;
@ -30,8 +29,7 @@ namespace Content.Client.Atmos.UI
{
RobustXamlLoader.Load(this);
ToggleStatusButton.OnPressed += _ => SetFilterStatus(!FilterStatus);
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
ToggleStatusButton.OnToggled += _ => ToggleStatusButtonPressed?.Invoke(ToggleStatusButton.Pressed);
FilterTransferRateInput.OnTextChanged += _ => SetFilterRate.Disabled = false;
SetFilterRate.OnPressed += _ =>
@ -53,15 +51,7 @@ namespace Content.Client.Atmos.UI
public void SetFilterStatus(bool enabled)
{
FilterStatus = enabled;
if (enabled)
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-filter-ui-status-enabled");
}
else
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-filter-ui-status-disabled");
}
ToggleStatusButton.Pressed = enabled;
}
public void SetGasFiltered(string? id, string name)

View File

@ -33,10 +33,9 @@ namespace Content.Client.Atmos.UI
_window.MixerNodePercentageChanged += OnMixerSetPercentagePressed;
}
private void OnToggleStatusButtonPressed()
private void OnToggleStatusButtonPressed(bool status)
{
if (_window is null) return;
SendMessage(new GasMixerToggleStatusMessage(_window.MixerStatus));
SendMessage(new GasMixerToggleStatusMessage(status));
}
private void OnMixerOutputPressurePressed(string value)

View File

@ -1,12 +1,12 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="200 200" Title="Gas Mixer">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc comp-gas-mixer-ui-mixer-status}"/>
<Control MinSize="5 0" />
<Button Name="ToggleStatusButton"/>
</BoxContainer>
<controls:SwitchButton
Name="ToggleStatusButton"
HorizontalAlignment="Left"
Pressed="True" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc comp-gas-mixer-ui-mixer-output-pressure}"/>
<Control MinSize="5 0" />

View File

@ -21,9 +21,7 @@ namespace Content.Client.Atmos.UI
[GenerateTypedNameReferences]
public sealed partial class GasMixerWindow : DefaultWindow
{
public bool MixerStatus = true;
public event Action? ToggleStatusButtonPressed;
public event Action<bool>? ToggleStatusButtonPressed;
public event Action<string>? MixerOutputPressureChanged;
public event Action<string>? MixerNodePercentageChanged;
@ -33,8 +31,7 @@ namespace Content.Client.Atmos.UI
{
RobustXamlLoader.Load(this);
ToggleStatusButton.OnPressed += _ => SetMixerStatus(!MixerStatus);
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
ToggleStatusButton.OnToggled += _ => ToggleStatusButtonPressed?.Invoke(ToggleStatusButton.Pressed);
MixerPressureOutputInput.OnTextChanged += _ => SetOutputPressureButton.Disabled = false;
SetOutputPressureButton.OnPressed += _ =>
@ -83,15 +80,7 @@ namespace Content.Client.Atmos.UI
public void SetMixerStatus(bool enabled)
{
MixerStatus = enabled;
if (enabled)
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-mixer-ui-status-enabled");
}
else
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-mixer-ui-status-disabled");
}
ToggleStatusButton.Pressed = enabled;
}
}
}

View File

@ -42,12 +42,9 @@ public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKe
_window.SetOutputPressure(pump.TargetPressure);
}
private void OnToggleStatusButtonPressed()
private void OnToggleStatusButtonPressed(bool status)
{
if (_window is null)
return;
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(status));
}
private void OnPumpOutputPressurePressed(float value)

View File

@ -4,8 +4,10 @@
SetSize="340 110" MinSize="340 110" Title="Pressure Pump">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc comp-gas-pump-ui-pump-status}" Margin="0 0 5 0"/>
<Button Name="ToggleStatusButton"/>
<controls:SwitchButton
Name="ToggleStatusButton"
HorizontalAlignment="Left"
Pressed="True" />
<Control HorizontalExpand="True"/>
<Button HorizontalAlignment="Right" Name="SetOutputPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" Disabled="True" Margin="0 0 5 0"/>
<Button Name="SetMaxPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-max}" />

View File

@ -11,9 +11,7 @@ namespace Content.Client.Atmos.UI
[GenerateTypedNameReferences]
public sealed partial class GasPressurePumpWindow : FancyWindow
{
public bool PumpStatus = true;
public event Action? ToggleStatusButtonPressed;
public event Action<bool>? ToggleStatusButtonPressed;
public event Action<float>? PumpOutputPressureChanged;
public float MaxPressure
@ -33,8 +31,7 @@ namespace Content.Client.Atmos.UI
{
RobustXamlLoader.Load(this);
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
ToggleStatusButton.OnToggled += _ => ToggleStatusButtonPressed?.Invoke(ToggleStatusButton.Pressed);
PumpPressureOutputInput.OnValueChanged += _ => SetOutputPressureButton.Disabled = false;
@ -59,15 +56,7 @@ namespace Content.Client.Atmos.UI
public void SetPumpStatus(bool enabled)
{
PumpStatus = enabled;
if (enabled)
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-pump-ui-status-enabled");
}
else
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-pump-ui-status-disabled");
}
ToggleStatusButton.Pressed = enabled;
}
}
}

View File

@ -37,7 +37,7 @@ namespace Content.Client.Atmos.UI
_window = this.CreateWindow<GasThermomachineWindow>();
_window.ToggleStatusButton.OnPressed += _ => OnToggleStatusButtonPressed();
_window.ToggleStatusButton.OnToggled += _ => OnToggleStatusButtonPressed();
_window.TemperatureSpinbox.OnValueChanged += _ => OnTemperatureChanged(_window.TemperatureSpinbox.Value);
_window.Entity = Owner;
Update();
@ -45,9 +45,6 @@ namespace Content.Client.Atmos.UI
private void OnToggleStatusButtonPressed()
{
if (_window is null) return;
_window.SetActive(!_window.Active);
SendPredictedMessage(new GasThermomachineToggleMessage());
}

View File

@ -3,11 +3,11 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="300 120" Title="{Loc comp-gas-thermomachine-ui-title-freezer}">
<BoxContainer Name="VboxContainer" Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc comp-gas-thermomachine-ui-toggle}"/>
<Control MinSize="5 0" />
<Button Access="Public" Name="ToggleStatusButton"/>
</BoxContainer>
<controls:SwitchButton
Name="ToggleStatusButton"
HorizontalAlignment="Left"
Pressed="True"
Access="Public" />
<BoxContainer Name="SpinboxHBox" Orientation="Horizontal">
<Label Text="{Loc comp-gas-thermomachine-ui-temperature}"/>
</BoxContainer>

View File

@ -12,8 +12,6 @@ public sealed partial class GasThermomachineWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
public bool Active = true;
public FloatSpinBox TemperatureSpinbox;
public EntityUid Entity;
@ -30,15 +28,7 @@ public sealed partial class GasThermomachineWindow : FancyWindow
public void SetActive(bool active)
{
Active = active;
if (active)
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-thermomachine-ui-status-enabled");
}
else
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-thermomachine-ui-status-disabled");
}
ToggleStatusButton.Pressed = active;
}
public void SetTemperature(float temperature)

View File

@ -38,11 +38,9 @@ namespace Content.Client.Atmos.UI
Update();
}
private void OnToggleStatusButtonPressed()
private void OnToggleStatusButtonPressed(bool status)
{
if (_window is null) return;
SendPredictedMessage(new GasVolumePumpToggleStatusMessage(_window.PumpStatus));
SendPredictedMessage(new GasVolumePumpToggleStatusMessage(status));
}
private void OnPumpTransferRatePressed(string value)

View File

@ -3,11 +3,10 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="200 120" Title="Volume Pump">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc comp-gas-pump-ui-pump-status}"/>
<Control MinSize="5 0" />
<Button Name="ToggleStatusButton"/>
</BoxContainer>
<controls:SwitchButton
Name="ToggleStatusButton"
HorizontalAlignment="Left"
Pressed="True" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc comp-gas-pump-ui-pump-transfer-rate}"/>

View File

@ -19,17 +19,14 @@ namespace Content.Client.Atmos.UI
[GenerateTypedNameReferences]
public sealed partial class GasVolumePumpWindow : FancyWindow
{
public bool PumpStatus = true;
public event Action? ToggleStatusButtonPressed;
public event Action<bool>? ToggleStatusButtonPressed;
public event Action<string>? PumpTransferRateChanged;
public GasVolumePumpWindow()
{
RobustXamlLoader.Load(this);
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
ToggleStatusButton.OnToggled += _ => ToggleStatusButtonPressed?.Invoke(ToggleStatusButton.Pressed);
PumpTransferRateInput.OnTextChanged += _ => SetTransferRateButton.Disabled = false;
SetTransferRateButton.OnPressed += _ =>
@ -52,15 +49,7 @@ namespace Content.Client.Atmos.UI
public void SetPumpStatus(bool enabled)
{
PumpStatus = enabled;
if (enabled)
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-pump-ui-status-enabled");
}
else
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-pump-ui-status-disabled");
}
ToggleStatusButton.Pressed = enabled;
}
}
}

View File

@ -24,7 +24,7 @@ public sealed class SpaceHeaterBoundUserInterface : BoundUserInterface
_window = this.CreateWindow<SpaceHeaterWindow>();
_window.ToggleStatusButton.OnPressed += _ => OnToggleStatusButtonPressed();
_window.ToggleStatusButton.OnToggled += _ => OnToggleStatusButtonPressed();
_window.IncreaseTempRange.OnPressed += _ => OnTemperatureRangeChanged(_window.TemperatureChangeDelta);
_window.DecreaseTempRange.OnPressed += _ => OnTemperatureRangeChanged(-_window.TemperatureChangeDelta);
_window.ModeSelector.OnItemSelected += OnModeChanged;
@ -34,7 +34,6 @@ public sealed class SpaceHeaterBoundUserInterface : BoundUserInterface
private void OnToggleStatusButtonPressed()
{
_window?.SetActive(!_window.Active);
SendMessage(new SpaceHeaterToggleMessage());
}

View File

@ -1,13 +1,15 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="280 160" Title="{Loc comp-space-heater-ui-title}">
<BoxContainer Name="VboxContainer" Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Text="{Loc comp-space-heater-ui-status-disabled}" Access="Public" Name="ToggleStatusButton"/>
</BoxContainer>
<controls:SwitchButton
Name="ToggleStatusButton"
HorizontalExpand="True"
Access="Public" />
<BoxContainer Orientation="Horizontal" SeparationOverride="5">
<Label Text="{Loc comp-space-heater-ui-mode}"/>
<OptionButton Access="Public" Name="ModeSelector"/>

View File

@ -15,7 +15,6 @@ public sealed partial class SpaceHeaterWindow : DefaultWindow
{
// To account for a minimum delta temperature for atmos equalization to trigger we use a fixed step for target temperature increment/decrement
public int TemperatureChangeDelta = 5;
public bool Active;
// Temperatures range bounds in Kelvin (K)
public float MinTemp;
@ -49,17 +48,7 @@ public sealed partial class SpaceHeaterWindow : DefaultWindow
public void SetActive(bool active)
{
Active = active;
ToggleStatusButton.Pressed = active;
if (active)
{
ToggleStatusButton.Text = Loc.GetString("comp-space-heater-ui-status-enabled");
}
else
{
ToggleStatusButton.Text = Loc.GetString("comp-space-heater-ui-status-disabled");
}
}
public void SetTemperature(float targetTemperature)

View File

@ -1,6 +1,7 @@
using System.Linq;
using Content.Shared.BarSign;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.BarSign.Ui;
@ -16,13 +17,12 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
{
base.Open();
var sign = EntMan.GetComponentOrNull<BarSignComponent>(Owner)?.Current is { } current
? _prototype.Index(current)
: null;
var allSigns = BarSignSystem.GetAllBarSigns(_prototype)
.OrderBy(p => Loc.GetString(p.Name))
.ToList();
_menu = new(sign, allSigns);
_menu = this.CreateWindow<BarSignMenu>();
_menu.LoadSigns(allSigns);
_menu.OnSignSelected += id =>
{
@ -30,16 +30,17 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
};
_menu.OnClose += Close;
_menu.OpenCentered();
_menu.OpenToLeft();
}
public override void Update()
{
if (!EntMan.TryGetComponent<BarSignComponent>(Owner, out var signComp))
if (!EntMan.TryGetComponent<BarSignComponent>(Owner, out var signComp)
|| !_prototype.Resolve(signComp.Current, out var signPrototype))
return;
if (_prototype.Resolve(signComp.Current, out var signPrototype))
_menu?.UpdateState(signPrototype);
_menu?.UpdateState(signPrototype);
}
}

View File

@ -8,23 +8,13 @@ namespace Content.Client.BarSign.Ui;
[GenerateTypedNameReferences]
public sealed partial class BarSignMenu : FancyWindow
{
private string? _currentId;
private readonly List<BarSignPrototype> _cachedPrototypes = new();
private List<BarSignPrototype> _cachedPrototypes = new();
public event Action<string>? OnSignSelected;
public BarSignMenu(BarSignPrototype? currentSign, List<BarSignPrototype> signs)
public BarSignMenu()
{
RobustXamlLoader.Load(this);
_currentId = currentSign?.ID;
_cachedPrototypes.Clear();
_cachedPrototypes = signs;
foreach (var proto in _cachedPrototypes)
{
SignOptions.AddItem(Loc.GetString(proto.Name));
}
SignOptions.OnItemSelected += idx =>
{
@ -32,18 +22,21 @@ public sealed partial class BarSignMenu : FancyWindow
SignOptions.SelectId(idx.Id);
};
if (currentSign != null)
}
public void LoadSigns(List<BarSignPrototype> signs)
{
_cachedPrototypes.Clear();
_cachedPrototypes = signs;
foreach (var proto in _cachedPrototypes)
{
var idx = _cachedPrototypes.IndexOf(currentSign);
SignOptions.TrySelectId(idx);
SignOptions.AddItem(Loc.GetString(proto.Name));
}
}
public void UpdateState(BarSignPrototype newSign)
{
if (_currentId != null && newSign.ID == _currentId)
return;
_currentId = newSign.ID;
var idx = _cachedPrototypes.IndexOf(newSign);
SignOptions.TrySelectId(idx);
}

View File

@ -1,7 +0,0 @@
using Content.Shared.Body.Systems;
namespace Content.Client.Body.Systems;
public sealed class BodySystem : SharedBodySystem
{
}

View File

@ -1,6 +0,0 @@
using Content.Shared.Body.Systems;
namespace Content.Client.Body.Systems;
/// <inheritdoc/>
public sealed class MetabolizerSystem : SharedMetabolizerSystem;

View File

@ -0,0 +1,264 @@
using System.Linq;
using Content.Shared.Body;
using Content.Shared.CCVar;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Body;
public sealed class VisualBodySystem : SharedVisualBodySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly MarkingManager _marking = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VisualOrganComponent, OrganGotInsertedEvent>(OnOrganGotInserted);
SubscribeLocalEvent<VisualOrganComponent, OrganGotRemovedEvent>(OnOrganGotRemoved);
SubscribeLocalEvent<VisualOrganComponent, AfterAutoHandleStateEvent>(OnOrganState);
SubscribeLocalEvent<VisualOrganMarkingsComponent, OrganGotInsertedEvent>(OnMarkingsGotInserted);
SubscribeLocalEvent<VisualOrganMarkingsComponent, OrganGotRemovedEvent>(OnMarkingsGotRemoved);
SubscribeLocalEvent<VisualOrganMarkingsComponent, AfterAutoHandleStateEvent>(OnMarkingsState);
SubscribeLocalEvent<VisualOrganMarkingsComponent, BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent>>(OnMarkingsChangedVisibility);
Subs.CVar(_cfg, CCVars.AccessibilityClientCensorNudity, OnCensorshipChanged, true);
Subs.CVar(_cfg, CCVars.AccessibilityServerCensorNudity, OnCensorshipChanged, true);
}
private void OnCensorshipChanged(bool value)
{
var query = AllEntityQuery<OrganComponent, VisualOrganMarkingsComponent>();
while (query.MoveNext(out var ent, out var organComp, out var markingsComp))
{
if (organComp.Body is not { } body)
continue;
RemoveMarkings((ent, markingsComp), body);
ApplyMarkings((ent, markingsComp), body);
}
}
private void OnOrganGotInserted(Entity<VisualOrganComponent> ent, ref OrganGotInsertedEvent args)
{
ApplyVisual(ent, args.Target);
}
private void OnOrganGotRemoved(Entity<VisualOrganComponent> ent, ref OrganGotRemovedEvent args)
{
RemoveVisual(ent, args.Target);
}
private void OnOrganState(Entity<VisualOrganComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (Comp<OrganComponent>(ent).Body is not { } body)
return;
ApplyVisual(ent, body);
}
private void ApplyVisual(Entity<VisualOrganComponent> ent, EntityUid target)
{
if (!_sprite.LayerMapTryGet(target, ent.Comp.Layer, out var index, true))
return;
_sprite.LayerSetData(target, index, ent.Comp.Data);
}
private void RemoveVisual(Entity<VisualOrganComponent> ent, EntityUid target)
{
if (!_sprite.LayerMapTryGet(target, ent.Comp.Layer, out var index, true))
return;
_sprite.LayerSetRsiState(target, index, RSI.StateId.Invalid);
}
private void OnMarkingsGotInserted(Entity<VisualOrganMarkingsComponent> ent, ref OrganGotInsertedEvent args)
{
ApplyMarkings(ent, args.Target);
}
private void OnMarkingsGotRemoved(Entity<VisualOrganMarkingsComponent> ent, ref OrganGotRemovedEvent args)
{
RemoveMarkings(ent, args.Target);
}
private void OnMarkingsState(Entity<VisualOrganMarkingsComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (Comp<OrganComponent>(ent).Body is not { } body)
return;
RemoveMarkings(ent, body);
ApplyMarkings(ent, body);
}
protected override void SetOrganColor(Entity<VisualOrganComponent> ent, Color color)
{
base.SetOrganColor(ent, color);
if (Comp<OrganComponent>(ent).Body is not { } body)
return;
ApplyVisual(ent, body);
}
protected override void SetOrganMarkings(Entity<VisualOrganMarkingsComponent> ent, Dictionary<HumanoidVisualLayers, List<Marking>> markings)
{
base.SetOrganMarkings(ent, markings);
if (Comp<OrganComponent>(ent).Body is not { } body)
return;
RemoveMarkings(ent, body);
ApplyMarkings(ent, body);
}
protected override void SetOrganAppearance(Entity<VisualOrganComponent> ent, PrototypeLayerData data)
{
base.SetOrganAppearance(ent, data);
if (Comp<OrganComponent>(ent).Body is not { } body)
return;
ApplyVisual(ent, body);
}
private IEnumerable<Marking> AllMarkings(Entity<VisualOrganMarkingsComponent> ent)
{
foreach (var markings in ent.Comp.Markings.Values)
{
foreach (var marking in markings)
{
yield return marking;
}
}
var censorNudity = _cfg.GetCVar(CCVars.AccessibilityClientCensorNudity) || _cfg.GetCVar(CCVars.AccessibilityServerCensorNudity);
if (!censorNudity)
yield break;
var group = _prototype.Index(ent.Comp.MarkingData.Group);
foreach (var layer in ent.Comp.MarkingData.Layers)
{
if (!group.Limits.TryGetValue(layer, out var layerLimits))
continue;
if (layerLimits.NudityDefault.Count < 1)
continue;
var markings = ent.Comp.Markings.GetValueOrDefault(layer) ?? [];
if (markings.Any(marking => _marking.TryGetMarking(marking, out var proto) && proto.BodyPart == layer))
continue;
foreach (var marking in layerLimits.NudityDefault)
{
yield return new(marking, 1);
}
}
}
private void ApplyMarkings(Entity<VisualOrganMarkingsComponent> ent, EntityUid target)
{
var applied = new List<Marking>();
foreach (var marking in AllMarkings(ent))
{
if (!_marking.TryGetMarking(marking, out var proto))
continue;
if (!_sprite.LayerMapTryGet(target, proto.BodyPart, out var index, true))
continue;
for (var i = 0; i < proto.Sprites.Count; i++)
{
var sprite = proto.Sprites[i];
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
if (sprite is not SpriteSpecifier.Rsi rsi)
continue;
var layerId = $"{proto.ID}-{rsi.RsiState}";
if (!_sprite.LayerMapTryGet(target, layerId, out _, false))
{
var layer = _sprite.AddLayer(target, sprite, index + i + 1);
_sprite.LayerMapSet(target, layerId, layer);
_sprite.LayerSetSprite(target, layerId, rsi);
}
if (marking.MarkingColors is not null && i < marking.MarkingColors.Count)
_sprite.LayerSetColor(target, layerId, marking.MarkingColors[i]);
else
_sprite.LayerSetColor(target, layerId, Color.White);
}
applied.Add(marking);
}
ent.Comp.AppliedMarkings = applied;
}
private void RemoveMarkings(Entity<VisualOrganMarkingsComponent> ent, EntityUid target)
{
foreach (var marking in ent.Comp.AppliedMarkings)
{
if (!_marking.TryGetMarking(marking, out var proto))
continue;
foreach (var sprite in proto.Sprites)
{
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
if (sprite is not SpriteSpecifier.Rsi rsi)
continue;
var layerId = $"{proto.ID}-{rsi.RsiState}";
if (!_sprite.LayerMapTryGet(target, layerId, out var index, false))
continue;
_sprite.LayerMapRemove(target, layerId);
_sprite.RemoveLayer(target, index);
}
}
}
private void OnMarkingsChangedVisibility(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent> args)
{
if (!ent.Comp.HideableLayers.Contains(args.Args.Layer))
return;
foreach (var markings in ent.Comp.Markings.Values)
{
foreach (var marking in markings)
{
if (!_marking.TryGetMarking(marking, out var proto))
continue;
if (proto.BodyPart != args.Args.Layer && !(ent.Comp.DependentHidingLayers.TryGetValue(args.Args.Layer, out var dependent) && dependent.Contains(proto.BodyPart)))
continue;
foreach (var sprite in proto.Sprites)
{
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
if (sprite is not SpriteSpecifier.Rsi rsi)
continue;
var layerId = $"{proto.ID}-{rsi.RsiState}";
if (!_sprite.LayerMapTryGet(args.Body.Owner, layerId, out var index, true))
continue;
_sprite.LayerSetVisible(args.Body.Owner, index, args.Args.Visible);
}
}
}
}
}

View File

@ -70,9 +70,9 @@ namespace Content.Client.Cargo.BUI
_menu.OnClose += Close;
_menu.OnItemSelected += (args) =>
_menu.OnItemSelected += (row) =>
{
if (args.Button.Parent is not CargoProductRow row)
if (row == null)
return;
description.Clear();
@ -175,23 +175,23 @@ namespace Content.Client.Cargo.BUI
return true;
}
private void RemoveOrder(ButtonEventArgs args)
private void RemoveOrder(CargoOrderData? order)
{
if (args.Button.Parent?.Parent is not CargoOrderRow row || row.Order == null)
if (order == null)
return;
SendMessage(new CargoConsoleRemoveOrderMessage(row.Order.OrderId));
SendMessage(new CargoConsoleRemoveOrderMessage(order.OrderId));
}
private void ApproveOrder(ButtonEventArgs args)
private void ApproveOrder(CargoOrderData? order)
{
if (args.Button.Parent?.Parent is not CargoOrderRow row || row.Order == null)
if (order == null)
return;
if (OrderCount >= OrderCapacity)
return;
SendMessage(new CargoConsoleApproveOrderMessage(row.Order.OrderId));
SendMessage(new CargoConsoleApproveOrderMessage(order.OrderId));
}
}
}

View File

@ -1,86 +1,226 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="600 600"
MinSize="600 600">
<BoxContainer Orientation="Vertical" Margin="15 5 15 10">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'cargo-console-menu-account-name-label'}"
StyleClasses="LabelKeyText" />
<RichTextLabel Name="AccountNameLabel"
Text="{Loc 'cargo-console-menu-account-name-none-text'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'cargo-console-menu-points-label'}"
StyleClasses="LabelKeyText" />
<RichTextLabel Name="PointsLabel"
Text="$0" />
</BoxContainer>
<Control MinHeight="10"/>
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="540 390"
SetSize="995 600">
<!-- Main Container -->
<BoxContainer Orientation="Vertical"
VerticalExpand="True">
<TabContainer Name="TabContainer" VerticalExpand="True">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer Orientation="Horizontal">
<OptionButton Name="Categories"
Prefix="{Loc 'cargo-console-menu-categories-label'}"
HorizontalExpand="True" />
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
HorizontalExpand="True" />
</BoxContainer>
<Control MinHeight="5"/>
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
SizeFlagsStretchRatio="2">
<BoxContainer Name="Products"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Products get added here by code -->
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5" Name="OrdersSpacer"/>
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="1"
Name="Orders">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000" />
</PanelContainer.PanelOverride>
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical" Margin="5">
<Label Text="{Loc 'cargo-console-menu-requests-label'}" />
<BoxContainer Name="Requests"
Orientation="Vertical"
VerticalExpand="True">
<!-- Requests are added here by code -->
</BoxContainer>
</BoxContainer>
</ScrollContainer>
</PanelContainer>
</BoxContainer>
<!-- Funds tab -->
<BoxContainer Orientation="Vertical" Margin="15">
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
<RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
<OptionButton Name="ActionOptions"/>
</BoxContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
<SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
</BoxContainer>
<Control MinHeight="15"/>
<BoxContainer HorizontalAlignment="Center">
<Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
</BoxContainer>
<Control VerticalExpand="True"/>
<BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
<Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
</BoxContainer>
</BoxContainer>
<!-- Sub-Main Container -->
<BoxContainer Orientation="Horizontal"
VerticalExpand="True"
Margin="8 4 8 6">
<!-- Left Part -->
<BoxContainer Orientation="Vertical"
SeparationOverride="4"
Margin="0 0 8 0"
HorizontalExpand="True">
<!-- Info -->
<BoxContainer Orientation="Vertical">
<GridContainer Columns="3">
<!-- Account -->
<Label Text="{Loc 'cargo-console-menu-account-name-label'}"
StyleClasses="LabelKeyText" />
<PanelContainer StyleClasses="LowDivider" Margin="0 -2"/>
<RichTextLabel Name="AccountNameLabel"
Text="{Loc 'cargo-console-menu-account-name-none-text'}"
Margin="4 0"/>
<!-- Balance -->
<Label Text="{Loc 'cargo-console-menu-points-label'}"
StyleClasses="LabelKeyText"/>
<PanelContainer StyleClasses="LowDivider" Margin="0 -2"/>
<RichTextLabel Name="PointsLabel"
Text="$0"
Margin="4 0" />
<!-- Orders Count/Capacity -->
<Label Text="{Loc 'cargo-console-menu-order-capacity-label'}"
StyleClasses="LabelKeyText" />
<PanelContainer StyleClasses="LowDivider" Margin="0 -2 0 -1"/>
<Label Name="ShuttleCapacityLabel"
Text="0/20"
Margin="4 0"/>
</GridContainer>
<PanelContainer StyleClasses="LowDivider" Margin="0 4.5 -8 0"/>
</BoxContainer>
<!-- Search -->
<BoxContainer Orientation="Horizontal"
Margin="0 2 0 0">
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
HorizontalExpand="True" />
<OptionButton Name="Categories"
Prefix="{Loc 'cargo-console-menu-categories-label'}"
StyleClasses="OpenLeft"/>
</BoxContainer>
<!-- Product list -->
<ScrollContainer
HorizontalExpand="False"
VerticalExpand="True"
HScrollEnabled="False">
<BoxContainer Name="Products"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Products get added here by code -->
</BoxContainer>
</ScrollContainer>
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" Margin="0 -8"/>
<!-- Right Part -->
<BoxContainer Orientation="Vertical"
SizeFlagsStretchRatio="0.8"
HorizontalExpand="True"
Name="RightPart">
<!-- Requests Part -->
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
SizeFlagsStretchRatio="2">
<!-- Title -->
<controls:StripeBack>
<Label Text="{Loc 'cargo-console-menu-requests-label'}"
HorizontalAlignment="Center"
Margin="4"/>
</controls:StripeBack>
<PanelContainer VerticalExpand="True"
Margin="0 -4 0 0">
<!-- Background -->
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#040404" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True">
<BoxContainer Name="Requests"
Orientation="Vertical"
StyleClasses="transparentItemList"
VerticalExpand="True"
SeparationOverride="8"
Margin="8">
<!-- Requests are added here by code -->
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>
<!-- Orders Part -->
<BoxContainer Orientation="Vertical"
VerticalExpand="True">
<!-- Title -->
<controls:StripeBack>
<Label Text="{Loc 'cargo-console-menu-orders-label'}"
HorizontalAlignment="Center"
Margin="4"/>
</controls:StripeBack>
<PanelContainer VerticalExpand="True"
Margin="0 -4 0 0">
<!-- Background -->
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#040404" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical"
Margin="6">
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical"
StyleClasses="transparentItemList"
VerticalExpand="True"
SeparationOverride="6">
<!-- Orders are added here by code -->
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Funds tab -->
<BoxContainer Orientation="Vertical" Margin="15">
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
<RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
<OptionButton Name="ActionOptions"/>
</BoxContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
<SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
</BoxContainer>
<Control MinHeight="15"/>
<BoxContainer HorizontalAlignment="Center">
<Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
</BoxContainer>
<Control VerticalExpand="True"/>
<BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
<Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
</BoxContainer>
</BoxContainer>
</TabContainer>
<!-- Footer -->
<!-- TODO: Create customControls element -->
<BoxContainer Orientation="Vertical"
VerticalAlignment="Bottom">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal"
Margin="12 0 6 2"
VerticalAlignment="Bottom">
<!-- Footer title -->
<Label Text="{Loc 'cargo-console-menu-flavor-left'}"
StyleClasses="WindowFooterText" />
<!-- Version -->
<Label Text="{Loc 'cargo-console-menu-flavor-right'}"
StyleClasses="WindowFooterText"
HorizontalAlignment="Right"
HorizontalExpand="True"
Margin="0 0 4 0" />
<TextureRect StyleClasses="NTLogoDark"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
HorizontalAlignment="Right"
SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@ -6,6 +6,7 @@ using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
@ -29,9 +30,9 @@ namespace Content.Client.Cargo.UI
private readonly EntityQuery<CargoOrderConsoleComponent> _orderConsoleQuery;
private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
public event Action<ButtonEventArgs>? OnItemSelected;
public event Action<ButtonEventArgs>? OnOrderApproved;
public event Action<ButtonEventArgs>? OnOrderCanceled;
public event Action<CargoProductRow?>? OnItemSelected;
public event Action<CargoOrderData?>? OnOrderApproved;
public event Action<CargoOrderData?>? OnOrderCanceled;
public event Action<ProtoId<CargoAccountPrototype>?, int>? OnAccountAction;
@ -164,7 +165,7 @@ namespace Content.Client.Cargo.UI
};
button.MainButton.OnPressed += args =>
{
OnItemSelected?.Invoke(args);
OnItemSelected?.Invoke(button);
};
Products.AddChild(button);
}
@ -210,38 +211,66 @@ namespace Content.Client.Cargo.UI
foreach (var order in orders)
{
if (order.Approved)
if (order.Approved || !_protoManager.Resolve(order.Product, out var productProto))
continue;
var product = _protoManager.Index<EntityPrototype>(order.ProductId);
var productName = product.Name;
var product = _protoManager.Index<EntityPrototype>(productProto.Product);
var productName = productProto.Name;
var requester = !string.IsNullOrEmpty(order.Requester) ?
order.Requester : Loc.GetString("cargo-console-menu-order-row-alerts-requester-unknown");
var account = _protoManager.Index(order.Account);
var row = new CargoOrderRow
{
Order = order,
Title =
{
Text = Loc.GetString(
"cargo-console-menu-order-row-title",
("productName", productName),
("orderAmount", order.OrderQuantity),
("orderPrice", productProto.Cost)),
},
Stride =
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = account.Color,
ContentMarginBottomOverride = 2,
},
},
Icon = { Texture = _spriteSystem.Frame0(product) },
ProductName =
{
Text = Loc.GetString(
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
("productName", productName),
("orderAmount", order.OrderQuantity),
("orderRequester", order.Requester),
("orderRequester", requester),
("accountColor", account.Color),
("account", Loc.GetString(account.Code)))
},
Description =
{
Text = Loc.GetString("cargo-console-menu-order-reason-description",
("reason", order.Reason))
Text = !string.IsNullOrEmpty(order.Reason) ?
Loc.GetString(
"cargo-console-menu-order-row-product-description",
("orderReason", order.Reason))
:
Loc.GetString(
"cargo-console-menu-order-row-product-description",
("orderReason", Loc.GetString("cargo-console-menu-order-row-alerts-reason-absent")))
}
};
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(order); };
// TODO: Disable based on access.
row.SetApproveVisible(orderConsole.Mode != CargoOrderConsoleMode.SendToPrimary);
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(order); };
Requests.AddChild(row);
}
}
@ -294,8 +323,7 @@ namespace Content.Client.Cargo.UI
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
_timing.CurTime < orderConsole.NextAccountActionTime;
OrdersSpacer.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
Orders.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
RightPart.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
}
}
}

View File

@ -1,33 +1,53 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'cargo-console-order-menu-title'}">
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'cargo-console-order-menu-title'}"
MinSize="460 261">
<BoxContainer Orientation="Vertical">
<GridContainer Columns="2">
<Label Text="{Loc 'cargo-console-order-menu-product-label'}" />
<Label Text="{Loc 'cargo-console-order-menu-product-label'}"
StyleClasses="LabelKeyText" />
<Label Name="ProductName"
Access="Public" />
<Label Text="{Loc 'cargo-console-order-menu-description-label'}" />
Access="Public" />
<Label Text="{Loc 'cargo-console-order-menu-description-label'}"
StyleClasses="LabelKeyText" />
<RichTextLabel Name="Description"
Access="Public"
VerticalExpand="True"
SetWidth="350"/>
<Label Text="{Loc 'cargo-console-order-menu-cost-label'}" />
Access="Public"
HorizontalExpand="True"
MaxWidth="460" />
<Label Text="{Loc 'cargo-console-order-menu-cost-label'}"
StyleClasses="LabelKeyText" />
<Label Name="PointCost"
Access="Public" />
<Label Text="{Loc 'cargo-console-order-menu-requester-label'}" />
Access="Public" />
<Label Text="{Loc 'cargo-console-order-menu-requester-label'}"
StyleClasses="LabelKeyText" />
<LineEdit Name="Requester"
Access="Public" />
<Label Text="{Loc 'cargo-console-order-menu-reason-label'}" />
<Label Text="{Loc 'cargo-console-order-menu-reason-label'}"
StyleClasses="LabelKeyText" />
<LineEdit Name="Reason"
Access="Public" />
<Label Text="{Loc 'cargo-console-order-menu-amount-label'}" />
<Label Text="{Loc 'cargo-console-order-menu-amount-label'}"
StyleClasses="LabelKeyText" />
<SpinBox Name="Amount"
Access="Public"
HorizontalExpand="True"
Value="1" />
</GridContainer>
<Control VerticalExpand="True"/>
<PanelContainer StyleClasses="LowDivider" Margin="0 6 0 2"/>
<Button Name="SubmitButton"
Access="Public"
Text="{Loc 'cargo-console-order-menu-submit-button'}"
TextAlign="Center" />
VerticalAlignment="Bottom" />
</BoxContainer>
</DefaultWindow>

View File

@ -1,33 +1,81 @@
<PanelContainer xmlns="https://spacestation14.io"
HorizontalExpand="True"
Margin="0 1">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True">
<TextureRect Name="Icon"
Access="Public"
MinSize="32 32"
RectClipContent="True" />
<Control MinWidth="5"/>
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<RichTextLabel Name="ProductName"
Access="Public"
HorizontalExpand="True"
StyleClasses="LabelSubText" />
<Label Name="Description"
Access="Public"
HorizontalExpand="True"
StyleClasses="LabelSubText"
ClipText="True" />
<PanelContainer
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
HorizontalExpand="True"
StyleClasses="BackgroundPanel">
<!-- Main Container -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
SeparationOverride="6"
Margin="-14 -2">
<BoxContainer Orientation="Vertical">
<Control>
<PanelContainer StyleClasses="WindowHeadingBackground" />
<BoxContainer Margin="6">
<Label Name="Title"
Access="Public"
MaxHeight="28"
StyleClasses="LabelKeyText"/>
</BoxContainer>
</Control>
<PanelContainer Name="Stride"
Access="Public"
StyleClasses="LowDivider" />
</BoxContainer>
<!-- Info -->
<BoxContainer>
<TextureRect Name="Icon"
Access="Public"
MinSize="32 32"
Margin="4"
Stretch="KeepAspectCentered"
RectClipContent="True"
VerticalAlignment="Center"/>
<PanelContainer StyleClasses="LowDivider" Margin="4 0"/>
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<RichTextLabel Name="ProductName"
Access="Public"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="LabelSubText" />
<Label Name="Description"
Access="Public"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="LabelSubText"
ClipText="True" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<!-- Buttons -->
<!-- Btn's position hardcoded (args.Button.Parent?.Parent?.Parent type) in CargoConsoleBUI 158 & 166 line -->
<BoxContainer Margin="6">
<Button Name="Approve"
Access="Public"
Text="{Loc 'cargo-console-menu-order-row-button-approve'}"
StyleClasses="OpenRight"
HorizontalExpand="True"/>
<Button Name="Cancel"
Access="Public"
Text="{Loc 'cargo-console-menu-order-row-button-cancel'}"
StyleClasses="OpenLeft"
HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
<Button Name="Approve"
Access="Public"
Text="{Loc 'cargo-console-menu-cargo-order-row-approve-button'}"
StyleClasses="OpenRight" />
<Button Name="Cancel"
Access="Public"
Text="{Loc 'cargo-console-menu-cargo-order-row-cancel-button'}"
StyleClasses="OpenLeft" />
</BoxContainer>
</PanelContainer>

View File

@ -1,26 +1,34 @@
<PanelContainer xmlns="https://spacestation14.io"
HorizontalExpand="True">
<Button Name="MainButton"
ToolTip=""
Access="Public"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="OpenBoth"/>
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True">
<TextureRect Name="Icon"
Access="Public"
MinSize="32 32"
RectClipContent="True" />
<Label Name="ProductName"
Access="Public"
HorizontalExpand="True" />
<PanelContainer StyleClasses="BackgroundDark">
<Label Name="PointCost"
<BoxContainer xmlns="https://spacestation14.io"
HorizontalExpand="True">
<PanelContainer HorizontalExpand="True">
<!-- Btn position hardcoded (args.Button.Parent?.Parent type) in CargoConsoleBUI 71 line -->
<Button Name="MainButton"
ToolTip=""
Access="Public"
VerticalExpand="False"
StyleClasses="OpenBoth" />
<!-- Icon & Name -->
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
Margin="4 0">
<TextureRect Name="Icon"
Access="Public"
MinSize="32 32"
RectClipContent="True" />
<Label Name="ProductName"
Access="Public"
MinSize="52 32"
Align="Right"
Margin="0 0 5 0"/>
</PanelContainer>
</BoxContainer>
</PanelContainer>
HorizontalExpand="True"
ClipText="True" />
</BoxContainer>
</PanelContainer>
<Label Name="PointCost"
Access="Public"
MinSize="56 32"
Align="Right"
Margin="0 0 5 0"
HorizontalAlignment="Right"/>
</BoxContainer>

View File

@ -7,7 +7,7 @@ using Robust.Client.UserInterface.XAML;
namespace Content.Client.Cargo.UI
{
[GenerateTypedNameReferences]
public sealed partial class CargoProductRow : PanelContainer
public sealed partial class CargoProductRow : BoxContainer
{
public CargoProductPrototype? Product { get; set; }

View File

@ -34,7 +34,10 @@ namespace Content.Client.Cargo.UI
foreach (var order in orders)
{
var product = protoManager.Index<EntityPrototype>(order.ProductId);
if (!protoManager.Resolve(order.Product, out var productProto))
continue;
var product = protoManager.Index<EntityPrototype>(productProto.Product);
var productName = product.Name;
var account = protoManager.Index(order.Account);

View File

@ -9,7 +9,6 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
protected override void OnAppearanceChange(EntityUid uid, TypingIndicatorComponent component, ref AppearanceChangeEvent args)
{

View File

@ -1,7 +1,7 @@
using System.Linq;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Atmos.Prototypes;
using Content.Shared.Body.Part;
using Content.Shared.Body;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
@ -94,7 +94,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
continue;
//these bloat the hell out of blood/fat
if (entProto.HasComponent<BodyPartComponent>())
if (entProto.HasComponent<OrganComponent>())
continue;
//these feel obvious...
@ -116,7 +116,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
}
if (extractableComponent.GrindableSolution is { } grindableSolutionId &&
if (extractableComponent.GrindableSolutionName is { } grindableSolutionId &&
entProto.TryGetComponent<SolutionContainerManagerComponent>(out var manager, EntityManager.ComponentFactory) &&
_solutionContainer.TryGetSolution(manager, grindableSolutionId, out var grindableSolution))
{

View File

@ -8,7 +8,7 @@
VerticalExpand="True"
HorizontalExpand="True"
MinSize="100 150">
<PanelContainer VerticalExpand="True" StyleClasses="Inset">
<PanelContainer VerticalExpand="True" StyleClasses="BackgroundPanelDark">
<BoxContainer Name="GeneticScannerContents" Margin="5 5 5 5" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<Label HorizontalAlignment="Center" Text="{Loc 'cloning-console-window-scanner-details-label'}" />
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
@ -35,7 +35,7 @@
</BoxContainer>
</PanelContainer>
<Control MinSize="50 5" />
<PanelContainer VerticalExpand="True" StyleClasses="Inset">
<PanelContainer VerticalExpand="True" StyleClasses="BackgroundPanelDark">
<BoxContainer Name="CloningPodContents" Margin="5 5 5 5" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<Label HorizontalAlignment="Center" Text="{Loc 'cloning-console-window-pod-details-label'}" />
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">

View File

@ -273,7 +273,7 @@ public sealed class ClientClothingSystem : ClothingSystem
// Select displacement maps
var displacementData = inventory.Displacements.GetValueOrDefault(slot); //Default unsexed map
var equipeeSex = CompOrNull<HumanoidAppearanceComponent>(equipee)?.Sex;
var equipeeSex = CompOrNull<HumanoidProfileComponent>(equipee)?.Sex;
if (equipeeSex != null)
{
switch (equipeeSex)

View File

@ -1,5 +1,4 @@
using Content.Client.Hands.Systems;
using Content.Client.NPC.HTN;
using Content.Shared.CCVar;
using Content.Shared.CombatMode;
using Robust.Client.Graphics;
@ -59,11 +58,6 @@ public sealed class CombatModeSystem : SharedCombatModeSystem
UpdateHud(entity);
}
protected override bool IsNpc(EntityUid uid)
{
return HasComp<HTNComponent>(uid);
}
private void UpdateHud(EntityUid entity)
{
if (entity != _playerManager.LocalEntity || !Timing.IsFirstTimePredicted)

View File

@ -1,36 +0,0 @@
using Content.Shared.Body.Organ;
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.Containers;
namespace Content.Client.Commands;
public sealed class HideMechanismsCommand : LocalizedEntityCommands
{
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
public override string Command => "hidemechanisms";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var query = EntityManager.AllEntityQueryEnumerator<OrganComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out _, out var sprite))
{
_spriteSystem.SetContainerOccluded((uid, sprite), false);
var tempParent = uid;
while (_containerSystem.TryGetContainingContainer((tempParent, null, null), out var container))
{
if (!container.ShowContents)
{
_spriteSystem.SetContainerOccluded((uid, sprite), true);
break;
}
tempParent = container.Owner;
}
}
}
}

View File

@ -1,22 +0,0 @@
using Content.Shared.Body.Organ;
using Robust.Client.GameObjects;
using Robust.Shared.Console;
namespace Content.Client.Commands;
public sealed class ShowMechanismsCommand : LocalizedEntityCommands
{
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
public override string Command => "showmechanisms";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var query = EntityManager.AllEntityQueryEnumerator<OrganComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out _, out var sprite))
{
_spriteSystem.SetContainerOccluded((uid, sprite), false);
}
}
}

View File

@ -291,7 +291,6 @@ namespace Content.Client.Construction
_ghosts.Add(comp.GhostId, ghost.Value);
var sprite = Comp<SpriteComponent>(ghost.Value);
_sprite.SetColor((ghost.Value, sprite), new Color(48, 255, 48, 128));
if (targetProto.TryGetComponent(out IconComponent? icon, EntityManager.ComponentFactory))
{
@ -306,20 +305,11 @@ namespace Content.Client.Construction
var targetSprite = EnsureComp<SpriteComponent>(dummy);
EntityManager.System<AppearanceSystem>().OnChangeData(dummy, targetSprite);
for (var i = 0; i < targetSprite.AllLayers.Count(); i++)
_sprite.CopySprite((dummy, targetSprite), (ghost.Value, sprite));
for (var i = 0; i < sprite.AllLayers.Count(); i++)
{
if (!targetSprite[i].Visible || !targetSprite[i].RsiState.IsValid)
continue;
var rsi = targetSprite[i].Rsi ?? targetSprite.BaseRSI;
if (rsi is null || !rsi.TryGetState(targetSprite[i].RsiState, out var state) ||
state.StateId.Name is null)
continue;
_sprite.AddBlankLayer((ghost.Value, sprite), i);
_sprite.LayerSetSprite((ghost.Value, sprite), i, new SpriteSpecifier.Rsi(rsi.Path, state.StateId.Name));
sprite.LayerSetShader(i, "unshaded");
_sprite.LayerSetVisible((ghost.Value, sprite), i, true);
}
Del(dummy);
@ -327,6 +317,8 @@ namespace Content.Client.Construction
else
return false;
_sprite.SetColor((ghost.Value, sprite), new Color(48, 255, 48, 128));
if (prototype.CanBuildInImpassable)
EnsureComp<WallMountComponent>(ghost.Value).Arc = new(Math.Tau);

View File

@ -1,4 +1,6 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Client.Damage;
@ -55,7 +57,7 @@ public sealed partial class DamageVisualsComponent : Component
/// (for example, Brute), and has a value
/// of a DamageVisualizerSprite (see below)
/// </summary>
[DataField("damageOverlayGroups")] public Dictionary<string, DamageVisualizerSprite>? DamageOverlayGroups;
[DataField("damageOverlayGroups")] public Dictionary<ProtoId<DamageGroupPrototype>, DamageVisualizerSprite>? DamageOverlayGroups;
/// <summary>
/// Sets if you want sprites to overlay the
@ -84,7 +86,7 @@ public sealed partial class DamageVisualsComponent : Component
/// what kind of damage combination
/// you would want, on which threshold.
/// </remarks>
[DataField("damageGroup")] public string? DamageGroup;
[DataField("damageGroup")] public ProtoId<DamageGroupPrototype>? DamageGroup;
/// <summary>
/// Set this if you want incoming damage to be

View File

@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
@ -28,6 +29,7 @@ namespace Content.Client.Damage;
public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
public override void Initialize()
{
@ -174,7 +176,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
// See if that group is in our entity's damage container.
else if (!damageVisComp.Overlay && damageVisComp.DamageGroup != null)
{
if (!damageContainer.SupportedGroups.Contains(damageVisComp.DamageGroup))
if (!damageContainer.SupportedGroups.Contains(damageVisComp.DamageGroup.Value))
{
Log.Error($"Damage keys were invalid for entity {entity}.");
damageVisComp.Valid = false;
@ -384,7 +386,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
if (!AppearanceSystem.TryGetData<DamageVisualizerGroupData>(uid, DamageVisualizerKeys.DamageUpdateGroups,
out var data, component))
{
data = new DamageVisualizerGroupData(Comp<DamageableComponent>(uid).DamagePerGroup.Keys.ToList());
data = new DamageVisualizerGroupData(_damageable.GetDamagePerGroup(uid).Keys.ToList());
}
UpdateDamageVisuals(data.GroupList, (uid, damageComponent, spriteComponent, damageVisComp));
@ -486,11 +488,10 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
/// </summary>
private void UpdateDamageVisuals(Entity<DamageableComponent, SpriteComponent, DamageVisualsComponent> entity)
{
var damageComponent = entity.Comp1;
var spriteComponent = entity.Comp2;
var damageVisComp = entity.Comp3;
if (!CheckThresholdBoundary(damageComponent.TotalDamage, damageVisComp.LastDamageThreshold, damageVisComp, out var threshold))
if (!CheckThresholdBoundary(_damageable.GetTotalDamage(entity.AsNullable()), damageVisComp.LastDamageThreshold, damageVisComp, out var threshold))
return;
damageVisComp.LastDamageThreshold = threshold;
@ -513,11 +514,11 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
/// according to the list of damage groups
/// passed into it.
/// </summary>
private void UpdateDamageVisuals(List<string> delta, Entity<DamageableComponent, SpriteComponent, DamageVisualsComponent> entity)
private void UpdateDamageVisuals(List<ProtoId<DamageGroupPrototype>> delta, Entity<DamageableComponent, SpriteComponent, DamageVisualsComponent> entity)
{
var damageComponent = entity.Comp1;
var spriteComponent = entity.Comp2;
var damageVisComp = entity.Comp3;
var damage = _damageable.GetAllDamage((entity.Owner, entity.Comp1));
foreach (var damageGroup in delta)
{
@ -525,7 +526,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
continue;
if (!_prototypeManager.TryIndex<DamageGroupPrototype>(damageGroup, out var damageGroupPrototype)
|| !damageComponent.Damage.TryGetDamageInGroup(damageGroupPrototype, out var damageTotal))
|| !damage.TryGetDamageInGroup(damageGroupPrototype, out var damageTotal))
continue;
if (!damageVisComp.LastThresholdPerGroup.TryGetValue(damageGroup, out var lastThreshold)
@ -590,7 +591,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
}
else if (damageVisComp.DamageGroup != null)
{
UpdateDamageVisuals(new List<string>() { damageVisComp.DamageGroup }, entity);
UpdateDamageVisuals(new() { damageVisComp.DamageGroup.Value }, entity);
}
else if (damageVisComp.DamageOverlay != null)
{

View File

@ -1,8 +1,8 @@
using Content.Shared.CCVar;
using Content.Shared.Drunk;
using Content.Shared.StatusEffect;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@ -11,19 +11,23 @@ namespace Content.Client.Drunk;
public sealed class DrunkOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> Shader = "Drunk";
private static readonly ProtoId<ShaderPrototype> DrunkShader = "Drunk";
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
private readonly Shared.StatusEffectNew.StatusEffectsSystem _statusEffectsSystem;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
private readonly ShaderInstance _drunkShader;
public float CurrentBoozePower = 0.0f;
// Starting phase for the rotation effect.
// Needed so it doesn't always look the same for 0 motion.
public float Phase = 0f;
private const float VisualThreshold = 10.0f;
private const float PowerDivisor = 250.0f;
@ -37,12 +41,22 @@ public sealed class DrunkOverlay : Overlay
private const float BoozePowerScale = 8f;
private float _visualScale = 0;
private float _visualScale = 0f;
private float _timeScale = 1f;
private float _distortionScale = 1f;
public DrunkOverlay()
{
IoCManager.InjectDependencies(this);
_drunkShader = _prototypeManager.Index(Shader).InstanceUnique();
_statusEffectsSystem = _entityManager.System<Shared.StatusEffectNew.StatusEffectsSystem>();
_drunkShader = _prototypeManager.Index(DrunkShader).InstanceUnique();
_configManager.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
}
private void OnReducedMotionChanged(bool reducedMotion)
{
_timeScale = reducedMotion ? 0.0f : 1.0f;
_distortionScale = reducedMotion ? 4.0f : 1.0f; // Make the offset stronger to compensate the lack of motion.
}
protected override void FrameUpdate(FrameEventArgs args)
@ -53,15 +67,14 @@ public sealed class DrunkOverlay : Overlay
if (playerEntity == null)
return;
var statusSys = _sysMan.GetEntitySystem<Shared.StatusEffectNew.StatusEffectsSystem>();
if (!statusSys.TryGetMaxTime<DrunkStatusEffectComponent>(playerEntity.Value, out var status))
if (!_statusEffectsSystem.TryGetMaxTime<DrunkStatusEffectComponent>(playerEntity.Value, out var status))
return;
var time = status.Item2;
var power = time == null ? MaxBoozePower : (float) Math.Min((time - _timing.CurTime).Value.TotalSeconds, MaxBoozePower);
var power = time == null ? MaxBoozePower : (float)Math.Min((time - _timing.CurTime).Value.TotalSeconds, MaxBoozePower);
CurrentBoozePower += BoozePowerScale * (power - CurrentBoozePower) * args.DeltaSeconds / (power+1);
CurrentBoozePower += BoozePowerScale * (power - CurrentBoozePower) * args.DeltaSeconds / (power + 1);
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
@ -82,8 +95,12 @@ public sealed class DrunkOverlay : Overlay
return;
var handle = args.WorldHandle;
_drunkShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_drunkShader.SetParameter("boozePower", _visualScale);
_drunkShader.SetParameter("timeScale", _timeScale);
_drunkShader.SetParameter("distortionScale", _distortionScale);
_drunkShader.SetParameter("phase", Phase);
handle.UseShader(_drunkShader);
handle.DrawRect(args.WorldBounds, Color.White);
handle.UseShader(null);

View File

@ -3,6 +3,7 @@ using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Client.Drunk;
@ -10,6 +11,7 @@ public sealed class DrunkSystem : SharedDrunkSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private DrunkOverlay _overlay = default!;
@ -29,7 +31,10 @@ public sealed class DrunkSystem : SharedDrunkSystem
private void OnStatusApplied(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectAppliedEvent args)
{
if (!_overlayMan.HasOverlay<DrunkOverlay>())
{
_overlay.Phase = _random.NextFloat(MathF.Tau); // random starting phase for movement effect
_overlayMan.AddOverlay(_overlay);
}
}
private void OnStatusRemoved(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRemovedEvent args)
@ -47,6 +52,7 @@ public sealed class DrunkSystem : SharedDrunkSystem
private void OnPlayerAttached(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRelayedEvent<LocalPlayerAttachedEvent> args)
{
_overlayMan.AddOverlay(_overlay);
}
private void OnPlayerDetached(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRelayedEvent<LocalPlayerDetachedEvent> args)

View File

@ -3,6 +3,7 @@ using Content.Client.Changelog;
using Content.Client.Chat.Managers;
using Content.Client.DebugMon;
using Content.Client.Eui;
using Content.Client.FeedbackPopup;
using Content.Client.Fullscreen;
using Content.Client.GameTicking.Managers;
using Content.Client.GhostKick;
@ -24,6 +25,7 @@ using Content.Client.UserInterface;
using Content.Client.Viewport;
using Content.Client.Voting;
using Content.Shared.Ame.Components;
using Content.Shared.FeedbackSystem;
using Content.Shared.Gravity;
using Content.Shared.Localizations;
using Robust.Client;
@ -78,6 +80,7 @@ namespace Content.Client.Entry
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ClientsidePlaytimeTrackingManager _clientsidePlaytimeManager = default!;
[Dependency] private readonly ClientFeedbackManager _feedbackManager = null!;
public override void PreInit()
{
@ -180,6 +183,7 @@ namespace Content.Client.Entry
_userInterfaceManager.SetActiveTheme(_configManager.GetCVar(CVars.InterfaceTheme));
_documentParsingManager.Initialize();
_titleWindowManager.Initialize();
_feedbackManager.Initialize();
_baseClient.RunLevelChanged += (_, args) =>
{

View File

@ -7,6 +7,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Input;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Radio.Components; // DeltaV - Client-side Radio Colors
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@ -158,6 +159,15 @@ namespace Content.Client.Examine
// since there's probably one open already if it's coming in from the server.
var entity = GetEntity(ev.EntityUid);
// BEGIN DeltaV - Client-Side Radio Colors
// This *seems* a bit hacky, but we basically don't want to overwrite what's been pushed on the client-side,
// because the server will send us the server-side colors (default). I've yet to find a scenario where the
// server and client don't match, so until then, this is probably fine, if a little hacky.
if (HasComp<EncryptionKeyHolderComponent>(entity) || HasComp<EncryptionKeyComponent>(entity))
return;
// END DeltaV
OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget);
UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs, getVerbs: false);
}

View File

@ -25,13 +25,13 @@ public sealed partial class FaxWindow : DefaultWindow
PaperButtonPressed += OnPaperButtonPressed;
FileButton.OnPressed += _ => FileButtonPressed?.Invoke();
PaperButton.OnPressed += _ => PaperButtonPressed?.Invoke();
FileButton.OnPressed += _ => FileButtonPressed?.Invoke();
PaperButton.OnPressed += _ => PaperButtonPressed?.Invoke();
CopyButton.OnPressed += _ => CopyButtonPressed?.Invoke();
SendButton.OnPressed += _ => SendButtonPressed?.Invoke();
RefreshButton.OnPressed += _ => RefreshButtonPressed?.Invoke();
PeerSelector.OnItemSelected += args =>
PeerSelected?.Invoke((string) args.Button.GetItemMetadata(args.Id)!);
PeerSelected?.Invoke((string)args.Button.GetItemMetadata(args.Id)!);
}
public void UpdateState(FaxUiState state)

View File

@ -0,0 +1,70 @@
using Content.Shared.FeedbackSystem;
using Robust.Shared.Prototypes;
namespace Content.Client.FeedbackPopup;
/// <inheritdoc />
public sealed class ClientFeedbackManager : SharedFeedbackManager
{
/// <summary>
/// A read-only set representing the currently displayed feedback popups.
/// </summary>
public override IReadOnlySet<ProtoId<FeedbackPopupPrototype>> DisplayedPopups => _displayedPopups;
private readonly HashSet<ProtoId<FeedbackPopupPrototype>> _displayedPopups = [];
public override void Initialize()
{
base.Initialize();
NetManager.RegisterNetMessage<FeedbackPopupMessage>(ReceivedPopupMessage);
NetManager.RegisterNetMessage<OpenFeedbackPopupMessage>(_ => Open());
}
/// <summary>
/// Opens the feedback popup window.
/// </summary>
public void Open()
{
InvokeDisplayedPopupsChanged(true);
}
/// <inheritdoc />
public override void Display(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
{
if (prototypes == null || !NetManager.IsClient)
return;
var count = _displayedPopups.Count;
_displayedPopups.UnionWith(prototypes);
InvokeDisplayedPopupsChanged(_displayedPopups.Count > count);
}
/// <inheritdoc />
public override void Remove(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
{
if (!NetManager.IsClient)
return;
if (prototypes == null)
{
_displayedPopups.Clear();
}
else
{
_displayedPopups.ExceptWith(prototypes);
}
InvokeDisplayedPopupsChanged(false);
}
private void ReceivedPopupMessage(FeedbackPopupMessage message)
{
if (message.Remove)
{
Remove(message.FeedbackPrototypes);
return;
}
Display(message.FeedbackPrototypes);
}
}

View File

@ -0,0 +1,24 @@
<Control xmlns="https://spacestation14.io"
MinHeight="100">
<PanelContainer StyleClasses="BackgroundPanel" ModulateSelfOverride="#2b2b31"/>
<BoxContainer Orientation="Vertical">
<!-- Title -->
<PanelContainer StyleIdentifier="FeedbackBorderThinBottom">
<RichTextLabel Name="TitleLabel" Margin="12 6 6 6" />
</PanelContainer>
<!-- Description -->
<RichTextLabel Name="DescriptionLabel" StyleClasses="LabelLight" Margin="12 4 12 8" VerticalExpand="True"/>
<!-- Footer -->
<PanelContainer StyleIdentifier="FeedbackBorderThinTop">
<BoxContainer>
<Label FontColorOverride="#b1b1b2" StyleClasses="LabelSmall" Name="TypeLabel" Margin="14 6 6 6" />
<Button Name="LinkButton" Text="{Loc feedbackpopup-control-button-text}" MinWidth="80"
Margin="8 6 14 6" HorizontalExpand="True" HorizontalAlignment="Right" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</Control>

View File

@ -0,0 +1,54 @@
using Content.Shared.FeedbackSystem;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.FeedbackPopup;
[GenerateTypedNameReferences]
public sealed partial class FeedbackEntry : Control
{
private readonly IUriOpener _uri;
private readonly FeedbackPopupPrototype? _prototype;
public FeedbackEntry(ProtoId<FeedbackPopupPrototype> popupProto, IPrototypeManager proto, IUriOpener uri)
{
RobustXamlLoader.Load(this);
_uri = uri;
_prototype = proto.Index(popupProto);
// Title
TitleLabel.Text = _prototype.Title;
DescriptionLabel.Text = _prototype.Description;
TypeLabel.Text = _prototype.ResponseType;
LinkButton.Visible = !string.IsNullOrEmpty(_prototype.ResponseLink);
// link button
if (!string.IsNullOrEmpty(_prototype.ResponseLink))
{
LinkButton.OnPressed += OnButtonPressed;
}
}
private void OnButtonPressed(BaseButton.ButtonEventArgs args)
{
if (!string.IsNullOrWhiteSpace(_prototype?.ResponseLink))
_uri.OpenUri(_prototype.ResponseLink);
}
protected override void Resized()
{
base.Resized();
// magic
TitleLabel.SetWidth = Width - TitleLabel.Margin.SumHorizontal;
TitleLabel.InvalidateArrange();
DescriptionLabel.SetWidth = Width - DescriptionLabel.Margin.SumHorizontal;
DescriptionLabel.InvalidateArrange();
}
}

View File

@ -0,0 +1,36 @@
using Content.Client.Stylesheets;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client.FeedbackPopup;
[CommonSheetlet]
public sealed class FeedbackPopupSheetlet : Sheetlet<PalettedStylesheet>
{
public override StyleRule[] GetRules(PalettedStylesheet sheet, object config)
{
var borderTop = new StyleBoxFlat()
{
BorderColor = sheet.SecondaryPalette.Base,
BorderThickness = new Thickness(0, 1, 0, 0),
};
var borderBottom = new StyleBoxFlat()
{
BorderColor = sheet.SecondaryPalette.Base,
BorderThickness = new Thickness(0, 0, 0, 1),
};
return
[
E<PanelContainer>()
.Identifier("FeedbackBorderThinTop")
.Prop(PanelContainer.StylePropertyPanel, borderTop),
E<PanelContainer>()
.Identifier("FeedbackBorderThinBottom")
.Prop(PanelContainer.StylePropertyPanel, borderBottom),
];
}
}

View File

@ -0,0 +1,75 @@
using Content.Shared.FeedbackSystem;
using Content.Shared.GameTicking;
using Robust.Client.UserInterface.Controllers;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.FeedbackPopup;
/// <summary>
/// This handles getting feedback popup messages from the server and making a popup in the client.
/// </summary>
[UsedImplicitly]
public sealed class FeedbackPopupUIController : UIController
{
[Dependency] private readonly ClientFeedbackManager _feedbackManager = null!;
[Dependency] private readonly IPrototypeManager _proto = null!;
[Dependency] private readonly IUriOpener _uri = null!;
private FeedbackPopupWindow _window = null!;
public override void Initialize()
{
_window = new FeedbackPopupWindow(_proto, _uri);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeNetworkEvent<RoundEndMessageEvent>(OnRoundEnd);
_feedbackManager.DisplayedPopupsChanged += OnPopupsChanged;
}
public void ToggleWindow()
{
if (_window.IsOpen)
{
_window.Close();
}
else
{
_window.OpenCentered();
}
}
private void OnRoundEnd(RoundEndMessageEvent ev, EntitySessionEventArgs args)
{
// Add round end prototypes.
var roundEndPrototypes = _feedbackManager.GetOriginFeedbackPrototypes(true);
if (roundEndPrototypes.Count == 0)
return;
_feedbackManager.Display(roundEndPrototypes);
// Even if no new prototypes were added, we still want to open the window.
if (!_window.IsOpen)
_window.OpenCentered();
}
private void OnPopupsChanged(bool newPopups)
{
UpdateWindow(_feedbackManager.DisplayedPopups);
if (newPopups && !_window.IsOpen)
_window.OpenCentered();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
{
UpdateWindow(_feedbackManager.DisplayedPopups);
}
private void UpdateWindow(IReadOnlyCollection<ProtoId<FeedbackPopupPrototype>> prototypes)
{
_window.Update(prototypes);
}
}

View File

@ -0,0 +1,24 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc feedbackpopup-window-name}" MinSize="510 460" RectClipContent="True">
<BoxContainer Orientation="Vertical">
<!-- main box area -->
<BoxContainer Margin="12 12 12 5" VerticalExpand="True">
<PanelContainer HorizontalExpand="True" StyleClasses="PanelDark">
<ScrollContainer HorizontalExpand="True" HScrollEnabled="False">
<BoxContainer Name="NotificationContainer" HorizontalExpand="True" Orientation="Vertical" Margin="10" SeparationOverride="10" />
</ScrollContainer>
</PanelContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical" SetHeight="30" Margin="2 0 0 0">
<BoxContainer SetHeight="33" Margin="10 0 10 5">
<Label Text="{Loc feedbackpopup-control-ui-footer}" Margin="6 0" StyleClasses="PdaContentFooterText"/>
<Label Name="NumNotifications" Margin="6 0" HorizontalExpand="True" HorizontalAlignment="Right"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

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