Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Zekins3366
2026-01-31 14:58:49 +03:00
1222 changed files with 25178 additions and 18112 deletions

View File

@@ -351,7 +351,7 @@ resharper_csharp_qualified_using_at_nested_scope = false
resharper_csharp_prefer_qualified_reference = false
resharper_csharp_allow_alias = false
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,slnx}]
indent_size = 2
[nuget.config]

View File

@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -20,7 +20,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Get Engine Tag
run: |

View File

@@ -47,7 +47,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Get Engine Tag
run: |

View File

@@ -65,7 +65,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -6,13 +6,10 @@ on:
branches: [ master, jsondump ]
paths:
- '.github/workflows/update-wiki.yml'
- 'Content.Shared/Chemistry/**.cs'
- 'Content.Server/Chemistry/**.cs'
- 'Content.Server/GuideGenerator/**.cs'
- 'Content.Server/Corvax/GuideGenerator/**.cs'
- 'Resources/Prototypes/Reagents/**.yml'
- 'Resources/Prototypes/Chemistry/**.yml'
- 'Resources/Prototypes/Recipes/Reactions/**.yml'
- 'Content.Shared/**'
- 'Content.Server/**'
- 'Content.Client/**'
- 'Resources/**'
- 'RobustToolbox/'
jobs:
@@ -51,42 +48,50 @@ jobs:
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
continue-on-error: true
- name: Upload chem_prototypes.json to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/chem_prototypes.json
edit_summary: Update chem_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
# Проходит по всем JSON-файлам в директории BASE и загружает каждый файл как страницу в MediaWiki.
# Имя страницы формируется из относительного пути к файлу.
- name: Upload JSON files to wiki
shell: bash
run: |
set -euo pipefail
- name: Upload react_prototypes.json to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/react_prototypes.json
edit_summary: Update react_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
BASE="./bin/Content.Server/data"
ROOT="${{ secrets.WIKI_PAGE_ROOT }}"
API="${{ secrets.WIKI_ROOT_URL }}/api.php"
USER="${{ secrets.WIKI_BOT_USER }}"
PASS="${{ secrets.WIKI_BOT_PASS }}"
- name: Upload entity_prototypes.json to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/entity_prototypes.json
edit_summary: Update entity_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
API="$(printf "%s" "$API" | tr -d '\r\n' | sed 's/[[:space:]]*$//')"
USER="$(printf "%s" "$USER" | tr -d '\r\n')"
PASS="$(printf "%s" "$PASS" | tr -d '\r\n')"
ROOT="$(printf "%s" "$ROOT" | tr -d '\r\n' | sed 's/[[:space:]]*$//')"
- name: Upload mealrecipes_prototypes.json to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/mealrecipes_prototypes.json
edit_summary: Update mealrecipes_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
cookiejar="$(mktemp)"
trap 'rm -f "$cookiejar"' EXIT
login_token=$(curl -sS -c "$cookiejar" --data "action=query&meta=tokens&type=login&format=json" "$API" | jq -r '.query.tokens.logintoken')
curl -sS -c "$cookiejar" -b "$cookiejar" \
--data-urlencode "action=login" \
--data-urlencode "lgname=$USER" \
--data-urlencode "lgpassword=$PASS" \
--data-urlencode "lgtoken=$login_token" \
--data-urlencode "format=json" \
"$API" > /dev/null
find "$BASE" -type f -name '*.json' | while IFS= read -r file; do
rel="${file#$BASE/}"
rel="$(printf "%s" "$rel" | tr -d '\r\n' | sed 's/:/_/g')"
page="$ROOT/$rel"
echo "Uploading $rel → $page"
token=$(curl -sS -b "$cookiejar" --data "action=query&meta=tokens&format=json" "$API" | jq -r '.query.tokens.csrftoken')
curl -sS -b "$cookiejar" \
--data-urlencode "action=edit" \
--data-urlencode "title=$page" \
--data-urlencode "summary=Update $rel via GitHub Actions" \
--data-urlencode "text@${file}" \
--data-urlencode "token=$token" \
--data-urlencode "format=json" \
"$API" | jq -r '.'
done

View File

@@ -29,7 +29,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
- name: Build

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# MSbuild binlog files
*.binlog
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

View File

@@ -11,7 +11,7 @@ import time
from pathlib import Path
from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
SOLUTION_PATH = Path("..") / "SpaceStation14.slnx"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "4"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"

View File

@@ -1,17 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<OutputPath>..\bin\Content.Benchmarks\</OutputPath>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>12</LangVersion>
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
<Nullable>disable</Nullable>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<!-- pin transitive deps -->
<PackageReference Include="System.Management" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
@@ -19,10 +22,12 @@
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Benchmarks\Robust.Benchmarks.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Benchmarks.props" />
<Import Project="..\RobustToolbox\Imports\Testing.props" />
</Project>

View File

@@ -0,0 +1,83 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
namespace Content.Benchmarks;
[Virtual]
[GcServer(true)]
[MemoryDiagnoser]
public class HeatCapacityBenchmark
{
private TestPair _pair = default!;
private IEntityManager _sEntMan = default!;
private IEntityManager _cEntMan = default!;
private Client.Atmos.EntitySystems.AtmosphereSystem _cAtmos = default!;
private AtmosphereSystem _sAtmos = default!;
private GasMixture _mix;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
await _pair.Connect();
_cEntMan = _pair.Client.ResolveDependency<IEntityManager>();
_sEntMan = _pair.Server.ResolveDependency<IEntityManager>();
_cAtmos = _cEntMan.System<Client.Atmos.EntitySystems.AtmosphereSystem>();
_sAtmos = _sEntMan.System<AtmosphereSystem>();
const float volume = 2500f;
const float temperature = 293.15f;
const float o2 = 12.3f;
const float n2 = 45.6f;
const float co2 = 0.42f;
const float plasma = 0.05f;
_mix = new GasMixture(volume) { Temperature = temperature };
_mix.AdjustMoles(Gas.Oxygen, o2);
_mix.AdjustMoles(Gas.Nitrogen, n2);
_mix.AdjustMoles(Gas.CarbonDioxide, co2);
_mix.AdjustMoles(Gas.Plasma, plasma);
}
[Benchmark]
public async Task ClientHeatCapacityBenchmark()
{
await _pair.Client.WaitPost(delegate
{
for (var i = 0; i < 10000; i++)
{
_cAtmos.GetHeatCapacity(_mix, applyScaling: true);
}
});
}
[Benchmark]
public async Task ServerHeatCapacityBenchmark()
{
await _pair.Server.WaitPost(delegate
{
for (var i = 0; i < 10000; i++)
{
_sAtmos.GetHeatCapacity(_mix, applyScaling: true);
}
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -134,7 +134,7 @@ internal sealed class AdminNameOverlay : Overlay
? null
: _prototypeManager.Index(playerInfo.RoleProto.Value);
var roleName = Loc.GetString(rolePrototype?.Name ?? RoleTypePrototype.FallbackName);
var roleName = rolePrototype?.Name ?? RoleTypePrototype.FallbackName;
var roleColor = rolePrototype?.Color ?? RoleTypePrototype.FallbackColor;
var roleSymbol = rolePrototype?.Symbol ?? RoleTypePrototype.FallbackSymbol;
@@ -213,7 +213,7 @@ internal sealed class AdminNameOverlay : Overlay
{
color = Color.GreenYellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? color : colorDisconnected);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.StartingJob, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
}
@@ -241,7 +241,7 @@ internal sealed class AdminNameOverlay : Overlay
color = roleColor;
symbol = IsFiltered(playerInfo.RoleProto) ? symbol : string.Empty;
text = IsFiltered(playerInfo.RoleProto)
? roleName.ToUpper()
? Loc.GetString(roleName).ToUpper()
: string.Empty;
break;
case AdminOverlayAntagFormat.Subtype:

View File

@@ -0,0 +1,35 @@
using System.Runtime.CompilerServices;
using Content.Shared.Atmos;
namespace Content.Client.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
/*
Partial class for operations involving GasMixtures.
Any method that is overridden here is usually because the server-sided implementation contains
code that would escape sandbox. As such these methods are overridden here with a safe
implementation.
*/
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override float GetHeatCapacityCalculation(float[] moles, bool space)
{
// Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms.
if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f))
{
return Atmospherics.SpaceHeatCapacity;
}
// explicit stackalloc call is banned on client tragically.
// the JIT does not stackalloc this during runtime,
// 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);
// 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

@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
namespace Content.Client.Atmos.EntitySystems;
public sealed class AtmosphereSystem : SharedAtmosphereSystem
public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
{
public override void Initialize()
{

View File

@@ -1,53 +0,0 @@
using Content.Client.BarSign.Ui;
using Content.Shared.BarSign;
using Content.Shared.Power;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.BarSign;
public sealed class BarSignSystem : VisualizerSystem<BarSignComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BarSignComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnAfterAutoHandleState(EntityUid uid, BarSignComponent component, ref AfterAutoHandleStateEvent args)
{
if (_ui.TryGetOpenUi<BarSignBoundUserInterface>(uid, BarSignUiKey.Key, out var bui))
bui.Update(component.Current);
UpdateAppearance(uid, component);
}
protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args)
{
UpdateAppearance(uid, component, args.Component, args.Sprite);
}
private void UpdateAppearance(EntityUid id, BarSignComponent sign, AppearanceComponent? appearance = null, SpriteComponent? sprite = null)
{
if (!Resolve(id, ref appearance, ref sprite))
return;
AppearanceSystem.TryGetData<bool>(id, PowerDeviceVisuals.Powered, out var powered, appearance);
if (powered
&& sign.Current != null
&& _prototypeManager.Resolve(sign.Current, out var proto))
{
SpriteSystem.LayerSetSprite((id, sprite), 0, proto.Icon);
sprite.LayerSetShader(0, "unshaded");
}
else
{
SpriteSystem.LayerSetRsiState((id, sprite), 0, "empty");
sprite.LayerSetShader(0, null, null);
}
}
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.BarSign;
using Content.Shared.Power;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.BarSign;
public sealed class BarSignVisualizerSystem : VisualizerSystem<BarSignComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args)
{
AppearanceSystem.TryGetData<bool>(uid, PowerDeviceVisuals.Powered, out var powered, args.Component);
AppearanceSystem.TryGetData<string>(uid, BarSignVisuals.BarSignPrototype, out var currentSign, args.Component);
if (powered
&& currentSign != null
&& _prototypeManager.Resolve<BarSignPrototype>(currentSign, out var proto))
{
SpriteSystem.LayerSetSprite((uid, args.Sprite), 0, proto.Icon);
args.Sprite?.LayerSetShader(0, "unshaded");
}
else
{
SpriteSystem.LayerSetRsiState((uid, args.Sprite), 0, "empty");
args.Sprite?.LayerSetShader(0, null, null);
}
}
}

View File

@@ -19,32 +19,27 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
var sign = EntMan.GetComponentOrNull<BarSignComponent>(Owner)?.Current is { } current
? _prototype.Index(current)
: null;
var allSigns = Shared.BarSign.BarSignSystem.GetAllBarSigns(_prototype)
var allSigns = BarSignSystem.GetAllBarSigns(_prototype)
.OrderBy(p => Loc.GetString(p.Name))
.ToList();
_menu = new(sign, allSigns);
_menu.OnSignSelected += id =>
{
SendMessage(new SetBarSignMessage(id));
SendPredictedMessage(new SetBarSignMessage(id));
};
_menu.OnClose += Close;
_menu.OpenCentered();
}
public void Update(ProtoId<BarSignPrototype>? sign)
public override void Update()
{
if (_prototype.Resolve(sign, out var signPrototype))
_menu?.UpdateState(signPrototype);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
if (!EntMan.TryGetComponent<BarSignComponent>(Owner, out var signComp))
return;
_menu?.Dispose();
if (_prototype.Resolve(signComp.Current, out var signPrototype))
_menu?.UpdateState(signPrototype);
}
}

View File

@@ -7,7 +7,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Serilog;
namespace Content.Client.Cargo.UI;

View File

@@ -1,16 +0,0 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class HyposprayStatusControlSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<HyposprayComponent>(ent => new HyposprayStatusControl(ent, _solutionContainers));
}
}

View File

@@ -0,0 +1,20 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class InjectorStatusControlSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<InjectorComponent>(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager));
}
}

View File

@@ -1,16 +0,0 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class InjectorSystem : SharedInjectorSystem
{
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
}
}

View File

@@ -48,6 +48,10 @@ namespace Content.Client.Chemistry.UI
(uint) _window.BottleDosage.Value, _window.LabelLine));
_window.BufferSortButton.OnPressed += _ => SendMessage(
new ChemMasterSortingTypeCycleMessage());
_window.OutputBufferDraw.OnPressed += _ => SendMessage(
new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.Internal));
_window.OutputBeakerDraw.OnPressed += _ => SendMessage(
new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.External));
for (uint i = 0; i < _window.PillTypeButtons.Length; i++)
{

View File

@@ -79,10 +79,13 @@
<!-- Packaging -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-packaging-text'}" />
<Label Text="{Loc 'chem-master-output-source'}" StyleClasses="LabelSecondaryColor" Margin="0 0 5 0"/>
<Button MinSize="80 0" Name="OutputBufferDraw" Access="Public" Text="{Loc 'chem-master-output-buffer-draw'}" ToggleMode="True" StyleClasses="OpenRight" />
<Button MinSize="80 0" Name="OutputBeakerDraw" Access="Public" Text="{Loc 'chem-master-output-beaker-draw'}" ToggleMode="True" StyleClasses="OpenLeft" />
<Control HorizontalExpand="True"/>
<Label Text="{Loc 'chem-master-window-buffer-label'}" />
<Label Name="BufferCurrentVolume" StyleClasses="LabelWeak" />
<!-- Output Draw Source -->
<Label Name="DrawSource"/>
<Label Name="BufferCurrentVolume" StyleClasses="LabelSecondaryColor" />
</BoxContainer>
<!-- Wrap the packaging info-->

View File

@@ -150,7 +150,17 @@ namespace Content.Client.Chemistry.UI
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
UpdatePanelInfo(castState);
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
switch (castState.DrawSource)
{
case ChemMasterDrawSource.Internal:
SetBufferText(castState.BufferCurrentVolume, "chem-master-output-buffer-draw");
break;
case ChemMasterDrawSource.External:
SetBufferText(castState.InputContainerInfo?.CurrentVolume, "chem-master-output-beaker-draw");
break;
default:
throw new($"Chemmaster {castState.OutputContainerInfo} draw source is not set");
}
InputEjectButton.Disabled = castState.InputContainerInfo is null;
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
@@ -168,9 +178,14 @@ namespace Content.Client.Chemistry.UI
var holdsReagents = output?.Reagents != null;
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
var outputVolume = castState.DrawSource switch
{
ChemMasterDrawSource.Internal => castState.BufferCurrentVolume?.Int() ?? 0,
ChemMasterDrawSource.External => castState.InputContainerInfo?.CurrentVolume.Int() ?? 0,
_ => 0,
};
PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
PillDosage.Value = (int)Math.Min(outputVolume, castState.PillDosageLimit);
PillTypeButtons[castState.SelectedPillType].Pressed = true;
@@ -186,25 +201,35 @@ namespace Content.Client.Chemistry.UI
// Avoid division by zero
if (PillDosage.Value > 0)
{
PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
PillNumber.Value = Math.Min(outputVolume / PillDosage.Value, pillNumberMax);
}
else
{
PillNumber.Value = 0;
}
BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
BottleDosage.Value = Math.Min(bottleAmountMax, outputVolume);
}
/// <summary>
/// Generate a product label based on reagents in the buffer.
/// Generate a product label based on reagents in the buffer or beaker.
/// </summary>
/// <param name="state">State data sent by the server.</param>
private string GenerateLabel(ChemMasterBoundUserInterfaceState state)
{
if (state.BufferCurrentVolume == 0)
if (
state.BufferCurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.Internal ||
state.InputContainerInfo?.CurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.External ||
state.InputContainerInfo?.Reagents == null
)
return "";
var reagent = state.BufferReagents.OrderBy(r => r.Quantity).First().Reagent;
var reagent = (state.DrawSource switch
{
ChemMasterDrawSource.Internal => state.BufferReagents,
ChemMasterDrawSource.External => state.InputContainerInfo.Reagents ?? [],
_ => throw new($"Chemmaster {state.OutputContainerInfo} draw source is not set"),
}).MinBy(r => r.Quantity)
.Reagent;
_prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
return proto?.LocalizedName ?? "";
}
@@ -233,6 +258,8 @@ namespace Content.Client.Chemistry.UI
_ => Loc.GetString("chem-master-window-sort-type-none")
};
OutputBufferDraw.Pressed = state.DrawSource == ChemMasterDrawSource.Internal;
OutputBeakerDraw.Pressed = state.DrawSource == ChemMasterDrawSource.External;
if (!state.BufferReagents.Any())
{
@@ -414,6 +441,12 @@ namespace Content.Client.Chemistry.UI
get => LabelLineEdit.Text;
set => LabelLineEdit.Text = value;
}
private void SetBufferText(FixedPoint2? volume, string text)
{
BufferCurrentVolume.Text = $" {volume ?? FixedPoint2.Zero}u";
DrawSource.Text = Loc.GetString(text);
}
}
public sealed class ReagentButton : Button

