Merge branch 'master' into engitape

Signed-off-by: zelezniciar1 <39102800+zelezniciar1@users.noreply.github.com>
This commit is contained in:
zelezniciar1 2026-02-21 18:48:56 +00:00 committed by GitHub
commit 7940c9a95c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1124 changed files with 87417 additions and 50851 deletions

View File

@ -204,49 +204,49 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|style="border:1px solid black;" | 301
|style= "border:1px solid black;"| [[File:SL_Manslaughter.png]]
|style= "border:1px solid black;"| Manslaughter
|style= "border:1px solid black;"| 16 minutes
|style= "border:1px solid black;"| 10 minutes
|style= "border:1px solid black;"| To act without malice in a manner which, directly or indirectly, leads to the death of an Employee.
|-
|style= "border:1px solid black;"| 302
|style= "border:1px solid black;"| [[File:SL_Kidnapping.png]]
|style= "border:1px solid black;"| Kidnapping
|style= "border:1px solid black;"| 16 minutes
|style= "border:1px solid black;"| 10 minutes
|style= "border:1px solid black;"| To unlawfully confine or restrict the free movement of an Employee against their will.
|-
| style="border:1px solid black;" | 303
| style="border:1px solid black;" | WIP
| style="border:1px solid black;" | Grand Possession
| style="border:1px solid black;" | 15-25 minutes
| style="border:1px solid black;" | 12-20 minutes
| style="border:1px solid black;" | To be in the possession of a Category C, D, or E Controlled Armament.
|-
| style="border:1px solid black;" | 304
| style="border:1px solid black;" | [[File:SL_Mindbreaking.png]]
| style="border:1px solid black;" | Noöspheric Tampering
| style="border:1px solid black;" | 14 minutes
| style="border:1px solid black;" | 8 minutes
| style="border:1px solid black;" | To maliciously tamper with or create noöspheric anomalies, entities, aberrations, or other elements; Or to otherwise deliberately perform actions that would raise Glimmer or other anomalous reading levels in a way that threatens normative reality.
|-
| style="border:1px solid black;" | 305
| style="border:1px solid black;" | [[File:SL_Sabotage.png]]
| style="border:1px solid black;" | Sabotage
| style="border:1px solid black;" | 14 minutes
| style="border:1px solid black;" | 8 minutes
| style="border:1px solid black;" | To maliciously commit an act that, directly or indirectly, hinders the operation of a vessel of its part; or, to commit an act that modifies and/or damages technology or equipment one is not authorized to access.
|-
| style="border:1px solid black;" | 306
| style="border:1px solid black;" | [[File:SL_AbuseOfPower.png]]
| style="border:1px solid black;" | Abuse of Power
| style="border:1px solid black;" | 14 minutes
| style="border:1px solid black;" | 10 minutes
| style="border:1px solid black;" | To intentionally misuse or wrongfully exercise ones own authority, influence, or control, resulting in harm, unjust treatment, or demonstratable loss to another Employee.
|-
| style="border:1px solid black;" | 307
| style="border:1px solid black;" | [[File:SL_GrandTheft.png]]
| style="border:1px solid black;" | Grand Larceny
| style="border:1px solid black;" | 12 minutes
| style="border:1px solid black;" | 8 minutes
| style="border:1px solid black;" | To deprive an Employee of any Controlled Item in their lawful possession.
|-
| style="border:1px solid black;" | 308
| style="border:1px solid black;" | [[File:SL_BlackMarketeering.png]]
| style="border:1px solid black;" | Black Marketeering
| style="border:1px solid black;" | 12 minutes
| style="border:1px solid black;" | 8 minutes
| style="border:1px solid black;" | To sell, distribute, or otherwise circulate Controlled Items to unauthorized entities.
|}
----
@ -262,49 +262,49 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | 201
| style="border: 1px solid black;" | [[File:SL_Assault.png]]
! style="border: 1px solid black;" | {{anchor|Assault}}Assault
| style="border: 1px solid black;" | 10 minutes
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To cause physical harm or to effect unwanted physical contact on an Employee, without the apparent intent to kill them, or to threaten such actions with both capability and intent to do so.
|-
| style="border: 1px solid black;" | 202
| style="border: 1px solid black;" | [[File:SL_BreakingAndEntering.png]]
! style="border: 1px solid black;" | {{anchor|Breaking and Entering}}Breaking and Entering
| style="border: 1px solid black;" | 10 minutes
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To break and enter into a high security area where one is not authorised nor invited, with intent to commit a crime within.
|-
| style="border: 1px solid black;" | 203
| style="border: 1px solid black;" | [[File:SL_Rioting.png]]
! style="border: 1px solid black;" | {{anchor|Rioting}}Rioting
| style="border: 1px solid black;" | 8 minutes
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | To partake in an unauthorised riotous, tumultuous, and disruptive public assembly that refuses to disperse after warning.
|-
| style="border: 1px solid black;" | 204
| style="border: 1px solid black;" | [[File:SL_Endangerment.png]]
! style="border: 1px solid black;" | {{anchor|Endangerment}}Endangerment
| style="border: 1px solid black;" | 8 minutes
| style="border: 1px solid black;" | 6 minutes
| style="border: 1px solid black;" | To recklessly abandon obligations involving the continued wellbeing and/or protection of life and property, through malpractice, action, or inaction.
|-
| style="border: 1px solid black;" | 205
| style="border: 1px solid black;" | [[File:SL_Possession.png]]
! style="border: 1px solid black;" | {{anchor|Possession}}Possession
| style="border: 1px solid black;" | 5-10 minutes
| style="border: 1px solid black;" | 3-8 minutes
| style="border: 1px solid black;" | To be in unauthorised possession of restricted items or items of particular danger. See [[Standard_Operating_Procedure#Controlled_Substances/Items|Controlled Substances/Items]], [[Standard_Operating_Procedure#Controlled_Armaments|Controlled Armaments]].
|-
| style="border: 1px solid black;" | 206
| style="border: 1px solid black;" | [[File:SL_ObstructionOfJustice.png]]
! style="border: 1px solid black;" | {{anchor|Obstruction of Justice}}Obstruction of Justice
| style="border: 1px solid black;" | 8 minutes
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | To wilfully disobey, interfere with, or refuse a decree of the court, warrant, or arrest.
|-
| style="border: 1px solid black;" | 207
| style="border: 1px solid black;" | [[File:SL_PerjuryOrFalseReport.png]]
! style="border: 1px solid black;" | {{anchor|Perjury or False Report}}Perjury or False Report
| style="border: 1px solid black;" | 6 minutes
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | To wilfully and maliciously tell an untruth either in court or in the process of making an actionable report to law enforcement.
|-
| style="border: 1px solid black;" | 208
| style="border: 1px solid black;" | [[File:SL_ContemptOfCourt.png]]
! style="border: 1px solid black;" | {{anchor|Contempt of Court}}Contempt of Court
| style="border: 1px solid black;" | 6 minutes
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | To conduct oneself disruptively and disrespectfully before the court.
|-
|style= "border:1px solid black;"| 209
@ -329,13 +329,13 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | 101
| style="border: 1px solid black;" | [[File:SL_AnimalCruelty.png]]
! style="border: 1px solid black;" | {{anchor|Animal Cruelty}}Animal Cruelty
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | 2 minutes
| style="border: 1px solid black;" | To inflict unnecessary suffering on a Pet with malicious intent.
|-
|style= "border:1px solid black;"| 102
|style= "border:1px solid black;"| WIP
!style= "border:1px solid black;"| Harassment
|style= "border:1px solid black;"| 5 minutes
|style= "border:1px solid black;"| 3 minutes
|style= "border:1px solid black;"| To verbally demean, humiliate, or harass an Employee with malice; or maliciously spread false information amongst a population.
|-
|style= "border:1px solid black;"| 103
@ -347,19 +347,19 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | 104
| style="border: 1px solid black;" | [[File:SL_Theft.png]]
! style="border: 1px solid black;" | {{anchor|Theft}}Petty Larceny
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | 2 minutes
| style="border: 1px solid black;" | To deprive an Employee of an item in their lawful property without consent.
|-
| style="border: 1px solid black;" | 105
| style="border: 1px solid black;" | [[File:SL_Trespass.png]]
! style="border: 1px solid black;" | {{anchor|Trespass}}Trespass
| style="border: 1px solid black;" | 3 minutes
| style="border: 1px solid black;" | 2 minutes
| style="border: 1px solid black;" | To enter into an area where one is not authorised nor invited.
|-
| style="border: 1px solid black;" | 106
| style="border: 1px solid black;" | [[File:SL_Vandalism.png]]
! style="border: 1px solid black;" | {{anchor|Vandalism}}Vandalism
| style="border: 1px solid black;" | 3 minutes
| style="border: 1px solid black;" | 2 minutes
| style="border: 1px solid black;" | To deface or superficially damage public property, or property belonging to another Employee.
|-
| style="border: 1px solid black;" | 107

View File

@ -102,25 +102,25 @@ A list of such Substances follow:
Controlled Armaments are grouped into categories of increasing severity. If the offender possesses several categories of contraband, only the highest severity of said items should be charged.
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 5min brig and further legal action as the situation demands.''
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 3min brig and further legal action as the situation demands.''
* Any object or instrument specifically designed, adapted, or used to cause physical harm in close combat
* Any object or equipment specifically designed or adapted to protect against physical harm caused by weapons, projectiles, or other forms of direct assault
* Any object or instrument specifically designed or adapted to incapacitate or disorient by using light or sound without causing permanent injury
* Any object or equipment specifically designed or adapted for explicitly training purposes that would otherwise fall under another category
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 10min brig and further legal action as the situation demands.''
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 8min brig and further legal action as the situation demands.''
* Any object or instrument specifically designed or adapted to cause physical harm in close combat, which employs a powered system to enhance performance
* Any manually-operated ballistic arms that possess an internal magazine or feeding system, or lack an internal magazine or feeding system altogether
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 15min brig and further legal action as the situation demands.''
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 12min brig and further legal action as the situation demands.''
* Any manually-operated ballistic arms that use an external magazine or feeding system
* Any semi-automatic ballistic arms not easily concealable within a pocket or coat
* Any object or instrument specifically designed or adapted for explicitly less-lethal purposes that would otherwise fall under another category
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 20min brig and further legal action as the situation demands.''
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 15min brig and further legal action as the situation demands.''
* Any semi-automatic ballistic arms that are easily concealable
* Any fully-automatic ballistic arms
@ -128,7 +128,7 @@ Controlled Armaments are grouped into categories of increasing severity. If the
* Any object or device specifically designed or adapted to cause physical harm through detonation of an explosive charge
* Any object or equipment specifically designed or adapted to protect against both a space environment and physical harm caused by weapons, projectiles, or other forms of direct assault
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 25min brig and further legal action as the situation demands.''
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 20min brig and further legal action as the situation demands.''
* Any crew-served or emplaced weapon
* Any object or instrument specifically designed or adapted to launch large-calibre explosives or projectiles for offensive or defensive purposes

View File

@ -23,6 +23,8 @@ public class RaiseEventBenchmark
PoolManager.Startup(typeof(BenchSystem).Assembly);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
var entMan = _pair.Server.EntMan;
var fact = _pair.Server.ResolveDependency<IComponentFactory>();
var bus = (EntityEventBus)entMan.EventBus;
_sys = entMan.System<BenchSystem>();
_pair.Server.WaitPost(() =>
@ -30,6 +32,8 @@ public class RaiseEventBenchmark
var uid = entMan.Spawn();
_sys.Ent = new(uid, entMan.GetComponent<TransformComponent>(uid));
_sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp);
_sys.NetId = fact.GetRegistration<TransformComponent>().NetID!.Value;
_sys.EvSubs = bus.GetNetCompEventHandlers<BenchSystem.BenchEv>();
})
.GetAwaiter()
.GetResult();
@ -60,6 +64,12 @@ public class RaiseEventBenchmark
return _sys.RaiseICompEvent();
}
[Benchmark]
public int RaiseNetEvent()
{
return _sys.RaiseNetIdEvent();
}
[Benchmark]
public int RaiseCSharpEvent()
{
@ -74,6 +84,8 @@ public class RaiseEventBenchmark
public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev);
public event EntityEventHandler? OnCSharpEvent;
public ushort NetId;
internal EntityEventBus.DirectedEventHandler?[] EvSubs = default!;
public override void Initialize()
{
@ -92,7 +104,7 @@ public class RaiseEventBenchmark
public int RaiseCompEvent()
{
var ev = new BenchEv();
EntityManager.EventBus.RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
return ev.N;
}
@ -100,7 +112,16 @@ public class RaiseEventBenchmark
{
// Raise with an IComponent instead of concrete type
var ev = new BenchEv();
EntityManager.EventBus.RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
return ev.N;
}
public int RaiseNetIdEvent()
{
// Raise a "IComponent" event using a net-id index delegate array (for PVS & client game-state events)
var ev = new BenchEv();
ref var unitEv = ref Unsafe.As<BenchEv, EntityEventBus.Unit>(ref ev);
EvSubs[NetId]?.Invoke(Ent2.Owner, Ent2.Comp, ref unitEv);
return ev.N;
}
@ -118,6 +139,7 @@ public class RaiseEventBenchmark
}
[ByRefEvent]
[ComponentEvent(Exclusive = false)]
public struct BenchEv
{
public int N;

View File

@ -60,8 +60,24 @@ public sealed class ClientAlertsSystem : AlertsSystem
if (args.Current is not AlertComponentState cast)
return;
// Save all client-sided alerts to later put back in
var clientAlerts = new Dictionary<AlertKey, AlertState>();
foreach (var alert in alerts.Comp.Alerts)
{
if (alert.Key.AlertType != null && TryGet(alert.Key.AlertType.Value, out var alertProto))
{
if (alertProto.ClientHandled)
clientAlerts[alert.Key] = alert.Value;
}
}
alerts.Comp.Alerts = new(cast.Alerts);
foreach (var alert in clientAlerts)
{
alerts.Comp.Alerts[alert.Key] = alert.Value;
}
UpdateHud(alerts);
}

View File

@ -3,6 +3,7 @@ using Content.Client.Atmos.Components;
using Content.Client.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@ -23,6 +24,7 @@ namespace Content.Client.Atmos.Overlays
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
private readonly SharedAtmosphereSystem _atmosphereSystem;
private readonly SharedMapSystem _mapSystem;
private readonly SharedTransformSystem _xformSys;
@ -54,6 +56,7 @@ namespace Content.Client.Atmos.Overlays
{
_entManager = entManager;
_mapManager = IoCManager.Resolve<IMapManager>();
_atmosphereSystem = entManager.System<SharedAtmosphereSystem>();
_mapSystem = entManager.System<SharedMapSystem>();
_xformSys = xformSys;
_shader = protoMan.Index(UnshadedShader).Instance();
@ -67,7 +70,7 @@ namespace Content.Client.Atmos.Overlays
for (var i = 0; i < _gasCount; i++)
{
var gasPrototype = protoMan.Index<GasPrototype>(system.VisibleGasId[i].ToString());
var gasPrototype = _atmosphereSystem.GetGas(system.VisibleGasId[i]);
SpriteSpecifier overlay;

View File

@ -52,14 +52,18 @@ namespace Content.Client.Atmos.UI
private void OnSelectGasPressed()
{
if (_window is null) return;
if (_window is null)
return;
if (_window.SelectedGas is null)
{
SendMessage(new GasFilterSelectGasMessage(null));
}
else
{
if (!int.TryParse(_window.SelectedGas, out var gas)) return;
if (!Enum.TryParse<Gas>(_window.SelectedGas, out var gas))
return;
SendMessage(new GasFilterSelectGasMessage(gas));
}
}

View File

@ -0,0 +1,52 @@
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Input;
using Robust.Client.Input;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
namespace Content.Client.Commands;
/// <summary>
/// Sets the a <see cref="CCVars.DebugQuickInspect"/> CVar to the name of a component, which allows the client to quickly open a VV window for that component
/// by using the Alt+C or Alt+B hotkeys.
/// </summary>
public sealed class QuickInspectCommand : LocalizedEntityCommands
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
public override string Command => "quickinspect";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine(Loc.GetString("shell-wrong-arguments-number"));
return;
}
_configurationManager.SetCVar(CCVars.DebugQuickInspect, args[0]);
var serverKey = _inputManager.GetKeyFunctionButtonString(ContentKeyFunctions.InspectServerComponent);
var clientKey = _inputManager.GetKeyFunctionButtonString(ContentKeyFunctions.InspectClientComponent);
shell.WriteLine(Loc.GetString($"cmd-quickinspect-success", ("component", args[0]), ("serverKeybind", serverKey), ("clientKeybind", clientKey)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
// Not ideal since it only shows client-side components, but you can still type in any name you want.
// If you know how to get server component names on the client then please fix this.
var options = EntityManager.ComponentFactory.AllRegisteredTypes
.Select(p => new CompletionOption(
EntityManager.ComponentFactory.GetComponentName(p)
));
return CompletionResult.FromOptions(options);
}
return CompletionResult.Empty;
}
}

View File

@ -73,7 +73,19 @@ public sealed class PuddleSystem : SharedPuddleSystem
// Maybe someday we'll have clientside prediction for entity spawning, but not today.
// Until then, these methods do nothing on the client.
/// <inheritdoc/>
public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
public override bool TrySplashSpillAt(Entity<SpillableComponent?> entity, EntityCoordinates coordinates, out EntityUid puddleUid, out Solution solution, bool sound = true, EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
solution = new Solution();
return false;
}
public override bool TrySplashSpillAt(EntityUid entity,
EntityCoordinates coordinates,
Solution spilled,
out EntityUid puddleUid,
bool sound = true,
EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
return false;

View File

@ -0,0 +1,7 @@
using Content.Shared.Fluids.Components;
using Content.Shared.Fluids.EntitySystems;
using Robust.Shared.Map;
namespace Content.Client.Fluids;
public sealed class SpraySystem : SharedSpraySystem;

View File

@ -3,6 +3,7 @@ using System.Numerics;
using Content.Client.Clickable;
using Content.Client.UserInterface;
using Content.Client.Viewport;
using Content.Shared.CCVar;
using Content.Shared.Input;
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
@ -13,6 +14,7 @@ using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
@ -40,6 +42,7 @@ namespace Content.Client.Gameplay
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IViewVariablesManager _vvm = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
private ClickableEntityComparer _comparer = default!;
@ -83,6 +86,8 @@ namespace Content.Client.Gameplay
_comparer = new ClickableEntityComparer();
CommandBinds.Builder
.Bind(ContentKeyFunctions.InspectEntity, new PointerInputCmdHandler(HandleInspect, outsidePrediction: true))
.Bind(ContentKeyFunctions.InspectServerComponent, new PointerInputCmdHandler(HandleInspectServerComponent, outsidePrediction: true))
.Bind(ContentKeyFunctions.InspectClientComponent, new PointerInputCmdHandler(HandleInspectClientComponent, outsidePrediction: true))
.Register<GameplayStateBase>();
}
@ -99,6 +104,21 @@ namespace Content.Client.Gameplay
return true;
}
private bool HandleInspectServerComponent(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
var component = _configurationManager.GetCVar(CCVars.DebugQuickInspect);
if (_entityManager.TryGetNetEntity(uid, out var net))
_conHost.ExecuteCommand($"vv /entity/{net.Value.Id}/{component}");
return true;
}
private bool HandleInspectClientComponent(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
var component = _configurationManager.GetCVar(CCVars.DebugQuickInspect);
_conHost.ExecuteCommand($"vv /c/entity/{uid}/{component}");
return true;
}
public EntityUid? GetClickedEntity(MapCoordinates coordinates)
{
return GetClickedEntity(coordinates, _eyeManager.CurrentEye);

View File

@ -2,6 +2,7 @@ using System.Linq;
using System.Numerics;
using Content.Client._DV.Traits.Assorted; // DeltaV
using Content.Shared._DV.Traits.Assorted; // DeltaV
using Content.Shared._DV.Medical; // DeltaV - Uncloneable
using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
using Content.Shared._DV.MedicalRecords; // DeltaV - Medical Records
@ -24,6 +25,7 @@ using Robust.Client.ResourceManagement;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.HealthAnalyzer.UI
{
[GenerateTypedNameReferences]
@ -35,6 +37,7 @@ namespace Content.Client.HealthAnalyzer.UI
private readonly IResourceCache _cache;
private readonly UnborgableSystem _unborgable; // DeltaV
private readonly RedshirtSystem _redshirt; // DeltaV
private readonly UncloneableSystem _uncloneable; // DeltaV
// Shitmed Change Start
public event Action<TargetBodyPart?, EntityUid>? OnBodyPartSelected;
@ -66,6 +69,7 @@ namespace Content.Client.HealthAnalyzer.UI
_cache = dependencies.Resolve<IResourceCache>();
_unborgable = _entityManager.System<UnborgableSystem>(); // DeltaV
_redshirt = _entityManager.System<RedshirtSystem>(); // DeltaV
_uncloneable = _entityManager.System<UncloneableSystem>(); // DeltaV
// Shitmed Change Start
_bodyPartControls = new Dictionary<TargetBodyPart, TextureButton>
{
@ -212,10 +216,13 @@ namespace Content.Client.HealthAnalyzer.UI
DamageLabel.Text = damageable.TotalDamage.ToString();
// Alerts
var unborgable = _unborgable.IsUnborgable(_target.Value); // DeltaV
// DeltaV traits - This is going to be horrid if we just keep adding things like this.
var unborgable = _unborgable.IsUnborgable(_target.Value);
var redshirt = _redshirt.IsRedshirt(_target.Value) && mobStateComponent?.CurrentState == MobState.Dead; // DeltaV - Redshirt
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true || unborgable || redshirt; // DeltaV - Unborgable/Redshirt
var uncloneable = _uncloneable.IsUncloneable(_target.Value) && mobStateComponent?.CurrentState == MobState.Dead; // DeltaV - Unclonable
// END DeltaV
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true || unborgable || redshirt || uncloneable; // DeltaV
AlertsDivider.Visible = showAlerts;
AlertsContainer.Visible = showAlerts;
@ -255,6 +262,14 @@ namespace Content.Client.HealthAnalyzer.UI
MaxWidth = 300
});
if (uncloneable) // DeltaV - Uncloneable
AlertsContainer.AddChild(new RichTextLabel
{
Text = Loc.GetString("health-analyzer-window-entity-uncloneable-text"),
Margin = new Thickness(0, 4),
MaxWidth = 300
});
// Damage Groups
var damageSortedGroups =

View File

@ -38,6 +38,8 @@ namespace Content.Client.Input
common.AddFunction(ContentKeyFunctions.ZoomIn);
common.AddFunction(ContentKeyFunctions.ResetZoom);
common.AddFunction(ContentKeyFunctions.InspectEntity);
common.AddFunction(ContentKeyFunctions.InspectServerComponent);
common.AddFunction(ContentKeyFunctions.InspectClientComponent);
common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow);
// Begin DeltaV Additions
common.AddFunction(ContentKeyFunctions.OpenCHelp);

