Stable release 2025-11-09 (#41368)

This commit is contained in:
Myra
2025-11-09 15:18:46 +01:00
committed by GitHub
572 changed files with 11065 additions and 29473 deletions

View File

@@ -8,7 +8,7 @@ This isnt an exhaustive list of things that you cant do. Rather, take it i
This code of conduct applies specifically to the Github repositories and its spaces managed by the Space Station 14 project or Space Wizards Federation. Some spaces, such as the Space Station 14 Discord or the official Wizard's Den game servers, have their own rules but are in spirit equal to what may be found in here.
If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [telecommunications@spacestation14.com](mailto:telecommunications@spacestation14.com).
If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [support@spacestation14.com](mailto:support@spacestation14.com).
- **Be friendly and patient.**
- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.

View File

@@ -0,0 +1,160 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Destructible;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Maps;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Benchmarks;
[Virtual]
[GcServer(true)]
[MemoryDiagnoser]
public class DestructibleBenchmark
{
/// <summary>
/// Number of destructible entities per prototype to spawn with a <see cref="DestructibleComponent"/>.
/// </summary>
[Params(1, 10, 100, 1000, 5000)]
public int EntityCount;
/// <summary>
/// Amount of blunt damage we do to each entity.
/// </summary>
[Params(10000)]
public FixedPoint2 DamageAmount;
[Params("Blunt")]
public ProtoId<DamageTypePrototype> DamageType;
private static readonly EntProtoId WindowProtoId = "Window";
private static readonly EntProtoId WallProtoId = "WallReinforced";
private static readonly EntProtoId HumanProtoId = "MobHuman";
private static readonly ProtoId<ContentTileDefinition> TileRef = "Plating";
private readonly EntProtoId[] _prototypes = [WindowProtoId, WallProtoId, HumanProtoId];
private readonly List<Entity<DamageableComponent>> _damageables = new();
private readonly List<Entity<DamageableComponent, DestructibleComponent>> _destructbiles = new();
private DamageSpecifier _damage;
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private IPrototypeManager _protoMan = default!;
private IRobustRandom _random = default!;
private ITileDefinitionManager _tileDefMan = default!;
private DamageableSystem _damageable = default!;
private DestructibleSystem _destructible = default!;
private SharedMapSystem _map = default!;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();
_entMan = server.ResolveDependency<IEntityManager>();
_protoMan = server.ResolveDependency<IPrototypeManager>();
_random = server.ResolveDependency<IRobustRandom>();
_tileDefMan = server.ResolveDependency<ITileDefinitionManager>();
_damageable = _entMan.System<DamageableSystem>();
_destructible = _entMan.System<DestructibleSystem>();
_map = _entMan.System<SharedMapSystem>();
if (!_protoMan.Resolve(DamageType, out var type))
return;
_damage = new DamageSpecifier(type, DamageAmount);
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
var plating = _tileDefMan[TileRef].TileId;
// We make a rectangular grid of destructible entities, and then damage them all simultaneously to stress test the system.
// Needed for managing the performance of destructive effects and damage application.
await server.WaitPost(() =>
{
// Set up a thin line of tiles to place our objects on. They should be anchored for a "realistic" scenario...
for (var x = 0; x < EntityCount; x++)
{
for (var y = 0; y < _prototypes.Length; y++)
{
_map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
}
}
for (var x = 0; x < EntityCount; x++)
{
var y = 0;
foreach (var protoId in _prototypes)
{
var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
_entMan.SpawnEntity(protoId, coords);
y++;
}
}
var query = _entMan.EntityQueryEnumerator<DamageableComponent, DestructibleComponent>();
while (query.MoveNext(out var uid, out var damageable, out var destructible))
{
_damageables.Add((uid, damageable));
_destructbiles.Add((uid, damageable, destructible));
}
});
}
[Benchmark]
public async Task PerformDealDamage()
{
await _pair.Server.WaitPost(() =>
{
_damageable.ApplyDamageToAllEntities(_damageables, _damage);
});
}
[Benchmark]
public async Task PerformTestTriggers()
{
await _pair.Server.WaitPost(() =>
{
_destructible.TestAllTriggers(_destructbiles);
});
}
[Benchmark]
public async Task PerformTestBehaviors()
{
await _pair.Server.WaitPost(() =>
{
_destructible.TestAllBehaviors(_destructbiles);
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -0,0 +1,253 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Reactions;
using Content.Shared.Atmos;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Benchmarks;
/// <summary>
/// Benchmarks the performance of different gas reactions.
/// Tests each reaction type with realistic gas mixtures to measure computational cost.
/// </summary>
[Virtual]
[GcServer(true)]
[MemoryDiagnoser]
public class GasReactionBenchmark
{
private const int Iterations = 1000;
private TestPair _pair = default!;
private AtmosphereSystem _atmosphereSystem = default!;
// Grid and tile for reactions that need a holder
private EntityUid _testGrid = default!;
private TileAtmosphere _testTile = default!;
// Reaction instances
private PlasmaFireReaction _plasmaFireReaction = default!;
private TritiumFireReaction _tritiumFireReaction = default!;
private FrezonProductionReaction _frezonProductionReaction = default!;
private FrezonCoolantReaction _frezonCoolantReaction = default!;
private AmmoniaOxygenReaction _ammoniaOxygenReaction = default!;
private N2ODecompositionReaction _n2oDecompositionReaction = default!;
private WaterVaporReaction _waterVaporReaction = default!;
// Gas mixtures for each reaction type
private GasMixture _plasmaFireMixture = default!;
private GasMixture _tritiumFireMixture = default!;
private GasMixture _frezonProductionMixture = default!;
private GasMixture _frezonCoolantMixture = default!;
private GasMixture _ammoniaOxygenMixture = default!;
private GasMixture _n2oDecompositionMixture = default!;
private GasMixture _waterVaporMixture = default!;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
// Create test map and grid
var mapData = await _pair.CreateTestMap();
_testGrid = mapData.Grid;
await server.WaitPost(() =>
{
var entMan = server.ResolveDependency<IEntityManager>();
_atmosphereSystem = entMan.System<AtmosphereSystem>();
_plasmaFireReaction = new PlasmaFireReaction();
_tritiumFireReaction = new TritiumFireReaction();
_frezonProductionReaction = new FrezonProductionReaction();
_frezonCoolantReaction = new FrezonCoolantReaction();
_ammoniaOxygenReaction = new AmmoniaOxygenReaction();
_n2oDecompositionReaction = new N2ODecompositionReaction();
_waterVaporReaction = new WaterVaporReaction();
SetupGasMixtures();
SetupTile();
});
}
private void SetupGasMixtures()
{
// Plasma Fire: Plasma + Oxygen at high temperature
// Temperature must be > PlasmaMinimumBurnTemperature for reaction to occur
_plasmaFireMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.PlasmaMinimumBurnTemperature + 100f // ~673K
};
_plasmaFireMixture.AdjustMoles(Gas.Plasma, 20f);
_plasmaFireMixture.AdjustMoles(Gas.Oxygen, 100f);
// Tritium Fire: Tritium + Oxygen at high temperature
// Temperature must be > FireMinimumTemperatureToExist for reaction to occur
_tritiumFireMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.FireMinimumTemperatureToExist + 100f // ~473K
};
_tritiumFireMixture.AdjustMoles(Gas.Tritium, 20f);
_tritiumFireMixture.AdjustMoles(Gas.Oxygen, 100f);
// Frezon Production: Oxygen + Tritium + Nitrogen catalyst
// Optimal temperature for efficiency (80% of max efficiency temp)
_frezonProductionMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.FrezonProductionMaxEfficiencyTemperature * 0.8f // ~48K
};
_frezonProductionMixture.AdjustMoles(Gas.Oxygen, 50f);
_frezonProductionMixture.AdjustMoles(Gas.Tritium, 50f);
_frezonProductionMixture.AdjustMoles(Gas.Nitrogen, 10f);
// Frezon Coolant: Frezon + Nitrogen
// Temperature must be > FrezonCoolLowerTemperature (23.15K) for reaction to occur
_frezonCoolantMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C + 50f // ~343K
};
_frezonCoolantMixture.AdjustMoles(Gas.Frezon, 30f);
_frezonCoolantMixture.AdjustMoles(Gas.Nitrogen, 100f);
// Ammonia + Oxygen reaction (concentration-dependent, no temp requirement)
_ammoniaOxygenMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C + 100f // ~393K
};
_ammoniaOxygenMixture.AdjustMoles(Gas.Ammonia, 40f);
_ammoniaOxygenMixture.AdjustMoles(Gas.Oxygen, 40f);
// N2O Decomposition (no temperature requirement, just needs N2O moles)
_n2oDecompositionMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C + 100f // ~393K
};
_n2oDecompositionMixture.AdjustMoles(Gas.NitrousOxide, 100f);
// Water Vapor - needs water vapor to condense
_waterVaporMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C
};
_waterVaporMixture.AdjustMoles(Gas.WaterVapor, 50f);
}
private void SetupTile()
{
// Create a tile atmosphere to use as holder for all reactions
var testIndices = new Vector2i(0, 0);
_testTile = new TileAtmosphere(_testGrid, testIndices, new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C
});
}
private static GasMixture CloneMixture(GasMixture original)
{
return new GasMixture(original);
}
[Benchmark]
public async Task PlasmaFireReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_plasmaFireMixture);
_plasmaFireReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task TritiumFireReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_tritiumFireMixture);
_tritiumFireReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task FrezonProductionReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_frezonProductionMixture);
_frezonProductionReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task FrezonCoolantReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_frezonCoolantMixture);
_frezonCoolantReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task AmmoniaOxygenReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_ammoniaOxygenMixture);
_ammoniaOxygenReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task N2ODecompositionReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_n2oDecompositionMixture);
_n2oDecompositionReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task WaterVaporReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_waterVaporMixture);
_waterVaporReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -9,7 +9,6 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
using System.Linq;
using Content.Client.Stylesheets;
namespace Content.Client.Access.UI
{

View File

@@ -294,19 +294,19 @@ public sealed partial class BanPanel : DefaultWindow
}
/// <summary>
/// Adds a check button specifically for one "role" in a "group"
/// Adds a toggle button specifically for one "role" in a "group"
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
/// </summary>
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
{
var roleCheckboxContainer = new BoxContainer();
var roleCheckButton = new Button
var roleToggleButton = new Button
{
Name = role,
Text = role,
ToggleMode = true,
};
roleCheckButton.OnToggled += args =>
roleToggleButton.OnToggled += args =>
{
// Checks the role group checkbox if all the children are pressed
if (args.Pressed && _roleCheckboxes[group].All(e => e.Item1.Pressed))
@@ -343,12 +343,12 @@ public sealed partial class BanPanel : DefaultWindow
roleCheckboxContainer.AddChild(jobIconTexture);
}
roleCheckboxContainer.AddChild(roleCheckButton);
roleCheckboxContainer.AddChild(roleToggleButton);
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
_roleCheckboxes.TryAdd(group, []);
_roleCheckboxes[group].Add((roleCheckButton, rolePrototype));
_roleCheckboxes[group].Add((roleToggleButton, rolePrototype));
}
public void UpdateBanFlag(bool newFlag)

View File

@@ -0,0 +1,5 @@
using Content.Shared.Atmos.EntitySystems;
namespace Content.Client.Atmos.EntitySystems;
public sealed class DeltaPressureSystem : SharedDeltaPressureSystem;

View File

@@ -52,6 +52,7 @@ namespace Content.Client.Changelog
// Open changelog purely to compare to the last viewed date.
var changelogs = await LoadChangelog();
UpdateChangelogs(changelogs);
_configManager.OnValueChanged(CCVars.ServerId, OnServerIdCVarChanged);
}
private void UpdateChangelogs(List<Changelog> changelogs)
@@ -81,6 +82,11 @@ namespace Content.Client.Changelog
MaxId = changelog.Entries.Max(c => c.Id);
CheckLastSeenEntry();
}
private void CheckLastSeenEntry()
{
var path = new ResPath($"/changelog_last_seen_{_configManager.GetCVar(CCVars.ServerId)}");
if (_resource.UserData.TryReadAllText(path, out var lastReadIdText))
{
@@ -92,6 +98,11 @@ namespace Content.Client.Changelog
NewChangelogEntriesChanged?.Invoke();
}
private void OnServerIdCVarChanged(string newValue)
{
CheckLastSeenEntry();
}
public Task<List<Changelog>> LoadChangelog()
{
return Task.Run(() =>

View File

@@ -31,6 +31,11 @@ internal sealed class ChatManager : IChatManager
// See server-side manager. This just exists for shared code.
}
public void SendAdminAlertNoFormatOrEscape(string message)
{
// See server-side manager. This just exists for shared code.
}
public void SendMessage(string text, ChatSelectChannel channel)
{
var str = text.ToString();

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Client.GameObjects;

View File

@@ -47,7 +47,7 @@
Access="Public"
Text="{Loc 'ui-disposal-unit-button-eject'}"
StyleClasses="OpenBoth" />
<CheckButton Name="Power"
<Button Name="Power"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-power'}"
StyleClasses="OpenLeft" />

View File

@@ -34,7 +34,7 @@
Access="Public"
Text="{Loc 'ui-disposal-unit-button-eject'}"
StyleClasses="OpenBoth" />
<CheckButton Name="Power"
<Button Name="Power"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-power'}"
StyleClasses="OpenLeft" />

View File

@@ -1,28 +1,22 @@
using System.Linq;
using System.Numerics;
using Content.Client.Message;
using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

View File

@@ -7,7 +7,7 @@
<Button Name="AllButton" Text="{Loc 'instruments-component-channels-all-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
<Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
</BoxContainer>
<CheckButton Name="DisplayTrackNames"
<Button Name="DisplayTrackNames"
Text="{Loc 'instruments-component-channels-track-names-toggle'}" />
</BoxContainer>
</DefaultWindow>

View File

@@ -190,7 +190,7 @@ namespace Content.Client.Inventory
if (EntMan.TryGetComponent<VirtualItemComponent>(heldEntity, out var virt))
{
button.Blocked = true;
if (EntMan.TryGetComponent<CuffableComponent>(Owner, out var cuff) && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
if (_cuffable.TryGetAllCuffs(Owner, out var cuffs) && cuffs.Contains(virt.BlockingEntity))
button.BlockedRect.MouseFilter = MouseFilterMode.Ignore;
}

View File

@@ -1,7 +1,7 @@
using System.Numerics;
using Content.Client.StatusIcon;
using Content.Client.UserInterface.Systems;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;

View File

@@ -1,5 +1,4 @@
using Content.Shared.Atmos.Rotting;
using Content.Shared.Damage;
using Content.Shared.Inventory.Events;
using Content.Shared.Mobs.Components;
using Content.Shared.Overlays;
@@ -7,6 +6,7 @@ using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
using System.Linq;
using Content.Shared.Damage.Components;
namespace Content.Client.Overlays;

View File

@@ -7,6 +7,9 @@ using Robust.Shared.Timing;
namespace Content.Client.Stack;
/// <summary>
/// Used by hands in player UI to display the stack count.
/// </summary>
public sealed class StackStatusControl : Control
{
private readonly StackComponent _parent;

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Client.Items;
using Content.Client.Storage.Systems;
using Content.Shared.Stacks;
@@ -7,6 +6,7 @@ using Robust.Client.GameObjects;
namespace Content.Client.Stack
{
/// <inheritdoc />
[UsedImplicitly]
public sealed class StackSystem : SharedStackSystem
{
@@ -16,33 +16,21 @@ namespace Content.Client.Stack
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StackComponent, AppearanceChangeEvent>(OnAppearanceChange);
Subs.ItemStatus<StackComponent>(ent => new StackStatusControl(ent));
}
public override void SetCount(EntityUid uid, int amount, StackComponent? component = null)
#region Appearance
private void OnAppearanceChange(Entity<StackComponent> ent, ref AppearanceChangeEvent args)
{
if (!Resolve(uid, ref component))
return;
var (uid, comp) = ent;
base.SetCount(uid, amount, component);
// TODO PREDICT ENTITY DELETION: This should really just be a normal entity deletion call.
if (component.Count <= 0)
{
Xform.DetachEntity(uid, Transform(uid));
return;
}
component.UiUpdateNeeded = true;
}
private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null || comp.LayerStates.Count < 1)
return;
// Skip processing if no actual
// Skip processing if no elements in the stack
if (!_appearanceSystem.TryGetData<int>(uid, StackVisuals.Actual, out var actual, args.Component))
return;
@@ -56,9 +44,24 @@ namespace Content.Client.Stack
ApplyLayerFunction((uid, comp), ref actual, ref maxCount);
if (comp.IsComposite)
_counterSystem.ProcessCompositeSprite(uid, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
{
_counterSystem.ProcessCompositeSprite(uid,
actual,
maxCount,
comp.LayerStates,
hidden,
sprite: args.Sprite);
}
else
_counterSystem.ProcessOpaqueSprite(uid, comp.BaseLayer, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
{
_counterSystem.ProcessOpaqueSprite(uid,
comp.BaseLayer,
actual,
maxCount,
comp.LayerStates,
hidden,
sprite: args.Sprite);
}
}
/// <summary>
@@ -67,7 +70,7 @@ namespace Content.Client.Stack
/// <param name="ent">The entity considered.</param>
/// <param name="actual">The actual number of items in the stack. Altered depending on the function to run.</param>
/// <param name="maxCount">The maximum number of items in the stack. Altered depending on the function to run.</param>
/// <returns>Whether or not a function was applied.</returns>
/// <returns>True if a function was applied.</returns>
private bool ApplyLayerFunction(Entity<StackComponent> ent, ref int actual, ref int maxCount)
{
switch (ent.Comp.LayerFunction)
@@ -78,8 +81,10 @@ namespace Content.Client.Stack
ApplyThreshold(threshold, ref actual, ref maxCount);
return true;
}
break;
}
// No function applied.
return false;
}
@@ -105,7 +110,10 @@ namespace Content.Client.Stack
else
break;
}
actual = newActual;
}
#endregion
}
}