View File

@@ -1,58 +0,0 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Chemistry.UI;
public sealed class HyposprayStatusControl : Control
{
private readonly Entity<HyposprayComponent> _parent;
private readonly RichTextLabel _label;
private readonly SharedSolutionContainerSystem _solutionContainers;
private FixedPoint2 PrevVolume;
private FixedPoint2 PrevMaxVolume;
private bool PrevOnlyAffectsMobs;
public HyposprayStatusControl(Entity<HyposprayComponent> parent, SharedSolutionContainerSystem solutionContainers)
{
_parent = parent;
_solutionContainers = solutionContainers;
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
AddChild(_label);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
return;
// only updates the UI if any of the details are different than they previously were
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
&& PrevOnlyAffectsMobs == _parent.Comp.OnlyAffectsMobs)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs;
var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch
{
false => "hypospray-all-mode-text",
true => "hypospray-mobs-only-mode-text",
});
_label.SetMarkup(Loc.GetString("hypospray-volume-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized)));
}
}

View File

@@ -2,26 +2,32 @@ using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Chemistry.UI;
public sealed class InjectorStatusControl : Control
{
private readonly IPrototypeManager _prototypeManager;
private readonly Entity<InjectorComponent> _parent;
private readonly SharedSolutionContainerSystem _solutionContainers;
private readonly RichTextLabel _label;
private FixedPoint2 PrevVolume;
private FixedPoint2 PrevMaxVolume;
private FixedPoint2 PrevTransferAmount;
private InjectorToggleMode PrevToggleState;
private FixedPoint2 _prevVolume;
private FixedPoint2 _prevMaxVolume;
private FixedPoint2? _prevTransferAmount;
private InjectorBehavior _prevBehavior;
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers, IPrototypeManager prototypeManager)
{
_prototypeManager = prototypeManager;
_parent = parent;
_solutionContainers = solutionContainers;
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
@@ -32,33 +38,38 @@ public sealed class InjectorStatusControl : Control
{
base.FrameUpdate(args);
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)
|| !_prototypeManager.Resolve(_parent.Comp.ActiveModeProtoId, out var activeMode))
return;
// only updates the UI if any of the details are different than they previously were
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
&& PrevTransferAmount == _parent.Comp.CurrentTransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
if (_prevVolume == solution.Volume
&& _prevMaxVolume == solution.MaxVolume
&& _prevTransferAmount == _parent.Comp.CurrentTransferAmount
&& _prevBehavior == activeMode.Behavior)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
_prevVolume = solution.Volume;
_prevMaxVolume = solution.MaxVolume;
_prevTransferAmount = _parent.Comp.CurrentTransferAmount;
_prevBehavior = activeMode.Behavior;
// Update current volume and injector state
var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch
// Seeing transfer volume is only important for injectors that can change it.
if (activeMode.TransferAmounts.Count > 1 && _parent.Comp.CurrentTransferAmount.HasValue)
{
InjectorToggleMode.Draw => "injector-draw-text",
InjectorToggleMode.Inject => "injector-inject-text",
_ => "injector-invalid-injector-toggle-mode"
});
_label.SetMarkup(Loc.GetString("injector-volume-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized),
("transferVolume", _parent.Comp.CurrentTransferAmount)));
_label.SetMarkup(Loc.GetString("injector-volume-transfer-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", Loc.GetString(activeMode.Name)),
("transferVolume", _parent.Comp.CurrentTransferAmount.Value)));
}
else
{
_label.SetMarkup(Loc.GetString("injector-volume-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", Loc.GetString(activeMode.Name))));
}
}
}

View File

@@ -2,41 +2,31 @@ using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.Chemistry.UI
namespace Content.Client.Chemistry.UI;
[UsedImplicitly]
public sealed class TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[UsedImplicitly]
public sealed class TransferAmountBoundUserInterface : BoundUserInterface
[ViewVariables]
private TransferAmountWindow? _window;
protected override void Open()
{
private IEntityManager _entManager;
private EntityUid _owner;
[ViewVariables]
private TransferAmountWindow? _window;
base.Open();
_window = this.CreateWindow<TransferAmountWindow>();
public TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
if (EntMan.TryGetComponent<SolutionTransferComponent>(Owner, out var comp))
_window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
_window.ApplyButton.OnPressed += _ =>
{
_owner = owner;
_entManager = IoCManager.Resolve<IEntityManager>();
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<TransferAmountWindow>();
if (_entManager.TryGetComponent<SolutionTransferComponent>(_owner, out var comp))
_window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
_window.ApplyButton.OnPressed += _ =>
if (int.TryParse(_window.AmountLineEdit.Text, out var i))
{
if (int.TryParse(_window.AmountLineEdit.Text, out var i))
{
SendMessage(new TransferAmountSetValueMessage(FixedPoint2.New(i)));
_window.Close();
}
};
}
SendPredictedMessage(new TransferAmountSetValueMessage(FixedPoint2.New(i)));
_window.Close();
}
};
}
}

View File

@@ -3,34 +3,33 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Chemistry.UI
namespace Content.Client.Chemistry.UI;
[GenerateTypedNameReferences]
public sealed partial class TransferAmountWindow : DefaultWindow
{
[GenerateTypedNameReferences]
public sealed partial class TransferAmountWindow : DefaultWindow
private int _max = Int32.MaxValue;
private int _min = 1;
public TransferAmountWindow()
{
private int _max = Int32.MaxValue;
private int _min = 1;
RobustXamlLoader.Load(this);
AmountLineEdit.OnTextChanged += OnValueChanged;
}
public TransferAmountWindow()
{
RobustXamlLoader.Load(this);
AmountLineEdit.OnTextChanged += OnValueChanged;
}
public void SetBounds(int min, int max)
{
_min = min;
_max = max;
MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
}
public void SetBounds(int min, int max)
{
_min = min;
_max = max;
MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
}
private void OnValueChanged(LineEdit.LineEditEventArgs args)
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
ApplyButton.Disabled = true;
else
ApplyButton.Disabled = false;
}
private void OnValueChanged(LineEdit.LineEditEventArgs args)
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
ApplyButton.Disabled = true;
else
ApplyButton.Disabled = false;
}
}

View File

@@ -4,10 +4,8 @@ using Content.Shared.Weapons.Ranged.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Serialization;
using Robust.Client.UserInterface;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Utility;
namespace Content.Client.CombatMode;

View File

@@ -30,7 +30,10 @@ namespace Content.Client.Construction.UI
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly SpriteSystem _spriteSystem;
private readonly ISawmill _sawmill;
private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem;
@@ -90,6 +93,7 @@ namespace Content.Client.Construction.UI
_constructionView = new ConstructionMenu();
_whitelistSystem = _entManager.System<EntityWhitelistSystem>();
_spriteSystem = _entManager.System<SpriteSystem>();
_sawmill = _logManager.GetSawmill("construction.ui");
// This is required so that if we load after the system is initialized, we can bind to it immediately
if (_systemManager.TryGetEntitySystem<ConstructionSystem>(out var constructionSystem))
@@ -284,7 +288,7 @@ namespace Content.Client.Construction.UI
if (!_constructionSystem!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
{
Logger.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
_sawmill.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
recipe.ID,
nameof(ConstructionPrototype));
continue;

View File

@@ -1,13 +1,11 @@
using System.Linq;
using Content.Client.Materials;
using Content.Client.Materials.UI;
using Content.Client.Message;
using Content.Client.UserInterface.Controls;
using Content.Shared.Construction.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Materials;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -61,57 +59,48 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
!_itemSlots.TryGetSlot(_owner, flatpacker.SlotId, out var itemSlot))
return;
var flatpackerEnt = (_owner, flatpacker);
if (flatpacker.Packing)
{
PackButton.Disabled = true;
}
else if (_currentBoard != null)
{
Dictionary<string, int> cost;
if (_entityManager.TryGetComponent<MachineBoardComponent>(_currentBoard, out var machineBoardComp))
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp));
else
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
PackButton.Disabled = !_materialStorage.CanChangeMaterialAmount(_owner, cost);
PackButton.Disabled = !_flatpack.TryGetFlatpackCreationCost(flatpackerEnt, _currentBoard.Value, out var curCost)
|| !_materialStorage.CanChangeMaterialAmount(_owner, curCost);
}
if (_currentBoard == itemSlot.Item)
return;
_currentBoard = itemSlot.Item;
CostHeaderLabel.Visible = _currentBoard != null;
CostHeaderLabel.Visible = false;
InsertLabel.Visible = _currentBoard == null;
if (_currentBoard is not null)
if (_currentBoard is null)
{
string? prototype = null;
Dictionary<string, int>? cost = null;
MachineSprite.SetPrototype(NoBoardEffectId);
CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
MachineNameLabel.SetMessage(string.Empty);
PackButton.Disabled = true;
return;
}
if (_entityManager.TryGetComponent<MachineBoardComponent>(_currentBoard, out var newMachineBoardComp))
{
prototype = newMachineBoardComp.Prototype;
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, newMachineBoardComp));
}
else if (_entityManager.TryGetComponent<ComputerBoardComponent>(_currentBoard, out var computerBoard))
{
prototype = computerBoard.Prototype;
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
}
if (prototype is not null && cost is not null)
{
var proto = _prototypeManager.Index<EntityPrototype>(prototype);
MachineSprite.SetPrototype(prototype);
MachineNameLabel.SetMessage(proto.Name);
CostLabel.SetMarkup(GetCostString(cost));
}
if (_flatpack.TryGetFlatpackResultPrototype(_currentBoard.Value, out var prototype) &&
_flatpack.TryGetFlatpackCreationCost(flatpackerEnt, _currentBoard.Value, out var cost))
{
var proto = _prototypeManager.Index<EntityPrototype>(prototype);
MachineSprite.SetPrototype(prototype);
MachineNameLabel.SetMessage(proto.Name);
CostLabel.SetMarkup(GetCostString(cost));
CostHeaderLabel.Visible = true;
}
else
{
MachineSprite.SetPrototype(NoBoardEffectId);
CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
MachineNameLabel.SetMessage(" ");
CostLabel.SetMarkup(Loc.GetString("flatpacker-ui-board-invalid-label"));
MachineNameLabel.SetMessage(string.Empty);
PackButton.Disabled = true;
}
}

View File

@@ -1,26 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Client\</OutputPath>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Tools;DebugOpt</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Nett" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="Pidgin" />
<PackageReference Include="Robust.Shared.AuthLib" />
</ItemGroup>
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Shared\Content.Corvax.Interfaces.Shared.csproj" />
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Client\Content.Corvax.Interfaces.Client.csproj" />

View File

