mirror of
https://github.com/space-wizards/space-station-14.git
synced 2026-02-14 19:29:53 +01:00
Optical thermal scanner implementation (#42613)
* revert of the revert * tests * changes * more fun * test * ccvvvar * works but bad * now its better * more fixes * more cleanup * cleaning * last fixes before move to glasses activ * x * glasses only * working * fix toolbox * cleanup * ThermalByte added * small fix * small optimalisations * float bux fix * comments add * more comments * more comments * last fix * revert cvar delete * wrong blue shades * cvar refactor * Update Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Update Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * tweak to TryGetTemperature comment * Factors are now const * renames * Interface for ThermalByte * tile color vaccum and more comments * saving yeeted * integration test * rename and cleanup * fix * cleanup * switch * UT fix (hopefully) * small bug+ rename * vaccum limit + space is now invalid * typo * typo * fix * cleanup --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
using Content.Client.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Atmos.Overlays;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a thermal heatmap overlay for gas tiles, used for equipment like thermal glasses.
|
||||
/// /// </summary>
|
||||
public sealed class GasTileDangerousTemperatureOverlay : Overlay
|
||||
{
|
||||
public override bool RequestScreenTexture { get; set; } = false;
|
||||
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
private GasTileOverlaySystem? _gasTileOverlay;
|
||||
private readonly SharedTransformSystem _xformSys;
|
||||
private EntityQuery<GasTileOverlayComponent> _overlayQuery;
|
||||
|
||||
private IRenderTexture? _temperatureTarget;
|
||||
|
||||
// Cache used to transform ThermalByte into Color for overlay
|
||||
private readonly Color[] _colorCache = new Color[256];
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
|
||||
public GasTileDangerousTemperatureOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_xformSys = _entManager.System<SharedTransformSystem>();
|
||||
|
||||
_overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
|
||||
|
||||
for (byte i = 0; i <= ThermalByte.TempResolution; i++)
|
||||
{
|
||||
_colorCache[i] = PreCalculateColor(i);
|
||||
}
|
||||
|
||||
_colorCache[ThermalByte.StateVacuum] = Color.Teal;
|
||||
_colorCache[ThermalByte.StateVacuum].A = 0.6f;
|
||||
_colorCache[ThermalByte.AtmosImpossible] = Color.Transparent;
|
||||
|
||||
#if DEBUG // This shouldn't happend so tell me if you see this LimeGreen on the screen
|
||||
_colorCache[ThermalByte.ReservedFuture0] = Color.LimeGreen;
|
||||
_colorCache[ThermalByte.ReservedFuture1] = Color.LimeGreen;
|
||||
_colorCache[ThermalByte.ReservedFuture2] = Color.LimeGreen;
|
||||
#else
|
||||
_colorCache[ThermalByte.ReservedFuture0] = Color.Transparent;
|
||||
_colorCache[ThermalByte.ReservedFuture1] = Color.Transparent;
|
||||
_colorCache[ThermalByte.ReservedFuture2] = Color.Transparent;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used for Calculating onscreen color from ThermalByte core value
|
||||
/// /// </summary>
|
||||
private static Color PreCalculateColor(byte byteTemp)
|
||||
{
|
||||
// Color Thresholds in Kelvin
|
||||
// -150 C
|
||||
const float deepFreezeK = 123.15f;
|
||||
// -50 C
|
||||
const float freezeStartK = 223.15f;
|
||||
// 0 C
|
||||
const float waterFreezeK = 273.15f;
|
||||
// 50 C
|
||||
const float heatStartK = 323.15f;
|
||||
// 100 C
|
||||
const float waterBoilK = 373.15f;
|
||||
// 300 C
|
||||
const float superHeatK = 573.15f;
|
||||
|
||||
var tempK = byteTemp * ThermalByte.TempDegreeResolution;
|
||||
|
||||
// Neutral Zone Check (0C to 50C)
|
||||
// If between 273.15K and 323.15K, it's transparent.
|
||||
if (tempK >= waterFreezeK && tempK < heatStartK)
|
||||
{
|
||||
return Color.Transparent;
|
||||
}
|
||||
|
||||
Color resultingColor;
|
||||
|
||||
switch (tempK)
|
||||
{
|
||||
case < deepFreezeK:
|
||||
resultingColor = Color.FromHex("#330066");
|
||||
resultingColor.A = 0.7f;
|
||||
break;
|
||||
case < freezeStartK:
|
||||
// Interpolate Deep Purple -> Blue
|
||||
// Range: 123.15 to 223.15 (Span: 100)
|
||||
resultingColor = Color.InterpolateBetween(
|
||||
Color.FromHex("#330066"),
|
||||
Color.Blue,
|
||||
(tempK - deepFreezeK) * 0.01f);
|
||||
resultingColor.A = 0.6f;
|
||||
break;
|
||||
case < waterFreezeK:
|
||||
// Interpolate Blue -> Transparent
|
||||
// Range: 223.15 to 273.15 (Span: 50)
|
||||
|
||||
resultingColor = Color.InterpolateBetween(
|
||||
new Color(Color.Blue.R, Color.Blue.G, Color.Blue.B, 0.6f),
|
||||
new Color(Color.Blue.R, Color.Blue.G, Color.Blue.B, 0.2f),
|
||||
(tempK - freezeStartK) * 0.02f);
|
||||
break;
|
||||
case < waterBoilK:
|
||||
// Interpolate Transparent -> Yellow
|
||||
// Range: 323.15 to 373.15 (Span: 50)
|
||||
|
||||
resultingColor = Color.InterpolateBetween(
|
||||
new Color(Color.Yellow.R, Color.Yellow.G, Color.Yellow.B, 0.2f),
|
||||
new Color(Color.Yellow.R, Color.Yellow.G, Color.Yellow.B, 0.6f),
|
||||
(tempK - heatStartK) * 0.02f);
|
||||
break;
|
||||
case < superHeatK:
|
||||
// Interpolate Yellow -> Red
|
||||
// Range: 373.15 to 573.15 (Span: 200)
|
||||
resultingColor = Color.InterpolateBetween(
|
||||
Color.Yellow,
|
||||
Color.Red,
|
||||
(tempK - waterBoilK) * 0.005f);
|
||||
resultingColor.A = 0.6f;
|
||||
break;
|
||||
default:
|
||||
resultingColor = Color.DarkRed;
|
||||
resultingColor.A = 0.7f;
|
||||
break;
|
||||
}
|
||||
|
||||
return resultingColor;
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (args.MapId == MapId.Nullspace)
|
||||
return false;
|
||||
|
||||
_gasTileOverlay ??= _entManager.System<GasTileOverlaySystem>();
|
||||
if (_gasTileOverlay == null)
|
||||
return false;
|
||||
|
||||
var target = args.Viewport.RenderTarget;
|
||||
|
||||
if (_temperatureTarget?.Texture.Size != target.Size)
|
||||
{
|
||||
_temperatureTarget?.Dispose();
|
||||
_temperatureTarget = _clyde.CreateRenderTarget(
|
||||
target.Size,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||
name: nameof(GasTileDangerousTemperatureOverlay));
|
||||
}
|
||||
|
||||
var drawHandle = args.WorldHandle;
|
||||
var worldBounds = args.WorldBounds;
|
||||
var worldAABB = args.WorldAABB;
|
||||
var mapId = args.MapId;
|
||||
var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
|
||||
|
||||
var anyGasDrawn = false;
|
||||
List<Entity<MapGridComponent>> grids = new();
|
||||
|
||||
drawHandle.RenderInRenderTarget(_temperatureTarget,
|
||||
() =>
|
||||
{
|
||||
grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref grids);
|
||||
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
if (!_overlayQuery.TryGetComponent(grid.Owner, out var comp))
|
||||
continue;
|
||||
|
||||
var gridTileSizeVec = grid.Comp.TileSizeVector;
|
||||
var gridTileCenterVec = grid.Comp.TileSizeHalfVector;
|
||||
var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner);
|
||||
var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
|
||||
|
||||
drawHandle.SetTransform(gridEntToViewportLocal);
|
||||
|
||||
var worldToGridLocal = _xformSys.GetInvWorldMatrix(grid.Owner);
|
||||
var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
|
||||
|
||||
var localBounds = new Box2i(
|
||||
(int)MathF.Floor(floatBounds.Left),
|
||||
(int)MathF.Floor(floatBounds.Bottom),
|
||||
(int)MathF.Ceiling(floatBounds.Right),
|
||||
(int)MathF.Ceiling(floatBounds.Top));
|
||||
|
||||
foreach (var chunk in comp.Chunks.Values)
|
||||
{
|
||||
var enumerator = new GasChunkEnumerator(chunk);
|
||||
while (enumerator.MoveNext(out var tileGas))
|
||||
{
|
||||
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
|
||||
if (!localBounds.Contains(tilePosition))
|
||||
continue;
|
||||
|
||||
var gasColor = _colorCache[tileGas.ByteGasTemperature.Value];
|
||||
|
||||
if (gasColor.A <= 0f)
|
||||
continue;
|
||||
|
||||
anyGasDrawn = true;
|
||||
|
||||
drawHandle.DrawRect(
|
||||
Box2.CenteredAround(tilePosition + gridTileCenterVec, gridTileSizeVec),
|
||||
gasColor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new Color(0, 0, 0, 0));
|
||||
|
||||
drawHandle.SetTransform(Matrix3x2.Identity);
|
||||
|
||||
if (!anyGasDrawn)
|
||||
{
|
||||
_temperatureTarget?.Dispose();
|
||||
_temperatureTarget = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (_temperatureTarget is null)
|
||||
return;
|
||||
|
||||
args.WorldHandle.DrawTextureRect(_temperatureTarget.Texture, args.WorldBounds);
|
||||
args.WorldHandle.SetTransform(Matrix3x2.Identity);
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_temperatureTarget?.Dispose();
|
||||
_temperatureTarget = null;
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
}
|
||||
34
Content.Client/Overlays/ThermalSightOverlaySystem.cs
Normal file
34
Content.Client/Overlays/ThermalSightOverlaySystem.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Content.Client.Atmos.Overlays;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class ThermalSightOverlaySystem : EquipmentHudSystem<ThermalSightComponent>
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
|
||||
private GasTileDangerousTemperatureOverlay _temperatureOverlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_temperatureOverlay = new();
|
||||
}
|
||||
|
||||
protected override void UpdateInternal(RefreshEquipmentHudEvent<ThermalSightComponent> component)
|
||||
{
|
||||
base.UpdateInternal(component);
|
||||
|
||||
_overlayMan.AddOverlay(_temperatureOverlay);
|
||||
}
|
||||
|
||||
protected override void DeactivateInternal()
|
||||
{
|
||||
base.DeactivateInternal();
|
||||
|
||||
_overlayMan.RemoveOverlay(_temperatureOverlay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Atmos;
|
||||
|
||||
/// <summary>
|
||||
/// GasTileOverlay is being tested here
|
||||
/// </summary>
|
||||
public sealed class GasTileOverlayTemperatureNetworkingTest : AtmosTest
|
||||
{
|
||||
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
|
||||
|
||||
[Test]
|
||||
public async Task TestGasOverlayDataSync()
|
||||
{
|
||||
var sMapSys = Server.System<SharedMapSystem>();
|
||||
|
||||
var gridComp = ProcessEnt.Comp3;
|
||||
var gridNetEnt = Server.EntMan.GetNetEntity(ProcessEnt);
|
||||
|
||||
var gridCoords = new EntityCoordinates(ProcessEnt, Vector2.Zero);
|
||||
var tileIndices = sMapSys.TileIndicesFor(ProcessEnt, gridComp, gridCoords);
|
||||
var mixture = SAtmos.GetTileMixture(ProcessEnt, null, tileIndices, true);
|
||||
|
||||
// Get data for client side.
|
||||
var cGridEnt = CEntMan.GetEntity(gridNetEnt);
|
||||
Assert.That(CEntMan.TryGetComponent<GasTileOverlayComponent>(cGridEnt, out var cOverlay),
|
||||
"Client grid is missing GasTileOverlayComponent");
|
||||
|
||||
// Check if the server actually sent the gas chunks
|
||||
Assert.That(cOverlay, Is.Not.Null, "Gas overlay is null on the client.");
|
||||
Assert.That(cOverlay.Chunks, Is.Not.Empty, "Gas overlay chunks are empty on the client.");
|
||||
|
||||
//Start real tests
|
||||
await InjectHotPlasma(ProcessEnt, tileIndices, mixture, 400f);
|
||||
|
||||
await CheckForInjectedGas(cOverlay, tileIndices, 400f);
|
||||
|
||||
await InjectHotPlasma(ProcessEnt, tileIndices, mixture, 800f + ThermalByte.TempDegreeResolution - 1); // Rounding test
|
||||
|
||||
await CheckForInjectedGas(cOverlay, tileIndices, 800f);
|
||||
|
||||
await InjectHotPlasma(ProcessEnt, tileIndices, mixture, ThermalByte.TempMaximum + 200f); // This one hits max temperature
|
||||
|
||||
await CheckForInjectedGas(cOverlay, tileIndices, ThermalByte.TempMaximum);
|
||||
|
||||
await InjectHotPlasma(ProcessEnt, tileIndices, mixture, ThermalByte.TempMinimum);
|
||||
await InjectHotPlasma(ProcessEnt, tileIndices, mixture, ThermalByte.TempMinimum + (ThermalByte.TempDegreeResolution * 2) - 1); // Test the networking optimisation, this should not be networked yet
|
||||
|
||||
await CheckForInjectedGas(cOverlay, tileIndices, ThermalByte.TempMinimum);
|
||||
|
||||
await InjectHotPlasma(ProcessEnt, tileIndices, mixture, ThermalByte.TempMinimum + (ThermalByte.TempDegreeResolution * 2)); // This should
|
||||
|
||||
await CheckForInjectedGas(cOverlay, tileIndices, ThermalByte.TempMinimum + (ThermalByte.TempDegreeResolution * 2));
|
||||
}
|
||||
|
||||
private async Task CheckForInjectedGas(GasTileOverlayComponent overlay, Vector2i indices, float expectedTemp)
|
||||
{
|
||||
await Client.WaitPost(() =>
|
||||
{
|
||||
var chunkIndices = SharedGasTileOverlaySystem.GetGasChunkIndices(indices);
|
||||
|
||||
Assert.That(overlay.Chunks.TryGetValue(chunkIndices, out var chunk), "Chunk not found");
|
||||
Assert.That(chunk, Is.Not.Null, "Chunk not found");
|
||||
|
||||
// Calculate the exact index in the TileData array
|
||||
var localX = MathHelper.Mod(indices.X, SharedGasTileOverlaySystem.ChunkSize);
|
||||
var localY = MathHelper.Mod(indices.Y, SharedGasTileOverlaySystem.ChunkSize);
|
||||
int tileIndex = localX + localY * SharedGasTileOverlaySystem.ChunkSize;
|
||||
|
||||
var tile = chunk.TileData[tileIndex];
|
||||
tile.ByteGasTemperature.TryGetTemperature(out var actualTemp);
|
||||
|
||||
Assert.That(actualTemp, Is.EqualTo(expectedTemp).Within(0.01f), $"Tile at {indices} had wrong temperature!");
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InjectHotPlasma(EntityUid gridEnt, Vector2i tileIndices, GasMixture mixture, float temperature)
|
||||
{
|
||||
//Server makes atmos
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
if (mixture != null)
|
||||
{
|
||||
mixture.Clear();
|
||||
mixture.AdjustMoles(Gas.Plasma, 100f); // Inject hot plasma
|
||||
mixture.Temperature = temperature;
|
||||
SAtmos.InvalidateVisuals(gridEnt, tileIndices);
|
||||
}
|
||||
});
|
||||
|
||||
await RunTicks(60);
|
||||
await Task.WhenAll(Client.WaitIdleAsync(), Server.WaitIdleAsync());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
@@ -13,7 +10,6 @@ using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
@@ -21,6 +17,7 @@ using Robust.Shared.Player;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// ReSharper disable once RedundantUsingDirective
|
||||
|
||||
@@ -32,7 +29,6 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
[Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _confMan = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IParallelManager _parMan = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly ChunkingSystem _chunkingSys = default!;
|
||||
@@ -85,9 +81,8 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
};
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
Subs.CVar(_confMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
|
||||
Subs.CVar(_confMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
|
||||
Subs.CVar(_confMan, CVars.NetPVS, OnPvsToggle, true);
|
||||
|
||||
InitializeCVars();
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
|
||||
@@ -175,7 +170,16 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
|
||||
public GasOverlayData GetOverlayData(GasMixture? mixture)
|
||||
{
|
||||
var data = new GasOverlayData(0, new byte[VisibleGasId.Length]);
|
||||
ThermalByte byteTemp;
|
||||
if (mixture == null)
|
||||
{
|
||||
byteTemp = new();
|
||||
byteTemp.SetVacuum();
|
||||
}
|
||||
else
|
||||
byteTemp = new(mixture.Temperature);
|
||||
|
||||
var data = new GasOverlayData(0, new byte[VisibleGasId.Length], byteTemp);
|
||||
|
||||
for (var i = 0; i < VisibleGasId.Length; i++)
|
||||
{
|
||||
@@ -215,15 +219,27 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
|
||||
ThermalByte newByteTemp = new();
|
||||
|
||||
if (tile.Hotspot.Valid)
|
||||
newByteTemp.SetTemperature(tile.Hotspot.Temperature);
|
||||
else if (!tile.Space && tile.Air?.TotalMoles <= 5f)
|
||||
newByteTemp.SetVacuum();
|
||||
else if (!tile.Space && tile.Air != null)
|
||||
newByteTemp = new(tile.Air.Temperature);
|
||||
|
||||
if (oldData.Equals(default))
|
||||
{
|
||||
changed = true;
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length]);
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], newByteTemp);
|
||||
}
|
||||
else if (oldData.FireState != tile.Hotspot.State)
|
||||
else if (oldData.FireState != tile.Hotspot.State ||
|
||||
Math.Abs(oldData.ByteGasTemperature.Value - newByteTemp.Value) > 1 || // Dirty Temperature when there is more then 1 byte difference. That should measure up to minimum 4 degreese difference, 6 degreese on average.
|
||||
(oldData.ByteGasTemperature.Value != newByteTemp.Value && newByteTemp.Value > ThermalByte.TempResolution)) // change of special ThermalByte value
|
||||
{
|
||||
changed = true;
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity);
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, newByteTemp);
|
||||
}
|
||||
|
||||
if (tile is {Air: not null, NoGridTile: false})
|
||||
@@ -465,5 +481,12 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void InitializeCVars()
|
||||
{
|
||||
Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
|
||||
Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
|
||||
Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +1,257 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Prototypes;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Atmos.EntitySystems
|
||||
{
|
||||
public abstract class SharedGasTileOverlaySystem : EntitySystem
|
||||
{
|
||||
public const byte ChunkSize = 8;
|
||||
protected float AccumulatedFrameTime;
|
||||
protected bool PvsEnabled;
|
||||
namespace Content.Shared.Atmos.EntitySystems;
|
||||
|
||||
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
|
||||
[Dependency] private readonly SharedAtmosphereSystem _atmosphere = default!;
|
||||
public abstract class SharedGasTileOverlaySystem : EntitySystem
|
||||
{
|
||||
public const byte ChunkSize = 8;
|
||||
protected float AccumulatedFrameTime;
|
||||
protected bool PvsEnabled;
|
||||
|
||||
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
|
||||
[Dependency] protected readonly IConfigurationManager ConfMan = default!;
|
||||
[Dependency] private readonly SharedAtmosphereSystem _atmosphere = default!;
|
||||
|
||||
/// <summary>
|
||||
/// array of the ids of all visible gases.
|
||||
/// </summary>
|
||||
public int[] VisibleGasId = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentGetState>(OnGetState);
|
||||
|
||||
List<int> visibleGases = new();
|
||||
|
||||
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
|
||||
{
|
||||
var gasPrototype = _atmosphere.GetGas(i);
|
||||
if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture) ||
|
||||
(!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState)))
|
||||
visibleGases.Add(i);
|
||||
}
|
||||
VisibleGasId = visibleGases.ToArray();
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, GasTileOverlayComponent component, ref ComponentGetState args)
|
||||
{
|
||||
if (PvsEnabled && !args.ReplayState)
|
||||
return;
|
||||
|
||||
// Should this be a full component state or a delta-state?
|
||||
if (args.FromTick <= component.CreationTick || args.FromTick <= component.ForceTick)
|
||||
{
|
||||
args.State = new GasTileOverlayState(component.Chunks);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new Dictionary<Vector2i, GasOverlayChunk>();
|
||||
foreach (var (index, chunk) in component.Chunks)
|
||||
{
|
||||
if (chunk.LastUpdate >= args.FromTick)
|
||||
data[index] = chunk;
|
||||
}
|
||||
|
||||
args.State = new GasTileOverlayDeltaState(data, new(component.Chunks.Keys));
|
||||
}
|
||||
|
||||
public static Vector2i GetGasChunkIndices(Vector2i indices)
|
||||
{
|
||||
return new Vector2i((int)MathF.Floor((float)indices.X / ChunkSize), (int)MathF.Floor((float)indices.Y / ChunkSize));
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public readonly struct GasOverlayData : IEquatable<GasOverlayData>
|
||||
{
|
||||
[ViewVariables] public readonly byte FireState;
|
||||
[ViewVariables] public readonly byte[] Opacity;
|
||||
// TODO change fire color based on ByteTemp
|
||||
|
||||
/// <summary>
|
||||
/// array of the ids of all visible gases.
|
||||
/// Network-synced air temperature, compressed to a single byte per tile for bandwidth optimization.
|
||||
/// Note: Values are approximate and may deviate even ~10°C from the precise server side only temperature.
|
||||
/// </summary>
|
||||
public int[] VisibleGasId = default!;
|
||||
[ViewVariables]
|
||||
public readonly ThermalByte ByteGasTemperature;
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
public GasOverlayData(byte fireState, byte[] opacity, ThermalByte byteTemp)
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentGetState>(OnGetState);
|
||||
|
||||
List<int> visibleGases = new();
|
||||
|
||||
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
|
||||
{
|
||||
var gasPrototype = _atmosphere.GetGas(i);
|
||||
if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture) || !string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState))
|
||||
visibleGases.Add(i);
|
||||
}
|
||||
|
||||
VisibleGasId = visibleGases.ToArray();
|
||||
FireState = fireState;
|
||||
Opacity = opacity;
|
||||
ByteGasTemperature = byteTemp;
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, GasTileOverlayComponent component, ref ComponentGetState args)
|
||||
public bool Equals(GasOverlayData other)
|
||||
{
|
||||
if (PvsEnabled && !args.ReplayState)
|
||||
return;
|
||||
if (FireState != other.FireState)
|
||||
return false;
|
||||
|
||||
// Should this be a full component state or a delta-state?
|
||||
if (args.FromTick <= component.CreationTick || args.FromTick <= component.ForceTick)
|
||||
if (Opacity?.Length != other.Opacity?.Length)
|
||||
return false;
|
||||
|
||||
if (Opacity != null && other.Opacity != null)
|
||||
{
|
||||
args.State = new GasTileOverlayState(component.Chunks);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new Dictionary<Vector2i, GasOverlayChunk>();
|
||||
foreach (var (index, chunk) in component.Chunks)
|
||||
{
|
||||
if (chunk.LastUpdate >= args.FromTick)
|
||||
data[index] = chunk;
|
||||
}
|
||||
|
||||
args.State = new GasTileOverlayDeltaState(data, new(component.Chunks.Keys));
|
||||
}
|
||||
|
||||
public static Vector2i GetGasChunkIndices(Vector2i indices)
|
||||
{
|
||||
return new((int) MathF.Floor((float) indices.X / ChunkSize), (int) MathF.Floor((float) indices.Y / ChunkSize));
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public readonly struct GasOverlayData : IEquatable<GasOverlayData>
|
||||
{
|
||||
[ViewVariables]
|
||||
public readonly byte FireState;
|
||||
|
||||
[ViewVariables]
|
||||
public readonly byte[] Opacity;
|
||||
|
||||
// TODO change fire color based on temps
|
||||
// But also: dont dirty on a 0.01 kelvin change in temperatures.
|
||||
// Either have a temp tolerance, or map temperature -> byte levels
|
||||
|
||||
public GasOverlayData(byte fireState, byte[] opacity)
|
||||
{
|
||||
FireState = fireState;
|
||||
Opacity = opacity;
|
||||
}
|
||||
|
||||
public bool Equals(GasOverlayData other)
|
||||
{
|
||||
if (FireState != other.FireState)
|
||||
return false;
|
||||
|
||||
if (Opacity?.Length != other.Opacity?.Length)
|
||||
return false;
|
||||
|
||||
if (Opacity != null && other.Opacity != null)
|
||||
for (var i = 0; i < Opacity.Length; i++)
|
||||
{
|
||||
for (var i = 0; i < Opacity.Length; i++)
|
||||
{
|
||||
if (Opacity[i] != other.Opacity[i])
|
||||
return false;
|
||||
}
|
||||
if (Opacity[i] != other.Opacity[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class GasOverlayUpdateEvent : EntityEventArgs
|
||||
{
|
||||
public Dictionary<NetEntity, List<GasOverlayChunk>> UpdatedChunks = new();
|
||||
public Dictionary<NetEntity, HashSet<Vector2i>> RemovedChunks = new();
|
||||
if (ByteGasTemperature != other.ByteGasTemperature)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class GasOverlayUpdateEvent : EntityEventArgs
|
||||
{
|
||||
public Dictionary<NetEntity, List<GasOverlayChunk>> UpdatedChunks = new();
|
||||
public Dictionary<NetEntity, HashSet<Vector2i>> RemovedChunks = new();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct for networking gas temperatures to all clients using a single struct(byte) per tile.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This struct compresses the gas temperature into a 1-byte value (0-255).
|
||||
/// It clamps the temperature to a maximum of 1000K and divides it by 4, creating a range of 0-250.
|
||||
/// This provides a resolution of 4 degrees Kelvin.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The remaining bytes are used as special flags:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>255</b>: Represents a Wall (block cannot hold atmosphere).</description></item>
|
||||
/// <item><description><b>254</b>: Represents a Vacuum.</description></item>
|
||||
/// <item><description><b>251-253</b>: Reserved for future use.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Dirtying Logic:</b> The value is only dirtied and networked if the difference between the
|
||||
/// networked byte and the real atmosphere byte is greater than 1. This prevents network spam
|
||||
/// from minor temperature fluctuations (e.g., heating from 1K to 8K will not trigger an update,
|
||||
/// but hitting 9K moves the byte index enough to sync).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Currently, the conversion is linear. Future improvements might involve a quadratic scale
|
||||
/// or pre-defined resolution points to offer higher precision at room temperatures
|
||||
/// and lower precision at extreme temperatures (1000K).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public struct ThermalByte : IEquatable<ThermalByte>
|
||||
{
|
||||
public const float TempMinimum = 0f;
|
||||
public const float TempMaximum = 1000f;
|
||||
public const int TempResolution = 250;
|
||||
|
||||
public const byte ReservedFuture0 = 251;
|
||||
public const byte ReservedFuture1 = 252;
|
||||
public const byte ReservedFuture2 = 253;
|
||||
public const byte StateVacuum = 254;
|
||||
public const byte AtmosImpossible = 255;
|
||||
|
||||
public const float TempDegreeResolution = (TempMaximum - TempMinimum) / TempResolution;
|
||||
public const float TempToByteFactor = TempResolution / (TempMaximum - TempMinimum);
|
||||
|
||||
private byte _coreValue;
|
||||
|
||||
public ThermalByte(float temperatureKelvin)
|
||||
{
|
||||
SetTemperature(temperatureKelvin);
|
||||
}
|
||||
|
||||
public ThermalByte()
|
||||
{
|
||||
_coreValue = AtmosImpossible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set temperature of air in this in Kelvin.
|
||||
/// </summary>
|
||||
public void SetTemperature(float temperatureKelvin)
|
||||
{
|
||||
var clampedTemp = Math.Clamp(temperatureKelvin, TempMinimum, TempMaximum);
|
||||
_coreValue = (byte)((clampedTemp - TempMinimum) * TempResolution / (TempMaximum - TempMinimum));
|
||||
}
|
||||
|
||||
public void SetAtmosIsImpossible()
|
||||
{
|
||||
_coreValue = AtmosImpossible;
|
||||
}
|
||||
|
||||
public void SetVacuum()
|
||||
{
|
||||
_coreValue = StateVacuum;
|
||||
}
|
||||
|
||||
public bool IsAtmosImpossible => _coreValue == AtmosImpossible; // Cold space, solid walls
|
||||
public bool IsVacuum => _coreValue == StateVacuum;
|
||||
public byte Value => _coreValue;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the air temperature in Kelvin.
|
||||
/// </summary>
|
||||
/// <param name="temperature">The temperature in Kelvin, if the tile has a valid temperature.</param>
|
||||
/// <param name="onVacuumReturnTcmb">
|
||||
/// If true and the tile is a vacuum, <paramref name="temperature"/> will be set to <see cref="Atmospherics.TCMB"/>
|
||||
/// and the method will return <see langword="true"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the tile contains a valid temperature (including vacuum if <paramref name="onVacuumReturnTcmb"/> is set);
|
||||
/// otherwise <see langword="false"/> (e.g., walls).
|
||||
/// </returns>
|
||||
public readonly bool TryGetTemperature(out float temperature, bool onVacuumReturnTcmb = true)
|
||||
{
|
||||
switch (_coreValue)
|
||||
{
|
||||
case AtmosImpossible:
|
||||
temperature = 0f;
|
||||
return false;
|
||||
case StateVacuum when onVacuumReturnTcmb:
|
||||
temperature = Atmospherics.TCMB;
|
||||
return true;
|
||||
case StateVacuum:
|
||||
temperature = 0f;
|
||||
return false;
|
||||
default:
|
||||
temperature = (_coreValue * TempDegreeResolution) + TempMinimum;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(ThermalByte other)
|
||||
{
|
||||
return _coreValue == other._coreValue;
|
||||
}
|
||||
|
||||
public static bool operator ==(ThermalByte left, ThermalByte right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ThermalByte left, ThermalByte right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ThermalByte other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _coreValue.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Shared.CCVar;
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ public partial class InventorySystem
|
||||
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<BlackAndWhiteOverlayComponent>>(RefRelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<NoirOverlayComponent>>(RefRelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ThermalSightComponent>>(RefRelayInventoryEvent);
|
||||
|
||||
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs);
|
||||
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<InnateVerb>>(OnGetInnateVerbs);
|
||||
|
||||
10
Content.Shared/Overlays/ThermalSightComponent.cs
Normal file
10
Content.Shared/Overlays/ThermalSightComponent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Overlays;
|
||||
|
||||
/// <summary>
|
||||
/// Makes the entity see air temperature.
|
||||
/// When added to a clothing item it will also grant the wearer the same overlay.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class ThermalSightComponent : Component;
|
||||
@@ -249,6 +249,7 @@
|
||||
- type: GroupExamine
|
||||
- type: IdentityBlocker
|
||||
coverage: EYES
|
||||
- type: ThermalSight
|
||||
|
||||
- type: entity
|
||||
parent: ClothingEyesBase
|
||||
|
||||
Reference in New Issue
Block a user