View File

@@ -689,7 +689,7 @@ public sealed partial class ChatUIController : UIController
radioChannel = null;
return _player.LocalEntity is EntityUid { Valid: true } uid
&& _chatSys != null
&& _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true);
&& _chatSys.TryProcessRadioMessage(uid, text, out _, out radioChannel, quiet: true);
}
public void UpdateSelectedChannel(ChatBox box)

View File

@@ -1,4 +1,4 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;

View File

@@ -216,7 +216,7 @@ public sealed partial class MeleeWeaponSystem
var query = EntityQueryEnumerator<TrackUserComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var arcComponent, out var xform))
{
if (arcComponent.User == null)
if (arcComponent.User == null || EntityManager.Deleted(arcComponent.User))
continue;
Vector2 targetPos = TransformSystem.GetWorldPosition(arcComponent.User.Value);

View File

@@ -149,6 +149,15 @@ public sealed class BulletRender : BaseBulletRenderer
public const int BulletHeight = 12;
public const int VerticalSeparation = 2;
private static readonly LayoutParameters LayoutLarge = new LayoutParameters
{
ItemHeight = BulletHeight,
ItemSeparation = 6,
ItemWidth = 5,
VerticalSeparation = VerticalSeparation,
MinCountPerRow = MinCountPerRow
};
private static readonly LayoutParameters LayoutNormal = new LayoutParameters
{
ItemHeight = BulletHeight,
@@ -185,8 +194,9 @@ public sealed class BulletRender : BaseBulletRenderer
if (_type == value)
return;
Parameters = _type switch
Parameters = value switch
{
BulletType.Large => LayoutLarge,
BulletType.Normal => LayoutNormal,
BulletType.Tiny => LayoutTiny,
_ => throw new ArgumentOutOfRangeException()
@@ -218,6 +228,7 @@ public sealed class BulletRender : BaseBulletRenderer
public enum BulletType
{
Large,
Normal,
Tiny
}

View File

@@ -110,7 +110,12 @@ public sealed partial class GunSystem
_bulletRender.Count = count;
_bulletRender.Capacity = capacity;
_bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
_bulletRender.Type = capacity switch
{
> 50 => BulletRender.BulletType.Tiny,
> 15 => BulletRender.BulletType.Normal,
_ => BulletRender.BulletType.Large
};
}
}
@@ -236,7 +241,12 @@ public sealed partial class GunSystem
_bulletRender.Count = count;
_bulletRender.Capacity = capacity;
_bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
_bulletRender.Type = capacity switch
{
> 50 => BulletRender.BulletType.Tiny,
> 15 => BulletRender.BulletType.Normal,
_ => BulletRender.BulletType.Large
};
_ammoCount.Text = $"x{count:00}";
}

View File

@@ -4,8 +4,13 @@ using System.Linq;
using Content.Server.Preferences.Managers;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Pair;
@@ -15,13 +20,49 @@ public sealed partial class TestPair
public Task<TestMapData> CreateTestMap(bool initialized = true)
=> CreateTestMap(initialized, "Plating");
/// <summary>
/// Loads a test map and returns a <see cref="TestMapData"/> representing it.
/// </summary>
/// <param name="testMapPath">The <see cref="ResPath"/> to the test map to load.</param>
/// <param name="initialized">Whether to initialize the map on load.</param>
/// <returns>A <see cref="TestMapData"/> representing the loaded map.</returns>
public async Task<TestMapData> LoadTestMap(ResPath testMapPath, bool initialized = true)
{
TestMapData mapData = new();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = initialized };
var mapLoaderSys = Server.EntMan.System<MapLoaderSystem>();
var mapSys = Server.System<SharedMapSystem>();
// Load our test map in and assert that it exists.
await Server.WaitAssertion(() =>
{
Assert.That(mapLoaderSys.TryLoadMap(testMapPath, out var map, out var gridSet, deserializationOptions),
$"Failed to load map {testMapPath}.");
Assert.That(gridSet, Is.Not.Empty, "There were no grids loaded from the map!");
mapData.MapUid = map!.Value.Owner;
mapData.MapId = map!.Value.Comp.MapId;
mapData.Grid = gridSet!.First();
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
mapData.Tile = mapSys.GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
});
await RunTicksSync(10);
mapData.CMapUid = ToClientUid(mapData.MapUid);
mapData.CGridUid = ToClientUid(mapData.Grid);
mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
return mapData;
}
/// <summary>
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
/// </summary>
public async Task SetAntagPreference(ProtoId<AntagPrototype> id, bool value, NetUserId? user = null)
{
user ??= Client.User!.Value;
if (user is not {} userId)
if (user is not { } userId)
return;
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
@@ -30,7 +71,7 @@ public sealed partial class TestPair
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var profile = (HumanoidCharacterProfile)prefs.Characters[0];
var newProfile = profile.WithAntagPreference(id, value);
_modifiedProfiles.Add(userId);
await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait());
@@ -58,7 +99,7 @@ public sealed partial class TestPair
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(user);
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var profile = (HumanoidCharacterProfile)prefs.Characters[0];
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(profile.JobPriorities);
// Automatic preference resetting only resets slot 0.

View File

@@ -4,6 +4,7 @@ using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;

View File

@@ -215,13 +215,10 @@ public sealed class CargoTest
[TestPrototypes]
private const string StackProto = @"
- type: entity
id: A
- type: stack
id: StackProto
name: stack-steel
spawn: A
spawn: StackEnt
- type: entity
id: StackEnt

View File

@@ -64,6 +64,11 @@ public sealed class SolutionRoundingTest
SolutionRoundingTestReagentD: 1
";
private const string SolutionRoundingTestReagentA = "SolutionRoundingTestReagentA";
private const string SolutionRoundingTestReagentB = "SolutionRoundingTestReagentB";
private const string SolutionRoundingTestReagentC = "SolutionRoundingTestReagentC";
private const string SolutionRoundingTestReagentD = "SolutionRoundingTestReagentD";
[Test]
public async Task Test()
{
@@ -84,12 +89,12 @@ public sealed class SolutionRoundingTest
solutionEnt = newSolutionEnt!.Value;
solution = newSolution!;
system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentC", 50));
system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentB", 30));
system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentC, 50));
system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentB, 30));
for (var i = 0; i < 9; i++)
{
system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentA", 10));
system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentA, 10));
}
});
@@ -98,21 +103,21 @@ public sealed class SolutionRoundingTest
Assert.Multiple(() =>
{
Assert.That(
solution.ContainsReagent("SolutionRoundingTestReagentA", null),
solution.ContainsReagent(SolutionRoundingTestReagentA, null),
Is.False,
"Solution should not contain reagent A");
Assert.That(
solution.ContainsReagent("SolutionRoundingTestReagentB", null),
solution.ContainsReagent(SolutionRoundingTestReagentB, null),
Is.False,
"Solution should not contain reagent B");
Assert.That(
solution![new ReagentId("SolutionRoundingTestReagentC", null)].Quantity,
solution![new ReagentId(SolutionRoundingTestReagentC, null)].Quantity,
Is.EqualTo((FixedPoint2) 20));
Assert.That(
solution![new ReagentId("SolutionRoundingTestReagentD", null)].Quantity,
solution![new ReagentId(SolutionRoundingTestReagentD, null)].Quantity,
Is.EqualTo((FixedPoint2) 30));
});
});

View File

@@ -43,6 +43,13 @@ public sealed class SolutionSystemTests
desc: reagent-desc-nothing
physicalDesc: reagent-physical-desc-nothing
";
private const string TestReagentA = "TestReagentA";
private const string TestReagentB = "TestReagentB";
private const string TestReagentC = "TestReagentC";
private const string Water = "Water";
private const string Oil = "Oil";
[Test]
public async Task TryAddTwoNonReactiveReagent()
{
@@ -62,8 +69,8 @@ public sealed class SolutionSystemTests
var oilQuantity = FixedPoint2.New(15);
var waterQuantity = FixedPoint2.New(10);
var oilAdded = new Solution("Oil", oilQuantity);
var originalWater = new Solution("Water", waterQuantity);
var oilAdded = new Solution(Oil, oilQuantity);
var originalWater = new Solution(Water, waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
@@ -73,8 +80,8 @@ public sealed class SolutionSystemTests
Assert.That(containerSystem
.TryAddSolution(solutionEnt.Value, oilAdded));
var water = solution.GetTotalPrototypeQuantity("Water");
var oil = solution.GetTotalPrototypeQuantity("Oil");
var water = solution.GetTotalPrototypeQuantity(Water);
var oil = solution.GetTotalPrototypeQuantity(Oil);
Assert.Multiple(() =>
{
Assert.That(water, Is.EqualTo(waterQuantity));
@@ -107,8 +114,8 @@ public sealed class SolutionSystemTests
var oilQuantity = FixedPoint2.New(1500);
var waterQuantity = FixedPoint2.New(10);
var oilAdded = new Solution("Oil", oilQuantity);
var originalWater = new Solution("Water", waterQuantity);
var oilAdded = new Solution(Oil, oilQuantity);
var originalWater = new Solution(Water, waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
@@ -118,8 +125,8 @@ public sealed class SolutionSystemTests
Assert.That(containerSystem
.TryAddSolution(solutionEnt.Value, oilAdded), Is.False);
var water = solution.GetTotalPrototypeQuantity("Water");
var oil = solution.GetTotalPrototypeQuantity("Oil");
var water = solution.GetTotalPrototypeQuantity(Water);
var oil = solution.GetTotalPrototypeQuantity(Oil);
Assert.Multiple(() =>
{
Assert.That(water, Is.EqualTo(waterQuantity));
@@ -153,8 +160,8 @@ public sealed class SolutionSystemTests
var waterQuantity = FixedPoint2.New(10);
var oilQuantity = FixedPoint2.New(ratio * waterQuantity.Int());
var oilAdded = new Solution("Oil", oilQuantity);
var originalWater = new Solution("Water", waterQuantity);
var oilAdded = new Solution(Oil, oilQuantity);
var originalWater = new Solution(Water, waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
@@ -168,15 +175,15 @@ public sealed class SolutionSystemTests
{
Assert.That(solution.Volume, Is.EqualTo(FixedPoint2.New(threshold)));
var waterMix = solution.GetTotalPrototypeQuantity("Water");
var oilMix = solution.GetTotalPrototypeQuantity("Oil");
var waterMix = solution.GetTotalPrototypeQuantity(Water);
var oilMix = solution.GetTotalPrototypeQuantity(Oil);
Assert.That(waterMix, Is.EqualTo(FixedPoint2.New(threshold / (ratio + 1))));
Assert.That(oilMix, Is.EqualTo(FixedPoint2.New(threshold / (ratio + 1) * ratio)));
Assert.That(overflowingSolution.Volume, Is.EqualTo(FixedPoint2.New(80)));
var waterOverflow = overflowingSolution.GetTotalPrototypeQuantity("Water");
var oilOverFlow = overflowingSolution.GetTotalPrototypeQuantity("Oil");
var waterOverflow = overflowingSolution.GetTotalPrototypeQuantity(Water);
var oilOverFlow = overflowingSolution.GetTotalPrototypeQuantity(Oil);
Assert.That(waterOverflow, Is.EqualTo(waterQuantity - waterMix));
Assert.That(oilOverFlow, Is.EqualTo(oilQuantity - oilMix));
});
@@ -207,8 +214,8 @@ public sealed class SolutionSystemTests
var waterQuantity = FixedPoint2.New(10);
var oilQuantity = FixedPoint2.New(ratio * waterQuantity.Int());
var oilAdded = new Solution("Oil", oilQuantity);
var originalWater = new Solution("Water", waterQuantity);
var oilAdded = new Solution(Oil, oilQuantity);
var originalWater = new Solution(Water, waterQuantity);
beaker = entityManager.SpawnEntity("SolutionTarget", coordinates);
Assert.That(containerSystem
@@ -234,24 +241,23 @@ public sealed class SolutionSystemTests
// Adding reagent with adjusts temperature
await server.WaitAssertion(() =>
{
var solution = new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp };
var solution = new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp };
Assert.That(solution.Temperature, Is.EqualTo(temp * 1));
solution.AddSolution(new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp * 3 }, protoMan);
solution.AddSolution(new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp * 3 }, protoMan);
Assert.That(solution.Temperature, Is.EqualTo(temp * 2));
solution.AddSolution(new Solution("TestReagentB", FixedPoint2.New(100)) { Temperature = temp * 5 }, protoMan);
solution.AddSolution(new Solution(TestReagentB, FixedPoint2.New(100)) { Temperature = temp * 5 }, protoMan);
Assert.That(solution.Temperature, Is.EqualTo(temp * 3));
});
// adding solutions combines thermal energy
await server.WaitAssertion(() =>
{
var solutionOne = new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp };
var solutionOne = new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp };
var solutionTwo = new Solution("TestReagentB", FixedPoint2.New(100)) { Temperature = temp };
solutionTwo.AddReagent("TestReagentC", FixedPoint2.New(100));
var solutionTwo = new Solution(TestReagentB, FixedPoint2.New(100)) { Temperature = temp };
solutionTwo.AddReagent(TestReagentC, FixedPoint2.New(100));
var thermalEnergyOne = solutionOne.GetHeatCapacity(protoMan) * solutionOne.Temperature;
var thermalEnergyTwo = solutionTwo.GetHeatCapacity(protoMan) * solutionTwo.Temperature;

View File

@@ -1,6 +1,8 @@
using Content.Shared.Administration.Systems;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;

View File

@@ -1,6 +1,8 @@
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.Execution;
using Content.Shared.FixedPoint;
using Content.Shared.Ghost;
@@ -280,7 +282,7 @@ public sealed class SuicideCommandTests
await server.WaitAssertion(() =>
{
// Heal all damage first (possible low pressure damage taken)
damageableSystem.SetAllDamage(player, damageableComp, 0);
damageableSystem.ClearAllDamage((player, damageableComp));
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
@@ -355,7 +357,7 @@ public sealed class SuicideCommandTests
await server.WaitAssertion(() =>
{
// Heal all damage first (possible low pressure damage taken)
damageableSystem.SetAllDamage(player, damageableComp, 0);
damageableSystem.ClearAllDamage((player, damageableComp));
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();

View File

@@ -95,8 +95,8 @@ public sealed class CraftingTests : InteractionTest
Assert.That(sys.IsEntityInContainer(shard), Is.True);
Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(rodStack, Has.Count.EqualTo(8));
Assert.That(wireStack, Has.Count.EqualTo(7));
Assert.That(rodStack.Count, Is.EqualTo(8));
Assert.That(wireStack.Count, Is.EqualTo(7));
await FindEntity(Spear, shouldSucceed: false);

View File

@@ -1,6 +1,8 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
@@ -21,7 +23,7 @@ public sealed class WindowRepair : InteractionTest
var damageType = Server.ProtoMan.Index(BluntDamageType);
var damage = new DamageSpecifier(damageType, FixedPoint2.New(10));
Assert.That(comp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero));
await Server.WaitPost(() => sys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));
await Server.WaitPost(() => sys.TryChangeDamage(SEntMan.GetEntity(Target).Value, damage, ignoreResistances: true));
await RunTicks(5);
Assert.That(comp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero));

View File

@@ -1,5 +1,7 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -232,10 +234,14 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
});
// Test SetAll function
sDamageableSystem.SetAllDamage(sDamageableEntity, sDamageableComponent, 10);
// Test SetAll and ClearAll function
sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 10);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(10 * sDamageableComponent.Damage.DamageDict.Count)));
sDamageableSystem.SetAllDamage(sDamageableEntity, sDamageableComponent, 0);
sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 0);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 10);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(10 * sDamageableComponent.Damage.DamageDict.Count)));
sDamageableSystem.ClearAllDamage((sDamageableEntity, sDamageableComponent));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
// Test 'wasted' healing