View File

@ -0,0 +1,5 @@
using Content.Shared.NameIdentifier;
namespace Content.Client.NameIdentifier;
public sealed class NameIdentifierSystem : SharedNameIdentifierSystem;

View File

@ -292,6 +292,8 @@ namespace Content.Client.Options.UI.Tabs
AddButton(EngineKeyFunctions.ShowDebugMonitors);
AddButton(EngineKeyFunctions.HideUI);
AddButton(ContentKeyFunctions.InspectEntity);
AddButton(ContentKeyFunctions.InspectServerComponent);
AddButton(ContentKeyFunctions.InspectClientComponent);
AddHeader("ui-options-header-text-cursor");
AddButton(EngineKeyFunctions.TextCursorLeft);

View File

@ -19,7 +19,7 @@
<!-- Power On/Off -->
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
StyleClasses="highlight" MinWidth="120"/>
<BoxContainer Orientation="Horizontal" MinWidth="90">
<BoxContainer Orientation="Horizontal" MinWidth="150">
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
</BoxContainer>
<!--Charging Status-->

View File

@ -40,7 +40,14 @@ namespace Content.Client.Power.APC.UI
if (PowerLabel != null)
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power));
if (castState.Tripped)
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-tripped");
}
else
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power), ("maxLoad", castState.MaxLoad));
}
}
if (ExternalPowerStateLabel != null)

View File

@ -18,7 +18,9 @@ public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequir
return;
}
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
if (!args.Silent)
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
args.Cancel();
}
}

View File

@ -1,5 +0,0 @@
using Content.Shared.Power.EntitySystems;
namespace Content.Client.Power.EntitySystems;
public sealed class ChargerSystem : SharedChargerSystem;

View File

@ -1,67 +0,0 @@
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;
[UsedImplicitly]
public sealed class PowerCellSystem : SharedPowerCellSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerCellVisualsComponent, AppearanceChangeEvent>(OnPowerCellVisualsChange);
}
/// <inheritdoc/>
public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return battery.CanUse;
}
/// <inheritdoc/>
public override bool HasDrawCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return battery.CanDraw;
}
private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
return;
// If no appearance data is set, rely on whatever existing sprite state is set being correct.
if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
return;
var positiveCharge = level > 0;
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);
if (positiveCharge)
_sprite.LayerSetRsiState((uid, args.Sprite), PowerCellVisualLayers.Unshaded, $"o{level}");
}
private enum PowerCellVisualLayers : byte
{
Base,
Unshaded,
}
}

View File

@ -0,0 +1,11 @@
namespace Content.Client.PowerCell;
/// <summary>
/// Sprite layers for power cells.
/// For use with the generic visualizer.
/// </summary>
public enum PowerCellVisualLayers : byte
{
Base,
Unshaded,
}

View File

@ -1,4 +0,0 @@
namespace Content.Client.PowerCell;
[RegisterComponent]
public sealed partial class PowerCellVisualsComponent : Component {}

View File

@ -1,4 +1,4 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
namespace Content.Client.PowerCell;
@ -9,15 +9,13 @@ public sealed partial class PowerChargerVisualsComponent : Component
/// <summary>
/// The base sprite state used if the power cell charger does not contain a power cell.
/// </summary>
[DataField("emptyState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string EmptyState = "empty";
/// <summary>
/// The base sprite state used if the power cell charger contains a power cell.
/// </summary>
[DataField("occupiedState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string OccupiedState = "full";
/// <summary>
@ -27,8 +25,7 @@ public sealed partial class PowerChargerVisualsComponent : Component
/// <see cref="CellChargerStatus.Charging"/> Maps to the state used when the charger is charging a power cell.
/// <see cref="CellChargerStatus.Charged"/> Maps to the state used when the charger contains a fully charged power cell.
/// </summary>
[DataField("lightStates")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public Dictionary<CellChargerStatus, string> LightStates = new()
{
[CellChargerStatus.Off] = "light-off",

View File

@ -1,4 +1,4 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;

View File

@ -20,7 +20,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
[Dependency] private readonly IPlacementManager _placementManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly HandsSystem _hands = default!;
private Direction _placementDirection = default;
public override void Update(float frameTime)
@ -42,6 +42,11 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
var heldEntity = _hands.GetActiveItem(player);
// Don't open the placement overlay for client-side RCDs.
// This may happen when predictively spawning one in your hands.
if (heldEntity != null && IsClientSide(heldEntity.Value))
return;
if (!TryComp<RCDComponent>(heldEntity, out var rcd))
{
// If the player was holding an RCD, but is no longer, cancel placement
@ -69,7 +74,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
MobUid = heldEntity.Value,
PlacementOption = PlacementMode,
EntityType = prototype.Prototype,
Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
Range = (int)Math.Ceiling(SharedInteractionSystem.InteractionRange),
IsTile = (prototype.Mode == RcdMode.ConstructTile),
UseEditorContext = false,
};

View File

@ -1,7 +1,6 @@
using Content.Shared._DV.Silicons.Borgs; // DeltaV
using Content.Shared.Silicons.Borgs;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.Silicons.Borgs;
@ -25,32 +24,31 @@ public sealed class BorgBoundUserInterface : BoundUserInterface
_menu.BrainButtonPressed += () =>
{
SendMessage(new BorgEjectBrainBuiMessage());
SendPredictedMessage(new BorgEjectBrainBuiMessage());
};
_menu.IdChipButtonPressed += () => SendMessage(new BorgEjectIdChipMessage()); // DeltaV
_menu.EjectBatteryButtonPressed += () =>
{
SendMessage(new BorgEjectBatteryBuiMessage());
SendPredictedMessage(new BorgEjectBatteryBuiMessage());
};
_menu.NameChanged += name =>
{
SendMessage(new BorgSetNameBuiMessage(name));
SendPredictedMessage(new BorgSetNameBuiMessage(name));
};
_menu.RemoveModuleButtonPressed += module =>
{
SendMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
SendPredictedMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
if (state is not BorgBuiState msg)
return;
_menu?.UpdateState(msg);
_menu?.UpdateBatteryButton();
_menu?.UpdateBrainButton();
_menu?.UpdateIdChipButton(); // DeltaV
_menu?.UpdateModulePanel();
}
}

View File

@ -5,8 +5,8 @@ using Content.Shared.Access.Components; // DeltaV
using Content.Shared.CCVar;
using Content.Shared.NameIdentifier;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Preferences;
using Content.Shared.Silicons.Borgs;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
@ -22,6 +22,8 @@ public sealed partial class BorgMenu : FancyWindow
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IEntityManager _entity = default!;
private readonly NameModifierSystem _nameModifier;
private readonly PowerCellSystem _powerCell;
private readonly SharedBatterySystem _battery;
public Action? BrainButtonPressed;
public Action? IdChipButtonPressed; // DeltaV
@ -44,6 +46,8 @@ public sealed partial class BorgMenu : FancyWindow
IoCManager.InjectDependencies(this);
_nameModifier = _entity.System<NameModifierSystem>();
_powerCell = _entity.System<PowerCellSystem>();
_battery = _entity.System<SharedBatterySystem>();
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
@ -64,8 +68,6 @@ public sealed partial class BorgMenu : FancyWindow
NameLineEdit.OnTextChanged += OnNameChanged;
NameLineEdit.OnTextEntered += OnNameEntered;
NameLineEdit.OnFocusExit += OnNameFocusExit;
UpdateBrainButton();
}
public void SetEntity(EntityUid entity)
@ -86,6 +88,10 @@ public sealed partial class BorgMenu : FancyWindow
NameIdentifierLabel.Visible = false;
NameLineEdit.Text = _entity.GetComponent<MetaDataComponent>(Entity).EntityName;
}
UpdateBatteryButton();
UpdateBrainButton();
UpdateModulePanel();
}
protected override void FrameUpdate(FrameEventArgs args)
@ -93,22 +99,24 @@ public sealed partial class BorgMenu : FancyWindow
base.FrameUpdate(args);
AccumulatedTime += args.DeltaSeconds;
BorgSprite.OverrideDirection = (Direction) ((int) AccumulatedTime % 4 * 2);
}
BorgSprite.OverrideDirection = (Direction)((int)AccumulatedTime % 4 * 2);
public void UpdateState(BorgBuiState state)
{
EjectBatteryButton.Disabled = !state.HasBattery;
ChargeBar.Value = state.ChargePercent;
var chargeFraction = 0f;
if (_powerCell.TryGetBatteryFromSlot(Entity, out var battery))
chargeFraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
ChargeBar.Value = chargeFraction;
ChargeLabel.Text = Loc.GetString("borg-ui-charge-label",
("charge", (int) MathF.Round(state.ChargePercent * 100)));
UpdateBrainButton();
UpdateIdChipButton(); // DeltaV
UpdateModulePanel();
("charge", (int)MathF.Round(chargeFraction * 100)));
}
private void UpdateBrainButton()
public void UpdateBatteryButton()
{
EjectBatteryButton.Disabled = !_powerCell.HasBattery(Entity);
}
public void UpdateBrainButton()
{
if (_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis) && chassis.BrainEntity is { } brain)
{
@ -130,7 +138,7 @@ public sealed partial class BorgMenu : FancyWindow
/// <summary>
/// DeltaV: Updates the Eject ID Chip button text and enabled status.
/// </summary>
private void UpdateIdChipButton()
public void UpdateIdChipButton()
{
if (!_entity.TryGetComponent<IdChipSlotComponent>(Entity, out var comp))
{
@ -151,7 +159,7 @@ public sealed partial class BorgMenu : FancyWindow
}
}
private void UpdateModulePanel()
public void UpdateModulePanel()
{
if (!_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis))
return;

View File

@ -0,0 +1,83 @@
using Content.Shared.PowerCell.Components;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Shared.Player;
namespace Content.Client.Silicons.Borgs;
public sealed partial class BorgSystem
{
// How often to update the battery alert.
// Also gets updated instantly when switching bodies or a battery is inserted or removed.
private static readonly TimeSpan AlertUpdateDelay = TimeSpan.FromSeconds(0.5f);
// Don't put this on the component because we only need to track the time for a single entity
// and we don't want to TryComp it every single tick.
private TimeSpan _nextAlertUpdate = TimeSpan.Zero;
private EntityQuery<BorgChassisComponent> _chassisQuery;
private EntityQuery<PowerCellSlotComponent> _slotQuery;
public void InitializeBattery()
{
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
_chassisQuery = GetEntityQuery<BorgChassisComponent>();
_slotQuery = GetEntityQuery<PowerCellSlotComponent>();
}
private void OnPlayerAttached(Entity<BorgChassisComponent> ent, ref LocalPlayerAttachedEvent args)
{
UpdateBatteryAlert((ent.Owner, ent.Comp, null));
}
private void OnPlayerDetached(Entity<BorgChassisComponent> ent, ref LocalPlayerDetachedEvent args)
{
// Remove all borg related alerts.
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
}
private void UpdateBatteryAlert(Entity<BorgChassisComponent, PowerCellSlotComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp2, false))
return;
if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, ent.Comp2), out var battery))
{
_alerts.ShowAlert(ent.Owner, ent.Comp1.NoBatteryAlert);
return;
}
// Alert levels from 0 to 10.
var chargeLevel = (short)MathF.Round(_battery.GetChargeLevel(battery.Value.AsNullable()) * 10f);
// we make sure 0 only shows if they have absolutely no battery.
// also account for floating point imprecision
if (chargeLevel == 0 && _powerCell.HasDrawCharge((ent.Owner, null, ent.Comp2)))
{
chargeLevel = 1;
}
_alerts.ShowAlert(ent.Owner, ent.Comp1.BatteryAlert, chargeLevel);
}
// Periodically update the charge indicator.
// We do this with a client-side alert so that we don't have to network the charge level.
public void UpdateBattery(float frameTime)
{
if (_player.LocalEntity is not { } localPlayer)
return;
var curTime = _timing.CurTime;
if (curTime < _nextAlertUpdate)
return;
_nextAlertUpdate = curTime + AlertUpdateDelay;
if (!_chassisQuery.TryComp(localPlayer, out var chassis) || !_slotQuery.TryComp(localPlayer, out var slot))
return;
UpdateBatteryAlert((localPlayer, chassis, slot));
}
}

View File

@ -1,72 +1,93 @@
using Content.Shared.Mobs;
using Content.Shared.Alert;
using Content.Shared.Mobs;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Silicons.Borgs;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Client.Silicons.Borgs;
/// <inheritdoc/>
public sealed class BorgSystem : SharedBorgSystem
public sealed partial class BorgSystem : SharedBorgSystem
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override void Initialize()
{
base.Initialize();
InitializeBattery();
SubscribeLocalEvent<BorgChassisComponent, AppearanceChangeEvent>(OnBorgAppearanceChanged);
SubscribeLocalEvent<MMIComponent, AppearanceChangeEvent>(OnMMIAppearanceChanged);
}
private void OnBorgAppearanceChanged(EntityUid uid, BorgChassisComponent component, ref AppearanceChangeEvent args)
public override void UpdateUI(Entity<BorgChassisComponent?> chassis)
{
if (_ui.TryGetOpenUi(chassis.Owner, BorgUiKey.Key, out var bui))
bui.Update();
}
private void OnBorgAppearanceChanged(Entity<BorgChassisComponent> chassis, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
UpdateBorgAppearance(uid, component, args.Component, args.Sprite);
UpdateBorgAppearance((chassis.Owner, chassis.Comp, args.Component, args.Sprite));
}
protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
protected override void OnInserted(Entity<BorgChassisComponent> chassis, ref EntInsertedIntoContainerMessage args)
{
if (!component.Initialized)
if (!chassis.Comp.Initialized)
return;
base.OnInserted(uid, component, args);
UpdateBorgAppearance(uid, component);
base.OnInserted(chassis, ref args);
UpdateUI(chassis.AsNullable());
UpdateBorgAppearance((chassis, chassis.Comp));
UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
}
protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args)
protected override void OnRemoved(Entity<BorgChassisComponent> chassis, ref EntRemovedFromContainerMessage args)
{
if (!component.Initialized)
if (!chassis.Comp.Initialized)
return;
base.OnRemoved(uid, component, args);
UpdateBorgAppearance(uid, component);
base.OnRemoved(chassis, ref args);
UpdateUI(chassis.AsNullable());
UpdateBorgAppearance((chassis, chassis.Comp));
UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
}
private void UpdateBorgAppearance(EntityUid uid,
BorgChassisComponent? component = null,
AppearanceComponent? appearance = null,
SpriteComponent? sprite = null)
private void UpdateBorgAppearance(Entity<BorgChassisComponent?, AppearanceComponent?, SpriteComponent?> ent)
{
if (!Resolve(uid, ref component, ref appearance, ref sprite))
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, ref ent.Comp3))
return;
if (_appearance.TryGetData<MobState>(uid, MobStateVisuals.State, out var state, appearance))
if (_appearance.TryGetData<MobState>(ent.Owner, MobStateVisuals.State, out var state, ent.Comp2))
{
if (state != MobState.Alive)
{
_sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, false);
_sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, false);
return;
}
}
if (!_appearance.TryGetData<bool>(uid, BorgVisuals.HasPlayer, out var hasPlayer, appearance))
if (!_appearance.TryGetData<bool>(ent.Owner, BorgVisuals.HasPlayer, out var hasPlayer, ent.Comp2))
hasPlayer = false;
_sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, component.BrainEntity != null || hasPlayer);
_sprite.LayerSetRsiState((uid, sprite), BorgVisualLayers.Light, hasPlayer ? component.HasMindState : component.NoMindState);
_sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, ent.Comp1.BrainEntity != null || hasPlayer);
_sprite.LayerSetRsiState((ent.Owner, ent.Comp3), BorgVisualLayers.Light, hasPlayer ? ent.Comp1.HasMindState : ent.Comp1.NoMindState);
}
private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref AppearanceChangeEvent args)
@ -107,4 +128,10 @@ public sealed class BorgSystem : SharedBorgSystem
borg.Comp.HasMindState = hasMindState;
borg.Comp.NoMindState = noMindState;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateBattery(frameTime);
}
}

View File

@ -1,7 +1,6 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'thief-backpack-window-title'}"
MinSize="700 700">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<!-- First Informational panel -->
@ -25,7 +24,6 @@
</BoxContainer>
</ScrollContainer>
</PanelContainer>
<!-- Third approve button panel -->
<PanelContainer Margin="10">
<Button Name="ApproveButton"

View File

@ -46,7 +46,8 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
selectedNumber++;
}
Description.Text = Loc.GetString("thief-backpack-window-description", ("maxCount", state.MaxSelectedSets));
Title = Loc.GetString(state.ToolName);
Description.Text = Loc.GetString(state.ToolDesc, ("maxCount", state.MaxSelectedSets));
SelectedSets.Text = Loc.GetString("thief-backpack-window-selected", ("selectedCount", selectedNumber), ("maxCount", state.MaxSelectedSets));
ApproveButton.Disabled = selectedNumber != state.MaxSelectedSets;
}

View File

@ -0,0 +1,5 @@
using Content.Shared.Tips;
namespace Content.Client.Tips;
public sealed class TipsSystem : SharedTipsSystem;

View File