@@ -89,16 +89,28 @@ public sealed class TTSSystem : EntitySystem
{
// Corvax-Wega-SoundInsolation-Start
if (!TryGetEntity(ev.SourceUid.Value, out var sourceEntityOpt) || !sourceEntityOpt.HasValue)
{
_contentRoot.RemoveFile(filePath);
return;
}
var sourceEntity = sourceEntityOpt.Value;
if (!Exists(sourceEntity) || Deleted(sourceEntity))
{
_contentRoot.RemoveFile(filePath);
return;
}
float volumeMultiplier = 1f;
if (_player.LocalEntity != null && Exists(_player.LocalEntity.Value))
{
var insulation = _soundInsulation.GetSoundInsulation(sourceEntity, _player.LocalEntity.Value);
if (insulation >= 0.95f)
{
_contentRoot.RemoveFile(filePath);
return;
}
if (insulation > 0.1f && insulation < 0.95f)
{

View File

@@ -73,7 +73,19 @@ public sealed class PuddleSystem : SharedPuddleSystem
// Maybe someday we'll have clientside prediction for entity spawning, but not today.
// Until then, these methods do nothing on the client.
/// <inheritdoc/>
public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
public override bool TrySplashSpillAt(Entity<SpillableComponent?> entity, EntityCoordinates coordinates, out EntityUid puddleUid, out Solution solution, bool sound = true, EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
solution = new Solution();
return false;
}
public override bool TrySplashSpillAt(EntityUid entity,
EntityCoordinates coordinates,
Solution spilled,
out EntityUid puddleUid,
bool sound = true,
EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
return false;

View File

@@ -42,7 +42,7 @@ public sealed class GuidebookRichPrototypeLink : Control, IPrototypeLinkControl
public void SetMessage(FormattedMessage message)
{
_message = message;
_richTextLabel.SetMessage(_message);
_richTextLabel.SetMessage(_message, tagsAllowed: null);
}
public IPrototype? LinkedPrototype { get; set; }

View File

@@ -82,7 +82,7 @@ public sealed partial class DocumentParsingManager
}
msg.Pop();
rt.SetMessage(msg);
rt.SetMessage(msg, tagsAllowed: null);
return rt;
},
TextParser)

View File

@@ -0,0 +1,53 @@
<BoxContainer
xmlns="https://spacestation14.io"
VerticalExpand="True"
Orientation="Vertical">
<Label
Name="NoPatientDataText"
Text="{Loc health-analyzer-window-no-patient-data-text}" />
<BoxContainer
Name="PatientDataContainer"
Margin="0 0 0 5"
Orientation="Vertical">
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
<TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
<RichTextLabel Name="NameLabel" SetWidth="150" />
<Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />
</BoxContainer>
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
VerticalAlignment="Top" Name="ScanModeLabel"
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" />
<GridContainer Margin="0 5 0 0" Columns="2">
<Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
<Label Name="StatusLabel" />
<Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
<Label Name="TemperatureLabel" />
<Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
<Label Name="BloodLabel" />
<Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
<Label Name="DamageLabel" />
</GridContainer>
</BoxContainer>
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer
Name="GroupsContainer"
Margin="0 5 0 5"
Orientation="Vertical">
</BoxContainer>
</BoxContainer>

View File

@@ -0,0 +1,241 @@
using System.Linq;
using System.Numerics;
using Content.Shared.Atmos;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.IdentityManagement;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.HealthAnalyzer.UI;
// Health analyzer UI is split from its window because it's used by both the
// health analyzer item and the cryo pod UI.
[GenerateTypedNameReferences]
public sealed partial class HealthAnalyzerControl : BoxContainer
{
private readonly IEntityManager _entityManager;
private readonly SpriteSystem _spriteSystem;
private readonly IPrototypeManager _prototypes;
private readonly IResourceCache _cache;
public HealthAnalyzerControl()
{
RobustXamlLoader.Load(this);
var dependencies = IoCManager.Instance!;
_entityManager = dependencies.Resolve<IEntityManager>();
_spriteSystem = _entityManager.System<SpriteSystem>();
_prototypes = dependencies.Resolve<IPrototypeManager>();
_cache = dependencies.Resolve<IResourceCache>();
}
public void Populate(HealthAnalyzerUiState state)
{
var target = _entityManager.GetEntity(state.TargetEntity);
if (target == null
|| !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
{
NoPatientDataText.Visible = true;
return;
}
NoPatientDataText.Visible = false;
// Scan Mode
ScanModeLabel.Text = state.ScanMode.HasValue
? state.ScanMode.Value
? Loc.GetString("health-analyzer-window-scan-mode-active")
: Loc.GetString("health-analyzer-window-scan-mode-inactive")
: Loc.GetString("health-analyzer-window-entity-unknown-text");
ScanModeLabel.FontColorOverride = state.ScanMode.HasValue && state.ScanMode.Value ? Color.Green : Color.Red;
// Patient Information
SpriteView.SetEntity(target.Value);
SpriteView.Visible = state.ScanMode.HasValue && state.ScanMode.Value;
NoDataTex.Visible = !SpriteView.Visible;
var name = new FormattedMessage();
name.PushColor(Color.White);
name.AddText(_entityManager.HasComponent<MetaDataComponent>(target.Value)
? Identity.Name(target.Value, _entityManager)
: Loc.GetString("health-analyzer-window-entity-unknown-text"));
NameLabel.SetMessage(name);
SpeciesLabel.Text =
_entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
out var humanoidAppearanceComponent)
? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
// Basic Diagnostic
TemperatureLabel.Text = !float.IsNaN(state.Temperature)
? $"{state.Temperature - Atmospherics.T0C:F1} °C ({state.Temperature:F1} K)"
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
BloodLabel.Text = !float.IsNaN(state.BloodLevel)
? $"{state.BloodLevel * 100:F1} %"
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
StatusLabel.Text =
_entityManager.TryGetComponent<MobStateComponent>(target.Value, out var mobStateComponent)
? GetStatus(mobStateComponent.CurrentState)
: Loc.GetString("health-analyzer-window-entity-unknown-text");
// Total Damage
DamageLabel.Text = damageable.TotalDamage.ToString();
// Alerts
var showAlerts = state.Unrevivable == true || state.Bleeding == true;
AlertsDivider.Visible = showAlerts;
AlertsContainer.Visible = showAlerts;
if (showAlerts)
AlertsContainer.RemoveAllChildren();
if (state.Unrevivable == true)
AlertsContainer.AddChild(new RichTextLabel
{
Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
Margin = new Thickness(0, 4),
MaxWidth = 300
});
if (state.Bleeding == true)
AlertsContainer.AddChild(new RichTextLabel
{
Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
Margin = new Thickness(0, 4),
MaxWidth = 300
});
// Damage Groups
var damageSortedGroups =
damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
.ToDictionary(x => x.Key, x => x.Value);
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
}
private static string GetStatus(MobState mobState)
{
return mobState switch
{
MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
_ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
};
}
private void DrawDiagnosticGroups(
Dictionary<string, FixedPoint2> groups,
IReadOnlyDictionary<string, FixedPoint2> damageDict)
{
GroupsContainer.RemoveAllChildren();
foreach (var (damageGroupId, damageAmount) in groups)
{
if (damageAmount == 0)
continue;
var groupTitleText = $"{Loc.GetString(
"health-analyzer-window-damage-group-text",
("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
("amount", damageAmount)
)}";
var groupContainer = new BoxContainer
{
Align = AlignMode.Begin,
Orientation = LayoutOrientation.Vertical,
};
groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
GroupsContainer.AddChild(groupContainer);
// Show the damage for each type in that group.
var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
foreach (var type in group.DamageTypes)
{
if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
continue;
var damageString = Loc.GetString(
"health-analyzer-window-damage-type-text",
("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
("amount", typeAmount)
);
groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
}
}
}
private Texture GetTexture(string texture)
{
var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
var rsi = _cache.GetResource<RSIResource>(rsiSprite.RsiPath).RSI;
if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
{
rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
}
return _spriteSystem.Frame0(rsiSprite);
}
private static Label CreateDiagnosticItemLabel(string text)
{
return new Label
{
Text = text,
};
}
private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
{
var rootContainer = new BoxContainer
{
Margin = new Thickness(0, 6, 0, 0),
VerticalAlignment = VAlignment.Bottom,
Orientation = LayoutOrientation.Horizontal,
};
rootContainer.AddChild(new TextureRect
{
SetSize = new Vector2(30, 30),
Texture = GetTexture(id.ToLower())
});
rootContainer.AddChild(CreateDiagnosticItemLabel(text));
return rootContainer;
}
}

View File

@@ -1,68 +1,15 @@
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:ui="clr-namespace:Content.Client.HealthAnalyzer.UI"
MaxHeight="525"
MinWidth="300">
<ScrollContainer
Margin="5 5 5 5"
ReturnMeasure="True"
VerticalExpand="True">
<BoxContainer
Name="RootContainer"
VerticalExpand="True"
Orientation="Vertical">
<Label
Name="NoPatientDataText"
Text="{Loc health-analyzer-window-no-patient-data-text}" />
<BoxContainer
Name="PatientDataContainer"
Margin="0 0 0 5"
Orientation="Vertical">
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
<TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
<RichTextLabel Name="NameLabel" SetWidth="150" />
<Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />
</BoxContainer>
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
VerticalAlignment="Top" Name="ScanModeLabel"
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" />
<GridContainer Margin="0 5 0 0" Columns="2">
<Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
<Label Name="StatusLabel" />
<Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
<Label Name="TemperatureLabel" />
<Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
<Label Name="BloodLabel" />
<Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
<Label Name="DamageLabel" />
<!-- Corvax-Wega-Disease-start -->
<Label Text="{Loc 'health-analyzer-window-entity-disease-text'}" />
<Label Name="StatusEffectsLabel" />
<!-- Corvax-Wega-Disease-end -->
</GridContainer>
</BoxContainer>
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer
Name="GroupsContainer"
Margin="0 5 0 5"
Orientation="Vertical">
</BoxContainer>
</BoxContainer>
<ui:HealthAnalyzerControl
Name="HealthAnalyzer"/>
</ScrollContainer>
</controls:FancyWindow>

View File

@@ -1,250 +1,20 @@
using System.Linq;
using System.Numerics;
using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Disease.Components; // Corvax-Wega-Disease
using Content.Shared.FixedPoint;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.IdentityManagement;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.ResourceManagement;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.HealthAnalyzer.UI
namespace Content.Client.HealthAnalyzer.UI;
[GenerateTypedNameReferences]
public sealed partial class HealthAnalyzerWindow : FancyWindow
{
[GenerateTypedNameReferences]
public sealed partial class HealthAnalyzerWindow : FancyWindow
public HealthAnalyzerWindow()
{
private readonly IEntityManager _entityManager;
private readonly SpriteSystem _spriteSystem;
private readonly IPrototypeManager _prototypes;
private readonly IResourceCache _cache;
RobustXamlLoader.Load(this);
}
public HealthAnalyzerWindow()
{
RobustXamlLoader.Load(this);
var dependencies = IoCManager.Instance!;
_entityManager = dependencies.Resolve<IEntityManager>();
_spriteSystem = _entityManager.System<SpriteSystem>();
_prototypes = dependencies.Resolve<IPrototypeManager>();
_cache = dependencies.Resolve<IResourceCache>();
}
public void Populate(HealthAnalyzerScannedUserMessage msg)
{
var target = _entityManager.GetEntity(msg.TargetEntity);
if (target == null
|| !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
{
NoPatientDataText.Visible = true;
return;
}
NoPatientDataText.Visible = false;
// Scan Mode
ScanModeLabel.Text = msg.ScanMode.HasValue
? msg.ScanMode.Value
? Loc.GetString("health-analyzer-window-scan-mode-active")
: Loc.GetString("health-analyzer-window-scan-mode-inactive")
: Loc.GetString("health-analyzer-window-entity-unknown-text");
ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red;
// Patient Information
SpriteView.SetEntity(target.Value);
SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
NoDataTex.Visible = !SpriteView.Visible;
var name = new FormattedMessage();
name.PushColor(Color.White);
name.AddText(_entityManager.HasComponent<MetaDataComponent>(target.Value)
? Identity.Name(target.Value, _entityManager)
: Loc.GetString("health-analyzer-window-entity-unknown-text"));
NameLabel.SetMessage(name);
SpeciesLabel.Text =
_entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
out var humanoidAppearanceComponent)
? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
// Basic Diagnostic
TemperatureLabel.Text = !float.IsNaN(msg.Temperature)
? $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)"
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
BloodLabel.Text = !float.IsNaN(msg.BloodLevel)
? $"{msg.BloodLevel * 100:F1} %"
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
StatusLabel.Text =
_entityManager.TryGetComponent<MobStateComponent>(target.Value, out var mobStateComponent)
? GetStatus(mobStateComponent.CurrentState)
: Loc.GetString("health-analyzer-window-entity-unknown-text");
// Total Damage
DamageLabel.Text = damageable.TotalDamage.ToString();
// Corvax-Wega-Disease-start
// Status Effects / Components
StatusEffectsLabel.Text =
_entityManager.HasComponent<DiseasedComponent>(target)
? Loc.GetString("disease-scanner-diseased")
: Loc.GetString("disease-scanner-not-diseased");
// Corvax-Wega-Disease-end
// Alerts
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true;
AlertsDivider.Visible = showAlerts;
AlertsContainer.Visible = showAlerts;
if (showAlerts)
AlertsContainer.RemoveAllChildren();
if (msg.Unrevivable == true)
AlertsContainer.AddChild(new RichTextLabel
{
Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
Margin = new Thickness(0, 4),
MaxWidth = 300
});
if (msg.Bleeding == true)
AlertsContainer.AddChild(new RichTextLabel
{
Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
Margin = new Thickness(0, 4),
MaxWidth = 300
});
// Damage Groups
var damageSortedGroups =
damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
.ToDictionary(x => x.Key, x => x.Value);
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
}
private static string GetStatus(MobState mobState)
{
return mobState switch
{
MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
_ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
};
}
private void DrawDiagnosticGroups(
Dictionary<string, FixedPoint2> groups,
IReadOnlyDictionary<string, FixedPoint2> damageDict)
{
GroupsContainer.RemoveAllChildren();
foreach (var (damageGroupId, damageAmount) in groups)
{
if (damageAmount == 0)
continue;
var groupTitleText = $"{Loc.GetString(
"health-analyzer-window-damage-group-text",
("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
("amount", damageAmount)
)}";
var groupContainer = new BoxContainer
{
Align = BoxContainer.AlignMode.Begin,
Orientation = BoxContainer.LayoutOrientation.Vertical,
};
groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
GroupsContainer.AddChild(groupContainer);
// Show the damage for each type in that group.
var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
foreach (var type in group.DamageTypes)
{
if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
continue;
var damageString = Loc.GetString(
"health-analyzer-window-damage-type-text",
("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
("amount", typeAmount)
);
groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
}
}
}
private Texture GetTexture(string texture)
{
var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
var rsi = _cache.GetResource<RSIResource>(rsiSprite.RsiPath).RSI;
if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
{
rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
}
return _spriteSystem.Frame0(rsiSprite);
}
private static Label CreateDiagnosticItemLabel(string text)
{
return new Label
{
Text = text,
};
}
private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
{
var rootContainer = new BoxContainer
{
Margin = new Thickness(0, 6, 0, 0),
VerticalAlignment = VAlignment.Bottom,
Orientation = BoxContainer.LayoutOrientation.Horizontal,
};
rootContainer.AddChild(new TextureRect
{
SetSize = new Vector2(30, 30),
Texture = GetTexture(id.ToLower())
});
rootContainer.AddChild(CreateDiagnosticItemLabel(text));
return rootContainer;
}
public void Populate(HealthAnalyzerScannedUserMessage msg)
{
HealthAnalyzer.Populate(msg.State);
}
}

View File

@@ -18,7 +18,7 @@ public sealed partial class InfoSection : BoxContainer
{
TitleLabel.Text = title;
if (markup)
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()), tagsAllowed: null);
else
Content.SetMessage(text);
}

View File

@@ -24,7 +24,7 @@ namespace Content.Client.Info
}
public void SetInfoBlob(string markup)
{
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup), tagsAllowed: null);
}
}
}

View File

@@ -0,0 +1,7 @@
using Content.Shared.Kitchen.EntitySystems;
using JetBrains.Annotations;
namespace Content.Client.Kitchen.EntitySystems;
[UsedImplicitly]
public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;

View File

@@ -20,8 +20,10 @@ namespace Content.Client.Launcher
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClipboardManager _clipboard = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private LauncherConnectingGui? _control;
private ISawmill _sawmill = default!;
private Page _currentPage;
private string? _connectFailReason;
@@ -61,6 +63,8 @@ namespace Content.Client.Launcher
{
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _clipboard);
_sawmill = _logManager.GetSawmill("launcher-ui");
_userInterfaceManager.StateRoot.AddChild(_control);
_clientNetManager.ConnectFailed += OnConnectFailed;
@@ -115,12 +119,12 @@ namespace Content.Client.Launcher
}
else
{
Logger.InfoS("launcher-ui", $"Redial not possible, no Ss14Address");
_sawmill.Info($"Redial not possible, no Ss14Address");
}
}
catch (Exception ex)
{
Logger.ErrorS("launcher-ui", $"Redial exception: {ex}");
_sawmill.Error($"Redial exception: {ex}");
}
return false;
}

View File

@@ -1,6 +1,5 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Prometheus;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;

View File

@@ -0,0 +1,285 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
// ReSharper disable CompareOfFloatsByEqualityOperator
namespace Content.Client.Medical.Cryogenics;
public sealed class BeakerBarChart : Control
{
private sealed class Entry
{
public float WidthFraction; // This entry's width as a fraction of the chart's total width (between 0 and 1)
public float TargetAmount;
public string Uid; // This UID is used to track entries between frames, for animation.
public string? Tooltip;
public Color Color;
public Label Label;
public Entry(string uid, Label label)
{
Uid = uid;
Label = label;
}
}
public float Capacity = 50;
public Color NotchColor = new(1, 1, 1, 0.25f);
public Color BackgroundColor = new(0.1f, 0.1f, 0.1f);
public int MediumNotchInterval = 5;
public int BigNotchInterval = 10;
// When we have a very large beaker (i.e. bluespace beaker) we might need to increase the distance between notches.
// The distance between notches is increased by ScaleMultiplier when the distance between notches is less than
// MinSmallNotchScreenDistance in UI units.
public int MinSmallNotchScreenDistance = 2;
public int ScaleMultiplier = 10;
public float SmallNotchHeight = 0.1f;
public float MediumNotchHeight = 0.25f;
public float BigNotchHeight = 1f;
// We don't animate new entries until this control has been drawn at least once.
private bool _hasBeenDrawn = false;
// This is used to keep the segments of the chart in the same order as the SetEntry calls.
// For example: In update 1 we might get cryox, alox, bic (in that order), and in update 2 we get alox, cryox, bic.
// To keep the order of the entries the same as the order of the SetEntry calls, we let the old cryox entry
// disappear and create a new cryox entry behind the alox entry.
private int _nextUpdateableEntry = 0;
private readonly List<Entry> _entries = new();
public BeakerBarChart()
{
MouseFilter = MouseFilterMode.Pass;
TooltipSupplier = SupplyTooltip;
}
public void Clear()
{
foreach (var entry in _entries)
{
entry.TargetAmount = 0;
}
_nextUpdateableEntry = 0;
}
/// <summary>
/// Either adds a new entry to the chart if the UID doesn't appear yet, or updates the amount of an existing entry.
/// </summary>
public void SetEntry(
string uid,
string label,
float amount,
Color color,
Color? textColor = null,
string? tooltip = null)
{
// If we can find an old entry we're allowed to update, update that one.
if (TryFindUpdateableEntry(uid, out var index))
{
_entries[index].TargetAmount = amount;
_entries[index].Tooltip = tooltip;
_entries[index].Label.Text = label;
_nextUpdateableEntry = index + 1;
return;
}
// Otherwise create a new entry.
if (amount <= 0)
return;
// If no text color is provided, use either white or black depending on how dark the background is.
textColor ??= (color.R + color.G + color.B < 1.5f ? Color.White : Color.Black);
var childLabel = new Label
{
Text = label,
ClipText = true,
FontColorOverride = textColor,
Margin = new Thickness(4, 0, 0, 0)
};
AddChild(childLabel);
_entries.Insert(
_nextUpdateableEntry,
new Entry(uid, childLabel)
{
WidthFraction = (_hasBeenDrawn ? 0 : amount / Capacity),
TargetAmount = amount,
Tooltip = tooltip,
Color = color
}
);
_nextUpdateableEntry += 1;
}
private bool TryFindUpdateableEntry(string uid, out int index)
{
for (int i = _nextUpdateableEntry; i < _entries.Count; i++)
{
if (_entries[i].Uid == uid)
{
index = i;
return true;
}
}
index = -1;
return false;
}
private IEnumerable<(Entry, float xMin, float xMax)> EntryRanges(float? pixelWidth = null)
{
float chartWidth = pixelWidth ?? PixelWidth;
var xStart = 0f;
foreach (var entry in _entries)
{
var entryWidth = entry.WidthFraction * chartWidth;
var xEnd = MathF.Min(xStart + entryWidth, chartWidth);
yield return (entry, xStart, xEnd);
xStart = xEnd;
}
}
private bool TryFindEntry(float x, [NotNullWhen(true)] out Entry? entry)
{
foreach (var (currentEntry, xMin, xMax) in EntryRanges())
{
if (xMin <= x && x < xMax)
{
entry = currentEntry;
return true;
}
}
entry = null;
return false;
}
protected override void FrameUpdate(FrameEventArgs args)
{
// Tween the amounts to their target amounts.
const float tweenInverseHalfLife = 8; // Half life of tween is 1/n
var hasChanged = false;
foreach (var entry in _entries)
{
var targetWidthFraction = entry.TargetAmount / Capacity;
if (entry.WidthFraction == targetWidthFraction)
continue;
// Tween with lerp abuse interpolation
entry.WidthFraction = MathHelper.Lerp(
entry.WidthFraction,
targetWidthFraction,
MathHelper.Clamp01(tweenInverseHalfLife * args.DeltaSeconds)
);
hasChanged = true;
if (MathF.Abs(entry.WidthFraction - targetWidthFraction) < 0.0001f)
entry.WidthFraction = targetWidthFraction;
}
if (!hasChanged)
return;
InvalidateArrange();
// Remove old entries whose animations have finished.
foreach (var entry in _entries)
{
if (entry.WidthFraction == 0 && entry.TargetAmount == 0)
RemoveChild(entry.Label);
}
_entries.RemoveAll(entry => entry.WidthFraction == 0 && entry.TargetAmount == 0);
}
protected override void MouseMove(GUIMouseMoveEventArgs args)
{
HideTooltip();
}
protected override void Draw(DrawingHandleScreen handle)
{
handle.DrawRect(PixelSizeBox, BackgroundColor);
// Draw the entry backgrounds
foreach (var (entry, xMin, xMax) in EntryRanges())
{
if (xMin != xMax)
handle.DrawRect(new(xMin, 0, xMax, PixelHeight), entry.Color);
}
// Draw notches
var unitWidth = PixelWidth / Capacity;
var unitsPerNotch = 1;
while (unitWidth < MinSmallNotchScreenDistance)
{
// This is here for 1000u bluespace beakers. If the distance between small notches is so small that it would
// be very ugly, we reduce the amount of notches by ScaleMultiplier (currently a factor of 10).
// (I could use an analytical algorithm here, but it would be more difficult to read with pretty much no
// performance benefit, since it loops zero times normally and one time for the bluespace beaker)
unitWidth *= ScaleMultiplier;
unitsPerNotch *= ScaleMultiplier;
}
for (int i = 0; i <= Capacity / unitsPerNotch; i++)
{
var x = i * unitWidth;
var height = (i % BigNotchInterval == 0 ? BigNotchHeight :
i % MediumNotchInterval == 0 ? MediumNotchHeight :
SmallNotchHeight) * PixelHeight;
var start = new Vector2(x, PixelHeight);
var end = new Vector2(x, PixelHeight - height);
handle.DrawLine(start, end, NotchColor);
}
_hasBeenDrawn = true;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
foreach (var (entry, xMin, xMax) in EntryRanges(finalSize.X))
{
entry.Label.Arrange(new((int)xMin, 0, (int)xMax, (int)finalSize.Y));
}
return finalSize;
}
private Control? SupplyTooltip(Control sender)
{
var globalMousePos = UserInterfaceManager.MousePositionScaled.Position;
var mousePos = globalMousePos - GlobalPosition;
if (!TryFindEntry(mousePos.X, out var entry) || entry.Tooltip == null)
return null;
var msg = new FormattedMessage();
msg.AddText(entry.Tooltip);
var tooltip = new Tooltip();
tooltip.SetMessage(msg);
return tooltip;
}
}

View File

@@ -0,0 +1,53 @@
using Content.Shared.FixedPoint;
using Content.Shared.Medical.Cryogenics;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Medical.Cryogenics;
[UsedImplicitly]
public sealed class CryoPodBoundUserInterface : BoundUserInterface
{
private CryoPodWindow? _window;
public CryoPodBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindowCenteredLeft<CryoPodWindow>();
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_window.OnEjectPatientPressed += EjectPatientPressed;
_window.OnEjectBeakerPressed += EjectBeakerPressed;
_window.OnInjectPressed += InjectPressed;
}
private void EjectPatientPressed()
{
var isLocked =
EntMan.TryGetComponent<CryoPodComponent>(Owner, out var cryoComp)
&& cryoComp.Locked;
_window?.SetEjectErrorVisible(isLocked);
SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectPatient));
}
private void EjectBeakerPressed()
{
SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectBeaker));
}
private void InjectPressed(FixedPoint2 transferAmount)
{
SendMessage(new CryoPodInjectUiMessage(transferAmount));
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (_window != null && message is CryoPodUserMessage cryoMsg)
{
_window.Populate(cryoMsg);
}
}
}

View File