View File

@@ -1,5 +1,7 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.Destructible.Thresholds.Triggers;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
@@ -130,7 +132,7 @@ namespace Content.IntegrationTests.Tests.Destructible
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal both classes of damage to 0
sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// No new thresholds reached, healing should not trigger it
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
@@ -174,7 +176,7 @@ namespace Content.IntegrationTests.Tests.Destructible
threshold.TriggersOnce = true;
// Heal brute and burn back to 0
sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// No new thresholds reached from healing
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);

View File

@@ -1,5 +1,7 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.Destructible.Thresholds.Triggers;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.Destructible.Thresholds;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;

View File

@@ -3,7 +3,9 @@ using Content.Server.Destructible;
using Content.Server.Destructible.Thresholds;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio.Systems;
using Content.Shared.Destructible;
@@ -124,7 +126,7 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
// Set damage to 0
sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// Damage for 100, up to 100
sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 10, true);
@@ -185,7 +187,7 @@ namespace Content.IntegrationTests.Tests.Destructible
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal all damage
sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// Damage up to 50
sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 5, true);
@@ -247,7 +249,7 @@ namespace Content.IntegrationTests.Tests.Destructible
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal the entity completely
sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0);
sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent));
// Check that the entity has 0 damage
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));

View File

@@ -10,7 +10,7 @@ using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;

View File

@@ -93,7 +93,7 @@ public abstract partial class InteractionTest
await Server.WaitPost(() =>
{
uid = SEntMan.SpawnEntity(stackProto.Spawn, coords);
Stack.SetCount(uid, spec.Quantity);
Stack.SetCount((uid, null), spec.Quantity);
});
return uid;
}

View File

@@ -24,6 +24,7 @@ using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Tests.Interaction;
@@ -39,10 +40,20 @@ namespace Content.IntegrationTests.Tests.Interaction;
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public abstract partial class InteractionTest
{
/// <summary>
/// The prototype that will be spawned for the player entity at <see cref="PlayerCoords"/>.
/// This is not a full humanoid and only has one hand by default.
/// </summary>
protected virtual string PlayerPrototype => "InteractionTestMob";
/// <summary>
/// The map path to load for the integration test.
/// If null an empty map with a single 1x1 plating grid will be generated.
/// </summary>
protected virtual ResPath? TestMapPath => null;
protected TestPair Pair = default!;
protected TestMapData MapData => Pair.TestMap!;
protected TestMapData MapData = default!;
protected RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server;
protected RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client;
@@ -199,7 +210,10 @@ public abstract partial class InteractionTest
CUiSys = CEntMan.System<SharedUserInterfaceSystem>();
// Setup map.
await Pair.CreateTestMap();
if (TestMapPath == null)
MapData = await Pair.CreateTestMap();
else
MapData = await Pair.LoadTestMap(TestMapPath.Value);
PlayerCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)), MapData.MapUid));
TargetCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)), MapData.MapUid));
@@ -214,14 +228,14 @@ public abstract partial class InteractionTest
ServerSession = sPlayerMan.GetSessionById(ClientSession.UserId);
// Spawn player entity & attach
EntityUid? old = default;
NetEntity? old = default;
await Server.WaitPost(() =>
{
// Fuck you mind system I want an hour of my life back
// Mind system is a time vampire
SEntMan.System<SharedMindSystem>().WipeMind(ServerSession.ContentData()?.Mind);
old = cPlayerMan.LocalEntity;
CEntMan.TryGetNetEntity(cPlayerMan.LocalEntity, out old);
SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords));
Player = SEntMan.GetNetEntity(SPlayer);
Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
@@ -237,8 +251,8 @@ public abstract partial class InteractionTest
// Delete old player entity.
await Server.WaitPost(() =>
{
if (old != null)
SEntMan.DeleteEntity(old.Value);
if (SEntMan.TryGetEntity(old, out var uid))
SEntMan.DeleteEntity(uid);
});
// Change UI state to in-game.

View File

@@ -0,0 +1,31 @@
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Interaction;
/// <summary>
/// Makes sure that interaction test helper methods are working as intended.
/// </summary>
public sealed class InteractionTestTests : InteractionTest
{
protected override ResPath? TestMapPath => new("Maps/Test/empty.yml");
/// <summary>
/// Tests that map loading is working correctly.
/// </summary>
[Test]
public void MapLoadingTest()
{
// Make sure that there is only one grid.
var grids = SEntMan.AllEntities<MapGridComponent>().ToList();
Assert.That(grids, Has.Count.EqualTo(1), "Test map did not have exactly one grid.");
Assert.That(grids, Does.Contain(MapData.Grid), "MapData did not contain the loaded grid.");
// Make sure we loaded the right map.
// This name is defined in empty.yml
Assert.That(SEntMan.GetComponent<MetaDataComponent>(MapData.MapUid).EntityName, Is.EqualTo("Empty Debug Map"));
}
}

View File

@@ -1,114 +1,122 @@
using Content.Client.Lobby;
using Content.Server.Preferences.Managers;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Robust.Client.State;
using Robust.Shared.Network;
namespace Content.IntegrationTests.Tests.Lobby
namespace Content.IntegrationTests.Tests.Lobby;
[TestFixture]
[TestOf(typeof(ClientPreferencesManager))]
[TestOf(typeof(ServerPreferencesManager))]
public sealed class CharacterCreationTest
{
[TestFixture]
[TestOf(typeof(ClientPreferencesManager))]
[TestOf(typeof(ServerPreferencesManager))]
public sealed class CharacterCreationTest
[Test]
public async Task CreateDeleteCreateTest()
{
[Test]
public async Task CreateDeleteCreateTest()
await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true });
var server = pair.Server;
var client = pair.Client;
var user = pair.Client.User!.Value;
var clientPrefManager = client.Resolve<IClientPreferencesManager>();
var serverPrefManager = server.Resolve<IServerPreferencesManager>();
Assert.That(client.Resolve<IStateManager>().CurrentState, Is.TypeOf<LobbyState>());
await client.WaitPost(() => clientPrefManager.SelectCharacter(0));
await pair.RunTicksSync(5);
var clientCharacters = clientPrefManager.Preferences?.Characters;
Assert.That(clientCharacters, Is.Not.Null);
Assert.That(clientCharacters, Has.Count.EqualTo(1));
HumanoidCharacterProfile profile = null;
await client.WaitPost(() =>
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true });
var server = pair.Server;
var client = pair.Client;
profile = HumanoidCharacterProfile.Random();
clientPrefManager.CreateCharacter(profile);
});
await pair.RunTicksSync(5);
var clientNetManager = client.ResolveDependency<IClientNetManager>();
var clientStateManager = client.ResolveDependency<IStateManager>();
var clientPrefManager = client.ResolveDependency<IClientPreferencesManager>();
clientCharacters = clientPrefManager.Preferences?.Characters;
Assert.That(clientCharacters, Is.Not.Null);
Assert.That(clientCharacters, Has.Count.EqualTo(2));
AssertEqual(clientCharacters[1], profile);
var serverPrefManager = server.ResolveDependency<IServerPreferencesManager>();
await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 2, maxTicks: 60);
var serverCharacters = serverPrefManager.GetPreferences(user).Characters;
Assert.That(serverCharacters, Has.Count.EqualTo(2));
AssertEqual(serverCharacters[1], profile);
// Need to run them in sync to receive the messages.
await pair.RunTicksSync(1);
await client.WaitAssertion(() => clientPrefManager.DeleteCharacter(1));
await pair.RunTicksSync(5);
Assert.That(clientPrefManager.Preferences?.Characters.Count, Is.EqualTo(1));
await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 1, maxTicks: 60);
Assert.That(serverPrefManager.GetPreferences(user).Characters.Count, Is.EqualTo(1));
await PoolManager.WaitUntil(client, () => clientStateManager.CurrentState is LobbyState, 600);
await client.WaitIdleAsync();
Assert.That(clientNetManager.ServerChannel, Is.Not.Null);
await client.WaitAssertion(() =>
{
profile = HumanoidCharacterProfile.Random();
clientPrefManager.CreateCharacter(profile);
});
await pair.RunTicksSync(5);
var clientNetId = clientNetManager.ServerChannel.UserId;
HumanoidCharacterProfile profile = null;
clientCharacters = clientPrefManager.Preferences?.Characters;
Assert.That(clientCharacters, Is.Not.Null);
Assert.That(clientCharacters, Has.Count.EqualTo(2));
AssertEqual(clientCharacters[1], profile);
await client.WaitAssertion(() =>
{
clientPrefManager.SelectCharacter(0);
await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 2, maxTicks: 60);
serverCharacters = serverPrefManager.GetPreferences(user).Characters;
Assert.That(serverCharacters, Has.Count.EqualTo(2));
AssertEqual(serverCharacters[1], profile);
await pair.CleanReturnAsync();
}
var clientCharacters = clientPrefManager.Preferences?.Characters;
Assert.That(clientCharacters, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(clientCharacters, Has.Count.EqualTo(1));
private void AssertEqual(ICharacterProfile clientCharacter, HumanoidCharacterProfile b)
{
if (clientCharacter.MemberwiseEquals(b))
return;
Assert.That(clientStateManager.CurrentState, Is.TypeOf<LobbyState>());
});
profile = HumanoidCharacterProfile.Random();
clientPrefManager.CreateCharacter(profile);
clientCharacters = clientPrefManager.Preferences?.Characters;
Assert.That(clientCharacters, Is.Not.Null);
Assert.That(clientCharacters, Has.Count.EqualTo(2));
Assert.That(clientCharacters[1].MemberwiseEquals(profile));
});
await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 2, maxTicks: 60);
await server.WaitAssertion(() =>
{
var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters;
Assert.That(serverCharacters, Has.Count.EqualTo(2));
Assert.That(serverCharacters[1].MemberwiseEquals(profile));
});
await client.WaitAssertion(() =>
{
clientPrefManager.DeleteCharacter(1);
var clientCharacters = clientPrefManager.Preferences?.Characters.Count;
Assert.That(clientCharacters, Is.EqualTo(1));
});
await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 1, maxTicks: 60);
await server.WaitAssertion(() =>
{
var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters.Count;
Assert.That(serverCharacters, Is.EqualTo(1));
});
await client.WaitIdleAsync();
await client.WaitAssertion(() =>
{
profile = HumanoidCharacterProfile.Random();
clientPrefManager.CreateCharacter(profile);
var clientCharacters = clientPrefManager.Preferences?.Characters;
Assert.That(clientCharacters, Is.Not.Null);
Assert.That(clientCharacters, Has.Count.EqualTo(2));
Assert.That(clientCharacters[1].MemberwiseEquals(profile));
});
await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 2, maxTicks: 60);
await server.WaitAssertion(() =>
{
var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters;
Assert.That(serverCharacters, Has.Count.EqualTo(2));
Assert.That(serverCharacters[1].MemberwiseEquals(profile));
});
await pair.CleanReturnAsync();
if (clientCharacter is not HumanoidCharacterProfile a)
{
Assert.Fail($"Not a {nameof(HumanoidCharacterProfile)}");
return;
}
Assert.Multiple(() =>
{
Assert.That(a.Name, Is.EqualTo(b.Name));
Assert.That(a.Age, Is.EqualTo(b.Age));
Assert.That(a.Sex, Is.EqualTo(b.Sex));
Assert.That(a.Gender, Is.EqualTo(b.Gender));
Assert.That(a.Species, Is.EqualTo(b.Species));
Assert.That(a.PreferenceUnavailable, Is.EqualTo(b.PreferenceUnavailable));
Assert.That(a.SpawnPriority, Is.EqualTo(b.SpawnPriority));
Assert.That(a.FlavorText, Is.EqualTo(b.FlavorText));
Assert.That(a.JobPriorities, Is.EquivalentTo(b.JobPriorities));
Assert.That(a.AntagPreferences, Is.EquivalentTo(b.AntagPreferences));
Assert.That(a.TraitPreferences, Is.EquivalentTo(b.TraitPreferences));
Assert.That(a.Loadouts, Is.EquivalentTo(b.Loadouts));
AssertEqual(a.Appearance, b.Appearance);
Assert.Fail("Profile not equal");
});
}
private void AssertEqual(HumanoidCharacterAppearance a, HumanoidCharacterAppearance b)
{
if (a.MemberwiseEquals(b))
return;
Assert.That(a.HairStyleId, Is.EqualTo(b.HairStyleId));
Assert.That(a.HairColor, Is.EqualTo(b.HairColor));
Assert.That(a.FacialHairStyleId, Is.EqualTo(b.FacialHairStyleId));
Assert.That(a.FacialHairColor, Is.EqualTo(b.FacialHairColor));
Assert.That(a.EyeColor, Is.EqualTo(b.EyeColor));
Assert.That(a.SkinColor, Is.EqualTo(b.SkinColor));
Assert.That(a.Markings, Is.EquivalentTo(b.Markings));
Assert.Fail("Appearance not equal");
}
}

View File

@@ -28,20 +28,10 @@ namespace Content.IntegrationTests.Tests;
[TestFixture]
public sealed class MaterialArbitrageTest
{
// These recipes are currently broken and need fixing. You should not be adding to these sets.
private readonly HashSet<string> _destructionArbitrageIgnore =
[
"BaseChemistryEmptyVial", "DrinkShotGlass", "SodiumLightTube", "DrinkGlassCoupeShaped",
"LedLightBulb", "ExteriorLightTube", "LightTube", "DrinkGlass", "DimLightBulb", "LightBulb", "LedLightTube",
"ChemistryEmptyBottle01", "WarmLightBulb",
];
private readonly HashSet<string> _compositionArbitrageIgnore =
[
"FoodPlateSmall", "AirTank", "FoodPlateTin", "FoodPlateMuffinTin", "WeaponCapacitorRechargerCircuitboard",
"WeaponCapacitorRechargerCircuitboard", "BorgChargerCircuitboard", "BorgChargerCircuitboard", "FoodPlate",
"CellRechargerCircuitboard", "CellRechargerCircuitboard",
];
// These sets are for selectively excluding recipes from arbitrage.
// You should NOT be adding to these. They exist here for downstreams and potential future issues.
private readonly HashSet<string> _destructionArbitrageIgnore = [];
private readonly HashSet<string> _compositionArbitrageIgnore = [];
[Test]
public async Task NoMaterialArbitrage()
@@ -469,7 +459,8 @@ public sealed class MaterialArbitrageTest
await server.WaitPost(() =>
{
var ent = entManager.SpawnEntity(id, testMap.GridCoords);
stackSys.SetCount(ent, 1);
if (entManager.TryGetComponent<StackComponent>(ent, out var stackComp))
stackSys.SetCount((ent, stackComp), 1);
priceCache[id] = price = pricing.GetPrice(ent, false);
entManager.DeleteEntity(ent);
});

View File

@@ -54,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Materials
$"{proto.ID} material has no stack prototype");
if (stackProto != null)
Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn));
Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn.Id));
}
});

View File

@@ -4,7 +4,9 @@ using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
@@ -147,7 +149,7 @@ public sealed partial class MindTests
var damageable = entMan.GetComponent<DamageableComponent>(entity);
var prototype = protoMan.Index(BluntDamageType);
damageableSystem.SetDamage(entity, damageable, new DamageSpecifier(prototype, FixedPoint2.New(401)));
damageableSystem.SetDamage((entity, damageable), new DamageSpecifier(prototype, FixedPoint2.New(401)));
Assert.That(mindSystem.GetMind(entity, mindContainerComp), Is.EqualTo(mindId));
});

View File

@@ -1,7 +1,6 @@
using Content.IntegrationTests.Tests.Movement;
using Content.Server.NPC.HTN;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Damage.Components;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Mobs;

View File

@@ -1,5 +1,6 @@
using Content.Server.Storage.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;

View File

@@ -2,7 +2,9 @@ using System.Linq;
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.VendingMachines;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.VendingMachines;
using Robust.Shared.Prototypes;
@@ -200,7 +202,7 @@ public sealed class VendingInteractionTest : InteractionTest
// Damage the vending machine to the point that it breaks
var damageType = ProtoMan.Index(TestDamageType);
var damage = new DamageSpecifier(damageType, FixedPoint2.New(100));
await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));
await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target).Value, damage, ignoreResistances: true));
await RunTicks(5);
Assert.That(damageableComp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero), $"{VendingMachineProtoId} did not take damage.");
}

View File

@@ -5,6 +5,7 @@ using Content.Server.Wires;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.Prototypes;
using Content.Shared.Storage.Components;
using Content.Shared.VendingMachines;
@@ -296,14 +297,12 @@ namespace Content.IntegrationTests.Tests
restock = entityManager.SpawnEntity("TestRestockExplode", coordinates);
var damageSpec = new DamageSpecifier(prototypeManager.Index(TestDamageType), 100);
var damageResult = damageableSystem.TryChangeDamage(restock, damageSpec);
var damageResult = damageableSystem.ChangeDamage(restock, damageSpec);
#pragma warning disable NUnit2045
Assert.That(damageResult, Is.Not.Null,
"Received null damageResult when attempting to damage restock box.");
Assert.That(!damageResult.Empty, "Received empty damageResult when attempting to damage restock box.");
Assert.That((int) damageResult!.GetTotal(), Is.GreaterThan(0),
"Box damage result was not greater than 0.");
Assert.That((int) damageResult.GetTotal(), Is.GreaterThan(0), "Box damage result was not greater than 0.");
#pragma warning restore NUnit2045
});
await server.WaitRunTicks(15);

View File

@@ -1,5 +1,5 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.Wieldable.Components;

View File

@@ -12,6 +12,7 @@ using Content.Shared.Chat;
using Content.Shared.Construction;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Roles;
using Content.Shared.StationRecords;

View File

@@ -12,13 +12,16 @@ using Content.Shared.Database;
using Content.Shared.Mind;
using Content.Shared.Players.PlayTimeTracking;
using Prometheus;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Logs;
@@ -338,7 +341,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
Players = players,
};
DoAdminAlerts(players, message, impact);
DoAdminAlerts(players, message, impact, handler);
if (preRound)
{
@@ -380,6 +383,34 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
return players;
}
/// <summary>
/// Get a list of coordinates from the <see cref="LogStringHandler"/>s values. Will transform all coordinate types
/// to map coordinates!
/// </summary>
/// <returns>A list of map coordinates that were found in the value input, can return an empty list.</returns>
private List<MapCoordinates> GetCoordinates(Dictionary<string, object?> values)
{
List<MapCoordinates> coordList = new();
EntityManager.TrySystem(out TransformSystem? transform);
foreach (var value in values.Values)
{
switch (value)
{
case EntityCoordinates entCords:
if (transform != null)
coordList.Add(transform.ToMapCoordinates(entCords));
continue;
case MapCoordinates mapCord:
coordList.Add(mapCord);
continue;
}
}
return coordList;
}
private void AddPlayer(List<AdminLogPlayer> players, Guid user, int logId)
{
// The majority of logs have a single player, or maybe two. Instead of allocating a List<AdminLogPlayer> and
@@ -397,10 +428,11 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
});
}
private void DoAdminAlerts(List<AdminLogPlayer> players, string message, LogImpact impact)
private void DoAdminAlerts(List<AdminLogPlayer> players, string message, LogImpact impact, LogStringHandler handler)
{
var adminLog = false;
var logMessage = message;
var playerNetEnts = new List<(NetEntity, string)>();
foreach (var player in players)
{
@@ -419,6 +451,8 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
("name", cachedInfo.CharacterName),
("subtype", subtype));
}
if (cachedInfo != null && cachedInfo.NetEntity != null)
playerNetEnts.Add((cachedInfo.NetEntity.Value, cachedInfo.CharacterName));
}
if (adminLog)
@@ -442,7 +476,73 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
}
if (adminLog)
{
_chat.SendAdminAlert(logMessage);
if (CreateTpLinks(playerNetEnts, out var tpLinks))
_chat.SendAdminAlertNoFormatOrEscape(tpLinks);
var coords = GetCoordinates(handler.Values);
if (CreateCordLinks(coords, out var cordLinks))
_chat.SendAdminAlertNoFormatOrEscape(cordLinks);
}
}
/// <summary>
/// Creates a list of tpto command links of the given players
/// </summary>
private bool CreateTpLinks(List<(NetEntity NetEnt, string CharacterName)> players, out string outString)
{
outString = string.Empty;
if (players.Count == 0)
return false;
outString = Loc.GetString("admin-alert-tp-to-players-header");
for (var i = 0; i < players.Count; i++)
{
var player = players[i];
outString += $"[cmdlink=\"{EscapeText(player.CharacterName)}\" command=\"tpto {player.NetEnt}\"/]";
if (i < players.Count - 1)
outString += ", ";
}
return true;
}
/// <summary>
/// Creates a list of toto command links for the given map coordinates.
/// </summary>
private bool CreateCordLinks(List<MapCoordinates> cords, out string outString)
{
outString = string.Empty;
if (cords.Count == 0)
return false;
outString = Loc.GetString("admin-alert-tp-to-coords-header");
for (var i = 0; i < cords.Count; i++)
{
var cord = cords[i];
outString += $"[cmdlink=\"{cord.ToString()}\" command=\"tp {cord.X} {cord.Y} {cord.MapId}\"/]";
if (i < cords.Count - 1)
outString += ", ";
}
return true;
}
/// <summary>
/// Escape the given text to not allow breakouts of the cmdlink tags.
/// </summary>
private string EscapeText(string text)
{
return FormattedMessage.EscapeText(text).Replace("\"", "\\\"").Replace("'", "\\'");
}
public async Task<List<SharedAdminLog>> All(LogFilter? filter = null, Func<List<SharedAdminLog>>? listProvider = null)

View File

@@ -1,6 +1,5 @@
using Content.Server.Administration.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Electrocution;
using Content.Server.Explosion.EntitySystems;
@@ -24,7 +23,6 @@ using Content.Shared.Body.Part;
using Content.Shared.Clothing.Components;
using Content.Shared.Clumsy;
using Content.Shared.Cluwne;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Electrocution;
@@ -58,6 +56,7 @@ using Robust.Shared.Spawners;
using Robust.Shared.Utility;
using System.Numerics;
using System.Threading;
using Content.Shared.Damage.Components;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.Administration.Systems;

View File

@@ -413,7 +413,7 @@ public sealed partial class AdminVerbSystem
// Unbounded intentionally.
_quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-adjust-stack"), Loc.GetString("admin-verbs-dialog-adjust-stack-amount", ("max", _stackSystem.GetMaxCount(stack))), (int newAmount) =>
{
_stackSystem.SetCount(args.Target, newAmount, stack);
_stackSystem.SetCount((args.Target, stack), newAmount);
});
},
Impact = LogImpact.Medium,
@@ -429,7 +429,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill-stack.png")),
Act = () =>
{
_stackSystem.SetCount(args.Target, _stackSystem.GetMaxCount(stack), stack);
_stackSystem.SetCount((args.Target, stack), _stackSystem.GetMaxCount(stack));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-fill-stack-description"),

View File

@@ -0,0 +1,69 @@
using Content.Server.Administration;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Atmos.Commands;
[AdminCommand(AdminFlags.Debug)]
public sealed class PauseAtmosCommand : LocalizedEntityCommands
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public override string Command => "pauseatmos";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var grid = default(EntityUid);
switch (args.Length)
{
case 0:
if (!EntityManager.TryGetComponent<TransformComponent>(shell.Player?.AttachedEntity,
out var playerxform) ||
playerxform.GridUid == null)
{
shell.WriteError(Loc.GetString("cmd-error-no-grid-provided-or-invalid-grid"));
return;
}
grid = playerxform.GridUid.Value;
break;
case 1:
if (!EntityUid.TryParse(args[0], out var parsedGrid) || !EntityManager.EntityExists(parsedGrid))
{
shell.WriteError(Loc.GetString("cmd-error-couldnt-parse-entity"));
return;
}
grid = parsedGrid;
break;
}
if (!EntityManager.TryGetComponent<GridAtmosphereComponent>(grid, out var gridAtmos))
{
shell.WriteError(Loc.GetString("cmd-error-no-gridatmosphere"));
return;
}
var newEnt = new Entity<GridAtmosphereComponent>(grid, gridAtmos);
_atmosphereSystem.SetAtmosphereSimulation(newEnt, !newEnt.Comp.Simulated);
shell.WriteLine(Loc.GetString("cmd-pauseatmos-set-atmos-simulation",
("grid", EntityManager.ToPrettyString(grid)),
("state", newEnt.Comp.Simulated)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.Components<GridAtmosphereComponent>(args[0], EntityManager),
Loc.GetString("cmd-pauseatmos-completion-grid-pause"));
}
return CompletionResult.Empty;
}
}

View File

@@ -0,0 +1,104 @@
using Content.Server.Administration;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Atmos.Components;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.Commands;
[AdminCommand(AdminFlags.Debug)]
public sealed class SubstepAtmosCommand : LocalizedEntityCommands
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public override string Command => "substepatmos";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var grid = default(EntityUid);
switch (args.Length)
{
case 0:
if (!EntityManager.TryGetComponent<TransformComponent>(shell.Player?.AttachedEntity,
out var playerxform) ||
playerxform.GridUid == null)
{
shell.WriteError(Loc.GetString("cmd-error-no-grid-provided-or-invalid-grid"));
return;
}
grid = playerxform.GridUid.Value;
break;
case 1:
if (!EntityUid.TryParse(args[0], out var parsedGrid) || !EntityManager.EntityExists(parsedGrid))
{
shell.WriteError(Loc.GetString("cmd-error-couldnt-parse-entity"));
return;
}
grid = parsedGrid;
break;
}
// i'm straight piratesoftwaremaxxing
if (!EntityManager.TryGetComponent<GridAtmosphereComponent>(grid, out var gridAtmos))
{
shell.WriteError(Loc.GetString("cmd-error-no-gridatmosphere"));
return;
}
if (!EntityManager.TryGetComponent<GasTileOverlayComponent>(grid, out var gasTile))
{
shell.WriteError(Loc.GetString("cmd-error-no-gastileoverlay"));
return;
}
if (!EntityManager.TryGetComponent<MapGridComponent>(grid, out var mapGrid))
{
shell.WriteError(Loc.GetString("cmd-error-no-mapgrid"));
return;
}
var xform = EntityManager.GetComponent<TransformComponent>(grid);
if (xform.MapUid == null || xform.MapID == MapId.Nullspace)
{
shell.WriteError(Loc.GetString("cmd-error-no-valid-map"));
return;
}
var newEnt =
new Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>(grid,
gridAtmos,
gasTile,
mapGrid,
xform);
if (gridAtmos.Simulated)
{
shell.WriteLine(Loc.GetString("cmd-substepatmos-info-implicitly-paused-simulation",
("grid", EntityManager.ToPrettyString(grid))));
}
_atmosphereSystem.SetAtmosphereSimulation(newEnt, false);
_atmosphereSystem.RunProcessingFull(newEnt, xform.MapUid.Value, _atmosphereSystem.AtmosTickRate);
shell.WriteLine(Loc.GetString("cmd-substepatmos-info-substepped-grid", ("grid", EntityManager.ToPrettyString(grid))));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.Components<GridAtmosphereComponent>(args[0], EntityManager),
Loc.GetString("cmd-substepatmos-completion-grid-substep"));
}
return CompletionResult.Empty;
}
}

View File

@@ -3,6 +3,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Serialization;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos.Components;
namespace Content.Server.Atmos.Components
{

View File

@@ -46,4 +46,27 @@ public sealed partial class AtmosphereSystem
return processingPaused;
}
/// <summary>
/// Fully runs one <see cref="GridAtmosphereComponent"/> entity through the entire Atmos processing loop.
/// </summary>
/// <param name="ent">The entity to simulate.</param>
/// <param name="mapAtmosphere">The <see cref="MapAtmosphereComponent"/> that belongs to the grid's map.</param>
/// <param name="frameTime">Elapsed time to simulate. Recommended value is <see cref="AtmosTickRate"/>.</param>
public void RunProcessingFull(Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
Entity<MapAtmosphereComponent?> mapAtmosphere,
float frameTime)
{
while (ProcessAtmosphere(ent, mapAtmosphere, frameTime) != AtmosphereProcessingCompletionState.Finished) { }
}
/// <summary>
/// Allows or disallows atmosphere simulation on a <see cref="GridAtmosphereComponent"/>.
/// </summary>
/// <param name="ent">The atmosphere to pause or unpause processing.</param>
/// <param name="simulate">The state to set. True means that the atmosphere is allowed to simulate, false otherwise.</param>
public void SetAtmosphereSimulation(Entity<GridAtmosphereComponent> ent, bool simulate)
{
ent.Comp.Simulated = simulate;
}
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Damage;
using Robust.Shared.Random;
using Robust.Shared.Threading;
@@ -175,7 +176,7 @@ public sealed partial class AtmosphereSystem
/// containing the queue.</param>
/// <param name="pressure">The current absolute pressure being experienced by the entity.</param>
/// <param name="delta">The current delta pressure being experienced by the entity.</param>
private static void EnqueueDeltaPressureDamage(Entity<DeltaPressureComponent> ent,
private void EnqueueDeltaPressureDamage(Entity<DeltaPressureComponent> ent,
GridAtmosphereComponent gridAtmosComp,
float pressure,
float delta)
@@ -184,7 +185,7 @@ public sealed partial class AtmosphereSystem
var aboveMinDeltaPressure = delta > ent.Comp.MinPressureDelta;
if (!aboveMinPressure && !aboveMinDeltaPressure)
{
ent.Comp.IsTakingDamage = false;
SetIsTakingDamageState(ent, false);
return;
}
@@ -250,8 +251,21 @@ public sealed partial class AtmosphereSystem
var maxPressureCapped = Math.Min(maxPressure, ent.Comp.MaxEffectivePressure);
var appliedDamage = ScaleDamage(ent, ent.Comp.BaseDamage, maxPressureCapped);
_damage.TryChangeDamage(ent, appliedDamage, ignoreResistances: true, interruptsDoAfters: false);
ent.Comp.IsTakingDamage = true;
_damage.ChangeDamage(ent.Owner, appliedDamage, ignoreResistances: true, interruptsDoAfters: false);
SetIsTakingDamageState(ent, true);
}
/// <summary>
/// Helper function to prevent spamming clients with dirty events when the damage state hasn't changed.
/// </summary>
/// <param name="ent">The entity to check.</param>
/// <param name="toSet">The value to set.</param>
private void SetIsTakingDamageState(Entity<DeltaPressureComponent> ent, bool toSet)
{
if (ent.Comp.IsTakingDamage == toSet)
return;
ent.Comp.IsTakingDamage = toSet;
Dirty(ent);
}
/// <summary>

View File