@ -147,7 +147,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
if (emote.Category == EmoteCategory.Invalid
|| emote.ChatTriggers.Count == 0
|| !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value))
|| whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
|| whitelistSystem.IsWhitelistPass(emote.Blacklist, player.Value))
continue;
if (!emote.Available

View File

@ -7,23 +7,20 @@ public sealed partial class GunSystem
protected override void InitializeBattery()
{
base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
SubscribeLocalEvent<BatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
SubscribeLocalEvent<BatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
}
private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args)
private void OnAmmoCountUpdate(Entity<BatteryAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is not BoxesStatusControl boxes) return;
if (args.Control is not BoxesStatusControl boxes)
return;
boxes.Update(component.Shots, component.Capacity);
boxes.Update(ent.Comp.Shots, ent.Comp.Capacity);
}
private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args)
private void OnControl(Entity<BatteryAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
{
args.Control = new BoxesStatusControl();
}

View File

@ -80,7 +80,6 @@ public sealed partial class GunSystem : SharedGunSystem
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
// Plays animated effects on the client.
@ -90,10 +89,6 @@ public sealed partial class GunSystem : SharedGunSystem
InitializeSpentAmmo();
}
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
{
UpdateAmmoCount(uid, ammoComp);
}
private void OnMuzzleFlash(MuzzleFlashEvent args)
{
@ -158,6 +153,8 @@ public sealed partial class GunSystem : SharedGunSystem
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!Timing.IsFirstTimePredicted)
return;

View File

@ -38,12 +38,10 @@ public sealed class ConstantsTest
Assert.That(Atmospherics.GasAbbreviations, Has.Count.EqualTo(Atmospherics.TotalNumberOfGases),
$"GasAbbreviations size is not equal to TotalNumberOfGases.");
// the ID for each gas has to be a number from 0 to TotalNumberOfGases-1
// the ID for each gas has to correspond to a value in the Gas enum (converted to a string)
foreach (var gas in gasProtos)
{
var validInteger = int.TryParse(gas.ID, out var number);
Assert.That(validInteger, Is.True, $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
Assert.That(number, Is.InRange(0, Atmospherics.TotalNumberOfGases - 1), $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
Assert.That(Enum.TryParse<Gas>(gas.ID, out _), $"GasPrototype {gas.ID} has an invalid ID. It must correspond to a value in the {nameof(Gas)} enum.");
}
});
});

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Reflection;
@ -64,17 +66,16 @@ namespace Content.IntegrationTests.Tests.DoAfter
var server = pair.Server;
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var entityManager = server.EntMan;
var timing = server.ResolveDependency<IGameTiming>();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
var ev = new TestDoAfterEvent();
// That it finishes successfully
await server.WaitPost(() =>
{
var tickTime = 1.0f / timing.TickRate;
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args = new DoAfterArgs(entityManager, mob, tickTime / 2, ev, null) { Broadcast = true };
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod / 2, ev, null) { Broadcast = true };
#pragma warning disable NUnit2045 // Interdependent assertions.
Assert.That(doAfterSystem.TryStartDoAfter(args));
Assert.That(ev.Cancelled, Is.False);
@ -92,23 +93,17 @@ namespace Content.IntegrationTests.Tests.DoAfter
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
var entityManager = server.EntMan;
var timing = server.ResolveDependency<IGameTiming>();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
var ev = new TestDoAfterEvent();
await server.WaitPost(() =>
{
var tickTime = 1.0f / timing.TickRate;
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args = new DoAfterArgs(entityManager, mob, tickTime * 2, ev, null) { Broadcast = true };
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 2, ev, null) { Broadcast = true };
if (!doAfterSystem.TryStartDoAfter(args, out var id))
{
Assert.Fail();
return;
}
Assert.That(doAfterSystem.TryStartDoAfter(args, out var id));
Assert.That(!ev.Cancelled);
doAfterSystem.Cancel(id);
@ -121,5 +116,67 @@ namespace Content.IntegrationTests.Tests.DoAfter
await pair.CleanReturnAsync();
}
/// <summary>
/// Spawns two sets of mobs with a targeted DoAfter to check that the GetEntitiesInteractingWithTarget result
/// includes the correct interacting entities.
/// </summary>
[Test]
public async Task TestGetInteractingEntities()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.EntMan;
var timing = server.ResolveDependency<IGameTiming>();
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
var interactionSystem = entityManager.System<SharedInteractionSystem>();
var ev = new TestDoAfterEvent();
EntityUid mob = default;
EntityUid target = default;
EntityUid mob2 = default;
EntityUid mob3 = default;
EntityUid target2 = default;
await server.WaitPost(() =>
{
// Spawn two targets to interact with
target = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
target2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
// Spawn a mob which is interacting with the first target
mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 5, ev, null, target) { Broadcast = true };
Assert.That(doAfterSystem.TryStartDoAfter(args));
// Spawn two more mobs which are interacting with the second target
mob2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args2 = new DoAfterArgs(entityManager, mob2, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
Assert.That(doAfterSystem.TryStartDoAfter(args2));
mob3 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args3 = new DoAfterArgs(entityManager, mob3, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
Assert.That(doAfterSystem.TryStartDoAfter(args3));
});
var list = new HashSet<EntityUid>();
interactionSystem.GetEntitiesInteractingWithTarget(target, list);
Assert.That(list, Is.EquivalentTo([mob]), $"{mob} was not considered to be interacting with {target}");
interactionSystem.GetEntitiesInteractingWithTarget(target2, list);
Assert.That(list, Is.EquivalentTo([mob2, mob3]), $"{mob2} and {mob3} were not considered to be interacting with {target2}");
await server.WaitPost(() =>
{
entityManager.DeleteEntity(mob);
entityManager.DeleteEntity(mob2);
entityManager.DeleteEntity(mob3);
entityManager.DeleteEntity(target);
entityManager.DeleteEntity(target2);
});
await pair.CleanReturnAsync();
}
}
}

View File

@ -49,6 +49,9 @@ public abstract partial class InteractionTest
public static implicit operator EntitySpecifier(string prototype)
=> new(prototype, 1);
public static implicit operator EntitySpecifier(EntProtoId prototype)
=> new(prototype.Id, 1);
public static implicit operator EntitySpecifier((string, int) tuple)
=> new(tuple.Item1, tuple.Item2);

View File

@ -54,6 +54,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryCharger
- type: entity
@ -68,6 +69,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryDischarger
- type: entity
@ -85,6 +87,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryDischarger
node: output
- type: BatteryCharger
@ -110,6 +113,7 @@ namespace Content.IntegrationTests.Tests.Power
maxSupply: 1000
supplyRampTolerance: 1000
- type: Battery
netsync: false
maxCharge: 1000
startingCharge: 1000
- type: Transform
@ -119,6 +123,7 @@ namespace Content.IntegrationTests.Tests.Power
id: ApcDummy
components:
- type: Battery
netsync: false
maxCharge: 10000
startingCharge: 10000
- type: PowerNetworkBattery
@ -380,6 +385,8 @@ namespace Content.IntegrationTests.Tests.Power
const float startingCharge = 100_000;
PowerNetworkBatteryComponent netBattery = default!;
EntityUid generatorEnt = default!;
EntityUid consumerEnt = default!;
BatteryComponent battery = default!;
PowerConsumerComponent consumer = default!;
@ -395,8 +402,8 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
}
var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(generatorEnt);
battery = entityManager.GetComponent<BatteryComponent>(generatorEnt);
@ -441,7 +448,8 @@ namespace Content.IntegrationTests.Tests.Power
// Trivial integral to calculate expected power spent.
const double spentExpected = (200 + 100) / 2.0 * 0.25;
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
var currentCharge = batterySys.GetCharge((generatorEnt, battery));
Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
});
@ -460,7 +468,8 @@ namespace Content.IntegrationTests.Tests.Power
// Trivial integral to calculate expected power spent.
const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25;
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
var currentCharge = batterySys.GetCharge((generatorEnt, battery));
Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
});
@ -576,6 +585,8 @@ namespace Content.IntegrationTests.Tests.Power
var entityManager = server.ResolveDependency<IEntityManager>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid generatorEnt = default!;
EntityUid batteryEnt = default!;
PowerSupplierComponent supplier = default!;
BatteryComponent battery = default!;
@ -591,8 +602,8 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
}
var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
var netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(batteryEnt);
@ -615,7 +626,8 @@ namespace Content.IntegrationTests.Tests.Power
{
// half a second @ 500 W = 250
// 50% efficiency, so 125 J stored total.
Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(125).Within(0.1));
Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1));
});
});
@ -633,6 +645,9 @@ namespace Content.IntegrationTests.Tests.Power
var gameTiming = server.ResolveDependency<IGameTiming>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid batteryEnt = default!;
EntityUid supplyEnt = default!;
EntityUid consumerEnt = default!;
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
@ -653,9 +668,9 @@ namespace Content.IntegrationTests.Tests.Power
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@ -694,7 +709,8 @@ namespace Content.IntegrationTests.Tests.Power
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1));
const int expectedSpent = 200;
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
});
@ -711,6 +727,9 @@ namespace Content.IntegrationTests.Tests.Power
var gameTiming = server.ResolveDependency<IGameTiming>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid batteryEnt = default!;
EntityUid supplyEnt = default!;
EntityUid consumerEnt = default!;
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
@ -731,9 +750,9 @@ namespace Content.IntegrationTests.Tests.Power
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@ -772,7 +791,8 @@ namespace Content.IntegrationTests.Tests.Power
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1));
const int expectedSpent = 400;
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
});
@ -1223,6 +1243,9 @@ namespace Content.IntegrationTests.Tests.Power
var entityManager = server.ResolveDependency<IEntityManager>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid generatorEnt = default!;
EntityUid substationEnt = default!;
EntityUid apcEnt = default!;
PowerNetworkBatteryComponent substationNetBattery = default!;
BatteryComponent apcBattery = default!;
@ -1242,9 +1265,9 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 1));
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 2));
var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
var generatorSupplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
substationNetBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(substationEnt);
@ -1262,8 +1285,9 @@ namespace Content.IntegrationTests.Tests.Power
{
Assert.Multiple(() =>
{
var currentCharge = batterySys.GetCharge((apcEnt, apcBattery));
Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power
Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
Assert.That(currentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
});
});

View File

@ -3,15 +3,16 @@ using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Power.Components;
using Content.Shared.NodeContainer;
using Robust.Server.GameObjects;
using Robust.Shared.EntitySerialization;
namespace Content.IntegrationTests.Tests.Power;
[Explicit]
public sealed class StationPowerTests
{
/// <summary>
@ -19,29 +20,32 @@ public sealed class StationPowerTests
/// </summary>
private const float MinimumPowerDurationSeconds = 10 * 60;
// Begin DeltaV Additions - DeltaV maps for testing
private static readonly string[] GameMaps =
[
"Fland",
"Meta",
"Packed",
"Omega",
"Bagel",
"Box",
"Core",
"Marathon",
"Saltern",
"Reach",
"Train",
"Oasis",
"Gate",
"Amber",
"Loop",
"Plasma",
"Elkridge",
"Convex",
"Relic",
"Academy",
"Arena",
"Asterisk",
"Byoin",
"Chibi",
"Division",
"Edge",
"Elegance",
"Glacier",
"Hammurabi",
"TheHive",
"Lighthouse",
"Micro",
"Ovni",
"Pebble",
"Shoukou",
"Submarine",
"Terra",
"Tortuga",
];
// Begin DeltaV Additions - DeltaV maps for testing
[Explicit]
[Test, TestCaseSource(nameof(GameMaps))]
public async Task TestStationStartingPowerWindow(string mapProtoId)
{
@ -54,6 +58,7 @@ public sealed class StationPowerTests
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var ticker = entMan.System<GameTicker>();
var batterySys = entMan.System<BatterySystem>();
// Load the map
await server.WaitAssertion(() =>
@ -77,7 +82,8 @@ public sealed class StationPowerTests
if (node.NodeGroup is not IBasePowerNet group)
continue;
networks.TryGetValue(group.NetworkNode, out var charge);
networks[group.NetworkNode] = charge + battery.CurrentCharge;
var currentCharge = batterySys.GetCharge((uid, battery));
networks[group.NetworkNode] = charge + currentCharge;
}
var totalStartingCharge = networks.MaxBy(n => n.Value).Value;
@ -100,6 +106,54 @@ public sealed class StationPowerTests
$"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!");
});
await pair.CleanReturnAsync();
}
[Test, TestCaseSource(nameof(GameMaps))]
public async Task TestApcLoad(string mapProtoId)
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Dirty = true,
});
var server = pair.Server;
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var ticker = entMan.System<GameTicker>();
var xform = entMan.System<TransformSystem>();
// Load the map
await server.WaitAssertion(() =>
{
Assert.That(protoMan.TryIndex<GameMapPrototype>(mapProtoId, out var mapProto));
var opts = DeserializationOptions.Default with { InitializeMaps = true };
ticker.LoadGameMap(mapProto, out var mapId, opts);
});
// Wait long enough for power to ramp up, but before anything can trip
await pair.RunSeconds(2);
// Check that no APCs start overloaded
var apcQuery = entMan.EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent>();
Assert.Multiple(() =>
{
while (apcQuery.MoveNext(out var uid, out var apc, out var battery))
{
// Uncomment the following line to log starting APC load to the console
//Console.WriteLine($"ApcLoad:{mapProtoId}:{uid}:{battery.CurrentSupply}");
if (xform.TryGetMapOrGridCoordinates(uid, out var coord))
{
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
$"APC {uid} on {mapProtoId} ({coord.Value.X}, {coord.Value.Y}) is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
}
else
{
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
$"APC {uid} on {mapProtoId} is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
}
}
});
await pair.CleanReturnAsync();
}

View File

@ -0,0 +1,38 @@
using System.Collections.Generic;
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Interaction;
using Content.Shared.Movement.Pulling.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Puller;
#nullable enable
public sealed class InteractingEntitiesTest : InteractionTest
{
private static readonly EntProtoId MobHuman = "MobHuman";
/// <summary>
/// Spawns a Target mob, and a second mob which drags it,
/// and checks that the dragger is considered to be interacting with the dragged mob.
/// </summary>
[Test]
public async Task PullerIsConsideredInteractingTest()
{
await SpawnTarget(MobHuman);
var puller = await SpawnEntity(MobHuman, ToServer(TargetCoords));
var pullSys = SEntMan.System<PullingSystem>();
await Server.WaitAssertion(() =>
{
Assert.That(pullSys.TryStartPull(puller, ToServer(Target.Value)),
$"{puller} failed to start pulling {Target}");
});
var list = new HashSet<EntityUid>();
Server.System<SharedInteractionSystem>()
.GetEntitiesInteractingWithTarget(ToServer(Target.Value), list);
Assert.That(list, Is.EquivalentTo([puller]), $"{puller} was not considered to be interacting with {Target}");
}
}

View File

@ -0,0 +1,263 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Containers;
using Content.Shared.Item;
using Content.Shared.Prototypes;
using Content.Shared.Storage;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Storage;
public sealed class StorageTest
{
/// <summary>
/// Can an item store more than itself weighs.
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
/// </summary>
[Test]
public async Task StorageSizeArbitrageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
var entMan = server.ResolveDependency<IEntityManager>();
var itemSys = entMan.System<SharedItemSystem>();
await server.WaitAssertion(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
storage.Whitelist != null ||
storage.MaxItemSize == null ||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
continue;
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
$"Found storage arbitrage on {proto.ID}");
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestStorageFillPrototypes()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
continue;
foreach (var entry in storage.Contents)
{
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
}
}
});
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestSufficientSpaceForFill()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var id = compFact.GetComponentName<StorageFillComponent>();
var itemSys = entMan.System<SharedItemSystem>();
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
allSizes.Sort();
await Assert.MultipleAsync(async () =>
{
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<EntityStorageComponent>(compFact))
continue;
StorageComponent? storage = null;
ItemComponent? item = null;
var size = 0;
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("Storage", out storage))
{
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
return;
}
proto.TryGetComponent("Item", out item);
size = GetFillSize(fill, false, protoMan, itemSys);
});
if (storage == null)
continue;
var maxSize = storage.MaxItemSize;
if (storage.MaxItemSize == null)
{
if (item?.Size == null)
{
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
}
else
{
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
var index = Math.Max(0, curIndex - 1);
maxSize = allSizes[index].ID;
}
}
if (maxSize == null)
continue;
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
foreach (var entry in fill.Contents)
{
if (entry.PrototypeId == null)
continue;
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
continue;
ItemComponent? entryItem = null;
await server.WaitPost(() =>
{
fillItem.TryGetComponent("Item", out entryItem);
});
if (entryItem == null)
continue;
Assert.That(protoMan.Index(entryItem.Size).Weight,
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
}
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestSufficientSpaceForEntityStorageFill()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var id = compFact.GetComponentName<StorageFillComponent>();
var itemSys = entMan.System<SharedItemSystem>();
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<StorageComponent>(compFact))
continue;
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
if (entStorage == null)
return;
var size = GetFillSize(fill, true, protoMan, itemSys);
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
$"{proto.ID} storage fill is too large.");
});
}
await pair.CleanReturnAsync();
}
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
{
if (entry.PrototypeId == null)
return 0;
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
{
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
return 0;
}
if (getCount)
return entry.Amount;
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
return 0;
}
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
{
var totalSize = 0;
var groups = new Dictionary<string, int>();
foreach (var entry in fill.Contents)
{
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
if (entry.GroupId == null)
totalSize += size;
else
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
}
return totalSize + groups.Values.Sum();
}
/// <summary>
/// Tests that prototypes are not using multiple container fill components at the same time.
/// </summary>
[Test]
public async Task NoMultipleContainerFillsTest()
{
await using var pair = await PoolManager.GetServerClient();
var compFact = pair.Server.ResolveDependency<IComponentFactory>();
Assert.Multiple(() =>
{
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<EntityTableContainerFillComponent>())
{
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(StorageFillComponent)}.");
Assert.That(!proto.HasComponent<ContainerFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(ContainerFillComponent)}.");
}
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<ContainerFillComponent>())
{
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(ContainerFillComponent)} and {nameof(StorageFillComponent)}.");
}
});
await pair.CleanReturnAsync();
}
}

View File

@ -1,240 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Storage.Components;
using Content.Shared.Item;
using Content.Shared.Prototypes;
using Content.Shared.Storage;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
public sealed class StorageTest
{
/// <summary>
/// Can an item store more than itself weighs.
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
/// </summary>
[Test]
public async Task StorageSizeArbitrageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
var entMan = server.ResolveDependency<IEntityManager>();
var itemSys = entMan.System<SharedItemSystem>();
await server.WaitAssertion(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
storage.Whitelist != null ||
storage.MaxItemSize == null ||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
continue;
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
$"Found storage arbitrage on {proto.ID}");
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestStorageFillPrototypes()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
continue;
foreach (var entry in storage.Contents)
{
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
}
}
});
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestSufficientSpaceForFill()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var id = compFact.GetComponentName<StorageFillComponent>();
var itemSys = entMan.System<SharedItemSystem>();
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
allSizes.Sort();
await Assert.MultipleAsync(async () =>
{
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<EntityStorageComponent>(compFact))
continue;
StorageComponent? storage = null;
ItemComponent? item = null;
var size = 0;
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("Storage", out storage))
{
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
return;
}
proto.TryGetComponent("Item", out item);
size = GetFillSize(fill, false, protoMan, itemSys);
});
if (storage == null)
continue;
var maxSize = storage.MaxItemSize;
if (storage.MaxItemSize == null)
{
if (item?.Size == null)
{
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
}
else
{
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
var index = Math.Max(0, curIndex - 1);
maxSize = allSizes[index].ID;
}
}
if (maxSize == null)
continue;
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
foreach (var entry in fill.Contents)
{
if (entry.PrototypeId == null)
continue;
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
continue;
ItemComponent? entryItem = null;
await server.WaitPost(() =>
{
fillItem.TryGetComponent("Item", out entryItem);
});
if (entryItem == null)
continue;
Assert.That(protoMan.Index(entryItem.Size).Weight,
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
}
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestSufficientSpaceForEntityStorageFill()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var id = compFact.GetComponentName<StorageFillComponent>();
var itemSys = entMan.System<SharedItemSystem>();
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<StorageComponent>(compFact))
continue;
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
if (entStorage == null)
return;
var size = GetFillSize(fill, true, protoMan, itemSys);
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
$"{proto.ID} storage fill is too large.");
});
}
await pair.CleanReturnAsync();
}
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
{
if (entry.PrototypeId == null)
return 0;
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
{
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
return 0;
}
if (getCount)
return entry.Amount;
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
return 0;
}
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
{
var totalSize = 0;
var groups = new Dictionary<string, int>();
foreach (var entry in fill.Contents)
{
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
if (entry.GroupId == null)
totalSize += size;
else
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
}
return totalSize + groups.Values.Sum();
}
}
}

