Files
Delta-v/Content.Server/Decals/DecalSystem.cs
T
29e55b9464 Upstream Merge (June / July) attempt 2 electric boogaloo (#4607)
* Fix the sensor monitoring console forcing a GC every 3 seconds (#38146)

* Optimize sensor monitoring window graph drawing

* Add shared static Vector2 pool for all GraphView instances

* Address requested changes

* remove lock

* Update submodule to 264.0.0 (#38629)

* Toy/Plushie Inhands and Wearables (#38514)

* Fixed dependency injection and some other issues in a few places.

* More issue fixes

* patchfor broken NetworkConfiguratorLinkMenu (#38632)

fix

* Fix solutions flickering when transferring contents (#34838)

* Use Solution clones when applying SolutionComponent states

* Revert "Use Solution clones when applying SolutionComponent states"

This reverts commit 013fd111cf92b22562e00f98a7aaa49bc4b4ed62.

* Make Solution implement ICloneable and rename Clone method.

* Copy CanReact value when cloning a Solution

* Convert to IRobustCloneable

* Fix typos in guidebook: Buisness → Business (#38636)

* Fix typo in `MinorAntagonists.xml`

Buisness → Business

* Fix typo in `YourFirstCharacter.xml`

Buisness → Business

* Various Headphones Fixes and Tweaks (#38479)

* sprites, wearables

* neck

* icon-on sprite

* Cleanup prototype instantiation in `DamageTest` (#38639)

Cleanup prototype instantiation in DamageTest

* Cleanup prototype instantiation in `ExplosionSystem` (#38642)

Cleanup prototype instantiation in ExplosionSystem

* AddBodyPartCommand localization. (#38612)

commit

* fix ItemSlotsSystem debug assert (#38655)

* Allow the Command & Super door remotes to use the access of their user. (Re-creation of PR due to changes to game balance) (#35536)

* Added directional beacons (#38284)

* Added directional beacons

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Fixed names

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Power stat and nuke codes commands get some LEC love. (#38585)

* commit

* requested changes.

* Dsay Dirty and Follow commands converted to LEC and localized. (#38666)

* commit

* whoopwhoopwhoop

* Retro laser sprite fix (#38676)

* Fixed everything except the icon

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* fixed icon

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* fix water coolers (#38681)

* Monochromacy typo fix (#38686)

* fixes the typo

* Fixed cloning looking for the trait, not the component, RE https://github.com/space-wizards/space-station-14/pull/38686#issuecomment-3025093504

* Apply suggestions from code review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Validate `CloningSettingsPrototype`s (#38688)

* Validate CloningSettingsPrototypes

* Update Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Check EventComponents too

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Add test of objective-related console commands (#36400)

* Add test of objective add/list/remove commands

* Not sure why we're validating test prototypes, but sure

* We don't need a map

* Improvements and fixups for New Status Effect API (#38660)

* Predict healing and bloodstream (#38690)

* initial commit

* reapply 38126

* fix rootable

* someone missed an important minus sign here

* try this

* fix

* fix

* reenable crit hits

* cleanup

* fix status time dirtying

* fix

* camelCase

* Healing and bloodstream prediction fixes.

* Resolving Wizard casting recall on nuke disk making it impossible to disarm (#38661)

* Resolving Wizard Recall on Nuke disk making it impossible to disarm
- Adding a DisarmBomb case to nuke status update loop
- Changing a few methods and parameters to properly follow formatting standards
- Updating some names to follow camelCase

* Updating missed tag

* Reverting DataField change

Should prevent this preventative bugfix being a breaking change.

* Switch HSV to the default colorspace for character customization (#38434)

* Made HSV default for character editor

* Adds/fixes comments to HSV defaulting

* Added dropbox fix, potentially cursed

* Revert "Added dropbox fix, potentially cursed"

This reverts commit a709883366fbee813e839742125e70844672af29.

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* Fix: Don't deploy foldables when clicking on items inside containers (#38709)

* Fix

* Apply suggestions from code review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Pressure Relief Valve (#36708)

* initial system (this math is probably WRONG)

* General code cleanup and OnExamined support
(holy moly this code sucks)

* UICode and related events foundation
TODO:
- Actually write the XAML UI and the underlying system
- Un-shitcode the entire thing
- Actually test everything...

* Working UI code
TODO: Make predicted, as this certainly isn't predicted. Even though I said it was. It isn't.

* Remove one TODO for unshitcoding the examine code

* Add reminder
yea

* Make predicted (defenitely isn't)
(also defenitely isn't a copypaste from pressure pump code)

* It's predicted!
TODO:
- Give it snazzy predicted visuals!
- Have a different field for pressure entry, lest it gets bulldozed every UI update.

* Improve gas pressure relief valve UI
TODO: Reminder to reduce amount of dirties using deltafields

* Implement DirtyField prediction

* Entity<T> cleanup
A lot of Entity<T> conversions and lukewarm cleanup.

Also got caught copy pasting code in 4K UHD but it's not like you couldn't tell.

* More cleanup and comments

* Remove TODO comment on bulldozing window title

* """refactoring"""
- Move appearance out of shared and finally fix it. Pointless to predict appearance in this instance.
- More Entity<T> conversions because I like them.
- Move UI creation handling over entirely to the ActivatableUI system.
- Fix a hardcoded locale string (why????).

* Add visuals

* Revert debugging variable replacememt
yea

* Revert skissue

* Remove unused using directives and remove TODO

* Localize, cleanup, document

* Fix adminlogging discrepancy

* Add ability to construct, add guidebook entry

* Clear up comment

* Add guidebook tooltip to valve

* Convert GasPressureReliefValveBoundUserInterface declaration into primary constructor

* Adds more input handling and adds autofill on open

* Un-deepfry input validator shitcode
Genuinely what was I smoking

* improve visuals logic

* Refactor again
- Update math to the correct implementation
- Moved code that could be re-used in the future into a helper method under AtmosphereSystem.Gases.cs

* I'm sorry but I hate warnings

* Remove unused using directive in AtmosphereSystem.Gases.cs

* Review and cleanup

* Lukewarm UI glossup

* Maintainer for the upstream project btw

* Remove redundant state sets and messy logic

* Unduplicate valve updater code

* Redo UI (im sorry Slarti)

* run tests

* Test refactored UI messaging

* Second round of UI improvements
- God please find a way to improve this system. Feels bad.

* Update loop implementation

* Further predict UI

* Clear up SetToCurrentThreshold

* cleanup

* Update to master + pipe layers and bug fixes
want to run tests

* fixes

* Deploy rename pipebomb

* Documentation and requested changes

* Rename the method that wiggled away

* Undo rounding changes

* Fix comment

* Rename and cleanup

* Apply suggestions from code review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Fix TextLinkTag (#32203)

* Adjust uplink buy button to be under item icon (#38596)

* Adjusted uplink buy button to be under item icon

* Put the discount subtext under the icon

* Indent fixes, added margin

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* Scurrets - Audio Improvements (#38482)

* Scurret audio tuning

* Add new sfx

* Update sneezing sfx

* YAML support

* Rename a folder

* make telesci wreck easier (#37569)

rel

* Validate `ShaderPrototype` IDs (#38728)

* Convert all shader prototype string literals to protoids in overlays

* Convert more shader prototype literal strings to protoids

* Convert ValidatePrototypeId to ProtoId

* Later

* [BUGFIX] Fixed revenant malfunction ability not working properly only MediBots and Stasis bed (#38664)

* fixed

* clean up

* orks fix smart

* review fix 1

* more requested changes

* less cursed

* more descriptive description

* better wording

* Tiny Tiny Cleanup of the EyeClosingSystem. (#38734)

Update EyeClosingSystem.cs

* Fix human skin tone distribution (#38701)

* fix: Use PredictedQueueDel for gib spell (#38729)

* fix: don't default to uncharged sprite state for cells (#38730)

* Fix Hristov description - remove inaccurate technical specs (#38746)

- Removes inaccurate 'armor piercing 14.5mm shells' reference
- Replaces it with a more funny description, matching the style of the other snipers and guns
- Keeps ammunition type '.60 anti-materiel ammo' specification
- Fixes issue #38590

Co-authored-by: Arthur Fiorese de Andrade <aandrade@cmcxs.gov.br>

* feat: allow mopping evaporating puddles (#38743)

* Validate remaining `ProtoId` strings (#38747)

Validate remaining ProtoId strings

* Validate `ProtoId`s in tests (#38745)

* Convert string literals to protoids in Content.Tests

* Convert string literals to protoids or consts in Content.IntegrationTests

* Fix linter failures
Tricksy static using misled me

* Cleanup warning in StomachSystem (#38748)

you did not see this

* Vox scars (#38592)

* Added vox scars n'stuff, renamed vox_tattoos.ftl to just vox.ftl

* Revert "Added vox scars n'stuff, renamed vox_tattoos.ftl to just vox.ftl"

This reverts commit c73da55ba3b39ddf93b493aecd85604c54dd8a15.

* locale key fix

* Changed top surgery scar names to be more generalized

* Adjusted face scars

* Formatting fixes

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* UnlockNode command to LEC. (#38751)

* commit

* Update UnlockNodeCommand.cs

* commit

* move command locale to its own file.

* Update Content.Server/Xenoarchaeology/Artifact/UnlockNodeCommand.cs

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Typofixes for figurine dialogue (#38737)

* Typofixes for figurine dialogue

* Forgot two

* Janitor Tool: Wire Brush (#38667)

* Wow! It's -brush-

* spacing.

* Update Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* fixed changes

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Minor escape menu UX improvements (#38650)

* fix: spellbooks can have infinite charges (#38376)

* fix: spellbooks can have infinite charges

* refactor: indicate infinite spellbook charges with null

Not sure if I like this much better...
2025-11-06 18:22:39 +00:00

617 lines
22 KiB
C#

using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Shared.Administration;
using Content.Shared.Chunking;
using Content.Shared.Database;
using Content.Shared.Decals;
using Content.Shared.Maps;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Content.Shared.Decals.DecalGridComponent;
using ChunkIndicesEnumerator = Robust.Shared.Map.Enumerators.ChunkIndicesEnumerator;
namespace Content.Server.Decals
{
public sealed class DecalSystem : SharedDecalSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly ChunkingSystem _chunking = default!;
[Dependency] private readonly IConfigurationManager _conf = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TurfSystem _turf = default!;
private readonly Dictionary<NetEntity, HashSet<Vector2i>> _dirtyChunks = new();
private readonly Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> _previousSentChunks = new();
private static readonly Vector2 _boundsMinExpansion = new(0.01f, 0.01f);
private static readonly Vector2 _boundsMaxExpansion = new(1.01f, 1.01f);
private UpdatePlayerJob _updateJob;
private List<ICommonSession> _sessions = new();
// If this ever gets parallelised then you'll want to increase the pooled count.
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);
public override void Initialize()
{
base.Initialize();
_updateJob = new UpdatePlayerJob()
{
System = this,
Sessions = _sessions,
};
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);
SubscribeNetworkEvent<RequestDecalPlacementEvent>(OnDecalPlacementRequest);
SubscribeNetworkEvent<RequestDecalRemovalEvent>(OnDecalRemovalRequest);
SubscribeLocalEvent<PostGridSplitEvent>(OnGridSplit);
Subs.CVar(_conf, CVars.NetPVS, OnPvsToggle, true);
}
private void OnPvsToggle(bool value)
{
if (value == PvsEnabled)
return;
PvsEnabled = value;
if (value)
return;
foreach (var playerData in _previousSentChunks.Values)
{
playerData.Clear();
}
var query = AllEntityQuery<DecalGridComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var grid, out var meta))
{
grid.ForceTick = _timing.CurTick;
Dirty(uid, grid, meta);
}
}
private void OnGridSplit(ref PostGridSplitEvent ev)
{
if (!TryComp(ev.OldGrid, out DecalGridComponent? oldComp))
return;
if (!TryComp(ev.Grid, out DecalGridComponent? newComp))
return;
// Transfer decals over to the new grid.
var enumerator = _mapSystem.GetAllTilesEnumerator(ev.Grid, Comp<MapGridComponent>(ev.Grid));
var oldChunkCollection = oldComp.ChunkCollection.ChunkCollection;
var chunkCollection = newComp.ChunkCollection.ChunkCollection;
while (enumerator.MoveNext(out var tile))
{
var tilePos = (Vector2) tile.Value.GridIndices;
var chunkIndices = GetChunkIndices(tilePos);
if (!oldChunkCollection.TryGetValue(chunkIndices, out var oldChunk))
continue;
var bounds = new Box2(tilePos - _boundsMinExpansion, tilePos + _boundsMaxExpansion);
var toRemove = new RemQueue<uint>();
foreach (var (oldDecalId, decal) in oldChunk.Decals)
{
if (!bounds.Contains(decal.Coordinates))
continue;
var newDecalId = newComp.ChunkCollection.NextDecalId++;
var newChunk = chunkCollection.GetOrNew(chunkIndices);
newChunk.Decals[newDecalId] = decal;
newComp.DecalIndex[newDecalId] = chunkIndices;
toRemove.Add(oldDecalId);
}
foreach (var oldDecalId in toRemove)
{
oldChunk.Decals.Remove(oldDecalId);
oldComp.DecalIndex.Remove(oldDecalId);
}
DirtyChunk(ev.Grid, chunkIndices, chunkCollection.GetOrNew(chunkIndices));
if (oldChunk.Decals.Count == 0)
oldChunkCollection.Remove(chunkIndices);
if (toRemove.List?.Count > 0)
DirtyChunk(ev.OldGrid, chunkIndices, oldChunk);
}
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnTileChanged(ref TileChangedEvent args)
{
if (!TryComp(args.Entity, out DecalGridComponent? grid))
return;
var toDelete = new HashSet<uint>();
foreach (var change in args.Changes)
{
if (!_turf.IsSpace(change.NewTile))
continue;
var indices = GetChunkIndices(change.GridIndices);
if (!grid.ChunkCollection.ChunkCollection.TryGetValue(indices, out var chunk))
continue;
toDelete.Clear();
foreach (var (uid, decal) in chunk.Decals)
{
if (new Vector2((int)Math.Floor(decal.Coordinates.X), (int)Math.Floor(decal.Coordinates.Y)) ==
change.GridIndices)
{
toDelete.Add(uid);
}
}
if (toDelete.Count == 0)
continue;
foreach (var decalId in toDelete)
{
grid.DecalIndex.Remove(decalId);
chunk.Decals.Remove(decalId);
}
DirtyChunk(args.Entity, indices, chunk);
if (chunk.Decals.Count == 0)
grid.ChunkCollection.ChunkCollection.Remove(indices);
}
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
switch (e.NewStatus)
{
case SessionStatus.InGame:
_previousSentChunks[e.Session] = new();
break;
case SessionStatus.Disconnected:
_previousSentChunks.Remove(e.Session);
break;
}
}
private void OnDecalPlacementRequest(RequestDecalPlacementEvent ev, EntitySessionEventArgs eventArgs)
{
if (eventArgs.SenderSession is not { } session)
return;
// bad
if (!_adminManager.HasAdminFlag(session, AdminFlags.Spawn))
return;
var coordinates = GetCoordinates(ev.Coordinates);
if (!coordinates.IsValid(EntityManager))
return;
if (!TryAddDecal(ev.Decal, coordinates, out _))
return;
if (eventArgs.SenderSession.AttachedEntity != null)
{
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low,
$"{ToPrettyString(eventArgs.SenderSession.AttachedEntity.Value):actor} drew a {ev.Decal.Color} {ev.Decal.Id} at {ev.Coordinates}");
}
else
{
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low,
$"{eventArgs.SenderSession.Name} drew a {ev.Decal.Color} {ev.Decal.Id} at {ev.Coordinates}");
}
}
private void OnDecalRemovalRequest(RequestDecalRemovalEvent ev, EntitySessionEventArgs eventArgs)
{
if (eventArgs.SenderSession is not { } session)
return;
// bad
if (!_adminManager.HasAdminFlag(session, AdminFlags.Spawn))
return;
var coordinates = GetCoordinates(ev.Coordinates);
if (!coordinates.IsValid(EntityManager))
return;
var gridId = _transform.GetGrid(coordinates);
if (gridId == null)
return;
// remove all decals on the same tile
foreach (var (decalId, decal) in GetDecalsInRange(gridId.Value, ev.Coordinates.Position))
{
if (eventArgs.SenderSession.AttachedEntity != null)
{
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low,
$"{ToPrettyString(eventArgs.SenderSession.AttachedEntity.Value):actor} removed a {decal.Color} {decal.Id} at {ev.Coordinates}");
}
else
{
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low,
$"{eventArgs.SenderSession.Name} removed a {decal.Color} {decal.Id} at {ev.Coordinates}");
}
RemoveDecal(gridId.Value, decalId);
}
}
protected override void DirtyChunk(EntityUid uid, Vector2i chunkIndices, DecalChunk chunk)
{
var id = GetNetEntity(uid);
chunk.LastModified = _timing.CurTick;
if(!_dirtyChunks.ContainsKey(id))
_dirtyChunks[id] = new HashSet<Vector2i>();
_dirtyChunks[id].Add(chunkIndices);
}
public bool TryAddDecal(string id, EntityCoordinates coordinates, out uint decalId, Color? color = null, Angle? rotation = null, int zIndex = 0, bool cleanable = false)
{
rotation ??= Angle.Zero;
var decal = new Decal(coordinates.Position, id, color, rotation.Value, zIndex, cleanable);
return TryAddDecal(decal, coordinates, out decalId);
}
public bool TryAddDecal(Decal decal, EntityCoordinates coordinates, out uint decalId)
{
decalId = 0;
if (!PrototypeManager.HasIndex<DecalPrototype>(decal.Id))
return false;
var gridId = _transform.GetGrid(coordinates);
if (!TryComp(gridId, out MapGridComponent? grid))
return false;
if (_turf.IsSpace(_mapSystem.GetTileRef(gridId.Value, grid, coordinates)))
return false;
if (!TryComp(gridId, out DecalGridComponent? comp))
return false;
decalId = comp.ChunkCollection.NextDecalId++;
var chunkIndices = GetChunkIndices(decal.Coordinates);
var chunk = comp.ChunkCollection.ChunkCollection.GetOrNew(chunkIndices);
chunk.Decals[decalId] = decal;
comp.DecalIndex[decalId] = chunkIndices;
DirtyChunk(gridId.Value, chunkIndices, chunk);
return true;
}
public override bool RemoveDecal(EntityUid gridId, uint decalId, DecalGridComponent? component = null)
=> RemoveDecalInternal(gridId, decalId, out _, component);
public override HashSet<(uint Index, Decal Decal)> GetDecalsInRange(EntityUid gridId, Vector2 position, float distance = 0.75f, Func<Decal, bool>? validDelegate = null)
{
var decalIds = new HashSet<(uint, Decal)>();
var chunkCollection = ChunkCollection(gridId);
var chunkIndices = GetChunkIndices(position);
if (chunkCollection == null || !chunkCollection.TryGetValue(chunkIndices, out var chunk))
return decalIds;
foreach (var (uid, decal) in chunk.Decals)
{
if ((position - decal.Coordinates - new Vector2(0.5f, 0.5f)).Length() > distance)
continue;
if (validDelegate == null || validDelegate(decal))
{
decalIds.Add((uid, decal));
}
}
return decalIds;
}
public HashSet<(uint Index, Decal Decal)> GetDecalsIntersecting(EntityUid gridUid, Box2 bounds, DecalGridComponent? component = null)
{
var decalIds = new HashSet<(uint, Decal)>();
var chunkCollection = ChunkCollection(gridUid, component);
if (chunkCollection == null)
return decalIds;
var chunks = new ChunkIndicesEnumerator(bounds, ChunkSize);
while (chunks.MoveNext(out var chunkOrigin))
{
if (!chunkCollection.TryGetValue(chunkOrigin.Value, out var chunk))
continue;
foreach (var (id, decal) in chunk.Decals)
{
if (!bounds.Contains(decal.Coordinates))
continue;
decalIds.Add((id, decal));
}
}
return decalIds;
}
/// <summary>
/// Changes a decals position. Note this will actually result in a new decal being created, possibly on a new grid or chunk.
/// </summary>
/// <remarks>
/// If the new position is invalid, this will result in the decal getting deleted.
/// </remarks>
public bool SetDecalPosition(EntityUid gridId, uint decalId, EntityCoordinates coordinates, DecalGridComponent? comp = null)
{
if (!Resolve(gridId, ref comp))
return false;
if (!RemoveDecalInternal(gridId, decalId, out var removed, comp))
return false;
return TryAddDecal(removed.WithCoordinates(coordinates.Position), coordinates, out _);
}
private bool ModifyDecal(EntityUid gridId, uint decalId, Func<Decal, Decal> modifyDecal, DecalGridComponent? comp = null)
{
if (!Resolve(gridId, ref comp))
return false;
if (!comp.DecalIndex.TryGetValue(decalId, out var indices))
return false;
var chunk = comp.ChunkCollection.ChunkCollection[indices];
var decal = chunk.Decals[decalId];
chunk.Decals[decalId] = modifyDecal(decal);
DirtyChunk(gridId, indices, chunk);
return true;
}
public bool SetDecalColor(EntityUid gridId, uint decalId, Color? value, DecalGridComponent? comp = null)
=> ModifyDecal(gridId, decalId, x => x.WithColor(value), comp);
public bool SetDecalRotation(EntityUid gridId, uint decalId, Angle value, DecalGridComponent? comp = null)
=> ModifyDecal(gridId, decalId, x => x.WithRotation(value), comp);
public bool SetDecalZIndex(EntityUid gridId, uint decalId, int value, DecalGridComponent? comp = null)
=> ModifyDecal(gridId, decalId, x => x.WithZIndex(value), comp);
public bool SetDecalCleanable(EntityUid gridId, uint decalId, bool value, DecalGridComponent? comp = null)
=> ModifyDecal(gridId, decalId, x => x.WithCleanable(value), comp);
public bool SetDecalId(EntityUid gridId, uint decalId, string id, DecalGridComponent? comp = null)
{
if (!PrototypeManager.HasIndex<DecalPrototype>(id))
throw new ArgumentOutOfRangeException($"Tried to set decal id to invalid prototypeid: {id}");
return ModifyDecal(gridId, decalId, x => x.WithId(id), comp);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var ent in _dirtyChunks.Keys)
{
if (TryGetEntity(ent, out var uid) && TryComp(uid, out DecalGridComponent? decals))
Dirty(uid.Value, decals);
}
if (!PvsEnabled)
{
_dirtyChunks.Clear();
return;
}
if (PvsEnabled)
{
_sessions.Clear();
foreach (var session in _playerManager.Sessions)
{
if (session.Status != SessionStatus.InGame)
continue;
_sessions.Add(session);
}
if (_sessions.Count > 0)
_parMan.ProcessNow(_updateJob, _sessions.Count);
}
_dirtyChunks.Clear();
}
public void UpdatePlayer(ICommonSession player)
{
var chunksInRange = _chunking.GetChunksForSession(player, ChunkSize, _chunkIndexPool, _chunkViewerPool);
var staleChunks = _chunkViewerPool.Get();
var previouslySent = _previousSentChunks[player];
// Get any chunks not in range anymore
// Then, remove them from previousSentChunks (for stuff like grids out of range)
// and also mark them as stale for networking.
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);
// Was the grid deleted?
if (TryGetEntity(netGrid, out var gridId) && HasComp<MapGridComponent>(gridId.Value))
{
// no -> add it to the list of stale chunks
staleChunks[netGrid] = oldIndices;
}
else
{
// If the grid was deleted then don't worry about telling the client to delete the chunk.
oldIndices.Clear();
_chunkIndexPool.Return(oldIndices);
}
continue;
}
var elmo = _chunkIndexPool.Get();
// Get individual stale chunks.
foreach (var chunk in oldIndices)
{
if (chunks.Contains(chunk))
continue;
elmo.Add(chunk);
}
if (elmo.Count == 0)
{
_chunkIndexPool.Return(elmo);
continue;
}
staleChunks.Add(netGrid, elmo);
}
var updatedChunks = _chunkViewerPool.Get();
foreach (var (netGrid, gridChunks) in chunksInRange)
{
var newChunks = _chunkIndexPool.Get();
_dirtyChunks.TryGetValue(netGrid, out var dirtyChunks);
if (!previouslySent.TryGetValue(netGrid, out var previousChunks))
newChunks.UnionWith(gridChunks);
else
{
foreach (var index in gridChunks)
{
if (!previousChunks.Contains(index) || dirtyChunks != null && dirtyChunks.Contains(index))
newChunks.Add(index);
}
previousChunks.Clear();
_chunkIndexPool.Return(previousChunks);
}
previouslySent[netGrid] = gridChunks;
if (newChunks.Count == 0)
_chunkIndexPool.Return(newChunks);
else
updatedChunks[netGrid] = newChunks;
}
//send all gridChunks to client
SendChunkUpdates(player, updatedChunks, staleChunks);
}
private void ReturnToPool(Dictionary<NetEntity, HashSet<Vector2i>> chunks)
{
foreach (var (_, previous) in chunks)
{
previous.Clear();
_chunkIndexPool.Return(previous);
}
chunks.Clear();
_chunkViewerPool.Return(chunks);
}
private void SendChunkUpdates(
ICommonSession session,
Dictionary<NetEntity, HashSet<Vector2i>> updatedChunks,
Dictionary<NetEntity, HashSet<Vector2i>> staleChunks)
{
var updatedDecals = new Dictionary<NetEntity, Dictionary<Vector2i, DecalChunk>>();
foreach (var (netGrid, chunks) in updatedChunks)
{
var gridId = GetEntity(netGrid);
var collection = ChunkCollection(gridId);
if (collection == null)
continue;
var gridChunks = new Dictionary<Vector2i, DecalChunk>();
foreach (var indices in chunks)
{
gridChunks.Add(indices,
collection.TryGetValue(indices, out var chunk)
? chunk
: new());
}
updatedDecals[netGrid] = gridChunks;
}
if (updatedDecals.Count != 0 || staleChunks.Count != 0)
RaiseNetworkEvent(new DecalChunkUpdateEvent{Data = updatedDecals, RemovedChunks = staleChunks}, session);
ReturnToPool(updatedChunks);
ReturnToPool(staleChunks);
}
#region Jobs
/// <summary>
/// Updates per-player data for decals.
/// </summary>
private record struct UpdatePlayerJob : IParallelRobustJob
{
public int BatchSize => 2;
public DecalSystem System;
public List<ICommonSession> Sessions;
public void Execute(int index)
{
System.UpdatePlayer(Sessions[index]);
}
}
#endregion
}
}