@@ -6,7 +6,6 @@ namespace Content.Client.Medical.Cryogenics;
public sealed class CryoPodSystem : SharedCryoPodSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
@@ -46,8 +45,8 @@ public sealed class CryoPodSystem : SharedCryoPodSystem
return;
}
if (!_appearance.TryGetData<bool>(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
|| !_appearance.TryGetData<bool>(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
if (!Appearance.TryGetData<bool>(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
|| !Appearance.TryGetData<bool>(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
{
return;
}
@@ -64,6 +63,11 @@ public sealed class CryoPodSystem : SharedCryoPodSystem
_sprite.LayerSetVisible((uid, args.Sprite), CryoPodVisualLayers.Cover, true);
}
}
protected override void UpdateUi(Entity<CryoPodComponent> cryoPod)
{
// Atmos and health scanner aren't predicted currently...
}
}
public enum CryoPodVisualLayers : byte

View File

@@ -0,0 +1,232 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:health="clr-namespace:Content.Client.HealthAnalyzer.UI"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:cryogenics="clr-namespace:Content.Client.Medical.Cryogenics"
MinSize="250 300"
Resizable="False">
<Label Name="LoadingPlaceHolder"
Text="{Loc 'cryo-pod-window-loading'}"
Align="Center"
HorizontalExpand="True"
VerticalExpand="True"/>
<BoxContainer Name="Sections"
Orientation="Horizontal"
Visible="False"
Margin="10"
SeparationOverride="16">
<BoxContainer Name="CryoSection"
VerticalExpand="True"
Orientation="Vertical"
MinWidth="250"
MaxWidth="250">
<!-- Flavor text -->
<BoxContainer Orientation="Horizontal"
SeparationOverride="10"
Margin="8 0 0 8">
<TextureRect StyleClasses="NTLogoDark"
VerticalExpand="True"
Stretch="KeepAspectCentered"
SetSize="32 32"/>
<BoxContainer Orientation="Vertical"
SeparationOverride="-4">
<Label Text="{Loc 'cryo-pod-window-product-name'}"
StyleClasses="FontLarge"/>
<Label Text="{Loc 'cryo-pod-window-product-subtitle'}"
StyleClasses="LabelSubText"/>
</BoxContainer>
</BoxContainer>
<!-- Atmos info -->
<BoxContainer Orientation="Horizontal"
SeparationOverride="20"
Margin="0 0 0 4">
<!-- Pressure -->
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'gas-analyzer-window-pressure-text'}"
StyleClasses="LabelSubText"/>
<Label Name="Pressure"/>
</BoxContainer>
<!-- Temperature -->
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'gas-analyzer-window-temperature-text'}"
StyleClasses="LabelSubText"/>
<Label Name="Temperature"/>
</BoxContainer>
</BoxContainer>
<!-- Gas mix -->
<Control Margin="0 0 0 22">
<controls:SplitBar Name="GasMixChart"
MinHeight="8"
MaxHeight="8"/>
</Control>
<!-- Warnings & status -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
Align="Center"
Margin="0 0 0 14"
SeparationOverride="20">
<!-- Ejection error (if the pod is locked) -->
<PanelContainer Name="EjectError"
Visible="False"
HorizontalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical"
Margin="6">
<Label Text="{Loc 'cryo-pod-window-error-header'}"
FontColorOverride="orange"
Align="Center"/>
<RichTextLabel Text="{Loc 'cryo-pod-window-eject-error'}"/>
</BoxContainer>
</PanelContainer>
<!-- Pressure warning -->
<PanelContainer Name="LowPressureWarning"
Visible="False"
HorizontalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical"
Margin="6">
<Label Text="{Loc 'cryo-pod-window-warning-header'}"
FontColorOverride="orange"
Align="Center"/>
<RichTextLabel Text="{Loc 'cryo-pod-window-low-pressure-warning'}"/>
</BoxContainer>
</PanelContainer>
<!-- Temperature warning -->
<PanelContainer Name="HighTemperatureWarning"
Visible="False"
HorizontalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical"
Margin="6">
<Label Text="{Loc 'cryo-pod-window-warning-header'}"
FontColorOverride="orange"
Align="Center"/>
<!-- Note: This placeholder text should never be visible. -->
<RichTextLabel Name="HighTemperatureWarningText"
Text="Temperature too high."/>
</BoxContainer>
</PanelContainer>
<!-- Status checklist -->
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal"
SeparationOverride="8">
<Label Text="{Loc 'cryo-pod-window-status'}"/>
<Label Name="StatusLabel"
Text="{Loc 'cryo-pod-window-status-not-ready'}"
FontColorOverride="Orange"/>
</BoxContainer>
<GridContainer Columns="2"
HSeparationOverride="0"
VSeparationOverride="6"
Margin="6 3 0 0">
<Label Text="⋄"
StyleClasses="LabelSubText"/>
<Label Name="PressureCheck"
Text="{Loc 'cryo-pod-window-checklist-pressure'}"
StyleClasses="LabelSubText"/>
<Label Text="⋄"
StyleClasses="LabelSubText"/>
<Label Name="ChemicalsCheck"
Text="{Loc 'cryo-pod-window-checklist-chemicals'}"
StyleClasses="LabelSubText"
FontColorOverride="Orange"/>
<Label Text="⋄"
StyleClasses="LabelSubText"/>
<Label Name="TemperatureCheck"
Text="{Loc 'cryo-pod-window-checklist-temperature'}"
StyleClasses="LabelSubText"/>
</GridContainer>
</BoxContainer>
</BoxContainer>
<!-- Reagents -->
<Control HorizontalExpand="True"
MinHeight="30">
<Label Name="NoBeakerText"
Text="{Loc 'cryo-pod-window-chems-no-beaker'}"
FontColorOverride="Gray"
VerticalExpand="True"
VAlign="Center"/>
<cryogenics:BeakerBarChart Name="ChemicalsChart"
HorizontalExpand="True"
VerticalExpand="True"/>
</Control>
<!-- Buttons -->
<BoxContainer Orientation="Vertical"
Margin="-2 2 -2 0">
<BoxContainer Orientation="Horizontal">
<Button Name="Inject1"
Text="{Loc 'cryo-pod-window-inject-1u'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenBoth"/>
<Button Name="Inject5"
Text="{Loc 'cryo-pod-window-inject-5u'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenBoth"/>
<Button Name="Inject10"
Text="{Loc 'cryo-pod-window-inject-10u'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenBoth"/>
<Button Name="Inject20"
Text="{Loc 'cryo-pod-window-inject-20u'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenBoth"/>
<Button Name="EjectBeakerButton"
Text="{Loc 'cryo-pod-window-eject-beaker'}"
Disabled="True"
StyleClasses="OpenBoth"/>
</BoxContainer>
<Button Name="EjectPatientButton"
Text="{Loc 'cryo-pod-window-eject-patient'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenRight"/>
</BoxContainer>
</BoxContainer>
<BoxContainer Name="HealthSection"
VerticalExpand="True"
Orientation="Vertical">
<health:HealthAnalyzerControl Name="HealthAnalyzer"/>
<!-- This label is used to deal with a stray hline at the end of the health analyzer UI -->
<Label Name="NoDamageText"
Text="{Loc 'cryo-pod-window-health-no-damage'}"
FontColorOverride="DeepSkyBlue"/>
<Control VerticalExpand="True"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,260 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage.Components;
using Content.Shared.EntityConditions.Conditions;
using Content.Shared.FixedPoint;
using Content.Shared.Medical.Cryogenics;
using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.Medical.Cryogenics;
[GenerateTypedNameReferences]
public sealed partial class CryoPodWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public event Action? OnEjectPatientPressed;
public event Action? OnEjectBeakerPressed;
public event Action<FixedPoint2>? OnInjectPressed;
public CryoPodWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
EjectPatientButton.OnPressed += _ => OnEjectPatientPressed?.Invoke();
EjectBeakerButton.OnPressed += _ => OnEjectBeakerPressed?.Invoke();
Inject1.OnPressed += _ => OnInjectPressed?.Invoke(1);
Inject5.OnPressed += _ => OnInjectPressed?.Invoke(5);
Inject10.OnPressed += _ => OnInjectPressed?.Invoke(10);
Inject20.OnPressed += _ => OnInjectPressed?.Invoke(20);
}
public void Populate(CryoPodUserMessage msg)
{
// Loading screen
if (LoadingPlaceHolder.Visible)
{
LoadingPlaceHolder.Visible = false;
Sections.Visible = true;
}
// Atmosphere
var hasCorrectPressure = (msg.GasMix.Pressure > Atmospherics.WarningLowPressure);
var hasGas = (msg.GasMix.Pressure > Atmospherics.GasMinMoles);
var showsPressureWarning = !hasCorrectPressure;
LowPressureWarning.Visible = showsPressureWarning;
Pressure.Text = Loc.GetString("gas-analyzer-window-pressure-val-text",
("pressure", $"{msg.GasMix.Pressure:0.00}"));
Temperature.Text = Loc.GetString("generic-not-available-shorthand");
if (hasGas)
{
var celsius = TemperatureHelpers.KelvinToCelsius(msg.GasMix.Temperature);
Temperature.Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
("tempK", $"{msg.GasMix.Temperature:0.0}"),
("tempC", $"{celsius:0.0}"));
}
// Gas mix segmented bar chart
GasMixChart.Clear();
GasMixChart.Visible = hasGas;
if (msg.GasMix.Gases != null)
{
var totalGasAmount = msg.GasMix.Gases.Sum(gas => gas.Amount);
foreach (var gas in msg.GasMix.Gases)
{
var color = Color.FromHex($"#{gas.Color}", Color.White);
var percent = gas.Amount / totalGasAmount * 100;
var localizedName = Loc.GetString(gas.Name);
var tooltip = Loc.GetString("gas-analyzer-window-molarity-percentage-text",
("gasName", localizedName),
("amount", $"{gas.Amount:0.##}"),
("percentage", $"{percent:0.#}"));
GasMixChart.AddEntry(gas.Amount, color, tooltip: tooltip);
}
}
// Health analyzer
var maybePatient = _entityManager.GetEntity(msg.Health.TargetEntity);
var hasPatient = msg.Health.TargetEntity.HasValue;
var hasDamage = (hasPatient
&& _entityManager.TryGetComponent(maybePatient, out DamageableComponent? damageable)
&& damageable.TotalDamage > 0);
NoDamageText.Visible = (hasPatient && !hasDamage);
HealthSection.Visible = hasPatient;
EjectPatientButton.Disabled = !hasPatient;
if (hasPatient)
HealthAnalyzer.Populate(msg.Health);
// Reagents
float? lowestTempRequirement = null;
ReagentId? lowestTempReagent = null;
var totalBeakerCapacity = msg.BeakerCapacity ?? 0;
var availableQuantity = new FixedPoint2();
var injectingQuantity =
msg.Injecting?.Aggregate(new FixedPoint2(), (sum, r) => sum + r.Quantity)
?? new FixedPoint2(); // Either the sum of the reagent quantities in `msg.Injecting` or zero.
var hasBeaker = (msg.Beaker != null);
ChemicalsChart.Clear();
ChemicalsChart.Capacity = (totalBeakerCapacity < 1 ? 50 : (int)totalBeakerCapacity);
var chartMaxChemsQuantity = ChemicalsChart.Capacity - injectingQuantity; // Ensure space for injection buffer
if (hasBeaker)
{
foreach (var (reagent, quantity) in msg.Beaker!)
{
availableQuantity += quantity;
// Make sure we don't add too many chemicals to the chart, so that there's still enough space to
// visualize the injection buffer.
var chemsQuantityOvershoot = FixedPoint2.Max(0, availableQuantity - chartMaxChemsQuantity);
var chartQuantity = FixedPoint2.Max(0, quantity - chemsQuantityOvershoot);
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
ChemicalsChart.SetEntry(
reagent.Prototype,
reagentProto.LocalizedName,
(float)chartQuantity,
reagentProto.SubstanceColor,
tooltip: $"{quantity}u {reagentProto.LocalizedName}"
);
var temp = TryFindMaxTemperatureRequirement(reagent);
if (lowestTempRequirement == null
|| temp < lowestTempRequirement)
{
lowestTempRequirement = temp;
lowestTempReagent = reagent;
}
}
}
if (injectingQuantity != 0)
{
var injectingText = (injectingQuantity > 1 ? $"{injectingQuantity}u" : "");
ChemicalsChart.SetEntry(
"injecting",
injectingText,
(float)injectingQuantity,
Color.MediumSpringGreen,
tooltip: Loc.GetString("cryo-pod-window-chems-injecting-tooltip",
("quantity", injectingQuantity))
);
}
var isBeakerEmpty = (injectingQuantity + availableQuantity == 0);
var isChemicalsChartVisible = (hasBeaker || injectingQuantity != 0);
NoBeakerText.Visible = !isChemicalsChartVisible;
ChemicalsChart.Visible = isChemicalsChartVisible;
Inject1.Disabled = (!hasPatient || availableQuantity < 0.1f);
Inject5.Disabled = (!hasPatient || availableQuantity <= 1);
Inject10.Disabled = (!hasPatient || availableQuantity <= 5);
Inject20.Disabled = (!hasPatient || availableQuantity <= 10);
EjectBeakerButton.Disabled = !hasBeaker;
// Temperature warning
var hasCorrectTemperature = (lowestTempRequirement == null || lowestTempRequirement > msg.GasMix.Temperature);
var showsTemperatureWarning = (!showsPressureWarning && !hasCorrectTemperature);
HighTemperatureWarning.Visible = showsTemperatureWarning;
if (showsTemperatureWarning)
{
var reagentName = _prototypeManager.Index<ReagentPrototype>(lowestTempReagent!.Value.Prototype)
.LocalizedName;
HighTemperatureWarningText.Text = Loc.GetString("cryo-pod-window-high-temperature-warning",
("reagent", reagentName),
("temperature", lowestTempRequirement!));
}
// Status checklist
const float fallbackTemperatureRequirement = 213;
var hasTemperatureCheck = (hasGas && hasCorrectTemperature
&& (lowestTempRequirement != null || msg.GasMix.Temperature < fallbackTemperatureRequirement));
var hasChemicals = (hasBeaker && !isBeakerEmpty);
UpdateChecklistItem(PressureCheck, Loc.GetString("cryo-pod-window-checklist-pressure"), hasCorrectPressure);
UpdateChecklistItem(ChemicalsCheck, Loc.GetString("cryo-pod-window-checklist-chemicals"), hasChemicals);
UpdateChecklistItem(TemperatureCheck, Loc.GetString("cryo-pod-window-checklist-temperature"), hasTemperatureCheck);
var isReady = (hasCorrectPressure && hasChemicals && hasTemperatureCheck);
var isCooling = (lowestTempRequirement != null && hasPatient
&& msg.Health.Temperature > lowestTempRequirement);
var isInjecting = (injectingQuantity > 0);
StatusLabel.Text = (!isReady ? Loc.GetString("cryo-pod-window-status-not-ready") :
isCooling ? Loc.GetString("cryo-pod-window-status-cooling") :
isInjecting ? Loc.GetString("cryo-pod-window-status-injecting") :
hasPatient ? Loc.GetString("cryo-pod-window-status-ready-to-inject") :
Loc.GetString("cryo-pod-window-status-ready-for-patient"));
StatusLabel.FontColorOverride = (isReady ? Color.DeepSkyBlue : Color.Orange);
}
private void UpdateChecklistItem(Label label, string text, bool isOkay)
{
label.Text = (isOkay ? text : Loc.GetString("cryo-pod-window-checklist-fail", ("item", text)));
label.FontColorOverride = (isOkay ? null : Color.Orange);
}
private float? TryFindMaxTemperatureRequirement(ReagentId reagent)
{
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
if (reagentProto.Metabolisms == null)
return null;
float? result = null;
foreach (var (_, metabolism) in reagentProto.Metabolisms)
{
foreach (var effect in metabolism.Effects)
{
if (effect.Conditions == null)
continue;
foreach (var condition in effect.Conditions)
{
// If there are multiple temperature conditions in the same reagent (which could hypothetically
// happen, although it currently doesn't), we return the lowest max temperature.
if (condition is TemperatureCondition tempCondition
&& float.IsFinite(tempCondition.Max)
&& (result == null || tempCondition.Max < result))
{
result = tempCondition.Max;
}
}
}
}
return result;
}
public void SetEjectErrorVisible(bool isVisible)
{
EjectError.Visible = isVisible;
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
const float antiJiggleSlackSpace = 80;
var oldSize = DesiredSize;
var newSize = base.MeasureOverride(availableSize);
// Reduce how often the height of the window jiggles
if (newSize.Y < oldSize.Y && newSize.Y + antiJiggleSlackSpace > oldSize.Y)
newSize.Y = oldSize.Y;
return newSize;
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Medical;
namespace Content.Client.Medical;
public sealed class DefibrillatorSystem : SharedDefibrillatorSystem;

View File

@@ -41,9 +41,9 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay
if (!Colors.TryGetValue(uid, out var color))
{
color = new Color(
_random.Next(0, 255),
_random.Next(0, 255),
_random.Next(0, 255));
_random.NextByte(0, 255),
_random.NextByte(0, 255),
_random.NextByte(0, 255));
Colors.Add(uid, color);
}

View File

@@ -1,8 +1,6 @@
using Content.Shared.Inventory.Events;
using Content.Shared.Overlays;
using Robust.Client.Graphics;
using System.Linq;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
@@ -35,6 +33,9 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
{
base.UpdateInternal(component);
_overlay.DamageContainers.Clear();
_overlay.StatusIcon = null;
foreach (var comp in component.Components)
{
foreach (var damageContainerId in comp.DamageContainers)

View File

@@ -5,7 +5,6 @@ using Content.Shared.Overlays;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
using System.Linq;
using Content.Shared.Damage.Components;
namespace Content.Client.Overlays;
@@ -32,9 +31,13 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
{
base.UpdateInternal(component);
foreach (var damageContainerId in component.Components.SelectMany(x => x.DamageContainers))
DamageContainers.Clear();
foreach (var comp in component.Components)
{
DamageContainers.Add(damageContainerId);
foreach (var damageContainerId in comp.DamageContainers)
{
DamageContainers.Add(damageContainerId);
}
}
}

View File

@@ -17,7 +17,11 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
base.Open();
EntityUid? gridUid = null;
if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.TargetGrid != null)
{
gridUid = comp.TargetGrid;
}
else if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
{
gridUid = xform.GridUid;
}
@@ -30,8 +34,8 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
{
stationName = gridMetaData.EntityName;
}
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.ShowLocation)
if (comp != null && comp.ShowLocation)
_window.Set(stationName, gridUid, Owner);
else
_window.Set(stationName, gridUid, null);

View File

@@ -3,5 +3,6 @@ namespace Content.Client.Power;
/// Remains in use by portable scrubbers and lathes.
public enum PowerDeviceVisualLayers : byte
{
Powered
Powered,
Charging
}

View File

@@ -32,8 +32,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
_cfg.GetCVar(CCVars.ICShowSSDIndicator) &&
!_mobState.IsDead(uid) &&
!HasComp<ActiveNPCComponent>(uid) &&
TryComp<MindContainerComponent>(uid, out var mindContainer) &&
mindContainer.ShowExamineInfo)
HasComp<MindExaminableComponent>(uid))
{
args.StatusIcons.Add(_prototype.Index(component.Icon));
}

View File

@@ -16,7 +16,7 @@ public sealed class GenpopLockerBoundUserInterface(EntityUid owner, Enum uiKey)
_menu.OnConfigurationComplete += (name, time, crime) =>
{
SendMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
SendPredictedMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
Close();
};

View File

@@ -23,7 +23,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
_window = this.CreateWindowCenteredLeft<IFFConsoleWindow>();
_window.ShowIFF += SendIFFMessage;
_window.ShowVessel += SendVesselMessage;
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -44,14 +43,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
});
}
private void SendVesselMessage(bool obj)
{
SendMessage(new IFFShowVesselMessage()
{
Show = obj,
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@@ -9,12 +9,6 @@
<Button Name="ShowIFFOnButton" Text="{Loc 'iff-console-on'}" StyleClasses="OpenRight" />
<Button Name="ShowIFFOffButton" Text="{Loc 'iff-console-off'}" StyleClasses="OpenLeft" />
</BoxContainer>
<Label Name="ShowVesselLabel" Text="{Loc 'iff-console-show-vessel-label'}" HorizontalExpand="True" StyleClasses="highlight" />
<BoxContainer Orientation="Horizontal" MinWidth="120">
<Button Name="ShowVesselOnButton" Text="{Loc 'iff-console-on'}" StyleClasses="OpenRight" />
<Button Name="ShowVesselOffButton" Text="{Loc 'iff-console-off'}" StyleClasses="OpenLeft" />
</BoxContainer>
</GridContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -13,9 +13,7 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
IComputerWindow<IFFConsoleBoundUserInterfaceState>
{
private readonly ButtonGroup _showIFFButtonGroup = new();
private readonly ButtonGroup _showVesselButtonGroup = new();
public event Action<bool>? ShowIFF;
public event Action<bool>? ShowVessel;
public IFFConsoleWindow()
{
@@ -24,11 +22,6 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFFOnButton.Group = _showIFFButtonGroup;
ShowIFFOnButton.OnPressed += args => ShowIFFPressed(true);
ShowIFFOffButton.OnPressed += args => ShowIFFPressed(false);
ShowVesselOffButton.Group = _showVesselButtonGroup;
ShowVesselOnButton.Group = _showVesselButtonGroup;
ShowVesselOnButton.OnPressed += args => ShowVesselPressed(true);
ShowVesselOffButton.OnPressed += args => ShowVesselPressed(false);
}
private void ShowIFFPressed(bool pressed)
@@ -36,19 +29,14 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFF?.Invoke(pressed);
}
private void ShowVesselPressed(bool pressed)
{
ShowVessel?.Invoke(pressed);
}
public void UpdateState(IFFConsoleBoundUserInterfaceState state)
{
if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0)
if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0 || (state.AllowedFlags & IFFFlags.Hide) != 0x0)
{
ShowIFFOffButton.Disabled = false;
ShowIFFOnButton.Disabled = false;
if ((state.Flags & IFFFlags.HideLabel) != 0x0)
if ((state.Flags & IFFFlags.HideLabel) != 0x0 || (state.Flags & IFFFlags.Hide) != 0x0)
{
ShowIFFOffButton.Pressed = true;
}
@@ -62,25 +50,5 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFFOffButton.Disabled = true;
ShowIFFOnButton.Disabled = true;
}
if ((state.AllowedFlags & IFFFlags.Hide) != 0x0)
{
ShowVesselOffButton.Disabled = false;
ShowVesselOnButton.Disabled = false;
if ((state.Flags & IFFFlags.Hide) != 0x0)
{
ShowVesselOffButton.Pressed = true;
}
else
{
ShowVesselOnButton.Pressed = true;
}
}
else
{
ShowVesselOffButton.Disabled = true;
ShowVesselOnButton.Disabled = true;
}
}
}

View File

@@ -153,7 +153,7 @@ public sealed partial class MapScreen : BoxContainer
break;
}
if (IsFTLBlocked())
if (IsPingBlocked())
{
MapRebuildButton.Disabled = true;
ClearMapObjects();
@@ -408,9 +408,21 @@ public sealed partial class MapScreen : BoxContainer
}
}
/// <summary>
/// Returns true if we shouldn't be able to select the Scan for Objects button.
/// </summary>
private bool IsPingBlocked()
{
return _state switch
{
FTLState.Available or FTLState.Cooldown => false,
_ => true,
};
}
private void OnMapObjectPress(IMapObject mapObject)
{
if (IsFTLBlocked())
if (IsPingBlocked())
return;
var coordinates = _shuttles.GetMapCoordinates(mapObject);
@@ -506,7 +518,7 @@ public sealed partial class MapScreen : BoxContainer
BumpMapDequeue();
}
if (!IsFTLBlocked() && _nextPing < curTime)
if (!IsPingBlocked() && _nextPing < curTime)
{
MapRebuildButton.Disabled = false;
}

View File