View File

@ -3,11 +3,13 @@ using System.Collections.Generic;
using Content.Server.VendingMachines;
using Content.Server.Wires;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Containers;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.EntityTable;
using Content.Shared.Prototypes;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.VendingMachines;
using Content.Shared.Wires;
using Robust.Shared.GameObjects;
@ -115,6 +117,7 @@ namespace Content.IntegrationTests.Tests
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var entityTable = server.EntMan.System<EntityTableSystem>();
await server.WaitAssertion(() =>
{
@ -134,17 +137,23 @@ namespace Content.IntegrationTests.Tests
restocks.Add(proto.ID);
}
// Collect all the prototypes with StorageFills referencing those entities.
// Collect all the prototypes with EntityTableContainerFills referencing those entities.
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageFillComponent>(out var storage, compFact))
if (!proto.TryGetComponent<EntityTableContainerFillComponent>(out var storage, compFact))
continue;
var containers = storage.Containers;
if (!containers.TryGetValue(SharedEntityStorageSystem.ContainerName, out var container)) // We only care about this container type.
continue;
List<string> restockStore = new();
foreach (var spawnEntry in storage.Contents)
foreach (var spawnEntry in entityTable.GetSpawns(container))
{
if (spawnEntry.PrototypeId != null && restocks.Contains(spawnEntry.PrototypeId))
restockStore.Add(spawnEntry.PrototypeId);
if (restocks.Contains(spawnEntry))
restockStore.Add(spawnEntry);
}
if (restockStore.Count > 0)
@ -153,7 +162,7 @@ namespace Content.IntegrationTests.Tests
// Iterate through every CargoProduct and make sure each
// prototype with a restock component is referenced in a
// purchaseable entity with a StorageFill.
// purchaseable entity with an EntityTableContianerFill.
foreach (var proto in prototypeManager.EnumeratePrototypes<CargoProductPrototype>())
{
if (restockStores.ContainsKey(proto.Product))

View File

@ -8,6 +8,7 @@ using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Kitchen;
using Content.Shared.Popups;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;

View File

@ -0,0 +1,103 @@
using Content.Shared.Administration;
using Content.Shared.Tips;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Fun)]
public sealed class TippyCommand : LocalizedEntityCommands
{
[Dependency] private readonly SharedTipsSystem _tips = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override string Command => "tippy";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteLine(Loc.GetString("cmd-tippy-help"));
return;
}
ICommonSession? targetSession = null;
if (args[0] != "all")
{
if (!_player.TryGetSessionByUsername(args[0], out targetSession))
{
shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user"));
return;
}
}
var msg = args[1];
EntProtoId? prototype = null;
if (args.Length > 2)
{
if (args[2] == "null")
prototype = null;
else if (!_prototype.HasIndex<EntityPrototype>(args[2]))
{
shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2])));
return;
}
else
prototype = args[2];
}
var speakTime = _tips.GetSpeechTime(msg);
var slideTime = 3f;
var waddleInterval = 0.5f;
if (args.Length > 3 && float.TryParse(args[3], out var parsedSpeakTime))
speakTime = parsedSpeakTime;
if (args.Length > 4 && float.TryParse(args[4], out var parsedSlideTime))
slideTime = parsedSlideTime;
if (args.Length > 5 && float.TryParse(args[5], out var parsedWaddleInterval))
waddleInterval = parsedWaddleInterval;
if (targetSession != null) // send to specified player
_tips.SendTippy(targetSession, msg, prototype, speakTime, slideTime, waddleInterval);
else // send to everyone
_tips.SendTippy(msg, prototype, speakTime, slideTime, waddleInterval);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHintOptions(
CompletionHelper.SessionNames(players: _player),
Loc.GetString("cmd-tippy-auto-1")),
2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")),
3 => CompletionResult.FromHintOptions(
CompletionHelper.PrototypeIdsLimited<EntityPrototype>(args[2], _prototype),
Loc.GetString("cmd-tippy-auto-3")),
4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")),
5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")),
6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")),
_ => CompletionResult.Empty
};
}
}
[AdminCommand(AdminFlags.Fun)]
public sealed class TipCommand : LocalizedEntityCommands
{
[Dependency] private readonly SharedTipsSystem _tips = default!;
public override string Command => "tip";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_tips.AnnounceRandomTip();
_tips.RecalculateNextTipTime();
}
}

View File

@ -15,6 +15,7 @@ using Content.Shared.Verbs;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Content.Shared.Roles.Components;
namespace Content.Server.Administration.Systems;
@ -32,6 +33,7 @@ public sealed partial class AdminVerbSystem
private static readonly EntProtoId DefaultThiefRule = "Thief";
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
private static readonly EntProtoId DefaultWizardRule = "Wizard";
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
// All antag verbs have names so invokeverb works.
@ -192,6 +194,22 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", paradoxCloneName, Loc.GetString("admin-verb-make-paradox-clone")),
};
var wizardName = Loc.GetString("admin-verb-text-make-wizard");
Verb wizard = new()
{
Text = wizardName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "Wizard"),
Act = () =>
{
// Wizard has no rule components as of writing, but I gotta put something here to satisfy the machine so just make it wizard mind rule :)
_antag.ForceMakeAntag<WizardRoleComponent>(targetPlayer, DefaultWizardRule);
},
Impact = LogImpact.High,
Message = string.Join(": ", wizardName, Loc.GetString("admin-verb-make-wizard")),
};
args.Verbs.Add(wizard);
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
args.Verbs.Add(paradox);

View File

@ -6,7 +6,6 @@ using Content.Server.Cargo.Components;
using Content.Server.Doors.Systems;
using Content.Server.Hands.Systems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Revenant.Components; // Imp
using Content.Server.Revenant.EntitySystems; // Imp
using Content.Server.Stack;
@ -28,6 +27,7 @@ using Content.Shared.Inventory;
using Content.Shared.Item; // Imp
using Content.Shared.PDA;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stacks;
using Content.Shared.Station.Components;
using Content.Shared.Verbs;
@ -54,7 +54,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly AdminTestArenaSystem _adminTestArenaSystem = default!;
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
[Dependency] private readonly JointSystem _jointSystem = default!;
[Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly SharedBatterySystem _batterySystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
[Dependency] private readonly RevenantAnimatedSystem _revenantAnimate = default!; // Imp
@ -206,6 +206,8 @@ public sealed partial class AdminVerbSystem
var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target);
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
Dirty(args.Target, recharger);
_batterySystem.RefreshChargeRate((args.Target, battery));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
@ -920,4 +922,4 @@ public sealed partial class AdminVerbSystem
MakeAnimate = -30, // Imp
MakeInanimate = -31, // Imp
}
}
}

View File

@ -0,0 +1,40 @@
using Content.Server.Antag.Components;
using Robust.Shared.Random;
namespace Content.Server.Antag;
public sealed class AntagMultipleRoleSpawnerSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ILogManager _log = default!;
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AntagMultipleRoleSpawnerComponent, AntagSelectEntityEvent>(OnSelectEntity);
_sawmill = _log.GetSawmill("antag_multiple_spawner");
}
private void OnSelectEntity(Entity<AntagMultipleRoleSpawnerComponent> ent, ref AntagSelectEntityEvent args)
{
// If its more than one the logic breaks
if (args.AntagRoles.Count != 1)
{
_sawmill.Fatal($"Antag multiple role spawner had more than one antag ({args.AntagRoles.Count})");
return;
}
var role = args.AntagRoles[0];
var entProtos = ent.Comp.AntagRoleToPrototypes[role];
if (entProtos.Count == 0)
return; // You will just get a normal job
args.Entity = Spawn(ent.Comp.PickAndTake ? _random.PickAndTake(entProtos) : _random.Pick(entProtos));
}
}

View File

@ -271,7 +271,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
bool midround = false)
{
var playerPool = GetPlayerPool(ent, pool, def);
var existingAntagCount = ent.Comp.PreSelectedSessions.TryGetValue(def, out var existingAntags) ? existingAntags.Count : 0;
var existingAntagCount = ent.Comp.PreSelectedSessions.TryGetValue(def, out var existingAntags) ? existingAntags.Count : 0;
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def) - existingAntagCount;
// if there is both a spawner and players getting picked, let it fall back to a spawner.
@ -396,7 +396,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!antagEnt.HasValue)
{
var getEntEv = new AntagSelectEntityEvent(session, ent);
var getEntEv = new AntagSelectEntityEvent(session, ent, def.PrefRoles);
RaiseLocalEvent(ent, ref getEntEv, true);
antagEnt = getEntEv.Entity;
}
@ -404,7 +405,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (antagEnt is not { } player)
{
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
_adminLogger.Add(LogType.AntagSelection,$"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
_adminLogger.Add(LogType.AntagSelection, $"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
if (session != null && ent.Comp.RemoveUponFailedSpawn)
{
ent.Comp.AssignedSessions.Remove(session);
@ -419,7 +420,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
// Therefore any component subscribing to this has to make sure both subscriptions return the same value
// or the ghost role raffle location preview will be wrong.
var getPosEv = new AntagSelectLocationEvent(session, ent);
var getPosEv = new AntagSelectLocationEvent(session, ent, player);
RaiseLocalEvent(ent, ref getPosEv, true);
if (getPosEv.Handled)
{
@ -435,7 +436,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
{
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
_adminLogger.Add(LogType.AntagSelection,$"Antag spawner {player} in gamerule {ToPrettyString(ent)} failed due to not having GhostRoleAntagSpawnerComponent.");
_adminLogger.Add(LogType.AntagSelection, $"Antag spawner {player} in gamerule {ToPrettyString(ent)} failed due to not having GhostRoleAntagSpawnerComponent.");
if (session != null)
{
ent.Comp.AssignedSessions.Remove(session);
@ -538,21 +539,21 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
switch (def.MultiAntagSetting)
{
case AntagAcceptability.None:
{
if (_role.MindIsAntagonist(mind))
return false;
if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet
return false;
break;
}
{
if (_role.MindIsAntagonist(mind))
return false;
if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet
return false;
break;
}
case AntagAcceptability.NotExclusive:
{
if (_role.MindIsExclusiveAntagonist(mind))
return false;
if (GetPreSelectedExclusiveAntagSessions(def).Contains(session))
return false;
break;
}
{
if (_role.MindIsExclusiveAntagonist(mind))
return false;
if (GetPreSelectedExclusiveAntagSessions(def).Contains(session))
return false;
break;
}
}
// todo: expand this to allow for more fine antag-selection logic for game rules.
@ -607,10 +608,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// Only raised if the selected player's current entity is invalid.
/// </summary>
[ByRefEvent]
public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule, List<ProtoId<AntagPrototype>> AntagRoles)
{
public readonly ICommonSession? Session = Session;
/// list of antag role prototypes associated with a entity. used by the <see cref="AntagMultipleRoleSpawnerComponent"/>
public readonly List<ProtoId<AntagPrototype>> AntagRoles = AntagRoles;
public bool Handled => Entity != null;
public EntityUid? Entity;
@ -620,12 +624,15 @@ public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<Anta
/// Event raised on a game rule entity to determine the location for the antagonist.
/// </summary>
[ByRefEvent]
public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule, EntityUid Entity)
{
public readonly ICommonSession? Session = Session;
public bool Handled => Coordinates.Any();
// the entity of the antagonist
public EntityUid Entity = Entity;
public List<MapCoordinates> Coordinates = new();
}

View File

@ -0,0 +1,23 @@
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server.Antag.Components;
/// <summary>
/// Selects and spawns one prototype from a list for each antag prototype selected by the <see cref="AntagSelectionSystem"/>
/// </summary>
[RegisterComponent]
public sealed partial class AntagMultipleRoleSpawnerComponent : Component
{
/// <summary>
/// antag prototype -> list of possible entities to spawn for that antag prototype. Will choose from the list randomly once with replacement unless <see cref="PickAndTake"/> is set to true
/// </summary>
[DataField]
public Dictionary<ProtoId<AntagPrototype>, List<EntProtoId>> AntagRoleToPrototypes;
/// <summary>
/// Should you remove ent prototypes from the list after spawning one.
/// </summary>
[DataField]
public bool PickAndTake;
}

View File

@ -52,12 +52,12 @@ public sealed class BlockGameArcadeSystem : EntitySystem
private void OnAfterUIOpen(EntityUid uid, BlockGameArcadeComponent component, AfterActivatableUIOpenEvent args)
{
if (component.Player == null)
component.Player = args.Actor;
component.Player = args.User;
else
component.Spectators.Add(args.Actor);
component.Spectators.Add(args.User);
UpdatePlayerStatus(uid, args.Actor, component);
component.Game?.UpdateNewPlayerUI(args.Actor);
UpdatePlayerStatus(uid, args.User, component);
component.Game?.UpdateNewPlayerUI(args.User);
}
private void OnAfterUiClose(EntityUid uid, BlockGameArcadeComponent component, BoundUIClosedEvent args)

View File

@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer.NodeGroups;
@ -14,6 +13,22 @@ namespace Content.Server.Atmos.EntitySystems;
public partial class AtmosphereSystem
{
/*
General API for interacting with AtmosphereSystem.
If you feel like you're stepping on eggshells because you can't access things in AtmosphereSystem,
consider adding a method here instead of making your own way to work around it.
*/
/// <summary>
/// Gets the <see cref="GasMixture"/> that an entity is contained within.
/// </summary>
/// <param name="ent">The entity to get the mixture for.</param>
/// <param name="ignoreExposed">If true, will ignore mixtures that the entity is contained in
/// (ex. lockers and cryopods) and just get the tile mixture.</param>
/// <param name="excite">If true, will mark the tile as active for atmosphere processing.</param>
/// <returns>A <see cref="GasMixture"/> if one could be found, null otherwise.</returns>
[PublicAPI]
public GasMixture? GetContainingMixture(Entity<TransformComponent?> ent, bool ignoreExposed = false, bool excite = false)
{
if (!Resolve(ent, ref ent.Comp))
@ -22,6 +37,17 @@ public partial class AtmosphereSystem
return GetContainingMixture(ent, ent.Comp.GridUid, ent.Comp.MapUid, ignoreExposed, excite);
}
/// <summary>
/// Gets the <see cref="GasMixture"/> that an entity is contained within.
/// </summary>
/// <param name="ent">The entity to get the mixture for.</param>
/// <param name="grid">The grid that the entity may be on.</param>
/// <param name="map">The map that the entity may be on.</param>
/// <param name="ignoreExposed">If true, will ignore mixtures that the entity is contained in
/// (ex. lockers and cryopods) and just get the tile mixture.</param>
/// <param name="excite">If true, will mark the tile as active for atmosphere processing.</param>
/// <returns>A <see cref="GasMixture"/> if one could be found, null otherwise.</returns>
[PublicAPI]
public GasMixture? GetContainingMixture(
Entity<TransformComponent?> ent,
Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid,
@ -49,16 +75,38 @@ public partial class AtmosphereSystem
return GetTileMixture(grid, map, position, excite);
}
public bool HasAtmosphere(EntityUid gridUid) => _atmosQuery.HasComponent(gridUid);
/// <summary>
/// Checks if a grid has an atmosphere.
/// </summary>
/// <param name="gridUid">The grid to check.</param>
/// <returns>True if the grid has an atmosphere, false otherwise.</returns>
[PublicAPI]
public bool HasAtmosphere(EntityUid gridUid)
{
return _atmosQuery.HasComponent(gridUid);
}
/// <summary>
/// Sets whether a grid is simulated by Atmospherics.
/// </summary>
/// <param name="gridUid">The grid to set.</param>
/// <param name="simulated">Whether the grid should be simulated.</param>
/// <returns>>True if the grid's simulated state was changed, false otherwise.</returns>
[PublicAPI]
public bool SetSimulatedGrid(EntityUid gridUid, bool simulated)
{
// TODO ATMOS this event literally has no subscribers. Did this just get silently refactored out?
var ev = new SetSimulatedGridMethodEvent(gridUid, simulated);
RaiseLocalEvent(gridUid, ref ev);
return ev.Handled;
}
/// <summary>
/// Checks whether a grid is simulated by Atmospherics.
/// </summary>
/// <param name="gridUid">The grid to check.</param>
/// <returns>>True if the grid is simulated, false otherwise.</returns>
public bool IsSimulatedGrid(EntityUid gridUid)
{
var ev = new IsSimulatedGridMethodEvent(gridUid);
@ -67,24 +115,53 @@ public partial class AtmosphereSystem
return ev.Simulated;
}
/// <summary>
/// Gets all <see cref="TileAtmosphere"/> <see cref="GasMixture"/>s on a grid.
/// </summary>
/// <param name="gridUid">The grid to get mixtures for.</param>
/// <param name="excite">Whether to mark all tiles as active for atmosphere processing.</param>
/// <returns>An enumerable of all gas mixtures on the grid.</returns>
[PublicAPI]
public IEnumerable<GasMixture> GetAllMixtures(EntityUid gridUid, bool excite = false)
{
var ev = new GetAllMixturesMethodEvent(gridUid, excite);
RaiseLocalEvent(gridUid, ref ev);
if(!ev.Handled)
return Enumerable.Empty<GasMixture>();
if (!ev.Handled)
return [];
DebugTools.AssertNotNull(ev.Mixtures);
return ev.Mixtures!;
}
/// <summary>
/// <para>Invalidates a tile on a grid, marking it for revalidation.</para>
///
/// <para>Frequently used tile data like <see cref="AirtightData"/> are determined once and cached.
/// If this tile's state changes, ex. being added or removed, then this position in the map needs to
/// be updated.</para>
///
/// <para>Tiles that need to be updated are marked as invalid and revalidated before all other
/// processing stages.</para>
/// </summary>
/// <param name="entity">The grid entity.</param>
/// <param name="tile">The tile to invalidate.</param>
[PublicAPI]
public void InvalidateTile(Entity<GridAtmosphereComponent?> entity, Vector2i tile)
{
if (_atmosQuery.Resolve(entity.Owner, ref entity.Comp, false))
entity.Comp.InvalidatedCoords.Add(tile);
}
/// <summary>
/// Gets the gas mixtures for a list of tiles on a grid or map.
/// </summary>
/// <param name="grid">The grid to get mixtures from.</param>
/// <param name="map">The map to get mixtures from.</param>
/// <param name="tiles">The list of tiles to get mixtures for.</param>
/// <param name="excite">Whether to mark the tiles as active for atmosphere processing.</param>
/// <returns>>An array of gas mixtures corresponding to the input tiles.</returns>
[PublicAPI]
public GasMixture?[]? GetTileMixtures(
Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid,
Entity<MapAtmosphereComponent?>? map,
@ -95,7 +172,7 @@ public partial class AtmosphereSystem
var handled = false;
// If we've been passed a grid, try to let it handle it.
if (grid is {} gridEnt && Resolve(gridEnt, ref gridEnt.Comp1))
if (grid is { } gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp1))
{
if (excite)
Resolve(gridEnt, ref gridEnt.Comp2);
@ -128,7 +205,7 @@ public partial class AtmosphereSystem
// We either don't have a grid, or the event wasn't handled.
// Let the map handle it instead, and also broadcast the event.
if (map is {} mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp))
if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp))
{
mixtures ??= new GasMixture?[tiles.Count];
for (var i = 0; i < tiles.Count; i++)
@ -145,10 +222,21 @@ public partial class AtmosphereSystem
{
mixtures[i] ??= GasMixture.SpaceGas;
}
return mixtures;
}
public GasMixture? GetTileMixture (Entity<TransformComponent?> entity, bool excite = false)
/// <summary>
/// Gets the gas mixture for a specific tile that an entity is on.
/// </summary>
/// <param name="entity">The entity to get the tile mixture for.</param>
/// <param name="excite">Whether to mark the tile as active for atmosphere processing.</param>
/// <returns>A <see cref="GasMixture"/> if one could be found, null otherwise.</returns>
/// <remarks>This does not return the <see cref="GasMixture"/> that the entity
/// may be contained in, ex. if the entity is currently in a locker/crate with its own
/// <see cref="GasMixture"/>.</remarks>
[PublicAPI]
public GasMixture? GetTileMixture(Entity<TransformComponent?> entity, bool excite = false)
{
if (!Resolve(entity.Owner, ref entity.Comp))
return null;
@ -157,6 +245,15 @@ public partial class AtmosphereSystem
return GetTileMixture(entity.Comp.GridUid, entity.Comp.MapUid, indices, excite);
}
/// <summary>
/// Gets the gas mixture for a specific tile on a grid or map.
/// </summary>
/// <param name="grid">The grid to get the mixture from.</param>
/// <param name="map">The map to get the mixture from.</param>
/// <param name="gridTile">The tile to get the mixture from.</param>
/// <param name="excite">Whether to mark the tile as active for atmosphere processing.</param>
/// <returns>>A <see cref="GasMixture"/> if one could be found, null otherwise.</returns>
[PublicAPI]
public GasMixture? GetTileMixture(
Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid,
Entity<MapAtmosphereComponent?>? map,
@ -164,8 +261,8 @@ public partial class AtmosphereSystem
bool excite = false)
{
// If we've been passed a grid, try to let it handle it.
if (grid is {} gridEnt
&& Resolve(gridEnt, ref gridEnt.Comp1, false)
if (grid is { } gridEnt
&& _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp1, false)
&& gridEnt.Comp1.Tiles.TryGetValue(gridTile, out var tile))
{
if (excite)
@ -177,13 +274,20 @@ public partial class AtmosphereSystem
return tile.Air;
}
if (map is {} mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false))
if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false))
return mapEnt.Comp.Mixture;
// Default to a space mixture... This is a space game, after all!
return GasMixture.SpaceGas;
}
/// <summary>
/// Triggers a tile's <see cref="GasMixture"/> to react.
/// </summary>
/// <param name="gridId">The grid to react the tile on.</param>
/// <param name="tile">The tile to react.</param>
/// <returns>The result of the reaction.</returns>
[PublicAPI]
public ReactionResult ReactTile(EntityUid gridId, Vector2i tile)
{
var ev = new ReactTileMethodEvent(gridId, tile);
@ -194,24 +298,49 @@ public partial class AtmosphereSystem
return ev.Result;
}
public bool IsTileAirBlocked(EntityUid gridUid, Vector2i tile, AtmosDirection directions = AtmosDirection.All, MapGridComponent? mapGridComp = null)
/// <summary>
/// Checks if a tile on a grid is air-blocked in the specified directions.
/// </summary>
/// <param name="gridUid">The grid to check.</param>
/// <param name="tile">The tile on the grid to check.</param>
/// <param name="directions">The directions to check for air-blockage.</param>
/// <param name="mapGridComp">Optional map grid component associated with the grid.</param>
/// <returns>True if the tile is air-blocked in the specified directions, false otherwise.</returns>
[PublicAPI]
public bool IsTileAirBlocked(EntityUid gridUid,
Vector2i tile,
AtmosDirection directions = AtmosDirection.All,
MapGridComponent? mapGridComp = null)
{
if (!Resolve(gridUid, ref mapGridComp, false))
return false;
// TODO ATMOS: This reconstructs the data instead of getting the cached version. Might want to include a method to get the cached version later.
var data = GetAirtightData(gridUid, mapGridComp, tile);
return data.BlockedDirections.IsFlagSet(directions);
}
/// <summary>
/// Checks if a tile on a grid or map is space as defined by a tile's definition of space.
/// Some tiles can hold back space and others cannot - for example, plating can hold
/// back space, whereas scaffolding cannot, exposing the map atmosphere beneath.
/// </summary>
/// <remarks>This does not check if the <see cref="GasMixture"/> on the tile is space,
/// it only checks the current tile's ability to hold back space.</remarks>
/// <param name="grid">The grid to check.</param>
/// <param name="map">The map to check.</param>
/// <param name="tile">The tile to check.</param>
/// <returns>True if the tile is space, false otherwise.</returns>
[PublicAPI]
public bool IsTileSpace(Entity<GridAtmosphereComponent?>? grid, Entity<MapAtmosphereComponent?>? map, Vector2i tile)
{
if (grid is {} gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp, false)
&& gridEnt.Comp.Tiles.TryGetValue(tile, out var tileAtmos))
if (grid is { } gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp, false)
&& gridEnt.Comp.Tiles.TryGetValue(tile, out var tileAtmos))
{
return tileAtmos.Space;
}
if (map is {} mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false))
if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false))
return mapEnt.Comp.Space;
// If nothing handled the event, it'll default to true.
@ -219,28 +348,77 @@ public partial class AtmosphereSystem
return true;
}
/// <summary>
/// Checks if the gas mixture on a tile is "probably safe".
/// Probably safe is defined as having at least air alarm-grade safe pressure and temperature.
/// (more than 260K, less than 360K, and between safe low and high pressure as defined in
/// <see cref="Atmospherics.WarningLowPressure"/> and <see cref="Atmospherics.WarningHighPressure"/>)
/// </summary>
/// <param name="grid">The grid to check.</param>
/// <param name="map">The map to check.</param>
/// <param name="tile">The tile to check.</param>
/// <returns>True if the tile's mixture is probably safe, false otherwise.</returns>
[PublicAPI]
public bool IsTileMixtureProbablySafe(Entity<GridAtmosphereComponent?>? grid, Entity<MapAtmosphereComponent?> map, Vector2i tile)
{
return IsMixtureProbablySafe(GetTileMixture(grid, map, tile));
}
/// <summary>
/// Gets the heat capacity of the gas mixture on a tile.
/// </summary>
/// <param name="grid">The grid to check.</param>
/// <param name="map">The map to check.</param>
/// <param name="tile">The tile on the grid/map to check.</param>
/// <returns>>The heat capacity of the tile's mixture, or the heat capacity of space if a mixture could not be found.</returns>
[PublicAPI]
public float GetTileHeatCapacity(Entity<GridAtmosphereComponent?>? grid, Entity<MapAtmosphereComponent?> map, Vector2i tile)
{
return GetHeatCapacity(GetTileMixture(grid, map, tile) ?? GasMixture.SpaceGas);
}
/// <summary>
/// Gets an enumerator for the adjacent tile mixtures of a tile on a grid.
/// </summary>
/// <param name="grid">The grid to get adjacent tile mixtures from.</param>
/// <param name="tile">The tile to get adjacent mixtures for.</param>
/// <param name="includeBlocked">Whether to include blocked adjacent tiles.</param>
/// <param name="excite">Whether to mark the adjacent tiles as active for atmosphere processing.</param>
/// <returns>An enumerator for the adjacent tile mixtures.</returns>
[PublicAPI]
public TileMixtureEnumerator GetAdjacentTileMixtures(Entity<GridAtmosphereComponent?> grid, Vector2i tile, bool includeBlocked = false, bool excite = false)
{
// TODO ATMOS includeBlocked and excite parameters are unhandled currently.
if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
return TileMixtureEnumerator.Empty;
return !grid.Comp.Tiles.TryGetValue(tile, out var atmosTile)
? TileMixtureEnumerator.Empty
: new(atmosTile.AdjacentTiles);
: new TileMixtureEnumerator(atmosTile.AdjacentTiles);
}
public void HotspotExpose(Entity<GridAtmosphereComponent?> grid, Vector2i tile, float exposedTemperature, float exposedVolume,
EntityUid? sparkSourceUid = null, bool soh = false)
/// <summary>
/// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met.
/// </summary>
/// <param name="grid">The grid to expose the tile on.</param>
/// <param name="tile">The tile 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>
[PublicAPI]
public void HotspotExpose(Entity<GridAtmosphereComponent?> grid,
Vector2i tile,
float exposedTemperature,
float exposedVolume,
EntityUid? sparkSourceUid = null,
bool soh = false)
{
if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
return;
@ -249,8 +427,26 @@ public partial class AtmosphereSystem
HotspotExpose(grid.Comp, atmosTile, exposedTemperature, exposedVolume, soh, sparkSourceUid);
}
public void HotspotExpose(TileAtmosphere tile, float exposedTemperature, float exposedVolume,
EntityUid? sparkSourceUid = null, bool soh = false)
/// <summary>
/// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met.
/// </summary>
/// <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>
[PublicAPI]
public void HotspotExpose(TileAtmosphere tile,
float exposedTemperature,
float exposedVolume,
EntityUid? sparkSourceUid = null,
bool soh = false)
{
if (!_atmosQuery.TryGetComponent(tile.GridIndex, out var atmos))
return;
@ -259,12 +455,25 @@ public partial class AtmosphereSystem
HotspotExpose(atmos, tile, exposedTemperature, exposedVolume, soh, sparkSourceUid);
}
/// <summary>
/// Extinguishes a hotspot on a tile.
/// </summary>
/// <param name="gridUid">The grid to extinguish the hotspot on.</param>
/// <param name="tile">The tile on the grid to extinguish the hotspot on.</param>
[PublicAPI]
public void HotspotExtinguish(EntityUid gridUid, Vector2i tile)
{
var ev = new HotspotExtinguishMethodEvent(gridUid, tile);
RaiseLocalEvent(gridUid, ref ev);
}
/// <summary>
/// Checks if a hotspot is active on a tile.
/// </summary>
/// <param name="gridUid">The grid to check.</param>
/// <param name="tile">The tile on the grid to check.</param>
/// <returns>True if a hotspot is active on the tile, false otherwise.</returns>
[PublicAPI]
public bool IsHotspotActive(EntityUid gridUid, Vector2i tile)
{
var ev = new IsHotspotActiveMethodEvent(gridUid, tile);
@ -274,11 +483,25 @@ public partial class AtmosphereSystem
return ev.Result;
}
/// <summary>
/// Adds a <see cref="PipeNet"/> to a grid.
/// </summary>
/// <param name="grid">The grid to add the pipe net to.</param>
/// <param name="pipeNet">The pipe net to add.</param>
/// <returns>True if the pipe net was added, false otherwise.</returns>
[PublicAPI]
public bool AddPipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet)
{
return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Add(pipeNet);
}
/// <summary>
/// Removes a <see cref="PipeNet"/> from a grid.
/// </summary>
/// <param name="grid">The grid to remove the pipe net from.</param>
/// <param name="pipeNet">The pipe net to remove.</param>
/// <returns>True if the pipe net was removed, false otherwise.</returns>
[PublicAPI]
public bool RemovePipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet)
{
// Technically this event can be fired even on grids that don't
@ -292,6 +515,13 @@ public partial class AtmosphereSystem
return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Remove(pipeNet);
}
/// <summary>
/// Adds an entity with an <see cref="AtmosDeviceComponent"/> to a grid's list of atmos devices.
/// </summary>
/// <param name="grid">The grid to add the device to.</param>
/// <param name="device">The device to add.</param>
/// <returns>True if the device was added, false otherwise.</returns>
[PublicAPI]
public bool AddAtmosDevice(Entity<GridAtmosphereComponent?> grid, Entity<AtmosDeviceComponent> device)
{
DebugTools.Assert(device.Comp.JoinedGrid == null);
@ -307,6 +537,12 @@ public partial class AtmosphereSystem
return true;
}
/// <summary>
/// Removes an entity with an <see cref="AtmosDeviceComponent"/> from a grid's list of atmos devices.
/// </summary>
/// <param name="grid">The grid to remove the device from.</param>
/// <param name="device">The device to remove.</param>
/// <returns>True if the device was removed, false otherwise.</returns>
public bool RemoveAtmosDevice(Entity<GridAtmosphereComponent?> grid, Entity<AtmosDeviceComponent> device)
{
DebugTools.Assert(device.Comp.JoinedGrid == grid);
@ -418,23 +654,44 @@ public partial class AtmosphereSystem
return contains;
}
[ByRefEvent] private record struct SetSimulatedGridMethodEvent
(EntityUid Grid, bool Simulated, bool Handled = false);
[ByRefEvent]
private record struct SetSimulatedGridMethodEvent(
EntityUid Grid,
bool Simulated,
bool Handled = false);
[ByRefEvent] private record struct IsSimulatedGridMethodEvent
(EntityUid Grid, bool Simulated = false, bool Handled = false);
[ByRefEvent]
private record struct IsSimulatedGridMethodEvent(
EntityUid Grid,
bool Simulated = false,
bool Handled = false);
[ByRefEvent] private record struct GetAllMixturesMethodEvent
(EntityUid Grid, bool Excite = false, IEnumerable<GasMixture>? Mixtures = null, bool Handled = false);
[ByRefEvent]
private record struct GetAllMixturesMethodEvent(
EntityUid Grid,
bool Excite = false,
IEnumerable<GasMixture>? Mixtures = null,
bool Handled = false);
[ByRefEvent] private record struct ReactTileMethodEvent
(EntityUid GridId, Vector2i Tile, ReactionResult Result = default, bool Handled = false);
[ByRefEvent]
private record struct ReactTileMethodEvent(
EntityUid GridId,
Vector2i Tile,
ReactionResult Result = default,
bool Handled = false);
[ByRefEvent] private record struct HotspotExtinguishMethodEvent
(EntityUid Grid, Vector2i Tile, bool Handled = false);
[ByRefEvent]
private record struct HotspotExtinguishMethodEvent(
EntityUid Grid,
Vector2i Tile,
bool Handled = false);
[ByRefEvent] private record struct IsHotspotActiveMethodEvent
(EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false);
[ByRefEvent]
private record struct IsHotspotActiveMethodEvent(
EntityUid Grid,
Vector2i Tile,
bool Result = false,
bool Handled = false);
}

View File

@ -11,9 +11,15 @@ namespace Content.Server.Atmos.EntitySystems;
public partial class AtmosphereSystem
{
/*
Partial class that stores miscellaneous utility methods for Atmospherics.
*/
/// <summary>
/// Gets the particular price of an air mixture.
/// Gets the particular price of a <see cref="GasMixture"/>.
/// </summary>
/// <param name="mixture">The <see cref="GasMixture"/> to get the price of.</param>
/// <returns>The price of the gas mixture.</returns>
public double GetPrice(GasMixture mixture)
{
float basePrice = 0; // moles of gas * price/mole
@ -26,7 +32,7 @@ public partial class AtmosphereSystem
maxComponent = Math.Max(maxComponent, mixture.Moles[i]);
}
// Pay more for gas canisters that are more pure
// Pay more for gas canisters that are purer
float purity = 1;
if (totalMoles > 0)
{
@ -36,12 +42,32 @@ public partial class AtmosphereSystem
return basePrice * purity;
}
/// <summary>
/// <para>Marks a tile's visual overlay as needing to be redetermined.</para>
///
/// <para>A tile's overlay (how it looks like, ex. water vapor's texture)
/// is determined via determining how much gas there is on the tile.
/// This is expensive to do for every tile/gas that may have a custom overlay,
/// so its done once and only updated when it needs to be updated.</para>
/// </summary>
/// <param name="grid">The grid the tile is on.</param>
/// <param name="tile">The tile to invalidate.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InvalidateVisuals(Entity<GasTileOverlayComponent?> grid, Vector2i tile)
{
_gasTileOverlaySystem.Invalidate(grid, tile);
}
/// <summary>
/// <para>Marks a tile's visual overlay as needing to be redetermined.</para>
///
/// <para>A tile's overlay (how it looks like, ex. water vapor's texture)
/// is determined via determining how much gas there is on the tile.
/// This is expensive to do for every tile/gas that may have a custom overlay,
/// so its done once and only updated when it needs to be updated.</para>
/// </summary>
/// <param name="ent">The grid the tile is on.</param>
/// <param name="tile">The tile to invalidate.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void InvalidateVisuals(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
@ -51,7 +77,7 @@ public partial class AtmosphereSystem
}
/// <summary>
/// Gets the volume in liters for a number of tiles, on a specific grid.
/// Gets the volume in liters for a number of tiles, on a specific grid.
/// </summary>
/// <param name="mapGrid">The grid in question.</param>
/// <param name="tiles">The amount of tiles.</param>
@ -79,6 +105,18 @@ public partial class AtmosphereSystem
bool NoAirWhenBlocked,
bool FixVacuum);
/// <summary>
/// Updates the <see cref="AirtightData"/> for a <see cref="TileAtmosphere"/>
/// immediately.
/// </summary>
/// <remarks>This method is extremely important if you are doing something in Atmospherics
/// that is time-sensitive! <see cref="AirtightData"/> is cached and invalidated on
/// a cycle, so airtight changes performed during or after an invalidation will
/// not take effect until the next Atmospherics tick!</remarks>
/// <param name="uid">The entity the grid is on.</param>
/// <param name="atmos">The <see cref="GridAtmosphereComponent"/> the tile is on.</param>
/// <param name="grid">The <see cref="MapGridComponent"/> the tile is on.</param>
/// <param name="tile">The <see cref="TileAtmosphere"/> to update.</param>
private void UpdateAirtightData(EntityUid uid, GridAtmosphereComponent atmos, MapGridComponent grid, TileAtmosphere tile)
{
var oldBlocked = tile.AirtightData.BlockedDirections;
@ -91,6 +129,15 @@ public partial class AtmosphereSystem
ExcitedGroupDispose(atmos, tile.ExcitedGroup);
}
/// <summary>
/// Retrieves current <see cref="AirtightData"/> for a tile on a grid.
/// This is determined on-the-fly, not from cached data, so it will reflect
/// changes done in the current Atmospherics tick.
/// </summary>
/// <param name="uid">The entity the grid is on.</param>
/// <param name="grid">The <see cref="MapGridComponent"/> the tile is on.</param>
/// <param name="tile">The indices of the tile.</param>
/// <returns>The current <see cref="AirtightData"/> for the tile.</returns>
private AirtightData GetAirtightData(EntityUid uid, MapGridComponent grid, Vector2i tile)
{
var blockedDirs = AtmosDirection.Invalid;
@ -118,7 +165,7 @@ public partial class AtmosphereSystem
}
/// <summary>
/// Pries a tile in a grid.
/// Pries a tile in a grid.
/// </summary>
/// <param name="mapGrid">The grid in question.</param>
/// <param name="tile">The indices of the tile.</param>

View File

@ -225,20 +225,14 @@ namespace Content.Server.Atmos.EntitySystems
mass2 = otherPhys.Mass;
}
// when the thing on fire is more massive than the other, the following happens:
// - the thing on fire loses a small number of firestacks
// - the other thing gains a large number of firestacks
// so a person on fire engulfs a mouse, but an engulfed mouse barely does anything to a person
var total = mass1 + mass2;
var avg = (flammable.FireStacks + otherFlammable.FireStacks) / total;
// Get the average of both entity's firestacks * mass
// Then for each entity, we divide the average by their mass and set their firestacks to that value
// An entity with a higher mass will lose some fire and transfer it to the one with lower mass.
var avg = (flammable.FireStacks * mass1 + otherFlammable.FireStacks * mass2) / 2f;
// swap the entity losing stacks depending on whichever has the most firestack kilos
var (src, dest) = flammable.FireStacks * mass1 > otherFlammable.FireStacks * mass2
? (-1f, 1f)
: (1f, -1f);
// bring each entity to the same firestack mass, firestacks being scaled by the other's mass
AdjustFireStacks(uid, src * avg * mass2, flammable, ignite: true);
AdjustFireStacks(otherUid, dest * avg * mass1, otherFlammable, ignite: true);
// bring each entity to the same firestack mass, firestack amount is scaled by the inverse of the entity's mass
SetFireStacks(uid, avg / mass1, flammable, ignite: true);
SetFireStacks(otherUid, avg / mass2, otherFlammable, ignite: true);
}
private void OnIsHot(EntityUid uid, FlammableComponent flammable, IsHotEvent args)

View File

