mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
Vacuum shader (#46038)
* init * slider * fixes * fixes * vacuum * toggle * turned on be default lol * typo * mediump * marker thingis * GetWorldPositionRotationMatrixWithInv * cvar dynamic * fixes * magic numbers bad * discard * refactors will continue until clean code improves * format * comm --------- Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com>
This commit is contained in:
parent
64381ef101
commit
cfd925d9e7
10 changed files with 715 additions and 410 deletions
|
|
@ -0,0 +1,65 @@
|
|||
using Content.Client.Atmos.Overlays;
|
||||
using Content.Shared.CCVar;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// System responsible for rendering vacuum effects using <see cref="GasTileVacuumOverlay"/>.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed partial class GasTileVacuumOverlaySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private IConfigurationManager _cfgManager = default!;
|
||||
|
||||
private GasTileVacuumOverlay? _gasTileVacuumOverlay;
|
||||
private bool _vacuumOverlayEnabledDueToCvar; // due to CVar being enabled
|
||||
private bool _vacuumOverlayEnabledIntensity; // due to non zero intensity in setting
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_cfgManager.OnValueChanged(CCVars.VacuumOverlay, OnVacuumOverlayChanged, true);
|
||||
_cfgManager.OnValueChanged(CCVars.VacuumOverlayIntensity, OnVacuumOverlayIntensityChanged, true);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_cfgManager.UnsubValueChanged(CCVars.VacuumOverlay, OnVacuumOverlayChanged);
|
||||
_cfgManager.UnsubValueChanged(CCVars.VacuumOverlayIntensity, OnVacuumOverlayIntensityChanged);
|
||||
_overlayMan.RemoveOverlay<GasTileVacuumOverlay>();
|
||||
}
|
||||
|
||||
private void OnVacuumOverlayChanged(bool enabled)
|
||||
{
|
||||
_vacuumOverlayEnabledDueToCvar = enabled;
|
||||
UpdateOverlay();
|
||||
}
|
||||
|
||||
private void OnVacuumOverlayIntensityChanged(float intensity)
|
||||
{
|
||||
_vacuumOverlayEnabledIntensity = intensity > 0f;
|
||||
UpdateOverlay();
|
||||
}
|
||||
|
||||
private void UpdateOverlay()
|
||||
{
|
||||
if (_vacuumOverlayEnabledDueToCvar && _vacuumOverlayEnabledIntensity)
|
||||
{
|
||||
_gasTileVacuumOverlay ??= new GasTileVacuumOverlay();
|
||||
|
||||
if (!_overlayMan.HasOverlay<GasTileVacuumOverlay>())
|
||||
_overlayMan.AddOverlay(_gasTileVacuumOverlay);
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayMan.RemoveOverlay<GasTileVacuumOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
196
Content.Client/Atmos/Overlays/GasTileVacuumOverlay.cs
Normal file
196
Content.Client/Atmos/Overlays/GasTileVacuumOverlay.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
using System.Numerics;
|
||||
using Content.Client.Graphics;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.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;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Content.Client.Atmos.Overlays;
|
||||
|
||||
/// <summary>
|
||||
/// Overlay responsible for rendering vacuum overlay.
|
||||
/// </summary>
|
||||
public sealed partial class GasTileVacuumOverlay : Overlay
|
||||
{
|
||||
public override bool RequestScreenTexture { get; set; } = true;
|
||||
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
|
||||
private static readonly ProtoId<ShaderPrototype> VacuumOverlayShader = "VacuumDesaturation";
|
||||
private static readonly Color EmptyColor = new(0, 0, 0, 0);
|
||||
private static readonly Color MarkerColor = new(255f, 0, 0);
|
||||
|
||||
[Dependency] private IEntityManager _entManager = default!;
|
||||
[Dependency] private IPrototypeManager _proto = default!;
|
||||
[Dependency] private IClyde _clyde = default!;
|
||||
[Dependency] private IConfigurationManager _configManager = default!;
|
||||
|
||||
private readonly SharedMapSystem _maps;
|
||||
private readonly SharedTransformSystem _xformSys;
|
||||
private readonly ShaderInstance _unshader;
|
||||
private readonly ShaderInstance _shader;
|
||||
|
||||
private List<Entity<MapGridComponent>> _intersectingGrids = new();
|
||||
private readonly OverlayResourceCache<CachedResources> _resources = new();
|
||||
|
||||
// Overlay settings
|
||||
private float _intensity; // overlay intensity. 0.0f = turned off, 1.0f = full grayscale
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public GasTileVacuumOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_maps = _entManager.System<SharedMapSystem>();
|
||||
_xformSys = _entManager.System<SharedTransformSystem>();
|
||||
|
||||
_unshader = _proto.Index(UnshadedShader).Instance();
|
||||
_shader = _proto.Index(VacuumOverlayShader).InstanceUnique();
|
||||
_configManager.OnValueChanged(CCVars.VacuumOverlayIntensity, SetVacuumOverlayIntensity, invokeImmediately: true);
|
||||
}
|
||||
|
||||
private void SetVacuumOverlayIntensity(float intensity)
|
||||
{
|
||||
_intensity = MathHelper.Clamp(intensity, 0f, 1f);
|
||||
_shader.SetParameter("intensity", _intensity);
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (args.MapId == MapId.Nullspace)
|
||||
return false;
|
||||
|
||||
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
||||
|
||||
var target = args.Viewport.RenderTarget;
|
||||
|
||||
// Probably the resolution of the game window changed, remake the textures.
|
||||
if (res.VacuumTarget?.Texture.Size != target.Size)
|
||||
{
|
||||
res.VacuumTarget?.Dispose();
|
||||
res.VacuumTarget = _clyde.CreateRenderTarget(
|
||||
target.Size,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||
name: $"{nameof(GasTileVacuumOverlay)}-blur");
|
||||
}
|
||||
|
||||
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
|
||||
|
||||
args.WorldHandle.UseShader(_unshader);
|
||||
|
||||
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 Vacuum after checking all visible tiles, we can bail early
|
||||
var anyVacuum = false;
|
||||
|
||||
// We're rendering in the context of the vacuum target texture, which will encode data as to where vacuum effect will be
|
||||
args.WorldHandle.RenderInRenderTarget(res.VacuumTarget,
|
||||
() =>
|
||||
{
|
||||
_intersectingGrids.Clear();
|
||||
_maps.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids);
|
||||
foreach (var grid in _intersectingGrids)
|
||||
{
|
||||
if (!overlayQuery.TryGetComponent(grid.Owner, out var comp))
|
||||
continue;
|
||||
|
||||
var (_, _, gridEntToWorld, worldToGridLocal) = _xformSys.GetWorldPositionRotationMatrixWithInv(grid.Owner);
|
||||
var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
|
||||
|
||||
if (!Matrix3x2.Invert(gridEntToViewportLocal, out _))
|
||||
continue;
|
||||
|
||||
// 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 = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
|
||||
|
||||
var localBounds = new Box2i(
|
||||
(int)MathF.Floor(floatBounds.Left),
|
||||
(int)MathF.Floor(floatBounds.Bottom),
|
||||
(int)MathF.Ceiling(floatBounds.Right),
|
||||
(int)MathF.Ceiling(floatBounds.Top));
|
||||
|
||||
// for each tile and its gas --->
|
||||
foreach (var chunk in comp.Chunks.Values)
|
||||
{
|
||||
var enumerator = new GasChunkEnumerator(chunk);
|
||||
|
||||
while (enumerator.MoveNext(out var tileGas))
|
||||
{
|
||||
// Check and make sure the tile is within the viewport/screen
|
||||
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
|
||||
if (!localBounds.Contains(tilePosition))
|
||||
continue;
|
||||
|
||||
if (tileGas.ByteGasTemperature.Value != ThermalByte.StateVacuum)
|
||||
continue;
|
||||
|
||||
// Encode the strength in the red channel
|
||||
// alpha set to 1 as tile is active
|
||||
worldHandle.DrawRect(Box2.CenteredAround(tilePosition + grid.Comp.TileSizeHalfVector,
|
||||
grid.Comp.TileSizeVector),
|
||||
MarkerColor);
|
||||
anyVacuum = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// This clears the buffer to all zero first...
|
||||
EmptyColor);
|
||||
|
||||
// no distortion, no need to render
|
||||
if (!anyVacuum)
|
||||
{
|
||||
args.WorldHandle.UseShader(null);
|
||||
args.WorldHandle.SetTransform(Matrix3x2.Identity);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
||||
|
||||
if (ScreenTexture is null || res.VacuumTarget is null)
|
||||
return;
|
||||
|
||||
_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
|
||||
|
||||
args.WorldHandle.UseShader(_shader);
|
||||
args.WorldHandle.DrawTextureRect(res.VacuumTarget.Texture, args.WorldBounds);
|
||||
|
||||
args.WorldHandle.UseShader(null);
|
||||
args.WorldHandle.SetTransform(Matrix3x2.Identity);
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_resources.Dispose();
|
||||
|
||||
_configManager.UnsubValueChanged(CCVars.VacuumOverlayIntensity, SetVacuumOverlayIntensity);
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
internal sealed class CachedResources : IDisposable
|
||||
{
|
||||
public IRenderTexture? VacuumTarget;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
VacuumTarget?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@
|
|||
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
|
||||
<ui:OptionSlider Name="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
|
||||
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
|
||||
<ui:OptionSlider Name="VacuumOverlayIntensityOpacitySlider" Title="{Loc 'ui-options-vacuum-overlay-intensity'}" />
|
||||
<CheckBox Name="AutoFillHighlightsCheckBox" Text="{Loc 'ui-options-auto-fill-highlights'}" />
|
||||
<ui:OptionColorSlider Name="HighlightsColorSlider"
|
||||
Title="{Loc 'ui-options-highlights-color'}"
|
||||
|
|
|
|||
|
|
@ -2,14 +2,19 @@ using Content.Shared.CCVar;
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Client.Options.UI.Tabs;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AccessibilityTab : Control
|
||||
{
|
||||
[Dependency] private IConfigurationManager _cfgManager = default!;
|
||||
|
||||
public AccessibilityTab()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Control.AddOptionCheckBox(CCVars.ChatEnableColorName, EnableColorNameCheckBox);
|
||||
|
|
@ -30,11 +35,14 @@ public sealed partial class AccessibilityTab : Control
|
|||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.VacuumOverlayIntensity, VacuumOverlayIntensityOpacitySlider);
|
||||
Control.AddOptionCheckBox(CCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox);
|
||||
Control.AddOptionColorSlider(CCVars.ChatHighlightsColor, HighlightsColorSlider);
|
||||
|
||||
Control.AddOptionCheckBox(CCVars.AccessibilityClientCensorNudity, CensorNudityCheckBox);
|
||||
|
||||
VacuumOverlayIntensityOpacitySlider.Visible = _cfgManager.GetCVar(CCVars.VacuumOverlay);
|
||||
|
||||
Control.Initialize();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ using Microsoft.Extensions.ObjectPool;
|
|||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Threading;
|
||||
|
|
@ -20,467 +19,466 @@ using System.Runtime.CompilerServices;
|
|||
|
||||
// ReSharper disable once RedundantUsingDirective
|
||||
|
||||
namespace Content.Server.Atmos.EntitySystems
|
||||
namespace Content.Server.Atmos.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed partial class GasTileOverlaySystem : SharedGasTileOverlaySystem
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed partial class GasTileOverlaySystem : SharedGasTileOverlaySystem
|
||||
[Robust.Shared.IoC.Dependency] private IGameTiming _gameTiming = default!;
|
||||
[Robust.Shared.IoC.Dependency] private IPlayerManager _playerManager = default!;
|
||||
[Robust.Shared.IoC.Dependency] private IParallelManager _parMan = default!;
|
||||
[Robust.Shared.IoC.Dependency] private AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Robust.Shared.IoC.Dependency] private ChunkingSystem _chunkingSys = default!;
|
||||
|
||||
[Robust.Shared.IoC.Dependency] private EntityQuery<MapGridComponent> _mapGridQuery;
|
||||
[Robust.Shared.IoC.Dependency] private EntityQuery<GasTileOverlayComponent> _gasTileOverlayQuery;
|
||||
|
||||
/// <summary>
|
||||
/// Per-tick cache of sessions.
|
||||
/// </summary>
|
||||
private readonly List<ICommonSession> _sessions = new();
|
||||
private UpdatePlayerJob _updateJob;
|
||||
|
||||
private readonly Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> _lastSentChunks = new();
|
||||
|
||||
// Oh look its more duplicated decal system code!
|
||||
private readonly ObjectPool<HashSet<Vector2i>> _chunkIndexPool =
|
||||
new DefaultObjectPool<HashSet<Vector2i>>(
|
||||
new DefaultPooledObjectPolicy<HashSet<Vector2i>>(), 64);
|
||||
private readonly ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> _chunkViewerPool =
|
||||
new DefaultObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>>(
|
||||
new DefaultPooledObjectPolicy<Dictionary<NetEntity, HashSet<Vector2i>>>(), 64);
|
||||
|
||||
private bool _doSessionUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Overlay update interval, in seconds.
|
||||
/// </summary>
|
||||
private float _updateInterval;
|
||||
|
||||
private int _thresholds;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Robust.Shared.IoC.Dependency] private IGameTiming _gameTiming = default!;
|
||||
[Robust.Shared.IoC.Dependency] private IPlayerManager _playerManager = default!;
|
||||
[Robust.Shared.IoC.Dependency] private IParallelManager _parMan = default!;
|
||||
[Robust.Shared.IoC.Dependency] private AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Robust.Shared.IoC.Dependency] private ChunkingSystem _chunkingSys = default!;
|
||||
base.Initialize();
|
||||
|
||||
[Robust.Shared.IoC.Dependency] private EntityQuery<MapGridComponent> _mapGridQuery = default!;
|
||||
[Robust.Shared.IoC.Dependency] private EntityQuery<GasTileOverlayComponent> _gasTileOverlayQuery = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Per-tick cache of sessions.
|
||||
/// </summary>
|
||||
private readonly List<ICommonSession> _sessions = new();
|
||||
private UpdatePlayerJob _updateJob;
|
||||
|
||||
private readonly Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> _lastSentChunks = new();
|
||||
|
||||
// Oh look its more duplicated decal system code!
|
||||
private ObjectPool<HashSet<Vector2i>> _chunkIndexPool =
|
||||
new DefaultObjectPool<HashSet<Vector2i>>(
|
||||
new DefaultPooledObjectPolicy<HashSet<Vector2i>>(), 64);
|
||||
private ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> _chunkViewerPool =
|
||||
new DefaultObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>>(
|
||||
new DefaultPooledObjectPolicy<Dictionary<NetEntity, HashSet<Vector2i>>>(), 64);
|
||||
|
||||
private bool _doSessionUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Overlay update interval, in seconds.
|
||||
/// </summary>
|
||||
private float _updateInterval;
|
||||
|
||||
private int _thresholds;
|
||||
|
||||
public override void Initialize()
|
||||
_updateJob = new UpdatePlayerJob()
|
||||
{
|
||||
base.Initialize();
|
||||
EntManager = EntityManager,
|
||||
System = this,
|
||||
ChunkIndexPool = _chunkIndexPool,
|
||||
Sessions = _sessions,
|
||||
ChunkingSys = _chunkingSys,
|
||||
ChunkViewerPool = _chunkViewerPool,
|
||||
LastSentChunks = _lastSentChunks,
|
||||
GridQuery = _mapGridQuery,
|
||||
};
|
||||
|
||||
_updateJob = new UpdatePlayerJob()
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
|
||||
InitializeCVars();
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, GasTileOverlayComponent component, ComponentStartup args)
|
||||
{
|
||||
// This **shouldn't** be required, but just in case we ever get entity prototypes that have gas overlays, we
|
||||
// need to ensure that we send an initial full state to players.
|
||||
Dirty(uid, component);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void OnPvsToggle(bool value)
|
||||
{
|
||||
if (value == PvsEnabled)
|
||||
return;
|
||||
|
||||
PvsEnabled = value;
|
||||
|
||||
if (value)
|
||||
return;
|
||||
|
||||
foreach (var lastSent in _lastSentChunks.Values)
|
||||
{
|
||||
foreach (var set in lastSent.Values)
|
||||
{
|
||||
EntManager = EntityManager,
|
||||
System = this,
|
||||
ChunkIndexPool = _chunkIndexPool,
|
||||
Sessions = _sessions,
|
||||
ChunkingSys = _chunkingSys,
|
||||
ChunkViewerPool = _chunkViewerPool,
|
||||
LastSentChunks = _lastSentChunks,
|
||||
GridQuery = _mapGridQuery,
|
||||
};
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
|
||||
InitializeCVars();
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
|
||||
set.Clear();
|
||||
_chunkIndexPool.Return(set);
|
||||
}
|
||||
lastSent.Clear();
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, GasTileOverlayComponent component, ComponentStartup args)
|
||||
// PVS was turned off, ensure data gets sent to all clients.
|
||||
var query = AllEntityQuery<GasTileOverlayComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out var grid, out var meta))
|
||||
{
|
||||
// This **shouldn't** be required, but just in case we ever get entity prototypes that have gas overlays, we
|
||||
// need to ensure that we send an initial full state to players.
|
||||
Dirty(uid, component);
|
||||
grid.ForceTick = _gameTiming.CurTick;
|
||||
Dirty(uid, grid, meta);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
|
||||
private void UpdateThresholds(int value) => _thresholds = value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Invalidate(Entity<GasTileOverlayComponent?> grid, Vector2i index)
|
||||
{
|
||||
if (_gasTileOverlayQuery.Resolve(grid.Owner, ref grid.Comp))
|
||||
grid.Comp.InvalidTiles.Add(index);
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus != SessionStatus.InGame)
|
||||
{
|
||||
base.Shutdown();
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void OnPvsToggle(bool value)
|
||||
{
|
||||
if (value == PvsEnabled)
|
||||
return;
|
||||
|
||||
PvsEnabled = value;
|
||||
|
||||
if (value)
|
||||
return;
|
||||
|
||||
foreach (var lastSent in _lastSentChunks.Values)
|
||||
if (_lastSentChunks.Remove(e.Session, out var sets))
|
||||
{
|
||||
foreach (var set in lastSent.Values)
|
||||
foreach (var set in sets.Values)
|
||||
{
|
||||
set.Clear();
|
||||
_chunkIndexPool.Return(set);
|
||||
}
|
||||
lastSent.Clear();
|
||||
}
|
||||
|
||||
// PVS was turned off, ensure data gets sent to all clients.
|
||||
var query = AllEntityQuery<GasTileOverlayComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out var grid, out var meta))
|
||||
{
|
||||
grid.ForceTick = _gameTiming.CurTick;
|
||||
Dirty(uid, grid, meta);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
|
||||
private void UpdateThresholds(int value) => _thresholds = value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Invalidate(Entity<GasTileOverlayComponent?> grid, Vector2i index)
|
||||
if (!_lastSentChunks.ContainsKey(e.Session))
|
||||
{
|
||||
if (_gasTileOverlayQuery.Resolve(grid.Owner, ref grid.Comp))
|
||||
grid.Comp.InvalidTiles.Add(index);
|
||||
_lastSentChunks[e.Session] = new();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax)
|
||||
{
|
||||
return (byte) (ContentHelpers.RoundToLevels(
|
||||
MathHelper.Clamp01((moles - molesVisible) /
|
||||
(molesVisibleMax - molesVisible)) * 255, byte.MaxValue,
|
||||
_thresholds) * 255 / (_thresholds - 1));
|
||||
}
|
||||
|
||||
public GasOverlayData GetOverlayData(GasMixture? mixture)
|
||||
{
|
||||
ThermalByte byteTemp;
|
||||
if (mixture == null)
|
||||
{
|
||||
if (e.NewStatus != SessionStatus.InGame)
|
||||
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++)
|
||||
{
|
||||
var id = VisibleGasId[i];
|
||||
var gas = _atmosphereSystem.GetGas(id);
|
||||
var moles = mixture?[id] ?? 0f;
|
||||
ref var opacity = ref data.Opacity[i];
|
||||
|
||||
if (moles < gas.GasMolesVisible)
|
||||
{
|
||||
if (_lastSentChunks.Remove(e.Session, out var sets))
|
||||
{
|
||||
foreach (var set in sets.Values)
|
||||
{
|
||||
set.Clear();
|
||||
_chunkIndexPool.Return(set);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_lastSentChunks.ContainsKey(e.Session))
|
||||
{
|
||||
_lastSentChunks[e.Session] = new();
|
||||
}
|
||||
}
|
||||
|
||||
private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax)
|
||||
{
|
||||
return (byte) (ContentHelpers.RoundToLevels(
|
||||
MathHelper.Clamp01((moles - molesVisible) /
|
||||
(molesVisibleMax - molesVisible)) * 255, byte.MaxValue,
|
||||
opacity = (byte) (ContentHelpers.RoundToLevels(
|
||||
MathHelper.Clamp01((moles - gas.GasMolesVisible) /
|
||||
(gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue,
|
||||
_thresholds) * 255 / (_thresholds - 1));
|
||||
}
|
||||
|
||||
public GasOverlayData GetOverlayData(GasMixture? mixture)
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the visuals for a tile on some grid chunk. Returns true if the visuals have changed.
|
||||
/// </summary>
|
||||
private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayChunk chunk, Vector2i index)
|
||||
{
|
||||
ref var oldData = ref chunk.TileData[chunk.GetDataIndex(index)];
|
||||
if (!gridAtmosphere.Tiles.TryGetValue(index, out var tile))
|
||||
{
|
||||
ThermalByte byteTemp;
|
||||
if (mixture == null)
|
||||
{
|
||||
byteTemp = new();
|
||||
byteTemp.SetVacuum();
|
||||
}
|
||||
else
|
||||
byteTemp = new(mixture.Temperature);
|
||||
if (oldData.Equals(default))
|
||||
return false;
|
||||
|
||||
var data = new GasOverlayData(0, new byte[VisibleGasId.Length], byteTemp);
|
||||
chunk.LastUpdate = _gameTiming.CurTick;
|
||||
oldData = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
|
||||
ThermalByte newByteTemp = new();
|
||||
|
||||
if (tile.Hotspot.Valid)
|
||||
newByteTemp.SetTemperature(tile.Hotspot.Temperature);
|
||||
else if (!tile.Space && tile.Air?.TotalMoles <= ThermalByte.MinimalPressureThreshold)
|
||||
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], newByteTemp);
|
||||
}
|
||||
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, newByteTemp);
|
||||
}
|
||||
|
||||
if (tile is {Air: not null, NoGridTile: false})
|
||||
{
|
||||
for (var i = 0; i < VisibleGasId.Length; i++)
|
||||
{
|
||||
var id = VisibleGasId[i];
|
||||
var gas = _atmosphereSystem.GetGas(id);
|
||||
var moles = mixture?[id] ?? 0f;
|
||||
ref var opacity = ref data.Opacity[i];
|
||||
var moles = tile.Air[id];
|
||||
ref var oldOpacity = ref oldData.Opacity[i];
|
||||
|
||||
if (moles < gas.GasMolesVisible)
|
||||
{
|
||||
if (oldOpacity != 0)
|
||||
{
|
||||
oldOpacity = 0;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
opacity = (byte) (ContentHelpers.RoundToLevels(
|
||||
MathHelper.Clamp01((moles - gas.GasMolesVisible) /
|
||||
(gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue,
|
||||
_thresholds) * 255 / (_thresholds - 1));
|
||||
}
|
||||
var opacity = GetOpacity(moles, gas.GasMolesVisible, gas.GasMolesVisibleMax);
|
||||
|
||||
return data;
|
||||
if (oldOpacity == opacity)
|
||||
continue;
|
||||
|
||||
oldOpacity = opacity;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < VisibleGasId.Length; i++)
|
||||
{
|
||||
changed |= oldData.Opacity[i] != 0;
|
||||
oldData.Opacity[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the visuals for a tile on some grid chunk. Returns true if the visuals have changed.
|
||||
/// </summary>
|
||||
private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayChunk chunk, Vector2i index)
|
||||
if (!changed)
|
||||
return false;
|
||||
|
||||
chunk.LastUpdate = _gameTiming.CurTick;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateOverlayData()
|
||||
{
|
||||
// TODO parallelize?
|
||||
var query = AllEntityQuery<GasTileOverlayComponent, GridAtmosphereComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out var overlay, out var gam, out var meta))
|
||||
{
|
||||
ref var oldData = ref chunk.TileData[chunk.GetDataIndex(index)];
|
||||
if (!gridAtmosphere.Tiles.TryGetValue(index, out var tile))
|
||||
{
|
||||
if (oldData.Equals(default))
|
||||
return false;
|
||||
|
||||
chunk.LastUpdate = _gameTiming.CurTick;
|
||||
oldData = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
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))
|
||||
foreach (var index in overlay.InvalidTiles)
|
||||
{
|
||||
changed = true;
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], newByteTemp);
|
||||
}
|
||||
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, newByteTemp);
|
||||
var chunkIndex = GetGasChunkIndices(index);
|
||||
|
||||
if (!overlay.Chunks.TryGetValue(chunkIndex, out var chunk))
|
||||
overlay.Chunks[chunkIndex] = chunk = new GasOverlayChunk(chunkIndex);
|
||||
|
||||
changed |= UpdateChunkTile(gam, chunk, index);
|
||||
}
|
||||
|
||||
if (tile is {Air: not null, NoGridTile: false})
|
||||
{
|
||||
for (var i = 0; i < VisibleGasId.Length; i++)
|
||||
{
|
||||
var id = VisibleGasId[i];
|
||||
var gas = _atmosphereSystem.GetGas(id);
|
||||
var moles = tile.Air[id];
|
||||
ref var oldOpacity = ref oldData.Opacity[i];
|
||||
if (changed)
|
||||
Dirty(uid, overlay, meta);
|
||||
|
||||
if (moles < gas.GasMolesVisible)
|
||||
{
|
||||
if (oldOpacity != 0)
|
||||
{
|
||||
oldOpacity = 0;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var opacity = GetOpacity(moles, gas.GasMolesVisible, gas.GasMolesVisibleMax);
|
||||
|
||||
if (oldOpacity == opacity)
|
||||
continue;
|
||||
|
||||
oldOpacity = opacity;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < VisibleGasId.Length; i++)
|
||||
{
|
||||
changed |= oldData.Opacity[i] != 0;
|
||||
oldData.Opacity[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
return false;
|
||||
|
||||
chunk.LastUpdate = _gameTiming.CurTick;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateOverlayData()
|
||||
{
|
||||
// TODO parallelize?
|
||||
var query = AllEntityQuery<GasTileOverlayComponent, GridAtmosphereComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out var overlay, out var gam, out var meta))
|
||||
{
|
||||
var changed = false;
|
||||
foreach (var index in overlay.InvalidTiles)
|
||||
{
|
||||
var chunkIndex = GetGasChunkIndices(index);
|
||||
|
||||
if (!overlay.Chunks.TryGetValue(chunkIndex, out var chunk))
|
||||
overlay.Chunks[chunkIndex] = chunk = new GasOverlayChunk(chunkIndex);
|
||||
|
||||
changed |= UpdateChunkTile(gam, chunk, index);
|
||||
}
|
||||
|
||||
if (changed)
|
||||
Dirty(uid, overlay, meta);
|
||||
|
||||
overlay.InvalidTiles.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
AccumulatedFrameTime += frameTime;
|
||||
|
||||
if (_doSessionUpdate)
|
||||
{
|
||||
UpdateSessions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (AccumulatedFrameTime < _updateInterval)
|
||||
return;
|
||||
|
||||
AccumulatedFrameTime -= _updateInterval;
|
||||
|
||||
// First, update per-chunk visual data for any invalidated tiles.
|
||||
UpdateOverlayData();
|
||||
|
||||
// Then, next tick we send the data to players.
|
||||
// This is to avoid doing all the work in the same tick.
|
||||
_doSessionUpdate = true;
|
||||
}
|
||||
|
||||
public void UpdateSessions()
|
||||
{
|
||||
_doSessionUpdate = false;
|
||||
|
||||
if (!PvsEnabled)
|
||||
return;
|
||||
|
||||
// Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
|
||||
// If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
|
||||
// Afterwards we reset all the chunk data for the next time we tick.
|
||||
_sessions.Clear();
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
if (player.Status != SessionStatus.InGame)
|
||||
continue;
|
||||
|
||||
_sessions.Add(player);
|
||||
}
|
||||
|
||||
if (_sessions.Count == 0)
|
||||
return;
|
||||
|
||||
_parMan.ProcessNow(_updateJob, _sessions.Count);
|
||||
_updateJob.LastSessionUpdate = _gameTiming.CurTick;
|
||||
}
|
||||
|
||||
public void Reset(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
foreach (var data in _lastSentChunks.Values)
|
||||
{
|
||||
foreach (var previous in data.Values)
|
||||
{
|
||||
previous.Clear();
|
||||
_chunkIndexPool.Return(previous);
|
||||
}
|
||||
|
||||
data.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
/// <summary>
|
||||
/// Updates per player gas overlay data.
|
||||
/// </summary>
|
||||
private record struct UpdatePlayerJob : IParallelRobustJob
|
||||
{
|
||||
public int BatchSize => 2;
|
||||
|
||||
public IEntityManager EntManager;
|
||||
public ChunkingSystem ChunkingSys;
|
||||
public GasTileOverlaySystem System;
|
||||
public ObjectPool<HashSet<Vector2i>> ChunkIndexPool;
|
||||
public ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> ChunkViewerPool;
|
||||
|
||||
public GameTick LastSessionUpdate;
|
||||
public Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> LastSentChunks;
|
||||
public List<ICommonSession> Sessions;
|
||||
|
||||
public EntityQuery<MapGridComponent> GridQuery;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var playerSession = Sessions[index];
|
||||
var chunksInRange = ChunkingSys.GetChunksForSession(playerSession, ChunkSize, ChunkIndexPool, ChunkViewerPool);
|
||||
var previouslySent = LastSentChunks[playerSession];
|
||||
|
||||
var ev = new GasOverlayUpdateEvent();
|
||||
|
||||
foreach (var (netGrid, oldIndices) in previouslySent)
|
||||
{
|
||||
// Mark the whole grid as stale and flag for removal.
|
||||
if (!chunksInRange.TryGetValue(netGrid, out var chunks))
|
||||
{
|
||||
previouslySent.Remove(netGrid);
|
||||
|
||||
// If grid was deleted then don't worry about sending it to the client.
|
||||
if (!EntManager.TryGetEntity(netGrid, out var gridId) || GridQuery.HasComp(gridId.Value))
|
||||
ev.RemovedChunks[netGrid] = oldIndices;
|
||||
else
|
||||
{
|
||||
oldIndices.Clear();
|
||||
ChunkIndexPool.Return(oldIndices);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var old = ChunkIndexPool.Get();
|
||||
DebugTools.Assert(old.Count == 0);
|
||||
foreach (var chunk in oldIndices)
|
||||
{
|
||||
if (!chunks.Contains(chunk))
|
||||
old.Add(chunk);
|
||||
}
|
||||
|
||||
if (old.Count == 0)
|
||||
ChunkIndexPool.Return(old);
|
||||
else
|
||||
ev.RemovedChunks.Add(netGrid, old);
|
||||
}
|
||||
|
||||
foreach (var (netGrid, gridChunks) in chunksInRange)
|
||||
{
|
||||
// Not all grids have atmospheres.
|
||||
if (!EntManager.TryGetEntity(netGrid, out var grid) || !EntManager.TryGetComponent(grid, out GasTileOverlayComponent? overlay))
|
||||
continue;
|
||||
|
||||
List<GasOverlayChunk> dataToSend = new();
|
||||
ev.UpdatedChunks[netGrid] = dataToSend;
|
||||
|
||||
previouslySent.TryGetValue(netGrid, out var previousChunks);
|
||||
|
||||
foreach (var gIndex in gridChunks)
|
||||
{
|
||||
if (!overlay.Chunks.TryGetValue(gIndex, out var value))
|
||||
continue;
|
||||
|
||||
// If the chunk was updated since we last sent it, send it again
|
||||
if (value.LastUpdate > LastSessionUpdate)
|
||||
{
|
||||
dataToSend.Add(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always send it if we didn't previously send it
|
||||
if (previousChunks == null || !previousChunks.Contains(gIndex))
|
||||
dataToSend.Add(value);
|
||||
}
|
||||
|
||||
previouslySent[netGrid] = gridChunks;
|
||||
if (previousChunks != null)
|
||||
{
|
||||
previousChunks.Clear();
|
||||
ChunkIndexPool.Return(previousChunks);
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.UpdatedChunks.Count != 0 || ev.RemovedChunks.Count != 0)
|
||||
System.RaiseNetworkEvent(ev, playerSession.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
#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);
|
||||
overlay.InvalidTiles.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
AccumulatedFrameTime += frameTime;
|
||||
|
||||
if (_doSessionUpdate)
|
||||
{
|
||||
UpdateSessions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (AccumulatedFrameTime < _updateInterval)
|
||||
return;
|
||||
|
||||
AccumulatedFrameTime -= _updateInterval;
|
||||
|
||||
// First, update per-chunk visual data for any invalidated tiles.
|
||||
UpdateOverlayData();
|
||||
|
||||
// Then, next tick we send the data to players.
|
||||
// This is to avoid doing all the work in the same tick.
|
||||
_doSessionUpdate = true;
|
||||
}
|
||||
|
||||
public void UpdateSessions()
|
||||
{
|
||||
_doSessionUpdate = false;
|
||||
|
||||
if (!PvsEnabled)
|
||||
return;
|
||||
|
||||
// Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
|
||||
// If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
|
||||
// Afterwards we reset all the chunk data for the next time we tick.
|
||||
_sessions.Clear();
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
if (player.Status != SessionStatus.InGame)
|
||||
continue;
|
||||
|
||||
_sessions.Add(player);
|
||||
}
|
||||
|
||||
if (_sessions.Count == 0)
|
||||
return;
|
||||
|
||||
_parMan.ProcessNow(_updateJob, _sessions.Count);
|
||||
_updateJob.LastSessionUpdate = _gameTiming.CurTick;
|
||||
}
|
||||
|
||||
public void Reset(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
foreach (var data in _lastSentChunks.Values)
|
||||
{
|
||||
foreach (var previous in data.Values)
|
||||
{
|
||||
previous.Clear();
|
||||
_chunkIndexPool.Return(previous);
|
||||
}
|
||||
|
||||
data.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
/// <summary>
|
||||
/// Updates per player gas overlay data.
|
||||
/// </summary>
|
||||
private record struct UpdatePlayerJob : IParallelRobustJob
|
||||
{
|
||||
public int BatchSize => 2;
|
||||
|
||||
public IEntityManager EntManager;
|
||||
public ChunkingSystem ChunkingSys;
|
||||
public GasTileOverlaySystem System;
|
||||
public ObjectPool<HashSet<Vector2i>> ChunkIndexPool;
|
||||
public ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> ChunkViewerPool;
|
||||
|
||||
public GameTick LastSessionUpdate;
|
||||
public Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> LastSentChunks;
|
||||
public List<ICommonSession> Sessions;
|
||||
|
||||
public EntityQuery<MapGridComponent> GridQuery;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var playerSession = Sessions[index];
|
||||
var chunksInRange = ChunkingSys.GetChunksForSession(playerSession, ChunkSize, ChunkIndexPool, ChunkViewerPool);
|
||||
var previouslySent = LastSentChunks[playerSession];
|
||||
|
||||
var ev = new GasOverlayUpdateEvent();
|
||||
|
||||
foreach (var (netGrid, oldIndices) in previouslySent)
|
||||
{
|
||||
// Mark the whole grid as stale and flag for removal.
|
||||
if (!chunksInRange.TryGetValue(netGrid, out var chunks))
|
||||
{
|
||||
previouslySent.Remove(netGrid);
|
||||
|
||||
// If grid was deleted then don't worry about sending it to the client.
|
||||
if (!EntManager.TryGetEntity(netGrid, out var gridId) || GridQuery.HasComp(gridId.Value))
|
||||
ev.RemovedChunks[netGrid] = oldIndices;
|
||||
else
|
||||
{
|
||||
oldIndices.Clear();
|
||||
ChunkIndexPool.Return(oldIndices);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var old = ChunkIndexPool.Get();
|
||||
DebugTools.Assert(old.Count == 0);
|
||||
foreach (var chunk in oldIndices)
|
||||
{
|
||||
if (!chunks.Contains(chunk))
|
||||
old.Add(chunk);
|
||||
}
|
||||
|
||||
if (old.Count == 0)
|
||||
ChunkIndexPool.Return(old);
|
||||
else
|
||||
ev.RemovedChunks.Add(netGrid, old);
|
||||
}
|
||||
|
||||
foreach (var (netGrid, gridChunks) in chunksInRange)
|
||||
{
|
||||
// Not all grids have atmospheres.
|
||||
if (!EntManager.TryGetEntity(netGrid, out var grid) || !EntManager.TryGetComponent(grid, out GasTileOverlayComponent? overlay))
|
||||
continue;
|
||||
|
||||
List<GasOverlayChunk> dataToSend = new();
|
||||
ev.UpdatedChunks[netGrid] = dataToSend;
|
||||
|
||||
previouslySent.TryGetValue(netGrid, out var previousChunks);
|
||||
|
||||
foreach (var gIndex in gridChunks)
|
||||
{
|
||||
if (!overlay.Chunks.TryGetValue(gIndex, out var value))
|
||||
continue;
|
||||
|
||||
// If the chunk was updated since we last sent it, send it again
|
||||
if (value.LastUpdate > LastSessionUpdate)
|
||||
{
|
||||
dataToSend.Add(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always send it if we didn't previously send it
|
||||
if (previousChunks == null || !previousChunks.Contains(gIndex))
|
||||
dataToSend.Add(value);
|
||||
}
|
||||
|
||||
previouslySent[netGrid] = gridChunks;
|
||||
if (previousChunks != null)
|
||||
{
|
||||
previousChunks.Clear();
|
||||
ChunkIndexPool.Return(previousChunks);
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.UpdatedChunks.Count != 0 || ev.RemovedChunks.Count != 0)
|
||||
System.RaiseNetworkEvent(ev, playerSession.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,11 @@ public struct ThermalByte : IEquatable<ThermalByte>
|
|||
public const float TempDegreeResolution = (TempMaximum - TempMinimum) / TempResolution;
|
||||
public const float TempToByteFactor = TempResolution / (TempMaximum - TempMinimum);
|
||||
|
||||
/// <summary>
|
||||
/// The minimum pressure gas must have for tile temperature to be networked to clients. Below this threshold thermal byte is set to StateVacuum.
|
||||
/// </summary>
|
||||
public const float MinimalPressureThreshold = 20f;
|
||||
|
||||
private byte _coreValue;
|
||||
|
||||
public ThermalByte(float temperatureKelvin)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,18 @@ public sealed partial class CCVars
|
|||
public static readonly CVarDef<bool> DisableFlashEffect =
|
||||
CVarDef.Create("accessibility.disable_flash_effect", false, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Decreases the intensity of the vacuum overlay 1.0f is maximum.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> VacuumOverlayIntensity =
|
||||
CVarDef.Create("accessibility.vacuum_overlay_intensity", 0.5f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles vacuum overlay serverside. If you do not want it as an option for your players use this.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VacuumOverlay =
|
||||
CVarDef.Create("accessibility.vacuum_overlay", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Decreases motion on the heat distortion shader.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@ ui-options-chat-window-opacity = Chat window opacity
|
|||
ui-options-speech-bubble-text-opacity = Speech bubble text opacity
|
||||
ui-options-speech-bubble-speaker-opacity = Speech bubble speaker opacity
|
||||
ui-options-speech-bubble-background-opacity = Speech bubble background opacity
|
||||
ui-options-vacuum-overlay-intensity = Vacuum overlay intensity
|
||||
|
||||
ui-options-censor-nudity = Censor character nudity
|
||||
|
||||
|
|
|
|||
|
|
@ -140,3 +140,8 @@
|
|||
id: Sharpness
|
||||
kind: source
|
||||
path: "/Textures/Shaders/sharpness.swsl"
|
||||
|
||||
- type: shader
|
||||
id: VacuumDesaturation
|
||||
kind: source
|
||||
path: "/Textures/Shaders/vacuum_desaturation.swsl"
|
||||
|
|
|
|||
14
Resources/Textures/Shaders/vacuum_desaturation.swsl
Normal file
14
Resources/Textures/Shaders/vacuum_desaturation.swsl
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
uniform sampler2D SCREEN_TEXTURE;
|
||||
uniform mediump float intensity;
|
||||
|
||||
void fragment()
|
||||
{
|
||||
if (texture2D(TEXTURE, UV).r < 0.1)
|
||||
discard;
|
||||
|
||||
mediump vec4 screen_pixel = texture2D(SCREEN_TEXTURE, UV);
|
||||
mediump float gray = dot(screen_pixel.rgb, vec3(0.299, 0.587, 0.114));
|
||||
|
||||
screen_pixel.rgb = mix(screen_pixel.rgb, vec3(gray), intensity);
|
||||
COLOR = screen_pixel;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue