mirror of
https://github.com/DeltaV-Station/Delta-v.git
synced 2026-09-29 23:57:29 +00:00
+24









Debug
deltanedas
metalgearsloth
Nemanja
PJBot
Psychpsyo
deltanedas
metalgearsloth
ravage
Pieter-Jan Briers
LEVELcat
Repo
nikthechampiongr
Doru991
DrSmugleaf
Skarletto
NULL882
lunarcomets
Miro Kavaliou
Kara
Kacper Urbańczyk
TsjipTsjip
daerSeebaer
Fluffiest Floofers
Flareguy
Leon Friedrich
Kevin Zheng
drteaspoon420
Ubaser
Nim
Kacper Urbańczyk
Tox Cruize
Ed
Michael Cu
fd2cc8570d
* Automatic changelog update * Make NPC names proper nouns & fix some genders (#20534) * Proper names & genders * Uppercase proper names * Make Smile female * fix bingus wrinkly head (#20531) * rouny meat and steak (#20526) * lost friendship flavor * add rouny steak * rouny special meat * rouny meat textures --------- Co-authored-by: deltanedas <@deltanedas:kde.org> * Automatic changelog update * Wearable Wet Floor Sign and 'Janitorial Suicide Vest' (#20311) * Explosive wet floor sign & janitorial suicide vest * fix attributions * Remove name & desc from explosive wet floor sign * Make wet floor sign chameleonable * Automatic changelog update * Minor slippery stuff (#20535) * Update submodule to 162.2.0 (#20570) * Predicted armor (#20560) * clean up some lines in smile the slime prototype (#20552) * Revert "Use full file path for temp replays (#19002)" (#20545) * Add EyesGlasses into ClothesMate (#20523) * Automatic changelog update * Fix Punpun crew monitor sensor (#20484) * Automatic changelog update * EasyPry airlocks for arrivals. Now also prying refactor I guess (#19394) Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Automatic changelog update * Make arcades hackable again (#20555) * Automatic changelog update * Health alert tweaks (#20557) * Automatic changelog update * Add active and ended game rule components, generic TryRoundStartAttempt and minPlayers field (#20564) * Improve active game rule querying, add generic try round start attempt method, move minPlayers to GameRuleComponent * Nukeops todo and cleanup * Remove Active field * Add EndedGameRuleComponent * bartender suit (#20521) * give me a drink bartender * guh * aARG * Necropolis and mine walls (#20578) * Automatic changelog update * archaic accent tweaks (#20567) * Automatic changelog update * Add confirmation to kick and respawn in the admin player actions panel (#20542) * Remove windows-latest CI runs, remove unused build-test-release.yml (#20540) * Change .editorconfig to keep existing attribute arrangement (#20538) * Automatic changelog update * Catch replay start and end errors on round restarts (#20565) * Update trivial components to use auto comp states (#20539) * Fix role unbans not applying in real time (#20547) * Disable AHelp buttons when no player is selected, update button styling (#20568) * Automatic changelog update * Added a toggle fullscreen button (default F11) (#20272) * Added a toggle fullscreen button (default F11) * Removed un-needed comments * Review Requested Changes * Fixed Acidental Spacing Change * bwoink, removed extraneous code * nothing, litterally * Automatic changelog update * Fix not networking markings (#20588) * Automatic changelog update * Update RobustToolbox to v162.2.1 (#20590) * Automatic changelog update * Glorfcode (force say on damage/stun/crit) (#20562) * Automatic changelog update * Fix AHelp progressively showing more AHelp panels (#20591) * Automatic changelog update * caninsert entitystorage tweaks (#20589) * Automatic changelog update * Fix mapping actions crashing on load (#20592) * Fix loading a map with a ThirstComponent crashing the game (#20594) * Fix electrocution displaying real name of disguised players (#20603) * Organ fixes (#20488) * Automatic changelog update * Kettle medical rework (#20435) * Initial try at medical rework for kettle * Re-add decal decorations to Kettle's medical * Add more lockers to kettle's med * Actually upload kettle's map, not its proto * Automatic changelog update * Added blocked visuals to volumetric pump (#20610) * Automatic changelog update * Move TimedDespawn to engine (#20515) * Update submodule to 163.0.0 (#20616) * Remove v0.1 version number from local main menu screen (#20617) * Remove cloneData parameter from AutoNetworkedField (#20596) * Update submodule to 164.0.0 (#20618) * fix cognizine ghost role (#20632) Co-authored-by: deltanedas <@deltanedas:kde.org> * Automatic changelog update * Move ID layer one pixel to correct alignment (#20630) * Update nukie hardsuit descriptions (#20529) * Make holofans destructable (#20445) * Automatic changelog update * bowl is open (#20453) Co-authored-by: deltanedas <@deltanedas:kde.org> * Add TestPair.WaitCommand() (#20615) * Rename ThreatPrototype and mark fields as required (#20611) * Adjust hard bomb shape (#20608) * dragon refactor, objectives and use GenericAntag (#20201) Co-authored-by: deltanedas <@deltanedas:kde.org> * Automatic changelog update * Added generic empty liquids tank (#20563) * Move view variables verb to the top of the list with no category and localize it (#20546) * saltern update (#20325) Co-authored-by: deltanedas <@deltanedas:kde.org> * Fix followers leaking (#20643) * Add Winter Boots (#20622) * add * fix prototype * Add QM mantle (#20621) * add * fix prototype * Automatic changelog update * Slime mobs breathe nitrogen and resprite their organs (#20577) * Slimes breathe nitrogen and resprite their organs * ups * mmm * Automatic changelog update * Fix the new lizard horn's consistency + issue (#20620) * fix * add * Revert "add" This reverts commit a054a3204a8f185a94ceb80b1bd3bc9f30423711. * Add RandomHumanoidAppearance component to for space ninjas (#20605) * Automatic changelog update * Space cat breathes space (#20550) * Space cat breathes space Made Space Cat lungs ROBUST * Made Space Cat's lungs ROBUST 2.0 * Automatic changelog update * Wide anomaly locator (#20581) * Add files via upload * Add files via upload * Add files via upload * Add files via upload * add textures * fix encoding * fix 2 * Automatic changelog update * Fixed Telescopic Shield Lighting (#20650) * Fixed Telescopic Shield Lighting Bug fix #20199 * no need for these at all --------- Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * Automatic changelog update * Add a special hardsuit for nukie medic (#20471) * Automatic changelog update * Carry over other mutations when doing species mutation (#20551) * Automatic changelog update * Add Spanish accent to poncho and sombrero (#20377) --------- Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> Co-authored-by: Psychpsyo <60073468+Psychpsyo@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Co-authored-by: ravage <142820619+ravage123321@users.noreply.github.com> Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com> Co-authored-by: LEVELcat <68501903+LEVELcat@users.noreply.github.com> Co-authored-by: Repo <47093363+Titian3@users.noreply.github.com> Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> Co-authored-by: Doru991 <75124791+Doru991@users.noreply.github.com> Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com> Co-authored-by: Skarletto <122584947+Skarletto@users.noreply.github.com> Co-authored-by: NULL882 <104377798+NULL882@users.noreply.github.com> Co-authored-by: lunarcomets <140772713+lunarcomets@users.noreply.github.com> Co-authored-by: Miro Kavaliou <miraslauk@gmail.com> Co-authored-by: Kara <lunarautomaton6@gmail.com> Co-authored-by: Kacper Urbańczyk <kacperjaroslawurbanczyk@gmail.com> Co-authored-by: TsjipTsjip <19798667+TsjipTsjip@users.noreply.github.com> Co-authored-by: daerSeebaer <61566539+daerSeebaer@users.noreply.github.com> Co-authored-by: Fluffiest Floofers <thebluewulf@gmail.com> Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com> Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Co-authored-by: Kevin Zheng <kevinz5000@gmail.com> Co-authored-by: drteaspoon420 <87363733+drteaspoon420@users.noreply.github.com> Co-authored-by: Ubaser <134914314+UbaserB@users.noreply.github.com> Co-authored-by: Nim <128169402+Nimfar11@users.noreply.github.com> Co-authored-by: Kacper Urbańczyk <mikrel071204@gmail.com> Co-authored-by: Tox Cruize <141375638+TexCruize@users.noreply.github.com> Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com> Co-authored-by: Michael Cu <43478115+michaelcu@users.noreply.github.com>
841 lines
32 KiB
C#
841 lines
32 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using Content.Server.Explosion.Components;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Explosion;
|
|
using Content.Shared.Maps;
|
|
using Content.Shared.Mind.Components;
|
|
using Content.Shared.Physics;
|
|
using Content.Shared.Projectiles;
|
|
using Robust.Shared.Spawners;
|
|
using Content.Shared.Tag;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Shared.Physics.Dynamics;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Timing;
|
|
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
|
|
|
|
namespace Content.Server.Explosion.EntitySystems;
|
|
|
|
public sealed partial class ExplosionSystem : EntitySystem
|
|
{
|
|
/// <summary>
|
|
/// Used to limit explosion processing time. See <see cref="MaxProcessingTime"/>.
|
|
/// </summary>
|
|
internal readonly Stopwatch Stopwatch = new();
|
|
|
|
/// <summary>
|
|
/// How many tiles to explode before checking the stopwatch timer
|
|
/// </summary>
|
|
internal static int TileCheckIteration = 1;
|
|
|
|
/// <summary>
|
|
/// Queue for delayed processing of explosions. If there is an explosion that covers more than <see
|
|
/// cref="TilesPerTick"/> tiles, other explosions will actually be delayed slightly. Unless it's a station
|
|
/// nuke, this delay should never really be noticeable.
|
|
/// </summary>
|
|
private Queue<Func<Explosion?>> _explosionQueue = new();
|
|
|
|
/// <summary>
|
|
/// The explosion currently being processed.
|
|
/// </summary>
|
|
private Explosion? _activeExplosion;
|
|
|
|
/// <summary>
|
|
/// While processing an explosion, the "progress" is sent to clients, so that the explosion fireball effect
|
|
/// syncs up with the damage. When the tile iteration increments, an update needs to be sent to clients.
|
|
/// This integer keeps track of the last value sent to clients.
|
|
/// </summary>
|
|
private int _previousTileIteration;
|
|
|
|
private void OnMapChanged(MapChangedEvent ev)
|
|
{
|
|
// If a map was deleted, check the explosion currently being processed belongs to that map.
|
|
if (ev.Created)
|
|
return;
|
|
|
|
if (_activeExplosion?.Epicenter.MapId != ev.Map)
|
|
return;
|
|
|
|
QueueDel(_activeExplosion.VisualEnt);
|
|
_activeExplosion = null;
|
|
_nodeGroupSystem.PauseUpdating = false;
|
|
_pathfindingSystem.PauseUpdating = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process the explosion queue.
|
|
/// </summary>
|
|
public override void Update(float frameTime)
|
|
{
|
|
if (_activeExplosion == null && _explosionQueue.Count == 0)
|
|
// nothing to do
|
|
return;
|
|
|
|
Stopwatch.Restart();
|
|
var x = Stopwatch.Elapsed.TotalMilliseconds;
|
|
|
|
var availableTime = MaxProcessingTime;
|
|
|
|
var tilesRemaining = TilesPerTick;
|
|
while (tilesRemaining > 0 && MaxProcessingTime > Stopwatch.Elapsed.TotalMilliseconds)
|
|
{
|
|
// if there is no active explosion, get a new one to process
|
|
if (_activeExplosion == null)
|
|
{
|
|
// EXPLOSION TODO allow explosion spawning to be interrupted by time limit. In the meantime, ensure that
|
|
// there is at-least 1ms of time left before creating a new explosion
|
|
if (MathF.Max(MaxProcessingTime - 1, 0.1f) < Stopwatch.Elapsed.TotalMilliseconds)
|
|
break;
|
|
|
|
if (!_explosionQueue.TryDequeue(out var spawnNextExplosion))
|
|
break;
|
|
|
|
_activeExplosion = spawnNextExplosion();
|
|
|
|
// explosion spawning can be null if something somewhere went wrong. (e.g., negative explosion
|
|
// intensity).
|
|
if (_activeExplosion == null)
|
|
continue;
|
|
|
|
_previousTileIteration = 0;
|
|
|
|
// just a lil nap
|
|
if (SleepNodeSys)
|
|
{
|
|
_nodeGroupSystem.PauseUpdating = true;
|
|
_pathfindingSystem.PauseUpdating = true;
|
|
// snooze grid-chunk regeneration?
|
|
// snooze power network (recipients look for new suppliers as wires get destroyed).
|
|
}
|
|
|
|
if (_activeExplosion.Area > SingleTickAreaLimit)
|
|
break; // start processing next turn.
|
|
}
|
|
|
|
// TODO EXPLOSION check if active explosion is on a paused map. If it is... I guess support swapping out &
|
|
// storing the "currently active" explosion?
|
|
|
|
#if EXCEPTION_TOLERANCE
|
|
try
|
|
{
|
|
#endif
|
|
var processed = _activeExplosion.Process(tilesRemaining);
|
|
tilesRemaining -= processed;
|
|
|
|
// has the explosion finished processing?
|
|
if (_activeExplosion.FinishedProcessing)
|
|
{
|
|
var comp = EnsureComp<TimedDespawnComponent>(_activeExplosion.VisualEnt);
|
|
comp.Lifetime = _cfg.GetCVar(CCVars.ExplosionPersistence);
|
|
_appearance.SetData(_activeExplosion.VisualEnt, ExplosionAppearanceData.Progress, int.MaxValue);
|
|
_activeExplosion = null;
|
|
}
|
|
#if EXCEPTION_TOLERANCE
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// Ensure the system does not get stuck in an error-loop.
|
|
if (_activeExplosion != null)
|
|
QueueDel(_activeExplosion.VisualEnt);
|
|
_activeExplosion = null;
|
|
_nodeGroupSystem.PauseUpdating = false;
|
|
_pathfindingSystem.PauseUpdating = false;
|
|
throw;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
Logger.InfoS("Explosion", $"Processed {TilesPerTick - tilesRemaining} tiles in {Stopwatch.Elapsed.TotalMilliseconds}ms");
|
|
|
|
// we have finished processing our tiles. Is there still an ongoing explosion?
|
|
if (_activeExplosion != null)
|
|
{
|
|
_appearance.SetData(_activeExplosion.VisualEnt, ExplosionAppearanceData.Progress, _activeExplosion.CurrentIteration + 1);
|
|
return;
|
|
}
|
|
|
|
if (_explosionQueue.Count > 0)
|
|
return;
|
|
|
|
//wakey wakey
|
|
_nodeGroupSystem.PauseUpdating = false;
|
|
_pathfindingSystem.PauseUpdating = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether an entity is blocking a tile or not. (whether it can prevent the tile from being uprooted
|
|
/// by an explosion).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Used for a variation of <see cref="TurfHelpers.IsBlockedTurf()"/> that makes use of the fact that we have
|
|
/// already done an entity lookup on a tile, and don't need to do so again.
|
|
/// </remarks>
|
|
public bool IsBlockingTurf(EntityUid uid, EntityQuery<PhysicsComponent> physicsQuery)
|
|
{
|
|
if (EntityManager.IsQueuedForDeletion(uid))
|
|
return false;
|
|
|
|
if (!physicsQuery.TryGetComponent(uid, out var physics))
|
|
return false;
|
|
|
|
return physics.CanCollide && physics.Hard && (physics.CollisionLayer & (int) CollisionGroup.Impassable) != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find entities on a grid tile using the EntityLookupComponent and apply explosion effects.
|
|
/// </summary>
|
|
/// <returns>True if the underlying tile can be uprooted, false if the tile is blocked by a dense entity</returns>
|
|
internal bool ExplodeTile(BroadphaseComponent lookup,
|
|
MapGridComponent grid,
|
|
Vector2i tile,
|
|
float throwForce,
|
|
DamageSpecifier damage,
|
|
MapCoordinates epicenter,
|
|
HashSet<EntityUid> processed,
|
|
string id,
|
|
EntityQuery<TransformComponent> xformQuery,
|
|
EntityQuery<DamageableComponent> damageQuery,
|
|
EntityQuery<PhysicsComponent> physicsQuery,
|
|
EntityQuery<TagComponent> tagQuery,
|
|
EntityQuery<ProjectileComponent> projectileQuery)
|
|
{
|
|
var gridBox = new Box2(tile * grid.TileSize, (tile + 1) * grid.TileSize);
|
|
|
|
// get the entities on a tile. Note that we cannot process them directly, or we get
|
|
// enumerator-changed-while-enumerating errors.
|
|
List<TransformComponent> list = new();
|
|
var state = (list, processed, xformQuery);
|
|
|
|
// get entities:
|
|
lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
|
|
lookup.StaticTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
|
|
lookup.SundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
|
|
lookup.StaticSundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
|
|
|
|
// process those entities
|
|
foreach (var xform in list)
|
|
{
|
|
ProcessEntity(xform.Owner, epicenter, damage, throwForce, id, xform, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
|
|
}
|
|
|
|
// process anchored entities
|
|
var tileBlocked = false;
|
|
var anchoredList = grid.GetAnchoredEntities(tile).ToList();
|
|
foreach (var entity in anchoredList)
|
|
{
|
|
processed.Add(entity);
|
|
ProcessEntity(entity, epicenter, damage, throwForce, id, null, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
|
|
}
|
|
|
|
// Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for
|
|
// the purposes of destroying floors. Again, ideally the process of damaging an entity should somehow return
|
|
// information about the entities that were spawned as a result, but without that information we just have to
|
|
// re-check for new anchored entities. Compared to entity spawning & deleting, this should still be relatively minor.
|
|
if (anchoredList.Count > 0)
|
|
{
|
|
foreach (var entity in grid.GetAnchoredEntities(tile))
|
|
{
|
|
tileBlocked |= IsBlockingTurf(entity, physicsQuery);
|
|
}
|
|
}
|
|
|
|
// Next, we get the intersecting entities AGAIN, but purely for throwing. This way, glass shards spawned from
|
|
// windows will be flung outwards, and not stay where they spawned. This is however somewhat unnecessary, and a
|
|
// prime candidate for computational cost-cutting. Alternatively, it would be nice if there was just some sort
|
|
// of spawned-on-destruction event that could be used to automatically assemble a list of new entities that need
|
|
// to be thrown.
|
|
//
|
|
// All things considered, until entity spawning & destruction is sped up, this isn't all that time consuming.
|
|
// And throwing is disabled for nukes anyways.
|
|
if (throwForce <= 0)
|
|
return !tileBlocked;
|
|
|
|
list.Clear();
|
|
lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
|
|
lookup.SundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
|
|
|
|
foreach (var xform in list)
|
|
{
|
|
// Here we only throw, no dealing damage. Containers n such might drop their entities after being destroyed, but
|
|
// they should handle their own damage pass-through, with their own damage reduction calculation.
|
|
ProcessEntity(xform.Owner, epicenter, null, throwForce, id, xform, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
|
|
}
|
|
|
|
return !tileBlocked;
|
|
}
|
|
|
|
private bool GridQueryCallback(
|
|
ref (List<TransformComponent> List, HashSet<EntityUid> Processed, EntityQuery<TransformComponent> XformQuery) state,
|
|
in EntityUid uid)
|
|
{
|
|
if (state.Processed.Add(uid) && state.XformQuery.TryGetComponent(uid, out var xform))
|
|
state.List.Add(xform);
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool GridQueryCallback(
|
|
ref (List<TransformComponent> List, HashSet<EntityUid> Processed, EntityQuery<TransformComponent> XformQuery) state,
|
|
in FixtureProxy proxy)
|
|
{
|
|
var owner = proxy.Entity;
|
|
return GridQueryCallback(ref state, in owner);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Same as <see cref="ExplodeTile"/>, but for SPAAAAAAACE.
|
|
/// </summary>
|
|
internal void ExplodeSpace(BroadphaseComponent lookup,
|
|
Matrix3 spaceMatrix,
|
|
Matrix3 invSpaceMatrix,
|
|
Vector2i tile,
|
|
float throwForce,
|
|
DamageSpecifier damage,
|
|
MapCoordinates epicenter,
|
|
HashSet<EntityUid> processed,
|
|
string id,
|
|
EntityQuery<TransformComponent> xformQuery,
|
|
EntityQuery<DamageableComponent> damageQuery,
|
|
EntityQuery<PhysicsComponent> physicsQuery,
|
|
EntityQuery<TagComponent> tagQuery,
|
|
EntityQuery<ProjectileComponent> projectileQuery)
|
|
{
|
|
var gridBox = Box2.FromDimensions(tile * DefaultTileSize, new Vector2(DefaultTileSize, DefaultTileSize));
|
|
var worldBox = spaceMatrix.TransformBox(gridBox);
|
|
var list = new List<TransformComponent>();
|
|
var state = (list, processed, invSpaceMatrix, lookup.Owner, xformQuery, gridBox);
|
|
|
|
// get entities:
|
|
lookup.DynamicTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
|
|
lookup.StaticTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
|
|
lookup.SundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
|
|
lookup.StaticSundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
|
|
|
|
foreach (var xform in state.Item1)
|
|
{
|
|
processed.Add(xform.Owner);
|
|
ProcessEntity(xform.Owner, epicenter, damage, throwForce, id, xform, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
|
|
}
|
|
|
|
if (throwForce <= 0)
|
|
return;
|
|
|
|
// Also, throw any entities that were spawned as shrapnel. Compared to entity spawning & destruction, this extra
|
|
// lookup is relatively minor computational cost, and throwing is disabled for nukes anyways.
|
|
list.Clear();
|
|
lookup.DynamicTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
|
|
lookup.SundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
|
|
|
|
foreach (var xform in list)
|
|
{
|
|
ProcessEntity(xform.Owner, epicenter, null, throwForce, id, xform, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
|
|
}
|
|
}
|
|
|
|
private bool SpaceQueryCallback(
|
|
ref (List<TransformComponent> List, HashSet<EntityUid> Processed, Matrix3 InvSpaceMatrix, EntityUid LookupOwner, EntityQuery<TransformComponent> XformQuery, Box2 GridBox) state,
|
|
in EntityUid uid)
|
|
{
|
|
if (state.Processed.Contains(uid))
|
|
return true;
|
|
|
|
var xform = state.XformQuery.GetComponent(uid);
|
|
|
|
if (xform.ParentUid == state.LookupOwner)
|
|
{
|
|
// parented directly to the map, use local position
|
|
if (state.GridBox.Contains(state.InvSpaceMatrix.Transform(xform.LocalPosition)))
|
|
state.List.Add(xform);
|
|
|
|
return true;
|
|
}
|
|
|
|
// finally check if it intersects our tile
|
|
if (state.GridBox.Contains(state.InvSpaceMatrix.Transform(_transformSystem.GetWorldPosition(xform, state.XformQuery))))
|
|
state.List.Add(xform);
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool SpaceQueryCallback(
|
|
ref (List<TransformComponent> List, HashSet<EntityUid> Processed, Matrix3 InvSpaceMatrix, EntityUid LookupOwner, EntityQuery<TransformComponent> XformQuery, Box2 GridBox) state,
|
|
in FixtureProxy proxy)
|
|
{
|
|
var uid = proxy.Entity;
|
|
return SpaceQueryCallback(ref state, in uid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function actually applies the explosion affects to an entity.
|
|
/// </summary>
|
|
private void ProcessEntity(
|
|
EntityUid uid,
|
|
MapCoordinates epicenter,
|
|
DamageSpecifier? damage,
|
|
float throwForce,
|
|
string id,
|
|
TransformComponent? xform,
|
|
EntityQuery<DamageableComponent> damageQuery,
|
|
EntityQuery<PhysicsComponent> physicsQuery,
|
|
EntityQuery<TransformComponent> transformQuery,
|
|
EntityQuery<TagComponent> tagQuery,
|
|
EntityQuery<ProjectileComponent> projectileQuery)
|
|
{
|
|
// damage
|
|
if (damage != null && damageQuery.TryGetComponent(uid, out var damageable))
|
|
{
|
|
var ev = new GetExplosionResistanceEvent(id);
|
|
RaiseLocalEvent(uid, ref ev, false);
|
|
|
|
ev.DamageCoefficient = Math.Max(0, ev.DamageCoefficient);
|
|
|
|
//todo need a way to track origin of explosion
|
|
if (ev.DamageCoefficient == 1)
|
|
{
|
|
// no damage-dict multiplication required.
|
|
_damageableSystem.TryChangeDamage(uid, damage, ignoreResistances: true, damageable: damageable);
|
|
if (HasComp<MindContainerComponent>(uid) || HasComp<ExplosiveComponent>(uid))
|
|
{
|
|
var damageStr = string.Join(", ", damage.DamageDict.Select(entry => $"{entry.Key}: {entry.Value}"));
|
|
_adminLogger.Add(LogType.Explosion, LogImpact.Medium,
|
|
$"Explosion caused [{damageStr}] to {ToPrettyString(uid):target} at {Transform(uid).Coordinates}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var appliedDamage = damage * ev.DamageCoefficient;
|
|
_damageableSystem.TryChangeDamage(uid, appliedDamage, ignoreResistances: true, damageable: damageable);
|
|
if (HasComp<MindContainerComponent>(uid) || HasComp<ExplosiveComponent>(uid))
|
|
{
|
|
var damageStr = string.Join(", ", appliedDamage.DamageDict.Select(entry => $"{entry.Key}: {entry.Value}"));
|
|
_adminLogger.Add(LogType.Explosion, LogImpact.Medium,
|
|
$"Explosion caused [{damageStr}] to {ToPrettyString(uid):target} at {Transform(uid).Coordinates}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// throw
|
|
if (xform != null // null implies anchored
|
|
&& !xform.Anchored
|
|
&& throwForce > 0
|
|
&& !EntityManager.IsQueuedForDeletion(uid)
|
|
&& physicsQuery.TryGetComponent(uid, out var physics)
|
|
&& physics.BodyType == BodyType.Dynamic)
|
|
{
|
|
var pos = _transformSystem.GetWorldPosition(xform, transformQuery);
|
|
_throwingSystem.TryThrow(
|
|
uid,
|
|
pos - epicenter.Position,
|
|
physics,
|
|
xform,
|
|
projectileQuery,
|
|
throwForce);
|
|
}
|
|
|
|
// TODO EXPLOSION puddle / flammable ignite?
|
|
|
|
// TODO EXPLOSION deaf/ear damage? other explosion effects?
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to damage floor tiles. Not to be confused with the function that damages entities intersecting the
|
|
/// grid tile.
|
|
/// </summary>
|
|
public void DamageFloorTile(TileRef tileRef,
|
|
float effectiveIntensity,
|
|
int maxTileBreak,
|
|
bool canCreateVacuum,
|
|
List<(Vector2i GridIndices, Tile Tile)> damagedTiles,
|
|
ExplosionPrototype type)
|
|
{
|
|
if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef)
|
|
return;
|
|
|
|
if (tileDef.IsSpace)
|
|
canCreateVacuum = true; // is already a vacuum.
|
|
|
|
int tileBreakages = 0;
|
|
while (maxTileBreak > tileBreakages && _robustRandom.Prob(type.TileBreakChance(effectiveIntensity)))
|
|
{
|
|
tileBreakages++;
|
|
effectiveIntensity -= type.TileBreakRerollReduction;
|
|
|
|
// does this have a base-turf that we can break it down to?
|
|
if (string.IsNullOrEmpty(tileDef.BaseTurf))
|
|
break;
|
|
|
|
if (_tileDefinitionManager[tileDef.BaseTurf] is not ContentTileDefinition newDef)
|
|
break;
|
|
|
|
if (newDef.IsSpace && !canCreateVacuum)
|
|
break;
|
|
|
|
tileDef = newDef;
|
|
}
|
|
|
|
if (tileDef.TileId == tileRef.Tile.TypeId)
|
|
return;
|
|
|
|
damagedTiles.Add((tileRef.GridIndices, new Tile(tileDef.TileId)));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is a data class that stores information about the area affected by an explosion, for processing by <see
|
|
/// cref="ExplosionSystem"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is basically the output of <see cref="ExplosionSystem.GetExplosionTiles()"/>, but with some utility functions for
|
|
/// iterating over the tiles, along with the ability to keep track of what entities have already been damaged by
|
|
/// this explosion.
|
|
/// </remarks>
|
|
sealed class Explosion
|
|
{
|
|
/// <summary>
|
|
/// For every grid (+ space) that the explosion reached, this data struct stores information about the tiles and
|
|
/// caches the entity-lookup component so that it doesn't have to be re-fetched for every tile.
|
|
/// </summary>
|
|
struct ExplosionData
|
|
{
|
|
/// <summary>
|
|
/// The tiles that the explosion damaged, grouped by the iteration (can be thought of as the distance from the epicenter)
|
|
/// </summary>
|
|
public Dictionary<int, List<Vector2i>> TileLists;
|
|
|
|
/// <summary>
|
|
/// Lookup component for this grid (or space/map).
|
|
/// </summary>
|
|
public BroadphaseComponent Lookup;
|
|
|
|
/// <summary>
|
|
/// The actual grid that this corresponds to. If null, this implies space.
|
|
/// </summary>
|
|
public MapGridComponent? MapGrid;
|
|
}
|
|
|
|
private readonly List<ExplosionData> _explosionData = new();
|
|
|
|
/// <summary>
|
|
/// The explosion intensity associated with each tile iteration.
|
|
/// </summary>
|
|
private readonly List<float> _tileSetIntensity;
|
|
|
|
/// <summary>
|
|
/// Used to avoid applying explosion effects repeatedly to the same entity. Particularly important if the
|
|
/// explosion throws this entity, as then it will be moving while the explosion is happening.
|
|
/// </summary>
|
|
public readonly HashSet<EntityUid> ProcessedEntities = new();
|
|
|
|
/// <summary>
|
|
/// This integer tracks how much of this explosion has been processed.
|
|
/// </summary>
|
|
public int CurrentIteration { get; private set; } = 0;
|
|
|
|
/// <summary>
|
|
/// The prototype for this explosion. Determines tile break chance, damage, etc.
|
|
/// </summary>
|
|
public readonly ExplosionPrototype ExplosionType;
|
|
|
|
/// <summary>
|
|
/// The center of the explosion. Used for physics throwing. Also used to identify the map on which the explosion is happening.
|
|
/// </summary>
|
|
public readonly MapCoordinates Epicenter;
|
|
|
|
/// <summary>
|
|
/// The matrix that defines the reference frame for the explosion in space.
|
|
/// </summary>
|
|
private readonly Matrix3 _spaceMatrix;
|
|
|
|
/// <summary>
|
|
/// Inverse of <see cref="_spaceMatrix"/>
|
|
/// </summary>
|
|
private readonly Matrix3 _invSpaceMatrix;
|
|
|
|
/// <summary>
|
|
/// Have all the tiles on all the grids been processed?
|
|
/// </summary>
|
|
public bool FinishedProcessing;
|
|
|
|
// Variables used for enumerating over tiles, grids, etc
|
|
private DamageSpecifier _currentDamage = default!;
|
|
private BroadphaseComponent _currentLookup = default!;
|
|
private MapGridComponent? _currentGrid;
|
|
private float _currentIntensity;
|
|
private float _currentThrowForce;
|
|
private List<Vector2i>.Enumerator _currentEnumerator;
|
|
private int _currentDataIndex;
|
|
|
|
/// <summary>
|
|
/// The set of tiles that need to be updated when the explosion has finished processing. Used to avoid having
|
|
/// the explosion trigger chunk regeneration & shuttle-system processing every tick.
|
|
/// </summary>
|
|
private readonly Dictionary<MapGridComponent, List<(Vector2i, Tile)>> _tileUpdateDict = new();
|
|
|
|
// Entity Queries
|
|
private readonly EntityQuery<TransformComponent> _xformQuery;
|
|
private readonly EntityQuery<PhysicsComponent> _physicsQuery;
|
|
private readonly EntityQuery<DamageableComponent> _damageQuery;
|
|
private readonly EntityQuery<ProjectileComponent> _projectileQuery;
|
|
private readonly EntityQuery<TagComponent> _tagQuery;
|
|
|
|
/// <summary>
|
|
/// Total area that the explosion covers.
|
|
/// </summary>
|
|
public readonly int Area;
|
|
|
|
/// <summary>
|
|
/// factor used to scale the tile break chances.
|
|
/// </summary>
|
|
private readonly float _tileBreakScale;
|
|
|
|
/// <summary>
|
|
/// Maximum number of times that an explosion will break a single tile.
|
|
/// </summary>
|
|
private readonly int _maxTileBreak;
|
|
|
|
/// <summary>
|
|
/// Whether this explosion can turn non-vacuum tiles into vacuum-tiles.
|
|
/// </summary>
|
|
private readonly bool _canCreateVacuum;
|
|
|
|
private readonly IEntityManager _entMan;
|
|
private readonly ExplosionSystem _system;
|
|
|
|
public readonly EntityUid VisualEnt;
|
|
|
|
/// <summary>
|
|
/// Initialize a new instance for processing
|
|
/// </summary>
|
|
public Explosion(ExplosionSystem system,
|
|
ExplosionPrototype explosionType,
|
|
ExplosionSpaceTileFlood? spaceData,
|
|
List<ExplosionGridTileFlood> gridData,
|
|
List<float> tileSetIntensity,
|
|
MapCoordinates epicenter,
|
|
Matrix3 spaceMatrix,
|
|
int area,
|
|
float tileBreakScale,
|
|
int maxTileBreak,
|
|
bool canCreateVacuum,
|
|
IEntityManager entMan,
|
|
IMapManager mapMan,
|
|
EntityUid visualEnt)
|
|
{
|
|
VisualEnt = visualEnt;
|
|
_system = system;
|
|
ExplosionType = explosionType;
|
|
_tileSetIntensity = tileSetIntensity;
|
|
Epicenter = epicenter;
|
|
Area = area;
|
|
|
|
_tileBreakScale = tileBreakScale;
|
|
_maxTileBreak = maxTileBreak;
|
|
_canCreateVacuum = canCreateVacuum;
|
|
_entMan = entMan;
|
|
|
|
_xformQuery = entMan.GetEntityQuery<TransformComponent>();
|
|
_physicsQuery = entMan.GetEntityQuery<PhysicsComponent>();
|
|
_damageQuery = entMan.GetEntityQuery<DamageableComponent>();
|
|
_tagQuery = entMan.GetEntityQuery<TagComponent>();
|
|
_projectileQuery = entMan.GetEntityQuery<ProjectileComponent>();
|
|
|
|
if (spaceData != null)
|
|
{
|
|
var mapUid = mapMan.GetMapEntityId(epicenter.MapId);
|
|
|
|
_explosionData.Add(new()
|
|
{
|
|
TileLists = spaceData.TileLists,
|
|
Lookup = entMan.GetComponent<BroadphaseComponent>(mapUid),
|
|
MapGrid = null
|
|
});
|
|
|
|
_spaceMatrix = spaceMatrix;
|
|
_invSpaceMatrix = Matrix3.Invert(spaceMatrix);
|
|
}
|
|
|
|
foreach (var grid in gridData)
|
|
{
|
|
_explosionData.Add(new ExplosionData
|
|
{
|
|
TileLists = grid.TileLists,
|
|
Lookup = entMan.GetComponent<BroadphaseComponent>(grid.Grid.Owner),
|
|
MapGrid = grid.Grid,
|
|
});
|
|
}
|
|
|
|
if (TryGetNextTileEnumerator())
|
|
MoveNext();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the next tile-enumerator. This either means retrieving a set of tiles on the next grid, or incrementing
|
|
/// the tile iteration by one and moving back to the first grid. This will also update the current damage, current entity-lookup, etc.
|
|
/// </summary>
|
|
private bool TryGetNextTileEnumerator()
|
|
{
|
|
while (CurrentIteration < _tileSetIntensity.Count)
|
|
{
|
|
_currentIntensity = _tileSetIntensity[CurrentIteration];
|
|
_currentDamage = ExplosionType.DamagePerIntensity * _currentIntensity;
|
|
|
|
// only throw if either the explosion is small, or if this is the outer ring of a large explosion.
|
|
var doThrow = Area < _system.ThrowLimit || CurrentIteration > _tileSetIntensity.Count - 6;
|
|
_currentThrowForce = doThrow ? 10 * MathF.Sqrt(_currentIntensity) : 0;
|
|
|
|
// for each grid/space tile set
|
|
while (_currentDataIndex < _explosionData.Count)
|
|
{
|
|
// try get any tile hash-set corresponding to this intensity
|
|
var tileSets = _explosionData[_currentDataIndex].TileLists;
|
|
if (!tileSets.TryGetValue(CurrentIteration, out var tileList))
|
|
{
|
|
_currentDataIndex++;
|
|
continue;
|
|
}
|
|
|
|
_currentEnumerator = tileList.GetEnumerator();
|
|
_currentLookup = _explosionData[_currentDataIndex].Lookup;
|
|
_currentGrid = _explosionData[_currentDataIndex].MapGrid;
|
|
_currentDataIndex++;
|
|
|
|
// sanity checks, in case something changed while the explosion was being processed over several ticks.
|
|
if (_currentLookup.Deleted || _currentGrid != null && !_entMan.EntityExists(_currentGrid.Owner))
|
|
continue;
|
|
|
|
return true;
|
|
}
|
|
|
|
// All the tiles belonging to this explosion iteration have been processed. Move onto the next iteration and
|
|
// reset the grid counter.
|
|
CurrentIteration++;
|
|
_currentDataIndex = 0;
|
|
}
|
|
|
|
// No more explosion tiles to process
|
|
FinishedProcessing = true;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the next tile that needs processing
|
|
/// </summary>
|
|
private bool MoveNext()
|
|
{
|
|
if (FinishedProcessing)
|
|
return false;
|
|
|
|
while (!FinishedProcessing)
|
|
{
|
|
if (_currentEnumerator.MoveNext())
|
|
return true;
|
|
else
|
|
TryGetNextTileEnumerator();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to process (i.e., damage entities) some number of grid tiles.
|
|
/// </summary>
|
|
public int Process(int processingTarget)
|
|
{
|
|
// In case the explosion terminated early last tick due to exceeding the allocated processing time, use this
|
|
// time to update the tiles.
|
|
SetTiles();
|
|
|
|
int processed;
|
|
for (processed = 0; processed < processingTarget; processed++)
|
|
{
|
|
if (processed % ExplosionSystem.TileCheckIteration == 0 &&
|
|
_system.Stopwatch.Elapsed.TotalMilliseconds > _system.MaxProcessingTime)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Is the current tile on a grid (instead of in space)?
|
|
if (_currentGrid != null &&
|
|
_currentGrid.TryGetTileRef(_currentEnumerator.Current, out var tileRef) &&
|
|
!tileRef.Tile.IsEmpty)
|
|
{
|
|
if (!_tileUpdateDict.TryGetValue(_currentGrid, out var tileUpdateList))
|
|
{
|
|
tileUpdateList = new();
|
|
_tileUpdateDict[_currentGrid] = tileUpdateList;
|
|
}
|
|
|
|
// damage entities on the tile. Also figures out whether there are any solid entities blocking the floor
|
|
// from being destroyed.
|
|
var canDamageFloor = _system.ExplodeTile(_currentLookup,
|
|
_currentGrid,
|
|
_currentEnumerator.Current,
|
|
_currentThrowForce,
|
|
_currentDamage,
|
|
Epicenter,
|
|
ProcessedEntities,
|
|
ExplosionType.ID,
|
|
_xformQuery,
|
|
_damageQuery,
|
|
_physicsQuery,
|
|
_tagQuery,
|
|
_projectileQuery);
|
|
|
|
// If the floor is not blocked by some dense object, damage the floor tiles.
|
|
if (canDamageFloor)
|
|
_system.DamageFloorTile(tileRef, _currentIntensity * _tileBreakScale, _maxTileBreak, _canCreateVacuum, tileUpdateList, ExplosionType);
|
|
}
|
|
else
|
|
{
|
|
// The current "tile" is in space. Damage any entities in that region
|
|
_system.ExplodeSpace(_currentLookup,
|
|
_spaceMatrix,
|
|
_invSpaceMatrix,
|
|
_currentEnumerator.Current,
|
|
_currentThrowForce,
|
|
_currentDamage,
|
|
Epicenter,
|
|
ProcessedEntities,
|
|
ExplosionType.ID,
|
|
_xformQuery,
|
|
_damageQuery,
|
|
_physicsQuery,
|
|
_tagQuery,
|
|
_projectileQuery);
|
|
}
|
|
|
|
if (!MoveNext())
|
|
break;
|
|
}
|
|
|
|
// Update damaged/broken tiles on the grid.
|
|
SetTiles();
|
|
return processed;
|
|
}
|
|
|
|
private void SetTiles()
|
|
{
|
|
// Updating the grid can result in chunk collision regeneration & slow processing by the shuttle system.
|
|
// Therefore, tile breaking may be configure to only happen at the end of an explosion, rather than during every
|
|
// tick.
|
|
if (!_system.IncrementalTileBreaking && !FinishedProcessing)
|
|
return;
|
|
|
|
foreach (var (grid, list) in _tileUpdateDict)
|
|
{
|
|
if (list.Count > 0 && _entMan.EntityExists(grid.Owner))
|
|
{
|
|
grid.SetTiles(list);
|
|
}
|
|
}
|
|
_tileUpdateDict.Clear();
|
|
}
|
|
}
|
|
|