@ -157,18 +157,18 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
private void OnSelectGasMessage(EntityUid uid, GasFilterComponent filter, GasFilterSelectGasMessage args)
{
if (args.ID.HasValue)
if (args.Gas.HasValue)
{
if (Enum.TryParse<Gas>(args.ID.ToString(), true, out var parsedGas))
if (Enum.IsDefined(typeof(Gas), args.Gas))
{
filter.FilteredGas = parsedGas;
filter.FilteredGas = args.Gas;
_adminLogger.Add(LogType.AtmosFilterChanged, LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} set the filter on {ToPrettyString(uid):device} to {parsedGas.ToString()}");
$"{ToPrettyString(args.Actor):player} set the filter on {ToPrettyString(uid):device} to {args.Gas.ToString()}");
DirtyUI(uid, filter);
}
else
{
Log.Warning($"{ToPrettyString(uid)} received GasFilterSelectGasMessage with an invalid ID: {args.ID}");
Log.Warning($"{ToPrettyString(uid)} received GasFilterSelectGasMessage with an invalid ID: {args.Gas}");
}
}
else

View File

@ -54,11 +54,13 @@ public sealed class SpaceHeaterSystem : EntitySystem
private void OnUIActivationAttempt(EntityUid uid, SpaceHeaterComponent spaceHeater, ActivatableUIOpenAttemptEvent args)
{
if (!Comp<TransformComponent>(uid).Anchored)
{
if (Comp<TransformComponent>(uid).Anchored)
return;
if (!args.Silent)
_popup.PopupEntity(Loc.GetString("comp-space-heater-unanchored", ("device", Loc.GetString("comp-space-heater-device-name"))), uid, args.User);
args.Cancel();
}
args.Cancel();
}
private void OnDeviceUpdated(EntityUid uid, SpaceHeaterComponent spaceHeater, ref AtmosDeviceUpdateEvent args)

View File

@ -37,7 +37,10 @@ public sealed class BloodstreamSystem : SharedBloodstreamSystem
// Fill blood solution with BLOOD
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
var solution = entity.Comp.BloodReagents.Clone();
solution.ScaleTo(entity.Comp.BloodMaxVolume - bloodSolution.Volume);
solution.SetReagentData(GetEntityBloodData(entity.Owner));
bloodSolution.AddSolution(solution, PrototypeManager);
}
// forensics is not predicted yet

View File

@ -299,6 +299,12 @@ public sealed partial class ChatSystem : SharedChatSystem
if (!_critLoocEnabled && _mobStateSystem.IsCritical(source))
return;
// Systems can differentiate Looc and DeadChat by type, and cancel the speak attempt if necessary.
var ev = new InGameOocMessageAttemptEvent(player, sendType);
RaiseLocalEvent(source, ref ev, true);
if (ev.Cancelled)
return;
switch (sendType)
{
case InGameOOCChatType.Dead:
@ -448,18 +454,18 @@ public sealed partial class ChatSystem : SharedChatSystem
if (originalMessage == message)
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user} as {name}: {originalMessage}.");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source} as {name}: {originalMessage}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {originalMessage}.");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source}: {originalMessage}.");
}
else
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Say from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
$"Say from {source} as {name}, original: {originalMessage}, transformed: {message}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Say from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
$"Say from {source}, original: {originalMessage}, transformed: {message}.");
}
}
@ -537,18 +543,18 @@ public sealed partial class ChatSystem : SharedChatSystem
if (originalMessage == message)
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user} as {name}: {originalMessage}.");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source} as {name}: {originalMessage}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}.");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source}: {originalMessage}.");
}
else
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Whisper from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
$"Whisper from {source} as {name}, original: {originalMessage}, transformed: {message}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
$"Whisper from {source}, original: {originalMessage}, transformed: {message}.");
}
}
@ -649,9 +655,9 @@ public sealed partial class ChatSystem : SharedChatSystem
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
if (!hideLog)
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
}
// ReSharper disable once InconsistentNaming
@ -674,7 +680,7 @@ public sealed partial class ChatSystem : SharedChatSystem
("message", FormattedMessage.EscapeText(message)));
SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {source}: {message}");
}
private void SendDeadChat(EntityUid source, ICommonSession player, string message, bool hideChat)
@ -688,7 +694,7 @@ public sealed partial class ChatSystem : SharedChatSystem
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
("userName", player.Channel.UserName),
("message", FormattedMessage.EscapeText(message)));
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {player:Player}: {message}");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {source}: {message}");
}
else
{
@ -696,7 +702,7 @@ public sealed partial class ChatSystem : SharedChatSystem
("deadChannelName", Loc.GetString("chat-manager-dead-channel-name")),
("playerName", (playerName)),
("message", FormattedMessage.EscapeText(message)));
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {player:Player}: {message}");
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {source}: {message}");
}
_chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList(), author: player.UserId);

View File

@ -1,7 +1,5 @@
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Fluids.Components;
using JetBrains.Annotations;
namespace Content.Server.Destructible.Thresholds.Behaviors;
@ -27,34 +25,14 @@ public sealed partial class SpillBehavior : IThresholdBehavior
/// <param name="cause">Optional entity that caused this behavior to trigger</param>
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
var solutionContainerSystem = system.EntityManager.System<SharedSolutionContainerSystem>();
var spillableSystem = system.EntityManager.System<PuddleSystem>();
var puddleSystem = system.EntityManager.System<PuddleSystem>();
var solutionContainer = system.EntityManager.System<SharedSolutionContainerSystem>();
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
Solution targetSolution;
// First try to get solution from SpillableComponent
if (system.EntityManager.TryGetComponent(owner, out SpillableComponent? spillableComponent) &&
solutionContainerSystem.TryGetSolution(owner, spillableComponent.SolutionName, out var solution, out var compSolution))
{
// If entity is drainable, drain the solution. Otherwise just split it.
// Both methods ensure the solution is properly removed.
targetSolution = system.EntityManager.HasComponent<DrainableSolutionComponent>(owner)
? solutionContainerSystem.Drain((owner, system.EntityManager.GetComponent<DrainableSolutionComponent>(owner)), solution.Value, compSolution.Volume)
: compSolution.SplitSolution(compSolution.Volume);
}
// Fallback to solution specified in behavior data
else if (Solution != null &&
solutionContainerSystem.TryGetSolution(owner, Solution, out var solutionEnt, out var behaviorSolution))
{
targetSolution = system.EntityManager.HasComponent<DrainableSolutionComponent>(owner)
? solutionContainerSystem.Drain((owner, system.EntityManager.GetComponent<DrainableSolutionComponent>(owner)), solutionEnt.Value, behaviorSolution.Volume)
: behaviorSolution.SplitSolution(behaviorSolution.Volume);
}
else
return;
// Spill the solution that was drained/split
spillableSystem.TrySplashSpillAt(owner, coordinates, targetSolution, out _, false, cause);
if (solutionContainer.TryGetSolution(owner, Solution, out _, out var solution))
puddleSystem.TrySplashSpillAt(owner, coordinates, solution, out _, false, cause);
else
puddleSystem.TrySplashSpillAt(owner, coordinates, out _, out _, false, cause);
}
}

View File

@ -839,11 +839,5 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
UpdateListUiState(conf, conf.Comp);
}
private void OnUiOpenAttempt(EntityUid uid, NetworkConfiguratorComponent configurator, ActivatableUIOpenAttemptEvent args)
{
if (configurator.LinkModeActive)
args.Cancel();
}
#endregion
}

View File

@ -32,16 +32,10 @@ public sealed partial class PuddleSystem
private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args)
{
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
if (!entity.Comp.SpillWhenThrown || Openable.IsClosed(entity.Owner))
return;
if (Openable.IsClosed(entity.Owner))
return;
if (!entity.Comp.SpillWhenThrown)
return;
if (args.User != null)
if (TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, out _, out var solution) && args.User != null)
{
// DeltaV - Beer Goggles Safe Throw
if (_safeSolutionThrower.GetSafeThrow(args.User.Value))
@ -55,9 +49,6 @@ public sealed partial class PuddleSystem
AdminLogger.Add(LogType.Landed,
$"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing");
}
var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume);
TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, drainedSolution, out _);
}
private void OnDoAfter(Entity<SpillableComponent> entity, ref SpillDoAfterEvent args)

View File

@ -369,16 +369,37 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged
/// <inheritdoc/>
public override bool TrySplashSpillAt(EntityUid uid,
public override bool TrySplashSpillAt(Entity<SpillableComponent?> entity,
EntityCoordinates coordinates,
Solution solution,
out EntityUid puddleUid,
out Solution spilled,
bool sound = true,
EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
spilled = new Solution();
if (!Resolve(entity, ref entity.Comp))
return false;
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var solution))
return false;
spilled = solution.Value.Comp.Solution;
return TrySplashSpillAt(entity, coordinates, spilled, out puddleUid, sound, user);
}
public override bool TrySplashSpillAt(EntityUid entity,
EntityCoordinates coordinates,
Solution spilled,
out EntityUid puddleUid,
bool sound = true,
EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
if (solution.Volume == 0)
if (spilled.Volume == 0)
return false;
var targets = new List<EntityUid>();
@ -392,26 +413,28 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
var owner = ent.Owner;
// between 5 and 30%
var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f);
var splitSolution = solution.SplitSolution(splitAmount);
var splitAmount = spilled.Volume * _random.NextFloat(0.05f, 0.30f);
var splitSolution = spilled.SplitSolution(splitAmount);
if (user != null)
{
AdminLogger.Add(LogType.Landed,
$"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
$"{ToPrettyString(user.Value):user} threw {ToPrettyString(entity):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(spilled):solution} onto {ToPrettyString(owner):target}");
}
targets.Add(owner);
Reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
Popups.PopupEntity(
Loc.GetString("spill-land-spilled-on-other", ("spillable", uid),
("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution);
Popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other",
("spillable", entity),
("target", Identity.Entity(owner, EntityManager))),
owner,
PopupType.SmallCaution);
}
_color.RaiseEffect(solution.GetColor(_prototypeManager), targets,
Filter.Pvs(uid, entityManager: EntityManager));
_color.RaiseEffect(spilled.GetColor(_prototypeManager), targets,
Filter.Pvs(entity, entityManager: EntityManager));
return TrySpillAt(coordinates, solution, out puddleUid, sound);
return TrySpillAt(coordinates, spilled, out puddleUid, sound);
}
/// <inheritdoc/>

View File

@ -1,6 +1,5 @@
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Server.Gravity;
using Content.Server.Popups;
using Content.Shared.CCVar;
@ -16,11 +15,14 @@ using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using System.Numerics;
using Content.Shared.Fluids.EntitySystems;
using Content.Shared.Fluids.Components;
using Robust.Server.Containers;
using Robust.Shared.Map;
namespace Content.Server.Fluids.EntitySystems;
public sealed class SpraySystem : EntitySystem
public sealed class SpraySystem : SharedSpraySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly GravitySystem _gravity = default!;
@ -33,6 +35,7 @@ public sealed class SpraySystem : EntitySystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ContainerSystem _container = default!;
private float _gridImpulseMultiplier;
@ -54,7 +57,7 @@ public sealed class SpraySystem : EntitySystem
var targetMapPos = _transform.GetMapCoordinates(GetEntityQuery<TransformComponent>().GetComponent(args.Target));
Spray(entity, args.User, targetMapPos);
Spray(entity, targetMapPos, args.User);
}
private void UpdateGridMassMultiplier(float value)
@ -71,10 +74,19 @@ public sealed class SpraySystem : EntitySystem
var clickPos = _transform.ToMapCoordinates(args.ClickLocation);
Spray(entity, args.User, clickPos);
Spray(entity, clickPos, args.User);
}
public void Spray(Entity<SprayComponent> entity, EntityUid user, MapCoordinates mapcoord)
public override void Spray(Entity<SprayComponent> entity, EntityUid? user = null)
{
var xform = Transform(entity);
var throwing = xform.LocalRotation.ToWorldVec() * entity.Comp.SprayDistance;
var direction = xform.Coordinates.Offset(throwing);
Spray(entity, _transform.ToMapCoordinates(direction), user);
}
public override void Spray(Entity<SprayComponent> entity, MapCoordinates mapcoord, EntityUid? user = null)
{
if (!_solutionContainer.TryGetSolution(entity.Owner, SprayComponent.SolutionName, out var soln, out var solution))
return;
@ -82,25 +94,29 @@ public sealed class SpraySystem : EntitySystem
var ev = new SprayAttemptEvent(user);
RaiseLocalEvent(entity, ref ev);
if (ev.Cancelled)
{
if (ev.CancelPopupMessage != null && user != null)
_popupSystem.PopupEntity(Loc.GetString(ev.CancelPopupMessage), entity.Owner, user.Value);
return;
}
if (TryComp<UseDelayComponent>(entity, out var useDelay)
&& _useDelay.IsDelayed((entity, useDelay)))
if (_useDelay.IsDelayed((entity, null)))
return;
if (solution.Volume <= 0)
{
_popupSystem.PopupEntity(Loc.GetString("spray-component-is-empty-message"), entity.Owner, user);
if (user != null)
_popupSystem.PopupEntity(Loc.GetString(entity.Comp.SprayEmptyPopupMessage, ("entity", entity)), entity.Owner, user.Value);
return;
}
var xformQuery = GetEntityQuery<TransformComponent>();
var userXform = xformQuery.GetComponent(user);
var sprayerXform = xformQuery.GetComponent(entity);
var userMapPos = _transform.GetMapCoordinates(userXform);
var sprayerMapPos = _transform.GetMapCoordinates(sprayerXform);
var clickMapPos = mapcoord;
var diffPos = clickMapPos.Position - userMapPos.Position;
var diffPos = clickMapPos.Position - sprayerMapPos.Position;
if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN)
return;
@ -127,12 +143,12 @@ public sealed class SpraySystem : EntitySystem
Angle.FromDegrees(spread * (amount - 1) / 2));
// Calculate the destination for the vapor cloud. Limit to the maximum spray distance.
var target = userMapPos
var target = sprayerMapPos
.Offset((diffNorm + rotation.ToVec()).Normalized() * diffLength + quarter);
var distance = (target.Position - userMapPos.Position).Length();
var distance = (target.Position - sprayerMapPos.Position).Length();
if (distance > entity.Comp.SprayDistance)
target = userMapPos.Offset(diffNorm * entity.Comp.SprayDistance);
target = sprayerMapPos.Offset(diffNorm * entity.Comp.SprayDistance);
var adjustedSolutionAmount = entity.Comp.TransferAmount / entity.Comp.VaporAmount;
var newSolution = _solutionContainer.SplitSolution(soln.Value, adjustedSolutionAmount);
@ -141,7 +157,7 @@ public sealed class SpraySystem : EntitySystem
break;
// Spawn the vapor cloud onto the grid/map the user is present on. Offset the start position based on how far the target destination is.
var vaporPos = userMapPos.Offset(distance < 1 ? quarter : threeQuarters);
var vaporPos = sprayerMapPos.Offset(distance < 1 ? quarter : threeQuarters);
var vapor = Spawn(entity.Comp.SprayedPrototype, vaporPos);
var vaporXform = xformQuery.GetComponent(vapor);
@ -164,17 +180,21 @@ public sealed class SpraySystem : EntitySystem
_vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, user);
if (TryComp<PhysicsComponent>(user, out var body))
var thingGettingPushed = entity.Owner;
if (_container.TryGetOuterContainer(entity, sprayerXform, out var container))
thingGettingPushed = container.Owner;
if (TryComp<PhysicsComponent>(thingGettingPushed, out var body))
{
if (_gravity.IsWeightless(user))
if (_gravity.IsWeightless(thingGettingPushed))
{
// push back the player
_physics.ApplyLinearImpulse(user, -impulseDirection * entity.Comp.PushbackAmount, body: body);
_physics.ApplyLinearImpulse(thingGettingPushed, -impulseDirection * entity.Comp.PushbackAmount, body: body);
}
else
{
// push back the grid the player is standing on
var userTransform = Transform(user);
var userTransform = Transform(thingGettingPushed);
if (userTransform.GridUid == userTransform.ParentUid)
{
// apply both linear and angular momentum depending on the player position
@ -187,7 +207,6 @@ public sealed class SpraySystem : EntitySystem
_audio.PlayPvs(entity.Comp.SpraySound, entity, entity.Comp.SpraySound.Params.WithVariation(0.125f));
if (useDelay != null)
_useDelay.TryResetDelay((entity, useDelay));
_useDelay.TryResetDelay(entity);
}
}

View File

@ -35,6 +35,7 @@ public sealed partial class GameTicker
private bool StartPreset(ICommonSession[] origReadyPlayers, bool force)
{
_sawmill.Info($"Attempting to start preset '{CurrentPreset?.ID}'");
var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force);
RaiseLocalEvent(startAttempt);
@ -56,10 +57,13 @@ public sealed partial class GameTicker
var fallbackPresets = _cfg.GetCVar(CCVars.GameLobbyFallbackPreset).Split(",");
var startFailed = true;
_sawmill.Info($"Fallback - Failed to start round, attempting to start fallback presets.");
foreach (var preset in fallbackPresets)
{
_sawmill.Info($"Fallback - Clearing up gamerules");
ClearGameRules();
SetGamePreset(preset);
_sawmill.Info($"Fallback - Attempting to start '{preset}'");
SetGamePreset(preset, resetDelay: 1);
AddGamePresetRules();
StartGamePresetRules();
@ -76,6 +80,7 @@ public sealed partial class GameTicker
startFailed = false;
break;
}
_sawmill.Info($"Fallback - '{preset}' failed to start.");
}
if (startFailed)
@ -87,6 +92,7 @@ public sealed partial class GameTicker
else
{
_sawmill.Info($"Fallback - Failed to start preset but fallbacks are disabled. Returning to Lobby.");
FailedPresetRestart();
return false;
}
@ -129,11 +135,11 @@ public sealed partial class GameTicker
}
}
public void SetGamePreset(string preset, bool force = false)
public void SetGamePreset(string preset, bool force = false, int? resetDelay = null)
{
var proto = FindGamePreset(preset);
if (proto != null)
SetGamePreset(proto, force);
SetGamePreset(proto, force, null, resetDelay);
}
public GamePresetPrototype? FindGamePreset(string preset)

View File

@ -254,36 +254,11 @@ namespace Content.Server.GameTicking
return;
}
PlayerJoinGame(player, silent);
var data = player.ContentData();
DebugTools.AssertNotNull(data);
var newMind = _mind.CreateMind(data!.UserId, character.Name);
_mind.SetUserId(newMind, data.UserId);
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
_playTimeTrackings.PlayerRolesChanged(player);
// Delta-V: Add AlwaysUseSpawner.
var spawnPointType = SpawnPointType.Unset;
if (jobPrototype.AlwaysUseSpawner)
{
// Begin DeltaV Additions - Override latejoin
DoSpawn(player, character, station, jobId, silent, out var mob, out var jobPrototype, out var jobName, out var clearLatejoin);
if (clearLatejoin)
lateJoin = false;
spawnPointType = SpawnPointType.Job;
}
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character, spawnPointType: spawnPointType); // DeltaV: pass in spawn point type
DebugTools.AssertNotNull(mobMaybe);
var mob = mobMaybe!.Value;
_mind.TransferTo(newMind, mob);
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
_admin.UpdatePlayerList(player);
// End DeltaV Additions - Override latejoin
if (lateJoin && !silent)
{
@ -356,6 +331,54 @@ namespace Content.Server.GameTicking
RaiseLocalEvent(mob, aev, true);
}
/// <summary>
/// Creates a mob on the specified station, creates the new mind, equips job-specific starting gear and loadout
/// </summary>
public void DoSpawn(
ICommonSession player,
HumanoidCharacterProfile character,
EntityUid station,
string jobId,
bool silent,
out EntityUid mob,
out JobPrototype jobPrototype,
out string jobName,
out bool clearLatejoin)
{
PlayerJoinGame(player, silent);
var data = player.ContentData();
DebugTools.AssertNotNull(data);
var newMind = _mind.CreateMind(data!.UserId, character.Name);
_mind.SetUserId(newMind, data.UserId);
jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
_playTimeTrackings.PlayerRolesChanged(player);
// Delta-V: Add AlwaysUseSpawner.
var spawnPointType = SpawnPointType.Unset;
if (jobPrototype.AlwaysUseSpawner)
{
clearLatejoin = true;
spawnPointType = SpawnPointType.Job;
}
else
clearLatejoin = false;
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character, spawnPointType: spawnPointType); // DeltaV: pass in spawn point type
DebugTools.AssertNotNull(mobMaybe);
mob = mobMaybe!.Value;
_mind.TransferTo(newMind, mob);
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId);
jobName = _jobs.MindTryGetJobName(newMind);
_admin.UpdatePlayerList(player);
}
public void Respawn(ICommonSession player)
{
_mind.WipeMind(player);

View File

@ -0,0 +1,39 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(XenoborgsRuleSystem))]
[AutoGenerateComponentPause]
public sealed partial class XenoborgsRuleComponent : Component
{
/// <summary>
/// When the round will next check for round end.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan? NextRoundEndCheck;
/// <summary>
/// The amount of time between each check for the end of the round.
/// </summary>
[DataField]
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(15);
/// <summary>
/// After this amount of the crew become xenoborgs, the shuttle will be automatically called.
/// </summary>
[DataField]
public float XenoborgShuttleCallPercentage = 0.7f;
/// <summary>
/// The most xenoborgs that existed at one point.
/// </summary>
[DataField]
public int MaxNumberXenoborgs = 0;
/// <summary>
/// If the announcment of the death of the mothership core was sent
/// </summary>
[DataField]
public bool MothershipCoreDeathAnnouncmentSent = false;
}