@@ -21,7 +21,7 @@ public sealed partial class BorgMenu : FancyWindow
[Dependency] private readonly IEntityManager _entity = default!;
private readonly NameModifierSystem _nameModifier;
private readonly PowerCellSystem _powerCell;
private readonly PredictedBatterySystem _battery;
private readonly SharedBatterySystem _battery;
public Action? BrainButtonPressed;
public Action? EjectBatteryButtonPressed;
@@ -44,7 +44,7 @@ public sealed partial class BorgMenu : FancyWindow
_nameModifier = _entity.System<NameModifierSystem>();
_powerCell = _entity.System<PowerCellSystem>();
_battery = _entity.System<PredictedBatterySystem>();
_battery = _entity.System<SharedBatterySystem>();
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);

View File

@@ -3,10 +3,10 @@
StyleClasses="PanelLight"
Margin="5 5 5 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 5 0 5">
<TextureButton Name="RemoveButton" VerticalAlignment="Top" HorizontalAlignment="Left" Scale="0.5 0.5"/>
<SpriteView Name="ModuleView" Margin="0 0 5 0"/>
<BoxContainer RectClipContent="True" HorizontalExpand="True">
<Label Name="ModuleName" HorizontalExpand="True" HorizontalAlignment="Center"/>
</BoxContainer>
<TextureButton Name="RemoveButton" VerticalAlignment="Top" HorizontalAlignment="Right" Scale="0.5 0.5"/>
</BoxContainer>
</PanelContainer>

View File

@@ -18,7 +18,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;

View File

@@ -19,12 +19,13 @@ public sealed class SmartFridgeBoundUserInterface : BoundUserInterface
_menu = this.CreateWindow<SmartFridgeMenu>();
_menu.OnItemSelected += OnItemSelected;
_menu.OnRemoveButtonPressed += data => SendPredictedMessage(new SmartFridgeRemoveEntryMessage(data.Entry));
Refresh();
}
public void Refresh()
{
if (_menu is not {} menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge))
if (_menu is not { } menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge))
return;
menu.SetFlavorText(Loc.GetString(fridge.FlavorText));

View File

@@ -13,4 +13,10 @@
SizeFlagsStretchRatio="3"
HorizontalExpand="True"
ClipText="True"/>
<TextureButton Name="RemoveButton"
StyleClasses="CrossButtonRed"
VerticalAlignment="Center"
Margin="0 0 10 0"
Scale="0.75 0.75"
Visible="True" />
</BoxContainer>

View File

@@ -8,11 +8,18 @@ namespace Content.Client.SmartFridge;
[GenerateTypedNameReferences]
public sealed partial class SmartFridgeItem : BoxContainer
{
public Action? RemoveButtonPressed;
public SmartFridgeItem(EntityUid uid, string text)
{
RobustXamlLoader.Load(this);
EntityView.SetEntity(uid);
NameLabel.Text = text;
RemoveButton.OnPressed += _ => RemoveButtonPressed?.Invoke();
if (uid.IsValid())
RemoveButton.Visible = false;
}
}

View File

@@ -17,6 +17,7 @@ public sealed partial class SmartFridgeMenu : FancyWindow
[Dependency] private readonly IEntityManager _entityManager = default!;
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
public event Action<SmartFridgeListData>? OnRemoveButtonPressed;
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
@@ -48,8 +49,10 @@ public sealed partial class SmartFridgeMenu : FancyWindow
return;
var label = Loc.GetString("smart-fridge-list-item", ("item", entry.Entry.Name), ("amount", entry.Amount));
button.AddChild(new SmartFridgeItem(entry.Representative, label));
var item = new SmartFridgeItem(entry.Representative, label);
item.RemoveButtonPressed += () => OnRemoveButtonPressed?.Invoke(entry);
button.AddChild(item);
button.ToolTip = label;
button.StyleBoxOverride = _styleBox;
}

View File

@@ -0,0 +1,18 @@
using Content.Shared.SmartFridge;
namespace Content.Client.SmartFridge;
public sealed class SmartFridgeSystem : SharedSmartFridgeSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
protected override void UpdateUI(Entity<SmartFridgeComponent> ent)
{
base.UpdateUI(ent);
if (!_uiSystem.TryGetOpenUi<SmartFridgeBoundUserInterface>(ent.Owner, SmartFridgeUiKey.Key, out var bui))
return;
bui.Refresh();
}
}

View File

@@ -1,24 +0,0 @@
using Content.Shared.SmartFridge;
using Robust.Shared.Analyzers;
namespace Content.Client.SmartFridge;
public sealed class SmartFridgeUISystem : EntitySystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SmartFridgeComponent, AfterAutoHandleStateEvent>(OnSmartFridgeAfterState);
}
private void OnSmartFridgeAfterState(Entity<SmartFridgeComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!_uiSystem.TryGetOpenUi<SmartFridgeBoundUserInterface>(ent.Owner, SmartFridgeUiKey.Key, out var bui))
return;
bui.Refresh();
}
}

View File

@@ -69,10 +69,10 @@ public sealed class LabelSheetlet : Sheetlet<PalettedStylesheet>
.Class(StyleClass.LabelMonospaceText)
.Prop(Label.StylePropertyFont, robotoMonoBold11),
E<Label>()
.Class(StyleClass.LabelMonospaceHeading)
.Class(StyleClass.LabelMonospaceSubHeading)
.Prop(Label.StylePropertyFont, robotoMonoBold12),
E<Label>()
.Class(StyleClass.LabelMonospaceSubHeading)
.Class(StyleClass.LabelMonospaceHeading)
.Prop(Label.StylePropertyFont, robotoMonoBold14),
];
}

View File

@@ -41,8 +41,8 @@ public static class StyleClass
public const string LabelKeyText = "LabelKeyText";
public const string LabelWeak = "LabelWeak"; // replaces `LabelSecondaryColor`
public const string LabelMonospaceText = "ConsoleText";
public const string LabelMonospaceHeading = "ConsoleText";
public const string LabelMonospaceSubHeading = "ConsoleText";
public const string LabelMonospaceHeading = "ConsoleHeading";
public const string LabelMonospaceSubHeading = "ConsoleSubHeading";
public const string BackgroundPanel = "BackgroundPanel"; // replaces `AngleRect`
public const string BackgroundPanelOpenLeft = "BackgroundPanelOpenLeft"; // replaces `BackgroundOpenLeft`

View File

@@ -34,11 +34,17 @@ public sealed class SurveillanceCameraMonitorBoundUserInterface : BoundUserInter
_window.SubnetRefresh += OnSubnetRefresh;
_window.CameraSwitchTimer += OnCameraSwitchTimer;
_window.CameraDisconnect += OnCameraDisconnect;
var xform = EntMan.GetComponent<TransformComponent>(Owner);
var gridUid = xform.GridUid ?? xform.MapUid;
if (gridUid is not null)
_window?.SetMap(gridUid.Value);
}
private void OnCameraSelected(string address)
private void OnCameraSelected(string address, string? subnet)
{
SendMessage(new SurveillanceCameraMonitorSwitchMessage(address));
SendMessage(new SurveillanceCameraMonitorSwitchMessage(address, subnet));
}
private void OnSubnetRequest(string subnet)

View File

@@ -1,25 +1,71 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:viewport="clr-namespace:Content.Client.Viewport"
xmlns:local="clr-namespace:Content.Client.SurveillanceCamera.UI"
Title="{Loc 'surveillance-camera-monitor-ui-window'}">
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical" MinWidth="350" VerticalExpand="True">
<!-- lazy -->
<OptionButton Name="SubnetSelector" />
<Button Name="SubnetRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}" />
<ScrollContainer VerticalExpand="True">
<ItemList Name="SubnetList" />
</ScrollContainer>
<Button Name="CameraRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-cameras'}" />
<Button Name="CameraDisconnectButton" Text="{Loc 'surveillance-camera-monitor-ui-disconnect'}" />
<Label Name="CameraStatus" />
<BoxContainer>
<!-- Panel with tabs -->
<BoxContainer Orientation="Vertical" MinWidth="350">
<TabContainer Name="ViewModeTabs" VerticalExpand="True">
<!-- Camera list tab -->
<BoxContainer Name="{Loc 'surveillance-camera-monitor-ui-tab-list'}" Orientation="Vertical">
<OptionButton Name="SubnetSelector"/>
<Button Name="SubnetRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}"/>
<ScrollContainer VerticalExpand="True">
<ItemList Name="SubnetList"/>
</ScrollContainer>
<Button Name="CameraRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-cameras'}"/>
</BoxContainer>
<!-- Map view tab -->
<BoxContainer Name="{Loc 'surveillance-camera-monitor-ui-tab-map'}" Orientation="Vertical" VerticalExpand="True">
<local:SurveillanceCameraNavMapControl Name="CameraMap"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="350 350"/>
<!-- Map legend -->
<BoxContainer Name="LegendContainer" Margin="0 10">
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
Modulate="#FF00FF"
SetSize="20 20"
Margin="10 0 5 0"/>
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-active'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
SetSize="20 20"
Modulate="#fbff19ff"
Margin="10 0 5 0"/>
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-selected'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
SetSize="20 20"
Modulate="#a09f9fff"
Margin="10 0 5 0"/>
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-inactive'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_square.png"
SetSize="20 20"
Modulate="#fa1f1fff"
Margin="10 0 5 0"/>
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-invalid'}"/>
</BoxContainer>
<Button Name="SubnetRefreshButtonMap" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}"/>
</BoxContainer>
</TabContainer>
<Button Name="CameraDisconnectButton" Text="{Loc 'surveillance-camera-monitor-ui-disconnect'}"/>
</BoxContainer>
<!-- Right panel with camera view -->
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<Label Name="CameraStatus"/>
<Control VerticalExpand="True" Margin="5" Name="CameraViewBox">
<viewport:ScalingViewport Name="CameraView" MinSize="500 500" MouseFilter="Ignore"/>
<TextureRect MinSize="500 500" Name="CameraViewBackground" />
</Control>
</BoxContainer>
<Control VerticalExpand="True" HorizontalExpand="True" Margin="5 5 5 5" Name="CameraViewBox">
<viewport:ScalingViewport Name="CameraView"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="500 500"
MouseFilter="Ignore" />
<TextureRect VerticalExpand="True" HorizontalExpand="True" MinSize="500 500" Name="CameraViewBackground" />
</Control>
</BoxContainer>
</DefaultWindow>

View File

@@ -3,6 +3,7 @@ using Content.Client.Resources;
using Content.Client.Viewport;
using Content.Shared.DeviceNetwork;
using Content.Shared.SurveillanceCamera;
using Content.Shared.SurveillanceCamera.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -21,8 +22,15 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
/// <summary>
/// Triggered when a camera is selected.
/// First parameter contains the camera's address.
/// Second optional parameter contains a subnet - if possible, the monitor will switch to this subnet.
/// </summary>
public event Action<string, string?>? CameraSelected;
public event Action<string>? CameraSelected;
public event Action<string>? SubnetOpened;
public event Action? CameraRefresh;
public event Action? SubnetRefresh;
@@ -33,6 +41,7 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
private bool _isSwitching;
private readonly FixedEye _defaultEye = new();
private readonly Dictionary<string, int> _subnetMap = new();
private EntityUid? _mapUid;
private string? SelectedSubnet
{
@@ -68,11 +77,15 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
SubnetSelector.OnItemSelected += args =>
{
// piss
SubnetOpened!((string) args.Button.GetItemMetadata(args.Id)!);
SubnetOpened?.Invoke((string) args.Button.GetItemMetadata(args.Id)!);
};
SubnetRefreshButton.OnPressed += _ => SubnetRefresh!();
CameraRefreshButton.OnPressed += _ => CameraRefresh!();
CameraDisconnectButton.OnPressed += _ => CameraDisconnect!();
SubnetRefreshButton.OnPressed += _ => SubnetRefresh?.Invoke();
SubnetRefreshButtonMap.OnPressed += _ => SubnetRefresh?.Invoke();
CameraRefreshButton.OnPressed += _ => CameraRefresh?.Invoke();
CameraDisconnectButton.OnPressed += _ => CameraDisconnect?.Invoke();
CameraMap.EnableCameraSelection = true;
CameraMap.CameraSelected += OnCameraMapSelected;
}
@@ -80,6 +93,9 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
// pass it here so that the UI can change its view.
public void UpdateState(IEye? eye, HashSet<string> subnets, string activeAddress, string activeSubnet, Dictionary<string, string> cameras)
{
CameraMap.SetActiveCameraAddress(activeAddress);
CameraMap.SetAvailableSubnets(subnets);
_currentAddress = activeAddress;
SetCameraView(eye);
@@ -189,6 +205,25 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
private void OnSubnetListSelect(ItemList.ItemListSelectedEventArgs args)
{
CameraSelected!((string) SubnetList[args.ItemIndex].Metadata!);
CameraSelected!((string) SubnetList[args.ItemIndex].Metadata!, null);
}
public void SetMap(EntityUid mapUid)
{
CameraMap.MapUid = _mapUid = mapUid;
}
private void OnCameraMapSelected(NetEntity netEntity)
{
if (_mapUid is null || !_entityManager.TryGetComponent<SurveillanceCameraMapComponent>(_mapUid.Value, out var mapComp))
return;
if (!mapComp.Cameras.TryGetValue(netEntity, out var marker) || !marker.Active)
return;
if (!string.IsNullOrEmpty(marker.Address))
CameraSelected?.Invoke(marker.Address, marker.Subnet);
else
_entityManager.RaisePredictiveEvent(new RequestCameraMarkerUpdateMessage(netEntity));
}
}

View File

@@ -0,0 +1,130 @@
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Map;
using Content.Client.Pinpointer.UI;
using Content.Client.Resources;
using Content.Shared.SurveillanceCamera.Components;
namespace Content.Client.SurveillanceCamera.UI;
public sealed class SurveillanceCameraNavMapControl : NavMapControl
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private static readonly Color CameraActiveColor = Color.FromHex("#FF00FF");
private static readonly Color CameraInactiveColor = Color.FromHex("#a09f9fff");
private static readonly Color CameraSelectedColor = Color.FromHex("#fbff19ff");
private static readonly Color CameraInvalidColor = Color.FromHex("#fa1f1fff");
private readonly Texture _activeTexture;
private readonly Texture _inactiveTexture;
private readonly Texture _selectedTexture;
private readonly Texture _invalidTexture;
private string _activeCameraAddress = string.Empty;
private HashSet<string> _availableSubnets = new();
private (Dictionary<NetEntity, CameraMarker> Cameras, string ActiveAddress, HashSet<string> AvailableSubnets) _lastState;
public bool EnableCameraSelection { get; set; }
public event Action<NetEntity>? CameraSelected;
public SurveillanceCameraNavMapControl()
{
IoCManager.InjectDependencies(this);
_activeTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_triangle.png");
_selectedTexture = _activeTexture;
_inactiveTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_circle.png");
_invalidTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_square.png");
TrackedEntitySelectedAction += entity =>
{
if (entity.HasValue)
CameraSelected?.Invoke(entity.Value);
};
}
public void SetActiveCameraAddress(string address)
{
if (_activeCameraAddress == address)
return;
_activeCameraAddress = address;
ForceNavMapUpdate();
}
public void SetAvailableSubnets(HashSet<string> subnets)
{
if (_availableSubnets.SetEquals(subnets))
return;
_availableSubnets = subnets;
ForceNavMapUpdate();
}
protected override void UpdateNavMap()
{
base.UpdateNavMap();
if (MapUid is null || !_entityManager.TryGetComponent<SurveillanceCameraMapComponent>(MapUid, out var mapComp))
return;
var currentState = (mapComp.Cameras, _activeCameraAddress, _availableSubnets);
if (_lastState.Equals(currentState))
return;
_lastState = currentState;
UpdateCameraMarkers(mapComp);
}
private void UpdateCameraMarkers(SurveillanceCameraMapComponent mapComp)
{
TrackedEntities.Clear();
if (MapUid is null)
return;
foreach (var (netEntity, marker) in mapComp.Cameras)
{
if (!marker.Visible || !_availableSubnets.Contains(marker.Subnet))
continue;
var coords = new EntityCoordinates(MapUid.Value, marker.Position);
Texture texture;
Color color;
if (string.IsNullOrEmpty(marker.Address))
{
color = CameraInvalidColor;
texture = _invalidTexture;
}
else if (marker.Address == _activeCameraAddress)
{
color = CameraSelectedColor;
texture = _selectedTexture;
}
else if (marker.Active)
{
color = CameraActiveColor;
texture = _activeTexture;
}
else
{
color = CameraInactiveColor;
texture = _inactiveTexture;
}
TrackedEntities[netEntity] = new NavMapBlip(
coords,
texture,
color,
false,
EnableCameraSelection
);
}
}
}