@@ -4,148 +4,199 @@ using Content.Shared.Atmos.Components;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.Server.Atmos.EntitySystems
namespace Content.Server.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
public sealed partial class AtmosphereSystem
/*
Handles Excited Groups, an optimization routine executed during LINDA
that groups active tiles together.
Groups of active tiles that have very low mole deltas between them
are dissolved after a cooldown period, performing a final equalization
on all tiles in the group before deactivating them.
If tiles are so close together in pressure that the final equalization
would result in negligible gas transfer, the group is dissolved without
performing an equalization.
This prevents LINDA from constantly transferring tiny amounts of gas
between tiles that are already nearly equalized.
*/
/// <summary>
/// Adds a tile to an <see cref="ExcitedGroups"/>, resetting the group's cooldowns in the process.
/// </summary>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to add the tile to.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to add.</param>
private void ExcitedGroupAddTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
{
private void ExcitedGroupAddTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(tile.ExcitedGroup == null, "Tried to add a tile to an excited group when it's already in another one!");
excitedGroup.Tiles.Add(tile);
tile.ExcitedGroup = excitedGroup;
ExcitedGroupResetCooldowns(excitedGroup);
}
/// <summary>
/// Removes a tile from an <see cref="ExcitedGroups"/>.
/// </summary>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to remove the tile from.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to remove.</param>
private void ExcitedGroupRemoveTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(tile.ExcitedGroup == excitedGroup, "Tried to remove a tile from an excited group it's not present in!");
tile.ExcitedGroup = null;
excitedGroup.Tiles.Remove(tile);
}
/// <summary>
/// Merges two <see cref="ExcitedGroups"/>, transferring all tiles from one to the other.
/// The larger group receives the tiles of the smaller group.
/// The smaller group is then disposed of without deactivating its tiles.
/// </summary>
/// <param name="gridAtmosphere">The <see cref="GridAtmosphereComponent"/> of the grid.</param>
/// <param name="ourGroup">The first <see cref="ExcitedGroups"/> to merge.</param>
/// <param name="otherGroup">The second <see cref="ExcitedGroups"/> to merge.</param>
private void ExcitedGroupMerge(GridAtmosphereComponent gridAtmosphere, ExcitedGroup ourGroup, ExcitedGroup otherGroup)
{
DebugTools.Assert(!ourGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(!otherGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(ourGroup), "Grid Atmosphere does not contain Excited Group!");
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(otherGroup), "Grid Atmosphere does not contain Excited Group!");
var ourSize = ourGroup.Tiles.Count;
var otherSize = otherGroup.Tiles.Count;
ExcitedGroup winner;
ExcitedGroup loser;
if (ourSize > otherSize)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(tile.ExcitedGroup == null, "Tried to add a tile to an excited group when it's already in another one!");
excitedGroup.Tiles.Add(tile);
tile.ExcitedGroup = excitedGroup;
ExcitedGroupResetCooldowns(excitedGroup);
winner = ourGroup;
loser = otherGroup;
}
else
{
winner = otherGroup;
loser = ourGroup;
}
private void ExcitedGroupRemoveTile(ExcitedGroup excitedGroup, TileAtmosphere tile)
foreach (var tile in loser.Tiles)
{
tile.ExcitedGroup = winner;
winner.Tiles.Add(tile);
}
loser.Tiles.Clear();
ExcitedGroupDispose(gridAtmosphere, loser);
ExcitedGroupResetCooldowns(winner);
}
/// <summary>
/// Resets the cooldowns of an excited group.
/// </summary>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to reset cooldowns for.</param>
private void ExcitedGroupResetCooldowns(ExcitedGroup excitedGroup)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
excitedGroup.BreakdownCooldown = 0;
excitedGroup.DismantleCooldown = 0;
}
/// <summary>
/// Performs a final equalization on all tiles in an excited group before deactivating it.
/// </summary>
/// <param name="ent">The grid.</param>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to equalize and dissolve.</param>
private void ExcitedGroupSelfBreakdown(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
ExcitedGroup excitedGroup)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(ent.Comp1.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
var combined = new GasMixture(Atmospherics.CellVolume);
var tileSize = excitedGroup.Tiles.Count;
if (excitedGroup.Disposed)
return;
if (tileSize == 0)
{
ExcitedGroupDispose(ent.Comp1, excitedGroup);
return;
}
// Combine all gasses in the group into a single mixture
// for distribution into each individual tile.
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null)
continue;
Merge(combined, tile.Air);
// If this tile is space and space is all-consuming, the final equalization
// will result in a vacuum, so we can skip the rest of the equalization.
if (!ExcitedGroupsSpaceIsAllConsuming || !tile.Space)
continue;
combined.Clear();
break;
}
combined.Multiply(1 / (float)tileSize);
// Distribute the combined mixture evenly to all tiles in the group.
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null)
continue;
tile.Air.CopyFrom(combined);
InvalidateVisuals(ent, tile);
}
excitedGroup.BreakdownCooldown = 0;
}
/// <summary>
/// Deactivates and removes all tiles from an excited group without performing a final equalization.
/// Used when an excited group is expected to be nearly equalized already to avoid unnecessary processing.
/// </summary>
/// <param name="gridAtmosphere">The <see cref="GridAtmosphereComponent"/> of the grid.</param>
/// <param name="excitedGroup">The <see cref="ExcitedGroups"/> to dissolve.</param>
private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
foreach (var tile in excitedGroup.Tiles)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(tile.ExcitedGroup == excitedGroup, "Tried to remove a tile from an excited group it's not present in!");
tile.ExcitedGroup = null;
excitedGroup.Tiles.Remove(tile);
RemoveActiveTile(gridAtmosphere, tile);
}
private void ExcitedGroupMerge(GridAtmosphereComponent gridAtmosphere, ExcitedGroup ourGroup, ExcitedGroup otherGroup)
excitedGroup.Tiles.Clear();
}
/// <summary>
/// Removes and disposes of an excited group without performing any final equalization
/// or deactivation of its tiles.
/// </summary>
private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
if (excitedGroup.Disposed)
return;
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
excitedGroup.Disposed = true;
gridAtmosphere.ExcitedGroups.Remove(excitedGroup);
foreach (var tile in excitedGroup.Tiles)
{
DebugTools.Assert(!ourGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(!otherGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(ourGroup), "Grid Atmosphere does not contain Excited Group!");
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(otherGroup), "Grid Atmosphere does not contain Excited Group!");
var ourSize = ourGroup.Tiles.Count;
var otherSize = otherGroup.Tiles.Count;
ExcitedGroup winner;
ExcitedGroup loser;
if (ourSize > otherSize)
{
winner = ourGroup;
loser = otherGroup;
}
else
{
winner = otherGroup;
loser = ourGroup;
}
foreach (var tile in loser.Tiles)
{
tile.ExcitedGroup = winner;
winner.Tiles.Add(tile);
}
loser.Tiles.Clear();
ExcitedGroupDispose(gridAtmosphere, loser);
ExcitedGroupResetCooldowns(winner);
tile.ExcitedGroup = null;
}
private void ExcitedGroupResetCooldowns(ExcitedGroup excitedGroup)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
excitedGroup.BreakdownCooldown = 0;
excitedGroup.DismantleCooldown = 0;
}
private void ExcitedGroupSelfBreakdown(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
ExcitedGroup excitedGroup)
{
DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!");
DebugTools.Assert(ent.Comp1.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
var combined = new GasMixture(Atmospherics.CellVolume);
var tileSize = excitedGroup.Tiles.Count;
if (excitedGroup.Disposed)
return;
if (tileSize == 0)
{
ExcitedGroupDispose(ent.Comp1, excitedGroup);
return;
}
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null)
continue;
Merge(combined, tile.Air);
if (!ExcitedGroupsSpaceIsAllConsuming || !tile.Space)
continue;
combined.Clear();
break;
}
combined.Multiply(1 / (float)tileSize);
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null)
continue;
tile.Air.CopyFrom(combined);
InvalidateVisuals(ent, tile);
}
excitedGroup.BreakdownCooldown = 0;
}
/// <summary>
/// This de-activates and removes all tiles in an excited group.
/// </summary>
private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
foreach (var tile in excitedGroup.Tiles)
{
tile.ExcitedGroup = null;
RemoveActiveTile(gridAtmosphere, tile);
}
excitedGroup.Tiles.Clear();
}
/// <summary>
/// This removes an excited group without de-activating its tiles.
/// </summary>
private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
if (excitedGroup.Disposed)
return;
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
excitedGroup.Disposed = true;
gridAtmosphere.ExcitedGroups.Remove(excitedGroup);
foreach (var tile in excitedGroup.Tiles)
{
tile.ExcitedGroup = null;
}
excitedGroup.Tiles.Clear();
}
excitedGroup.Tiles.Clear();
}
}

View File

@@ -10,198 +10,280 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Atmos.EntitySystems
namespace Content.Server.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
public sealed partial class AtmosphereSystem
/*
Handles Hotspots, which are gas-based tile fires that slowly grow and spread
to adjacent tiles if conditions are met.
You can think of a hotspot as a small flame on a tile that
grows by consuming a fuel and oxidizer from the tile's air,
with a certain volume and temperature.
This volume grows bigger and bigger as the fire continues,
until it effectively engulfs the entire tile, at which point
it starts spreading to adjacent tiles by radiating heat.
*/
/// <summary>
/// Collection of hotspot sounds to play.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultHotspotSounds = "AtmosHotspot";
[Dependency] private readonly DecalSystem _decalSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
/// <summary>
/// Number of cycles the hotspot system must process before it can play another sound
/// on a hotspot.
/// </summary>
private const int HotspotSoundCooldownCycles = 200;
/// <summary>
/// Cooldown counter for hotspot sounds.
/// </summary>
private int _hotspotSoundCooldown = 0;
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier? HotspotSound = new SoundCollectionSpecifier(DefaultHotspotSounds);
/// <summary>
/// Processes a hotspot on a <see cref="TileAtmosphere"/>.
/// </summary>
/// <param name="ent">The grid entity that belongs to the tile to process.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to process.</param>
private void ProcessHotspot(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile)
{
private static readonly ProtoId<SoundCollectionPrototype> DefaultHotspotSounds = "AtmosHotspot";
var gridAtmosphere = ent.Comp1;
[Dependency] private readonly DecalSystem _decalSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private const int HotspotSoundCooldownCycles = 200;
private int _hotspotSoundCooldown = 0;
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier? HotspotSound { get; private set; } = new SoundCollectionSpecifier(DefaultHotspotSounds);
private void ProcessHotspot(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile)
// Hotspots that have fizzled out are assigned a new Hotspot struct
// with Valid set to false, so we can just check that here in
// one central place instead of manually removing it everywhere.
if (!tile.Hotspot.Valid)
{
var gridAtmosphere = ent.Comp1;
if (!tile.Hotspot.Valid)
gridAtmosphere.HotspotTiles.Remove(tile);
return;
}
AddActiveTile(gridAtmosphere, tile);
// Prevent the hotspot from processing on the same cycle it was created (???)
// TODO ATMOS: Is this even necessary anymore? The queue is kept per processing stage
// and is not updated until tne next cycle, so the condition of a hotspot being created
// and processed in the same cycle is impossible.
if (!tile.Hotspot.SkippedFirstProcess)
{
tile.Hotspot.SkippedFirstProcess = true;
return;
}
if (tile.ExcitedGroup != null)
ExcitedGroupResetCooldowns(tile.ExcitedGroup);
if (tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist ||
tile.Hotspot.Volume <= 1f ||
tile.Air == null ||
tile.Air.GetMoles(Gas.Oxygen) < 0.5f ||
tile.Air.GetMoles(Gas.Plasma) < 0.5f && tile.Air.GetMoles(Gas.Tritium) < 0.5f)
{
tile.Hotspot = new Hotspot();
InvalidateVisuals(ent, tile);
return;
}
PerformHotspotExposure(tile);
// This tile has now turned into a full-blown tile-fire.
// Start applying fire effects and spreading to adjacent tiles.
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.State = 3;
var gridUid = ent.Owner;
var tilePos = tile.GridIndices;
// Get the existing decals on the tile
var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos);
// Count the burnt decals on the tile
var tileBurntDecals = 0;
foreach (var set in tileDecals)
{
gridAtmosphere.HotspotTiles.Remove(tile);
return;
if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1)
continue;
tileBurntDecals++;
if (tileBurntDecals > 4)
break;
}
AddActiveTile(gridAtmosphere, tile);
if (!tile.Hotspot.SkippedFirstProcess)
// Add a random burned decal to the tile only if there are less than 4 of them
if (tileBurntDecals < 4)
{
tile.Hotspot.SkippedFirstProcess = true;
return;
_decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)],
new EntityCoordinates(gridUid, tilePos),
out _,
cleanable: true);
}
if(tile.ExcitedGroup != null)
ExcitedGroupResetCooldowns(tile.ExcitedGroup);
if ((tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist) || (tile.Hotspot.Volume <= 1f)
|| tile.Air == null || tile.Air.GetMoles(Gas.Oxygen) < 0.5f || (tile.Air.GetMoles(Gas.Plasma) < 0.5f && tile.Air.GetMoles(Gas.Tritium) < 0.5f))
if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread)
{
tile.Hotspot = new Hotspot();
InvalidateVisuals(ent, tile);
return;
}
PerformHotspotExposure(tile);
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.State = 3;
var gridUid = ent.Owner;
var tilePos = tile.GridIndices;
// Get the existing decals on the tile
var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos);
// Count the burnt decals on the tile
var tileBurntDecals = 0;
foreach (var set in tileDecals)
var radiatedTemperature = tile.Air.Temperature * Atmospherics.FireSpreadRadiosityScale;
foreach (var otherTile in tile.AdjacentTiles)
{
if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1)
// TODO ATMOS: This is sus. Suss this out.
// Spread this fire to other tiles by exposing them to a hotspot if air can flow there.
// Unsure as to why this is sus.
if (otherTile == null)
continue;
tileBurntDecals++;
if (tileBurntDecals > 4)
break;
if (!otherTile.Hotspot.Valid)
HotspotExpose(gridAtmosphere, otherTile, radiatedTemperature, Atmospherics.CellVolume / 4);
}
// Add a random burned decal to the tile only if there are less than 4 of them
if (tileBurntDecals < 4)
_decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], new EntityCoordinates(gridUid, tilePos), out _, cleanable: true);
if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread)
{
var radiatedTemperature = tile.Air.Temperature * Atmospherics.FireSpreadRadiosityScale;
foreach (var otherTile in tile.AdjacentTiles)
{
// TODO ATMOS: This is sus. Suss this out.
if (otherTile == null)
continue;
if(!otherTile.Hotspot.Valid)
HotspotExpose(gridAtmosphere, otherTile, radiatedTemperature, Atmospherics.CellVolume/4);
}
}
}
else
{
tile.Hotspot.State = (byte) (tile.Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1);
}
if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained)
tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature;
if (_hotspotSoundCooldown++ == 0 && HotspotSound != null)
{
var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
// A few details on the audio parameters for fire.
// The greater the fire state, the lesser the pitch variation.
// The greater the fire state, the greater the volume.
_audio.PlayPvs(HotspotSound, coordinates, HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State).WithVolume(-5f + 5f * tile.Hotspot.State));
}
if (_hotspotSoundCooldown > HotspotSoundCooldownCycles)
_hotspotSoundCooldown = 0;
// TODO ATMOS Maybe destroy location here?
}
private void HotspotExpose(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile,
float exposedTemperature, float exposedVolume, bool soh = false, EntityUid? sparkSourceUid = null)
{
if (tile.Air == null)
return;
var oxygen = tile.Air.GetMoles(Gas.Oxygen);
if (oxygen < 0.5f)
return;
var plasma = tile.Air.GetMoles(Gas.Plasma);
var tritium = tile.Air.GetMoles(Gas.Tritium);
if (tile.Hotspot.Valid)
{
if (soh)
{
if (plasma > 0.5f || tritium > 0.5f)
{
if (tile.Hotspot.Temperature < exposedTemperature)
tile.Hotspot.Temperature = exposedTemperature;
if (tile.Hotspot.Volume < exposedVolume)
tile.Hotspot.Volume = exposedVolume;
}
}
return;
}
if ((exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature) && (plasma > 0.5f || tritium > 0.5f))
{
if (sparkSourceUid.HasValue)
_adminLog.Add(LogType.Flammable, LogImpact.High, $"Heat/spark of {ToPrettyString(sparkSourceUid.Value)} caused atmos ignition of gas: {tile.Air.Temperature.ToString():temperature}K - {oxygen}mol Oxygen, {plasma}mol Plasma, {tritium}mol Tritium");
tile.Hotspot = new Hotspot
{
Volume = exposedVolume * 25f,
Temperature = exposedTemperature,
SkippedFirstProcess = tile.CurrentCycle > gridAtmosphere.UpdateCounter,
Valid = true,
State = 1
};
AddActiveTile(gridAtmosphere, tile);
gridAtmosphere.HotspotTiles.Add(tile);
}
}
private void PerformHotspotExposure(TileAtmosphere tile)
else
{
if (tile.Air == null || !tile.Hotspot.Valid) return;
// Little baby fire. Set the sprite state based on the current size of the fire.
tile.Hotspot.State = (byte)(tile.Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1);
}
tile.Hotspot.Bypassing = tile.Hotspot.SkippedFirstProcess && tile.Hotspot.Volume > tile.Air.Volume*0.95f;
if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained)
tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature;
if (tile.Hotspot.Bypassing)
if (_hotspotSoundCooldown++ == 0 && HotspotSound != null)
{
var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
// A few details on the audio parameters for fire.
// The greater the fire state, the lesser the pitch variation.
// The greater the fire state, the greater the volume.
_audio.PlayPvs(HotspotSound,
coordinates,
HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State)
.WithVolume(-5f + 5f * tile.Hotspot.State));
}
if (_hotspotSoundCooldown > HotspotSoundCooldownCycles)
_hotspotSoundCooldown = 0;
// TODO ATMOS Maybe destroy location here?
}
/// <summary>
/// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met.
/// </summary>
/// <param name="gridAtmosphere">The <see cref="GridAtmosphereComponent"/> of the grid the tile is on.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to expose.</param>
/// <param name="exposedTemperature">The temperature of the hotspot to expose.
/// You can think of this as exposing a temperature of a flame.</param>
/// <param name="exposedVolume">The volume of the hotspot to expose.
/// You can think of this as how big the flame is initially.
/// Bigger flames will ramp a fire faster.</param>
/// <param name="soh">Whether to "boost" a fire that's currently on the tile already.
/// Does nothing if the tile isn't already a hotspot.
/// This clamps the temperature and volume of the hotspot to the maximum
/// of the provided parameters and whatever's on the tile.</param>
/// <param name="sparkSourceUid">Entity that started the exposure for admin logging.</param>
private void HotspotExpose(GridAtmosphereComponent gridAtmosphere,
TileAtmosphere tile,
float exposedTemperature,
float exposedVolume,
bool soh = false,
EntityUid? sparkSourceUid = null)
{
if (tile.Air == null)
return;
var oxygen = tile.Air.GetMoles(Gas.Oxygen);
if (oxygen < 0.5f)
return;
var plasma = tile.Air.GetMoles(Gas.Plasma);
var tritium = tile.Air.GetMoles(Gas.Tritium);
if (tile.Hotspot.Valid)
{
if (soh)
{
tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Temperature = tile.Air.Temperature;
}
else
{
var affected = tile.Air.RemoveVolume(tile.Hotspot.Volume);
affected.Temperature = tile.Hotspot.Temperature;
React(affected, tile);
tile.Hotspot.Temperature = affected.Temperature;
tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
Merge(tile.Air, affected);
if (plasma > 0.5f || tritium > 0.5f)
{
tile.Hotspot.Temperature = MathF.Max(tile.Hotspot.Temperature, exposedTemperature);
tile.Hotspot.Volume = MathF.Max(tile.Hotspot.Volume, exposedVolume);
}
}
var fireEvent = new TileFireEvent(tile.Hotspot.Temperature, tile.Hotspot.Volume);
_entSet.Clear();
_lookup.GetLocalEntitiesIntersecting(tile.GridIndex, tile.GridIndices, _entSet, 0f);
return;
}
foreach (var entity in _entSet)
if (exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature && (plasma > 0.5f || tritium > 0.5f))
{
if (sparkSourceUid.HasValue)
{
RaiseLocalEvent(entity, ref fireEvent);
_adminLog.Add(LogType.Flammable,
LogImpact.High,
$"Heat/spark of {ToPrettyString(sparkSourceUid.Value)} caused atmos ignition of gas: {tile.Air.Temperature.ToString():temperature}K - {oxygen}mol Oxygen, {plasma}mol Plasma, {tritium}mol Tritium");
}
tile.Hotspot = new Hotspot
{
Volume = exposedVolume * 25f,
Temperature = exposedTemperature,
SkippedFirstProcess = tile.CurrentCycle > gridAtmosphere.UpdateCounter,
Valid = true,
State = 1
};
AddActiveTile(gridAtmosphere, tile);
gridAtmosphere.HotspotTiles.Add(tile);
}
}
/// <summary>
/// Performs hotspot exposure processing on a <see cref="TileAtmosphere"/>.
/// </summary>
/// <param name="tile">The <see cref="TileAtmosphere"/> to process.</param>
private void PerformHotspotExposure(TileAtmosphere tile)
{
if (tile.Air == null || !tile.Hotspot.Valid)
return;
// Determine if the tile has become a full-blown fire if the volume of the fire has effectively reached
// the volume of the tile's air.
tile.Hotspot.Bypassing = tile.Hotspot.SkippedFirstProcess && tile.Hotspot.Volume > tile.Air.Volume * 0.95f;
// If the tile is effectively a full fire, use the tile's air for reactions, don't bother partitioning.
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Temperature = tile.Air.Temperature;
}
// Otherwise, pull out a fraction of the tile's air (the current hotspot volume) to perform reactions on.
else
{
var affected = tile.Air.RemoveVolume(tile.Hotspot.Volume);
affected.Temperature = tile.Hotspot.Temperature;
React(affected, tile);
tile.Hotspot.Temperature = affected.Temperature;
// Scale the fire based on the type of reaction that occured.
tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
Merge(tile.Air, affected);
}
var fireEvent = new TileFireEvent(tile.Hotspot.Temperature, tile.Hotspot.Volume);
_entSet.Clear();
_lookup.GetLocalEntitiesIntersecting(tile.GridIndex, tile.GridIndices, _entSet, 0f);
foreach (var entity in _entSet)
{
RaiseLocalEvent(entity, ref fireEvent);
}
}
}