View File

@ -129,5 +129,6 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
}
args.AddLine(Loc.GetString("point-scoreboard-header"));
args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
args.AddLine("");
}
}

View File

@ -38,6 +38,8 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
while (query.MoveNext(out var uid, out _, out var gameRule))
{
var minPlayers = gameRule.MinPlayers;
var name = ToPrettyString(uid);
if (args.Players.Length >= minPlayers)
continue;
@ -46,8 +48,10 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
("readyPlayersCount", args.Players.Length),
("minimumPlayers", minPlayers),
("presetName", ToPrettyString(uid))));
("presetName", name)));
args.Cancel();
//TODO remove this once announcements are logged
Log.Info($"Rule '{name}' requires {minPlayers} players, but only {args.Players.Length} are ready.");
}
else
{

View File

@ -116,6 +116,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName)));
}
args.AddLine("");
}
private void OnNukeExploded(NukeExplodedEvent ev)

View File

@ -118,6 +118,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
// TODO: someone suggested listing all alive? revs maybe implement at some point
}
args.AddLine("");
}
private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args)

View File

@ -65,6 +65,12 @@ public sealed class RuleGridsSystem : GameRuleSystem<RuleGridsComponent>
if (_whitelist.IsWhitelistFail(ent.Comp.SpawnerWhitelist, uid))
continue;
if (TryComp<GridSpawnPointWhitelistComponent>(uid, out var gridSpawnPointWhitelistComponent))
{
if (!_whitelist.CheckBoth(args.Entity, gridSpawnPointWhitelistComponent.Blacklist, gridSpawnPointWhitelistComponent.Whitelist))
continue;
}
args.Coordinates.Add(_transform.GetMapCoordinates(xform));
}
}

View File

@ -106,6 +106,7 @@ public sealed class SurvivorRuleSystem : GameRuleSystem<SurvivorRuleComponent>
args.AddLine(Loc.GetString("survivor-round-end-dead-count", ("deadCount", deadSurvivors)));
args.AddLine(Loc.GetString("survivor-round-end-alive-count", ("aliveCount", aliveMarooned)));
args.AddLine(Loc.GetString("survivor-round-end-alive-on-shuttle-count", ("aliveCount", aliveOnShuttle)));
args.AddLine("");
// Player manifest at EOR shows who's a survivor so no need for extra info here.
}

View File

@ -25,7 +25,7 @@ public sealed class CutWireVariationPassSystem : VariationPassSystem<CutWireVari
continue;
// Check against blacklist
if (_whitelistSystem.IsBlacklistPass(ent.Comp.Blacklist, uid))
if (_whitelistSystem.IsWhitelistPass(ent.Comp.Blacklist, uid))
continue;
if (Random.Prob(ent.Comp.WireCutChance))

View File

@ -0,0 +1,175 @@
using Content.Server.Antag;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.RoundEnd;
using Content.Server.Station.Systems;
using Content.Shared.Destructible;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Xenoborgs.Components;
using Robust.Shared.Timing;
namespace Content.Server.GameTicking.Rules;
public sealed class XenoborgsRuleSystem : GameRuleSystem<XenoborgsRuleComponent>
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private static readonly Color AnnouncmentColor = Color.Gold;
public void SendXenoborgDeathAnnouncement(Entity<XenoborgsRuleComponent> ent, bool mothershipCoreAlive)
{
if (ent.Comp.MothershipCoreDeathAnnouncmentSent)
return;
var status = mothershipCoreAlive ? "alive" : "dead";
_chatSystem.DispatchGlobalAnnouncement(
Loc.GetString($"xenoborgs-no-more-threat-mothership-core-{status}-announcement"),
colorOverride: AnnouncmentColor);
}
public void SendMothershipDeathAnnouncement(Entity<XenoborgsRuleComponent> ent)
{
_chatSystem.DispatchGlobalAnnouncement(
Loc.GetString("mothership-destroyed-announcement"),
colorOverride: AnnouncmentColor);
ent.Comp.MothershipCoreDeathAnnouncmentSent = true;
}
// TODO: Refactor the end of round text
protected override void AppendRoundEndText(EntityUid uid,
XenoborgsRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
base.AppendRoundEndText(uid, component, gameRule, ref args);
var numXenoborgs = GetNumberXenoborgs();
var numHumans = _mindSystem.GetAliveHumans().Count;
if (numXenoborgs < 5)
args.AddLine(Loc.GetString("xenoborgs-crewmajor"));
else if (4 * numXenoborgs < numHumans)
args.AddLine(Loc.GetString("xenoborgs-crewmajor"));
else if (2 * numXenoborgs < numHumans)
args.AddLine(Loc.GetString("xenoborgs-crewminor"));
else if (1.5 * numXenoborgs < numHumans)
args.AddLine(Loc.GetString("xenoborgs-neutral"));
else if (numXenoborgs < numHumans)
args.AddLine(Loc.GetString("xenoborgs-borgsminor"));
else
args.AddLine(Loc.GetString("xenoborgs-borgsmajor"));
var numMothershipCores = GetNumberMothershipCores();
if (numMothershipCores == 0)
args.AddLine(Loc.GetString("xenoborgs-cond-all-xenoborgs-dead-core-dead"));
else if (numXenoborgs == 0)
args.AddLine(Loc.GetString("xenoborgs-cond-all-xenoborgs-dead-core-alive"));
else
{
args.AddLine(Loc.GetString("xenoborg-number-xenoborg-alive-end", ("count", numXenoborgs)));
args.AddLine(Loc.GetString("xenoborg-number-crew-alive-end", ("count", numHumans)));
}
args.AddLine(Loc.GetString("xenoborg-max-number", ("count", component.MaxNumberXenoborgs)));
args.AddLine(Loc.GetString("xenoborgs-list-start"));
var antags = _antag.GetAntagIdentifiers(uid);
foreach (var (_, sessionData, name) in antags)
{
args.AddLine(Loc.GetString("xenoborgs-list", ("name", name), ("user", sessionData.UserName)));
}
args.AddLine("");
}
private void CheckRoundEnd(XenoborgsRuleComponent xenoborgsRuleComponent)
{
var numXenoborgs = GetNumberXenoborgs();
var numHumans = _mindSystem.GetAliveHumans().Count;
xenoborgsRuleComponent.MaxNumberXenoborgs = Math.Max(xenoborgsRuleComponent.MaxNumberXenoborgs, numXenoborgs);
if ((float)numXenoborgs / (numHumans + numXenoborgs) > xenoborgsRuleComponent.XenoborgShuttleCallPercentage)
{
foreach (var station in _station.GetStations())
{
_chatSystem.DispatchStationAnnouncement(station, Loc.GetString("xenoborg-shuttle-call"), colorOverride: Color.BlueViolet);
}
_roundEnd.RequestRoundEnd(null, false);
}
}
protected override void Started(EntityUid uid, XenoborgsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
}
protected override void ActiveTick(EntityUid uid, XenoborgsRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime)
return;
CheckRoundEnd(component);
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
}
/// <summary>
/// Get the number of xenoborgs
/// </summary>
/// <param name="playerControlled">if it should only include xenoborgs with a mind</param>
/// <param name="alive">if it should only include xenoborgs that are alive</param>
/// <returns>the number of xenoborgs</returns>
private int GetNumberXenoborgs(bool playerControlled = true, bool alive = true)
{
var numberXenoborgs = 0;
var query = EntityQueryEnumerator<XenoborgComponent>();
while (query.MoveNext(out var xenoborg, out _))
{
if (HasComp<MothershipCoreComponent>(xenoborg))
continue;
if (playerControlled && !_mindSystem.TryGetMind(xenoborg, out _, out _))
continue;
if (alive && !_mobState.IsAlive(xenoborg))
continue;
numberXenoborgs++;
}
return numberXenoborgs;
}
/// <summary>
/// Gets the number of xenoborg cores
/// </summary>
/// <returns>the number of xenoborg cores</returns>
private int GetNumberMothershipCores()
{
var numberMothershipCores = 0;
var mothershipCoreQuery = EntityQueryEnumerator<MothershipCoreComponent>();
while (mothershipCoreQuery.MoveNext(out _, out _))
{
numberMothershipCores++;
}
return numberMothershipCores;
}
}

View File

@ -106,6 +106,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
("name", meta.EntityName),
("username", username)));
}
args.AddLine("");
}
/// <summary>

View File

@ -342,7 +342,8 @@ namespace Content.Server.Ghost
if (_followerSystem.GetMostGhostFollowed() is not {} target)
return;
WarpTo(uid, target);
// If there is a ghostnado happening you almost definitely wanna join it, so we automatically follow instead of just warping.
_followerSystem.StartFollowingEntity(uid, target);
}
private void WarpTo(EntityUid uid, EntityUid target)

View File

@ -1,8 +1,7 @@
using Content.Shared.Examine;
using Content.Shared.Coordinates.Helpers;
using Content.Server.PowerCell;
using Content.Shared.PowerCell;
using Content.Shared.Interaction;
using Content.Shared.Power.Components;
using Content.Shared.Storage;
namespace Content.Server.Holosign;
@ -23,9 +22,8 @@ public sealed class HolosignSystem : EntitySystem
{
// TODO: This should probably be using an itemstatus
// TODO: I'm too lazy to do this rn but it's literally copy-paste from emag.
_powerCell.TryGetBatteryFromSlot(uid, out var battery);
var charges = UsesRemaining(component, battery);
var maxCharges = MaxUses(component, battery);
var charges = _powerCell.GetRemainingUses(uid, component.ChargeUse);
var maxCharges = _powerCell.GetMaxUses(uid, component.ChargeUse);
using (args.PushGroup(nameof(HolosignProjectorComponent)))
{
@ -52,25 +50,10 @@ public sealed class HolosignSystem : EntitySystem
// overlapping of the same holo on one tile remains allowed to allow holofan refreshes
var holoUid = Spawn(component.SignProto, args.ClickLocation.SnapToGrid(EntityManager));
var xform = Transform(holoUid);
// TODO: Just make the prototype anchored
if (!xform.Anchored)
_transform.AnchorEntity(holoUid, xform); // anchor to prevent any tempering with (don't know what could even interact with it)
args.Handled = true;
}
private int UsesRemaining(HolosignProjectorComponent component, BatteryComponent? battery = null)
{
if (battery == null ||
component.ChargeUse == 0f) return 0;
return (int) (battery.CurrentCharge / component.ChargeUse);
}
private int MaxUses(HolosignProjectorComponent component, BatteryComponent? battery = null)
{
if (battery == null ||
component.ChargeUse == 0f) return 0;
return (int) (battery.MaxCharge / component.ChargeUse);
}
}

View File

@ -443,7 +443,7 @@ namespace Content.Server.Kitchen.EntitySystems
private void OnAnchorChanged(EntityUid uid, MicrowaveComponent component, ref AnchorStateChangedEvent args)
{
// DeltaV - start of microwave ejection bugfix
if (!args.Anchored)
if (!args.Anchored)
{
// DeltaV's MicrowaveEventsSystem changes prevent ejection from active microwave, so stop cooking first
StopCooking((uid, component));
@ -470,7 +470,7 @@ namespace Content.Server.Kitchen.EntitySystems
GetNetEntityArray(component.Storage.ContainedEntities.ToArray()),
// DeltaV - start of microwave ejection bugfix
(
EntityManager.TryGetComponent<ActiveMicrowaveComponent>(uid, out var active)
EntityManager.TryGetComponent<ActiveMicrowaveComponent>(uid, out var active)
&& active.LifeStage < ComponentLifeStage.Stopping
),
// DeltaV - end of microwave ejection bugfix
@ -514,7 +514,7 @@ namespace Content.Server.Kitchen.EntitySystems
// DeltaV - start of microwave ejection bugfix
UpdateUserInterfaceState(ent, ent.Comp);
// DeltaV - end of microwave ejection bugfix
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(ent)} exploded from unsafe cooking!");
}
@ -562,7 +562,7 @@ namespace Content.Server.Kitchen.EntitySystems
foreach (var item in component.Storage.ContainedEntities.ToArray())
{
// special behavior when being microwaved ;)
var ev = new BeingMicrowavedEvent(uid, user, component.CurrentCookTimerTime);
var ev = new BeingMicrowavedEvent(uid, user, component.CurrentCookTimerTime); // DeltaV Additions - Improve animal cube interactions (31668 - Upstream)
RaiseLocalEvent(item, ev);
// TODO MICROWAVE SPARKS & EFFECTS

View File

@ -153,7 +153,7 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
}
else
{
_battery.SetCharge((entity.Owner, battery), battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency);
_battery.ChangeCharge((entity.Owner, battery), entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency);
if (_battery.IsFull((entity.Owner, battery)))
{
if (TryComp<ApcPowerReceiverComponent>(entity.Owner, out var receiver))

View File

@ -1,12 +1,11 @@
using Content.Server.Actions;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Shared.Actions;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Light;
using Content.Shared.Light.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Rounding;
using Content.Shared.Toggleable;
using JetBrains.Annotations;
@ -25,7 +24,7 @@ namespace Content.Server.Light.EntitySystems
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!;
@ -44,7 +43,6 @@ namespace Content.Server.Light.EntitySystems
SubscribeLocalEvent<HandheldLightComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<HandheldLightComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<HandheldLightComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<HandheldLightComponent, ActivateInWorldEvent>(OnActivate);
@ -108,13 +106,15 @@ namespace Content.Server.Light.EntitySystems
// Curently every single flashlight has the same number of levels for status and that's all it uses the charge for
// Thus we'll just check if the level changes.
if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery))
if (!_powerCell.TryGetBatteryFromSlotOrEntity(ent.Owner, out var battery))
return null;
if (MathHelper.CloseToPercent(battery.CurrentCharge, 0) || ent.Comp.Wattage > battery.CurrentCharge)
var currentCharge = _battery.GetCharge(battery.Value.AsNullable());
if (MathHelper.CloseToPercent(currentCharge, 0) || ent.Comp.Wattage > currentCharge)
return 0;
return (byte?) ContentHelpers.RoundToNearestLevels(battery.CurrentCharge / battery.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
return (byte?)ContentHelpers.RoundToNearestLevels(currentCharge / battery.Value.Comp.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
}
private void OnRemove(Entity<HandheldLightComponent> ent, ref ComponentRemove args)
@ -140,19 +140,14 @@ namespace Content.Server.Light.EntitySystems
return ent.Comp.Activated ? TurnOff(ent) : TurnOn(user, ent);
}
private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args)
{
args.PushMarkup(component.Activated
? Loc.GetString("handheld-light-component-on-examine-is-on-message")
: Loc.GetString("handheld-light-component-on-examine-is-off-message"));
}
public override void Shutdown()
{
base.Shutdown();
_activeLights.Clear();
}
// TODO: Very important: Make this charge rate based instead of instantly removing charge each update step.
// See BatteryComponent
public override void Update(float frameTime)
{
var toRemove = new RemQueue<Entity<HandheldLightComponent>>();
@ -199,8 +194,7 @@ namespace Content.Server.Light.EntitySystems
return false;
}
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery) &&
!TryComp(uid, out battery))
if (!_powerCell.TryGetBatteryFromSlotOrEntity(uid.Owner, out var battery))
{
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), uid, user);
@ -210,7 +204,7 @@ namespace Content.Server.Light.EntitySystems
// To prevent having to worry about frame time in here.
// Let's just say you need a whole second of charge before you can turn it on.
// Simple enough.
if (component.Wattage > battery.CurrentCharge)
if (component.Wattage > _battery.GetCharge(battery.Value.AsNullable()))
{
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), uid, user);
@ -227,24 +221,20 @@ namespace Content.Server.Light.EntitySystems
public void TryUpdate(Entity<HandheldLightComponent> uid, float frameTime)
{
var component = uid.Comp;
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery, null) &&
!TryComp(uid, out battery))
if (!_powerCell.TryGetBatteryFromSlotOrEntity(uid.Owner, out var battery))
{
TurnOff(uid, false);
return;
}
if (batteryUid == null)
return;
var appearanceComponent = EntityManager.GetComponentOrNull<AppearanceComponent>(uid);
var fraction = battery.CurrentCharge / battery.MaxCharge;
if (fraction >= 0.30)
var chargeFraction = _battery.GetChargeLevel(battery.Value.AsNullable());
if (chargeFraction >= 0.30)
{
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower, appearanceComponent);
}
else if (fraction >= 0.10)
else if (chargeFraction >= 0.10)
{
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.LowPower, appearanceComponent);
}
@ -253,7 +243,7 @@ namespace Content.Server.Light.EntitySystems
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.Dying, appearanceComponent);
}
if (component.Activated && !_battery.TryUseCharge((batteryUid.Value, battery), component.Wattage * frameTime))
if (component.Activated && !_battery.TryUseCharge(battery.Value.AsNullable(), component.Wattage * frameTime))
TurnOff(uid, false);
UpdateLevel(uid);

View File

@ -2,7 +2,6 @@ using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Mech.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.ActionBlocker;
using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
@ -14,6 +13,7 @@ using Content.Shared.Mech.EntitySystems;
using Content.Shared.Movement.Events;
using Content.Shared.Popups;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Content.Shared.Tools.Systems;
@ -33,7 +33,7 @@ public sealed partial class MechSystem : SharedMechSystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
@ -112,7 +112,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (args.Container != component.BatterySlot || !TryComp<BatteryComponent>(args.Entity, out var battery))
return;
component.Energy = battery.CurrentCharge;
component.Energy = _battery.GetCharge((args.Entity, battery));
component.MaxEnergy = battery.MaxCharge;
Dirty(uid, component);
@ -340,11 +340,13 @@ public sealed partial class MechSystem : SharedMechSystem
if (!TryComp<BatteryComponent>(battery, out var batteryComp))
return false;
_battery.SetCharge((battery.Value, batteryComp), batteryComp.CurrentCharge + delta.Float());
if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them
_battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float());
// TODO: Power cells are predicted now, so no need to duplicate the charge level
var charge = _battery.GetCharge((battery.Value, batteryComp));
if (charge != component.Energy) //if there's a discrepency, we have to resync them
{
Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}");
component.Energy = batteryComp.CurrentCharge;
Log.Debug($"Battery charge was not equal to mech charge. Battery {charge}. Mech {component.Energy}");
component.Energy = charge;
Dirty(uid, component);
}
_actionBlocker.UpdateCanMove(uid);
@ -360,7 +362,7 @@ public sealed partial class MechSystem : SharedMechSystem
return;
_container.Insert(toInsert, component.BatterySlot);
component.Energy = battery.CurrentCharge;
component.Energy = _battery.GetCharge((toInsert, battery));
component.MaxEnergy = battery.MaxCharge;
_actionBlocker.UpdateCanMove(uid);

