using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Chemistry.ReactionEffects;
using Content.Server.Spreader;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Smoking;
using Content.Shared.Spawners;
using Content.Shared.Spawners.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Fluids.EntitySystems;
///
/// Handles non-atmos solution entities similar to puddles.
///
public sealed class SmokeSystem : EntitySystem
{
// If I could do it all again this could probably use a lot more of puddles.
[Dependency] private readonly IAdminLogManager _logger = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly BloodstreamSystem _blood = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly ReactiveSystem _reactive = default!;
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
///
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnSmokeUnpaused);
SubscribeLocalEvent(OnReactionAttempt);
SubscribeLocalEvent(OnSmokeSpread);
SubscribeLocalEvent(OnSmokeDissipate);
SubscribeLocalEvent(OnSpreadUpdateRate);
}
private void OnSpreadUpdateRate(ref SpreadGroupUpdateRate ev)
{
if (ev.Name != "smoke")
return;
ev.UpdatesPerSecond = 8;
}
private void OnSmokeDissipate(EntityUid uid, SmokeDissipateSpawnComponent component, ref TimedDespawnEvent args)
{
if (!TryComp(uid, out var xform))
{
return;
}
Spawn(component.Prototype, xform.Coordinates);
}
private void OnSmokeSpread(EntityUid uid, SmokeComponent component, ref SpreadNeighborsEvent args)
{
if (component.SpreadAmount == 0 ||
!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution) ||
args.NeighborFreeTiles.Count == 0)
{
RemCompDeferred(uid);
return;
}
var prototype = MetaData(uid).EntityPrototype;
if (prototype == null)
{
RemCompDeferred(uid);
return;
}
TryComp(uid, out var timer);
var smokePerSpread = component.SpreadAmount / args.NeighborFreeTiles.Count;
component.SpreadAmount -= smokePerSpread;
foreach (var neighbor in args.NeighborFreeTiles)
{
var coords = neighbor.Grid.GridTileToLocal(neighbor.Tile);
var ent = Spawn(prototype.ID, coords.SnapToGrid());
var neighborSmoke = EnsureComp(ent);
neighborSmoke.SpreadAmount = Math.Max(0, smokePerSpread - 1);
args.Updates--;
// Listen this is the old behaviour iunno
Start(ent, neighborSmoke, solution.Clone(), timer?.Lifetime ?? 10f);
if (_appearance.TryGetData(uid, SmokeVisuals.Color, out var color))
{
_appearance.SetData(ent, SmokeVisuals.Color, color);
}
// Only 1 spread then ig?
if (smokePerSpread == 0)
{
component.SpreadAmount--;
if (component.SpreadAmount == 0)
{
RemCompDeferred(uid);
break;
}
}
if (args.Updates <= 0)
break;
}
// Give our spread to neighbor tiles.
if (args.NeighborFreeTiles.Count == 0 && args.Neighbors.Count > 0 && component.SpreadAmount > 0)
{
var smokeQuery = GetEntityQuery();
foreach (var neighbor in args.Neighbors)
{
if (!smokeQuery.TryGetComponent(neighbor, out var smoke))
continue;
smoke.SpreadAmount++;
args.Updates--;
if (component.SpreadAmount == 0)
{
RemCompDeferred(uid);
break;
}
if (args.Updates <= 0)
break;
}
}
}
private void OnReactionAttempt(EntityUid uid, SmokeComponent component, ReactionAttemptEvent args)
{
if (args.Solution.Name != SmokeComponent.SolutionName)
return;
// Prevent smoke/foam fork bombs (smoke creating more smoke).
foreach (var effect in args.Reaction.Effects)
{
if (effect is AreaReactionEffect)
{
args.Cancel();
return;
}
}
}
private void OnSmokeUnpaused(EntityUid uid, SmokeComponent component, ref EntityUnpausedEvent args)
{
component.NextReact += args.PausedTime;
}
///
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator();
var curTime = _timing.CurTime;
while (query.MoveNext(out var uid, out var smoke))
{
if (smoke.NextReact > curTime)
continue;
smoke.NextReact += TimeSpan.FromSeconds(1.5);
SmokeReact(uid, 1f, smoke);
}
}
///
/// Does the relevant smoke reactions for an entity for the specified exposure duration.
///
public void SmokeReact(EntityUid uid, float frameTime, SmokeComponent? component = null, TransformComponent? xform = null)
{
if (!Resolve(uid, ref component, ref xform))
return;
if (!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution) ||
solution.Contents.Count == 0)
{
return;
}
if (!_mapManager.TryGetGrid(xform.GridUid, out var mapGrid))
return;
var tile = mapGrid.GetTileRef(xform.Coordinates.ToVector2i(EntityManager, _mapManager));
var solutionFraction = 1 / Math.Floor(frameTime);
var ents = _lookup.GetEntitiesIntersecting(tile, LookupFlags.Uncontained).ToArray();
foreach (var reagentQuantity in solution.Contents.ToArray())
{
if (reagentQuantity.Quantity == FixedPoint2.Zero)
continue;
// NOOP, react with entities on the tile or whatever.
}
foreach (var entity in ents)
{
if (entity == uid)
continue;
ReactWithEntity(entity, solution, solutionFraction);
}
UpdateVisuals(uid);
}
private void UpdateVisuals(EntityUid uid)
{
if (TryComp(uid, out AppearanceComponent? appearance) &&
_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution))
{
var color = solution.GetColor(_prototype);
_appearance.SetData(uid, SmokeVisuals.Color, color, appearance);
}
}
private void ReactWithEntity(EntityUid entity, Solution solution, double solutionFraction)
{
// NOOP due to people complaining constantly.
return;
}
///
/// Sets up a smoke component for spreading.
///
public void Start(EntityUid uid, SmokeComponent component, Solution solution, float duration)
{
TryAddSolution(uid, component, solution);
EnsureComp(uid);
var timer = EnsureComp(uid);
timer.Lifetime = duration;
}
///
/// Adds the specified solution to the relevant smoke solution.
///
public void TryAddSolution(EntityUid uid, SmokeComponent component, Solution solution)
{
if (solution.Volume == FixedPoint2.Zero)
return;
if (!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solutionArea))
return;
var addSolution =
solution.SplitSolution(FixedPoint2.Min(solution.Volume, solutionArea.AvailableVolume));
_solutionSystem.TryAddSolution(uid, solutionArea, addSolution);
UpdateVisuals(uid);
}
}