View File

@@ -129,9 +129,16 @@ namespace Content.Server.Atmos.EntitySystems
switch (tile.LastShare)
{
// Refresh this tile's suspension cooldown if it had significant sharing.
case > Atmospherics.MinimumAirToSuspend:
ExcitedGroupResetCooldowns(tile.ExcitedGroup);
break;
// If this tile moved a very small amount of air, but not enough to matter,
// we set the dismantle cooldown to 0.
// This dissolves the group without performing an equalization as we expect
// the group to be mostly equalized already if we're moving around miniscule
// amounts of air.
case > Atmospherics.MinimumMolesDeltaToMove:
tile.ExcitedGroup.DismantleCooldown = 0;
break;

View File

@@ -365,7 +365,6 @@ namespace Content.Server.Atmos.EntitySystems
ExcitedGroupSelfBreakdown(ent, excitedGroup);
else if (excitedGroup.DismantleCooldown > Atmospherics.ExcitedGroupsDismantleCycles)
DeactivateGroupTiles(gridAtmosphere, excitedGroup);
// TODO ATMOS. What is the point of this? why is this only de-exciting the group? Shouldn't it also dismantle it?
if (number++ < LagCheckIterations)
continue;
@@ -649,143 +648,196 @@ namespace Content.Server.Atmos.EntitySystems
if (atmosphere.LifeStage >= ComponentLifeStage.Stopping || Paused(owner) || !atmosphere.Simulated)
continue;
atmosphere.Timer += frameTime;
if (atmosphere.Timer < AtmosTime)
continue;
// We subtract it so it takes lost time into account.
atmosphere.Timer -= AtmosTime;
var map = new Entity<MapAtmosphereComponent?>(xform.MapUid.Value, _mapAtmosQuery.CompOrNull(xform.MapUid.Value));
switch (atmosphere.State)
var completionState = ProcessAtmosphere(ent, map, frameTime);
switch (completionState)
{
case AtmosphereProcessingState.Revalidate:
if (!ProcessRevalidate(ent))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
// Next state depends on whether monstermos equalization is enabled or not.
// Note: We do this here instead of on the tile equalization step to prevent ending it early.
// Therefore, a change to this CVar might only be applied after that step is over.
atmosphere.State = MonstermosEqualization
? AtmosphereProcessingState.TileEqualize
: AtmosphereProcessingState.ActiveTiles;
case AtmosphereProcessingCompletionState.Return:
return;
case AtmosphereProcessingCompletionState.Continue:
continue;
case AtmosphereProcessingState.TileEqualize:
if (!ProcessTileEqualize(ent))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.ActiveTiles;
continue;
case AtmosphereProcessingState.ActiveTiles:
if (!ProcessActiveTiles(ent))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
// Next state depends on whether excited groups are enabled or not.
atmosphere.State = ExcitedGroups ? AtmosphereProcessingState.ExcitedGroups : AtmosphereProcessingState.HighPressureDelta;
continue;
case AtmosphereProcessingState.ExcitedGroups:
if (!ProcessExcitedGroups(ent))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.HighPressureDelta;
continue;
case AtmosphereProcessingState.HighPressureDelta:
if (!ProcessHighPressureDelta((ent, ent)))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = DeltaPressureDamage
? AtmosphereProcessingState.DeltaPressure
: AtmosphereProcessingState.Hotspots;
continue;
case AtmosphereProcessingState.DeltaPressure:
if (!ProcessDeltaPressure(ent))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.Hotspots;
continue;
case AtmosphereProcessingState.Hotspots:
if (!ProcessHotspots(ent))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
// Next state depends on whether superconduction is enabled or not.
// Note: We do this here instead of on the tile equalization step to prevent ending it early.
// Therefore, a change to this CVar might only be applied after that step is over.
atmosphere.State = Superconduction
? AtmosphereProcessingState.Superconductivity
: AtmosphereProcessingState.PipeNet;
continue;
case AtmosphereProcessingState.Superconductivity:
if (!ProcessSuperconductivity(atmosphere))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.PipeNet;
continue;
case AtmosphereProcessingState.PipeNet:
if (!ProcessPipeNets(atmosphere))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.AtmosDevices;
continue;
case AtmosphereProcessingState.AtmosDevices:
if (!ProcessAtmosDevices(ent, map))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.Revalidate;
// We reached the end of this atmosphere's update tick. Break out of the switch.
case AtmosphereProcessingCompletionState.Finished:
break;
}
// And increase the update counter.
atmosphere.UpdateCounter++;
}
// We finished processing all atmospheres successfully, therefore we won't be paused next tick.
_simulationPaused = false;
}
/// <summary>
/// Processes a <see cref="GridAtmosphereComponent"/> through its processing stages.
/// </summary>
/// <param name="ent">The entity to process.</param>
/// <param name="mapAtmosphere">The <see cref="MapAtmosphereComponent"/> belonging to the
/// <see cref="GridAtmosphereComponent"/>'s map.</param>
/// <param name="frameTime">The elapsed time since the last frame.</param>
/// <returns>An <see cref="AtmosphereProcessingCompletionState"/> that represents the completion state.</returns>
private AtmosphereProcessingCompletionState ProcessAtmosphere(Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
Entity<MapAtmosphereComponent?> mapAtmosphere,
float frameTime)
{
// They call me the deconstructor the way i be deconstructing it
// and by it, i mean... my entity
var (owner, atmosphere, visuals, grid, xform) = ent;
atmosphere.Timer += frameTime;
if (atmosphere.Timer < AtmosTime)
return AtmosphereProcessingCompletionState.Continue;
// We subtract it so it takes lost time into account.
atmosphere.Timer -= AtmosTime;
switch (atmosphere.State)
{
case AtmosphereProcessingState.Revalidate:
if (!ProcessRevalidate(ent))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
// Next state depends on whether monstermos equalization is enabled or not.
// Note: We do this here instead of on the tile equalization step to prevent ending it early.
// Therefore, a change to this CVar might only be applied after that step is over.
atmosphere.State = MonstermosEqualization
? AtmosphereProcessingState.TileEqualize
: AtmosphereProcessingState.ActiveTiles;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.TileEqualize:
if (!ProcessTileEqualize(ent))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.ActiveTiles;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.ActiveTiles:
if (!ProcessActiveTiles(ent))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
// Next state depends on whether excited groups are enabled or not.
atmosphere.State = ExcitedGroups ? AtmosphereProcessingState.ExcitedGroups : AtmosphereProcessingState.HighPressureDelta;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.ExcitedGroups:
if (!ProcessExcitedGroups(ent))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.HighPressureDelta;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.HighPressureDelta:
if (!ProcessHighPressureDelta((ent, ent)))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = DeltaPressureDamage
? AtmosphereProcessingState.DeltaPressure
: AtmosphereProcessingState.Hotspots;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.DeltaPressure:
if (!ProcessDeltaPressure(ent))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.Hotspots;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.Hotspots:
if (!ProcessHotspots(ent))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
// Next state depends on whether superconduction is enabled or not.
// Note: We do this here instead of on the tile equalization step to prevent ending it early.
// Therefore, a change to this CVar might only be applied after that step is over.
atmosphere.State = Superconduction
? AtmosphereProcessingState.Superconductivity
: AtmosphereProcessingState.PipeNet;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.Superconductivity:
if (!ProcessSuperconductivity(atmosphere))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.PipeNet;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.PipeNet:
if (!ProcessPipeNets(atmosphere))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.AtmosDevices;
return AtmosphereProcessingCompletionState.Continue;
case AtmosphereProcessingState.AtmosDevices:
if (!ProcessAtmosDevices(ent, mapAtmosphere))
{
atmosphere.ProcessingPaused = true;
return AtmosphereProcessingCompletionState.Return;
}
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.Revalidate;
// We reached the end of this atmosphere's update tick. Break out of the switch.
break;
}
atmosphere.UpdateCounter++;
return AtmosphereProcessingCompletionState.Finished;
}
}
/// <summary>
/// An enum representing the completion state of a <see cref="GridAtmosphereComponent"/>'s processing steps.
/// The processing of a <see cref="GridAtmosphereComponent"/> spans over multiple stages and sticks,
/// with the method handling the processing having multiple return types.
/// </summary>
public enum AtmosphereProcessingCompletionState : byte
{
/// <summary>
/// Method is returning, ex. due to delegating processing to the next tick.
/// </summary>
Return,
/// <summary>
/// Method is continuing, ex. due to finishing a single processing stage.
/// </summary>
Continue,
/// <summary>
/// Method is finished with the GridAtmosphere.
/// </summary>
Finished,
}
public enum AtmosphereProcessingState : byte

View File