View File

@ -1,3 +1,4 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Storage;
namespace Content.Server.Medical.BiomassReclaimer
@ -34,10 +35,10 @@ namespace Content.Server.Medical.BiomassReclaimer
public float CurrentExpectedYield = 0f;
/// <summary>
/// The reagent that will be spilled while processing a mob.
/// The reagents that will be spilled while processing a mob.
/// </summary>
[ViewVariables]
public string? BloodReagent;
public Solution? BloodReagents = null;
/// <summary>
/// Entities that can be randomly spawned while processing a mob.

View File

@ -7,7 +7,7 @@ using Content.Shared.Administration.Logs;
using Content.Shared.Audio;
using Content.Shared.Body.Components;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Climbing.Events;
using Content.Shared.Construction.Components;
using Content.Shared.Database;
@ -45,6 +45,7 @@ namespace Content.Server.Medical.BiomassReclaimer
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
@ -68,10 +69,8 @@ namespace Content.Server.Medical.BiomassReclaimer
if (reclaimer.RandomMessTimer <= 0)
{
if (_robustRandom.Prob(0.2f) && reclaimer.BloodReagent is not null)
if (_robustRandom.Prob(0.2f) && reclaimer.BloodReagents is { } blood)
{
Solution blood = new();
blood.AddReagent(reclaimer.BloodReagent, 50);
_puddleSystem.TrySpillAt(uid, blood, out _);
}
if (_robustRandom.Prob(0.03f) && reclaimer.SpawnedEntities.Count > 0)
@ -92,7 +91,7 @@ namespace Content.Server.Medical.BiomassReclaimer
reclaimer.CurrentExpectedYield = reclaimer.CurrentExpectedYield - actualYield; // store non-integer leftovers
_material.SpawnMultipleFromMaterial(actualYield, BiomassPrototype, Transform(uid).Coordinates);
reclaimer.BloodReagent = null;
reclaimer.BloodReagents = null;
reclaimer.SpawnedEntities.Clear();
RemCompDeferred<ActiveBiomassReclaimerComponent>(uid);
}
@ -208,9 +207,11 @@ namespace Content.Server.Medical.BiomassReclaimer
var component = ent.Comp;
AddComp<ActiveBiomassReclaimerComponent>(ent);
if (TryComp<BloodstreamComponent>(toProcess, out var stream))
if (TryComp<BloodstreamComponent>(toProcess, out var stream) &&
_solution.ResolveSolution(toProcess, stream.BloodSolutionName, ref stream.BloodSolution, out var solution))
{
component.BloodReagent = stream.BloodReagent;
component.BloodReagents = solution.Clone();
component.BloodReagents.ScaleSolution(50 / component.BloodReagents.Volume);
}
if (TryComp<ButcherableComponent>(toProcess, out var butcherableComponent))
{

View File

@ -2,7 +2,7 @@ using System.Linq;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.EntitySystems; // DeltaV
using Content.Server.PowerCell;
using Content.Shared.PowerCell;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Medical.CrewMonitoring;

View File

@ -5,7 +5,7 @@ using Content.Server.Electrocution;
using Content.Server.EUI;
using Content.Server.Ghost;
using Content.Server.Popups;
using Content.Server.PowerCell;
using Content.Shared.PowerCell;
using Content.Shared.Traits.Assorted;
using Content.Shared.Chat;
using Content.Shared.Damage.Components;
@ -18,6 +18,8 @@ using Content.Shared.Mind;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.PowerCell;
using Content.Shared.Timing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
@ -44,6 +46,7 @@ public sealed class DefibrillatorSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
/// <inheritdoc/>
public override void Initialize()
@ -179,6 +182,18 @@ public sealed class DefibrillatorSystem : EntitySystem
_audio.PlayPvs(component.ZapSound, uid);
_electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true);
var interacters = new HashSet<EntityUid>();
_interactionSystem.GetEntitiesInteractingWithTarget(target, interacters);
foreach (var other in interacters)
{
if (other == user)
continue;
// Anyone else still operating on the target gets zapped too
_electrocution.TryDoElectrocution(other, null, component.ZapDamage, component.WritheDuration, true);
}
if (!TryComp<UseDelayComponent>(uid, out var useDelay))
return;
_useDelay.SetLength((uid, useDelay), component.ZapDelay, component.DelayId);

View File

@ -1,5 +1,4 @@
using Content.Server.Medical.Components;
using Content.Server.PowerCell;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Components;
@ -12,6 +11,7 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.PowerCell;
using Content.Shared.Temperature.Components;
using Content.Shared.Traits.Assorted;
using Robust.Server.GameObjects;
@ -25,9 +25,10 @@ using Content.Shared.Body.Systems;
using Content.Shared._Shitmed.Targeting;
using System.Linq;
// DeltaV - Medical Records
// Begin DeltaV
using Content.Server._DV.MedicalRecords;
using Content.Shared._DV.MedicalRecords;
// End DeltaV
namespace Content.Server.Medical;
@ -113,7 +114,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
/// </summary>
private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid.Owner, user: args.User))
return;
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
@ -133,7 +134,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid.Owner, user: args.User))
return;
if (!uid.Comp.Silent)

View File

@ -33,7 +33,7 @@ public sealed partial class TargetObjectiveMindFilter : MindFilter
if (entMan.TryGetComponent<TargetObjectiveComponent>(objective, out var kill) && kill.Target == mind.Owner)
{
// remove the mind if this objective is blacklisted
if (whitelistSys.IsBlacklistPassOrNull(Blacklist, objective))
if (whitelistSys.IsWhitelistPassOrNull(Blacklist, objective))
return true;
}
}

View File

@ -161,7 +161,7 @@ public sealed partial class NPCSteeringSystem
// Try smashing obstacles.
else if ((component.Flags & PathFlags.Smashing) != 0x0)
{
if (_melee.TryGetWeapon(uid, out _, out var meleeWeapon) && meleeWeapon.NextAttack <= _timing.CurTime && TryComp<CombatModeComponent>(uid, out var combatMode))
if (_melee.TryGetWeapon(uid, out var weaponUid, out var meleeWeapon) && meleeWeapon.NextAttack <= _timing.CurTime && TryComp<CombatModeComponent>(uid, out var combatMode)) // DeltaV - Get weaponuid
{
_combat.SetInCombatMode(uid, true, combatMode);
var destructibleQuery = GetEntityQuery<DestructibleComponent>();
@ -175,7 +175,7 @@ public sealed partial class NPCSteeringSystem
// TODO: Validate we can damage it
if (destructibleQuery.HasComponent(ent))
{
attackResult = _melee.AttemptLightAttack(uid, uid, meleeWeapon, ent);
attackResult = _melee.AttemptLightAttack(uid, weaponUid, meleeWeapon, ent); // DeltaV - Pass weaponUid
break;
}
}

View File

@ -7,10 +7,7 @@ using Robust.Shared.Random;
namespace Content.Server.NameIdentifier;
/// <summary>
/// Handles unique name identifiers for entities e.g. `monkey (MK-912)`
/// </summary>
public sealed class NameIdentifierSystem : EntitySystem
public sealed class NameIdentifierSystem : SharedNameIdentifierSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
@ -28,7 +25,6 @@ public sealed class NameIdentifierSystem : EntitySystem
SubscribeLocalEvent<NameIdentifierComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<NameIdentifierComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<NameIdentifierComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
SubscribeLocalEvent<RoundRestartCleanupEvent>(CleanupIds);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnReloadPrototypes);
@ -122,24 +118,6 @@ public sealed class NameIdentifierSystem : EntitySystem
_nameModifier.RefreshNameModifiers(ent.Owner);
}
private void OnRefreshNameModifiers(Entity<NameIdentifierComponent> ent, ref RefreshNameModifiersEvent args)
{
if (ent.Comp.Group is null)
return;
// Don't apply the modifier if the component is being removed
if (ent.Comp.LifeStage > ComponentLifeStage.Running)
return;
if (!_prototypeManager.Resolve(ent.Comp.Group, out var group))
return;
var format = group.FullName ? "name-identifier-format-full" : "name-identifier-format-append";
// We apply the modifier with a low priority to keep it near the base name
// "Beep (Si-4562) the zombie" instead of "Beep the zombie (Si-4562)"
args.AddModifier(format, -10, ("identifier", ent.Comp.FullIdentifier));
}
private void InitialSetupPrototypes()
{
EnsureIds();

View File

@ -1,12 +1,12 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Ninja.Systems;
@ -16,7 +16,7 @@ namespace Content.Server.Ninja.Systems;
/// </summary>
public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@ -37,7 +37,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
var (uid, comp) = ent;
var target = args.Target;
if (args.Handled || comp.BatteryUid is not {} battery || !HasComp<PowerNetworkBatteryComponent>(target))
if (args.Handled || comp.BatteryUid is not { } battery || !HasComp<PowerNetworkBatteryComponent>(target))
return;
// handles even if battery is full so you can actually see the poup
@ -70,7 +70,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
base.OnDoAfterAttempt(ent, ref args);
if (ent.Comp.BatteryUid is not {} battery || _battery.IsFull(battery))
if (ent.Comp.BatteryUid is not { } battery || _battery.IsFull(battery))
args.Cancel();
}
@ -84,14 +84,14 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
if (!TryComp<BatteryComponent>(target, out var targetBattery) || !TryComp<PowerNetworkBatteryComponent>(target, out var pnb))
return false;
if (MathHelper.CloseToPercent(targetBattery.CurrentCharge, 0))
var available = _battery.GetCharge((target, targetBattery));
if (MathHelper.CloseToPercent(available, 0))
{
_popup.PopupEntity(Loc.GetString("battery-drainer-empty", ("battery", target)), uid, uid, PopupType.Medium);
return false;
}
var available = targetBattery.CurrentCharge;
var required = battery.MaxCharge - battery.CurrentCharge;
var required = battery.MaxCharge - _battery.GetCharge((comp.BatteryUid.Value, battery));
// higher tier storages can charge more
var maxDrained = pnb.MaxSupply * comp.DrainTime;
var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
@ -99,7 +99,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
return false;
var output = input * comp.DrainEfficiency;
_battery.SetCharge((comp.BatteryUid.Value, battery), battery.CurrentCharge + output);
_battery.ChangeCharge((comp.BatteryUid.Value, battery), output);
// TODO: create effect message or something
Spawn("EffectSparks", Transform(target).Coordinates);
_audio.PlayPvs(comp.SparkSound, target);

View File

@ -1,15 +1,15 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
namespace Content.Server.Ninja.Systems;
public sealed class ItemCreatorSystem : SharedItemCreatorSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@ -24,7 +24,7 @@ public sealed class ItemCreatorSystem : SharedItemCreatorSystem
private void OnCreateItem(Entity<ItemCreatorComponent> ent, ref CreateItemEvent args)
{
var (uid, comp) = ent;
if (comp.Battery is not {} battery)
if (comp.Battery is not { } battery)
return;
args.Handled = true;

View File

@ -1,11 +1,10 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.Components;
using Content.Server.PowerCell;
using Content.Shared.Emp;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Robust.Shared.Containers;
@ -13,6 +12,7 @@ namespace Content.Server.Ninja.Systems;
/// <summary>
/// Handles power cell upgrading and actions.
/// TODO: Move all of this to shared and predict it
/// </summary>
public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
{
@ -51,8 +51,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
RaiseLocalEvent(user, ref ev);
}
// TODO: if/when battery is in shared, put this there too
// TODO: or put MaxCharge in shared along with powercellslot
private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, ContainerIsInsertingAttemptEvent args)
{
// this is for handling battery upgrading, not stopping actions from being added
@ -61,7 +59,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
return;
// no power cell for some reason??? allow it
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
return;
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting))
@ -73,7 +71,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
var user = Transform(uid).ParentUid;
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(batteryUid.Value, battery))
if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(battery.Value, battery.Value))
{
args.Cancel();
Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user);
@ -94,7 +92,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
{
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp))
return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue;
return battcomp.MaxCharge;
}
@ -136,7 +134,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
Popup.PopupEntity(Loc.GetString(message), user, user);
}
// TODO: Move this to shared when power cells are predicted.
private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args)
{
var (uid, comp) = ent;

View File

@ -2,8 +2,6 @@ using Content.Server.Communications;
using Content.Server.CriminalRecords.Systems;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Server.Research.Systems;
using Content.Shared.Alert;
using Content.Shared.Doors.Components;
@ -12,6 +10,8 @@ using Content.Shared.Mind;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Popups;
using Content.Shared.Rounding;
using System.Diagnostics.CodeAnalysis;
@ -24,7 +24,7 @@ namespace Content.Server.Ninja.Systems;
public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
@ -39,6 +39,8 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
SubscribeLocalEvent<SpaceNinjaComponent, CriminalRecordsHackedEvent>(OnCriminalRecordsHacked);
}
// TODO: Make this charge rate based instead of updating it every single tick.
// Or make it client side, since power cells are predicted.
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<SpaceNinjaComponent>();
@ -62,7 +64,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
return newCount - oldCount;
}
// TODO: can probably copy paste borg code here
// TODO: Generic charge indicator that is combined with borg code.
/// <summary>
/// Update the alert for the ninja's suit power indicator.
/// </summary>
@ -75,10 +77,10 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
return;
}
if (GetNinjaBattery(uid, out _, out var battery))
if (GetNinjaBattery(uid, out var batteryUid, out var batteryComp))
{
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8);
_alerts.ShowAlert(uid, comp.SuitPowerAlert, (short) severity);
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, _battery.GetCharge((batteryUid.Value, batteryComp))), batteryComp.MaxCharge, 8);
_alerts.ShowAlert(uid, comp.SuitPowerAlert, (short)severity);
}
else
{
@ -89,17 +91,19 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
/// <summary>
/// Get the battery component in a ninja's suit, if it's worn.
/// </summary>
public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? uid, [NotNullWhen(true)] out BatteryComponent? battery)
public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? batteryComp)
{
if (TryComp<SpaceNinjaComponent>(user, out var ninja)
&& ninja.Suit != null
&& _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out uid, out battery))
&& _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out var battery))
{
batteryUid = battery.Value.Owner;
batteryComp = battery.Value.Comp;
return true;
}
uid = null;
battery = null;
batteryUid = null;
batteryComp = null;
return false;
}

View File

@ -1,10 +1,10 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Damage.Systems;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stunnable;
using Content.Shared.Timing;
using Content.Shared.Whitelist;
@ -17,7 +17,7 @@ namespace Content.Server.Ninja.Systems;
/// </summary>
public sealed class StunProviderSystem : SharedStunProviderSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;

View File

@ -2,7 +2,6 @@ using Content.Server.AlertLevel;
using Content.Server.Audio;
using Content.Server.Chat.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Kitchen.Components;
using Content.Server.Pinpointer;
using Content.Server.Popups;
using Content.Server.Station.Systems;
@ -11,7 +10,7 @@ using Content.Shared.Containers.ItemSlots;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Kitchen.Components;
using Content.Shared.Kitchen;
using Content.Shared.Maps;
using Content.Shared.Nuke;
using Content.Shared.Popups;

View File

@ -44,8 +44,11 @@ public sealed class WarDeclaratorSystem : EntitySystem
{
if (!_accessReaderSystem.IsAllowed(args.User, ent))
{
var msg = Loc.GetString("war-declarator-not-working");
_popupSystem.PopupEntity(msg, ent);
if (!args.Silent)
{
var msg = Loc.GetString("war-declarator-not-working");
_popupSystem.PopupEntity(msg, ent);
}
args.Cancel();
return;
}

View File

@ -59,7 +59,7 @@ public sealed class NinjaConditionsSystem : EntitySystem
while (allEnts.MoveNext(out var warpUid, out var warp))
{
if (_whitelist.IsBlacklistFail(bombingBlacklist, warpUid)
if (_whitelist.IsWhitelistFail(bombingBlacklist, warpUid)
&& !string.IsNullOrWhiteSpace(warp.Location))
{
warps.Add(warpUid);

View File

@ -25,7 +25,7 @@ public sealed class ObjectiveBlacklistRequirementSystem : EntitySystem
foreach (var objective in args.Mind.Objectives)
{
if (_whitelistSystem.IsBlacklistPass(comp.Blacklist, objective))
if (_whitelistSystem.IsWhitelistPass(comp.Blacklist, objective))
{
args.Cancelled = true;
return;

View File

@ -2,9 +2,9 @@ using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Instruments;
using Content.Shared.Kitchen.Components; // DeltaV - shared
using Content.Server.Store.Systems;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind.Components;
using Content.Shared.Kitchen;
using Content.Shared.PAI;
using Content.Shared.Popups;
using Content.Shared.Instruments;

View File

@ -17,50 +17,49 @@ namespace Content.Server.Payload.EntitySystems;
public sealed class PayloadSystem : EntitySystem
{
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private static readonly ProtoId<TagPrototype> PayloadTag = "Payload";
// TODO: Construction System Integration tests and remove the EnsureContainer from ConstructionSystem. :(
private static readonly string PayloadContainer = "payload";
private static readonly string TriggerContainer = "payloadTrigger";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PayloadCaseComponent, TriggerEvent>(OnCaseTriggered);
SubscribeLocalEvent<PayloadTriggerComponent, TriggerEvent>(OnTriggerTriggered);
SubscribeLocalEvent<PayloadCaseComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<PayloadCaseComponent, EntInsertedIntoContainerMessage>(OnEntityInserted);
SubscribeLocalEvent<PayloadCaseComponent, EntRemovedFromContainerMessage>(OnEntityRemoved);
SubscribeLocalEvent<PayloadCaseComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<ChemicalPayloadComponent, TriggerEvent>(HandleChemicalPayloadTrigger);
}
public IEnumerable<EntityUid> GetAllPayloads(EntityUid uid, ContainerManagerComponent? contMan = null)
public IEnumerable<EntityUid> GetAllPayloads(EntityUid uid)
{
if (!Resolve(uid, ref contMan, false))
if (!_container.TryGetContainer(uid, PayloadContainer, out var container))
yield break;
foreach (var container in contMan.Containers.Values)
foreach (var entity in container.ContainedEntities)
{
foreach (var entity in container.ContainedEntities)
{
if (_tagSystem.HasTag(entity, PayloadTag))
yield return entity;
}
if (_tagSystem.HasTag(entity, PayloadTag))
yield return entity;
}
}
private void OnCaseTriggered(EntityUid uid, PayloadCaseComponent component, TriggerEvent args)
{
// TODO: Adjust to the new trigger system
if (!TryComp(uid, out ContainerManagerComponent? contMan))
return;
// Pass trigger event onto all contained payloads. Payload capacity configurable by construction graphs.
foreach (var ent in GetAllPayloads(uid, contMan))
foreach (var ent in GetAllPayloads(uid))
{
RaiseLocalEvent(ent, ref args, false);
}
@ -82,9 +81,18 @@ public sealed class PayloadSystem : EntitySystem
RaiseLocalEvent(parent, ref args);
}
private void OnInsertAttempt(Entity<PayloadCaseComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (args.Container.ID == PayloadContainer && !_tagSystem.HasTag(args.EntityUid, PayloadTag))
args.Cancel();
if (args.Container.ID == TriggerContainer && !HasComp<PayloadTriggerComponent>(args.EntityUid))
args.Cancel();
}
private void OnEntityInserted(EntityUid uid, PayloadCaseComponent _, EntInsertedIntoContainerMessage args)
{
if (!TryComp(args.Entity, out PayloadTriggerComponent? trigger))
if (args.Container.ID != TriggerContainer || !TryComp(args.Entity, out PayloadTriggerComponent? trigger))
return;
trigger.Active = true;
@ -114,7 +122,7 @@ public sealed class PayloadSystem : EntitySystem
private void OnEntityRemoved(EntityUid uid, PayloadCaseComponent component, EntRemovedFromContainerMessage args)
{
if (!TryComp(args.Entity, out PayloadTriggerComponent? trigger))
if (args.Container.ID != TriggerContainer || !TryComp(args.Entity, out PayloadTriggerComponent? trigger))
return;
trigger.Active = false;

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