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:
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/publish-testing.yml
vendored
2
.github/workflows/publish-testing.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/test-packaging.yml
vendored
2
.github/workflows/test-packaging.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
174
Content.Benchmarks/DeltaPressureBenchmark.cs
Normal file
174
Content.Benchmarks/DeltaPressureBenchmark.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Administration;
|
||||
|
||||
namespace Content.Client.Administration.Systems;
|
||||
|
||||
public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
|
||||
{
|
||||
}
|
||||
@@ -57,12 +57,43 @@ public sealed partial class ObjectsTab : Control
|
||||
|
||||
private void TeleportTo(NetEntity 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()
|
||||
@@ -82,9 +113,7 @@ public sealed partial class ObjectsTab : Control
|
||||
{
|
||||
var query = _entityManager.AllEntityQueryEnumerator<MapGridComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var metadata))
|
||||
{
|
||||
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -92,9 +121,7 @@ public sealed partial class ObjectsTab : Control
|
||||
{
|
||||
var query = _entityManager.AllEntityQueryEnumerator<MapComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var metadata))
|
||||
{
|
||||
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
40
Content.Client/Anomaly/AnomalyScannerScreenComponent.cs
Normal file
40
Content.Client/Anomaly/AnomalyScannerScreenComponent.cs
Normal 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);
|
||||
}
|
||||
110
Content.Client/Anomaly/AnomalyScannerSystem.cs
Normal file
110
Content.Client/Anomaly/AnomalyScannerSystem.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
210
Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs
Normal file
210
Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 power = SharedDrunkSystem.MagicNumber;
|
||||
|
||||
if (time != null)
|
||||
{
|
||||
var curTime = _timing.CurTime;
|
||||
var timeLeft = (float) (time.Value.Item2 - curTime).TotalSeconds;
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private void OnDrunkShutdown(EntityUid uid, DrunkComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (_player.LocalEntity == uid)
|
||||
private void OnPlayerDetached(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRelayedEvent<LocalPlayerDetachedEvent> args)
|
||||
{
|
||||
_overlay.CurrentBoozePower = 0;
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
5
Content.Client/Implants/SubdermalImplantSystem.cs
Normal file
5
Content.Client/Implants/SubdermalImplantSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Implants;
|
||||
|
||||
namespace Content.Client.Implants;
|
||||
|
||||
public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem;
|
||||
@@ -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!;
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
using Content.Shared.Kitchen;
|
||||
|
||||
namespace Content.Client.Kitchen;
|
||||
|
||||
public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,14 +129,20 @@ 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));
|
||||
if (idx >= oldChildCount)
|
||||
{
|
||||
var control = new RecipeControl(_lathe, prototype, tooltipFunction, canProduce, GetRecipeDisplayControl(prototype));
|
||||
control.OnButtonPressed += s =>
|
||||
{
|
||||
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
|
||||
@@ -131,6 +151,29 @@ public sealed partial class LatheMenu : DefaultWindow
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateTooltipText(LatheRecipePrototype prototype)
|
||||
@@ -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);
|
||||
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)
|
||||
|
||||
35
Content.Client/Lathe/UI/QueuedRecipeControl.xaml
Normal file
35
Content.Client/Lathe/UI/QueuedRecipeControl.xaml
Normal 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>
|
||||
56
Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
Normal file
56
Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
Content.Client/Light/EntitySystems/PoweredLightSystem.cs
Normal file
5
Content.Client/Light/EntitySystems/PoweredLightSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
|
||||
namespace Content.Client.Light.EntitySystems;
|
||||
|
||||
public sealed class PoweredLightSystem : SharedPoweredLightSystem;
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
5
Content.Client/Medical/SuitSensors/SuitSensorSystem.cs
Normal file
5
Content.Client/Medical/SuitSensors/SuitSensorSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Medical.SuitSensors;
|
||||
|
||||
namespace Content.Client.Medical.SuitSensors;
|
||||
|
||||
public sealed class SuitSensorSystem : SharedSuitSensorSystem;
|
||||
5
Content.Client/Morgue/CrematoriumSystem.cs
Normal file
5
Content.Client/Morgue/CrematoriumSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Morgue;
|
||||
|
||||
namespace Content.Client.Morgue;
|
||||
|
||||
public sealed class CrematoriumSystem : SharedCrematoriumSystem;
|
||||
5
Content.Client/Morgue/MorgueSystem.cs
Normal file
5
Content.Client/Morgue/MorgueSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Morgue;
|
||||
|
||||
namespace Content.Client.Morgue;
|
||||
|
||||
public sealed class MorgueSystem : SharedMorgueSystem;
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ namespace Content.Client.Power.Components;
|
||||
[RegisterComponent]
|
||||
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
|
||||
{
|
||||
public override float Load { get; set; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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[]
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
ItemNameLabel.Visible = true;
|
||||
NoItemLabel.Visible = false;
|
||||
|
||||
_entity = entity.Value;
|
||||
BuildNewEntityStatus();
|
||||
|
||||
UpdateItemName();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
417
Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
Normal file
417
Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
85
Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
Normal file
85
Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ namespace Content.IntegrationTests.Tests.Doors
|
||||
components:
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
- type: GravityAffected
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
fix1:
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Content.IntegrationTests.Tests.Gravity
|
||||
- type: Alerts
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
- type: GravityAffected
|
||||
|
||||
- type: entity
|
||||
name: WeightlessGravityGeneratorDummy
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -144,6 +144,7 @@ public abstract partial class InteractionTest
|
||||
- type: Stripping
|
||||
- type: Puller
|
||||
- type: Physics
|
||||
- type: GravityAffected
|
||||
- type: Tag
|
||||
tags:
|
||||
- CanPilot
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
10
Content.Packaging/SharedPackaging.cs
Normal file
10
Content.Packaging/SharedPackaging.cs
Normal 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",
|
||||
};
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user