@@ -61,7 +61,22 @@ public partial class AtmosphereSystem
return Atmospherics.CellVolume * mapGrid.TileSize * tiles;
}
public readonly record struct AirtightData(AtmosDirection BlockedDirections, bool NoAirWhenBlocked,
/// <summary>
/// Data on the airtightness of a <see cref="TileAtmosphere"/>.
/// Cached on the <see cref="TileAtmosphere"/> and updated during
/// <see cref="AtmosphereSystem.ProcessRevalidate"/> if it was invalidated.
/// </summary>
/// <param name="BlockedDirections">The current directions blocked on this tile.
/// This is where air cannot flow to.</param>
/// <param name="NoAirWhenBlocked">Whether the tile can have air when blocking directions.
/// Common for entities like thin windows which only block one face but can still have air in the residing tile.</param>
/// <param name="FixVacuum">If true, Atmospherics will generate air (yes, creating matter from nothing)
/// using the adjacent tiles as a seed if the airtightness is removed and the tile has no air.
/// This allows stuff like airlocks that void air when becoming airtight to keep opening/closing without
/// draining a room by continuously voiding air.</param>
public readonly record struct AirtightData(
AtmosDirection BlockedDirections,
bool NoAirWhenBlocked,
bool FixVacuum);
private void UpdateAirtightData(EntityUid uid, GridAtmosphereComponent atmos, MapGridComponent grid, TileAtmosphere tile)

View File

@@ -14,7 +14,7 @@ using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Robust.Shared.Threading;
namespace Content.Server.Atmos.EntitySystems;

View File

@@ -3,7 +3,8 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;

View File

@@ -1,6 +1,6 @@
using Content.Server.Atmos.Components;
using Content.Shared.Examine;
using Robust.Shared.Map.Components;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
namespace Content.Server.Atmos.EntitySystems;
@@ -14,7 +14,7 @@ namespace Content.Server.Atmos.EntitySystems;
/// This system handles the adding and removing of entities to a processing list,
/// as well as any field changes via the API.</para>
/// </summary>
public sealed class DeltaPressureSystem : EntitySystem
public sealed partial class DeltaPressureSystem : SharedDeltaPressureSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
@@ -24,8 +24,6 @@ public sealed class DeltaPressureSystem : EntitySystem
SubscribeLocalEvent<DeltaPressureComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DeltaPressureComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<DeltaPressureComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DeltaPressureComponent, GridUidChangedEvent>(OnGridChanged);
}
@@ -48,12 +46,6 @@ public sealed class DeltaPressureSystem : EntitySystem
_atmosphereSystem.TryRemoveDeltaPressureEntity(ent.Comp.GridUid.Value, ent);
}
private void OnExamined(Entity<DeltaPressureComponent> ent, ref ExaminedEvent args)
{
if (ent.Comp.IsTakingDamage)
args.PushMarkup(Loc.GetString("window-taking-damage"));
}
private void OnGridChanged(Entity<DeltaPressureComponent> ent, ref GridUidChangedEvent args)
{
if (args.OldGrid != null)

View File

@@ -7,7 +7,7 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.IgnitionSource;
using Content.Shared.Interaction;

View File

@@ -1,13 +1,44 @@
namespace Content.Server.Atmos
namespace Content.Server.Atmos;
/// <summary>
/// <para>Internal Atmospherics class that stores data about a group of <see cref="TileAtmosphere"/>s
/// that are excited and need to be processed.</para>
///
/// <para>Excited Groups is an optimization routine executed during LINDA
/// that bunches small groups of active <see cref="TileAtmosphere"/>s
/// together and performs equalization processing on the entire group when the group dissolves.
/// Dissolution happens when LINDA operations between the tiles decrease to very low mole deltas.</para>
/// </summary>
public sealed class ExcitedGroup
{
public sealed class ExcitedGroup
{
[ViewVariables] public bool Disposed = false;
/// <summary>
/// Whether this Active Group has been disposed of.
/// Used to make sure we don't perform operations on active groups that
/// we've already dissolved.
/// </summary>
[ViewVariables]
public bool Disposed = false;
[ViewVariables] public readonly List<TileAtmosphere> Tiles = new(100);
/// <summary>
/// List of tiles that belong to this excited group.
/// </summary>
[ViewVariables]
public readonly List<TileAtmosphere> Tiles = new(100);
[ViewVariables] public int DismantleCooldown { get; set; } = 0;
/// <summary>
/// Cycles before this excited group will be queued for dismantling.
/// Dismantling is the process of equalizing the atmosphere
/// across all tiles in the excited group and removing the group.
/// </summary>
[ViewVariables]
public int DismantleCooldown = 0;
[ViewVariables] public int BreakdownCooldown { get; set; } = 0;
}
/// <summary>
/// Cycles before this excited group will be allowed to break down and deactivate.
/// Breakdown occurs when the excited group is small enough and inactive enough
/// to be safely removed without equalization. Used where the mole deltas across
/// the group are very low but not high enough for an equalization to occur.
/// </summary>
[ViewVariables]
public int BreakdownCooldown = 0;
}

View File

@@ -1,26 +1,57 @@
namespace Content.Server.Atmos
namespace Content.Server.Atmos;
/// <summary>
/// Internal Atmospherics struct that stores data about a hotspot in a tile.
/// Hotspots are used to model (slow-spreading) fires and firestarters.
/// </summary>
public struct Hotspot
{
public struct Hotspot
{
[ViewVariables]
public bool Valid;
/// <summary>
/// Whether this hotspot is currently representing fire and needs to be processed.
/// Set when the hotspot "becomes alight". This is never set to false
/// because Atmospherics will just assign <see cref="TileAtmosphere"/>
/// a new <see cref="Hotspot"/> struct when the fire goes out.
/// </summary>
[ViewVariables]
public bool Valid;
[ViewVariables]
public bool SkippedFirstProcess;
/// <summary>
/// Whether this hotspot has skipped its first process cycle.
/// AtmosphereSystem.Hotspot skips processing a hotspot beyond
/// setting it to active (for LINDA processing) the first
/// time it is processed.
/// </summary>
[ViewVariables]
public bool SkippedFirstProcess;
[ViewVariables]
public bool Bypassing;
/// <summary>
/// <para>Whether this hotspot is currently using the tile for reacting and fire processing
/// instead of a fraction of the tile's air.</para>
///
/// <para>When a tile is considered a hotspot, Hotspot will pull a fraction of that tile's
/// air out of the tile and perform a reaction on that air, merging it back afterward.
/// Bypassing triggers when the hotspot volume nears the tile's volume, making the system
/// use the tile's GasMixture instead of pulling a fraction out.</para>
/// </summary>
[ViewVariables]
public bool Bypassing;
[ViewVariables]
public float Temperature;
/// <summary>
/// Current temperature of the hotspot's volume, in Kelvin.
/// </summary>
[ViewVariables]
public float Temperature;
[ViewVariables]
public float Volume;
/// <summary>
/// Current volume of the hotspot, in liters.
/// You can think of this as the volume of the current fire in the tile.
/// </summary>
[ViewVariables]
public float Volume;
/// <summary>
/// State for the fire sprite.
/// </summary>
[ViewVariables]
public byte State;
}
/// <summary>
/// State for the fire sprite.
/// </summary>
[ViewVariables]
public byte State;
}

View File

@@ -1,80 +1,148 @@
using Content.Shared.Atmos;
namespace Content.Server.Atmos
namespace Content.Server.Atmos;
/// <summary>
/// Atmospherics class that stores data on tiles for Monstermos calculations and operations.
/// </summary>
public struct MonstermosInfo
{
public struct MonstermosInfo
/// <summary>
/// The last cycle this tile was processed for monstermos calculations.
/// Used to determine if Monstermos has already processed this tile in the
/// current tick's processing run.
/// </summary>
[ViewVariables]
public int LastCycle;
/// <summary>
/// <para>The last global cycle (on the GridAtmosphereComponent) this tile was processed for
/// monstermos calculations.
/// Monstermos can process multiple groups, and these groups may intersect with each other.
/// This allows Monstermos to check if a tile belongs to another group that has already been processed,
/// and skip processing it again.</para>
///
/// <para>Used for exploring the current area for determining tiles that should be equalized
/// using a BFS fill (see https://en.wikipedia.org/wiki/Breadth-first_search)</para>
/// </summary>
[ViewVariables]
public long LastQueueCycle;
/// <summary>
/// Similar to <see cref="LastQueueCycle"/>. Monstermos performs a second slow pass after the main
/// BFS fill in order to build a gradient map to determine transfer directions and amounts.
/// This field also tracks if we've already processed this tile in that slow pass so we don't re-queue it.
/// </summary>
[ViewVariables]
public long LastSlowQueueCycle;
/// <summary>
/// Difference in the amount of moles in this tile compared to the tile's neighbors.
/// Used to determine "how strongly" air wants to flow in/out of this tile from/to its neighbors.
/// </summary>
[ViewVariables]
public float MoleDelta;
/// <summary>
/// Number of moles that are going to be transferred in this direction during final equalization.
/// </summary>
[ViewVariables]
public float TransferDirectionEast;
/// <summary>
/// Number of moles that are going to be transferred in this direction during final equalization.
/// </summary>
[ViewVariables]
public float TransferDirectionWest;
/// <summary>
/// Number of moles that are going to be transferred in this direction during final equalization.
/// </summary>
[ViewVariables]
public float TransferDirectionNorth;
/// <summary>
/// Number of moles that are going to be transferred in this direction during final equalization.
/// </summary>
[ViewVariables]
public float TransferDirectionSouth;
/// <summary>
/// <para>Number of moles that are going to be transferred to this tile during final equalization.
/// You can think of this as molar flow rate, or the amount of air currently flowing through this tile.
/// Used for space wind and airflow sounds during explosive decompression or big movements.</para>
///
/// <para>During equalization calculations, Monstermos determines how much air is going to be transferred
/// between tiles, and sums that up into this field. It then either
///
/// determines how many moles to transfer in the direction of <see cref="CurrentTransferDirection"/>, or
///
/// determines how many moles to move in each direction using <see cref="MoleDelta"/>,
/// setting the TransferDirection fields accordingly based on the ratio obtained
/// from <see cref="MoleDelta"/>.</para>
/// </summary>
[ViewVariables]
public float CurrentTransferAmount;
/// <summary>
/// A pointer from the current tile to the direction in which air is being transferred the most.
/// </summary>
[ViewVariables]
public AtmosDirection CurrentTransferDirection;
/// <summary>
/// Marks this tile as being equalized using the O(n log n) algorithm.
/// </summary>
[ViewVariables]
public bool FastDone;
/// <summary>
/// Gets or sets the TransferDirection in the given direction.
/// </summary>
/// <param name="direction"></param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when an invalid direction is given
/// (a non-cardinal direction)</exception>
public float this[AtmosDirection direction]
{
[ViewVariables]
public int LastCycle;
[ViewVariables]
public long LastQueueCycle;
[ViewVariables]
public long LastSlowQueueCycle;
[ViewVariables]
public float MoleDelta;
[ViewVariables]
public float TransferDirectionEast;
[ViewVariables]
public float TransferDirectionWest;
[ViewVariables]
public float TransferDirectionNorth;
[ViewVariables]
public float TransferDirectionSouth;
[ViewVariables]
public float CurrentTransferAmount;
[ViewVariables]
public AtmosDirection CurrentTransferDirection;
[ViewVariables]
public bool FastDone;
public float this[AtmosDirection direction]
{
get =>
direction switch
{
AtmosDirection.East => TransferDirectionEast,
AtmosDirection.West => TransferDirectionWest,
AtmosDirection.North => TransferDirectionNorth,
AtmosDirection.South => TransferDirectionSouth,
_ => throw new ArgumentOutOfRangeException(nameof(direction))
};
set
get =>
direction switch
{
switch (direction)
{
case AtmosDirection.East:
TransferDirectionEast = value;
break;
case AtmosDirection.West:
TransferDirectionWest = value;
break;
case AtmosDirection.North:
TransferDirectionNorth = value;
break;
case AtmosDirection.South:
TransferDirectionSouth = value;
break;
default:
throw new ArgumentOutOfRangeException(nameof(direction));
}
AtmosDirection.East => TransferDirectionEast,
AtmosDirection.West => TransferDirectionWest,
AtmosDirection.North => TransferDirectionNorth,
AtmosDirection.South => TransferDirectionSouth,
_ => throw new ArgumentOutOfRangeException(nameof(direction))
};
set
{
switch (direction)
{
case AtmosDirection.East:
TransferDirectionEast = value;
break;
case AtmosDirection.West:
TransferDirectionWest = value;
break;
case AtmosDirection.North:
TransferDirectionNorth = value;
break;
case AtmosDirection.South:
TransferDirectionSouth = value;
break;
default:
throw new ArgumentOutOfRangeException(nameof(direction));
}
}
}
public float this[int index]
{
get => this[(AtmosDirection) (1 << index)];
set => this[(AtmosDirection) (1 << index)] = value;
}
/// <summary>
/// Gets or sets the TransferDirection by index.
/// </summary>
/// <param name="index">The index of the direction</param>
public float this[int index]
{
get => this[(AtmosDirection) (1 << index)];
set => this[(AtmosDirection) (1 << index)] = value;
}
}

View File

@@ -51,6 +51,7 @@ public sealed partial class GasPipeManifoldSystem : EntitySystem
return;
var pipeNames = ent.Comp.InletNames.Union(ent.Comp.OutletNames);
var pipeCount = pipeNames.Count();
foreach (var pipeName in pipeNames)
{
@@ -58,8 +59,8 @@ public sealed partial class GasPipeManifoldSystem : EntitySystem
continue;
var pipeLocal = pipe.Air.Clone();
pipeLocal.Multiply(pipe.Volume / pipe.Air.Volume);
pipeLocal.Volume = pipe.Volume;
pipeLocal.Multiply(pipe.Volume * pipeCount / pipe.Air.Volume);
pipeLocal.Volume = pipe.Volume * pipeCount;
args.GasMixtures.Add((Name(ent), pipeLocal));
break;

View File

@@ -2,7 +2,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Events;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Temperature.Components;
using Robust.Server.Containers;
using Robust.Shared.Physics.Components;

View File

@@ -1,165 +1,248 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Maps;
using Robust.Shared.Map;
namespace Content.Server.Atmos
namespace Content.Server.Atmos;
/// <summary>
/// Internal Atmospherics class that stores data on an atmosphere in a single tile.
/// You should not be using these directly outside of <see cref="AtmosphereSystem"/>.
/// Use the public APIs in <see cref="AtmosphereSystem"/> instead.
/// </summary>
[Access(typeof(AtmosphereSystem), typeof(GasTileOverlaySystem), typeof(AtmosDebugOverlaySystem))]
public sealed class TileAtmosphere : IGasMixtureHolder
{
/// <summary>
/// Internal Atmos class that stores data about the atmosphere in a grid.
/// You shouldn't use this directly, use <see cref="AtmosphereSystem"/> instead.
/// The last cycle this tile's air was archived into <see cref="AirArchived"/>.
/// See <see cref="AirArchived"/> for more info on archival.
/// </summary>
[Access(typeof(AtmosphereSystem), typeof(GasTileOverlaySystem), typeof(AtmosDebugOverlaySystem))]
public sealed class TileAtmosphere : IGasMixtureHolder
[ViewVariables]
public int ArchivedCycle;
/// <summary>
/// Current cycle this tile was processed.
/// Used to prevent double-processing in a single cycle in many processing stages.
/// </summary>
[ViewVariables]
public int CurrentCycle;
/// <summary>
/// Current temperature of this tile, in Kelvin.
/// Used for Superconduction.
/// This is not the temperature of the attached <see cref="GasMixture"/>!
/// </summary>
[ViewVariables]
public float Temperature = Atmospherics.T20C;
/// <summary>
/// The current target tile for pressure movement for the current cycle.
/// Gas will be moved towards this tile during pressure equalization.
/// Also see <see cref="PressureDifference"/>.
/// </summary>
[ViewVariables]
public TileAtmosphere? PressureSpecificTarget;
/// <summary>
/// The current pressure difference (delta) between this tile and its pressure target.
/// If Monstermos is enabled, this value represents the quantity of moles transferred.
/// </summary>
[ViewVariables]
public float PressureDifference;
/// <summary>
/// The current heat capacity of this tile.
/// Used for Superconduction.
/// This is not the heat capacity of the attached <see cref="GasMixture"/>!
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float HeatCapacity = Atmospherics.MinimumHeatCapacity;
/// <summary>
/// The current thermal conductivity of this tile.
/// Describes how well heat moves between this tile and adjacent tiles during superconduction.
/// </summary>
[ViewVariables]
public float ThermalConductivity = 0.05f;
/// <summary>
/// Designates whether this tile is currently excited for processing in an excited group or LINDA.
/// </summary>
[ViewVariables]
public bool Excited;
/// <summary>
/// Whether this tile should be considered space.
/// </summary>
[ViewVariables]
public bool Space;
/// <summary>
/// Cached adjacent <see cref="TileAtmosphere"/> tiles for this tile.
/// Ordered in the same order as <see cref="Atmospherics.Directions"/>
/// (should be North, South, East, West).
/// Adjacent tiles can be null if air cannot flow to them.
/// </summary>
[ViewVariables]
public readonly TileAtmosphere?[] AdjacentTiles = new TileAtmosphere[Atmospherics.Directions];
/// <summary>
/// Neighbouring tiles to which air can flow. This is a combination of this tile's unblocked direction, and the
/// unblocked directions on adjacent tiles.
/// </summary>
[ViewVariables]
public AtmosDirection AdjacentBits = AtmosDirection.Invalid;
/// <summary>
/// Current <see cref="MonstermosInfo"/> information for this tile.
/// </summary>
[ViewVariables]
[Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)]
public MonstermosInfo MonstermosInfo;
/// <summary>
/// Current <see cref="Hotspot"/> information for this tile.
/// </summary>
[ViewVariables]
public Hotspot Hotspot;
/// <summary>
/// Points to the direction of the recipient tile for pressure equalization logic
/// (Monstermos or HighPressureDelta otherwise).
/// </summary>
[ViewVariables]
public AtmosDirection PressureDirection;
/// <summary>
/// Last cycle's <see cref="PressureDirection"/> for debugging purposes.
/// </summary>
[ViewVariables]
public AtmosDirection LastPressureDirection;
/// <summary>
/// Grid entity this tile belongs to.
/// </summary>
[ViewVariables]
[Access(typeof(AtmosphereSystem))]
public EntityUid GridIndex;
/// <summary>
/// The grid indices of this tile.
/// </summary>
[ViewVariables]
public Vector2i GridIndices;
/// <summary>
/// The excited group this tile belongs to, if any.
/// </summary>
[ViewVariables]
public ExcitedGroup? ExcitedGroup;
/// <summary>
/// The air in this tile. If null, this tile is completely air-blocked.
/// This can be immutable if the tile is spaced.
/// </summary>
[ViewVariables]
[Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public GasMixture? Air;
/// <summary>
/// A copy of the air in this tile from the last time it was archived at <see cref="ArchivedCycle"/>.
/// LINDA archives the air before doing any necessary processing and uses this to perform its calculations,
/// making the results of LINDA independent of the order in which tiles are processed.
/// </summary>
[ViewVariables]
public GasMixture? AirArchived;
/// <summary>
/// The amount of gas last shared to adjacent tiles during LINDA processing.
/// Used to determine when LINDA should dismantle an excited group
/// or extend its time alive.
/// </summary>
[DataField("lastShare")]
public float LastShare;
/// <summary>
/// Implementation of <see cref="IGasMixtureHolder.Air"/>.
/// </summary>
GasMixture IGasMixtureHolder.Air
{
[ViewVariables]
public int ArchivedCycle;
get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature };
set => Air = value;
}
[ViewVariables]
public int CurrentCycle;
/// <summary>
/// The maximum temperature this tile has sustained during hotspot fire processing.
/// Used for debugging.
/// </summary>
[ViewVariables]
public float MaxFireTemperatureSustained;
[ViewVariables]
public float Temperature { get; set; } = Atmospherics.T20C;
/// <summary>
/// If true, then this tile is directly exposed to the map's atmosphere, either because the grid has no tile at
/// this position, or because the tile type is not airtight.
/// </summary>
[ViewVariables]
public bool MapAtmosphere;
[ViewVariables]
public TileAtmosphere? PressureSpecificTarget { get; set; }
/// <summary>
/// If true, this tile does not actually exist on the grid, it only exists to represent the map's atmosphere for
/// adjacent grid tiles.
/// This tile often has immutable air and is sitting off the edge of the grid, where there is no grid.
/// </summary>
[ViewVariables]
public bool NoGridTile;
/// <summary>
/// This is either the pressure difference, or the quantity of moles transferred if monstermos is enabled.
/// </summary>
[ViewVariables]
public float PressureDifference { get; set; }
/// <summary>
/// If true, this tile is queued for processing in <see cref="GridAtmosphereComponent.PossiblyDisconnectedTiles"/>
/// </summary>
[ViewVariables]
public bool TrimQueued;
[ViewVariables(VVAccess.ReadWrite)]
public float HeatCapacity { get; set; } = Atmospherics.MinimumHeatCapacity;
/// <summary>
/// Cached information about airtight entities on this tile. This gets updated anytime a tile gets invalidated
/// (i.e., gets added to <see cref="GridAtmosphereComponent.InvalidatedCoords"/>).
/// </summary>
public AtmosphereSystem.AirtightData AirtightData;
[ViewVariables]
public float ThermalConductivity { get; set; } = 0.05f;
/// <summary>
/// Creates a new TileAtmosphere.
/// </summary>
/// <param name="gridIndex">The grid entity this tile belongs to.</param>
/// <param name="gridIndices">>The grid indices of this tile.</param>
/// <param name="mixture">The gas mixture of this tile.</param>
/// <param name="immutable">If true, the gas mixture will be marked immutable.</param>
/// <param name="space">If true, this tile is considered space.</param>
public TileAtmosphere(EntityUid gridIndex, Vector2i gridIndices, GasMixture? mixture = null, bool immutable = false, bool space = false)
{
GridIndex = gridIndex;
GridIndices = gridIndices;
Air = mixture;
AirArchived = Air?.Clone();
Space = space;
[ViewVariables]
public bool Excited { get; set; }
if(immutable)
Air?.MarkImmutable();
}
/// <summary>
/// Whether this tile should be considered space.
/// </summary>
[ViewVariables]
public bool Space { get; set; }
/// <summary>
/// Creates a copy of another TileAtmosphere.
/// </summary>
/// <param name="other">The TileAtmosphere to copy.</param>
public TileAtmosphere(TileAtmosphere other)
{
GridIndex = other.GridIndex;
GridIndices = other.GridIndices;
Space = other.Space;
NoGridTile = other.NoGridTile;
MapAtmosphere = other.MapAtmosphere;
Air = other.Air?.Clone();
AirArchived = Air != null ? Air.Clone() : null;
}
/// <summary>
/// Adjacent tiles in the same order as <see cref="AtmosDirection"/>. (NSEW)
/// </summary>
[ViewVariables]
public readonly TileAtmosphere?[] AdjacentTiles = new TileAtmosphere[Atmospherics.Directions];
/// <summary>
/// Neighbouring tiles to which air can flow. This is a combination of this tile's unblocked direction, and the
/// unblocked directions on adjacent tiles.
/// </summary>
[ViewVariables]
public AtmosDirection AdjacentBits = AtmosDirection.Invalid;
[ViewVariables, Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)]
public MonstermosInfo MonstermosInfo;
[ViewVariables]
public Hotspot Hotspot;
[ViewVariables]
public AtmosDirection PressureDirection;
// For debug purposes.
[ViewVariables]
public AtmosDirection LastPressureDirection;
[ViewVariables]
[Access(typeof(AtmosphereSystem))]
public EntityUid GridIndex { get; set; }
[ViewVariables]
public Vector2i GridIndices;
[ViewVariables]
public ExcitedGroup? ExcitedGroup { get; set; }
/// <summary>
/// The air in this tile. If null, this tile is completely air-blocked.
/// This can be immutable if the tile is spaced.
/// </summary>
[ViewVariables]
[Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public GasMixture? Air { get; set; }
/// <summary>
/// Like Air, but a copy stored each atmos tick before tile processing takes place. This lets us update Air
/// in-place without affecting the results based on update order.
/// </summary>
[ViewVariables]
public GasMixture? AirArchived;
[DataField("lastShare")]
public float LastShare;
GasMixture IGasMixtureHolder.Air
{
get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature };
set => Air = value;
}
[ViewVariables]
public float MaxFireTemperatureSustained { get; set; }
/// <summary>
/// If true, then this tile is directly exposed to the map's atmosphere, either because the grid has no tile at
/// this position, or because the tile type is not airtight.
/// </summary>
[ViewVariables]
public bool MapAtmosphere;
/// <summary>
/// If true, this tile does not actually exist on the grid, it only exists to represent the map's atmosphere for
/// adjacent grid tiles.
/// </summary>
[ViewVariables]
public bool NoGridTile;
/// <summary>
/// If true, this tile is queued for processing in <see cref="GridAtmosphereComponent.PossiblyDisconnectedTiles"/>
/// </summary>
[ViewVariables]
public bool TrimQueued;
/// <summary>
/// Cached information about airtight entities on this tile. This gets updated anytime a tile gets invalidated
/// (i.e., gets added to <see cref="GridAtmosphereComponent.InvalidatedCoords"/>).
/// </summary>
public AtmosphereSystem.AirtightData AirtightData;
public TileAtmosphere(EntityUid gridIndex, Vector2i gridIndices, GasMixture? mixture = null, bool immutable = false, bool space = false)
{
GridIndex = gridIndex;
GridIndices = gridIndices;
Air = mixture;
AirArchived = Air != null ? Air.Clone() : null;
Space = space;
if(immutable)
Air?.MarkImmutable();
}
public TileAtmosphere(TileAtmosphere other)
{
GridIndex = other.GridIndex;
GridIndices = other.GridIndices;
Space = other.Space;
NoGridTile = other.NoGridTile;
MapAtmosphere = other.MapAtmosphere;
Air = other.Air?.Clone();
AirArchived = Air != null ? Air.Clone() : null;
}
public TileAtmosphere()
{
}
/// <summary>
/// Creates a new empty TileAtmosphere.
/// </summary>
public TileAtmosphere()
{
}
}

View File

@@ -1,18 +0,0 @@
namespace Content.Server.Atmos
{
/// <summary>
/// Event raised directed to an entity when it is standing on a tile that's on fire.
/// </summary>
[ByRefEvent]
public readonly struct TileFireEvent
{
public readonly float Temperature;
public readonly float Volume;
public TileFireEvent(float temperature, float volume)
{
Temperature = temperature;
Volume = volume;
}
}
}

View File

@@ -2,7 +2,7 @@ using Content.Shared.Bed;
using Content.Shared.Bed.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Mobs.Systems;
namespace Content.Server.Bed

View File

@@ -4,7 +4,7 @@ using Content.Server.Popups;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
using Content.Shared.Bible;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Ghost.Roles.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
@@ -14,7 +14,6 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
@@ -133,9 +132,7 @@ namespace Content.Server.Bible
}
}
var damage = _damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid);
if (damage == null || damage.Empty)
if (_damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid))
{
var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
_popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium);