View File

@@ -10,7 +10,7 @@ public static class BoundKeyHelper
public static string ShortKeyName(BoundKeyFunction keyFunction)
{
// need to use shortened key names so they fit in the buttons.
return TryGetShortKeyName(keyFunction, out var name) ? Loc.GetString(name) : " ";
return TryGetShortKeyName(keyFunction, out var name) ? name : " ";
}
public static bool IsBound(BoundKeyFunction keyFunction)

View File

@@ -28,7 +28,16 @@ public class ListContainer : Control
/// Called when creating a button on the UI.
/// The provided <see cref="ListContainerButton"/> is the generated button that Controls should be parented to.
/// </summary>
public Action<ListData, ListContainerButton>? GenerateItem;
public Action<ListData, ListContainerButton>? GenerateItem
{
get => _generateItem;
set {
_generateItem = value;
// Invalidate _itemHeight so we recalculate the size of children the next
// time PopulateList() is called
_itemHeight = 0;
}
}
/// <inheritdoc cref="BaseButton.OnPressed"/>
public Action<BaseButton.ButtonEventArgs, ListData>? ItemPressed;
@@ -59,6 +68,7 @@ public class ListContainer : Control
private bool _updateChildren = false;
private bool _suppressScrollValueChanged;
private ButtonGroup? _buttonGroup;
public Action<ListData, ListContainerButton>? _generateItem;
public int ScrollSpeedY { get; set; } = 50;

View File

@@ -198,8 +198,8 @@ public sealed class ActionButton : Control, IEntityControl
if (!_entities.TryGetComponent(Action, out MetaDataComponent? metadata))
return null;
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
var desc = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
var name = FormattedMessage.FromMarkupPermissive(metadata.EntityName);
var desc = FormattedMessage.FromMarkupPermissive(metadata.EntityDescription);
if (_player.LocalEntity is null)
return null;

View File

@@ -117,7 +117,7 @@ public partial class ChatBox : UIWidget
formatted.PushColor(color);
formatted.AddMarkupOrThrow(message);
formatted.Pop();
Contents.AddMessage(formatted);
Contents.AddMessage(formatted, tagsAllowed: null);
}
public void Focus(ChatSelectChannel? channel = null)

View File

@@ -26,6 +26,8 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
_window.OnNameChange += OnNameSelected;
_window.OnVerbChange += verb => SendMessage(new VoiceMaskChangeVerbMessage(verb));
_window.OnToggle += OnToggle;
_window.OnAccentToggle += OnAccentToggle;
_window.OnVoiceChange += voice => SendMessage(new VoiceMaskChangeVoiceMessage(voice)); // Corvax-TTS
}
@@ -34,6 +36,16 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
SendMessage(new VoiceMaskChangeNameMessage(name));
}
private void OnToggle()
{
SendMessage(new VoiceMaskToggleMessage());
}
private void OnAccentToggle()
{
SendMessage(new VoiceMaskAccentToggleMessage());
}
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not VoiceMaskBuiState cast || _window == null)
@@ -41,7 +53,7 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
return;
}
_window.UpdateState(cast.Name, cast.Voice, cast.Verb); // Corvax-TTS
_window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide, cast.Voice);//cast.Voice Corvax-TTS
}
protected override void Dispose(bool disposing)

View File

@@ -12,6 +12,8 @@
<Label Text="{Loc 'voice-mask-name-change-speech-style'}" />
<OptionButton Name="SpeechVerbSelector" /> <!-- Populated in LoadVerbs -->
</BoxContainer>
<Button Name="ToggleAccentButton" Text="{Loc 'voice-mask-name-change-accent-toggle'}" HorizontalExpand="True" ToggleMode="True" Margin="5"/>
<Button Name="ToggleButton" Text="{Loc 'voice-mask-name-change-toggle'}" HorizontalExpand="True" ToggleMode="True" Margin="5"/>
<!-- Corvax-TTS-Start -->
<BoxContainer Orientation="Horizontal" Margin="5" Visible="False" Name="TTSContainer">
<Label Text="{Loc 'voice-mask-voice-change-info'}" />

View File

@@ -15,6 +15,8 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
{
public Action<string>? OnNameChange;
public Action<string?>? OnVerbChange;
public Action? OnToggle;
public Action? OnAccentToggle;
public Action<string>? OnVoiceChange; // Corvax-TTS
private List<(string, string)> _verbs = new();
@@ -37,6 +39,9 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
SpeechVerbSelector.SelectId(args.Id);
};
ToggleButton.OnPressed += args => OnToggle?.Invoke();
ToggleAccentButton.OnPressed += args => OnAccentToggle?.Invoke();
// Corvax-TTS-Start
if (IoCManager.Resolve<IConfigurationManager>().GetCVar(CCCVars.TTSEnabled))
{
@@ -100,10 +105,12 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
}
// Corvax-TTS-End
public void UpdateState(string name, string voice, string? verb) // Corvax-TTS
public void UpdateState(string name, string? verb, bool active, bool accentHide, string voice) // Corvax-TTS
{
NameSelector.Text = name;
_verb = verb;
ToggleButton.Pressed = active;
ToggleAccentButton.Pressed = accentHide;
for (int id = 0; id < SpeechVerbSelector.ItemCount; id++)
{

View File

@@ -43,6 +43,9 @@ public sealed partial class MeleeWeaponSystem
return;
}
var length = 1f;
var offset = 1f;
var spriteRotation = Angle.Zero;
if (arcComponent.Animation != WeaponArcAnimation.None
&& TryComp(weapon, out MeleeWeaponComponent? meleeWeaponComponent))
@@ -55,9 +58,11 @@ public sealed partial class MeleeWeaponSystem
if (meleeWeaponComponent.SwingLeft)
angle *= -1;
length = (1 / meleeWeaponComponent.AttackRate) * 0.6f;
offset = meleeWeaponComponent.AnimationOffset;
}
_sprite.SetRotation((animationUid, sprite), localPos.ToWorldAngle());
var distance = Math.Clamp(localPos.Length() / 2f, 0.2f, 1f);
var xform = _xformQuery.GetComponent(animationUid);
TrackUserComponent track;
@@ -67,16 +72,16 @@ public sealed partial class MeleeWeaponSystem
case WeaponArcAnimation.Slash:
track = EnsureComp<TrackUserComponent>(animationUid);
track.User = user;
_animation.Play(animationUid, GetSlashAnimation(sprite, angle, spriteRotation), SlashAnimationKey);
_animation.Play(animationUid, GetSlashAnimation((animationUid, sprite), angle, spriteRotation, length, offset), SlashAnimationKey);
if (arcComponent.Fadeout)
_animation.Play(animationUid, GetFadeAnimation(sprite, 0.065f, 0.065f + 0.05f), FadeAnimationKey);
_animation.Play(animationUid, GetFadeAnimation(sprite, length * 0.5f, length + 0.15f), FadeAnimationKey);
break;
case WeaponArcAnimation.Thrust:
track = EnsureComp<TrackUserComponent>(animationUid);
track.User = user;
_animation.Play(animationUid, GetThrustAnimation((animationUid, sprite), distance, spriteRotation), ThrustAnimationKey);
_animation.Play(animationUid, GetThrustAnimation((animationUid, sprite), offset, spriteRotation, length), ThrustAnimationKey);
if (arcComponent.Fadeout)
_animation.Play(animationUid, GetFadeAnimation(sprite, 0.05f, 0.15f), FadeAnimationKey);
_animation.Play(animationUid, GetFadeAnimation(sprite, length * 0.5f, length + 0.15f), FadeAnimationKey);
break;
case WeaponArcAnimation.None:
var (mapPos, mapRot) = TransformSystem.GetWorldPositionRotation(userXform);
@@ -89,21 +94,22 @@ public sealed partial class MeleeWeaponSystem
}
}
private Animation GetSlashAnimation(SpriteComponent sprite, Angle arc, Angle spriteRotation)
private Animation GetSlashAnimation(Entity<SpriteComponent> sprite, Angle arc, Angle spriteRotation, float length, float offset)
{
const float slashStart = 0.03f;
const float slashEnd = 0.065f;
const float length = slashEnd + 0.05f;
var startRotation = sprite.Rotation + arc / 2;
var endRotation = sprite.Rotation - arc / 2;
var startRotationOffset = startRotation.RotateVec(new Vector2(0f, -1f));
var endRotationOffset = endRotation.RotateVec(new Vector2(0f, -1f));
var startRotation = sprite.Comp.Rotation + (arc * 0.5f);
var endRotation = sprite.Comp.Rotation - (arc * 0.5f);
var startRotationOffset = startRotation.RotateVec(new Vector2(0f, -offset * 0.9f));
var minRotationOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -offset * 1.1f));
var endRotationOffset = endRotation.RotateVec(new Vector2(0f, -offset * 0.9f));
startRotation += spriteRotation;
endRotation += spriteRotation;
sprite.Comp.NoRotation = true;
return new Animation()
{
Length = TimeSpan.FromSeconds(length),
Length = TimeSpan.FromSeconds(length + 0.05f),
AnimationTracks =
{
new AnimationTrackComponentProperty()
@@ -112,10 +118,12 @@ public sealed partial class MeleeWeaponSystem
Property = nameof(SpriteComponent.Rotation),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startRotation, 0f),
new AnimationTrackProperty.KeyFrame(startRotation, slashStart),
new AnimationTrackProperty.KeyFrame(endRotation, slashEnd)
}
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.0f), length * 0.0f),
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.5f), length * 0.10f),
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,1.0f), length * 0.15f),
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.9f), length * 0.20f),
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.80f), length * 0.6f, Easings.OutQuart)
},
},
new AnimationTrackComponentProperty()
{
@@ -123,21 +131,21 @@ public sealed partial class MeleeWeaponSystem
Property = nameof(SpriteComponent.Offset),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startRotationOffset, 0f),
new AnimationTrackProperty.KeyFrame(startRotationOffset, slashStart),
new AnimationTrackProperty.KeyFrame(endRotationOffset, slashEnd)
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,0.0f), length * 0.0f),
new AnimationTrackProperty.KeyFrame(minRotationOffset, length * 0.10f),
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,1.0f), length * 0.15f),
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,0.80f), length * 0.6f, Easings.OutQuart)
}
},
}
};
}
private Animation GetThrustAnimation(Entity<SpriteComponent> sprite, float distance, Angle spriteRotation)
private Animation GetThrustAnimation(Entity<SpriteComponent> sprite, float offset, Angle spriteRotation, float length)
{
const float thrustEnd = 0.05f;
const float length = 0.15f;
var startOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -distance / 5f));
var endOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -distance));
var startOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, 0f));
var endOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -offset * 1.2f));
_sprite.SetRotation(sprite.AsNullable(), sprite.Comp.Rotation + spriteRotation);
return new Animation()
@@ -151,9 +159,11 @@ public sealed partial class MeleeWeaponSystem
Property = nameof(SpriteComponent.Offset),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startOffset, 0f),
new AnimationTrackProperty.KeyFrame(endOffset, thrustEnd),
new AnimationTrackProperty.KeyFrame(endOffset, length),
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0f), length * 0f),
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.65f), length * 0.10f),
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 1f), length * 0.20f),
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.9f), length * 0.30f),
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.7f), length * 0.60f, Easings.OutQuart)
}
},
}
@@ -200,11 +210,12 @@ public sealed partial class MeleeWeaponSystem
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(direction.Normalized() * 0.15f, 0f),
new AnimationTrackProperty.KeyFrame(Vector2.Zero, length)
}
}
}
new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0f),
new AnimationTrackProperty.KeyFrame(direction.Normalized() * 0.15f, length*0.4f),
new AnimationTrackProperty.KeyFrame(Vector2.Zero, length*0.6f),
},
},
},
};
}

View File

@@ -11,17 +11,20 @@ public sealed partial class MagazineVisualsComponent : Component
/// <summary>
/// What RsiState we use.
/// </summary>
[DataField("magState")] public string? MagState;
[DataField]
public string? MagState;
/// <summary>
/// How many steps there are
/// </summary>
[DataField("steps")] public int MagSteps;
[DataField("steps")]
public int MagSteps;
/// <summary>
/// Should we hide when the count is 0
/// </summary>
[DataField("zeroVisible")] public bool ZeroVisible;
[DataField]
public bool ZeroVisible;
}
public enum GunVisualLayers : byte

View File

@@ -8,9 +8,10 @@ public sealed partial class SpentAmmoVisualsComponent : Component
/// <summary>
/// Should we do "{_state}-spent" or just "spent"
/// </summary>
[DataField("suffix")] public bool Suffix = true;
[DataField]
public bool Suffix = true;
[DataField("state")]
[DataField]
public string State = "base";
}

View File

@@ -48,7 +48,7 @@ public sealed class GunSpreadOverlay : Overlay
if (mapPos.MapId == MapId.Nullspace)
return;
if (!_guns.TryGetGun(player.Value, out var gunUid, out var gun))
if (!_guns.TryGetGun(player.Value, out var gun))
return;
var mouseScreenPos = _input.MouseScreenPosition;
@@ -58,12 +58,12 @@ public sealed class GunSpreadOverlay : Overlay
return;
// (☞゚ヮ゚)☞
var maxSpread = gun.MaxAngleModified;
var minSpread = gun.MinAngleModified;
var timeSinceLastFire = (_timing.CurTime - gun.NextFire).TotalSeconds;
var currentAngle = new Angle(MathHelper.Clamp(gun.CurrentAngle.Theta - gun.AngleDecayModified.Theta * timeSinceLastFire,
gun.MinAngleModified.Theta, gun.MaxAngleModified.Theta));
var direction = (mousePos.Position - mapPos.Position);
var maxSpread = gun.Comp.MaxAngleModified;
var minSpread = gun.Comp.MinAngleModified;
var timeSinceLastFire = (_timing.CurTime - gun.Comp.NextFire).TotalSeconds;
var currentAngle = new Angle(MathHelper.Clamp(gun.Comp.CurrentAngle.Theta - gun.Comp.AngleDecayModified.Theta * timeSinceLastFire,
gun.Comp.MinAngleModified.Theta, gun.Comp.MaxAngleModified.Theta));
var direction = mousePos.Position - mapPos.Position;
worldHandle.DrawLine(mapPos.Position, mousePos.Position + direction, Color.Orange);

View File

