Files
wwdpublic/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs
SimpleStation14 b1e3b79253 Mirror: Obsolete Logger cleanup for EntitySystems (#132)
## Mirror of PR #25941: [Obsolete `Logger` cleanup for
`EntitySystem`s](https://github.com/space-wizards/space-station-14/pull/25941)
from <img src="https://avatars.githubusercontent.com/u/10567778?v=4"
alt="space-wizards" width="22"/>
[space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14)

###### `aafe81512258b5a80776ada1f471b58e7507ca2d`

PR opened by <img
src="https://avatars.githubusercontent.com/u/27449516?v=4"
width="16"/><a href="https://github.com/LordCarve"> LordCarve</a> at
2024-03-09 12:19:14 UTC
PR merged by <img
src="https://avatars.githubusercontent.com/u/19864447?v=4"
width="16"/><a href="https://github.com/web-flow"> web-flow</a> at
2024-03-10 00:15:13 UTC

---

PR changed 25 files with 41 additions and 45 deletions.

The PR had the following labels:
- Status: Needs Review


---

<details open="true"><summary><h1>Original Body</h1></summary>

> <!-- Please read these guidelines before opening your PR:
https://docs.spacestation14.io/en/getting-started/pr-guideline -->
> <!-- The text between the arrows are comments - they will not be
visible on your PR. -->
> 
> ## About the PR
> <!-- What did you change in this PR? -->
> Changed almost all[^1] obsolete `Logger` calls in Content's
`EntitySystem`s to use `Log` instead. `Log` automatically selects the
system-specific `Sawmill`, so this isn't just a refactor - it makes logs
slightly more accurate (puts them in appropriate sawmill/context rather
than the root sawmill).
> 
> ## Why / Balance
> <!-- Why was it changed? Link any discussions or issues here. Please
discuss how this would affect game balance. -->
> Using `Logger` directly for logging is marked obsolete. Assumed this
is a desired change.
> 
> ## Technical details
> <!-- If this is a code change, summarize at high level how your new
code works. This makes it easier to review. -->
> This changes some log contexts, but generally in a desirable way:
> - For most, it put logs in `system.appropriate` `Sawmill` rather than
root.
> - For some that were forced into another `Sawmill` it changes it to a
more specific one, i.e. from `atmos` it becomes `system.gas_filter` or
`system.automatic_atmos` and `system.station` into
`system.station_jobs`.
> - For the rest it remains unchanged
> 
> **I assumed that all of the above was desirable because this seems to
be the standard convention** - I imagine that was the idea behind the
`Log` in all `EntitySystem`s coupled with `[Obsolete] Logger` methods -
**but if my assumptions are incorrect and/or there are exceptions,
please let me know and I will adjust.**
> 
> [^1]: There is only one `EntitySystem` that I didn't update -
`ExamineSystemShared`. That is because the `Logger` is in a `static`
method and refactoring that away causes a lot of cascading changes. May
do it as a separate PR to avoid overly diluting this one.
> 
> ## Media
> <!-- 
> PRs which make ingame changes (adding clothing, items, new features,
etc) are required to have media attached that showcase the changes.
> Small fixes/refactors are exempt.
> Any media may be used in SS14 progress reports, with clear credit
given.
> 
> If you're unsure whether your PR will require media, ask a maintainer.
> 
> Check the box below to confirm that you have in fact seen this (put an
X in the brackets, like [X]):
> -->
> 
> - [X] I have added screenshots/videos to this PR showcasing its
changes ingame, **or** this PR does not require an ingame showcase
> 
> ## Breaking changes
> <!--
> List any breaking changes, including namespace, public
class/method/field changes, prototype renames; and provide instructions
for fixing them. This will be pasted in #codebase-changes.
> -->
> Log output changes. `EntitySystem` logs now go to the expected
`Sawmill` for the system rather than hardcoded/root one.
> Any log analyzers anticipating these logs' context must be updated.


</details>

Co-authored-by: LordCarve <27449516+LordCarve@users.noreply.github.com>
2024-05-04 19:54:48 -04:00

353 lines
15 KiB
C#

using System.Linq;
using System.Numerics;
using Content.Shared.Administration;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
// This partial part of the explosion system has all of the functions used to create the actual explosion map.
// I.e, to get the sets of tiles & intensity values that describe an explosion.
public sealed partial class ExplosionSystem : EntitySystem
{
/// <summary>
/// This is the main explosion generating function.
/// </summary>
/// <param name="epicenter">The center of the explosion</param>
/// <param name="typeID">The explosion type. this determines the explosion damage</param>
/// <param name="totalIntensity">The final sum of the tile intensities. This governs the overall size of the
/// explosion</param>
/// <param name="slope">How quickly does the intensity decrease when moving away from the epicenter.</param>
/// <param name="maxIntensity">The maximum intensity that the explosion can have at any given tile. This
/// effectively caps the damage that this explosion can do.</param>
/// <returns>A list of tile-sets and a list of intensity values which describe the explosion.</returns>
private (int, List<float>, ExplosionSpaceTileFlood?, Dictionary<EntityUid, ExplosionGridTileFlood>, Matrix3)? GetExplosionTiles(
MapCoordinates epicenter,
string typeID,
float totalIntensity,
float slope,
float maxIntensity)
{
if (totalIntensity <= 0 || slope <= 0)
return null;
if (!_explosionTypes.TryGetValue(typeID, out var typeIndex))
{
Log.Error("Attempted to spawn explosion using a prototype that was not defined during initialization. Explosion prototype hot-reload is not currently supported.");
return null;
}
Vector2i initialTile;
EntityUid? epicentreGrid = null;
var (localGrids, referenceGrid, maxDistance) = GetLocalGrids(epicenter, totalIntensity, slope, maxIntensity);
// get the epicenter tile indices
if (_mapManager.TryFindGridAt(epicenter, out var gridUid, out var candidateGrid) &&
candidateGrid.TryGetTileRef(candidateGrid.WorldToTile(epicenter.Position), out var tileRef) &&
!tileRef.Tile.IsEmpty)
{
epicentreGrid = gridUid;
initialTile = tileRef.GridIndices;
}
else if (referenceGrid != null)
{
// reference grid defines coordinate system that the explosion in space will use
initialTile = _mapManager.GetGrid(referenceGrid.Value).WorldToTile(epicenter.Position);
}
else
{
// this is a space-based explosion that (should) not touch any grids.
initialTile = new Vector2i(
(int) Math.Floor(epicenter.Position.X / DefaultTileSize),
(int) Math.Floor(epicenter.Position.Y / DefaultTileSize));
}
// Main data for the exploding tiles in space and on various grids
Dictionary<EntityUid, ExplosionGridTileFlood> gridData = new();
ExplosionSpaceTileFlood? spaceData = null;
// The intensity slope is how much the intensity drop over a one-tile distance. The actual algorithm step-size is half of thhat.
var stepSize = slope / 2;
// Hashsets used for when grid-based explosion propagate into space. Basically: used to move data between
// `gridData` and `spaceData` in-between neighbor finding iterations.
HashSet<Vector2i> spaceJump = new();
HashSet<Vector2i> previousSpaceJump;
// As above, but for space-based explosion propagating from space onto grids.
HashSet<EntityUid> encounteredGrids = new();
Dictionary<EntityUid, HashSet<Vector2i>>? previousGridJump;
// variables for transforming between grid and space-coordinates
var spaceMatrix = Matrix3.Identity;
var spaceAngle = Angle.Zero;
if (referenceGrid != null)
{
var xform = Transform(_mapManager.GetGrid(referenceGrid.Value).Owner);
spaceMatrix = xform.WorldMatrix;
spaceAngle = xform.WorldRotation;
}
// is the explosion starting on a grid?
if (epicentreGrid != null)
{
// set up the initial `gridData` instance
encounteredGrids.Add(epicentreGrid.Value);
if (!_airtightMap.TryGetValue(epicentreGrid.Value, out var airtightMap))
airtightMap = new();
var initialGridData = new ExplosionGridTileFlood(
_mapManager.GetGrid(epicentreGrid.Value),
airtightMap,
maxIntensity,
stepSize,
typeIndex,
_gridEdges[epicentreGrid.Value],
referenceGrid,
spaceMatrix,
spaceAngle);
gridData[epicentreGrid.Value] = initialGridData;
initialGridData.InitTile(initialTile);
}
else
{
// set up the space explosion data
spaceData = new ExplosionSpaceTileFlood(this, epicenter, referenceGrid, localGrids, maxDistance);
spaceData.InitTile(initialTile);
}
// Is this even a multi-tile explosion?
if (totalIntensity < stepSize)
// Bit anticlimactic. All that set up for nothing....
return (1, new List<float> { totalIntensity }, spaceData, gridData, spaceMatrix);
// These variables keep track of the total intensity we have distributed
List<int> tilesInIteration = new() { 1 };
List<float> iterationIntensity = new() {stepSize};
var totalTiles = 1;
var remainingIntensity = totalIntensity - stepSize;
var iteration = 1;
var maxIntensityIndex = 0;
// If an explosion is trapped in an indestructible room, we can end the neighbor finding steps early.
// These variables are used to check if we can abort early.
float previousIntensity;
var intensityUnchangedLastLoop = false;
// Main flood-fill / neighbor-finding loop
while (remainingIntensity > 0 && iteration <= MaxIterations && totalTiles < MaxArea)
{
previousIntensity = remainingIntensity;
// First, we increase the intensity of the tiles that were already discovered in previous iterations.
for (var i = maxIntensityIndex; i < iteration; i++)
{
var intensityIncrease = MathF.Min(stepSize, maxIntensity - iterationIntensity[i]);
if (tilesInIteration[i] * intensityIncrease >= remainingIntensity)
{
// there is not enough intensity left to distribute. add a fractional amount and break.
iterationIntensity[i] += remainingIntensity / tilesInIteration[i];
remainingIntensity = 0;
break;
}
iterationIntensity[i] += intensityIncrease;
remainingIntensity -= tilesInIteration[i] * intensityIncrease;
// Has this tile-set has reached max intensity? If so, stop iterating over it in future
if (intensityIncrease < stepSize)
maxIntensityIndex++;
}
if (remainingIntensity <= 0) break;
// Next, we will add a new iteration of tiles
// In order to treat "cost" of moving off a grid on the same level as moving onto a grid, both space -> grid and grid -> space have to be delayed by one iteration.
previousSpaceJump = spaceJump;
previousGridJump = spaceData?.GridJump;
spaceJump = new();
var newTileCount = 0;
if (previousGridJump != null)
encounteredGrids.UnionWith(previousGridJump.Keys);
foreach (var grid in encounteredGrids)
{
// is this a new grid, for which we must create a new explosion data set
if (!gridData.TryGetValue(grid, out var data))
{
if (!_airtightMap.TryGetValue(grid, out var airtightMap))
airtightMap = new();
data = new ExplosionGridTileFlood(
_mapManager.GetGrid(grid),
airtightMap,
maxIntensity,
stepSize,
typeIndex,
_gridEdges[grid],
referenceGrid,
spaceMatrix,
spaceAngle);
gridData[grid] = data;
}
// get the new neighbours, and populate gridToSpaceTiles in the process.
newTileCount += data.AddNewTiles(iteration, previousGridJump?.GetValueOrDefault(grid));
spaceJump.UnionWith(data.SpaceJump);
}
// if space-data is null, but some grid-based explosion reached space, we need to initialize it.
if (spaceData == null && previousSpaceJump.Count != 0)
spaceData = new ExplosionSpaceTileFlood(this, epicenter, referenceGrid, localGrids, maxDistance);
// If the explosion has reached space, do that neighbors finding step as well.
if (spaceData != null)
newTileCount += spaceData.AddNewTiles(iteration, previousSpaceJump);
// Does adding these tiles bring us above the total target intensity?
tilesInIteration.Add(newTileCount);
if (newTileCount * stepSize >= remainingIntensity)
{
iterationIntensity.Add(remainingIntensity / newTileCount);
break;
}
// add the new tiles and decrement available intensity
remainingIntensity -= newTileCount * stepSize;
iterationIntensity.Add(stepSize);
totalTiles += newTileCount;
// It is possible that the explosion has some max intensity and is stuck in a container whose walls it
// cannot break. if the remaining intensity remains unchanged TWO loops in a row, we know that this is the
// case.
if (intensityUnchangedLastLoop && remainingIntensity == previousIntensity)
break;
intensityUnchangedLastLoop = remainingIntensity == previousIntensity;
iteration += 1;
}
// Neighbor finding is done. Perform final clean up and return.
foreach (var grid in gridData.Values)
{
grid.CleanUp();
}
spaceData?.CleanUp();
return (totalTiles, iterationIntensity, spaceData, gridData, spaceMatrix);
}
/// <summary>
/// Look for grids in an area and returns them. Also selects a special grid that will be used to determine the
/// orientation of an explosion in space.
/// </summary>
/// <remarks>
/// Note that even though an explosion may start ON a grid, the explosion in space may still be orientated to
/// match a separate grid. This is done so that if you have something like a tiny suicide-bomb shuttle exploding
/// near a large station, the explosion will still orient to match the station, not the tiny shuttle.
/// </remarks>
public (List<EntityUid>, EntityUid?, float) GetLocalGrids(MapCoordinates epicenter, float totalIntensity, float slope, float maxIntensity)
{
// Get the explosion radius (approx radius if it were in open-space). Note that if the explosion is confined in
// some directions but not in others, the actual explosion may reach further than this distance from the
// epicenter. Conversely, it might go nowhere near as far.
var radius = 0.5f + IntensityToRadius(totalIntensity, slope, maxIntensity);
// to avoid a silly lookup for silly input numbers, cap the radius to half of the theoretical maximum (lookup area gets doubled later on).
radius = Math.Min(radius, MaxIterations / 4);
EntityUid? referenceGrid = null;
float mass = 0;
// First attempt to find a grid that is relatively close to the explosion's center. Instead of looking in a
// diameter x diameter sized box, use a smaller box with radius sized sides:
var box = Box2.CenteredAround(epicenter.Position, new Vector2(radius, radius));
foreach (var grid in _mapManager.FindGridsIntersecting(epicenter.MapId, box))
{
if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.Mass > mass)
{
mass = physics.Mass;
referenceGrid = grid.Owner;
}
}
// Next, we use a much larger lookup to determine all grids relevant to the explosion. This is used to determine
// what grids should be included during the grid-edge transformation steps. This means that if a grid is not in
// this set, the explosion can never propagate from space onto this grid.
// As mentioned before, the `diameter` is only indicative, as an explosion that is obstructed (e.g., in a
// tunnel) may travel further away from the epicenter. But this should be very rare for space-traversing
// explosions. So instead of using the largest possible distance that an explosion could theoretically travel
// and using that for the grid look-up, we will just arbitrarily fudge the lookup size to be twice the diameter.
radius *= 4;
box = Box2.CenteredAround(epicenter.Position, new Vector2(radius, radius));
var mapGrids = _mapManager.FindGridsIntersecting(epicenter.MapId, box).ToList();
var grids = mapGrids.Select(x => x.Owner).ToList();
if (referenceGrid != null)
return (grids, referenceGrid, radius);
// We still don't have are reference grid. So lets also look in the enlarged region
foreach (var grid in mapGrids)
{
if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.Mass > mass)
{
mass = physics.Mass;
referenceGrid = grid.Owner;
}
}
return (grids, referenceGrid, radius);
}
public ExplosionVisualsState? GenerateExplosionPreview(SpawnExplosionEuiMsg.PreviewRequest request)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
var results = GetExplosionTiles(
request.Epicenter,
request.TypeId,
request.TotalIntensity,
request.IntensitySlope,
request.MaxIntensity);
if (results == null)
return null;
var (area, iterationIntensity, spaceData, gridData, spaceMatrix) = results.Value;
Log.Info($"Generated explosion preview with {area} tiles in {stopwatch.Elapsed.TotalMilliseconds}ms");
Dictionary<NetEntity, Dictionary<int, List<Vector2i>>> tileLists = new();
foreach (var (grid, data) in gridData)
{
tileLists.Add(GetNetEntity(grid), data.TileLists);
}
return new ExplosionVisualsState(
request.Epicenter,
request.TypeId,
iterationIntensity,
spaceData?.TileLists,
tileLists, spaceMatrix,
spaceData?.TileSize ?? DefaultTileSize
);
}
}