Merge remote-tracking branch 'upstream/master' into up/drinks-yml

# Conflicts:
#	Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml
This commit is contained in:
iaada
2025-09-05 17:07:09 -05:00
1874 changed files with 122270 additions and 59095 deletions

7
.github/CODEOWNERS vendored
View File

@@ -24,6 +24,11 @@
/Content.*/Forensics/ @ficcialfaint
/Content.*/Trigger/ @slarticodefast
/Content.*/Stunnable/ @Princess-Cheeseballs
/Content.*/Nutrition/ @Princess-Cheeseballs
# SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @crazybrain23
@@ -37,3 +42,5 @@
/Content.*/NPC @metalgearsloth
/Content.*/Shuttles @metalgearsloth
/Content.*/Weapons @metalgearsloth
/Content.Server/Discord/ @Simyon264

View File

@@ -34,7 +34,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release

View File

@@ -41,7 +41,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release

View File

@@ -60,7 +60,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release

View File

@@ -1,17 +1,19 @@
#!/usr/bin/env python3
# Installs git hooks, updates them, updates submodules, that kind of thing.
"""
Installs git hooks, updates them, updates submodules, that kind of thing.
"""
import subprocess
import sys
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "2"
CURRENT_HOOKS_VERSION = "3"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
@@ -25,12 +27,10 @@ def run_command(command: List[str], capture: bool = False) -> subprocess.Complet
sys.stdout.flush()
completed = None
if capture:
completed = subprocess.run(command, cwd="..", stdout=subprocess.PIPE)
completed = subprocess.run(command, stdout=subprocess.PIPE, text=True)
else:
completed = subprocess.run(command, cwd="..")
completed = subprocess.run(command)
if completed.returncode != 0:
print("Error: command exited with code {}!".format(completed.returncode))
@@ -43,7 +43,7 @@ def update_submodules():
Updates all submodules.
"""
if ('GITHUB_ACTIONS' in os.environ):
if 'GITHUB_ACTIONS' in os.environ:
return
if os.path.isfile("DISABLE_SUBMODULE_AUTOUPDATE"):
@@ -76,22 +76,21 @@ def install_hooks():
print("No hooks change detected.")
return
with open("INSTALLED_HOOKS_VERSION", "w") as f:
f.write(CURRENT_HOOKS_VERSION)
print("Hooks need updating.")
hooks_target_dir = Path("..")/".git"/"hooks"
hooks_target_dir = Path(run_command(["git", "rev-parse", "--git-path", "hooks"], True).stdout.strip())
hooks_source_dir = Path("hooks")
# Clear entire tree since we need to kill deleted files too.
for filename in os.listdir(str(hooks_target_dir)):
os.remove(str(hooks_target_dir/filename))
for filename in os.listdir(hooks_target_dir):
os.remove(hooks_target_dir / filename)
for filename in os.listdir(str(hooks_source_dir)):
for filename in os.listdir(hooks_source_dir):
print("Copying hook {}".format(filename))
shutil.copy2(str(hooks_source_dir/filename),
str(hooks_target_dir/filename))
shutil.copy2(hooks_source_dir / filename, hooks_target_dir / filename)
with open("INSTALLED_HOOKS_VERSION", "w") as f:
f.write(CURRENT_HOOKS_VERSION)
def reset_solution():
@@ -107,8 +106,7 @@ def reset_solution():
def check_for_zip_download():
# Check if .git exists,
cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if not os.path.isdir(os.path.join(cur_dir, ".git")):
if run_command(["git", "rev-parse"]).returncode != 0:
print("It appears that you downloaded this repository directly from GitHub. (Using the .zip download option) \n"
"When downloading straight from GitHub, it leaves out important information that git needs to function. "
"Such as information to download the engine or even the ability to even be able to create contributions. \n"

View File

@@ -1,10 +1,10 @@
#!/bin/bash
gitroot=`git rev-parse --show-toplevel`
gitroot=$(git rev-parse --show-toplevel)
cd "$gitroot/BuildChecker"
cd "$gitroot/BuildChecker" || exit
if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then
if [[ $(uname) == MINGW* || $(uname) == CYGWIN* ]]; then
# Windows
py -3 git_helper.py --quiet
else

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# Just call post-checkout since it does the same thing.
gitroot=`git rev-parse --show-toplevel`
bash "$gitroot/.git/hooks/post-checkout"
gitroot=$(git rev-parse --git-path hooks)
bash "$gitroot/post-checkout"

View File

@@ -0,0 +1,174 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.CCVar;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Benchmarks;
/// <summary>
/// Spawns N number of entities with a <see cref="DeltaPressureComponent"/> and
/// simulates them for a number of ticks M.
/// </summary>
[Virtual]
[GcServer(true)]
//[MemoryDiagnoser]
//[ThreadingDiagnoser]
public class DeltaPressureBenchmark
{
/// <summary>
/// Number of entities (windows, really) to spawn with a <see cref="DeltaPressureComponent"/>.
/// </summary>
[Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
public int EntityCount;
/// <summary>
/// Number of entities that each parallel processing job will handle.
/// </summary>
// [Params(1, 10, 100, 1000, 5000, 10000)] For testing how multithreading parameters affect performance (THESE TESTS TAKE 16+ HOURS TO RUN)
[Params(10)]
public int BatchSize;
/// <summary>
/// Number of entities to process per iteration in the DeltaPressure
/// processing loop.
/// </summary>
// [Params(100, 1000, 5000, 10000, 50000)]
[Params(1000)]
public int EntitiesPerIteration;
private readonly EntProtoId _windowProtoId = "Window";
private readonly EntProtoId _wallProtoId = "WallPlastitaniumIndestructible";
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private SharedMapSystem _map = default!;
private IRobustRandom _random = default!;
private IConfigurationManager _cvar = default!;
private ITileDefinitionManager _tileDefMan = default!;
private AtmosphereSystem _atmospereSystem = default!;
private Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>
_testEnt;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();
_entMan = server.ResolveDependency<IEntityManager>();
_map = _entMan.System<SharedMapSystem>();
_random = server.ResolveDependency<IRobustRandom>();
_cvar = server.ResolveDependency<IConfigurationManager>();
_tileDefMan = server.ResolveDependency<ITileDefinitionManager>();
_atmospereSystem = _entMan.System<AtmosphereSystem>();
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
_cvar.SetCVar(CCVars.DeltaPressureParallelToProcessPerIteration, EntitiesPerIteration);
_cvar.SetCVar(CCVars.DeltaPressureParallelBatchSize, BatchSize);
var plating = _tileDefMan["Plating"].TileId;
/*
Basically, we want to have a 5-wide grid of tiles.
Edges are walled, and the length of the grid is determined by N + 2.
Windows should only touch the top and bottom walls, and each other.
*/
var length = EntityCount + 2; // ensures we can spawn exactly N windows between side walls
const int height = 5;
await server.WaitPost(() =>
{
// Fill required tiles (extend grid) with plating
for (var x = 0; x < length; x++)
{
for (var y = 0; y < height; y++)
{
_map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
}
}
// Spawn perimeter walls and windows row in the middle (y = 2)
const int midY = height / 2;
for (var x = 0; x < length; x++)
{
for (var y = 0; y < height; y++)
{
var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
var isPerimeter = x == 0 || x == length - 1 || y == 0 || y == height - 1;
if (isPerimeter)
{
_entMan.SpawnEntity(_wallProtoId, coords);
continue;
}
// Spawn windows only on the middle row, spanning interior (excluding side walls)
if (y == midY)
{
_entMan.SpawnEntity(_windowProtoId, coords);
}
}
}
});
// Next we run the fixgridatmos command to ensure that we have some air on our grid.
// Wait a little bit as well.
// TODO: Unhardcode command magic string when fixgridatmos is an actual command we can ref and not just
// a stamp-on in AtmosphereSystem.
await _pair.WaitCommand("fixgridatmos " + mapdata.Grid.Owner, 1);
var uid = mapdata.Grid.Owner;
_testEnt = new Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>(
uid,
_entMan.GetComponent<GridAtmosphereComponent>(uid),
_entMan.GetComponent<GasTileOverlayComponent>(uid),
_entMan.GetComponent<MapGridComponent>(uid),
_entMan.GetComponent<TransformComponent>(uid));
}
[Benchmark]
public async Task PerformFullProcess()
{
await _pair.Server.WaitPost(() =>
{
while (!_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure)) { }
});
}
[Benchmark]
public async Task PerformSingleRunProcess()
{
await _pair.Server.WaitPost(() =>
{
_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure);
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -47,7 +47,7 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
public static readonly string[] MapsSource = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
public static string[] MapsSource { get; } = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
[ParamsSource(nameof(MapsSource))]
public string Map;

View File

@@ -21,7 +21,7 @@ namespace Content.Client.Actions.UI
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null, FormattedMessage? charges = null)
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
@@ -52,17 +52,6 @@ namespace Content.Client.Actions.UI
vbox.AddChild(description);
}
if (charges != null && !string.IsNullOrWhiteSpace(charges.ToString()))
{
var chargesLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipActionCharges }
};
chargesLabel.SetMessage(charges);
vbox.AddChild(chargesLabel);
}
vbox.AddChild(_cooldownLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,

View File

@@ -15,6 +15,7 @@ namespace Content.Client.Administration.Managers
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IClientNetManager _netMgr = default!;
[Dependency] private readonly IClientConGroupController _conGroup = default!;
[Dependency] private readonly IClientConsoleHost _host = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
@@ -86,12 +87,12 @@ namespace Content.Client.Administration.Managers
private void UpdateMessageRx(MsgUpdateAdminStatus message)
{
_availableCommands.Clear();
var host = IoCManager.Resolve<IClientConsoleHost>();
// Anything marked as Any we'll just add even if the server doesn't know about it.
foreach (var (command, instance) in host.AvailableCommands)
foreach (var (command, instance) in _host.AvailableCommands)
{
if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) continue;
if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null)
continue;
_availableCommands.Add(command);
}

View File

@@ -1,7 +0,0 @@
using Content.Shared.Administration;
namespace Content.Client.Administration.Systems;
public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
{
}

View File

@@ -57,12 +57,43 @@ public sealed partial class ObjectsTab : Control
private void TeleportTo(NetEntity nent)
{
_console.ExecuteCommand($"tpto {nent}");
var selection = _selections[ObjectTypeOptions.SelectedId];
switch (selection)
{
case ObjectsTabSelection.Grids:
{
// directly teleport to the entity
_console.ExecuteCommand($"tpto {nent}");
}
break;
case ObjectsTabSelection.Maps:
{
// teleport to the map, not to the map entity (which is in nullspace)
if (!_entityManager.TryGetEntity(nent, out var map) || !_entityManager.TryGetComponent<MapComponent>(map, out var mapComp))
break;
_console.ExecuteCommand($"tp 0 0 {mapComp.MapId}");
break;
}
case ObjectsTabSelection.Stations:
{
// teleport to the station's largest grid, not to the station entity (which is in nullspace)
if (!_entityManager.TryGetEntity(nent, out var station))
break;
var largestGrid = _entityManager.EntitySysManager.GetEntitySystem<StationSystem>().GetLargestGrid(station.Value);
if (largestGrid == null)
break;
_console.ExecuteCommand($"tpto {largestGrid.Value}");
break;
}
default:
throw new NotImplementedException();
}
}
private void Delete(NetEntity nent)
{
_console.ExecuteCommand($"delete {nent}");
RefreshObjectList();
}
public void RefreshObjectList()
@@ -79,25 +110,21 @@ public sealed partial class ObjectsTab : Control
entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem<StationSystem>().GetStationNames());
break;
case ObjectsTabSelection.Grids:
{
var query = _entityManager.AllEntityQueryEnumerator<MapGridComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
{
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
}
var query = _entityManager.AllEntityQueryEnumerator<MapGridComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
break;
}
break;
}
case ObjectsTabSelection.Maps:
{
var query = _entityManager.AllEntityQueryEnumerator<MapComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
{
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
}
var query = _entityManager.AllEntityQueryEnumerator<MapComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
break;
}
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(selection), selection, null);
}

View File

@@ -1,5 +1,6 @@
<PanelContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Name="BackgroundColorPanel">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
@@ -20,7 +21,7 @@
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Button Name="DeleteButton"
<controls:ConfirmButton Name="DeleteButton"
Text="{Loc object-tab-entity-delete}"
SizeFlagsStretchRatio="3"
HorizontalExpand="True"

View File

@@ -0,0 +1,40 @@
using Robust.Client.Graphics;
using SixLabors.ImageSharp.PixelFormats;
namespace Content.Client.Anomaly;
/// <summary>
/// This component creates and handles the drawing of a ScreenTexture to be used on the Anomaly Scanner
/// for an indicator of Anomaly Severity.
/// </summary>
/// <remarks>
/// In the future I would like to make this a more generic "DynamicTextureComponent" that can contain a dictionary
/// of texture components like "Bar(offset, size, minimumValue, maximumValue, AppearanceKey, LayerMapKey)" that can
/// just draw a bar or other basic drawn element that will show up on a texture layer.
/// </remarks>
[RegisterComponent]
[Access(typeof(AnomalyScannerSystem))]
public sealed partial class AnomalyScannerScreenComponent : Component
{
/// <summary>
/// This is the texture drawn as a layer on the Anomaly Scanner device.
/// </summary>
public OwnedTexture? ScreenTexture;
/// <summary>
/// A small buffer that we can reuse to draw the severity bar.
/// </summary>
public Rgba32[]? BarBuf;
/// <summary>
/// The position of the top-left of the severity bar in pixels.
/// </summary>
[DataField(readOnly: true)]
public Vector2i Offset = new Vector2i(12, 17);
/// <summary>
/// The width and height of the severity bar in pixels.
/// </summary>
[DataField(readOnly: true)]
public Vector2i Size = new Vector2i(10, 3);
}

View File

@@ -0,0 +1,110 @@
using System.Numerics;
using Content.Shared.Anomaly;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
using SixLabors.ImageSharp.PixelFormats;
namespace Content.Client.Anomaly;
/// <inheritdoc cref="SharedAnomalyScannerSystem"/>
public sealed class AnomalyScannerSystem : SharedAnomalyScannerSystem
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private const float MaxHueDegrees = 360f;
private const float GreenHueDegrees = 110f;
private const float RedHueDegrees = 0f;
private const float GreenHue = GreenHueDegrees / MaxHueDegrees;
private const float RedHue = RedHueDegrees / MaxHueDegrees;
// Just an array to initialize the pixels of a new OwnedTexture
private static readonly Rgba32[] EmptyTexture = new Rgba32[32*32];
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnomalyScannerScreenComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<AnomalyScannerScreenComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<AnomalyScannerScreenComponent, AppearanceChangeEvent>(OnScannerAppearanceChanged);
}
private void OnComponentInit(Entity<AnomalyScannerScreenComponent> ent, ref ComponentInit args)
{
if(!_sprite.TryGetLayer(ent.Owner, AnomalyScannerVisualLayers.Base, out var layer, true))
return;
// Allocate the OwnedTexture
ent.Comp.ScreenTexture = _clyde.CreateBlankTexture<Rgba32>(layer.PixelSize);
if (layer.PixelSize.X < ent.Comp.Offset.X + ent.Comp.Size.X ||
layer.PixelSize.Y < ent.Comp.Offset.Y + ent.Comp.Size.Y)
{
// If the bar doesn't fit, just bail here, ScreenTexture and BarBuf will remain null, and appearance updates
// will do nothing.
DebugTools.Assert(false, "AnomalyScannerScreenComponent: Bar does not fit within sprite");
return;
}
// Initialize the texture
ent.Comp.ScreenTexture.SetSubImage((0, 0), layer.PixelSize, new ReadOnlySpan<Rgba32>(EmptyTexture));
// Initialize bar drawing buffer
ent.Comp.BarBuf = new Rgba32[ent.Comp.Size.X * ent.Comp.Size.Y];
}
private void OnComponentStartup(Entity<AnomalyScannerScreenComponent> ent, ref ComponentStartup args)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
_sprite.LayerSetTexture((ent, sprite), AnomalyScannerVisualLayers.Screen, ent.Comp.ScreenTexture);
}
private void OnScannerAppearanceChanged(Entity<AnomalyScannerScreenComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite is null || ent.Comp.ScreenTexture is null || ent.Comp.BarBuf is null)
return;
args.AppearanceData.TryGetValue(AnomalyScannerVisuals.AnomalySeverity, out var severityObj);
if (severityObj is not float severity)
severity = 0;
// Get the bar length
var barLength = (int)(severity * ent.Comp.Size.X);
// Calculate the bar color
// Hue "angle" of two colors to interpolate between depending on severity
// Just a lerp from Green hue at severity = 0.5 to Red hue at 1.0
var hue = Math.Clamp(2*GreenHue * (1 - severity), RedHue, GreenHue);
var color = new Rgba32(Color.FromHsv(new Vector4(hue, 1f, 1f, 1f)).RGBA);
var transparent = new Rgba32(0, 0, 0, 255);
for(var y = 0; y < ent.Comp.Size.Y; y++)
{
for (var x = 0; x < ent.Comp.Size.X; x++)
{
ent.Comp.BarBuf[y*ent.Comp.Size.X + x] = x < barLength ? color : transparent;
}
}
// Copy the buffer to the texture
try
{
ent.Comp.ScreenTexture.SetSubImage(
ent.Comp.Offset,
ent.Comp.Size,
new ReadOnlySpan<Rgba32>(ent.Comp.BarBuf)
);
}
catch (IndexOutOfRangeException)
{
Log.Warning($"Bar dimensions out of bounds with the texture on entity {ent.Owner}");
}
}
}

View File

@@ -7,7 +7,7 @@ using Robust.Shared.Timing;
namespace Content.Client.Anomaly;
public sealed class AnomalySystem : SharedAnomalySystem
public sealed partial class AnomalySystem : SharedAnomalySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly FloatingVisualizerSystem _floating = default!;
@@ -24,6 +24,7 @@ public sealed class AnomalySystem : SharedAnomalySystem
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentShutdown>(OnShutdown);
}
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
{
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);

View File

@@ -19,6 +19,7 @@ namespace Content.Client.Atmos.EntitySystems
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
private GasTileOverlay _overlay = default!;
private GasTileHeatOverlay _heatOverlay = default!;
public override void Initialize()
{
@@ -28,12 +29,16 @@ namespace Content.Client.Atmos.EntitySystems
_overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
_overlayMan.AddOverlay(_overlay);
_heatOverlay = new GasTileHeatOverlay();
_overlayMan.AddOverlay(_heatOverlay);
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay<GasTileOverlay>();
_overlayMan.RemoveOverlay<GasTileHeatOverlay>();
}
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)

View File

@@ -0,0 +1,210 @@
using System.Numerics;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Client.Atmos.EntitySystems;
using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Atmos.Overlays;
public sealed class GasTileHeatOverlay : Overlay
{
public override bool RequestScreenTexture { get; set; } = true;
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private static readonly ProtoId<ShaderPrototype> HeatOverlayShader = "Heat";
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
// We can't resolve this immediately, because it's an entitysystem, but we will attempt to resolve and cache this
// once we begin to draw.
private GasTileOverlaySystem? _gasTileOverlay;
private readonly SharedTransformSystem _xformSys;
private IRenderTexture? _heatTarget;
private IRenderTexture? _heatBlurTarget;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _shader;
public GasTileHeatOverlay()
{
IoCManager.InjectDependencies(this);
_xformSys = _entManager.System<SharedTransformSystem>();
_shader = _proto.Index(HeatOverlayShader).InstanceUnique();
_configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true);
}
private void SetReducedMotion(bool reducedMotion)
{
_shader.SetParameter("strength_scale", reducedMotion ? 0.5f : 1f);
_shader.SetParameter("speed_scale", reducedMotion ? 0.25f : 1f);
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return false;
// If we haven't resolved this yet, give it a try or bail
_gasTileOverlay ??= _entManager.System<GasTileOverlaySystem>();
if (_gasTileOverlay == null)
return false;
var target = args.Viewport.RenderTarget;
// Probably the resolution of the game window changed, remake the textures.
if (_heatTarget?.Texture.Size != target.Size)
{
_heatTarget?.Dispose();
_heatTarget = _clyde.CreateRenderTarget(
target.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: nameof(GasTileHeatOverlay));
}
if (_heatBlurTarget?.Texture.Size != target.Size)
{
_heatBlurTarget?.Dispose();
_heatBlurTarget = _clyde.CreateRenderTarget(
target.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: $"{nameof(GasTileHeatOverlay)}-blur");
}
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
var mapId = args.MapId;
var worldAABB = args.WorldAABB;
var worldBounds = args.WorldBounds;
var worldHandle = args.WorldHandle;
var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
// If there is no distortion after checking all visible tiles, we can bail early
var anyDistortion = false;
// We're rendering in the context of the heat target texture, which will encode data as to where and how strong
// the heat distortion will be
args.WorldHandle.RenderInRenderTarget(_heatTarget,
() =>
{
List<Entity<MapGridComponent>> grids = new();
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref grids);
foreach (var grid in grids)
{
if (!overlayQuery.TryGetComponent(grid.Owner, out var comp))
continue;
var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner);
var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt))
continue;
var uvToUi = Matrix3Helpers.CreateScale(_heatTarget.Size.X, -_heatTarget.Size.Y);
var uvToGridEnt = uvToUi * viewportLocalToGridEnt;
// Because we want the actual distortion to be calculated based on the grid coordinates*, we need
// to pass a matrix transformation to go from the viewport coordinates to grid coordinates.
// * (why? because otherwise the effect would shimmer like crazy as you moved around, think
// moving a piece of warped glass above a picture instead of placing the warped glass on the
// paper and moving them together)
_shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt);
// Draw commands (like DrawRect) will be using grid coordinates from here
worldHandle.SetTransform(gridEntToViewportLocal);
// We only care about tiles that fit in these bounds
var floatBounds = worldToViewportLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
var localBounds = new Box2i(
(int)MathF.Floor(floatBounds.Left),
(int)MathF.Floor(floatBounds.Bottom),
(int)MathF.Ceiling(floatBounds.Right),
(int)MathF.Ceiling(floatBounds.Top));
// for each tile and its gas --->
foreach (var chunk in comp.Chunks.Values)
{
var enumerator = new GasChunkEnumerator(chunk);
while (enumerator.MoveNext(out var tileGas))
{
// --->
// Check and make sure the tile is within the viewport/screen
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
if (!localBounds.Contains(tilePosition))
continue;
// Get the distortion strength from the temperature and bail if it's not hot enough
var strength = _gasTileOverlay.GetHeatDistortionStrength(tileGas.Temperature);
if (strength <= 0f)
continue;
anyDistortion = true;
// Encode the strength in the red channel, then 1.0 alpha if it's an active tile.
// BlurRenderTarget will then apply a blur around the edge, but we don't want it to bleed
// past the tile.
// So we use this alpha channel to chop the lower alpha values off in the shader to fit a
// fit mask back into the tile.
worldHandle.DrawRect(
Box2.CenteredAround(tilePosition + new Vector2(0.5f, 0.5f), grid.Comp.TileSizeVector),
new Color(strength,0f, 0f, strength > 0f ? 1.0f : 0f));
}
}
}
},
// This clears the buffer to all zero first...
new Color(0, 0, 0, 0));
// no distortion, no need to render
if (!anyDistortion)
{
// Return the draw handle to normal settings
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
return false;
}
// Clear to draw
return true;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture is null || _heatTarget is null || _heatBlurTarget is null)
return;
// Blur to soften the edges of the distortion. the lower parts of the alpha channel need to get cut off in the
// distortion shader to keep them in tile bounds.
_clyde.BlurRenderTarget(args.Viewport, _heatTarget, _heatBlurTarget, args.Viewport.Eye!, 14f);
// Set up and render the distortion
_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
args.WorldHandle.UseShader(_shader);
args.WorldHandle.DrawTextureRect(_heatTarget.Texture, args.WorldBounds);
// Return the draw handle to normal settings
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
protected override void DisposeBehavior()
{
_heatTarget = null;
_heatBlurTarget = null;
_configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion);
base.DisposeBehavior();
}
}

View File

@@ -208,7 +208,7 @@ namespace Content.Client.Atmos.UI
});
presBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.##}")),
Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.00}")),
Align = Label.AlignMode.Right,
HorizontalExpand = true
});
@@ -232,8 +232,8 @@ namespace Content.Client.Atmos.UI
tempBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
("tempK", $"{gasMix.Temperature:0.#}"),
("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.#}")),
("tempK", $"{gasMix.Temperature:0.0}"),
("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.0}")),
Align = Label.AlignMode.Right,
HorizontalExpand = true
});

View File

@@ -3,17 +3,13 @@ using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
@@ -31,6 +27,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -65,18 +62,19 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
get => _overlayEnabled;
set
{
if (_overlayEnabled == value) return;
if (_overlayEnabled == value)
return;
_overlayEnabled = value;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (_overlayEnabled)
{
_overlay = new AmbientSoundOverlay(EntityManager, this, EntityManager.System<EntityLookupSystem>());
overlayManager.AddOverlay(_overlay);
_overlayManager.AddOverlay(_overlay);
}
else
{
overlayManager.RemoveOverlay(_overlay!);
_overlayManager.RemoveOverlay(_overlay!);
_overlay = null;
}
}

View File

@@ -3,11 +3,8 @@ using Content.Client.Gameplay;
using Content.Shared.Audio;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Random;
using Content.Shared.Random.Rules;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
@@ -25,6 +22,7 @@ public sealed partial class ContentAudioSystem
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -61,7 +59,7 @@ public sealed partial class ContentAudioSystem
private void InitializeAmbientMusic()
{
Subs.CVar(_configManager, CCVars.AmbientMusicVolume, AmbienceCVarChanged, true);
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("audio.ambience");
_sawmill = _logManager.GetSawmill("audio.ambience");
// Reset audio
_nextAudio = TimeSpan.MaxValue;

View File

@@ -0,0 +1,30 @@
using Content.Shared.Changeling.Components;
using Content.Shared.Changeling.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Changeling.Systems;
public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChangelingIdentityComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnAfterAutoHandleState(Entity<ChangelingIdentityComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
public void UpdateUi(EntityUid uid)
{
if (_ui.TryGetOpenUi(uid, ChangelingTransformUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

@@ -1,8 +1,8 @@
using Content.Shared.Changeling.Transform;
using Content.Shared.Changeling.Systems;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Changeling.Transform;
namespace Content.Client.Changeling.UI;
[UsedImplicitly]
public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
@@ -16,16 +16,16 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
_window = this.CreateWindow<ChangelingTransformMenu>();
_window.OnIdentitySelect += SendIdentitySelect;
_window.Update(Owner);
}
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
if (state is not ChangelingTransformBoundUserInterfaceState current)
if (_window == null)
return;
_window?.UpdateState(current);
_window.Update(Owner);
}
public void SendIdentitySelect(NetEntity identityId)

View File

@@ -1,11 +1,11 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Changeling.Transform;
using Content.Shared.Changeling.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Changeling.Transform;
namespace Content.Client.Changeling.UI;
[GenerateTypedNameReferences]
public sealed partial class ChangelingTransformMenu : RadialMenu
@@ -19,13 +19,15 @@ public sealed partial class ChangelingTransformMenu : RadialMenu
IoCManager.InjectDependencies(this);
}
public void UpdateState(ChangelingTransformBoundUserInterfaceState state)
public void Update(EntityUid uid)
{
Main.DisposeAllChildren();
foreach (var identity in state.Identites)
{
var identityUid = _entity.GetEntity(identity);
if (!_entity.TryGetComponent<ChangelingIdentityComponent>(uid, out var identityComp))
return;
foreach (var identityUid in identityComp.ConsumedIdentities)
{
if (!_entity.TryGetComponent<MetaDataComponent>(identityUid, out var metadata))
continue;
@@ -48,7 +50,7 @@ public sealed partial class ChangelingTransformMenu : RadialMenu
entView.SetEntity(identityUid);
button.OnButtonUp += _ =>
{
OnIdentitySelect?.Invoke(identity);
OnIdentitySelect?.Invoke(_entity.GetNetEntity(identityUid));
Close();
};
button.AddChild(entView);

View File

@@ -2,7 +2,6 @@ using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.GameStates;
namespace Content.Client.Chemistry.EntitySystems;
@@ -11,6 +10,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainers));
Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
}
}

View File

@@ -38,13 +38,13 @@ public sealed class InjectorStatusControl : Control
// 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.TransferAmount
&& PrevTransferAmount == _parent.Comp.CurrentTransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevTransferAmount = _parent.Comp.TransferAmount;
PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
// Update current volume and injector state
@@ -59,6 +59,6 @@ public sealed class InjectorStatusControl : Control
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized),
("transferVolume", _parent.Comp.TransferAmount)));
("transferVolume", _parent.Comp.CurrentTransferAmount)));
}
}

View File

@@ -1,12 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Client.DisplacementMap;
using Content.Client.Inventory;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.DisplacementMap;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
@@ -14,7 +12,6 @@ using Content.Shared.Item;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
@@ -177,6 +174,7 @@ public sealed class ClientClothingSystem : ClothingSystem
var layer = new PrototypeLayerData();
layer.RsiPath = rsi.Path.ToString();
layer.State = state;
layer.Scale = clothing.Scale;
layers = new() { layer };
return true;

View File

@@ -80,7 +80,7 @@ public sealed partial class CreditsWindow : DefaultWindow
private async void PopulateAttributions(BoxContainer attributionsContainer, int count)
{
attributionsContainer.DisposeAllChildren();
attributionsContainer.RemoveAllChildren();
if (_attributions.Count == 0)
{
@@ -253,6 +253,8 @@ public sealed partial class CreditsWindow : DefaultWindow
private void PopulateLicenses(BoxContainer licensesContainer)
{
licensesContainer.RemoveAllChildren();
foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name))
{
licensesContainer.AddChild(new Label
@@ -269,6 +271,8 @@ public sealed partial class CreditsWindow : DefaultWindow
private void PopulatePatrons(BoxContainer patronsContainer)
{
patronsContainer.RemoveAllChildren();
var patrons = LoadPatrons();
// Do not show "become a patron" button on Steam builds
@@ -318,6 +322,8 @@ public sealed partial class CreditsWindow : DefaultWindow
private void PopulateContributors(BoxContainer ss14ContributorsContainer)
{
ss14ContributorsContainer.RemoveAllChildren();
Button contributeButton;
ss14ContributorsContainer.AddChild(new BoxContainer
@@ -356,6 +362,7 @@ public sealed partial class CreditsWindow : DefaultWindow
AddSection(Loc.GetString("credits-window-contributors-section-title"), "GitHub.txt");
AddSection(Loc.GetString("credits-window-codebases-section-title"), "SpaceStation13.txt");
AddSection(Loc.GetString("credits-window-original-remake-team-section-title"), "OriginalRemake.txt");
AddSection(Loc.GetString("credits-window-immortals-title"), "Immortals.txt", true);
AddSection(Loc.GetString("credits-window-special-thanks-section-title"), "SpecialThanks.txt", true);
var linkGithub = _cfg.GetCVar(CCVars.InfoLinksGithub);

View File

@@ -1,5 +1,6 @@
using Content.Shared.Drunk;
using Content.Shared.StatusEffect;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
@@ -43,19 +44,21 @@ public sealed class DrunkOverlay : Overlay
if (playerEntity == null)
return;
if (!_entityManager.HasComponent<DrunkComponent>(playerEntity)
|| !_entityManager.TryGetComponent<StatusEffectsComponent>(playerEntity, out var status))
var statusSys = _sysMan.GetEntitySystem<Shared.StatusEffectNew.StatusEffectsSystem>();
if (!statusSys.TryGetMaxTime<DrunkStatusEffectComponent>(playerEntity.Value, out var status))
return;
var statusSys = _sysMan.GetEntitySystem<StatusEffectsSystem>();
if (!statusSys.TryGetTime(playerEntity.Value, SharedDrunkSystem.DrunkKey, out var time, status))
return;
var time = status.Item2;
var curTime = _timing.CurTime;
var timeLeft = (float) (time.Value.Item2 - curTime).TotalSeconds;
var power = SharedDrunkSystem.MagicNumber;
if (time != null)
{
var curTime = _timing.CurTime;
power = (float) (time - curTime).Value.TotalSeconds;
}
CurrentBoozePower += 8f * (0.5f*timeLeft - CurrentBoozePower) * args.DeltaSeconds / (timeLeft+1);
CurrentBoozePower += 8f * (power * 0.5f - CurrentBoozePower) * args.DeltaSeconds / (power+1);
}
protected override bool BeforeDraw(in OverlayDrawArgs args)

View File

@@ -1,4 +1,5 @@
using Content.Shared.Drunk;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
@@ -16,38 +17,41 @@ public sealed class DrunkSystem : SharedDrunkSystem
{
base.Initialize();
SubscribeLocalEvent<DrunkComponent, ComponentInit>(OnDrunkInit);
SubscribeLocalEvent<DrunkComponent, ComponentShutdown>(OnDrunkShutdown);
SubscribeLocalEvent<DrunkStatusEffectComponent, StatusEffectAppliedEvent>(OnStatusApplied);
SubscribeLocalEvent<DrunkStatusEffectComponent, StatusEffectRemovedEvent>(OnStatusRemoved);
SubscribeLocalEvent<DrunkComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<DrunkComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<DrunkStatusEffectComponent, StatusEffectRelayedEvent<LocalPlayerAttachedEvent>>(OnPlayerAttached);
SubscribeLocalEvent<DrunkStatusEffectComponent, StatusEffectRelayedEvent<LocalPlayerDetachedEvent>>(OnPlayerDetached);
_overlay = new();
}
private void OnPlayerAttached(EntityUid uid, DrunkComponent component, LocalPlayerAttachedEvent args)
private void OnStatusApplied(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectAppliedEvent args)
{
_overlayMan.AddOverlay(_overlay);
if (!_overlayMan.HasOverlay<DrunkOverlay>())
_overlayMan.AddOverlay(_overlay);
}
private void OnPlayerDetached(EntityUid uid, DrunkComponent component, LocalPlayerDetachedEvent args)
private void OnStatusRemoved(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRemovedEvent args)
{
if (Status.HasEffectComp<DrunkStatusEffectComponent>(args.Target))
return;
if (_player.LocalEntity != args.Target)
return;
_overlay.CurrentBoozePower = 0;
_overlayMan.RemoveOverlay(_overlay);
}
private void OnDrunkInit(EntityUid uid, DrunkComponent component, ComponentInit args)
private void OnPlayerAttached(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRelayedEvent<LocalPlayerAttachedEvent> args)
{
if (_player.LocalEntity == uid)
_overlayMan.AddOverlay(_overlay);
_overlayMan.AddOverlay(_overlay);
}
private void OnDrunkShutdown(EntityUid uid, DrunkComponent component, ComponentShutdown args)
private void OnPlayerDetached(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRelayedEvent<LocalPlayerDetachedEvent> args)
{
if (_player.LocalEntity == uid)
{
_overlay.CurrentBoozePower = 0;
_overlayMan.RemoveOverlay(_overlay);
}
_overlay.CurrentBoozePower = 0;
_overlayMan.RemoveOverlay(_overlay);
}
}

View File

@@ -34,6 +34,8 @@ namespace Content.Client.GameTicking.Managers
[ViewVariables] public TimeSpan StartTime { get; private set; }
[ViewVariables] public new bool Paused { get; private set; }
public override IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => new List<(TimeSpan, string)>();
[ViewVariables] public IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> JobsAvailable => _jobsAvailable;
[ViewVariables] public IReadOnlyDictionary<NetEntity, string> StationNames => _stationNames;

View File

@@ -23,6 +23,8 @@ public sealed class ImplanterSystem : SharedImplanterSystem
{
if (_uiSystem.TryGetOpenUi<DeimplantBoundUserInterface>(uid, DeimplantUiKey.Key, out var bui))
{
// TODO: Don't use protoId for deimplanting
// and especially not raw strings!
Dictionary<string, string> implants = new();
foreach (var implant in component.DeimplantWhitelist)
{

View File

@@ -0,0 +1,5 @@
using Content.Shared.Implants;
namespace Content.Client.Implants;
public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem;

View File

@@ -21,7 +21,6 @@ namespace Content.Client.Inventory
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ClientClothingSystem _clothingVisualsSystem = default!;
[Dependency] private readonly ExamineSystem _examine = default!;

View File

@@ -1,8 +0,0 @@
using Content.Shared.Kitchen;
namespace Content.Client.Kitchen;
public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
{
}

View File

@@ -30,6 +30,10 @@ namespace Content.Client.Lathe.UI
{
SendMessage(new LatheQueueRecipeMessage(recipe, amount));
};
_menu.QueueDeleteAction += index => SendMessage(new LatheDeleteRequestMessage(index));
_menu.QueueMoveUpAction += index => SendMessage(new LatheMoveRequestMessage(index, -1));
_menu.QueueMoveDownAction += index => SendMessage(new LatheMoveRequestMessage(index, 1));
_menu.DeleteFabricatingAction += () => SendMessage(new LatheAbortFabricationMessage());
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -1,6 +1,7 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="clr-namespace:Content.Client.Materials.UI"
Title="{Loc 'lathe-menu-title'}"
MinSize="550 450"
@@ -110,6 +111,18 @@
HorizontalAlignment="Left"
Margin="130 0 0 0">
</Label>
<Button
Name="DeleteFabricating"
Margin="0"
Text="✖"
SetSize="38 32"
HorizontalAlignment="Right"
ToolTip="{Loc 'lathe-menu-delete-fabricating-tooltip'}">
<Button.StyleClasses>
<system:String>Caution</system:String>
<system:String>OpenLeft</system:String>
</Button.StyleClasses>
</Button>
</PanelContainer>
</BoxContainer>
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">

View File

@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Lathe.UI;
@@ -26,6 +27,10 @@ public sealed partial class LatheMenu : DefaultWindow
public event Action<BaseButton.ButtonEventArgs>? OnServerListButtonPressed;
public event Action<string, int>? RecipeQueueAction;
public event Action<int>? QueueDeleteAction;
public event Action<int>? QueueMoveUpAction;
public event Action<int>? QueueMoveDownAction;
public event Action? DeleteFabricatingAction;
public List<ProtoId<LatheRecipePrototype>> Recipes = new();
@@ -50,12 +55,21 @@ public sealed partial class LatheMenu : DefaultWindow
};
AmountLineEdit.OnTextChanged += _ =>
{
if (int.TryParse(AmountLineEdit.Text, out var amount))
{
if (amount > LatheSystem.MaxItemsPerRequest)
AmountLineEdit.Text = LatheSystem.MaxItemsPerRequest.ToString();
else if (amount < 0)
AmountLineEdit.Text = "0";
}
PopulateRecipes();
};
FilterOption.OnItemSelected += OnItemSelected;
ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a);
DeleteFabricating.OnPressed += _ => DeleteFabricatingAction?.Invoke();
}
public void SetEntity(EntityUid uid)
@@ -115,21 +129,50 @@ public sealed partial class LatheMenu : DefaultWindow
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
RecipeList.Children.Clear();
// Get the existing list of queue controls
var oldChildCount = RecipeList.ChildCount;
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
int idx = 0;
foreach (var prototype in sortedRecipesToShow)
{
var canProduce = _lathe.CanProduce(Entity, prototype, quantity, component: lathe);
var tooltipFunction = () => GenerateTooltipText(prototype);
var control = new RecipeControl(_lathe, prototype, () => GenerateTooltipText(prototype), canProduce, GetRecipeDisplayControl(prototype));
control.OnButtonPressed += s =>
if (idx >= oldChildCount)
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
amount = 1;
RecipeQueueAction?.Invoke(s, amount);
};
RecipeList.AddChild(control);
var control = new RecipeControl(_lathe, prototype, tooltipFunction, canProduce, GetRecipeDisplayControl(prototype));
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
amount = 1;
RecipeQueueAction?.Invoke(s, amount);
};
RecipeList.AddChild(control);
}
else
{
var child = RecipeList.GetChild(idx) as RecipeControl;
if (child == null)
{
DebugTools.Assert($"Lathe menu recipe control at {idx} is not of type RecipeControl"); // Something's gone terribly wrong.
continue;
}
child.SetRecipe(prototype);
child.SetTooltipSupplier(tooltipFunction);
child.SetCanProduce(canProduce);
child.SetDisplayControl(GetRecipeDisplayControl(prototype));
}
idx++;
}
// Shrink list if new list is shorter than old list.
for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
{
RecipeList.RemoveChild(childIdx);
}
}
@@ -223,25 +266,53 @@ public sealed partial class LatheMenu : DefaultWindow
/// Populates the build queue list with all queued items
/// </summary>
/// <param name="queue"></param>
public void PopulateQueueList(IReadOnlyCollection<ProtoId<LatheRecipePrototype>> queue)
public void PopulateQueueList(IReadOnlyCollection<LatheRecipeBatch> queue)
{
QueueList.DisposeAllChildren();
// Get the existing list of queue controls
var oldChildCount = QueueList.ChildCount;
var idx = 1;
foreach (var recipeProto in queue)
var idx = 0;
foreach (var batch in queue)
{
var recipe = _prototypeManager.Index(recipeProto);
var queuedRecipeBox = new BoxContainer();
queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
var recipe = _prototypeManager.Index(batch.Recipe);
queuedRecipeBox.AddChild(GetRecipeDisplayControl(recipe));
var itemName = _lathe.GetRecipeName(batch.Recipe);
string displayText;
if (batch.ItemsRequested > 1)
displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx + 1), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested));
else
displayText = Loc.GetString("lathe-menu-item-single", ("index", idx + 1), ("name", itemName));
var queuedRecipeLabel = new Label();
queuedRecipeLabel.Text = $"{idx}. {_lathe.GetRecipeName(recipe)}";
queuedRecipeBox.AddChild(queuedRecipeLabel);
QueueList.AddChild(queuedRecipeBox);
if (idx >= oldChildCount)
{
var queuedRecipeBox = new QueuedRecipeControl(displayText, idx, GetRecipeDisplayControl(recipe));
queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s);
queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s);
queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s);
QueueList.AddChild(queuedRecipeBox);
}
else
{
var child = QueueList.GetChild(idx) as QueuedRecipeControl;
if (child == null)
{
DebugTools.Assert($"Lathe menu queued recipe control at {idx} is not of type QueuedRecipeControl"); // Something's gone terribly wrong.
continue;
}
child.SetDisplayText(displayText);
child.SetIndex(idx);
child.SetDisplayControl(GetRecipeDisplayControl(recipe));
}
idx++;
}
// Shrink list if new list is shorter than old list.
for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
{
QueueList.RemoveChild(childIdx);
}
}
public void SetQueueInfo(ProtoId<LatheRecipePrototype>? recipeProto)

View File

@@ -0,0 +1,35 @@
<Control xmlns="https://spacestation14.io"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<BoxContainer Orientation="Horizontal">
<BoxContainer
Name="RecipeDisplayContainer"
Margin="0 0 4 0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MinSize="32 32"
/>
<Label Name="RecipeName" HorizontalExpand="True" />
<Button
Name="MoveUp"
Margin="0"
Text="⏶"
StyleClasses="OpenRight"
ToolTip="{Loc 'lathe-menu-move-up-tooltip'}"/>
<Button
Name="MoveDown"
Margin="0"
Text="⏷"
StyleClasses="OpenBoth"
ToolTip="{Loc 'lathe-menu-move-down-tooltip'}"/>
<Button
Name="Delete"
Margin="0"
Text="✖"
ToolTip="{Loc 'lathe-menu-delete-item-tooltip'}">
<Button.StyleClasses>
<system:String>Caution</system:String>
<system:String>OpenLeft</system:String>
</Button.StyleClasses>
</Button>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,56 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Lathe.UI;
[GenerateTypedNameReferences]
public sealed partial class QueuedRecipeControl : Control
{
public Action<int>? OnDeletePressed;
public Action<int>? OnMoveUpPressed;
public Action<int>? OnMoveDownPressed;
private int _index;
public QueuedRecipeControl(string displayText, int index, Control displayControl)
{
RobustXamlLoader.Load(this);
SetDisplayText(displayText);
SetDisplayControl(displayControl);
SetIndex(index);
_index = index;
MoveUp.OnPressed += (_) =>
{
OnMoveUpPressed?.Invoke(_index);
};
MoveDown.OnPressed += (_) =>
{
OnMoveDownPressed?.Invoke(_index);
};
Delete.OnPressed += (_) =>
{
OnDeletePressed?.Invoke(_index);
};
}
public void SetDisplayText(string displayText)
{
RecipeName.Text = displayText;
}
public void SetDisplayControl(Control displayControl)
{
RecipeDisplayContainer.Children.Clear();
RecipeDisplayContainer.AddChild(displayControl);
}
public void SetIndex(int index)
{
_index = index;
}
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -11,20 +12,47 @@ public sealed partial class RecipeControl : Control
public Action<string>? OnButtonPressed;
public Func<string> TooltipTextSupplier;
private ProtoId<LatheRecipePrototype> _recipeId;
private LatheSystem _latheSystem;
public RecipeControl(LatheSystem latheSystem, LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, Control displayControl)
{
RobustXamlLoader.Load(this);
RecipeName.Text = latheSystem.GetRecipeName(recipe);
RecipeDisplayContainer.AddChild(displayControl);
Button.Disabled = !canProduce;
_latheSystem = latheSystem;
_recipeId = recipe.ID;
TooltipTextSupplier = tooltipTextSupplier;
Button.TooltipSupplier = SupplyTooltip;
SetRecipe(recipe);
SetCanProduce(canProduce);
SetDisplayControl(displayControl);
Button.OnPressed += (_) =>
{
OnButtonPressed?.Invoke(recipe.ID);
OnButtonPressed?.Invoke(_recipeId);
};
Button.TooltipSupplier = SupplyTooltip;
}
public void SetRecipe(LatheRecipePrototype recipe)
{
RecipeName.Text = _latheSystem.GetRecipeName(recipe);
_recipeId = recipe.ID;
}
public void SetTooltipSupplier(Func<string> tooltipTextSupplier)
{
TooltipTextSupplier = tooltipTextSupplier;
}
public void SetCanProduce(bool canProduce)
{
Button.Disabled = !canProduce;
}
public void SetDisplayControl(Control displayControl)
{
RecipeDisplayContainer.Children.Clear();
RecipeDisplayContainer.AddChild(displayControl);
}
private Control? SupplyTooltip(Control sender)

View File

@@ -1,36 +1,46 @@
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Client.GameObjects;
namespace Content.Client.Light.Visualizers;
namespace Content.Client.Light.EntitySystems;
public sealed class LightBulbSystem : VisualizerSystem<LightBulbComponent>
public sealed class LightBulbSystem : SharedLightBulbSystem
{
protected override void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LightBulbComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
// update sprite state
if (AppearanceSystem.TryGetData<LightBulbState>(uid, LightBulbVisuals.State, out var state, args.Component))
if (_appearance.TryGetData<LightBulbState>(uid, LightBulbVisuals.State, out var state, args.Component))
{
switch (state)
{
case LightBulbState.Normal:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
break;
case LightBulbState.Broken:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
break;
case LightBulbState.Burned:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
break;
}
}
// also update sprites color
if (AppearanceSystem.TryGetData<Color>(uid, LightBulbVisuals.Color, out var color, args.Component))
if (_appearance.TryGetData<Color>(uid, LightBulbVisuals.Color, out var color, args.Component))
{
SpriteSystem.SetColor((uid, args.Sprite), color);
_sprite.SetColor((uid, args.Sprite), color);
}
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Light.EntitySystems;
namespace Content.Client.Light.EntitySystems;
public sealed class PoweredLightSystem : SharedPoweredLightSystem;

View File

@@ -72,6 +72,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
});
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
_configurationManager.OnValueChanged(CCVars.GameRoleLoadoutTimers, _ => RefreshProfileEditor());
_configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Medical.SuitSensors;
namespace Content.Client.Medical.SuitSensors;
public sealed class SuitSensorSystem : SharedSuitSensorSystem;

View File

@@ -0,0 +1,5 @@
using Content.Shared.Morgue;
namespace Content.Client.Morgue;
public sealed class CrematoriumSystem : SharedCrematoriumSystem;

View File

@@ -0,0 +1,5 @@
using Content.Shared.Morgue;
namespace Content.Client.Morgue;
public sealed class MorgueSystem : SharedMorgueSystem;

View File

@@ -1,7 +1,6 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
@@ -63,4 +62,15 @@ public sealed class ContentEyeSystem : SharedContentEyeSystem
UpdateEyeOffset((entity, eyeComponent));
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// TODO: Ideally we wouldn't want this to run in both FrameUpdate and Update, but we kind of have to since the visual update happens in FrameUpdate, but interaction update happens in Update. It's a workaround and a better solution should be found.
var eyeEntities = AllEntityQuery<ContentEyeComponent, EyeComponent>();
while (eyeEntities.MoveNext(out var entity, out ContentEyeComponent? contentComponent, out EyeComponent? eyeComponent))
{
UpdateEyeOffset((entity, eyeComponent));
}
}
}

View File

@@ -1,10 +1,10 @@
using System.Numerics;
using Content.Client.Movement.Components;
using Content.Client.Viewport;
using Content.Shared.Camera;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Map;
using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
@@ -12,13 +12,10 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IClyde _clyde = default!;
// This value is here to make sure the user doesn't have to move their mouse
// all the way out to the edge of the screen to get the full offset.
static private float _edgeOffset = 0.9f;
private static float _edgeOffset = 0.8f;
public override void Initialize()
{
@@ -38,25 +35,29 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component)
{
var localPlayer = _player.LocalEntity;
var mousePos = _inputManager.MouseScreenPosition;
var screenSize = _clyde.MainWindow.Size;
var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset;
var mouseNormalizedPos = new Vector2(-(mousePos.X - screenSize.X / 2) / minValue, (mousePos.Y - screenSize.Y / 2) / minValue); // X needs to be inverted here for some reason, otherwise it ends up flipped.
if (localPlayer == null)
// We need the main viewport where the game content is displayed, as certain UI layouts (e.g. Separated HUD) can make it a different size to the game window.
if (_eyeManager.MainViewport is not ScalingViewport vp)
return null;
var playerPos = _transform.GetWorldPosition(localPlayer.Value);
var mousePos = _inputManager.MouseScreenPosition.Position; // TODO: If we ever get a right-aligned Separated HUD setting, this might need to be adjusted for that.
var viewportSize = vp.PixelSize; // The size of the game viewport, including black bars - does not include the chatbox in Separated HUD view.
var scalingViewportSize = vp.ViewportSize * vp.CurrentRenderScale; // The size of the viewport in which the game is rendered (i.e. not including black bars). Note! Can extend outside the game window with certain zoom settings!
var visibleViewportSize = Vector2.Min(viewportSize, scalingViewportSize); // The size of the game viewport that is "actually visible" to the player, cutting off over-extensions and not counting black bar padding.
Matrix3x2.Invert(_eyeManager.MainViewport.GetLocalToScreenMatrix(), out var matrix);
var mouseCoords = Vector2.Transform(mousePos, matrix); // Gives the mouse position inside of the *scaling viewport*, i.e. 0,0 is inside the black bars. Note! 0,0 can be outside the game window with certain zoom settings!
var boundedMousePos = Vector2.Clamp(Vector2.Min(mouseCoords, mousePos), Vector2.Zero, visibleViewportSize); // Mouse position inside the visible game viewport's bounds.
var offsetRadius = MathF.Min(visibleViewportSize.X / 2f, visibleViewportSize.Y / 2f) * _edgeOffset;
var mouseNormalizedPos = new Vector2(-(boundedMousePos.X - visibleViewportSize.X / 2f) / offsetRadius, (boundedMousePos.Y - visibleViewportSize.Y / 2f) / offsetRadius);
if (component == null)
{
component = EnsureComp<EyeCursorOffsetComponent>(uid);
}
// Doesn't move the offset if the mouse has left the game window!
if (mousePos.Window != WindowId.Invalid)
if (_inputManager.MouseScreenPosition.Window != WindowId.Invalid)
{
// The offset must account for the in-world rotation.
var eyeRotation = _eyeManager.CurrentEye.Rotation;
@@ -77,7 +78,7 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
Vector2 vectorOffset = component.TargetPosition - component.CurrentPosition;
if (vectorOffset.Length() > component.OffsetSpeed)
{
vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed;
vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed; // TODO: Probably needs to properly account for time delta or something.
}
component.CurrentPosition += vectorOffset;
}

View File

@@ -20,6 +20,7 @@ namespace Content.Client.NPC
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly NPCSteeringSystem _steering = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
@@ -30,17 +31,15 @@ namespace Content.Client.NPC
get => _modes;
set
{
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (value == PathfindingDebugMode.None)
{
Breadcrumbs.Clear();
Polys.Clear();
overlayManager.RemoveOverlay<PathfindingOverlay>();
_overlayManager.RemoveOverlay<PathfindingOverlay>();
}
else if (!overlayManager.HasOverlay<PathfindingOverlay>())
else if (!_overlayManager.HasOverlay<PathfindingOverlay>())
{
overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
_overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
}
if ((value & PathfindingDebugMode.Steering) != 0x0)

View File

@@ -120,8 +120,8 @@ public sealed class MoverController : SharedMoverController
base.SetSprinting(entity, subTick, walking);
if (walking && _cfg.GetCVar(CCVars.ToggleWalk))
_alerts.ShowAlert(entity, WalkingAlert, showCooldown: false, autoRemove: false);
_alerts.ShowAlert(entity.Owner, WalkingAlert, showCooldown: false, autoRemove: false);
else
_alerts.ClearAlert(entity, WalkingAlert);
_alerts.ClearAlert(entity.Owner, WalkingAlert);
}
}

View File

@@ -5,4 +5,5 @@ namespace Content.Client.Power.Components;
[RegisterComponent]
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
{
public override float Load { get; set; }
}

View File

@@ -11,6 +11,8 @@ namespace Content.Client.Radiation.Overlays;
public sealed class RadiationDebugOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IResourceCache _cache = default!;
private readonly SharedMapSystem _mapSystem;
private readonly RadiationSystem _radiation;
@@ -24,8 +26,7 @@ public sealed class RadiationDebugOverlay : Overlay
_radiation = _entityManager.System<RadiationSystem>();
_mapSystem = _entityManager.System<SharedMapSystem>();
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
_font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
}
protected override void Draw(in OverlayDrawArgs args)

View File

@@ -28,6 +28,8 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
public EntityUid Entity;
private bool _allowBorgControl = true;
public RoboticsConsoleWindow()
{
RobustXamlLoader.Load(this);
@@ -72,6 +74,7 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
public void UpdateState(RoboticsConsoleState state)
{
_cyborgs = state.Cyborgs;
_allowBorgControl = state.AllowBorgControl;
// clear invalid selection
if (_selected is {} selected && !_cyborgs.ContainsKey(selected))
@@ -85,8 +88,8 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
PopulateData();
var locked = _lock.IsLocked(Entity);
DangerZone.Visible = !locked;
LockedMessage.Visible = locked;
DangerZone.Visible = !locked && _allowBorgControl;
LockedMessage.Visible = locked && _allowBorgControl; // Only show if locked AND control is allowed
}
private void PopulateCyborgs()
@@ -120,11 +123,19 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
BorgSprite.Texture = _sprite.Frame0(data.ChassisSprite!);
var batteryColor = data.Charge switch {
< 0.2f => "red",
< 0.4f => "orange",
< 0.6f => "yellow",
< 0.8f => "green",
_ => "blue"
< 0.2f => "#FF6C7F", // red
< 0.4f => "#EF973C", // orange
< 0.6f => "#E8CB2D", // yellow
< 0.8f => "#30CC19", // green
_ => "#00D3B8" // cyan
};
var hpPercentColor = data.HpPercent switch {
< 0.2f => "#FF6C7F", // red
< 0.4f => "#EF973C", // orange
< 0.6f => "#E8CB2D", // yellow
< 0.8f => "#30CC19", // green
_ => "#00D3B8" // cyan
};
var text = new FormattedMessage();
@@ -132,12 +143,14 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
text.AddMarkupOrThrow(Loc.GetString("robotics-console-designation"));
text.AddText($" {data.Name}\n"); // prevent players trolling by naming borg [color=red]satan[/color]
text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-battery", ("charge", (int)(data.Charge * 100f)), ("color", batteryColor))}\n");
text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-hp", ("hp", (int)(data.HpPercent * 100f)), ("color", hpPercentColor))}\n");
text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-brain", ("brain", data.HasBrain))}\n");
text.AddMarkupOrThrow(Loc.GetString("robotics-console-modules", ("count", data.ModuleCount)));
BorgInfo.SetMessage(text);
// how the turntables
DisableButton.Disabled = !(data.HasBrain && data.CanDisable);
DisableButton.Disabled = !_allowBorgControl || !(data.HasBrain && data.CanDisable);
DestroyButton.Disabled = !_allowBorgControl;
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -16,20 +16,20 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
get => _enableShuttlePosition;
set
{
if (_enableShuttlePosition == value) return;
if (_enableShuttlePosition == value)
return;
_enableShuttlePosition = value;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (_enableShuttlePosition)
{
_overlay = new EmergencyShuttleOverlay(EntityManager.TransformQuery, XformSystem);
overlayManager.AddOverlay(_overlay);
_overlays.AddOverlay(_overlay);
RaiseNetworkEvent(new EmergencyShuttleRequestPositionMessage());
}
else
{
overlayManager.RemoveOverlay(_overlay!);
_overlays.RemoveOverlay(_overlay!);
_overlay = null;
}
}

View File

@@ -40,6 +40,8 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
private readonly HashSet<DockingPortState> _drawnDocks = new();
private readonly Dictionary<DockingPortState, Button> _dockButtons = new();
private readonly Color _fallbackHighlightedColor = Color.Magenta;
/// <summary>
/// Store buttons for every other dock
/// </summary>
@@ -213,11 +215,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
if (HighlightedDock == dock.Entity)
{
otherDockColor = Color.ToSrgb(Color.Magenta);
otherDockColor = Color.ToSrgb(dock.HighlightedColor);
}
else
{
otherDockColor = Color.ToSrgb(Color.Purple);
otherDockColor = Color.ToSrgb(dock.Color);
}
/*
@@ -311,7 +313,7 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
ScalePosition(Vector2.Transform(new Vector2(-0.5f, 0.5f), rotation)),
ScalePosition(Vector2.Transform(new Vector2(0.5f, -0.5f), rotation)));
var dockColor = Color.Magenta;
var dockColor = _viewedState?.HighlightedColor ?? _fallbackHighlightedColor;
var connectionColor = Color.Pink;
handle.DrawRect(ourDockConnection, connectionColor.WithAlpha(0.2f));

View File

@@ -308,7 +308,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
-dockRadius * UIScale,
(Size.X + dockRadius) * UIScale,
(Size.Y + dockRadius) * UIScale);
if (_docks.TryGetValue(nent, out var docks))
{
foreach (var state in docks)
@@ -321,7 +321,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
continue;
}
var color = Color.ToSrgb(Color.Magenta);
var color = Color.ToSrgb(state.HighlightedColor);
var verts = new[]
{

View File

@@ -12,7 +12,6 @@ namespace Content.Client.Stack
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly ItemCounterSystem _counterSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@@ -28,22 +27,8 @@ namespace Content.Client.Stack
base.SetCount(uid, amount, component);
if (component.Lingering &&
TryComp<SpriteComponent>(uid, out var sprite))
{
// tint the stack gray and make it transparent if it's lingering.
var color = component.Count == 0 && component.Lingering
? Color.DarkGray.WithAlpha(0.65f)
: Color.White;
for (var i = 0; i < sprite.AllLayers.Count(); i++)
{
_sprite.LayerSetColor((uid, sprite), i, color);
}
}
// TODO PREDICT ENTITY DELETION: This should really just be a normal entity deletion call.
if (component.Count <= 0 && !component.Lingering)
if (component.Count <= 0)
{
Xform.DetachEntity(uid, Transform(uid));
return;

View File

@@ -1,10 +0,0 @@
using Content.Shared.Storage.Components;
using Robust.Shared.GameStates;
namespace Content.Client.Storage.Components;
[RegisterComponent]
public sealed partial class EntityStorageComponent : SharedEntityStorageComponent
{
}

View File

@@ -31,7 +31,7 @@ public sealed class EntityStorageSystem : SharedEntityStorageSystem
SubscribeLocalEvent<EntityStorageComponent, ComponentHandleState>(OnHandleState);
}
public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref SharedEntityStorageComponent? component)
public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref EntityStorageComponent? component)
{
if (component != null)
return true;

View File

@@ -25,7 +25,7 @@ public sealed class MenuButton : ContainerButton
private Color NormalColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedNormal : ColorNormal;
private Color HoveredColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedHovered : ColorHovered;
private BoundKeyFunction _function;
private BoundKeyFunction? _function;
private readonly BoxContainer _root;
private readonly TextureRect? _buttonIcon;
private readonly Label? _buttonLabel;
@@ -33,13 +33,13 @@ public sealed class MenuButton : ContainerButton
public string AppendStyleClass { set => AddStyleClass(value); }
public Texture? Icon { get => _buttonIcon!.Texture; set => _buttonIcon!.Texture = value; }
public BoundKeyFunction BoundKey
public BoundKeyFunction? BoundKey
{
get => _function;
set
{
_function = value;
_buttonLabel!.Text = BoundKeyHelper.ShortKeyName(value);
_buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
}
@@ -95,12 +95,12 @@ public sealed class MenuButton : ContainerButton
private void OnKeyBindingChanged(IKeyBinding obj)
{
_buttonLabel!.Text = BoundKeyHelper.ShortKeyName(_function);
_buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
private void OnKeyBindingChanged()
{
_buttonLabel!.Text = BoundKeyHelper.ShortKeyName(_function);
_buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
protected override void StylePropertiesChanged()

View File

@@ -1,9 +1,11 @@
using System.Numerics;
using Content.Client.Cooldown;
using Content.Client.UserInterface.Systems.Inventory.Controls;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls
{
@@ -20,6 +22,7 @@ namespace Content.Client.UserInterface.Controls
public CooldownGraphic CooldownDisplay { get; }
private SpriteView SpriteView { get; }
private EntityPrototypeView ProtoView { get; }
public EntityUid? Entity => SpriteView.Entity;
@@ -141,6 +144,13 @@ namespace Content.Client.UserInterface.Controls
SetSize = new Vector2(DefaultButtonSize, DefaultButtonSize),
OverrideDirection = Direction.South
});
AddChild(ProtoView = new EntityPrototypeView
{
Visible = false,
Scale = new Vector2(2, 2),
SetSize = new Vector2(DefaultButtonSize, DefaultButtonSize),
OverrideDirection = Direction.South
});
AddChild(HoverSpriteView = new SpriteView
{
@@ -209,12 +219,35 @@ namespace Content.Client.UserInterface.Controls
HoverSpriteView.SetEntity(null);
}
/// <summary>
/// Causes the control to display a placeholder prototype, optionally faded
/// </summary>
public void SetEntity(EntityUid? ent)
{
SpriteView.SetEntity(ent);
SpriteView.Visible = true;
ProtoView.Visible = false;
UpdateButtonTexture();
}
/// <summary>
/// Causes the control to display a placeholder prototype, optionally faded
/// </summary>
public void SetPrototype(EntProtoId? proto, bool fade)
{
ProtoView.SetPrototype(proto);
SpriteView.Visible = false;
ProtoView.Visible = true;
UpdateButtonTexture();
if (ProtoView.Entity is not { } ent || !fade)
return;
var sprites = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
sprites.SetColor((ent.Owner, ent.Comp1), Color.DarkGray.WithAlpha(0.65f));
}
private void UpdateButtonTexture()
{
var fullTexture = Theme.ResolveTextureOrNull(_fullButtonTexturePath);

View File

@@ -3,12 +3,12 @@ using Content.Client.Actions;
using Content.Client.Actions.UI;
using Content.Client.Cooldown;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Examine;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
@@ -23,9 +23,9 @@ namespace Content.Client.UserInterface.Systems.Actions.Controls;
public sealed class ActionButton : Control, IEntityControl
{
private IEntityManager _entities;
private IPlayerManager _player;
private SpriteSystem? _spriteSys;
private ActionUIController? _controller;
private SharedChargesSystem _sharedChargesSys;
private bool _beingHovered;
private bool _depressed;
private bool _toggled;
@@ -67,8 +67,8 @@ public sealed class ActionButton : Control, IEntityControl
// TODO why is this constructor so slooooow. The rest of the code is fine
_entities = entities;
_player = IoCManager.Resolve<IPlayerManager>();
_spriteSys = spriteSys;
_sharedChargesSys = _entities.System<SharedChargesSystem>();
_controller = controller;
MouseFilter = MouseFilterMode.Pass;
@@ -197,23 +197,17 @@ public sealed class ActionButton : Control, IEntityControl
return null;
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
FormattedMessage? chargesText = null;
var desc = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
// TODO: Don't touch this use an event make callers able to add their own shit for actions or I kill you.
if (_entities.TryGetComponent(Action, out LimitedChargesComponent? actionCharges))
{
var charges = _sharedChargesSys.GetCurrentCharges((Action.Value, actionCharges, null));
chargesText = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {charges.ToString()}/{actionCharges.MaxCharges}"));
if (_player.LocalEntity is null)
return null;
if (_entities.TryGetComponent(Action, out AutoRechargeComponent? autoRecharge))
{
var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((Action.Value, actionCharges, autoRecharge));
chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
}
}
var ev = new ExaminedEvent(desc, Action.Value, _player.LocalEntity.Value, true, !desc.IsEmpty);
_entities.EventBus.RaiseLocalEvent(Action.Value.Owner, ev);
return new ActionAlertTooltip(name, decr, charges: chargesText);
var newDesc = ev.GetTotalMessage();
return new ActionAlertTooltip(name, newDesc);
}
protected override void ControlFocusExited()

View File

@@ -16,6 +16,7 @@ using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
@@ -37,6 +38,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IInputManager _input = default!;
[UISystemDependency] private readonly AudioSystem _audio = default!;
private BwoinkSystem? _bwoinkSystem;
@@ -98,15 +100,13 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
_bwoinkSystem = system;
_bwoinkSystem.OnBwoinkTextMessageRecieved += ReceivedBwoink;
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenAHelp,
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
.Register<AHelpUIController>();
_input.SetInputCommand(ContentKeyFunctions.OpenAHelp,
InputCmdHandler.FromDelegate(_ => ToggleWindow()));
}
public void OnSystemUnloaded(BwoinkSystem system)
{
CommandBinds.Unregister<AHelpUIController>();
_input.SetInputCommand(ContentKeyFunctions.OpenAHelp, null);
DebugTools.Assert(_bwoinkSystem != null);
_bwoinkSystem!.OnBwoinkTextMessageRecieved -= ReceivedBwoink;

View File

@@ -116,8 +116,9 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
keyword = EndDoubleQuote.Replace(keyword, "(?<!\\w)");
}
// Make sure any name tagged as ours gets highlighted only when others say it.
keyword = StartAtSign.Replace(keyword, "(?<=(?<=/name.*)|(?<=,.*\"\".*))");
// Make sure the character's name is highlighted only when mentioned directly (eg. it's said by someone),
// for example in 'Name Surname says, "..."' 'Name Surname' won't be highlighted.
keyword = StartAtSign.Replace(keyword, @"(?<=(?<=^.?OOC:.*:.*)|(?<=,.*"".*)|(?<=\n.*))");
_highlights.Add(keyword);
}

View File

@@ -12,6 +12,7 @@ using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Input;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -73,7 +74,8 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
{
if (entity.Owner != _player.LocalEntity)
return;
AddHand(name, location);
if (_handsSystem.TryGetHand((entity.Owner, entity.Comp), name, out var hand))
AddHand(name, hand.Value);
}
private void OnRemoveHand(Entity<HandsComponent> entity, string name)
@@ -139,7 +141,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_playerHandsComponent = handsComp;
foreach (var (name, hand) in handsComp.Comp.Hands)
{
var handButton = AddHand(name, hand.Location);
var handButton = AddHand(name, hand);
if (_handsSystem.TryGetHeldItem(handsComp.AsNullable(), name, out var held) &&
_entities.TryGetComponent(held, out VirtualItemComponent? virt))
@@ -147,11 +149,25 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
handButton.SetEntity(virt.BlockingEntity);
handButton.Blocked = true;
}
else
else if (held != null)
{
handButton.SetEntity(held);
handButton.Blocked = false;
}
else
{
if (hand.EmptyRepresentative is { } representative)
{
// placeholder, view it
SetRepresentative(handButton, representative);
}
else
{
// otherwise empty
handButton.SetEntity(null);
}
handButton.Blocked = false;
}
}
if (handsComp.Comp.ActiveHandId == null)
@@ -159,6 +175,11 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
SetActiveHand(handsComp.Comp.ActiveHandId);
}
private void SetRepresentative(HandButton handButton, EntProtoId prototype)
{
handButton.SetPrototype(prototype, true);
}
private void HandBlocked(string handName)
{
if (!_handLookup.TryGetValue(handName, out var hand))
@@ -203,7 +224,12 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
hand.Blocked = false;
}
UpdateHandStatus(hand, entity);
if (_playerHandsComponent != null &&
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), name, out var handData))
{
UpdateHandStatus(hand, entity, handData);
}
}
private void OnItemRemoved(string name, EntityUid entity)
@@ -212,8 +238,19 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (hand == null)
return;
if (_playerHandsComponent != null &&
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), name, out var handData))
{
UpdateHandStatus(hand, null, handData);
if (handData?.EmptyRepresentative is { } representative)
{
SetRepresentative(hand, representative);
return;
}
}
hand.SetEntity(null);
UpdateHandStatus(hand, null);
}
private HandsContainer GetFirstAvailableContainer()
@@ -276,13 +313,13 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (foldedLocation == HandUILocation.Left)
{
_statusHandLeft = handControl;
HandsGui.UpdatePanelEntityLeft(heldEnt);
HandsGui.UpdatePanelEntityLeft(heldEnt, hand.Value);
}
else
{
// Middle or right
_statusHandRight = handControl;
HandsGui.UpdatePanelEntityRight(heldEnt);
HandsGui.UpdatePanelEntityRight(heldEnt, hand.Value);
}
HandsGui.SetHighlightHand(foldedLocation);
@@ -295,9 +332,9 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
return handControl;
}
private HandButton AddHand(string handName, HandLocation location)
private HandButton AddHand(string handName, Hand hand)
{
var button = new HandButton(handName, location);
var button = new HandButton(handName, hand.Location);
button.StoragePressed += StorageActivate;
button.Pressed += HandPressed;
@@ -313,10 +350,16 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
GetFirstAvailableContainer().AddButton(button);
}
if (hand.EmptyRepresentative is { } representative)
{
SetRepresentative(button, representative);
}
UpdateHandStatus(button, null, hand);
// If we don't have a status for this hand type yet, set it.
// This means we have status filled by default in most scenarios,
// otherwise the user'd need to switch hands to "activate" the hands the first time.
if (location.GetUILocation() == HandUILocation.Left)
if (hand.Location.GetUILocation() == HandUILocation.Left)
_statusHandLeft ??= button;
else
_statusHandRight ??= button;
@@ -480,12 +523,12 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
}
}
private void UpdateHandStatus(HandButton hand, EntityUid? entity)
private void UpdateHandStatus(HandButton hand, EntityUid? entity, Hand? handData)
{
if (hand == _statusHandLeft)
HandsGui?.UpdatePanelEntityLeft(entity);
HandsGui?.UpdatePanelEntityLeft(entity, handData);
if (hand == _statusHandRight)
HandsGui?.UpdatePanelEntityRight(entity);
HandsGui?.UpdatePanelEntityRight(entity, handData);
}
}

View File

@@ -19,14 +19,14 @@ public sealed partial class HotbarGui : UIWidget
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
}
public void UpdatePanelEntityLeft(EntityUid? entity)
public void UpdatePanelEntityLeft(EntityUid? entity, Hand? hand)
{
StatusPanelLeft.Update(entity);
StatusPanelLeft.Update(entity, hand);
}
public void UpdatePanelEntityRight(EntityUid? entity)
public void UpdatePanelEntityRight(EntityUid? entity, Hand? hand)
{
StatusPanelRight.Update(entity);
StatusPanelRight.Update(entity, hand);
}
public void SetHighlightHand(HandUILocation? hand)

View File

@@ -17,6 +17,7 @@ public sealed partial class ItemStatusPanel : Control
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] private EntityUid? _entity;
[ViewVariables] private Hand? _hand;
// Tracked so we can re-run SetSide() if the theme changes.
private HandUILocation _side;
@@ -101,29 +102,45 @@ public sealed partial class ItemStatusPanel : Control
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateItemName();
UpdateItemName(_hand);
}
public void Update(EntityUid? entity)
public void Update(EntityUid? entity, Hand? hand)
{
ItemNameLabel.Visible = entity != null;
NoItemLabel.Visible = entity == null;
if (entity == _entity && hand == _hand)
return;
_hand = hand;
if (entity == null)
{
ItemNameLabel.Text = "";
ClearOldStatus();
_entity = null;
if (hand?.EmptyLabel is { } label)
{
ItemNameLabel.Visible = true;
NoItemLabel.Visible = false;
ItemNameLabel.Text = Loc.GetString(label);
}
else
{
ItemNameLabel.Visible = false;
NoItemLabel.Visible = true;
ItemNameLabel.Text = "";
}
return;
}
if (entity != _entity)
{
_entity = entity.Value;
BuildNewEntityStatus();
ItemNameLabel.Visible = true;
NoItemLabel.Visible = false;
UpdateItemName();
}
_entity = entity.Value;
BuildNewEntityStatus();
UpdateItemName(hand);
}
public void UpdateHighlight(bool highlight)
@@ -131,14 +148,14 @@ public sealed partial class ItemStatusPanel : Control
HighlightPanel.Visible = highlight;
}
private void UpdateItemName()
private void UpdateItemName(Hand? hand)
{
if (_entity == null)
return;
if (!_entityManager.TryGetComponent<MetaDataComponent>(_entity, out var meta) || meta.Deleted)
{
Update(null);
Update(null, hand);
return;
}

View File

@@ -30,6 +30,7 @@ public sealed partial class GunSystem : SharedGunSystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IStateManager _state = default!;
[Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
@@ -50,11 +51,10 @@ public sealed partial class GunSystem : SharedGunSystem
return;
_spreadOverlay = value;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (_spreadOverlay)
{
overlayManager.AddOverlay(new GunSpreadOverlay(
_overlayManager.AddOverlay(new GunSpreadOverlay(
EntityManager,
_eyeManager,
Timing,
@@ -65,7 +65,7 @@ public sealed partial class GunSystem : SharedGunSystem
}
else
{
overlayManager.RemoveOverlay<GunSpreadOverlay>();
_overlayManager.RemoveOverlay<GunSpreadOverlay>();
}
}
}

View File

@@ -29,7 +29,10 @@ public sealed class WieldableSystem : SharedWieldableSystem
return;
if (_gameTiming.IsFirstTimePredicted)
{
cursorOffsetComp.CurrentPosition = Vector2.Zero;
cursorOffsetComp.TargetPosition = Vector2.Zero;
}
}
public void OnGetEyeOffset(Entity<CursorOffsetRequiresWieldComponent> entity, ref HeldRelayedEvent<GetEyeOffsetRelayedEvent> args)

View File

@@ -5,4 +5,4 @@
// https://github.com/dotnet/runtime/issues/107197
// So we can't really parallelize integration tests harder either until the runtime fixes that,
// *or* we fix serv3 to not spam expression trees.
[assembly: LevelOfParallelism(3)]
[assembly: LevelOfParallelism(2)]

View File

@@ -21,6 +21,7 @@ public static partial class PoolManager
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
(CCVars.GameRoleLoadoutTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
(CCVars.PreloadGrids.Name, "false"),

View File

@@ -0,0 +1,417 @@
using System.Linq;
using System.Numerics;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
/// <summary>
/// Tests for AtmosphereSystem.DeltaPressure and surrounding systems
/// handling the DeltaPressureComponent.
/// </summary>
[TestFixture]
[TestOf(typeof(DeltaPressureSystem))]
public sealed class DeltaPressureTest
{
#region Prototypes
[TestPrototypes]
private const string Prototypes = @"
- type: entity
parent: BaseStructure
id: DeltaPressureSolidTest
placement:
mode: SnapgridCenter
snap:
- Wall
components:
- type: Physics
bodyType: Static
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: ""-0.5,-0.5,0.5,0.5""
mask:
- FullTileMask
layer:
- WallLayer
density: 1000
- type: Airtight
- type: DeltaPressure
minPressure: 15000
minPressureDelta: 10000
scalingType: Threshold
baseDamage:
types:
Structural: 1000
- type: Damageable
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 300
behaviors:
- !type:SpawnEntitiesBehavior
spawn:
Girder:
min: 1
max: 1
- !type:DoActsBehavior
acts: [ ""Destruction"" ]
- type: entity
parent: DeltaPressureSolidTest
id: DeltaPressureSolidTestNoAutoJoin
components:
- type: DeltaPressure
autoJoinProcessingList: false
- type: entity
parent: DeltaPressureSolidTest
id: DeltaPressureSolidTestAbsolute
components:
- type: DeltaPressure
minPressure: 10000
minPressureDelta: 15000
scalingType: Threshold
baseDamage:
types:
Structural: 1000
";
#endregion
private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
/// <summary>
/// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
/// set to true is automatically added to the DeltaPressure processing list
/// on the grid's GridAtmosphereComponent.
///
/// Also asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
/// set to false is not automatically added to the DeltaPressure processing list.
/// </summary>
[Test]
public async Task ProcessingListAutoJoinTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
await server.WaitAssertion(() =>
{
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
entMan.DeleteEntity(uid);
Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that doesn't need to be damaged by DeltaPressure
/// is not damaged by DeltaPressure.
/// </summary>
[Test]
public async Task ProcessingDeltaStandbyTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressureDelta - 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
await server.WaitAssertion(() =>
{
Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that needs to be damaged by DeltaPressure
/// is damaged by DeltaPressure when the pressure is above the threshold.
/// </summary>
[Test]
public async Task ProcessingDeltaDamageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
// Need to spawn an entity each run to ensure it works for all directions.
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressureDelta + 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
await server.WaitAssertion(() =>
{
Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that doesn't need to be damaged by DeltaPressure
/// is not damaged by DeltaPressure when using absolute pressure thresholds.
/// </summary>
[Test]
public async Task ProcessingAbsoluteStandbyTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressure - 10; // just below absolute threshold
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await server.WaitAssertion(() =>
{
Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that needs to be damaged by DeltaPressure
/// is damaged by DeltaPressure when the pressure is above the absolute threshold.
/// </summary>
[Test]
public async Task ProcessingAbsoluteDamageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
// Spawn fresh entity each iteration to verify all directions work
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
// Above absolute threshold but below delta threshold to ensure absolute alone causes damage
var toPressurize = dpEnt.Comp!.MinPressure + 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await server.WaitAssertion(() =>
{
Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,85 @@
using System.Linq;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Atmos;
[TestFixture]
[TestOf(typeof(Atmospherics))]
public sealed class GasArrayTest
{
private const string GasTankTestDummyId = "GasTankTestDummy";
private const string GasTankLegacyTestDummyId = "GasTankLegacyTestDummy";
[TestPrototypes]
private const string Prototypes = $@"
- type: entity
id: {GasTankTestDummyId}
components:
- type: GasTank
air:
volume: 5
moles:
Frezon: 20
Oxygen: 10
- type: entity
id: {GasTankLegacyTestDummyId}
components:
- type: GasTank
air:
volume: 5
moles:
- 0
- 0
- 0
- 10
";
[Test]
public async Task TestGasArrayDeserialization()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var compFactory = server.ResolveDependency<IComponentFactory>();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
var gasTank = prototypeManager.Index(GasTankTestDummyId);
Assert.Multiple(() =>
{
Assert.That(gasTank.TryGetComponent<GasTankComponent>(out var gasTankComponent, compFactory));
Assert.That(gasTankComponent!.Air.GetMoles(Gas.Oxygen), Is.EqualTo(10));
Assert.That(gasTankComponent!.Air.GetMoles(Gas.Frezon), Is.EqualTo(20));
foreach (var gas in Enum.GetValues<Gas>().Where(p => p != Gas.Oxygen && p != Gas.Frezon))
{
Assert.That(gasTankComponent!.Air.GetMoles(gas), Is.EqualTo(0));
}
});
var legacyGasTank = prototypeManager.Index(GasTankLegacyTestDummyId);
Assert.Multiple(() =>
{
Assert.That(legacyGasTank.TryGetComponent<GasTankComponent>(out var gasTankComponent, compFactory));
Assert.That(gasTankComponent!.Air.GetMoles(3), Is.EqualTo(10));
// Iterate through all other gases: check for 0 values
for (var i = 0; i < Atmospherics.AdjustedNumberOfGases; i++)
{
if (i == 3) // our case with a value.
continue;
Assert.That(gasTankComponent!.Air.GetMoles(i), Is.EqualTo(0));
}
});
});
await pair.CleanReturnAsync();
}
}

View File

@@ -6,6 +6,7 @@ using Content.Server.Cargo.Systems;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Mobs.Components;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
using Content.Shared.Whitelist;
@@ -250,4 +251,25 @@ public sealed class CargoTest
await pair.CleanReturnAsync();
}
[Test]
public async Task MobPrice()
{
await using var pair = await PoolManager.GetServerClient();
var componentFactory = pair.Server.ResolveDependency<IComponentFactory>();
await pair.Server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var (proto, comp) in pair.GetPrototypesWithComponent<MobPriceComponent>())
{
Assert.That(proto.TryGetComponent<MobStateComponent>(out _, componentFactory), $"Found MobPriceComponent on {proto.ID}, but no MobStateComponent!");
}
});
});
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,49 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.Construction.Components;
using Content.Shared.Temperature;
namespace Content.IntegrationTests.Tests.Construction.Interaction;
public sealed class EdgeClobbering : InteractionTest
{
[TestPrototypes]
private const string Prototypes = @"
- type: constructionGraph
id: ExampleGraph
start: A
graph:
- node: A
edges:
- to: B
steps:
- tool: Anchoring
doAfter: 1
- to: C
steps:
- tool: Screwing
doAfter: 1
- node: B
- node: C
- type: entity
id: ExampleEntity
components:
- type: Construction
graph: ExampleGraph
node: A
";
[Test]
public async Task EnsureNoEdgeClobbering()
{
await SpawnTarget("ExampleEntity");
var sTarget = SEntMan.GetEntity(Target!.Value);
await InteractUsing(Screw, false);
SEntMan.EventBus.RaiseLocalEvent(sTarget, new OnTemperatureChangeEvent(0f, 0f, 0f));
await AwaitDoAfters();
Assert.That(SEntMan.GetComponent<ConstructionComponent>(sTarget).Node, Is.EqualTo("C"));
}
}

View File

@@ -0,0 +1,54 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Disposal;
public sealed class DisposalUnitInteractionTest : InteractionTest
{
private static readonly EntProtoId DisposalUnit = "DisposalUnit";
private static readonly EntProtoId TrashItem = "BrokenBottle";
private const string TestDisposalUnitId = "TestDisposalUnit";
[TestPrototypes]
private static readonly string TestPrototypes = $@"
# A modified disposal unit with a 100% chance of a thrown item being inserted
- type: entity
parent: {DisposalUnit.Id}
id: {TestDisposalUnitId}
components:
- type: ThrowInsertContainer
probability: 1
";
/// <summary>
/// Spawns a disposal unit, gives the player a trash item, and makes the
/// player throw the item at the disposal unit.
/// After a short delay, verifies that the thrown item is contained inside
/// the disposal unit.
/// </summary>
[Test]
public async Task ThrowItemIntoDisposalUnitTest()
{
var containerSys = Server.System<SharedContainerSystem>();
// Spawn the target disposal unit
var disposalUnit = await SpawnTarget(TestDisposalUnitId);
// Give the player some trash to throw
var trash = await PlaceInHands(TrashItem);
// Throw the item at the disposal unit
await ThrowItem();
// Wait a moment
await RunTicks(10);
// Make sure the trash is in the disposal unit
var throwInsertComp = Comp<ThrowInsertContainerComponent>();
var container = containerSys.GetContainer(ToServer(disposalUnit), throwInsertComp.ContainerId);
Assert.That(container.ContainedEntities, Contains.Item(ToServer(trash)));
}
}

View File

@@ -21,6 +21,7 @@ namespace Content.IntegrationTests.Tests.Doors
components:
- type: Physics
bodyType: Dynamic
- type: GravityAffected
- type: Fixtures
fixtures:
fix1:

View File

@@ -0,0 +1,20 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Engineering.Systems;
namespace Content.IntegrationTests.Tests.Engineering;
[TestFixture]
[TestOf(typeof(InflatableSafeDisassemblySystem))]
public sealed class InflatablesDeflateTest : InteractionTest
{
[Test]
public async Task Test()
{
await SpawnTarget(InflatableWall);
await InteractUsing(Needle);
AssertDeleted();
await AssertEntityLookup(new EntitySpecifier(InflatableWallStack.Id, 1));
}
}

View File

@@ -9,7 +9,6 @@ using Content.Server.Mind;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
@@ -20,6 +19,7 @@ using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.NukeOps;
using Content.Shared.Pinpointer;
using Content.Shared.Roles.Components;
using Content.Shared.Station.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;

View File

@@ -19,6 +19,7 @@ namespace Content.IntegrationTests.Tests.Gravity
- type: Alerts
- type: Physics
bodyType: Dynamic
- type: GravityAffected
- type: entity
name: WeightlessGravityGeneratorDummy

View File

@@ -76,8 +76,8 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.True);
Assert.That(!entityMan.GetComponent<GravityComponent>(grid1).EnabledVV);
Assert.That(entityMan.GetComponent<GravityComponent>(grid2).EnabledVV);
Assert.That(!entityMan.GetComponent<GravityComponent>(grid1).Enabled);
Assert.That(entityMan.GetComponent<GravityComponent>(grid2).Enabled);
});
// Re-enable needs power so it turns off again.
@@ -94,7 +94,7 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.False);
Assert.That(entityMan.GetComponent<GravityComponent>(grid2).EnabledVV, Is.False);
Assert.That(entityMan.GetComponent<GravityComponent>(grid2).Enabled, Is.False);
});
});

View File

@@ -1,3 +1,6 @@
using Content.Shared.Stacks;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Interaction;
// This partial class contains various constant prototype IDs common to interaction tests.
@@ -32,4 +35,9 @@ public abstract partial class InteractionTest
protected const string Manipulator1 = "MicroManipulatorStockPart";
protected const string Battery1 = "PowerCellSmall";
protected const string Battery4 = "PowerCellHyper";
// Inflatables & Needle used to pop them
protected static readonly EntProtoId InflatableWall = "InflatableWall";
protected static readonly EntProtoId Needle = "WeaponMeleeNeedle";
protected static readonly ProtoId<StackPrototype> InflatableWallStack = "InflatableWall";
}

View File

@@ -264,9 +264,10 @@ public abstract partial class InteractionTest
/// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
/// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
/// <param name="altInteract">If true, perform an alternate interaction instead of a standard one.
protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true, bool altInteract = false)
{
await InteractUsing((id, quantity), awaitDoAfters);
await InteractUsing((id, quantity), awaitDoAfters, altInteract);
}
/// <summary>
@@ -274,7 +275,8 @@ public abstract partial class InteractionTest
/// </summary>
/// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true)
/// <param name="altInteract">If true, perform an alternate interaction instead of a standard one.
protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true, bool altInteract = false)
{
// For every interaction, we will also examine the entity, just in case this breaks something, somehow.
// (e.g., servers attempt to assemble construction examine hints).
@@ -284,18 +286,19 @@ public abstract partial class InteractionTest
}
await PlaceInHands(entity);
await Interact(awaitDoAfters);
await Interact(awaitDoAfters, altInteract);
}
/// <summary>
/// Interact with an entity using the currently held entity.
/// </summary>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task Interact(bool awaitDoAfters = true)
/// <param name="altInteract">If true, performs an alternate interaction instead of a standard one.
protected async Task Interact(bool awaitDoAfters = true, bool altInteract = false)
{
if (Target == null || !Target.Value.IsClientSide())
{
await Interact(Target, TargetCoords, awaitDoAfters);
await Interact(Target, TargetCoords, awaitDoAfters, altInteract);
return;
}
@@ -311,23 +314,23 @@ public abstract partial class InteractionTest
await CheckTargetChange();
}
/// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool)"/>
protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
/// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool,bool)"/>
protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
{
Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
var coords = SEntMan.GetCoordinates(coordinates);
Assert.That(coords.IsValid(SEntMan));
await Interact(sTarget, coords, awaitDoAfters);
await Interact(sTarget, coords, awaitDoAfters, altInteract);
}
/// <summary>
/// Interact with an entity using the currently held entity.
/// </summary>
protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
{
Assert.That(SEntMan.TryGetEntity(Player, out var player));
await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target, altInteract: altInteract));
await RunTicks(1);
if (awaitDoAfters)

View File

@@ -144,6 +144,7 @@ public abstract partial class InteractionTest
- type: Stripping
- type: Puller
- type: Physics
- type: GravityAffected
- type: Tag
tags:
- CanPilot

View File

@@ -3,7 +3,6 @@ using System.Linq;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
using Content.Server.Roles;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
@@ -11,7 +10,7 @@ using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Players;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.Components;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;

View File

@@ -1,7 +1,5 @@
using System.Linq;
using Content.Server.Roles;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Reflection;

View File

@@ -0,0 +1,99 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Storage.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Nutrition;
public sealed class WaterCoolerInteractionTest : InteractionTest
{
/// <summary>
/// ProtoId of the water cooler entity.
/// </summary>
private static readonly EntProtoId WaterCooler = "WaterCooler";
/// <summary>
/// ProtoId of the paper cup entity dispensed by the water cooler.
/// </summary>
private static readonly EntProtoId PaperCup = "DrinkWaterCup";
/// <summary>
/// ProtoId of the water reagent that is stored in the water cooler.
/// </summary>
private static readonly ProtoId<ReagentPrototype> Water = "Water";
/// <summary>
/// Spawns a water cooler and tests that the player can retrieve a paper cup
/// by interacting with it, and can return the paper cup by alt-interacting with it.
/// </summary>
[Test]
public async Task GetAndReturnCup()
{
// Spawn the water cooler
var cooler = await SpawnTarget(WaterCooler);
// Record how many paper cups are in the cooler
var binComp = Comp<BinComponent>(cooler);
var initialCount = binComp.Items.Count;
Assert.That(binComp.Items, Is.Not.Empty, "Water cooler didn't start with any cups");
// Interact with the water cooler using an empty hand to grab a paper cup
await Interact();
var cup = HandSys.GetActiveItem((SPlayer, Hands));
Assert.Multiple(() =>
{
// Make sure the player is now holding a cup
Assert.That(cup, Is.Not.Null, "Player's hand is empty");
AssertPrototype(PaperCup, SEntMan.GetNetEntity(cup));
// Make sure the number of cups in the cooler has decreased by one
Assert.That(binComp.Items, Has.Count.EqualTo(initialCount - 1), "Number of cups in cooler bin did not decrease by one");
// Make sure the cup isn't somehow still in the cooler too
Assert.That(binComp.Items, Does.Not.Contain(cup));
});
// Alt-interact with the water cooler while holding the cup to put it back
await Interact(altInteract: true);
Assert.Multiple(() =>
{
// Make sure the player's hand is empty
Assert.That(HandSys.ActiveHandIsEmpty((SPlayer, Hands)), "Player's hand is not empty");
// Make sure the count has gone back up by one
Assert.That(binComp.Items, Has.Count.EqualTo(initialCount), "Number of cups in cooler bin did not return to initial count");
// Make sure the cup is in the cooler
Assert.That(binComp.Items, Contains.Item(cup), "Cup was not returned to cooler");
});
}
/// <summary>
/// Spawns a water cooler and gives the player an empty paper cup.
/// Tests that the player can put water into the cup by interacting
/// with the water cooler while holding the cup.
/// </summary>
[Test]
public async Task FillCup()
{
var solutionSys = Server.System<SharedSolutionContainerSystem>();
// Spawn the water cooler
await SpawnTarget(WaterCooler);
// Give the player a cup
var cup = await PlaceInHands(PaperCup);
// Make the player interact with the water cooler using the held cup
await Interact();
// Make sure the cup now contains water
Assert.That(solutionSys.GetTotalPrototypeQuantity(ToServer(cup), Water), Is.GreaterThan(FixedPoint2.Zero),
"Cup does not contain any water");
}
}

View File

@@ -78,7 +78,11 @@ public static class ClientPackaging
new[] { "Content.Client", "Content.Shared", "Content.Shared.Database" },
cancel: cancel);
await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel);
await RobustClientPackaging.WriteClientResources(
contentDir,
inputPass,
SharedPackaging.AdditionalIgnoredResources,
cancel);
inputPass.InjectFinished();
}

View File

@@ -25,6 +25,12 @@ public static class ServerPackaging
new PlatformReg("freebsd-x64", "FreeBSD", false),
};
private static IReadOnlySet<string> ServerContentIgnoresResources { get; } = new HashSet<string>
{
"ServerInfo",
"Changelog",
};
private static List<string> PlatformRids => Platforms
.Select(o => o.Rid)
.ToList();
@@ -211,7 +217,11 @@ public static class ServerPackaging
contentAssemblies,
cancel: cancel);
await RobustServerPackaging.WriteServerResources(contentDir, inputPassResources, cancel);
await RobustServerPackaging.WriteServerResources(
contentDir,
inputPassResources,
ServerContentIgnoresResources.Concat(SharedPackaging.AdditionalIgnoredResources).ToHashSet(),
cancel);
if (hybridAcz)
{

View File

@@ -0,0 +1,10 @@
namespace Content.Packaging;
public sealed class SharedPackaging
{
public static readonly IReadOnlySet<string> AdditionalIgnoredResources = new HashSet<string>
{
// MapRenderer outputs into Resources. Avoid these getting included in packaging.
"MapImages",
};
}

View File

@@ -13,6 +13,7 @@ using Content.Server.Clothing.Systems;
using Content.Server.Implants;
using Content.Shared.Implants;
using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.PDA;
namespace Content.Server.Access.Systems
@@ -25,6 +26,7 @@ namespace Content.Server.Access.Systems
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChameleonClothingSystem _chameleon = default!;
[Dependency] private readonly ChameleonControllerSystem _chamController = default!;
[Dependency] private readonly LockSystem _lock = default!;
public override void Initialize()
{
@@ -79,7 +81,8 @@ namespace Content.Server.Access.Systems
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || !TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
if (args.Target == null || !args.CanReach || _lock.IsLocked(uid) ||
!TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
return;
if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid))

View File

@@ -1,4 +1,4 @@
using Content.Server.Storage.Components;
using Content.Shared.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;

View File

@@ -6,17 +6,13 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Fun)]
public sealed class AddPolymorphActionCommand : IConsoleCommand
public sealed class AddPolymorphActionCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly PolymorphSystem _polySystem = default!;
public string Command => "addpolymorphaction";
public override string Command => "addpolymorphaction";
public string Description => Loc.GetString("add-polymorph-action-command-description");
public string Help => Loc.GetString("add-polymorph-action-command-help-text");
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
@@ -24,15 +20,13 @@ public sealed class AddPolymorphActionCommand : IConsoleCommand
return;
}
if (!NetEntity.TryParse(args[0], out var entityUidNet) || !_entityManager.TryGetEntity(entityUidNet, out var entityUid))
if (!NetEntity.TryParse(args[0], out var entityUidNet) || !EntityManager.TryGetEntity(entityUidNet, out var entityUid))
{
shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
shell.WriteError(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
return;
}
var polySystem = _entityManager.EntitySysManager.GetEntitySystem<PolymorphSystem>();
var polymorphable = _entityManager.EnsureComponent<PolymorphableComponent>(entityUid.Value);
polySystem.CreatePolymorphAction(args[1], (entityUid.Value, polymorphable));
var polymorphable = EntityManager.EnsureComponent<PolymorphableComponent>(entityUid.Value);
_polySystem.CreatePolymorphAction(args[1], (entityUid.Value, polymorphable));
}
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Storage.Components;
using Content.Shared.Administration;
using Content.Shared.Storage.Components;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;

View File

@@ -1,4 +1,4 @@
using Content.Server.Storage.Components;
using Content.Shared.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;

View File

@@ -2,9 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Administration.Logs.Converters;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Collections;
namespace Content.Server.Administration.Logs;
@@ -22,55 +20,25 @@ public sealed partial class AdminLogManager
PropertyNamingPolicy = NamingPolicy
};
var interfaces = new ValueList<IAdminLogConverter>();
foreach (var converter in _reflection.FindTypesWithAttribute<AdminLogConverterAttribute>())
{
var instance = _typeFactory.CreateInstance<JsonConverter>(converter);
(instance as IAdminLogConverter)?.Init(_dependencies);
if (instance is IAdminLogConverter converterInterface)
{
interfaces.Add(converterInterface);
converterInterface.Init(_dependencies);
}
_jsonOptions.Converters.Add(instance);
}
foreach (var @interface in interfaces)
{
@interface.Init2(_jsonOptions);
}
var converterNames = _jsonOptions.Converters.Select(converter => converter.GetType().Name);
_sawmill.Debug($"Admin log converters found: {string.Join(" ", converterNames)}");
}
private (JsonDocument Json, HashSet<Guid> Players) ToJson(
Dictionary<string, object?> properties)
{
var players = new HashSet<Guid>();
var parsed = new Dictionary<string, object?>();
foreach (var key in properties.Keys)
{
var value = properties[key];
value = value switch
{
ICommonSession player => new SerializablePlayer(player),
EntityCoordinates entityCoordinates => new SerializableEntityCoordinates(_entityManager, entityCoordinates),
_ => value
};
var parsedKey = NamingPolicy.ConvertName(key);
parsed.Add(parsedKey, value);
var entityId = properties[key] switch
{
EntityUid id => id,
EntityStringRepresentation rep => rep.Uid,
ICommonSession {AttachedEntity: {Valid: true}} session => session.AttachedEntity,
IComponent component => component.Owner,
_ => null
};
if (_entityManager.TryGetComponent(entityId, out ActorComponent? actor))
{
players.Add(actor.PlayerSession.UserId.UserId);
}
else if (value is SerializablePlayer player)
{
players.Add(player.Player.UserId.UserId);
}
}
return (JsonSerializer.SerializeToDocument(parsed, _jsonOptions), players);
}
}

View File

@@ -25,7 +25,6 @@ namespace Content.Server.Administration.Logs;
public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogManager
{
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -72,7 +71,6 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
// CVars
private bool _metricsEnabled;
private bool _enabled;
private TimeSpan _queueSendDelay;
private int _queueMax;
private int _preRoundQueueMax;
@@ -103,7 +101,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
_configuration.OnValueChanged(CVars.MetricsEnabled,
value => _metricsEnabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsEnabled,
value => _enabled = value, true);
value => Enabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueSendDelay,
value => _queueSendDelay = TimeSpan.FromSeconds(value), true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueMax,
@@ -123,6 +121,12 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
}
}
public override string ConvertName(string name)
{
// JsonNamingPolicy is not whitelisted by the sandbox.
return NamingPolicy.ConvertName(name);
}
public async Task Shutdown()
{
if (!_logQueue.IsEmpty)
@@ -292,8 +296,17 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
}
}
private void Add(LogType type, LogImpact impact, string message, JsonDocument json, HashSet<Guid> players)
public override void Add(LogType type, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] ref LogStringHandler handler)
{
Add(type, LogImpact.Medium, ref handler);
}
public override void Add(LogType type, LogImpact impact, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] ref LogStringHandler handler)
{
var message = handler.ToStringAndClear();
if (!Enabled)
return;
var preRound = _runLevel == GameRunLevel.PreRoundLobby;
var count = preRound ? _preRoundLogQueue.Count : _logQueue.Count;
if (count >= _dropThreshold)
@@ -302,6 +315,10 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
return;
}
var json = JsonSerializer.SerializeToDocument(handler.Values, _jsonOptions);
var id = NextLogId;
var players = GetPlayers(handler.Values, id);
// PostgreSQL does not support storing null chars in text values.
if (message.Contains('\0'))
{
@@ -311,31 +328,85 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
var log = new AdminLog
{
Id = NextLogId,
Id = id,
RoundId = _currentRoundId,
Type = type,
Impact = impact,
Date = DateTime.UtcNow,
Message = message,
Json = json,
Players = new List<AdminLogPlayer>(players.Count)
Players = players,
};
DoAdminAlerts(players, message, impact);
if (preRound)
{
_preRoundLogQueue.Enqueue(log);
}
else
{
_logQueue.Enqueue(log);
CacheLog(log);
}
}
private List<AdminLogPlayer> GetPlayers(Dictionary<string, object?> values, int logId)
{
List<AdminLogPlayer> players = new();
foreach (var value in values.Values)
{
switch (value)
{
case SerializablePlayer player:
AddPlayer(players, player.UserId, logId);
continue;
case EntityStringRepresentation rep:
if (rep.Session is {} session)
AddPlayer(players, session.UserId.UserId, logId);
continue;
case IAdminLogsPlayerValue playerValue:
foreach (var player in playerValue.Players)
{
AddPlayer(players, player, logId);
}
break;
}
}
return players;
}
private void AddPlayer(List<AdminLogPlayer> players, Guid user, int logId)
{
// The majority of logs have a single player, or maybe two. Instead of allocating a List<AdminLogPlayer> and
// HashSet<Guid>, we just iterate over the list to check for duplicates.
foreach (var player in players)
{
if (player.PlayerUserId == user)
return;
}
players.Add(new AdminLogPlayer
{
LogId = logId,
PlayerUserId = user
});
}
private void DoAdminAlerts(List<AdminLogPlayer> players, string message, LogImpact impact)
{
var adminLog = false;
var adminSys = _entityManager.SystemOrNull<AdminSystem>();
var logMessage = message;
foreach (var id in players)
foreach (var player in players)
{
var player = new AdminLogPlayer
{
LogId = log.Id,
PlayerUserId = id
};
var id = player.PlayerUserId;
log.Players.Add(player);
if (adminSys != null)
if (EntityManager.TrySystem(out AdminSystem? adminSys))
{
var cachedInfo = adminSys.GetCachedPlayerInfo(new NetUserId(id));
if (cachedInfo != null && cachedInfo.Antag)
@@ -372,35 +443,6 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
if (adminLog)
_chat.SendAdminAlert(logMessage);
if (preRound)
{
_preRoundLogQueue.Enqueue(log);
}
else
{
_logQueue.Enqueue(log);
CacheLog(log);
}
}
public override void Add(LogType type, LogImpact impact, ref LogStringHandler handler)
{
if (!_enabled)
{
handler.ToStringAndClear();
return;
}
var (json, players) = ToJson(handler.Values);
var message = handler.ToStringAndClear();
Add(type, impact, message, json, players);
}
public override void Add(LogType type, ref LogStringHandler handler)
{
Add(type, LogImpact.Medium, ref handler);
}
public async Task<List<SharedAdminLog>> All(LogFilter? filter = null, Func<List<SharedAdminLog>>? listProvider = null)

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