@@ -41,7 +41,7 @@ public abstract class BaseBulletRenderer : Control
{
var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X));
var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows);
var rows = Math.Min((int)MathF.Ceiling(Capacity / (float)countPerRow), Rows);
var height = _params.ItemHeight * rows + (_params.VerticalSeparation * rows - 1);
var width = RowWidth(countPerRow);
@@ -110,7 +110,7 @@ public abstract class BaseBulletRenderer : Control
private int CountPerRow(float width)
{
return (int) ((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
return (int)((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
}
private int RowWidth(int count)

View File

@@ -2,10 +2,8 @@ using Content.Shared.Projectiles;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Client.Weapons.Ranged.Systems;
@@ -22,26 +20,26 @@ public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
SubscribeLocalEvent<FlyBySoundComponent, StartCollideEvent>(OnCollide);
}
private void OnCollide(EntityUid uid, FlyBySoundComponent component, ref StartCollideEvent args)
private void OnCollide(Entity<FlyBySoundComponent> ent, ref StartCollideEvent args)
{
var attachedEnt = _player.LocalEntity;
// If it's not our ent or we shot it.
if (attachedEnt == null ||
args.OtherEntity != attachedEnt ||
TryComp<ProjectileComponent>(uid, out var projectile) &&
TryComp<ProjectileComponent>(ent, out var projectile) &&
projectile.Shooter == attachedEnt)
{
return;
}
if (args.OurFixtureId != FlyByFixture ||
!_random.Prob(component.Prob))
!_random.Prob(ent.Comp.Prob))
{
return;
}
// Play attached to our entity because the projectile may immediately delete or the likes.
_audio.PlayPredicted(component.Sound, attachedEnt.Value, attachedEnt.Value);
_audio.PlayPredicted(ent.Comp.Sound, attachedEnt.Value, attachedEnt.Value);
}
}

View File

@@ -14,12 +14,12 @@ namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
private void OnAmmoCounterCollect(EntityUid uid, AmmoCounterComponent component, ItemStatusCollectMessage args)
private void OnAmmoCounterCollect(Entity<AmmoCounterComponent> ent, ref ItemStatusCollectMessage args)
{
RefreshControl(uid, component);
RefreshControl(ent);
if (component.Control != null)
args.Controls.Add(component.Control);
if (ent.Comp.Control != null)
args.Controls.Add(ent.Comp.Control);
}
/// <summary>
@@ -27,35 +27,32 @@ public sealed partial class GunSystem
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
private void RefreshControl(EntityUid uid, AmmoCounterComponent? component = null)
private void RefreshControl(Entity<AmmoCounterComponent> ent)
{
if (!Resolve(uid, ref component, false))
return;
component.Control?.Dispose();
component.Control = null;
ent.Comp.Control?.Dispose();
ent.Comp.Control = null;
var ev = new AmmoCounterControlEvent();
RaiseLocalEvent(uid, ev, false);
RaiseLocalEvent(ent, ev, false);
// Fallback to default if none specified
ev.Control ??= new DefaultStatusControl();
component.Control = ev.Control;
UpdateAmmoCount(uid, component);
ent.Comp.Control = ev.Control;
UpdateAmmoCount(ent);
}
private void UpdateAmmoCount(EntityUid uid, AmmoCounterComponent component)
private void UpdateAmmoCount(Entity<AmmoCounterComponent> ent)
{
if (component.Control == null)
if (ent.Comp.Control == null)
return;
var ev = new UpdateAmmoCounterEvent()
{
Control = component.Control
Control = ent.Comp.Control
};
RaiseLocalEvent(uid, ev, false);
RaiseLocalEvent(ent, ev, false);
}
protected override void UpdateAmmoCount(EntityUid uid, bool prediction = true)
@@ -68,7 +65,7 @@ public sealed partial class GunSystem
return;
}
UpdateAmmoCount(uid, clientComp);
UpdateAmmoCount((uid, clientComp));
}
/// <summary>

View File

@@ -12,41 +12,41 @@ public sealed partial class GunSystem
SubscribeLocalEvent<BallisticAmmoProviderComponent, UpdateAmmoCounterEvent>(OnBallisticAmmoCount);
}
private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, UpdateAmmoCounterEvent args)
private void OnBallisticAmmoCount(Entity<BallisticAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is DefaultStatusControl control)
{
control.Update(GetBallisticShots(component), component.Capacity);
control.Update(GetBallisticShots(ent.Comp), ent.Comp.Capacity);
}
}
protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates)
protected override void Cycle(Entity<BallisticAmmoProviderComponent> ent, MapCoordinates coordinates)
{
if (!Timing.IsFirstTimePredicted)
return;
EntityUid? ent = null;
EntityUid? ammoEnt = null;
// TODO: Combine with TakeAmmo
if (component.Entities.Count > 0)
if (ent.Comp.Entities.Count > 0)
{
var existing = component.Entities[^1];
component.Entities.RemoveAt(component.Entities.Count - 1);
var existing = ent.Comp.Entities[^1];
ent.Comp.Entities.RemoveAt(ent.Comp.Entities.Count - 1);
Containers.Remove(existing, component.Container);
Containers.Remove(existing, ent.Comp.Container);
EnsureShootable(existing);
}
else if (component.UnspawnedCount > 0)
else if (ent.Comp.UnspawnedCount > 0)
{
component.UnspawnedCount--;
ent = Spawn(component.Proto, coordinates);
EnsureShootable(ent.Value);
ent.Comp.UnspawnedCount--;
ammoEnt = Spawn(ent.Comp.Proto, coordinates);
EnsureShootable(ammoEnt.Value);
}
if (ent != null && IsClientSide(ent.Value))
Del(ent.Value);
if (ammoEnt != null && IsClientSide(ammoEnt.Value))
Del(ammoEnt.Value);
var cycledEvent = new GunCycledEvent();
RaiseLocalEvent(uid, ref cycledEvent);
RaiseLocalEvent(ent, ref cycledEvent);
}
}

View File

@@ -10,11 +10,11 @@ public partial class GunSystem
SubscribeLocalEvent<BasicEntityAmmoProviderComponent, UpdateAmmoCounterEvent>(OnBasicEntityAmmoCount);
}
private void OnBasicEntityAmmoCount(EntityUid uid, BasicEntityAmmoProviderComponent component, UpdateAmmoCounterEvent args)
private void OnBasicEntityAmmoCount(Entity<BasicEntityAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is DefaultStatusControl control && component.Count != null && component.Capacity != null)
if (args.Control is DefaultStatusControl control && ent.Comp.Count != null && ent.Comp.Capacity != null)
{
control.Update(component.Count.Value, component.Capacity.Value);
control.Update(ent.Comp.Count.Value, ent.Comp.Capacity.Value);
}
}
}

View File

@@ -18,11 +18,11 @@ public sealed partial class GunSystem
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, AppearanceChangeEvent>(OnChamberMagazineAppearance);
}
private void OnChamberMagazineAppearance(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ref AppearanceChangeEvent args)
private void OnChamberMagazineAppearance(Entity<ChamberMagazineAmmoProviderComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null ||
!_sprite.LayerMapTryGet((uid, args.Sprite), GunVisualLayers.Base, out var boltLayer, false) ||
!Appearance.TryGetData(uid, AmmoVisuals.BoltClosed, out bool boltClosed))
!_sprite.LayerMapTryGet((ent, args.Sprite), GunVisualLayers.Base, out var boltLayer, false) ||
!Appearance.TryGetData(ent, AmmoVisuals.BoltClosed, out bool boltClosed))
{
return;
}
@@ -30,11 +30,11 @@ public sealed partial class GunSystem
// Maybe re-using base layer for this will bite me someday but screw you future sloth.
if (boltClosed)
{
_sprite.LayerSetRsiState((uid, args.Sprite), boltLayer, "base");
_sprite.LayerSetRsiState((ent, args.Sprite), boltLayer, "base");
}
else
{
_sprite.LayerSetRsiState((uid, args.Sprite), boltLayer, "bolt-open");
_sprite.LayerSetRsiState((ent, args.Sprite), boltLayer, "bolt-open");
}
}
@@ -55,17 +55,17 @@ public sealed partial class GunSystem
// to avoid 6-7 additional entity spawns.
}
private void OnChamberMagazineCounter(EntityUid uid, ChamberMagazineAmmoProviderComponent component, AmmoCounterControlEvent args)
private void OnChamberMagazineCounter(Entity<ChamberMagazineAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
{
args.Control = new ChamberMagazineStatusControl();
}
private void OnChamberMagazineAmmoUpdate(EntityUid uid, ChamberMagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
private void OnChamberMagazineAmmoUpdate(Entity<ChamberMagazineAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is not ChamberMagazineStatusControl control) return;
var chambered = GetChamberEntity(uid);
var magEntity = GetMagazineEntity(uid);
var chambered = GetChamberEntity(ent);
var magEntity = GetMagazineEntity(ent);
var ammoCountEv = new GetAmmoCountEvent();
if (magEntity != null)

View File

@@ -11,11 +11,11 @@ public sealed partial class GunSystem
SubscribeLocalEvent<MagazineAmmoProviderComponent, AmmoCounterControlEvent>(OnMagazineControl);
}
private void OnMagazineAmmoUpdate(EntityUid uid, MagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
private void OnMagazineAmmoUpdate(Entity<MagazineAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
var ent = GetMagazineEntity(uid);
var magEnt = GetMagazineEntity(ent);
if (ent == null)
if (magEnt == null)
{
if (args.Control is DefaultStatusControl control)
{
@@ -25,14 +25,14 @@ public sealed partial class GunSystem
return;
}
RaiseLocalEvent(ent.Value, args, false);
RaiseLocalEvent(magEnt.Value, args, false);
}
private void OnMagazineControl(EntityUid uid, MagazineAmmoProviderComponent component, AmmoCounterControlEvent args)
private void OnMagazineControl(Entity<MagazineAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
{
var ent = GetMagazineEntity(uid);
if (ent == null)
var magEnt = GetMagazineEntity(ent);
if (magEnt == null)
return;
RaiseLocalEvent(ent.Value, args, false);
RaiseLocalEvent(magEnt.Value, args, false);
}
}

View File

@@ -14,24 +14,24 @@ public sealed partial class GunSystem
SubscribeLocalEvent<MagazineVisualsComponent, AppearanceChangeEvent>(OnMagazineVisualsChange);
}
private void OnMagazineVisualsInit(EntityUid uid, MagazineVisualsComponent component, ComponentInit args)
private void OnMagazineVisualsInit(Entity<MagazineVisualsComponent> ent, ref ComponentInit args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite)) return;
if (!TryComp<SpriteComponent>(ent, out var sprite)) return;
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
{
_sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.Mag, $"{component.MagState}-{component.MagSteps - 1}");
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
_sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.Mag, $"{ent.Comp.MagState}-{ent.Comp.MagSteps - 1}");
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
}
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
{
_sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.MagUnshaded, $"{component.MagState}-unshaded-{component.MagSteps - 1}");
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
_sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.MagUnshaded, $"{ent.Comp.MagState}-unshaded-{ent.Comp.MagSteps - 1}");
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
}
}
private void OnMagazineVisualsChange(EntityUid uid, MagazineVisualsComponent component, ref AppearanceChangeEvent args)
private void OnMagazineVisualsChange(Entity<MagazineVisualsComponent> ent, ref AppearanceChangeEvent args)
{
// tl;dr
// 1.If no mag then hide it OR
@@ -41,70 +41,58 @@ public sealed partial class GunSystem
if (sprite == null) return;
// Corvax-Wega-MagVisuals-start
string magState = component.MagState ?? string.Empty;
if (args.AppearanceData.TryGetValue(BatteryWeaponFireModeVisuals.MagState, out var customMagStateObj)
&& customMagStateObj is string customMagState && !string.IsNullOrEmpty(customMagState))
{
magState = $"mag-{customMagState}";
component.MagState = magState;
}
else if (!string.IsNullOrEmpty(component.MagState))
magState = component.MagState;
// Corvax-Wega-MagVisuals-end
if (!args.AppearanceData.TryGetValue(AmmoVisuals.MagLoaded, out var magloaded) ||
magloaded is true)
{
if (!args.AppearanceData.TryGetValue(AmmoVisuals.AmmoMax, out var capacity))
{
capacity = component.MagSteps;
capacity = ent.Comp.MagSteps;
}
if (!args.AppearanceData.TryGetValue(AmmoVisuals.AmmoCount, out var current))
{
current = component.MagSteps;
current = ent.Comp.MagSteps;
}
var step = ContentHelpers.RoundToLevels((int)current, (int)capacity, component.MagSteps);
var step = ContentHelpers.RoundToLevels((int)current, (int)capacity, ent.Comp.MagSteps);
if (step == 0 && !component.ZeroVisible)
if (step == 0 && !ent.Comp.ZeroVisible)
{
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
{
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
}
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
{
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
}
return;
}
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
{
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, true);
_sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.Mag, $"{magState}-{step}"); // Corvax-Wega-MagVisuals-Edit
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, true);
_sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.Mag, $"{ent.Comp.MagState}-{step}");
}
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
{
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, true);
_sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.MagUnshaded, $"{magState}-unshaded-{step}"); // Corvax-Wega-MagVisuals-Edit
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, true);
_sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.MagUnshaded, $"{ent.Comp.MagState}-unshaded-{step}");
}
}
else
{
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
{
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
}
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
{
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
}
}
}

View File

@@ -14,25 +14,25 @@ public sealed partial class GunSystem
SubscribeLocalEvent<RevolverAmmoProviderComponent, EntRemovedFromContainerMessage>(OnRevolverEntRemove);
}
private void OnRevolverEntRemove(EntityUid uid, RevolverAmmoProviderComponent component, EntRemovedFromContainerMessage args)
private void OnRevolverEntRemove(Entity<RevolverAmmoProviderComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (args.Container.ID != RevolverContainer)
return;
// See ChamberMagazineAmmoProvider
// <See ChamberMagazineAmmoProvider>
if (!IsClientSide(args.Entity))
return;
QueueDel(args.Entity);
}
private void OnRevolverAmmoUpdate(EntityUid uid, RevolverAmmoProviderComponent component, UpdateAmmoCounterEvent args)
private void OnRevolverAmmoUpdate(Entity<RevolverAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is not RevolverStatusControl control) return;
control.Update(component.CurrentIndex, component.Chambers);
control.Update(ent.Comp.CurrentIndex, ent.Comp.Chambers);
}
private void OnRevolverCounter(EntityUid uid, RevolverAmmoProviderComponent component, AmmoCounterControlEvent args)
private void OnRevolverCounter(Entity<RevolverAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
{
args.Control = new RevolverStatusControl();
}

View File

@@ -11,7 +11,7 @@ public sealed partial class GunSystem
SubscribeLocalEvent<SpentAmmoVisualsComponent, AppearanceChangeEvent>(OnSpentAmmoAppearance);
}
private void OnSpentAmmoAppearance(EntityUid uid, SpentAmmoVisualsComponent component, ref AppearanceChangeEvent args)
private void OnSpentAmmoAppearance(Entity<SpentAmmoVisualsComponent> ent, ref AppearanceChangeEvent args)
{
var sprite = args.Sprite;
if (sprite == null) return;
@@ -21,15 +21,15 @@ public sealed partial class GunSystem
return;
}
var spent = (bool) varSpent;
var spent = (bool)varSpent;
string state;
if (spent)
state = component.Suffix ? $"{component.State}-spent" : "spent";
state = ent.Comp.Suffix ? $"{ent.Comp.State}-spent" : "spent";
else
state = component.State;
state = ent.Comp.State;
_sprite.LayerSetRsiState((uid, sprite), AmmoVisualLayers.Base, state);
_sprite.RemoveLayer((uid, sprite), AmmoVisualLayers.Tip, false);
_sprite.LayerSetRsiState((ent, sprite), AmmoVisualLayers.Base, state);
_sprite.RemoveLayer((ent, sprite), AmmoVisualLayers.Tip, false);
}
}

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