View File

@@ -12,7 +12,7 @@ using Content.Shared.Chat;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.EntityConditions;
using Content.Shared.EntityConditions.Conditions.Body;
@@ -367,7 +367,7 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles == 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating");
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
_damageableSys.ChangeDamage(ent.Owner, ent.Comp.Damage, interruptsDoAfters: false);
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
return;
@@ -381,7 +381,7 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles >= 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating");
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
_damageableSys.ChangeDamage(ent.Owner, ent.Comp.DamageRecovery);
var ev = new StopSuffocatingEvent();
RaiseLocalEvent(ent, ref ev);

View File

@@ -23,7 +23,6 @@ using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Shared.Administration.Logs;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;

View File

@@ -1,20 +1,16 @@
using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.CardboardBox;
using Content.Shared.CardboardBox.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Interaction;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Stealth;
using Content.Shared.Stealth.Components;
using Content.Shared.Storage.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.CardboardBox;
@@ -109,10 +105,10 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
//Relay damage to the mover
private void OnDamage(EntityUid uid, CardboardBoxComponent component, DamageChangedEvent args)
{
if (args.DamageDelta != null && args.DamageIncreased)
{
_damageable.TryChangeDamage(component.Mover, args.DamageDelta, origin: args.Origin);
}
if (args.DamageDelta == null || !args.DamageIncreased || component.Mover is not { } mover)
return;
_damageable.ChangeDamage(mover, args.DamageDelta, origin: args.Origin);
}
private void OnEntInserted(EntityUid uid, CardboardBoxComponent component, EntInsertedIntoContainerMessage args)

View File

@@ -56,7 +56,7 @@ public sealed partial class CargoSystem
if (args.Account == null)
{
var stackPrototype = _protoMan.Index(ent.Comp.CashType);
_stack.Spawn(args.Amount, stackPrototype, Transform(ent).Coordinates);
_stack.SpawnAtPosition(args.Amount, stackPrototype, Transform(ent).Coordinates);
if (!_emag.CheckFlag(ent, EmagType.Interaction))
{

View File

@@ -160,14 +160,20 @@ internal sealed partial class ChatManager : IChatManager
public void SendAdminAlert(string message)
{
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients);
SendAdminAlertNoFormatOrEscape(wrappedMessage);
}
public void SendAdminAlertNoFormatOrEscape(string message)
{
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
ChatMessageToMany(ChatChannel.AdminAlert, message, message, default, false, true, clients);
}
public void SendAdminAlert(EntityUid player, string message)
{
var mindSystem = _entityManager.System<SharedMindSystem>();

View File

@@ -2,7 +2,7 @@ using Content.Server.Ghost;
using Content.Server.Hands.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Events;

View File

@@ -57,7 +57,6 @@ public sealed partial class ChatSystem : SharedChatSystem
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
private bool _loocEnabled = true;
@@ -212,7 +211,7 @@ public sealed partial class ChatSystem : SharedChatSystem
// This message may have a radio prefix, and should then be whispered to the resolved radio channel
if (checkRadioPrefix)
{
if (TryProccessRadioMessage(source, message, out var modMessage, out var channel))
if (TryProcessRadioMessage(source, message, out var modMessage, out var channel))
{
SendEntityWhisper(source, modMessage, range, channel, nameOverride, hideLog, ignoreActionBlocker);
return;

View File

@@ -1,3 +1,5 @@
using Content.Shared.Damage.Systems;
namespace Content.Server.Chat.Systems;
using Content.Shared.Chat;

View File

@@ -12,7 +12,7 @@ using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Cloning;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
@@ -29,6 +29,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Chemistry.Reagent;
namespace Content.Server.Cloning;
@@ -58,6 +59,7 @@ public sealed class CloningPodSystem : EntitySystem
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
public readonly ProtoId<CloningSettingsPrototype> SettingsId = "CloningPod";
public const float EasyModeCloningCost = 0.7f;
private static readonly ProtoId<ReagentPrototype> BloodId = "Blood";
public override void Initialize()
{
@@ -302,7 +304,7 @@ public sealed class CloningPodSystem : EntitySystem
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
bloodSolution.AddReagent("Blood", 50);
bloodSolution.AddReagent(BloodId, 50);
if (_robustRandom.Prob(0.2f))
i++;
}

View File

@@ -60,7 +60,7 @@ public sealed partial class CloningSystem
{
// if the clone is a stack as well, adjust the count of the copy
if (TryComp<StackComponent>(args.CloneUid, out var cloneStackComp))
_stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp);
_stack.SetCount((args.CloneUid, cloneStackComp), ent.Comp.Count);
}
private void OnCloneItemLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)

View File

@@ -84,6 +84,14 @@ public sealed partial class CloningSystem : SharedCloningSystem
return true;
}
public override void CloneComponents(EntityUid original, EntityUid clone, ProtoId<CloningSettingsPrototype> settings)
{
if (!_prototype.Resolve(settings, out var proto))
return;
CloneComponents(original, clone, proto);
}
public override void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
{
var componentsToCopy = settings.Components;

View File

@@ -8,7 +8,7 @@ using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Clumsy;
using Content.Shared.Cluwne;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Mobs;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;

View File

@@ -27,14 +27,14 @@ public sealed partial class GivePrototype : IGraphAction
if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype))
{
var stackSystem = entityManager.EntitySysManager.GetEntitySystem<StackSystem>();
var stacks = stackSystem.SpawnMultiple(Prototype, Amount, userUid ?? uid);
var stacks = stackSystem.SpawnMultipleNextToOrDrop(Prototype, Amount, userUid ?? uid);
if (userUid is null || !entityManager.TryGetComponent(userUid, out HandsComponent? handsComp))
return;
foreach (var item in stacks)
{
stackSystem.TryMergeToHands(item, userUid.Value, hands: handsComp);
stackSystem.TryMergeToHands(item, (userUid.Value, handsComp));
}
}
else

View File

@@ -12,7 +12,7 @@ namespace Content.Server.Construction.Completions
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(uid, Amount);
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount((uid, null), Amount);
}
}
}

View File

@@ -28,7 +28,7 @@ namespace Content.Server.Construction.Completions
{
var stackEnt = entityManager.SpawnEntity(Prototype, coordinates);
var stack = entityManager.GetComponent<StackComponent>(stackEnt);
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(stackEnt, Amount, stack);
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount((stackEnt, stack), Amount);
}
else
{

View File

@@ -1,13 +1,8 @@
using Content.Server.Destructible;
using Content.Shared.Construction;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Content.Server.Construction.Conditions;

View File

@@ -187,7 +187,7 @@ namespace Content.Server.Construction
// TODO allow taking from several stacks.
// Also update crafting steps to check if it works.
var splitStack = _stackSystem.Split(entity, materialStep.Amount, user.ToCoordinates(0, 0), stack);
var splitStack = _stackSystem.Split((entity, stack), materialStep.Amount, user.ToCoordinates(0, 0));
if (splitStack == null)
continue;

View File

@@ -49,7 +49,7 @@ public sealed partial class ConstructionSystem
foreach (var (stackType, amount) in machineBoard.StackRequirements)
{
var stack = _stackSystem.Spawn(amount, stackType, xform.Coordinates);
var stack = _stackSystem.SpawnAtPosition(amount, stackType, xform.Coordinates);
if (!_container.Insert(stack, partContainer))
throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Prototype(uid)?.ID ?? "N/A"}");
}

View File

@@ -182,7 +182,7 @@ public sealed class MachineFrameSystem : EntitySystem
return true;
}
var splitStack = _stack.Split(used, needed, Transform(uid).Coordinates, stack);
var splitStack = _stack.Split((used, stack), needed, Transform(uid).Coordinates);
if (splitStack == null)
return false;

View File

@@ -34,8 +34,8 @@ public sealed class CrayonSystem : SharedCrayonSystem
SubscribeLocalEvent<CrayonComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<CrayonComponent, CrayonSelectMessage>(OnCrayonBoundUI);
SubscribeLocalEvent<CrayonComponent, CrayonColorMessage>(OnCrayonBoundUIColor);
SubscribeLocalEvent<CrayonComponent, UseInHandEvent>(OnCrayonUse, before: new[] { typeof(FoodSystem) });
SubscribeLocalEvent<CrayonComponent, AfterInteractEvent>(OnCrayonAfterInteract, after: new[] { typeof(FoodSystem) });
SubscribeLocalEvent<CrayonComponent, UseInHandEvent>(OnCrayonUse);
SubscribeLocalEvent<CrayonComponent, AfterInteractEvent>(OnCrayonAfterInteract, after: [typeof(IngestionSystem)]);
SubscribeLocalEvent<CrayonComponent, DroppedEvent>(OnCrayonDropped);
}
@@ -47,6 +47,7 @@ public sealed class CrayonSystem : SharedCrayonSystem
Dirty(ent);
}
// Runs after IngestionSystem so it doesn't bulldoze force-feeding
private void OnCrayonAfterInteract(EntityUid uid, CrayonComponent component, AfterInteractEvent args)
{
if (args.Handled || !args.CanReach)

View File

@@ -15,7 +15,7 @@ namespace Content.Server.Cuffs
SubscribeLocalEvent<CuffableComponent, ComponentGetState>(OnCuffableGetState);
}
private void OnCuffableGetState(EntityUid uid, CuffableComponent component, ref ComponentGetState args)
private void OnCuffableGetState(Entity<CuffableComponent> entity, ref ComponentGetState args)
{
// there are 2 approaches i can think of to handle the handcuff overlay on players
// 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
@@ -23,12 +23,12 @@ namespace Content.Server.Cuffs
// approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
// right now we're doing approach #1.
HandcuffComponent? cuffs = null;
if (component.CuffedHandCount > 0)
TryComp(component.LastAddedCuffs, out cuffs);
args.State = new CuffableComponentState(component.CuffedHandCount,
component.CanStillInteract,
if (TryGetLastCuff((entity, entity.Comp), out var cuff))
TryComp(cuff, out cuffs);
args.State = new CuffableComponentState(entity.Comp.CuffedHandCount,
entity.Comp.CanStillInteract,
cuffs?.CuffedRSI,
$"{cuffs?.BodyIconState}-{component.CuffedHandCount}",
$"{cuffs?.BodyIconState}-{entity.Comp.CuffedHandCount}",
cuffs?.Color);
// the iconstate is formatted as blah-2, blah-4, blah-6, etc.
// the number corresponds to how many hands are cuffed.

View File

@@ -4,7 +4,7 @@ using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Damage.Systems;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;

View File

@@ -2,6 +2,7 @@ using Content.Shared.Bed.Sleep;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;

Some files were not shown because too many files have changed in this diff Show More