Stable release (#41558)

This commit is contained in:
Myra
2025-11-23 16:17:04 +01:00
committed by GitHub
331 changed files with 7561 additions and 4428 deletions

View File

@@ -10,13 +10,13 @@
<!-- Summary of code changes for easier review. --> <!-- Summary of code changes for easier review. -->
## Media ## Media
<!-- Attach media if the PR makes ingame changes (clothing, items, features, etc). <!-- Attach media if the PR makes in-game changes (clothing, items, features, etc).
Small fixes/refactors are exempt. Media may be used in SS14 progress reports with credit. --> Small fixes/refactors are exempt. Media may be used in SS14 progress reports with credit. -->
## Requirements ## Requirements
<!-- Confirm the following by placing an X in the brackets [X]: --> <!-- Confirm the following by placing an X in the brackets [X]: -->
- [ ] I have read and am following the [Pull Request and Changelog Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html). - [ ] I have read and am following the [Pull Request and Changelog Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
- [ ] I have added media to this PR or it does not require an ingame showcase. - [ ] I have added media to this PR or it does not require an in-game showcase.
<!-- You should understand that not following the above may get your PR closed at maintainers discretion --> <!-- You should understand that not following the above may get your PR closed at maintainers discretion -->
## Breaking changes ## Breaking changes

View File

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

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

@@ -98,7 +98,6 @@ namespace Content.Client.Entry
_componentFactory.IgnoreMissingComponents(); _componentFactory.IgnoreMissingComponents();
// Do not add to these, they are legacy. // Do not add to these, they are legacy.
_componentFactory.RegisterClass<SharedGravityGeneratorComponent>();
_componentFactory.RegisterClass<SharedAmeControllerComponent>(); _componentFactory.RegisterClass<SharedAmeControllerComponent>();
// Do not add to the above, they are legacy // Do not add to the above, they are legacy

View File

@@ -3,6 +3,7 @@ using System.Numerics;
using Content.Client.Clickable; using Content.Client.Clickable;
using Content.Client.UserInterface; using Content.Client.UserInterface;
using Content.Client.Viewport; using Content.Client.Viewport;
using Content.Shared.CCVar;
using Content.Shared.Input; using Content.Shared.Input;
using Robust.Client.ComponentTrees; using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
@@ -13,6 +14,7 @@ using Robust.Client.State;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Configuration;
using Robust.Shared.Console; using Robust.Shared.Console;
using Robust.Shared.Graphics; using Robust.Shared.Graphics;
using Robust.Shared.Input; using Robust.Shared.Input;
@@ -40,6 +42,7 @@ namespace Content.Client.Gameplay
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IViewVariablesManager _vvm = default!; [Dependency] private readonly IViewVariablesManager _vvm = default!;
[Dependency] private readonly IConsoleHost _conHost = default!; [Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
private ClickableEntityComparer _comparer = default!; private ClickableEntityComparer _comparer = default!;
@@ -83,6 +86,8 @@ namespace Content.Client.Gameplay
_comparer = new ClickableEntityComparer(); _comparer = new ClickableEntityComparer();
CommandBinds.Builder CommandBinds.Builder
.Bind(ContentKeyFunctions.InspectEntity, new PointerInputCmdHandler(HandleInspect, outsidePrediction: true)) .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>(); .Register<GameplayStateBase>();
} }
@@ -99,6 +104,21 @@ namespace Content.Client.Gameplay
return true; 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) public EntityUid? GetClickedEntity(MapCoordinates coordinates)
{ {
return GetClickedEntity(coordinates, _eyeManager.CurrentEye); return GetClickedEntity(coordinates, _eyeManager.CurrentEye);

View File

@@ -0,0 +1,8 @@
using Content.Shared.Gravity;
namespace Content.Client.Gravity;
public sealed class GravityGeneratorSystem : SharedGravityGeneratorSystem
{
}

View File

@@ -12,14 +12,14 @@ public sealed partial class GravitySystem : SharedGravitySystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SharedGravityGeneratorComponent, AppearanceChangeEvent>(OnAppearanceChange); SubscribeLocalEvent<GravityGeneratorComponent, AppearanceChangeEvent>(OnAppearanceChange);
InitializeShake(); InitializeShake();
} }
/// <summary> /// <summary>
/// Ensures that the visible state of gravity generators are synced with their sprites. /// Ensures that the visible state of gravity generators are synced with their sprites.
/// </summary> /// </summary>
private void OnAppearanceChange(EntityUid uid, SharedGravityGeneratorComponent comp, ref AppearanceChangeEvent args) private void OnAppearanceChange(EntityUid uid, GravityGeneratorComponent comp, ref AppearanceChangeEvent args)
{ {
if (args.Sprite == null) if (args.Sprite == null)
return; return;

View File

@@ -38,6 +38,8 @@ namespace Content.Client.Input
common.AddFunction(ContentKeyFunctions.ZoomIn); common.AddFunction(ContentKeyFunctions.ZoomIn);
common.AddFunction(ContentKeyFunctions.ResetZoom); common.AddFunction(ContentKeyFunctions.ResetZoom);
common.AddFunction(ContentKeyFunctions.InspectEntity); common.AddFunction(ContentKeyFunctions.InspectEntity);
common.AddFunction(ContentKeyFunctions.InspectServerComponent);
common.AddFunction(ContentKeyFunctions.InspectClientComponent);
common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow); common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow);
// Not in engine, because engine cannot check for sanbox/admin status before starting placement. // Not in engine, because engine cannot check for sanbox/admin status before starting placement.

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
using Content.Server.Gravity;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.Gravity; using Content.Shared.Gravity;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;

View File

@@ -402,8 +402,8 @@ namespace Content.IntegrationTests.Tests.Power
battery = entityManager.GetComponent<BatteryComponent>(generatorEnt); battery = entityManager.GetComponent<BatteryComponent>(generatorEnt);
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt); consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
batterySys.SetMaxCharge(generatorEnt, startingCharge, battery); batterySys.SetMaxCharge((generatorEnt, battery), startingCharge);
batterySys.SetCharge(generatorEnt, startingCharge, battery); batterySys.SetCharge((generatorEnt, battery), startingCharge);
netBattery.MaxSupply = 400; netBattery.MaxSupply = 400;
netBattery.SupplyRampRate = 400; netBattery.SupplyRampRate = 400;
netBattery.SupplyRampTolerance = 100; netBattery.SupplyRampTolerance = 100;
@@ -513,8 +513,8 @@ namespace Content.IntegrationTests.Tests.Power
supplier.SupplyRampRate = rampRate; supplier.SupplyRampRate = rampRate;
supplier.SupplyRampTolerance = rampTol; supplier.SupplyRampTolerance = rampTol;
batterySys.SetMaxCharge(batteryEnt, 100_000, battery); batterySys.SetMaxCharge((batteryEnt, battery), 100_000);
batterySys.SetCharge(batteryEnt, 100_000, battery); batterySys.SetCharge((batteryEnt, battery), 100_000);
netBattery.MaxSupply = draw / 2; netBattery.MaxSupply = draw / 2;
netBattery.SupplyRampRate = rampRate; netBattery.SupplyRampRate = rampRate;
netBattery.SupplyRampTolerance = rampTol; netBattery.SupplyRampTolerance = rampTol;
@@ -600,7 +600,7 @@ namespace Content.IntegrationTests.Tests.Power
supplier.MaxSupply = 500; supplier.MaxSupply = 500;
supplier.SupplyRampTolerance = 500; supplier.SupplyRampTolerance = 500;
batterySys.SetMaxCharge(batteryEnt, 100_000, battery); batterySys.SetMaxCharge((batteryEnt, battery), 100_000);
netBattery.MaxChargeRate = 1_000; netBattery.MaxChargeRate = 1_000;
netBattery.Efficiency = 0.5f; netBattery.Efficiency = 0.5f;
}); });
@@ -670,8 +670,8 @@ namespace Content.IntegrationTests.Tests.Power
netBattery.MaxSupply = 400; netBattery.MaxSupply = 400;
netBattery.SupplyRampTolerance = 400; netBattery.SupplyRampTolerance = 400;
netBattery.SupplyRampRate = 100_000; netBattery.SupplyRampRate = 100_000;
batterySys.SetMaxCharge(batteryEnt, 100_000, battery); batterySys.SetMaxCharge((batteryEnt, battery), 100_000);
batterySys.SetCharge(batteryEnt, 100_000, battery); batterySys.SetCharge((batteryEnt, battery), 100_000);
}); });
// Run some ticks so everything is stable. // Run some ticks so everything is stable.
@@ -750,8 +750,8 @@ namespace Content.IntegrationTests.Tests.Power
netBattery.SupplyRampTolerance = 400; netBattery.SupplyRampTolerance = 400;
netBattery.SupplyRampRate = 100_000; netBattery.SupplyRampRate = 100_000;
netBattery.Efficiency = 0.5f; netBattery.Efficiency = 0.5f;
batterySys.SetMaxCharge(batteryEnt, 1_000_000, battery); batterySys.SetMaxCharge((batteryEnt, battery), 1_000_000);
batterySys.SetCharge(batteryEnt, 1_000_000, battery); batterySys.SetCharge((batteryEnt, battery), 1_000_000);
}); });
// Run some ticks so everything is stable. // Run some ticks so everything is stable.
@@ -841,8 +841,8 @@ namespace Content.IntegrationTests.Tests.Power
supplier.MaxSupply = 1000; supplier.MaxSupply = 1000;
supplier.SupplyRampTolerance = 1000; supplier.SupplyRampTolerance = 1000;
batterySys.SetMaxCharge(batteryEnt1, 1_000_000, battery1); batterySys.SetMaxCharge((batteryEnt1, battery1), 1_000_000);
batterySys.SetMaxCharge(batteryEnt2, 1_000_000, battery2); batterySys.SetMaxCharge((batteryEnt2, battery2), 1_000_000);
netBattery1.MaxChargeRate = 1_000; netBattery1.MaxChargeRate = 1_000;
netBattery2.MaxChargeRate = 1_000; netBattery2.MaxChargeRate = 1_000;
@@ -945,10 +945,10 @@ namespace Content.IntegrationTests.Tests.Power
netBattery2.SupplyRampTolerance = 1000; netBattery2.SupplyRampTolerance = 1000;
netBattery1.SupplyRampRate = 100_000; netBattery1.SupplyRampRate = 100_000;
netBattery2.SupplyRampRate = 100_000; netBattery2.SupplyRampRate = 100_000;
batterySys.SetMaxCharge(batteryEnt1, 100_000, battery1); batterySys.SetMaxCharge((batteryEnt1, battery1), 100_000);
batterySys.SetMaxCharge(batteryEnt2, 100_000, battery2); batterySys.SetMaxCharge((batteryEnt2, battery2), 100_000);
batterySys.SetCharge(batteryEnt1, 100_000, battery1); batterySys.SetCharge((batteryEnt1, battery1), 100_000);
batterySys.SetCharge(batteryEnt2, 100_000, battery2); batterySys.SetCharge((batteryEnt2, battery2), 100_000);
}); });
// Run some ticks so everything is stable. // Run some ticks so everything is stable.
@@ -1031,8 +1031,8 @@ namespace Content.IntegrationTests.Tests.Power
supplier.MaxSupply = 1000; supplier.MaxSupply = 1000;
supplier.SupplyRampTolerance = 1000; supplier.SupplyRampTolerance = 1000;
batterySys.SetMaxCharge(batteryEnt1, 1_000_000, battery1); batterySys.SetMaxCharge((batteryEnt1, battery1), 1_000_000);
batterySys.SetMaxCharge(batteryEnt2, 1_000_000, battery2); batterySys.SetMaxCharge((batteryEnt2, battery2), 1_000_000);
netBattery1.MaxChargeRate = 20; netBattery1.MaxChargeRate = 20;
netBattery2.MaxChargeRate = 20; netBattery2.MaxChargeRate = 20;
@@ -1107,8 +1107,8 @@ namespace Content.IntegrationTests.Tests.Power
netBattery.MaxSupply = 1000; netBattery.MaxSupply = 1000;
netBattery.SupplyRampTolerance = 200; netBattery.SupplyRampTolerance = 200;
netBattery.SupplyRampRate = 10; netBattery.SupplyRampRate = 10;
batterySys.SetMaxCharge(batteryEnt, 100_000, battery); batterySys.SetMaxCharge((batteryEnt, battery), 100_000);
batterySys.SetCharge(batteryEnt, 100_000, battery); batterySys.SetCharge((batteryEnt, battery), 100_000);
}); });
// Run some ticks so everything is stable. // Run some ticks so everything is stable.
@@ -1253,7 +1253,7 @@ namespace Content.IntegrationTests.Tests.Power
generatorSupplier.MaxSupply = 1000; generatorSupplier.MaxSupply = 1000;
generatorSupplier.SupplyRampTolerance = 1000; generatorSupplier.SupplyRampTolerance = 1000;
batterySys.SetCharge(apcEnt, 0, apcBattery); batterySys.SetCharge((apcEnt, apcBattery), 0);
}); });
server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc
@@ -1314,8 +1314,8 @@ namespace Content.IntegrationTests.Tests.Power
extensionCableSystem.SetProviderTransferRange(apcExtensionEnt, range); extensionCableSystem.SetProviderTransferRange(apcExtensionEnt, range);
extensionCableSystem.SetReceiverReceptionRange(powerReceiverEnt, range); extensionCableSystem.SetReceiverReceptionRange(powerReceiverEnt, range);
batterySys.SetMaxCharge(apcEnt, 10000, battery); //arbitrary nonzero amount of charge batterySys.SetMaxCharge((apcEnt, battery), 10000); //arbitrary nonzero amount of charge
batterySys.SetCharge(apcEnt, battery.MaxCharge, battery); //fill battery batterySys.SetCharge((apcEnt, battery), battery.MaxCharge); //fill battery
receiver.Load = 1; //arbitrary small amount of power receiver.Load = 1; //arbitrary small amount of power
}); });

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

@@ -169,7 +169,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")),
Act = () => Act = () =>
{ {
_batterySystem.SetCharge(args.Target, battery.MaxCharge, battery); _batterySystem.SetCharge((args.Target, battery), battery.MaxCharge);
}, },
Impact = LogImpact.Medium, Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-refill-battery-description"), Message = Loc.GetString("admin-trick-refill-battery-description"),
@@ -184,7 +184,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")),
Act = () => Act = () =>
{ {
_batterySystem.SetCharge(args.Target, 0, battery); _batterySystem.SetCharge((args.Target, battery), 0);
}, },
Impact = LogImpact.Medium, Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-drain-battery-description"), Message = Loc.GetString("admin-trick-drain-battery-description"),
@@ -200,9 +200,8 @@ public sealed partial class AdminVerbSystem
Act = () => Act = () =>
{ {
var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target); var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target);
recharger.AutoRecharge = true;
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill. recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePause = false; // No delay. recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
}, },
Impact = LogImpact.Medium, Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"), Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
@@ -553,7 +552,7 @@ public sealed partial class AdminVerbSystem
if (!HasComp<StationInfiniteBatteryTargetComponent>(ent)) if (!HasComp<StationInfiniteBatteryTargetComponent>(ent))
continue; continue;
var battery = EnsureComp<BatteryComponent>(ent); var battery = EnsureComp<BatteryComponent>(ent);
_batterySystem.SetCharge(ent, battery.MaxCharge, battery); _batterySystem.SetCharge((ent, battery), battery.MaxCharge);
} }
}, },
Impact = LogImpact.Extreme, Impact = LogImpact.Extreme,
@@ -574,7 +573,7 @@ public sealed partial class AdminVerbSystem
if (!HasComp<StationInfiniteBatteryTargetComponent>(ent)) if (!HasComp<StationInfiniteBatteryTargetComponent>(ent))
continue; continue;
var battery = EnsureComp<BatteryComponent>(ent); var battery = EnsureComp<BatteryComponent>(ent);
_batterySystem.SetCharge(ent, 0, battery); _batterySystem.SetCharge((ent, battery), 0);
} }
}, },
Impact = LogImpact.Extreme, Impact = LogImpact.Extreme,
@@ -599,9 +598,8 @@ public sealed partial class AdminVerbSystem
var recharger = EnsureComp<BatterySelfRechargerComponent>(ent); var recharger = EnsureComp<BatterySelfRechargerComponent>(ent);
var battery = EnsureComp<BatteryComponent>(ent); var battery = EnsureComp<BatteryComponent>(ent);
recharger.AutoRecharge = true;
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill. recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePause = false; // No delay. recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
} }
}, },
Impact = LogImpact.Extreme, Impact = LogImpact.Extreme,

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) bool midround = false)
{ {
var playerPool = GetPlayerPool(ent, pool, def); 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; 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. // 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) if (!antagEnt.HasValue)
{ {
var getEntEv = new AntagSelectEntityEvent(session, ent); var getEntEv = new AntagSelectEntityEvent(session, ent, def.PrefRoles);
RaiseLocalEvent(ent, ref getEntEv, true); RaiseLocalEvent(ent, ref getEntEv, true);
antagEnt = getEntEv.Entity; antagEnt = getEntEv.Entity;
} }
@@ -404,7 +405,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (antagEnt is not { } player) if (antagEnt is not { } player)
{ {
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for 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) if (session != null && ent.Comp.RemoveUponFailedSpawn)
{ {
ent.Comp.AssignedSessions.Remove(session); 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 // 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. // 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); RaiseLocalEvent(ent, ref getPosEv, true);
if (getPosEv.Handled) if (getPosEv.Handled)
{ {
@@ -435,7 +436,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp)) if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
{ {
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent."); 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) if (session != null)
{ {
ent.Comp.AssignedSessions.Remove(session); ent.Comp.AssignedSessions.Remove(session);
@@ -538,21 +539,21 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
switch (def.MultiAntagSetting) switch (def.MultiAntagSetting)
{ {
case AntagAcceptability.None: case AntagAcceptability.None:
{ {
if (_role.MindIsAntagonist(mind)) if (_role.MindIsAntagonist(mind))
return false; return false;
if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet
return false; return false;
break; break;
} }
case AntagAcceptability.NotExclusive: case AntagAcceptability.NotExclusive:
{ {
if (_role.MindIsExclusiveAntagonist(mind)) if (_role.MindIsExclusiveAntagonist(mind))
return false; return false;
if (GetPreSelectedExclusiveAntagSessions(def).Contains(session)) if (GetPreSelectedExclusiveAntagSessions(def).Contains(session))
return false; return false;
break; break;
} }
} }
// todo: expand this to allow for more fine antag-selection logic for game rules. // 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. /// Only raised if the selected player's current entity is invalid.
/// </summary> /// </summary>
[ByRefEvent] [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; 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 bool Handled => Entity != null;
public EntityUid? Entity; 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. /// Event raised on a game rule entity to determine the location for the antagonist.
/// </summary> /// </summary>
[ByRefEvent] [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 readonly ICommonSession? Session = Session;
public bool Handled => Coordinates.Any(); public bool Handled => Coordinates.Any();
// the entity of the antagonist
public EntityUid Entity = Entity;
public List<MapCoordinates> Coordinates = new(); 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

@@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
@@ -14,6 +13,22 @@ namespace Content.Server.Atmos.EntitySystems;
public partial class AtmosphereSystem 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) public GasMixture? GetContainingMixture(Entity<TransformComponent?> ent, bool ignoreExposed = false, bool excite = false)
{ {
if (!Resolve(ent, ref ent.Comp)) 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); 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( public GasMixture? GetContainingMixture(
Entity<TransformComponent?> ent, Entity<TransformComponent?> ent,
Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid, Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid,
@@ -49,16 +75,38 @@ public partial class AtmosphereSystem
return GetTileMixture(grid, map, position, excite); 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) 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); var ev = new SetSimulatedGridMethodEvent(gridUid, simulated);
RaiseLocalEvent(gridUid, ref ev); RaiseLocalEvent(gridUid, ref ev);
return ev.Handled; 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) public bool IsSimulatedGrid(EntityUid gridUid)
{ {
var ev = new IsSimulatedGridMethodEvent(gridUid); var ev = new IsSimulatedGridMethodEvent(gridUid);
@@ -67,24 +115,53 @@ public partial class AtmosphereSystem
return ev.Simulated; 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) public IEnumerable<GasMixture> GetAllMixtures(EntityUid gridUid, bool excite = false)
{ {
var ev = new GetAllMixturesMethodEvent(gridUid, excite); var ev = new GetAllMixturesMethodEvent(gridUid, excite);
RaiseLocalEvent(gridUid, ref ev); RaiseLocalEvent(gridUid, ref ev);
if(!ev.Handled) if (!ev.Handled)
return Enumerable.Empty<GasMixture>(); return [];
DebugTools.AssertNotNull(ev.Mixtures); DebugTools.AssertNotNull(ev.Mixtures);
return 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) public void InvalidateTile(Entity<GridAtmosphereComponent?> entity, Vector2i tile)
{ {
if (_atmosQuery.Resolve(entity.Owner, ref entity.Comp, false)) if (_atmosQuery.Resolve(entity.Owner, ref entity.Comp, false))
entity.Comp.InvalidatedCoords.Add(tile); 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( public GasMixture?[]? GetTileMixtures(
Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid, Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid,
Entity<MapAtmosphereComponent?>? map, Entity<MapAtmosphereComponent?>? map,
@@ -95,7 +172,7 @@ public partial class AtmosphereSystem
var handled = false; var handled = false;
// If we've been passed a grid, try to let it handle it. // 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) if (excite)
Resolve(gridEnt, ref gridEnt.Comp2); 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. // We either don't have a grid, or the event wasn't handled.
// Let the map handle it instead, and also broadcast the event. // 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]; mixtures ??= new GasMixture?[tiles.Count];
for (var i = 0; i < tiles.Count; i++) for (var i = 0; i < tiles.Count; i++)
@@ -145,10 +222,21 @@ public partial class AtmosphereSystem
{ {
mixtures[i] ??= GasMixture.SpaceGas; mixtures[i] ??= GasMixture.SpaceGas;
} }
return mixtures; 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)) if (!Resolve(entity.Owner, ref entity.Comp))
return null; return null;
@@ -157,6 +245,15 @@ public partial class AtmosphereSystem
return GetTileMixture(entity.Comp.GridUid, entity.Comp.MapUid, indices, excite); 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( public GasMixture? GetTileMixture(
Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid, Entity<GridAtmosphereComponent?, GasTileOverlayComponent?>? grid,
Entity<MapAtmosphereComponent?>? map, Entity<MapAtmosphereComponent?>? map,
@@ -164,8 +261,8 @@ public partial class AtmosphereSystem
bool excite = false) bool excite = false)
{ {
// If we've been passed a grid, try to let it handle it. // If we've been passed a grid, try to let it handle it.
if (grid is {} gridEnt if (grid is { } gridEnt
&& Resolve(gridEnt, ref gridEnt.Comp1, false) && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp1, false)
&& gridEnt.Comp1.Tiles.TryGetValue(gridTile, out var tile)) && gridEnt.Comp1.Tiles.TryGetValue(gridTile, out var tile))
{ {
if (excite) if (excite)
@@ -177,13 +274,20 @@ public partial class AtmosphereSystem
return tile.Air; 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; return mapEnt.Comp.Mixture;
// Default to a space mixture... This is a space game, after all! // Default to a space mixture... This is a space game, after all!
return GasMixture.SpaceGas; 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) public ReactionResult ReactTile(EntityUid gridId, Vector2i tile)
{ {
var ev = new ReactTileMethodEvent(gridId, tile); var ev = new ReactTileMethodEvent(gridId, tile);
@@ -194,24 +298,49 @@ public partial class AtmosphereSystem
return ev.Result; 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)) if (!Resolve(gridUid, ref mapGridComp, false))
return 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); var data = GetAirtightData(gridUid, mapGridComp, tile);
return data.BlockedDirections.IsFlagSet(directions); 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) public bool IsTileSpace(Entity<GridAtmosphereComponent?>? grid, Entity<MapAtmosphereComponent?>? map, Vector2i tile)
{ {
if (grid is {} gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp, false) if (grid is { } gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp, false)
&& gridEnt.Comp.Tiles.TryGetValue(tile, out var tileAtmos)) && gridEnt.Comp.Tiles.TryGetValue(tile, out var tileAtmos))
{ {
return tileAtmos.Space; 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; return mapEnt.Comp.Space;
// If nothing handled the event, it'll default to true. // If nothing handled the event, it'll default to true.
@@ -219,28 +348,77 @@ public partial class AtmosphereSystem
return true; 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) public bool IsTileMixtureProbablySafe(Entity<GridAtmosphereComponent?>? grid, Entity<MapAtmosphereComponent?> map, Vector2i tile)
{ {
return IsMixtureProbablySafe(GetTileMixture(grid, map, 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) public float GetTileHeatCapacity(Entity<GridAtmosphereComponent?>? grid, Entity<MapAtmosphereComponent?> map, Vector2i tile)
{ {
return GetHeatCapacity(GetTileMixture(grid, map, tile) ?? GasMixture.SpaceGas); 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) 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)) if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
return TileMixtureEnumerator.Empty; return TileMixtureEnumerator.Empty;
return !grid.Comp.Tiles.TryGetValue(tile, out var atmosTile) return !grid.Comp.Tiles.TryGetValue(tile, out var atmosTile)
? TileMixtureEnumerator.Empty ? TileMixtureEnumerator.Empty
: new(atmosTile.AdjacentTiles); : new TileMixtureEnumerator(atmosTile.AdjacentTiles);
} }
public void HotspotExpose(Entity<GridAtmosphereComponent?> grid, Vector2i tile, float exposedTemperature, float exposedVolume, /// <summary>
EntityUid? sparkSourceUid = null, bool soh = false) /// 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)) if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
return; return;
@@ -249,8 +427,26 @@ public partial class AtmosphereSystem
HotspotExpose(grid.Comp, atmosTile, exposedTemperature, exposedVolume, soh, sparkSourceUid); HotspotExpose(grid.Comp, atmosTile, exposedTemperature, exposedVolume, soh, sparkSourceUid);
} }
public void HotspotExpose(TileAtmosphere tile, float exposedTemperature, float exposedVolume, /// <summary>
EntityUid? sparkSourceUid = null, bool soh = false) /// 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)) if (!_atmosQuery.TryGetComponent(tile.GridIndex, out var atmos))
return; return;
@@ -259,12 +455,25 @@ public partial class AtmosphereSystem
HotspotExpose(atmos, tile, exposedTemperature, exposedVolume, soh, sparkSourceUid); 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) public void HotspotExtinguish(EntityUid gridUid, Vector2i tile)
{ {
var ev = new HotspotExtinguishMethodEvent(gridUid, tile); var ev = new HotspotExtinguishMethodEvent(gridUid, tile);
RaiseLocalEvent(gridUid, ref ev); 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) public bool IsHotspotActive(EntityUid gridUid, Vector2i tile)
{ {
var ev = new IsHotspotActiveMethodEvent(gridUid, tile); var ev = new IsHotspotActiveMethodEvent(gridUid, tile);
@@ -274,11 +483,25 @@ public partial class AtmosphereSystem
return ev.Result; 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) public bool AddPipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet)
{ {
return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Add(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) public bool RemovePipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet)
{ {
// Technically this event can be fired even on grids that don't // 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); 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) public bool AddAtmosDevice(Entity<GridAtmosphereComponent?> grid, Entity<AtmosDeviceComponent> device)
{ {
DebugTools.Assert(device.Comp.JoinedGrid == null); DebugTools.Assert(device.Comp.JoinedGrid == null);
@@ -307,6 +537,12 @@ public partial class AtmosphereSystem
return true; 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) public bool RemoveAtmosDevice(Entity<GridAtmosphereComponent?> grid, Entity<AtmosDeviceComponent> device)
{ {
DebugTools.Assert(device.Comp.JoinedGrid == grid); DebugTools.Assert(device.Comp.JoinedGrid == grid);
@@ -418,23 +654,44 @@ public partial class AtmosphereSystem
return contains; return contains;
} }
[ByRefEvent] private record struct SetSimulatedGridMethodEvent [ByRefEvent]
(EntityUid Grid, bool Simulated, bool Handled = false); private record struct SetSimulatedGridMethodEvent(
EntityUid Grid,
bool Simulated,
bool Handled = false);
[ByRefEvent] private record struct IsSimulatedGridMethodEvent [ByRefEvent]
(EntityUid Grid, bool Simulated = false, bool Handled = false); private record struct IsSimulatedGridMethodEvent(
EntityUid Grid,
bool Simulated = false,
bool Handled = false);
[ByRefEvent] private record struct GetAllMixturesMethodEvent [ByRefEvent]
(EntityUid Grid, bool Excite = false, IEnumerable<GasMixture>? Mixtures = null, bool Handled = false); private record struct GetAllMixturesMethodEvent(
EntityUid Grid,
bool Excite = false,
IEnumerable<GasMixture>? Mixtures = null,
bool Handled = false);
[ByRefEvent] private record struct ReactTileMethodEvent [ByRefEvent]
(EntityUid GridId, Vector2i Tile, ReactionResult Result = default, bool Handled = false); private record struct ReactTileMethodEvent(
EntityUid GridId,
Vector2i Tile,
ReactionResult Result = default,
bool Handled = false);
[ByRefEvent] private record struct HotspotExtinguishMethodEvent [ByRefEvent]
(EntityUid Grid, Vector2i Tile, bool Handled = false); private record struct HotspotExtinguishMethodEvent(
EntityUid Grid,
Vector2i Tile,
bool Handled = false);
[ByRefEvent] private record struct IsHotspotActiveMethodEvent [ByRefEvent]
(EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false); 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 public partial class AtmosphereSystem
{ {
/*
Partial class that stores miscellaneous utility methods for Atmospherics.
*/
/// <summary> /// <summary>
/// Gets the particular price of an air mixture. /// Gets the particular price of a <see cref="GasMixture"/>.
/// </summary> /// </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) public double GetPrice(GasMixture mixture)
{ {
float basePrice = 0; // moles of gas * price/mole float basePrice = 0; // moles of gas * price/mole
@@ -26,7 +32,7 @@ public partial class AtmosphereSystem
maxComponent = Math.Max(maxComponent, mixture.Moles[i]); 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; float purity = 1;
if (totalMoles > 0) if (totalMoles > 0)
{ {
@@ -36,12 +42,32 @@ public partial class AtmosphereSystem
return basePrice * purity; 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InvalidateVisuals(Entity<GasTileOverlayComponent?> grid, Vector2i tile) public void InvalidateVisuals(Entity<GasTileOverlayComponent?> grid, Vector2i tile)
{ {
_gasTileOverlaySystem.Invalidate(grid, 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void InvalidateVisuals( private void InvalidateVisuals(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent, Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
@@ -51,7 +77,7 @@ public partial class AtmosphereSystem
} }
/// <summary> /// <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> /// </summary>
/// <param name="mapGrid">The grid in question.</param> /// <param name="mapGrid">The grid in question.</param>
/// <param name="tiles">The amount of tiles.</param> /// <param name="tiles">The amount of tiles.</param>
@@ -79,6 +105,18 @@ public partial class AtmosphereSystem
bool NoAirWhenBlocked, bool NoAirWhenBlocked,
bool FixVacuum); 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) private void UpdateAirtightData(EntityUid uid, GridAtmosphereComponent atmos, MapGridComponent grid, TileAtmosphere tile)
{ {
var oldBlocked = tile.AirtightData.BlockedDirections; var oldBlocked = tile.AirtightData.BlockedDirections;
@@ -91,6 +129,15 @@ public partial class AtmosphereSystem
ExcitedGroupDispose(atmos, tile.ExcitedGroup); 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) private AirtightData GetAirtightData(EntityUid uid, MapGridComponent grid, Vector2i tile)
{ {
var blockedDirs = AtmosDirection.Invalid; var blockedDirs = AtmosDirection.Invalid;
@@ -118,7 +165,7 @@ public partial class AtmosphereSystem
} }
/// <summary> /// <summary>
/// Pries a tile in a grid. /// Pries a tile in a grid.
/// </summary> /// </summary>
/// <param name="mapGrid">The grid in question.</param> /// <param name="mapGrid">The grid in question.</param>
/// <param name="tile">The indices of the tile.</param> /// <param name="tile">The indices of the tile.</param>

View File

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

View File

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

View File

@@ -111,6 +111,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{ {
args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName))); args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName)));
} }
args.AddLine("");
} }
private void OnNukeExploded(NukeExplodedEvent ev) 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 // TODO: someone suggested listing all alive? revs maybe implement at some point
} }
args.AddLine("");
} }
private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args) 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)) if (_whitelist.IsWhitelistFail(ent.Comp.SpawnerWhitelist, uid))
continue; continue;
if (TryComp<GridSpawnPointWhitelistComponent>(uid, out var gridSpawnPointWhitelistComponent))
{
if (!_whitelist.CheckBoth(args.Entity, gridSpawnPointWhitelistComponent.Blacklist, gridSpawnPointWhitelistComponent.Whitelist))
continue;
}
args.Coordinates.Add(_transform.GetMapCoordinates(xform)); 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-dead-count", ("deadCount", deadSurvivors)));
args.AddLine(Loc.GetString("survivor-round-end-alive-count", ("aliveCount", aliveMarooned))); 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(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. // Player manifest at EOR shows who's a survivor so no need for extra info here.
} }

View File

@@ -0,0 +1,168 @@
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("xenoborgs-cond-xenoborgs-alive", ("count", numXenoborgs)));
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;
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), ("name", meta.EntityName),
("username", username))); ("username", username)));
} }
args.AddLine("");
} }
/// <summary> /// <summary>

View File

@@ -333,7 +333,8 @@ namespace Content.Server.Ghost
if (_followerSystem.GetMostGhostFollowed() is not {} target) if (_followerSystem.GetMostGhostFollowed() is not {} target)
return; 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) private void WarpTo(EntityUid uid, EntityUid target)

View File

@@ -1,20 +0,0 @@
using Content.Shared.Gravity;
using Content.Shared.Construction.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Gravity
{
[RegisterComponent]
[Access(typeof(GravityGeneratorSystem))]
public sealed partial class GravityGeneratorComponent : SharedGravityGeneratorComponent
{
[DataField("lightRadiusMin")] public float LightRadiusMin { get; set; }
[DataField("lightRadiusMax")] public float LightRadiusMax { get; set; }
/// <summary>
/// Is the gravity generator currently "producing" gravity?
/// </summary>
[ViewVariables]
public bool GravityActive { get; set; } = false;
}
}

View File

@@ -4,7 +4,7 @@ using Content.Shared.Gravity;
namespace Content.Server.Gravity; namespace Content.Server.Gravity;
public sealed class GravityGeneratorSystem : EntitySystem public sealed class GravityGeneratorSystem : SharedGravityGeneratorSystem
{ {
[Dependency] private readonly GravitySystem _gravitySystem = default!; [Dependency] private readonly GravitySystem _gravitySystem = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!; [Dependency] private readonly SharedPointLightSystem _lights = default!;
@@ -36,6 +36,7 @@ public sealed class GravityGeneratorSystem : EntitySystem
private void OnActivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineActivatedEvent args) private void OnActivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineActivatedEvent args)
{ {
ent.Comp.GravityActive = true; ent.Comp.GravityActive = true;
Dirty(ent, ent.Comp);
var xform = Transform(ent); var xform = Transform(ent);
@@ -48,6 +49,7 @@ public sealed class GravityGeneratorSystem : EntitySystem
private void OnDeactivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineDeactivatedEvent args) private void OnDeactivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineDeactivatedEvent args)
{ {
ent.Comp.GravityActive = false; ent.Comp.GravityActive = false;
Dirty(ent, ent.Comp);
var xform = Transform(ent); var xform = Transform(ent);

View File

@@ -145,7 +145,7 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
{ {
if (entity.Comp.State == EmergencyLightState.On) if (entity.Comp.State == EmergencyLightState.On)
{ {
if (!_battery.TryUseCharge(entity.Owner, entity.Comp.Wattage * frameTime, battery)) if (!_battery.TryUseCharge((entity.Owner, battery), entity.Comp.Wattage * frameTime))
{ {
SetState(entity.Owner, entity.Comp, EmergencyLightState.Empty); SetState(entity.Owner, entity.Comp, EmergencyLightState.Empty);
TurnOff(entity); TurnOff(entity);
@@ -153,8 +153,8 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
} }
else else
{ {
_battery.SetCharge(entity.Owner, battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency, battery); _battery.SetCharge((entity.Owner, battery), battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency);
if (_battery.IsFull(entity, battery)) if (_battery.IsFull((entity.Owner, battery)))
{ {
if (TryComp<ApcPowerReceiverComponent>(entity.Owner, out var receiver)) if (TryComp<ApcPowerReceiverComponent>(entity.Owner, out var receiver))
{ {

View File

@@ -253,7 +253,7 @@ namespace Content.Server.Light.EntitySystems
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.Dying, appearanceComponent); _appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.Dying, appearanceComponent);
} }
if (component.Activated && !_battery.TryUseCharge(batteryUid.Value, component.Wattage * frameTime, battery)) if (component.Activated && !_battery.TryUseCharge((batteryUid.Value, battery), component.Wattage * frameTime))
TurnOff(uid, false); TurnOff(uid, false);
UpdateLevel(uid); UpdateLevel(uid);

View File

@@ -340,7 +340,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (!TryComp<BatteryComponent>(battery, out var batteryComp)) if (!TryComp<BatteryComponent>(battery, out var batteryComp))
return false; return false;
_battery.SetCharge(battery!.Value, batteryComp.CurrentCharge + delta.Float(), batteryComp); _battery.SetCharge((battery.Value, batteryComp), batteryComp.CurrentCharge + delta.Float());
if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them if (batteryComp.CurrentCharge != 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}"); Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}");

View File

@@ -95,17 +95,17 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
// higher tier storages can charge more // higher tier storages can charge more
var maxDrained = pnb.MaxSupply * comp.DrainTime; var maxDrained = pnb.MaxSupply * comp.DrainTime;
var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained); var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
if (!_battery.TryUseCharge(target, input, targetBattery)) if (!_battery.TryUseCharge((target, targetBattery), input))
return false; return false;
var output = input * comp.DrainEfficiency; var output = input * comp.DrainEfficiency;
_battery.SetCharge(comp.BatteryUid.Value, battery.CurrentCharge + output, battery); _battery.SetCharge((comp.BatteryUid.Value, battery), battery.CurrentCharge + output);
// TODO: create effect message or something // TODO: create effect message or something
Spawn("EffectSparks", Transform(target).Coordinates); Spawn("EffectSparks", Transform(target).Coordinates);
_audio.PlayPvs(comp.SparkSound, target); _audio.PlayPvs(comp.SparkSound, target);
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid); _popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// repeat the doafter until battery is full // repeat the doafter until battery is full
return !_battery.IsFull(comp.BatteryUid.Value, battery); return !_battery.IsFull((comp.BatteryUid.Value, battery));
} }
} }

View File

@@ -106,7 +106,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
/// <inheritdoc/> /// <inheritdoc/>
public override bool TryUseCharge(EntityUid user, float charge) public override bool TryUseCharge(EntityUid user, float charge)
{ {
return GetNinjaBattery(user, out var uid, out var battery) && _battery.TryUseCharge(uid.Value, charge, battery); return GetNinjaBattery(user, out var uid, out var battery) && _battery.TryUseCharge((uid.Value, battery), charge);
} }
/// <summary> /// <summary>

View File

@@ -1,36 +0,0 @@
using System;
namespace Content.Server.Power.Components
{
/// <summary>
/// Self-recharging battery.
/// </summary>
[RegisterComponent]
public sealed partial class BatterySelfRechargerComponent : Component
{
/// <summary>
/// Does the entity auto recharge?
/// </summary>
[DataField] public bool AutoRecharge;
/// <summary>
/// At what rate does the entity automatically recharge?
/// </summary>
[DataField] public float AutoRechargeRate;
/// <summary>
/// Should this entity stop automatically recharging if a charge is used?
/// </summary>
[DataField] public bool AutoRechargePause = false;
/// <summary>
/// How long should the entity stop automatically recharging if a charge is used?
/// </summary>
[DataField] public float AutoRechargePauseTime = 0f;
/// <summary>
/// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system.
/// </summary>
[DataField] public TimeSpan NextAutoRecharge = TimeSpan.FromSeconds(0f);
}
}

View File

@@ -0,0 +1,104 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
namespace Content.Server.Power.EntitySystems;
public sealed partial class BatterySystem
{
public override float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
{
if (!Resolve(ent, ref ent.Comp))
return 0;
var newValue = Math.Clamp(ent.Comp.CurrentCharge + amount, 0, ent.Comp.MaxCharge);
var delta = newValue - ent.Comp.CurrentCharge;
ent.Comp.CurrentCharge = newValue;
TrySetChargeCooldown(ent.Owner);
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
return delta;
}
public override float UseCharge(Entity<BatteryComponent?> ent, float amount)
{
if (amount <= 0 || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
return 0;
return ChangeCharge(ent, -amount);
}
public override bool TryUseCharge(Entity<BatteryComponent?> ent, float amount)
{
if (!Resolve(ent, ref ent.Comp, false) || amount > ent.Comp.CurrentCharge)
return false;
UseCharge(ent, amount);
return true;
}
public override void SetCharge(Entity<BatteryComponent?> ent, float value)
{
if (!Resolve(ent, ref ent.Comp))
return;
var oldCharge = ent.Comp.CurrentCharge;
ent.Comp.CurrentCharge = MathHelper.Clamp(value, 0, ent.Comp.MaxCharge);
if (MathHelper.CloseTo(ent.Comp.CurrentCharge, oldCharge) &&
!(oldCharge != ent.Comp.CurrentCharge && ent.Comp.CurrentCharge == ent.Comp.MaxCharge))
{
return;
}
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
}
public override void SetMaxCharge(Entity<BatteryComponent?> ent, float value)
{
if (!Resolve(ent, ref ent.Comp))
return;
var old = ent.Comp.MaxCharge;
ent.Comp.MaxCharge = Math.Max(value, 0);
ent.Comp.CurrentCharge = Math.Min(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
if (MathHelper.CloseTo(ent.Comp.MaxCharge, old))
return;
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
}
public override void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
if (ent.Comp.AutoRechargePauseTime == TimeSpan.Zero)
return; // no recharge pause
if (_timing.CurTime + ent.Comp.AutoRechargePauseTime <= ent.Comp.NextAutoRecharge)
return; // the current pause is already longer
SetChargeCooldown(ent, ent.Comp.AutoRechargePauseTime);
}
public override void SetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent, TimeSpan cooldown)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.NextAutoRecharge = _timing.CurTime + cooldown;
}
/// <summary>
/// Returns whether the battery is full.
/// </summary>
public bool IsFull(Entity<BatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return false;
return ent.Comp.CurrentCharge >= ent.Comp.MaxCharge;
}
}

View File

@@ -9,232 +9,115 @@ using JetBrains.Annotations;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Power.EntitySystems namespace Content.Server.Power.EntitySystems;
[UsedImplicitly]
public sealed partial class BatterySystem : SharedBatterySystem
{ {
[UsedImplicitly] [Dependency] private readonly IGameTiming _timing = default!;
public sealed class BatterySystem : SharedBatterySystem
public override void Initialize()
{ {
[Dependency] private readonly IGameTiming _timing = default!; base.Initialize();
public override void Initialize() SubscribeLocalEvent<ExaminableBatteryComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
SubscribeLocalEvent<BatteryComponent, ChangeChargeEvent>(OnChangeCharge);
SubscribeLocalEvent<BatteryComponent, GetChargeEvent>(OnGetCharge);
SubscribeLocalEvent<NetworkBatteryPreSync>(PreSync);
SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync);
}
private void OnNetBatteryRejuvenate(Entity<PowerNetworkBatteryComponent> ent, ref RejuvenateEvent args)
{
ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity;
}
private void OnBatteryRejuvenate(Entity<BatteryComponent> ent, ref RejuvenateEvent args)
{
SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
}
private void OnExamine(Entity<ExaminableBatteryComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (!TryComp<BatteryComponent>(ent, out var battery))
return;
var chargePercentRounded = 0;
if (battery.MaxCharge != 0)
chargePercentRounded = (int)(100 * battery.CurrentCharge / battery.MaxCharge);
args.PushMarkup(
Loc.GetString(
"examinable-battery-component-examine-detail",
("percent", chargePercentRounded),
("markupPercentColor", "green")
)
);
}
private void PreSync(NetworkBatteryPreSync ev)
{
// Ignoring entity pausing. If the entity was paused, neither component's data should have been changed.
var enumerator = AllEntityQuery<PowerNetworkBatteryComponent, BatteryComponent>();
while (enumerator.MoveNext(out var netBat, out var bat))
{ {
base.Initialize(); DebugTools.Assert(bat.CurrentCharge <= bat.MaxCharge && bat.CurrentCharge >= 0);
netBat.NetworkBattery.Capacity = bat.MaxCharge;
SubscribeLocalEvent<ExaminableBatteryComponent, ExaminedEvent>(OnExamine); netBat.NetworkBattery.CurrentStorage = bat.CurrentCharge;
SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
SubscribeLocalEvent<BatteryComponent, ChangeChargeEvent>(OnChangeCharge);
SubscribeLocalEvent<BatteryComponent, GetChargeEvent>(OnGetCharge);
SubscribeLocalEvent<NetworkBatteryPreSync>(PreSync);
SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync);
} }
}
private void OnNetBatteryRejuvenate(EntityUid uid, PowerNetworkBatteryComponent component, RejuvenateEvent args) private void PostSync(NetworkBatteryPostSync ev)
{
// Ignoring entity pausing. If the entity was paused, neither component's data should have been changed.
var enumerator = AllEntityQuery<PowerNetworkBatteryComponent, BatteryComponent>();
while (enumerator.MoveNext(out var uid, out var netBat, out var bat))
{ {
component.NetworkBattery.CurrentStorage = component.NetworkBattery.Capacity; SetCharge((uid, bat), netBat.NetworkBattery.CurrentStorage);
} }
}
private void OnBatteryRejuvenate(EntityUid uid, BatteryComponent component, RejuvenateEvent args) /// <summary>
/// Gets the price for the power contained in an entity's battery.
/// </summary>
private void CalculateBatteryPrice(Entity<BatteryComponent> ent, ref PriceCalculationEvent args)
{
args.Price += ent.Comp.CurrentCharge * ent.Comp.PricePerJoule;
}
private void OnChangeCharge(Entity<BatteryComponent> ent, ref ChangeChargeEvent args)
{
if (args.ResidualValue == 0)
return;
args.ResidualValue -= ChangeCharge(ent.AsNullable(), args.ResidualValue);
}
private void OnGetCharge(Entity<BatteryComponent> entity, ref GetChargeEvent args)
{
args.CurrentCharge += entity.Comp.CurrentCharge;
args.MaxCharge += entity.Comp.MaxCharge;
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<BatterySelfRechargerComponent, BatteryComponent>();
var curTime = _timing.CurTime;
while (query.MoveNext(out var uid, out var comp, out var bat))
{ {
SetCharge(uid, component.MaxCharge, component); if (!comp.AutoRecharge || IsFull((uid, bat)))
} continue;
private void OnExamine(EntityUid uid, ExaminableBatteryComponent component, ExaminedEvent args) if (comp.NextAutoRecharge > curTime)
{ continue;
if (!TryComp<BatteryComponent>(uid, out var batteryComponent))
return;
if (args.IsInDetailsRange)
{
var effectiveMax = batteryComponent.MaxCharge;
if (effectiveMax == 0)
effectiveMax = 1;
var chargeFraction = batteryComponent.CurrentCharge / effectiveMax;
var chargePercentRounded = (int)(chargeFraction * 100);
args.PushMarkup(
Loc.GetString(
"examinable-battery-component-examine-detail",
("percent", chargePercentRounded),
("markupPercentColor", "green")
)
);
}
}
private void PreSync(NetworkBatteryPreSync ev) SetCharge((uid, bat), bat.CurrentCharge + comp.AutoRechargeRate * frameTime);
{
// Ignoring entity pausing. If the entity was paused, neither component's data should have been changed.
var enumerator = AllEntityQuery<PowerNetworkBatteryComponent, BatteryComponent>();
while (enumerator.MoveNext(out var netBat, out var bat))
{
DebugTools.Assert(bat.CurrentCharge <= bat.MaxCharge && bat.CurrentCharge >= 0);
netBat.NetworkBattery.Capacity = bat.MaxCharge;
netBat.NetworkBattery.CurrentStorage = bat.CurrentCharge;
}
}
private void PostSync(NetworkBatteryPostSync ev)
{
// Ignoring entity pausing. If the entity was paused, neither component's data should have been changed.
var enumerator = AllEntityQuery<PowerNetworkBatteryComponent, BatteryComponent>();
while (enumerator.MoveNext(out var uid, out var netBat, out var bat))
{
SetCharge(uid, netBat.NetworkBattery.CurrentStorage, bat);
}
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<BatterySelfRechargerComponent, BatteryComponent>();
while (query.MoveNext(out var uid, out var comp, out var batt))
{
if (!comp.AutoRecharge || IsFull(uid, batt))
continue;
if (comp.AutoRechargePause)
{
if (comp.NextAutoRecharge > _timing.CurTime)
continue;
}
SetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt);
}
}
/// <summary>
/// Gets the price for the power contained in an entity's battery.
/// </summary>
private void CalculateBatteryPrice(EntityUid uid, BatteryComponent component, ref PriceCalculationEvent args)
{
args.Price += component.CurrentCharge * component.PricePerJoule;
}
private void OnChangeCharge(Entity<BatteryComponent> entity, ref ChangeChargeEvent args)
{
if (args.ResidualValue == 0)
return;
args.ResidualValue -= ChangeCharge(entity, args.ResidualValue);
}
private void OnGetCharge(Entity<BatteryComponent> entity, ref GetChargeEvent args)
{
args.CurrentCharge += entity.Comp.CurrentCharge;
args.MaxCharge += entity.Comp.MaxCharge;
}
public override float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (value <= 0 || !Resolve(uid, ref battery) || battery.CurrentCharge == 0)
return 0;
return ChangeCharge(uid, -value, battery);
}
public override void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery))
return;
var old = battery.MaxCharge;
battery.MaxCharge = Math.Max(value, 0);
battery.CurrentCharge = Math.Min(battery.CurrentCharge, battery.MaxCharge);
if (MathHelper.CloseTo(battery.MaxCharge, old))
return;
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
RaiseLocalEvent(uid, ref ev);
}
public void SetCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery))
return;
var old = battery.CurrentCharge;
battery.CurrentCharge = MathHelper.Clamp(value, 0, battery.MaxCharge);
if (MathHelper.CloseTo(battery.CurrentCharge, old) &&
!(old != battery.CurrentCharge && battery.CurrentCharge == battery.MaxCharge))
{
return;
}
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
RaiseLocalEvent(uid, ref ev);
}
/// <summary>
/// Changes the current battery charge by some value
/// </summary>
public override float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery))
return 0;
var newValue = Math.Clamp(battery.CurrentCharge + value, 0, battery.MaxCharge);
var delta = newValue - battery.CurrentCharge;
battery.CurrentCharge = newValue;
TrySetChargeCooldown(uid);
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
RaiseLocalEvent(uid, ref ev);
return delta;
}
public override void TrySetChargeCooldown(EntityUid uid, float value = -1)
{
if (!TryComp<BatterySelfRechargerComponent>(uid, out var batteryself))
return;
if (!batteryself.AutoRechargePause)
return;
// If no answer or a negative is given for value, use the default from AutoRechargePauseTime.
if (value < 0)
value = batteryself.AutoRechargePauseTime;
if (_timing.CurTime + TimeSpan.FromSeconds(value) <= batteryself.NextAutoRecharge)
return;
SetChargeCooldown(uid, batteryself.AutoRechargePauseTime, batteryself);
}
/// <summary>
/// Puts the entity's self recharge on cooldown for the specified time.
/// </summary>
public void SetChargeCooldown(EntityUid uid, float value, BatterySelfRechargerComponent? batteryself = null)
{
if (!Resolve(uid, ref batteryself))
return;
if (value >= 0)
batteryself.NextAutoRecharge = _timing.CurTime + TimeSpan.FromSeconds(value);
else
batteryself.NextAutoRecharge = _timing.CurTime;
}
/// <summary>
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
/// </summary>
public override bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery, false) || value > battery.CurrentCharge)
return false;
UseCharge(uid, value, battery);
return true;
}
/// <summary>
/// Returns whether the battery is full.
/// </summary>
public bool IsFull(EntityUid uid, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery))
return false;
return battery.CurrentCharge >= battery.MaxCharge;
} }
} }
} }

View File

@@ -221,7 +221,7 @@ public sealed class ChargerSystem : SharedChargerSystem
if (!SearchForBattery(container.ContainedEntities[0], out var heldEnt, out var heldBattery)) if (!SearchForBattery(container.ContainedEntities[0], out var heldEnt, out var heldBattery))
return CellChargerStatus.Off; return CellChargerStatus.Off;
if (_battery.IsFull(heldEnt.Value, heldBattery)) if (_battery.IsFull((heldEnt.Value, heldBattery)))
return CellChargerStatus.Charged; return CellChargerStatus.Charged;
return CellChargerStatus.Charging; return CellChargerStatus.Charging;
@@ -241,7 +241,7 @@ public sealed class ChargerSystem : SharedChargerSystem
if (!SearchForBattery(targetEntity, out var batteryUid, out var heldBattery)) if (!SearchForBattery(targetEntity, out var batteryUid, out var heldBattery))
return; return;
_battery.SetCharge(batteryUid.Value, heldBattery.CurrentCharge + component.ChargeRate * frameTime, heldBattery); _battery.SetCharge((batteryUid.Value, heldBattery), heldBattery.CurrentCharge + component.ChargeRate * frameTime);
UpdateStatus(uid, component); UpdateStatus(uid, component);
} }

View File

@@ -358,13 +358,13 @@ namespace Content.Server.Power.EntitySystems
if (requireBattery) if (requireBattery)
{ {
_battery.SetCharge(uid, battery.CurrentCharge - apcBattery.IdleLoad * frameTime, battery); _battery.SetCharge((uid, battery), battery.CurrentCharge - apcBattery.IdleLoad * frameTime);
} }
// Otherwise try to charge the battery // Otherwise try to charge the battery
else if (powered && !_battery.IsFull(uid, battery)) else if (powered && !_battery.IsFull((uid, battery)))
{ {
apcReceiver.Load += apcBattery.BatteryRechargeRate * apcBattery.BatteryRechargeEfficiency; apcReceiver.Load += apcBattery.BatteryRechargeRate * apcBattery.BatteryRechargeEfficiency;
_battery.SetCharge(uid, battery.CurrentCharge + apcBattery.BatteryRechargeRate * frameTime, battery); _battery.SetCharge((uid, battery), battery.CurrentCharge + apcBattery.BatteryRechargeRate * frameTime);
} }
// Enable / disable the battery if the state changed // Enable / disable the battery if the state changed

View File

@@ -40,7 +40,7 @@ namespace Content.Server.Power
shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id))); shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id)));
return; return;
} }
_batterySystem.SetCharge(id.Value, battery.MaxCharge * percent / 100, battery); _batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
// Don't acknowledge b/c people WILL forall this // Don't acknowledge b/c people WILL forall this
} }
} }

View File

@@ -28,7 +28,7 @@ public sealed partial class PowerCellSystem
if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, slot)) if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, slot))
continue; continue;
if (_battery.TryUseCharge(batteryEnt.Value, comp.DrawRate * (float)comp.Delay.TotalSeconds, battery)) if (_battery.TryUseCharge((batteryEnt.Value, battery), comp.DrawRate * (float)comp.Delay.TotalSeconds))
continue; continue;
var ev = new PowerCellSlotEmptyEvent(); var ev = new PowerCellSlotEmptyEvent();

View File

@@ -174,7 +174,7 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem
return false; return false;
} }
if (!_battery.TryUseCharge(batteryEnt.Value, charge, battery)) if (!_battery.TryUseCharge((batteryEnt.Value, battery), charge))
{ {
if (user != null) if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value); _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value);

View File

@@ -66,7 +66,7 @@ namespace Content.Server.PowerSink
if (!transform.Anchored) if (!transform.Anchored)
continue; continue;
_battery.ChangeCharge(entity, networkLoad.NetworkLoad.ReceivingPower * frameTime, battery); _battery.ChangeCharge((entity, battery), networkLoad.NetworkLoad.ReceivingPower * frameTime);
var currentBatteryThreshold = battery.CurrentCharge / battery.MaxCharge; var currentBatteryThreshold = battery.CurrentCharge / battery.MaxCharge;

View File

@@ -34,7 +34,7 @@ public sealed class JammerSystem : SharedJammerSystem
if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery)) if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
{ {
if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage((uid, jam)) * frameTime, battery)) if (!_battery.TryUseCharge((batteryUid.Value, battery), GetCurrentWattage((uid, jam)) * frameTime))
{ {
ChangeLEDState(uid, false); ChangeLEDState(uid, false);
RemComp<ActiveRadioJammerComponent>(uid); RemComp<ActiveRadioJammerComponent>(uid);

View File

@@ -15,7 +15,7 @@ public sealed class RoleSystem : SharedRoleSystem
{ {
if (mindId == null) if (mindId == null)
{ {
Log.Error($"MingGetBriefing failed for mind {mindId}"); Log.Error($"MindGetBriefing failed for mind {mindId}");
return null; return null;
} }
@@ -23,7 +23,7 @@ public sealed class RoleSystem : SharedRoleSystem
if (mindComp is null) if (mindComp is null)
{ {
Log.Error($"MingGetBriefing failed for mind {mindId}"); Log.Error($"MindGetBriefing failed for mind {mindId}");
return null; return null;
} }

View File

@@ -801,7 +801,11 @@ public sealed partial class ShuttleSystem
while (iteration < FTLProximityIterations) while (iteration < FTLProximityIterations)
{ {
grids.Clear(); grids.Clear();
_mapManager.FindGridsIntersecting(mapId, targetAABB, ref grids); // We pass in an expanded offset here so we can safely do a random offset later.
// We don't include this in the actual targetAABB because then we would be double-expanding it.
// Once in this loop, then again when placing the shuttle later.
// Note that targetAABB already has expansionAmount factored in already.
_mapManager.FindGridsIntersecting(mapId, targetAABB.Enlarged(maxOffset), ref grids);
foreach (var grid in grids) foreach (var grid in grids)
{ {
@@ -834,10 +838,6 @@ public sealed partial class ShuttleSystem
if (nearbyGrids.Contains(uid)) if (nearbyGrids.Contains(uid))
continue; continue;
// We pass in an expanded offset here so we can safely do a random offset later.
// We don't include this in the actual targetAABB because then we would be double-expanding it.
// Once in this loop, then again when placing the shuttle later.
// Note that targetAABB already has expansionAmount factored in already.
targetAABB = targetAABB.Union( targetAABB = targetAABB.Union(
_transform.GetWorldMatrix(uid) _transform.GetWorldMatrix(uid)
.TransformBox(Comp<MapGridComponent>(uid).LocalAABB.Enlarged(expansionAmount))); .TransformBox(Comp<MapGridComponent>(uid).LocalAABB.Enlarged(expansionAmount)));
@@ -857,7 +857,7 @@ public sealed partial class ShuttleSystem
// TODO: This should prefer the position's angle instead. // TODO: This should prefer the position's angle instead.
// TODO: This is pretty crude for multiple landings. // TODO: This is pretty crude for multiple landings.
if (nearbyGrids.Count >= 1) if (nearbyGrids.Count > 1 || !HasComp<MapComponent>(targetXform.GridUid))
{ {
// Pick a random angle // Pick a random angle
var offsetAngle = _random.NextAngle(); var offsetAngle = _random.NextAngle();
@@ -866,9 +866,13 @@ public sealed partial class ShuttleSystem
var minRadius = MathF.Max(targetAABB.Width / 2f, targetAABB.Height / 2f); var minRadius = MathF.Max(targetAABB.Width / 2f, targetAABB.Height / 2f);
spawnPos = targetAABB.Center + offsetAngle.RotateVec(new Vector2(_random.NextFloat(minRadius + minOffset, minRadius + maxOffset), 0f)); spawnPos = targetAABB.Center + offsetAngle.RotateVec(new Vector2(_random.NextFloat(minRadius + minOffset, minRadius + maxOffset), 0f));
} }
else if (shuttleBody != null)
{
(spawnPos, angle) = _transform.GetWorldPositionRotation(targetXform);
}
else else
{ {
spawnPos = _transform.ToWorldPosition(targetCoordinates); spawnPos = _transform.GetWorldPosition(targetXform);
} }
var offset = Vector2.Zero; var offset = Vector2.Zero;
@@ -889,10 +893,10 @@ public sealed partial class ShuttleSystem
} }
// Rotate our localcenter around so we spawn exactly where we "think" we should (center of grid on the dot). // Rotate our localcenter around so we spawn exactly where we "think" we should (center of grid on the dot).
var transform = new Transform(_transform.ToWorldPosition(xform.Coordinates), angle); var transform = new Transform(spawnPos, angle);
var adjustedOffset = Robust.Shared.Physics.Transform.Mul(transform, offset); spawnPos = Robust.Shared.Physics.Transform.Mul(transform, offset);
coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos + adjustedOffset); coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos - offset);
return true; return true;
} }

View File

@@ -98,7 +98,7 @@ public sealed partial class BorgSystem
if (command == RoboticsConsoleConstants.NET_DISABLE_COMMAND) if (command == RoboticsConsoleConstants.NET_DISABLE_COMMAND)
Disable(ent); Disable(ent);
else if (command == RoboticsConsoleConstants.NET_DESTROY_COMMAND) else if (command == RoboticsConsoleConstants.NET_DESTROY_COMMAND)
Destroy(ent); Destroy(ent.Owner);
} }
private void Disable(Entity<BorgTransponderComponent, BorgChassisComponent?> ent) private void Disable(Entity<BorgTransponderComponent, BorgChassisComponent?> ent)
@@ -118,8 +118,15 @@ public sealed partial class BorgSystem
ent.Comp1.NextDisable = _timing.CurTime + ent.Comp1.DisableDelay; ent.Comp1.NextDisable = _timing.CurTime + ent.Comp1.DisableDelay;
} }
private void Destroy(Entity<BorgTransponderComponent> ent) /// <summary>
/// Makes a borg with <see cref="BorgTransponderComponent"/> explode
/// </summary>
/// <param name="ent">the entity of the borg</param>
public void Destroy(Entity<BorgTransponderComponent?> ent)
{ {
if (!Resolve(ent, ref ent.Comp))
return;
// this is stealthy until someone realises you havent exploded // this is stealthy until someone realises you havent exploded
if (CheckEmagged(ent, "destroyed")) if (CheckEmagged(ent, "destroyed"))
{ {

View File

@@ -125,7 +125,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
// into an AI core that has a full battery and full integrity. // into an AI core that has a full battery and full integrity.
if (TryComp<BatteryComponent>(ent, out var battery)) if (TryComp<BatteryComponent>(ent, out var battery))
{ {
_battery.SetCharge(ent, battery.MaxCharge); _battery.SetCharge((ent, battery), battery.MaxCharge);
} }
_damageable.ClearAllDamage(ent.Owner); _damageable.ClearAllDamage(ent.Owner);

View File

@@ -0,0 +1,22 @@
using Content.Shared.Whitelist;
namespace Content.Server.Spawners.Components;
/// <summary>
/// Defines whitelist and blacklist for entities that can spawn at a spawnpoint when they are spawned via the <see cref="RuleGridsSystem"/>
/// </summary>
[RegisterComponent]
public sealed partial class GridSpawnPointWhitelistComponent : Component
{
/// <summary>
/// Whitelist of entities that can be spawned at this SpawnPoint
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Whitelist of entities that can't be spawned at this SpawnPoint
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}

View File

@@ -33,7 +33,7 @@ namespace Content.Server.Stunnable.Systems
private void OnStaminaHitAttempt(Entity<StunbatonComponent> entity, ref StaminaDamageOnHitAttemptEvent args) private void OnStaminaHitAttempt(Entity<StunbatonComponent> entity, ref StaminaDamageOnHitAttemptEvent args)
{ {
if (!_itemToggle.IsActivated(entity.Owner) || if (!_itemToggle.IsActivated(entity.Owner) ||
!TryComp<BatteryComponent>(entity.Owner, out var battery) || !_battery.TryUseCharge(entity.Owner, entity.Comp.EnergyPerUse, battery)) !TryComp<BatteryComponent>(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse))
{ {
args.Cancelled = true; args.Cancelled = true;
} }

View File

@@ -24,7 +24,7 @@ public sealed class TeslaCoilSystem : EntitySystem
{ {
if (TryComp<BatteryComponent>(coil, out var batteryComponent)) if (TryComp<BatteryComponent>(coil, out var batteryComponent))
{ {
_battery.SetCharge(coil, batteryComponent.CurrentCharge + coil.Comp.ChargeFromLightning); _battery.SetCharge((coil, batteryComponent), batteryComponent.CurrentCharge + coil.Comp.ChargeFromLightning);
} }
} }
} }

View File

@@ -4,10 +4,7 @@ using Content.Shared.CCVar;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Dataset; using Content.Shared.Dataset;
using Content.Shared.Tips; using Content.Shared.Tips;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
@@ -15,10 +12,7 @@ using Robust.Shared.Timing;
namespace Content.Server.Tips; namespace Content.Server.Tips;
/// <summary> public sealed class TipsSystem : SharedTipsSystem
/// Handles periodically displaying gameplay tips to all players ingame.
/// </summary>
public sealed class TipsSystem : EntitySystem
{ {
[Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPrototypeManager _prototype = default!;
@@ -26,8 +20,6 @@ public sealed class TipsSystem : EntitySystem
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly GameTicker _ticker = default!; [Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private bool _tipsEnabled; private bool _tipsEnabled;
private float _tipTimeOutOfRound; private float _tipTimeOutOfRound;
@@ -35,16 +27,6 @@ public sealed class TipsSystem : EntitySystem
private string _tipsDataset = ""; private string _tipsDataset = "";
private float _tipTippyChance; private float _tipTippyChance;
/// <summary>
/// Always adds this time to a speech message. This is so really short message stay around for a bit.
/// </summary>
private const float SpeechBuffer = 3f;
/// <summary>
/// Expected reading speed.
/// </summary>
private const float Wpm = 180f;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
private TimeSpan _nextTipTime = TimeSpan.Zero; private TimeSpan _nextTipTime = TimeSpan.Zero;
@@ -53,110 +35,45 @@ public sealed class TipsSystem : EntitySystem
base.Initialize(); base.Initialize();
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged); SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
Subs.CVar(_cfg, CCVars.TipFrequencyOutOfRound, SetOutOfRound, true);
Subs.CVar(_cfg, CCVars.TipFrequencyInRound, SetInRound, true);
Subs.CVar(_cfg, CCVars.TipsEnabled, SetEnabled, true); Subs.CVar(_cfg, CCVars.TipsEnabled, SetEnabled, true);
Subs.CVar(_cfg, CCVars.TipsDataset, SetDataset, true); Subs.CVar(_cfg, CCVars.TipFrequencyOutOfRound, value => _tipTimeOutOfRound = value, true);
Subs.CVar(_cfg, CCVars.TipsTippyChance, SetTippyChance, true); Subs.CVar(_cfg, CCVars.TipFrequencyInRound, value => _tipTimeInRound = value, true);
Subs.CVar(_cfg, CCVars.TipsDataset, value => _tipsDataset = value, true);
Subs.CVar(_cfg, CCVars.TipsTippyChance, value => _tipTippyChance = value, true);
RecalculateNextTipTime(); RecalculateNextTipTime();
_conHost.RegisterCommand("tippy", Loc.GetString("cmd-tippy-desc"), Loc.GetString("cmd-tippy-help"), SendTippy, SendTippyHelper);
_conHost.RegisterCommand("tip", Loc.GetString("cmd-tip-desc"), "tip", SendTip);
} }
private CompletionResult SendTippyHelper(IConsoleShell shell, string[] args) private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev)
{ {
return args.Length switch // reset for lobby -> inround
// reset for inround -> post but not post -> lobby
if (ev.New == GameRunLevel.InRound || ev.Old == GameRunLevel.InRound)
{ {
1 => CompletionResult.FromHintOptions( RecalculateNextTipTime();
CompletionHelper.SessionNames(players: _playerManager), }
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
};
} }
private void SendTip(IConsoleShell shell, string argstr, string[] args) private void SetEnabled(bool value)
{ {
AnnounceRandomTip(); _tipsEnabled = value;
RecalculateNextTipTime();
if (_nextTipTime != TimeSpan.Zero)
RecalculateNextTipTime();
} }
private void SendTippy(IConsoleShell shell, string argstr, string[] args) public override void RecalculateNextTipTime()
{ {
if (args.Length < 2) if (_ticker.RunLevel == GameRunLevel.InRound)
{ {
shell.WriteLine(Loc.GetString("cmd-tippy-help")); _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeInRound);
return;
} }
ActorComponent? actor = null;
if (args[0] != "all")
{
ICommonSession? session;
if (args.Length > 0)
{
// Get player entity
if (!_playerManager.TryGetSessionByUsername(args[0], out session))
{
shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user"));
return;
}
}
else
{
session = shell.Player;
}
if (session?.AttachedEntity is not { } user)
{
shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user"));
return;
}
if (!TryComp(user, out actor))
{
shell.WriteError(Loc.GetString("cmd-tippy-error-no-user"));
return;
}
}
var ev = new TippyEvent(args[1]);
if (args.Length > 2)
{
ev.Proto = args[2];
if (!_prototype.HasIndex<EntityPrototype>(args[2]))
{
shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2])));
return;
}
}
if (args.Length > 3)
ev.SpeakTime = float.Parse(args[3]);
else else
ev.SpeakTime = GetSpeechTime(ev.Msg); {
_nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound);
if (args.Length > 4) }
ev.SlideTime = float.Parse(args[4]);
if (args.Length > 5)
ev.WaddleInterval = float.Parse(args[5]);
if (actor != null)
RaiseNetworkEvent(ev, actor.PlayerSession);
else
RaiseNetworkEvent(ev);
} }
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
@@ -171,41 +88,30 @@ public sealed class TipsSystem : EntitySystem
} }
} }
private void SetOutOfRound(float value) public override void SendTippy(
string message,
EntProtoId? prototype = null,
float speakTime = 5f,
float slideTime = 3f,
float waddleInterval = 0.5f)
{ {
_tipTimeOutOfRound = value; var ev = new TippyEvent(message, prototype, speakTime, slideTime, waddleInterval);
RaiseNetworkEvent(ev);
} }
private void SetInRound(float value) public override void SendTippy(
ICommonSession session,
string message,
EntProtoId? prototype = null,
float speakTime = 5f,
float slideTime = 3f,
float waddleInterval = 0.5f)
{ {
_tipTimeInRound = value; var ev = new TippyEvent(message, prototype, speakTime, slideTime, waddleInterval);
RaiseNetworkEvent(ev, session);
} }
private void SetEnabled(bool value) public override void AnnounceRandomTip()
{
_tipsEnabled = value;
if (_nextTipTime != TimeSpan.Zero)
RecalculateNextTipTime();
}
private void SetDataset(string value)
{
_tipsDataset = value;
}
private void SetTippyChance(float value)
{
_tipTippyChance = value;
}
public static float GetSpeechTime(string text)
{
var wordCount = (float)text.Split().Length;
return SpeechBuffer + wordCount * (60f / Wpm);
}
private void AnnounceRandomTip()
{ {
if (!_prototype.TryIndex<LocalizedDatasetPrototype>(_tipsDataset, out var tips)) if (!_prototype.TryIndex<LocalizedDatasetPrototype>(_tipsDataset, out var tips))
return; return;
@@ -215,35 +121,20 @@ public sealed class TipsSystem : EntitySystem
if (_random.Prob(_tipTippyChance)) if (_random.Prob(_tipTippyChance))
{ {
var ev = new TippyEvent(msg); var speakTime = GetSpeechTime(msg);
ev.SpeakTime = GetSpeechTime(msg); SendTippy(msg, speakTime: speakTime);
RaiseNetworkEvent(ev);
} else
{
_chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg,
EntityUid.Invalid, false, false, Color.MediumPurple);
}
}
private void RecalculateNextTipTime()
{
if (_ticker.RunLevel == GameRunLevel.InRound)
{
_nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeInRound);
} }
else else
{ {
_nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound); _chat.ChatMessageToManyFiltered(
} Filter.Broadcast(),
} ChatChannel.OOC,
tip,
private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev) msg,
{ EntityUid.Invalid,
// reset for lobby -> inround false,
// reset for inround -> post but not post -> lobby false,
if (ev.New == GameRunLevel.InRound || ev.Old == GameRunLevel.InRound) Color.MediumPurple);
{
RecalculateNextTipTime();
} }
} }
} }

View File

@@ -68,6 +68,7 @@ public sealed partial class GunSystem
protected override void TakeCharge(Entity<BatteryAmmoProviderComponent> entity) protected override void TakeCharge(Entity<BatteryAmmoProviderComponent> entity)
{ {
// Take charge from either the BatteryComponent or PowerCellSlotComponent.
var ev = new ChangeChargeEvent(-entity.Comp.FireCost); var ev = new ChangeChargeEvent(-entity.Comp.FireCost);
RaiseLocalEvent(entity, ref ev); RaiseLocalEvent(entity, ref ev);
} }

View File

@@ -25,7 +25,7 @@ public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryCompo
_lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities); _lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities);
foreach (var battery in _batteryEntities) foreach (var battery in _batteryEntities)
{ {
_battery.SetCharge(battery, battery.Comp.MaxCharge, battery); _battery.SetCharge(battery.AsNullable(), battery.Comp.MaxCharge);
} }
} }
} }

View File

@@ -0,0 +1,101 @@
using Content.Server.Antag;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Silicons.Borgs;
using Content.Shared.Destructible;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Xenoborgs.Components;
using Robust.Shared.Audio;
using Robust.Shared.Player;
namespace Content.Server.Xenoborgs;
public sealed partial class XenoborgSystem : EntitySystem
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly BorgSystem _borg = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly XenoborgsRuleSystem _xenoborgsRule = default!;
private static readonly Color XenoborgBriefingColor = Color.BlueViolet;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<XenoborgComponent, DestructionEventArgs>(OnXenoborgDestroyed);
SubscribeLocalEvent<MothershipCoreComponent, DestructionEventArgs>(OnCoreDestroyed);
SubscribeLocalEvent<XenoborgComponent, MindAddedMessage>(OnXenoborgMindAdded);
SubscribeLocalEvent<XenoborgComponent, MindRemovedMessage>(OnXenoborgMindRemoved);
}
private void OnXenoborgDestroyed(EntityUid uid, XenoborgComponent component, DestructionEventArgs args)
{
// if a xenoborg is destroyed, it will check to see if it was the last one
var xenoborgQuery = AllEntityQuery<XenoborgComponent>(); // paused xenoborgs still count
while (xenoborgQuery.MoveNext(out var xenoborg, out _))
{
if (xenoborg != uid)
return;
}
var mothershipCoreQuery = AllEntityQuery<MothershipCoreComponent>(); // paused mothership cores still count
var mothershipCoreAlive = mothershipCoreQuery.MoveNext(out _, out _);
var xenoborgsRuleQuery = EntityQueryEnumerator<XenoborgsRuleComponent>();
if (xenoborgsRuleQuery.MoveNext(out var xenoborgsRuleEnt, out var xenoborgsRuleComp))
_xenoborgsRule.SendXenoborgDeathAnnouncement((xenoborgsRuleEnt, xenoborgsRuleComp), mothershipCoreAlive);
}
private void OnCoreDestroyed(EntityUid ent, MothershipCoreComponent component, DestructionEventArgs args)
{
// if a mothership core is destroyed, it will see if there are any others
var mothershipCoreQuery = AllEntityQuery<MothershipCoreComponent>(); // paused mothership cores still count
while (mothershipCoreQuery.MoveNext(out var mothershipCoreEnt, out _))
{
// if it finds a mothership core that is different from the one just destroyed,
// it doesn't explode the xenoborgs
if (mothershipCoreEnt != ent)
return;
}
var xenoborgsRuleQuery = EntityQueryEnumerator<XenoborgsRuleComponent>();
if (xenoborgsRuleQuery.MoveNext(out var xenoborgsRuleEnt, out var xenoborgsRuleComp))
_xenoborgsRule.SendMothershipDeathAnnouncement((xenoborgsRuleEnt, xenoborgsRuleComp));
// explode all xenoborgs
var xenoborgQuery = AllEntityQuery<XenoborgComponent, BorgTransponderComponent>(); // paused xenoborgs still explode
while (xenoborgQuery.MoveNext(out var xenoborgEnt, out _, out _))
{
if (HasComp<MothershipCoreComponent>(xenoborgEnt))
continue;
// I got tired to trying to make this work via the device network.
// so brute force it is...
_borg.Destroy(xenoborgEnt);
}
}
private void OnXenoborgMindAdded(EntityUid ent, XenoborgComponent comp, MindAddedMessage args)
{
_roles.MindAddRole(args.Mind, comp.MindRole, silent: true);
if (!TryComp<ActorComponent>(ent, out var actorComp))
return;
_antag.SendBriefing(actorComp.PlayerSession,
Loc.GetString(comp.BriefingText),
XenoborgBriefingColor,
comp.BriefingSound
);
}
private void OnXenoborgMindRemoved(EntityUid ent, XenoborgComponent comp, MindRemovedMessage args)
{
_roles.MindRemoveRole(args.Mind.Owner, comp.MindRole);
}
}

View File

@@ -40,7 +40,8 @@ public ref struct LogStringHandler
format = argument[0] == '@' ? argument[1..] : argument; format = argument[0] == '@' ? argument[1..] : argument;
} }
if (Values.TryAdd(Logger.ConvertName(format), value) format = Logger.ConvertName(format);
if (Values.TryAdd(format, value)
|| Values[format] is T val && val.Equals(value) ) || Values[format] is T val && val.Equals(value) )
{ {
return; return;
@@ -50,7 +51,7 @@ public ref struct LogStringHandler
var i = 2; var i = 2;
format = $"{originalFormat}_{i}"; format = $"{originalFormat}_{i}";
while (!(Values.TryAdd(Logger.ConvertName(format), value) while (!(Values.TryAdd(format, value)
|| Values[format] is T val2 && val2.Equals(value))) || Values[format] is T val2 && val2.Equals(value)))
{ {
format = $"{originalFormat}_{i}"; format = $"{originalFormat}_{i}";

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Configuration;
namespace Content.Shared.CCVar;
public sealed partial class CCVars
{
/// <summary>
/// Component to be inspected using the "Quick Inspect Component" keybind.
/// Set by the "quickinspect" command.
/// </summary>
public static readonly CVarDef<string> DebugQuickInspect =
CVarDef.Create("debug.quick_inspect", "", CVar.CLIENTONLY | CVar.ARCHIVE);
}

View File

@@ -302,7 +302,7 @@ namespace Content.Shared.Chemistry.Components
/// If you only want the volume of a single reagent, use <see cref="GetReagentQuantity"/> /// If you only want the volume of a single reagent, use <see cref="GetReagentQuantity"/>
/// </summary> /// </summary>
[Pure] [Pure]
public FixedPoint2 GetTotalPrototypeQuantity(params string[] prototypes) public FixedPoint2 GetTotalPrototypeQuantity(params ProtoId<ReagentPrototype>[] prototypes)
{ {
var total = FixedPoint2.Zero; var total = FixedPoint2.Zero;
foreach (var (reagent, quantity) in Contents) foreach (var (reagent, quantity) in Contents)
@@ -314,7 +314,7 @@ namespace Content.Shared.Chemistry.Components
return total; return total;
} }
public FixedPoint2 GetTotalPrototypeQuantity(string id) public FixedPoint2 GetTotalPrototypeQuantity(ProtoId<ReagentPrototype> id)
{ {
var total = FixedPoint2.Zero; var total = FixedPoint2.Zero;
foreach (var (reagent, quantity) in Contents) foreach (var (reagent, quantity) in Contents)
@@ -645,7 +645,7 @@ namespace Content.Shared.Chemistry.Components
/// <summary> /// <summary>
/// Splits a solution with only the specified reagent prototypes. /// Splits a solution with only the specified reagent prototypes.
/// </summary> /// </summary>
public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includedPrototypes) public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params ProtoId<ReagentPrototype>[] includedPrototypes)
{ {
// First remove the non-included prototypes // First remove the non-included prototypes
List<ReagentQuantity> excluded = new(); List<ReagentQuantity> excluded = new();
@@ -844,7 +844,7 @@ namespace Content.Shared.Chemistry.Components
ValidateSolution(); ValidateSolution();
} }
public Color GetColorWithout(IPrototypeManager? protoMan, params string[] without) public Color GetColorWithout(IPrototypeManager? protoMan, params ProtoId<ReagentPrototype>[] without)
{ {
if (Volume == FixedPoint2.Zero) if (Volume == FixedPoint2.Zero)
{ {
@@ -887,7 +887,7 @@ namespace Content.Shared.Chemistry.Components
return GetColorWithout(protoMan); return GetColorWithout(protoMan);
} }
public Color GetColorWithOnly(IPrototypeManager? protoMan, params string[] included) public Color GetColorWithOnly(IPrototypeManager? protoMan, params ProtoId<ReagentPrototype>[] included)
{ {
if (Volume == FixedPoint2.Zero) if (Volume == FixedPoint2.Zero)
{ {

View File

@@ -858,7 +858,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
args.PushMarkup(Loc.GetString(entity.Comp.LocPhysicalQuality, args.PushMarkup(Loc.GetString(entity.Comp.LocPhysicalQuality,
("color", colorHex), ("color", colorHex),
("desc", primary.LocalizedPhysicalDescription), ("desc", primary.LocalizedPhysicalDescription),
("chemCount", solution.Contents.Count) )); ("chemCount", solution.Contents.Count)));
// Push the recognizable reagents // Push the recognizable reagents
@@ -1048,7 +1048,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
public bool EnsureSolution( public bool EnsureSolution(
Entity<MetaDataComponent?> entity, Entity<MetaDataComponent?> entity,
string name, string name,
[NotNullWhen(true)]out Solution? solution, [NotNullWhen(true)] out Solution? solution,
FixedPoint2 maxVol = default) FixedPoint2 maxVol = default)
{ {
return EnsureSolution(entity, name, maxVol, null, out _, out solution); return EnsureSolution(entity, name, maxVol, null, out _, out solution);
@@ -1058,7 +1058,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
Entity<MetaDataComponent?> entity, Entity<MetaDataComponent?> entity,
string name, string name,
out bool existed, out bool existed,
[NotNullWhen(true)]out Solution? solution, [NotNullWhen(true)] out Solution? solution,
FixedPoint2 maxVol = default) FixedPoint2 maxVol = default)
{ {
return EnsureSolution(entity, name, maxVol, null, out existed, out solution); return EnsureSolution(entity, name, maxVol, null, out existed, out solution);
@@ -1217,7 +1217,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name }; var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
AddComp(uid, relation); AddComp(uid, relation);
MetaDataSys.SetEntityName(uid, $"solution - {name}"); MetaDataSys.SetEntityName(uid, $"solution - {name}", raiseEvents: false);
ContainerSystem.Insert(uid, container, force: true); ContainerSystem.Insert(uid, container, force: true);
return (uid, solution, relation); return (uid, solution, relation);
@@ -1240,13 +1240,13 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
} }
else else
{ {
dissolvedSol.RemoveReagent(reagent,amtChange); dissolvedSol.RemoveReagent(reagent, amtChange);
} }
UpdateChemicals(dissolvedSolution); UpdateChemicals(dissolvedSolution);
} }
public FixedPoint2 GetReagentQuantityFromConcentration(Entity<SolutionComponent> dissolvedSolution, public FixedPoint2 GetReagentQuantityFromConcentration(Entity<SolutionComponent> dissolvedSolution,
FixedPoint2 volume,float concentration) FixedPoint2 volume, float concentration)
{ {
var dissolvedSol = dissolvedSolution.Comp.Solution; var dissolvedSol = dissolvedSolution.Comp.Solution;
if (volume == 0 if (volume == 0

View File

@@ -221,7 +221,7 @@ namespace Content.Shared.Chemistry.Reagent
if (effect.EntityEffectGuidebookText(prototype, entSys) is not { } description) if (effect.EntityEffectGuidebookText(prototype, entSys) is not { } description)
return null; return null;
var quantity = metabolism == null ? 0f : (double) (effect.MinScale * metabolism); var quantity = metabolism == null ? 0f : (double)(effect.MinScale * metabolism);
return Loc.GetString( return Loc.GetString(
"guidebook-reagent-effect-description", "guidebook-reagent-effect-description",

View File

@@ -8,13 +8,41 @@ namespace Content.Shared.Damage.Systems;
public sealed partial class DamageableSystem public sealed partial class DamageableSystem
{ {
/// <summary> /// <summary>
/// Directly sets the damage specifier of a damageable component. /// Directly sets the damage in a damageable component.
/// This method keeps the damage types supported by the DamageContainerPrototype in the component.
/// If a type is given in <paramref name="damage"/>, but not supported then it will not be set.
/// If a type is supported but not given in <paramref name="damage"/> then it will be set to 0.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed /// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed
/// event is raised. /// event is raised.
/// </remarks> /// </remarks>
public void SetDamage(Entity<DamageableComponent?> ent, DamageSpecifier damage) public void SetDamage(Entity<DamageableComponent?> ent, DamageSpecifier damage)
{
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return;
foreach (var type in ent.Comp.Damage.DamageDict.Keys)
{
if (damage.DamageDict.TryGetValue(type, out var value))
ent.Comp.Damage.DamageDict[type] = value;
else
ent.Comp.Damage.DamageDict[type] = 0;
}
OnEntityDamageChanged((ent, ent.Comp));
}
/// <summary>
/// Directly sets the damage specifier of a damageable component.
/// This will overwrite the complete damage dict, meaning it will bulldoze the supported damage types.
/// </summary>
/// <remarks>
/// This may break persistance as the supported types are reset in case the component is initialized again.
/// So this only makes sense if you also change the DamageContainerPrototype in the component at the same time.
/// Only use this method if you know what you are doing.
/// </remarks>
public void SetDamageSpecifier(Entity<DamageableComponent?> ent, DamageSpecifier damage)
{ {
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false)) if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return; return;

View File

@@ -1,7 +1,9 @@
using System.Linq; using System.Linq;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Content.Shared.Fluids.Components; using Content.Shared.Fluids.Components;
using Content.Shared.Chemistry.Reagent;
namespace Content.Shared.Fluids; namespace Content.Shared.Fluids;
@@ -78,9 +80,9 @@ public abstract partial class SharedPuddleSystem
} }
public string[] GetEvaporatingReagents(Solution solution) public ProtoId<ReagentPrototype>[] GetEvaporatingReagents(Solution solution)
{ {
List<string> evaporatingReagents = []; List<ProtoId<ReagentPrototype>> evaporatingReagents = [];
foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys) foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
{ {
if (solProto.EvaporationSpeed > FixedPoint2.Zero) if (solProto.EvaporationSpeed > FixedPoint2.Zero)
@@ -89,10 +91,10 @@ public abstract partial class SharedPuddleSystem
return evaporatingReagents.ToArray(); return evaporatingReagents.ToArray();
} }
public string[] GetAbsorbentReagents(Solution solution) public ProtoId<ReagentPrototype>[] GetAbsorbentReagents(Solution solution)
{ {
List<string> absorbentReagents = []; var absorbentReagents = new List<ProtoId<ReagentPrototype>>();
foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys) foreach (ReagentPrototype solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
{ {
if (solProto.Absorbent) if (solProto.Absorbent)
absorbentReagents.Add(solProto.ID); absorbentReagents.Add(solProto.ID);
@@ -109,9 +111,9 @@ public abstract partial class SharedPuddleSystem
/// Gets a mapping of evaporating speed of the reagents within a solution. /// Gets a mapping of evaporating speed of the reagents within a solution.
/// The speed at which a solution evaporates is the average of the speed of all evaporating reagents in it. /// The speed at which a solution evaporates is the average of the speed of all evaporating reagents in it.
/// </summary> /// </summary>
public Dictionary<string, FixedPoint2> GetEvaporationSpeeds(Solution solution) public Dictionary<ProtoId<ReagentPrototype>, FixedPoint2> GetEvaporationSpeeds(Solution solution)
{ {
Dictionary<string, FixedPoint2> evaporatingSpeeds = []; Dictionary<ProtoId<ReagentPrototype>, FixedPoint2> evaporatingSpeeds = [];
foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys) foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
{ {
if (solProto.EvaporationSpeed > FixedPoint2.Zero) if (solProto.EvaporationSpeed > FixedPoint2.Zero)

View File

@@ -42,7 +42,7 @@ public abstract partial class SharedPuddleSystem : EntitySystem
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!; [Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
[Dependency] private readonly TileFrictionController _tile = default!; [Dependency] private readonly TileFrictionController _tile = default!;
private string[] _standoutReagents = []; private ProtoId<ReagentPrototype>[] _standoutReagents = [];
/// <summary> /// <summary>
/// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle. /// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle.

View File

@@ -35,6 +35,7 @@ public sealed class FollowerSystem : EntitySystem
[Dependency] private readonly ISharedAdminManager _adminManager = default!; [Dependency] private readonly ISharedAdminManager _adminManager = default!;
private static readonly ProtoId<TagPrototype> ForceableFollowTag = "ForceableFollow"; private static readonly ProtoId<TagPrototype> ForceableFollowTag = "ForceableFollow";
private static readonly ProtoId<TagPrototype> PreventGhostnadoWarpTag = "NotGhostnadoWarpable";
public override void Initialize() public override void Initialize()
{ {
@@ -320,11 +321,17 @@ public sealed class FollowerSystem : EntitySystem
var query = EntityQueryEnumerator<FollowerComponent, GhostComponent, ActorComponent>(); var query = EntityQueryEnumerator<FollowerComponent, GhostComponent, ActorComponent>();
while (query.MoveNext(out _, out var follower, out _, out var actor)) while (query.MoveNext(out _, out var follower, out _, out var actor))
{ {
// Exclude admins // Don't count admin followers so that players cannot notice if admins are in stealth mode and following someone.
if (_adminManager.IsAdmin(actor.PlayerSession)) if (_adminManager.IsAdmin(actor.PlayerSession))
continue; continue;
var followed = follower.Following; var followed = follower.Following;
// If the followed entity cannot be ghostnado'd to, we don't count it.
// Used for making admins not warpable to, but IsAdmin isn't used for cases where the admin wants to be followed, for example during events.
if (_tagSystem.HasTag(followed, PreventGhostnadoWarpTag))
continue;
// Add new entry or increment existing // Add new entry or increment existing
followedEnts.TryGetValue(followed, out var currentValue); followedEnts.TryGetValue(followed, out var currentValue);
followedEnts[followed] = currentValue + 1; followedEnts[followed] = currentValue + 1;

View File

@@ -3,42 +3,45 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Gravity; namespace Content.Shared.Gravity;
[NetworkedComponent()] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Virtual] public sealed partial class GravityGeneratorComponent : Component
public partial class SharedGravityGeneratorComponent : Component
{ {
[DataField] public float LightRadiusMin { get; set; }
[DataField] public float LightRadiusMax { get; set; }
/// <summary> /// <summary>
/// A map of the sprites used by the gravity generator given its status. /// A map of the sprites used by the gravity generator given its status.
/// </summary> /// </summary>
[DataField("spriteMap")] [DataField, Access(typeof(SharedGravitySystem))]
[Access(typeof(SharedGravitySystem))] public Dictionary<PowerChargeStatus, string> SpriteMap = [];
public Dictionary<PowerChargeStatus, string> SpriteMap = new();
/// <summary> /// <summary>
/// The sprite used by the core of the gravity generator when the gravity generator is starting up. /// The sprite used by the core of the gravity generator when the gravity generator is starting up.
/// </summary> /// </summary>
[DataField("coreStartupState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string CoreStartupState = "startup"; public string CoreStartupState = "startup";
/// <summary> /// <summary>
/// The sprite used by the core of the gravity generator when the gravity generator is idle. /// The sprite used by the core of the gravity generator when the gravity generator is idle.
/// </summary> /// </summary>
[DataField("coreIdleState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string CoreIdleState = "idle"; public string CoreIdleState = "idle";
/// <summary> /// <summary>
/// The sprite used by the core of the gravity generator when the gravity generator is activating. /// The sprite used by the core of the gravity generator when the gravity generator is activating.
/// </summary> /// </summary>
[DataField("coreActivatingState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string CoreActivatingState = "activating"; public string CoreActivatingState = "activating";
/// <summary> /// <summary>
/// The sprite used by the core of the gravity generator when the gravity generator is active. /// The sprite used by the core of the gravity generator when the gravity generator is active.
/// </summary> /// </summary>
[DataField("coreActivatedState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string CoreActivatedState = "activated"; public string CoreActivatedState = "activated";
/// <summary>
/// Is the gravity generator currently "producing" gravity?
/// </summary>
[DataField, AutoNetworkedField, Access(typeof(SharedGravityGeneratorSystem))]
public bool GravityActive = false;
} }

View File

@@ -0,0 +1,29 @@
using Content.Shared.Popups;
using Content.Shared.Construction.Components;
namespace Content.Shared.Gravity;
public abstract class SharedGravityGeneratorSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GravityGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
}
/// <summary>
/// Prevent unanchoring when gravity is active
/// </summary>
private void OnUnanchorAttempt(Entity<GravityGeneratorComponent> ent, ref UnanchorAttemptEvent args)
{
if (!ent.Comp.GravityActive)
return;
_popupSystem.PopupClient(Loc.GetString("gravity-generator-unanchoring-failed"), ent.Owner, args.User, PopupType.Medium);
args.Cancel();
}
}

View File

@@ -11,6 +11,7 @@ public abstract partial class SharedHandsSystem
private void InitializeEventListeners() private void InitializeEventListeners()
{ {
SubscribeLocalEvent<HandsComponent, GetStandUpTimeEvent>(OnStandupArgs); SubscribeLocalEvent<HandsComponent, GetStandUpTimeEvent>(OnStandupArgs);
SubscribeLocalEvent<HandsComponent, KnockedDownRefreshEvent>(OnKnockedDownRefresh);
} }
/// <summary> /// <summary>
@@ -28,4 +29,17 @@ public abstract partial class SharedHandsSystem
time.DoAfterTime *= (float)ent.Comp.Count / (hands + ent.Comp.Count); time.DoAfterTime *= (float)ent.Comp.Count / (hands + ent.Comp.Count);
} }
private void OnKnockedDownRefresh(Entity<HandsComponent> ent, ref KnockedDownRefreshEvent args)
{
var freeHands = CountFreeHands(ent.AsNullable());
var totalHands = GetHandCount(ent.AsNullable());
// Can't crawl around without any hands.
// Entities without the HandsComponent will always have full crawling speed.
if (totalHands == 0)
args.SpeedModifier = 0f;
else
args.SpeedModifier *= (float)freeHands / totalHands;
}
} }

View File

@@ -123,7 +123,8 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction EditorCopyObject = "EditorCopyObject"; public static readonly BoundKeyFunction EditorCopyObject = "EditorCopyObject";
public static readonly BoundKeyFunction EditorFlipObject = "EditorFlipObject"; public static readonly BoundKeyFunction EditorFlipObject = "EditorFlipObject";
public static readonly BoundKeyFunction InspectEntity = "InspectEntity"; public static readonly BoundKeyFunction InspectEntity = "InspectEntity";
public static readonly BoundKeyFunction InspectServerComponent = "InspectServerComponent";
public static readonly BoundKeyFunction InspectClientComponent = "InspectClientComponent";
public static readonly BoundKeyFunction MappingUnselect = "MappingUnselect"; public static readonly BoundKeyFunction MappingUnselect = "MappingUnselect";
public static readonly BoundKeyFunction SaveMap = "SaveMap"; public static readonly BoundKeyFunction SaveMap = "SaveMap";
public static readonly BoundKeyFunction MappingEnablePick = "MappingEnablePick"; public static readonly BoundKeyFunction MappingEnablePick = "MappingEnablePick";

View File

@@ -119,7 +119,7 @@ public sealed class LockSystem : EntitySystem
if (!lockComp.ShowExamine) if (!lockComp.ShowExamine)
return; return;
args.PushText(Loc.GetString(lockComp.Locked args.PushMarkup(Loc.GetString(lockComp.Locked
? "lock-comp-on-examined-is-locked" ? "lock-comp-on-examined-is-locked"
: "lock-comp-on-examined-is-unlocked", : "lock-comp-on-examined-is-unlocked",
("entityName", Identity.Name(uid, EntityManager)))); ("entityName", Identity.Name(uid, EntityManager))));

View File

@@ -6,6 +6,7 @@ using Content.Shared.Charges.Systems;
using Content.Shared.Coordinates.Helpers; using Content.Shared.Coordinates.Helpers;
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems; using Content.Shared.Doors.Systems;
using Content.Shared.Examine;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction; using Content.Shared.Interaction;
@@ -66,6 +67,7 @@ public abstract class SharedMagicSystem : EntitySystem
[Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly TurfSystem _turf = default!; [Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!; [Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly ExamineSystemShared _examine= default!;
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell"; private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
@@ -399,22 +401,30 @@ public abstract class SharedMagicSystem : EntitySystem
#endregion #endregion
#region Knock Spells #region Knock Spells
/// <summary> /// <summary>
/// Opens all doors and locks within range /// Opens all doors and locks within range.
/// </summary> /// </summary>
/// <param name="args"></param>
private void OnKnockSpell(KnockSpellEvent args) private void OnKnockSpell(KnockSpellEvent args)
{ {
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer)) if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
return; return;
args.Handled = true; args.Handled = true;
Knock(args.Performer, args.Range);
}
var transform = Transform(args.Performer); /// <summary>
/// Opens all doors and locks within range.
/// </summary>
/// <param name="performer">Performer of spell. </param>
/// <param name="range">Radius around <see cref="performer"/> in which all doors and locks should be opened.</param>
public void Knock(EntityUid performer, float range)
{
var transform = Transform(performer);
// Look for doors and lockers, and don't open/unlock them if they're already opened/unlocked. // Look for doors and lockers, and don't open/unlock them if they're already opened/unlocked.
foreach (var target in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(args.Performer, transform), args.Range, flags: LookupFlags.Dynamic | LookupFlags.Static)) foreach (var target in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(performer, transform), range, flags: LookupFlags.Dynamic | LookupFlags.Static))
{ {
if (!_interaction.InRangeUnobstructed(args.Performer, target, range: 0, collisionMask: CollisionGroup.Opaque)) if (!_examine.InRangeUnOccluded(performer, target, range: 0))
continue; continue;
if (TryComp<DoorBoltComponent>(target, out var doorBoltComp) && doorBoltComp.BoltsDown) if (TryComp<DoorBoltComponent>(target, out var doorBoltComp) && doorBoltComp.BoltsDown)
@@ -424,7 +434,7 @@ public abstract class SharedMagicSystem : EntitySystem
_door.StartOpening(target); _door.StartOpening(target);
if (TryComp<LockComponent>(target, out var lockComp) && lockComp.Locked) if (TryComp<LockComponent>(target, out var lockComp) && lockComp.Locked)
_lock.Unlock(target, args.Performer, lockComp); _lock.Unlock(target, performer, lockComp);
} }
} }
// End Knock Spells // End Knock Spells

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -43,6 +44,8 @@ public abstract partial class SharedMindSystem : EntitySystem
private HashSet<Entity<MindComponent>> _pickingMinds = new(); private HashSet<Entity<MindComponent>> _pickingMinds = new();
private readonly EntProtoId _mindProto = "MindBase";
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -226,7 +229,7 @@ public abstract partial class SharedMindSystem : EntitySystem
public Entity<MindComponent> CreateMind(NetUserId? userId, string? name = null) public Entity<MindComponent> CreateMind(NetUserId? userId, string? name = null)
{ {
var mindId = Spawn(null, MapCoordinates.Nullspace); var mindId = Spawn(_mindProto, MapCoordinates.Nullspace);
_metadata.SetEntityName(mindId, name == null ? "mind" : $"mind ({name})"); _metadata.SetEntityName(mindId, name == null ? "mind" : $"mind ({name})");
var mind = EnsureComp<MindComponent>(mindId); var mind = EnsureComp<MindComponent>(mindId);
mind.CharacterName = name; mind.CharacterName = name;

View File

@@ -1,4 +1,4 @@
using System.Numerics; using System.Numerics;
using Content.Shared.Conveyor; using Content.Shared.Conveyor;
using Content.Shared.Gravity; using Content.Shared.Gravity;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
@@ -58,6 +58,9 @@ public abstract class SharedConveyorController : VirtualController
private void OnConveyedFriction(Entity<ConveyedComponent> ent, ref TileFrictionEvent args) private void OnConveyedFriction(Entity<ConveyedComponent> ent, ref TileFrictionEvent args)
{ {
if(!ent.Comp.Conveying)
return;
// Conveyed entities don't get friction, they just get wishdir applied so will inherently slowdown anyway. // Conveyed entities don't get friction, they just get wishdir applied so will inherently slowdown anyway.
args.Modifier = 0f; args.Modifier = 0f;
} }
@@ -140,7 +143,15 @@ public abstract class SharedConveyorController : VirtualController
continue; continue;
var physics = ent.Entity.Comp3; var physics = ent.Entity.Comp3;
if (physics.BodyStatus != BodyStatus.OnGround)
{
SetConveying(ent.Entity.Owner, ent.Entity.Comp1, false);
continue;
}
var velocity = physics.LinearVelocity; var velocity = physics.LinearVelocity;
var angularVelocity = physics.AngularVelocity;
var targetDir = ent.Direction; var targetDir = ent.Direction;
// If mob is moving with the conveyor then combine the directions. // If mob is moving with the conveyor then combine the directions.
@@ -163,6 +174,7 @@ public abstract class SharedConveyorController : VirtualController
// We provide a small minimum friction speed as well for those times where the friction would stop large objects // We provide a small minimum friction speed as well for those times where the friction would stop large objects
// snagged on corners from sliding into the centerline. // snagged on corners from sliding into the centerline.
_mover.Friction(0.2f, frameTime: frameTime, friction: 5f, ref velocity); _mover.Friction(0.2f, frameTime: frameTime, friction: 5f, ref velocity);
_mover.Friction(0f, frameTime: frameTime, friction: 5f, ref angularVelocity);
} }
SharedMoverController.Accelerate(ref velocity, targetDir, 20f, frameTime); SharedMoverController.Accelerate(ref velocity, targetDir, 20f, frameTime);
@@ -172,8 +184,10 @@ public abstract class SharedConveyorController : VirtualController
// Need friction to outweigh the movement as it will bounce a bit against the wall. // Need friction to outweigh the movement as it will bounce a bit against the wall.
// This facilitates being able to sleep entities colliding into walls. // This facilitates being able to sleep entities colliding into walls.
_mover.Friction(0f, frameTime: frameTime, friction: 40f, ref velocity); _mover.Friction(0f, frameTime: frameTime, friction: 40f, ref velocity);
_mover.Friction(0f, frameTime: frameTime, friction: 40f, ref angularVelocity);
} }
PhysicsSystem.SetAngularVelocity(ent.Entity.Owner, angularVelocity);
PhysicsSystem.SetLinearVelocity(ent.Entity.Owner, velocity, wakeBody: false); PhysicsSystem.SetLinearVelocity(ent.Entity.Owner, velocity, wakeBody: false);
if (!IsConveyed((ent.Entity.Owner, ent.Entity.Comp2))) if (!IsConveyed((ent.Entity.Owner, ent.Entity.Comp2)))

View File

@@ -1,3 +1,6 @@
using Content.Shared.Power.Components;
using Content.Shared.PowerCell.Components;
namespace Content.Shared.Power; namespace Content.Shared.Power;
/// <summary> /// <summary>
@@ -7,27 +10,36 @@ namespace Content.Shared.Power;
public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge); public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
/// <summary> /// <summary>
/// Event that supports multiple battery types.
/// Raised when it is necessary to get information about battery charges. /// Raised when it is necessary to get information about battery charges.
/// Works with either <see cref="BatteryComponent"/> or <see cref="PowerCellSlotComponent"/>.
/// If there are multiple batteries then the results will be summed up.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public sealed class GetChargeEvent : EntityEventArgs public record struct GetChargeEvent
{ {
public float CurrentCharge; public float CurrentCharge;
public float MaxCharge; public float MaxCharge;
} }
/// <summary> /// <summary>
/// Raised when it is necessary to change the current battery charge to a some value. /// Method event that supports multiple battery types.
/// Raised when it is necessary to change the current battery charge by some value.
/// Works with either <see cref="BatteryComponent"/> or <see cref="PowerCellSlotComponent"/>.
/// If there are multiple batteries then they will be changed in order of subscription until the total value was reached.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public sealed class ChangeChargeEvent : EntityEventArgs public record struct ChangeChargeEvent(float Amount)
{ {
public float OriginalValue; /// <summary>
public float ResidualValue; /// The total amount of charge to change the battery's storage by (in joule).
/// A positive value adds charge, a negative value removes charge.
/// </summary>
public readonly float Amount = Amount;
public ChangeChargeEvent(float value) /// <summary>
{ /// The amount of charge that still has to be removed.
OriginalValue = value; /// For cases where there are multiple batteries.
ResidualValue = value; /// </summary>
} public float ResidualValue = Amount;
} }

View File

@@ -11,10 +11,8 @@ namespace Content.Shared.Power.Components;
[Access(typeof(SharedBatterySystem))] [Access(typeof(SharedBatterySystem))]
public partial class BatteryComponent : Component public partial class BatteryComponent : Component
{ {
public string SolutionName = "battery";
/// <summary> /// <summary>
/// Maximum charge of the battery in joules (ie. watt seconds) /// Maximum charge of the battery in joules (i.e. watt seconds)
/// </summary> /// </summary>
[DataField] [DataField]
[GuidebookData] [GuidebookData]
@@ -23,11 +21,11 @@ public partial class BatteryComponent : Component
/// <summary> /// <summary>
/// Current charge of the battery in joules (ie. watt seconds) /// Current charge of the battery in joules (ie. watt seconds)
/// </summary> /// </summary>
[DataField("startingCharge")] [DataField("startingCharge")] // TODO: rename this datafield to currentCharge
public float CurrentCharge; public float CurrentCharge;
/// <summary> /// <summary>
/// The price per one joule. Default is 1 credit for 10kJ. /// The price per one joule. Default is 1 speso for 10kJ.
/// </summary> /// </summary>
[DataField] [DataField]
public float PricePerJoule = 0.0001f; public float PricePerJoule = 0.0001f;

View File

@@ -0,0 +1,36 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Power.Components;
/// <summary>
/// Self-recharging battery.
/// To be used in combination with <see cref="BatteryComponent"/>.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class BatterySelfRechargerComponent : Component
{
/// <summary>
/// Is the component currently enabled?
/// </summary>
[DataField]
public bool AutoRecharge = true;
/// <summary>
/// At what rate does the entity automatically recharge?
/// </summary>
[DataField]
public float AutoRechargeRate;
/// <summary>
/// How long should the entity stop automatically recharging if charge is used?
/// </summary>
[DataField]
public TimeSpan AutoRechargePauseTime = TimeSpan.FromSeconds(0);
/// <summary>
/// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextAutoRecharge = TimeSpan.FromSeconds(0);
}

View File

@@ -12,33 +12,61 @@ public abstract class SharedBatterySystem : EntitySystem
SubscribeLocalEvent<BatteryComponent, EmpPulseEvent>(OnEmpPulse); SubscribeLocalEvent<BatteryComponent, EmpPulseEvent>(OnEmpPulse);
} }
private void OnEmpPulse(Entity<BatteryComponent> entity, ref EmpPulseEvent args) private void OnEmpPulse(Entity<BatteryComponent> ent, ref EmpPulseEvent args)
{ {
args.Affected = true; args.Affected = true;
UseCharge(entity, args.EnergyConsumption, entity.Comp); UseCharge(ent.AsNullable(), args.EnergyConsumption);
// Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP. // Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP.
TrySetChargeCooldown(entity); TrySetChargeCooldown(ent.Owner);
} }
public virtual float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null) /// <summary>
{ /// Changes the battery's charge by the given amount.
return 0f; /// A positive value will add charge, a negative value will remove charge.
} /// </summary>
/// <returns>The actually changed amount.</returns>
public virtual void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null) { } public virtual float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
public virtual float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{ {
return 0f; return 0f;
} }
/// <summary> /// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable. /// Removes the given amount of charge from the battery.
/// </summary> /// </summary>
public virtual void TrySetChargeCooldown(EntityUid uid, float value = -1) { } /// <returns>The actually changed amount.</returns>
public virtual float UseCharge(Entity<BatteryComponent?> ent, float amount)
{
return 0f;
}
public virtual bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null) /// <summary>
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
/// Always returns false on the client.
/// </summary>
/// <returns>If the full amount was able to be removed.</returns>
public virtual bool TryUseCharge(Entity<BatteryComponent?> ent, float amount)
{ {
return false; return false;
} }
/// <summary>
/// Sets the battery's charge.
/// </summary>
public virtual void SetCharge(Entity<BatteryComponent?> ent, float value) { }
/// <summary>
/// Sets the battery's maximum charge.
/// </summary>
public virtual void SetMaxCharge(Entity<BatteryComponent?> ent, float value) { }
/// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable.
/// Uses the cooldown time given in the component.
/// </summary>
public virtual void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent) { }
/// <summary>
/// Puts the entity's self recharge on cooldown for the specified time.
/// </summary>
public virtual void SetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent, TimeSpan cooldown) { }
} }

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Roles.Components;
/// <summary>
/// Added to mind role entities to tag that they are a xenoborg.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class XenoborgRoleComponent : Component;

View File

@@ -78,6 +78,12 @@ public sealed partial class BorgChassisComponent : Component
[DataField] [DataField]
public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone"; public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
/// <summary>
/// If the entity can open own UI.
/// </summary>
[DataField]
public bool CanOpenSelfUi;
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Silicons.Borgs.Components; namespace Content.Shared.Silicons.Borgs.Components;
@@ -24,6 +25,13 @@ public sealed partial class BorgModuleComponent : Component
[DataField] [DataField]
[AutoNetworkedField] [AutoNetworkedField]
public bool DefaultModule; public bool DefaultModule;
/// <summary>
/// List of types of borgs this module fits into.
/// This only affects examine text. The actual whitelist for modules that can be inserted into a borg is defined in its <see cref="BorgChassisComponent"/>.
/// </summary>
[DataField]
public HashSet<LocId>? BorgFitTypes;
} }
/// <summary> /// <summary>

View File

@@ -1,6 +1,8 @@
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Item.ItemToggle; using Content.Shared.Item.ItemToggle;
using Content.Shared.Localizations;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
@@ -35,10 +37,30 @@ public abstract partial class SharedBorgSystem : EntitySystem
SubscribeLocalEvent<BorgChassisComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers); SubscribeLocalEvent<BorgChassisComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
SubscribeLocalEvent<BorgChassisComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt); SubscribeLocalEvent<BorgChassisComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo); SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
SubscribeLocalEvent<BorgModuleComponent, ExaminedEvent>(OnModuleExamine);
InitializeRelay(); InitializeRelay();
} }
private void OnModuleExamine(Entity<BorgModuleComponent> ent, ref ExaminedEvent args)
{
if (ent.Comp.BorgFitTypes == null)
return;
if (ent.Comp.BorgFitTypes.Count == 0)
return;
var typeList = new List<string>();
foreach (var type in ent.Comp.BorgFitTypes)
{
typeList.Add(Loc.GetString(type));
}
var types = ContentLocalizationManager.FormatList(typeList);
args.PushMarkup(Loc.GetString("borg-module-fit", ("types", types)));
}
private void OnTryGetIdentityShortInfo(TryGetIdentityShortInfoEvent args) private void OnTryGetIdentityShortInfo(TryGetIdentityShortInfoEvent args)
{ {
if (args.Handled) if (args.Handled)
@@ -98,8 +120,8 @@ public abstract partial class SharedBorgSystem : EntitySystem
private void OnUIOpenAttempt(EntityUid uid, BorgChassisComponent component, ActivatableUIOpenAttemptEvent args) private void OnUIOpenAttempt(EntityUid uid, BorgChassisComponent component, ActivatableUIOpenAttemptEvent args)
{ {
// borgs can't view their own ui // borgs generaly can't view their own ui
if (args.User == uid) if (args.User == uid && !component.CanOpenSelfUi)
args.Cancel(); args.Cancel();
} }

View File

@@ -1,12 +1,12 @@
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Buckle.Components; using Content.Shared.Buckle.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems; using Content.Shared.Damage.Systems;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Gravity; using Content.Shared.Gravity;
using Content.Shared.Hands;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Movement.Events; using Content.Shared.Movement.Events;
@@ -54,7 +54,7 @@ public abstract partial class SharedStunSystem
SubscribeLocalEvent<KnockedDownComponent, BuckleAttemptEvent>(OnBuckleAttempt); SubscribeLocalEvent<KnockedDownComponent, BuckleAttemptEvent>(OnBuckleAttempt);
SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt); SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt);
// Updating movement a friction // Updating movement and friction
SubscribeLocalEvent<KnockedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshKnockedSpeed); SubscribeLocalEvent<KnockedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshKnockedSpeed);
SubscribeLocalEvent<KnockedDownComponent, RefreshFrictionModifiersEvent>(OnRefreshFriction); SubscribeLocalEvent<KnockedDownComponent, RefreshFrictionModifiersEvent>(OnRefreshFriction);
SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction); SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
@@ -66,6 +66,9 @@ public abstract partial class SharedStunSystem
SubscribeLocalEvent<CrawlerComponent, KnockedDownRefreshEvent>(OnKnockdownRefresh); SubscribeLocalEvent<CrawlerComponent, KnockedDownRefreshEvent>(OnKnockdownRefresh);
SubscribeLocalEvent<CrawlerComponent, DamageChangedEvent>(OnDamaged); SubscribeLocalEvent<CrawlerComponent, DamageChangedEvent>(OnDamaged);
SubscribeLocalEvent<KnockedDownComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged); SubscribeLocalEvent<KnockedDownComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
SubscribeLocalEvent<KnockedDownComponent, DidEquipHandEvent>(OnHandEquipped);
SubscribeLocalEvent<KnockedDownComponent, DidUnequipHandEvent>(OnHandUnequipped);
SubscribeLocalEvent<KnockedDownComponent, HandCountChangedEvent>(OnHandCountChanged);
SubscribeLocalEvent<GravityAffectedComponent, KnockDownAttemptEvent>(OnKnockdownAttempt); SubscribeLocalEvent<GravityAffectedComponent, KnockDownAttemptEvent>(OnKnockdownAttempt);
SubscribeLocalEvent<GravityAffectedComponent, GetStandUpTimeEvent>(OnGetStandUpTime); SubscribeLocalEvent<GravityAffectedComponent, GetStandUpTimeEvent>(OnGetStandUpTime);
@@ -380,7 +383,7 @@ public abstract partial class SharedStunSystem
private void OnForceStandup(ForceStandUpEvent msg, EntitySessionEventArgs args) private void OnForceStandup(ForceStandUpEvent msg, EntitySessionEventArgs args)
{ {
if (args.SenderSession.AttachedEntity is not {} user) if (args.SenderSession.AttachedEntity is not { } user)
return; return;
ForceStandUp(user); ForceStandUp(user);
@@ -522,6 +525,30 @@ public abstract partial class SharedStunSystem
RemCompDeferred<KnockedDownComponent>(entity); RemCompDeferred<KnockedDownComponent>(entity);
} }
private void OnHandEquipped(Entity<KnockedDownComponent> entity, ref DidEquipHandEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnHandUnequipped(Entity<KnockedDownComponent> entity, ref DidUnequipHandEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnHandCountChanged(Entity<KnockedDownComponent> entity, ref HandCountChangedEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnKnockdownAttempt(Entity<GravityAffectedComponent> entity, ref KnockDownAttemptEvent args) private void OnKnockdownAttempt(Entity<GravityAffectedComponent> entity, ref KnockDownAttemptEvent args)
{ {
// Directed, targeted moth attack. // Directed, targeted moth attack.
@@ -582,6 +609,7 @@ public abstract partial class SharedStunSystem
ent.Comp.SpeedModifier = ev.SpeedModifier; ent.Comp.SpeedModifier = ev.SpeedModifier;
ent.Comp.FrictionModifier = ev.FrictionModifier; ent.Comp.FrictionModifier = ev.FrictionModifier;
Dirty(ent);
_movementSpeedModifier.RefreshMovementSpeedModifiers(ent); _movementSpeedModifier.RefreshMovementSpeedModifiers(ent);
_movementSpeedModifier.RefreshFrictionModifiers(ent); _movementSpeedModifier.RefreshFrictionModifiers(ent);

View File

@@ -0,0 +1,74 @@
using Content.Shared.CCVar;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Shared.Tips;
/// <summary>
/// Handles periodically displaying gameplay tips to all players ingame.
/// </summary>
public abstract class SharedTipsSystem : EntitySystem
{
/// <summary>
/// Always adds this time to a speech message. This is so really short message stay around for a bit.
/// </summary>
private const float SpeechBuffer = 3f;
/// <summary>
/// Expected reading speed.
/// </summary>
private const float Wpm = 180f;
/// <summary>
/// Send a tippy message to all clients.
/// </summary>
/// <param name="message">The text to show in the speech bubble.</param>
/// <param name="prototype">The entity to show. Defaults to tippy.</param>
/// <param name="speakTime">The time the speech bubble is shown, in seconds.</param>
/// <param name="slideTime">The time the entity takes to walk onto the screen, in seconds.</param>
/// <param name="waddleInterval">The time between waddle animation steps, in seconds.</param>
public virtual void SendTippy(
string message,
EntProtoId? prototype = null,
float speakTime = 5f,
float slideTime = 3f,
float waddleInterval = 0.5f)
{ }
/// <summary>
/// Send a tippy message to the given player session.
/// </summary>
/// <param name="session">The player session to send the message to.</param>
/// <param name="message">The text to show in the speech bubble.</param>
/// <param name="prototype">The entity to show. Defaults to tippy.</param>
/// <param name="speakTime">The time the speech bubble is shown, in seconds.</param>
/// <param name="slideTime">The time the entity takes to walk onto the screen, in seconds.</param>
/// <param name="waddleInterval">The time between waddle animation steps, in seconds.</param>
public virtual void SendTippy(
ICommonSession session,
string message,
EntProtoId? prototype = null,
float speakTime = 5f,
float slideTime = 3f,
float waddleInterval = 0.5f)
{ }
/// <summary>
/// Send a random tippy message from the dataset given in <see cref="CCVars.TipsDataset"/>.
/// </summary>
public virtual void AnnounceRandomTip() { }
/// <summary>
/// Set a random time stamp for the next automatic game tip.
/// </summary>
public virtual void RecalculateNextTipTime() { }
/// <summary>
/// Calculate the recommended speak time for a given message.
/// </summary>
public float GetSpeechTime(string text)
{
var wordCount = (float)text.Split().Length;
return SpeechBuffer + wordCount * (60f / Wpm);
}
}

View File

@@ -1,21 +1,37 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Tips; namespace Content.Shared.Tips;
/// <summary>
/// Networked event that makes a client show a message on their screen using tippy or another protoype.
/// </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class TippyEvent : EntityEventArgs public sealed class TippyEvent(string msg, EntProtoId? proto, float speakTime, float slideTime, float waddleInterval) : EntityEventArgs
{ {
public TippyEvent(string msg) /// <summary>
{ /// The text to show in the speech bubble.
Msg = msg; /// </summary>
} public string Msg = msg;
public string Msg; /// <summary>
public string? Proto; /// The entity to show. Defaults to tippy.
/// </summary>
public EntProtoId? Proto = proto;
// TODO: Why are these defaults even here, have the caller specify. This get overriden only most of the time. /// <summary>
public float SpeakTime = 5; /// The time the speech bubble is shown, in seconds.
public float SlideTime = 3; /// </summary>
public float WaddleInterval = 0.5f; public float SpeakTime = speakTime;
/// <summary>
/// The time the entity takes to walk onto the screen, in seconds.
/// </summary>
public float SlideTime = slideTime;
/// <summary>
/// The time between waddle animation steps, in seconds.
/// </summary>
public float WaddleInterval = waddleInterval;
} }

View File

@@ -0,0 +1,20 @@
using Content.Shared.Tag;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Adds the given tags when triggered.
/// If TargetUser is true the tags will be added to the user instead.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AddTagsOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// The tags to add.
/// </summary>
[DataField, AutoNetworkedField]
public List<ProtoId<TagPrototype>> Tags = new();
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Database;
using Content.Shared.Random;
using Content.Shared.Trigger.Components.Triggers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// This component creates an admin log when receiving a trigger.
/// <see cref="BaseXOnTriggerComponent.TargetUser"/> is ignored.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AdminLogOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// The message displayed in the logs describing what specifically was done by this trigger.
/// This entity and the user will be included alongside the message.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public LocId Message = string.Empty;
/// <summary>
/// What type of action took place?
/// </summary>
[DataField, AutoNetworkedField]
public LogType LogType = LogType.Trigger;
/// <summary>
/// How important is this trigger?
/// </summary>
[DataField, AutoNetworkedField]
public LogImpact LogImpact = LogImpact.Low;
}

View File

@@ -0,0 +1,22 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Trigger effect for removing and *deleting* all items in container(s) on the target.
/// </summary>
/// <remarks>
/// Be very careful when setting <see cref="BaseXOnTriggerComponent.TargetUser"/> to true or all your organs might fall out.
/// In fact, never set it to true.
/// </remarks>
/// <seealso cref="EmptyContainersOnTriggerComponent"/>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class CleanContainersOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// Names of containers to empty.
/// If null, all containers will be emptied.
/// </summary>
[DataField, AutoNetworkedField]
public List<string>? Container;
}

View File

@@ -0,0 +1,22 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Trigger effect for removing all items in container(s) on the target.
/// </summary>
/// <remarks>
/// Be very careful when setting <see cref="BaseXOnTriggerComponent.TargetUser"/> to true or all your organs might fall out.
/// In fact, never set it to true.
/// </remarks>
/// <seealso cref="CleanContainersOnTriggerComponent"/>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class EmptyContainersOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// Names of containers to empty.
/// If null, all containers will be emptied.
/// </summary>
[DataField, AutoNetworkedField]
public List<string>? Container;
}

View File

@@ -0,0 +1,47 @@
using Content.Shared.StatusEffect;
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Makes the entity play a jitter animation when triggered.
/// If TargetUser is true the user will jitter instead.
/// </summary>
/// <summary>
/// The target requires <see cref="StatusEffectsComponent"/>.
/// TODO: Convert jitter to the new status effects system.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class JitterOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// Jitteriness of the animation.
/// </summary>
[DataField, AutoNetworkedField]
public float Amplitude = 10.0f;
/// <summary>
/// Frequency for jittering.
/// </summary>
[DataField, AutoNetworkedField]
public float Frequency = 4.0f;
/// <summary>
/// For how much time to apply the effect.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan Time = TimeSpan.FromSeconds(2);
/// <summary>
/// The status effect cooldown should be refreshed (true) or accumulated (false).
/// </summary>
[DataField, AutoNetworkedField]
public bool Refresh;
/// <summary>
/// Whether to change any existing jitter value even if they're greater than the ones we're setting.
/// </summary>
[DataField, AutoNetworkedField]
public bool ForceValueChange;
}

View File

@@ -0,0 +1,36 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Trigger effect for sending the target sidewise (crawling).
/// Knockdowns the user if <see cref="BaseXOnTriggerComponent.TargetUser"/> is true.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class KnockdownOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// How long the target is forced to be on the ground.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan KnockdownAmount = TimeSpan.FromSeconds(1);
/// <summary>
/// If true, refresh the duration.
/// If false, time is added on-top of any existing forced knockdown.
/// </summary>
[DataField, AutoNetworkedField]
public bool Refresh = true;
/// <summary>
/// Should the entity try and stand automatically?
/// </summary>
[DataField, AutoNetworkedField]
public bool AutoStand = true;
/// <summary>
/// Should the entity drop their items upon first being knocked down?
/// </summary>
[DataField, AutoNetworkedField]
public bool Drop = true;
}

View File

@@ -0,0 +1,21 @@
using Content.Shared.Random;
using Content.Shared.Trigger.Components.Triggers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// When triggered this component will choose a key and send a new trigger.
/// Trigger is sent to user if <see cref="BaseXOnTriggerComponent.TargetUser"/> is true.
/// </summary>
/// <remarks>Does not support recursive loops where this component triggers itself. Use <see cref="RepeatingTriggerComponent"/> instead.</remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class RandomTriggerOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// The trigger keys and their weights.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public ProtoId<WeightedRandomPrototype> RandomKeyOut;
}

View File

@@ -0,0 +1,20 @@
using Content.Shared.Tag;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Remove the given tags when triggered.
/// If TargetUser is true the tags will be added to the user instead.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class RemoveTagsOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// The tags to remove.
/// </summary>
[DataField, AutoNetworkedField]
public List<ProtoId<TagPrototype>> Tags = new();
}

View File

@@ -0,0 +1,24 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Trigger effect for stunning an entity.
/// Stuns the user if <see cref="BaseXOnTriggerComponent.TargetUser"/> is true.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class StunOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// How long to stun the target.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan StunAmount = TimeSpan.FromSeconds(1);
/// <summary>
/// If true, refresh the stun duration.
/// If false, stun is added on-top of any existing stun.
/// </summary>
[DataField, AutoNetworkedField]
public bool Refresh = true;
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Swaps the location of the target and the user of the trigger when triggered.
/// <see cref="BaseXOnTriggerComponent.TargetUser"/> is ignored.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class SwapLocationOnTriggerComponent : BaseXOnTriggerComponent;

View File

@@ -0,0 +1,66 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Sends a tippy message to either the entity or all players when triggered.
/// If TargetUser is true the user will receive the message.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class TippyOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// Unlocalized message text to send to the player(s).
/// Intended only for admeme purposes. For anything else you should use <see cref="LocMessage"/> instead.
/// </summary>
[DataField, AutoNetworkedField]
public string Message = string.Empty;
/// <summary>
/// Localized message text to send to the player(s).
/// This has priority over <see cref="Message"/>.
/// </summary>
[DataField, AutoNetworkedField]
public LocId? LocMessage;
/// <summary>
/// If true the message will be send to all players.
/// If false it will be send to the user or owning entity, depending on <see cref="BaseXOnTriggerComponent.TargetUser"/>.
/// </summary>
[DataField, AutoNetworkedField]
public bool SendToAll;
/// <summary>
/// The entity prototype to show to the client.
/// Will default to tippy if null.
/// </summary>
[DataField, AutoNetworkedField]
public EntProtoId? Prototype;
/// <summary>
/// Use the prototype of the entity owning this component?
/// Will take priority over <see cref="Prototype"/>.
/// </summary>
[DataField, AutoNetworkedField]
public bool UseOwnerPrototype;
/// <summary>
/// The time the speech bubble is shown, in seconds.
/// Will be calculated automatically from the message length if null.
/// </summary>
[DataField, AutoNetworkedField]
public float? SpeakTime;
/// <summary>
/// The time the entity takes to walk onto the screen, in seconds.
/// </summary>
[DataField, AutoNetworkedField]
public float SlideTime = 3f;
/// <summary>
/// The time between waddle animation steps, in seconds.
/// </summary>
[DataField, AutoNetworkedField]
public float WaddleInterval = 0.5f;
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Triggers;
/// <summary>
/// Trigger for when this entity is thrown and then hits a second entity.
/// User is the entity hit.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class TriggerOnThrowDoHitComponent : BaseTriggerOnXComponent;

View File

@@ -0,0 +1,19 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Trigger.Components.Effects;
namespace Content.Shared.Trigger.Systems;
public sealed class AdminLogOnTriggerSystem : XOnTriggerSystem<AdminLogOnTriggerComponent>
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
protected override void OnTrigger(Entity<AdminLogOnTriggerComponent> ent, EntityUid target, ref TriggerEvent args)
{
_adminLogger.Add(
ent.Comp.LogType,
ent.Comp.LogImpact,
$"{ToPrettyString(args.User)} sent a trigger using {ToPrettyString(ent)}: {Loc.GetString(ent.Comp.Message)}"
);
// Intentionally does not handle the event since this shouldn't affect the gamestate.
}
}

View File

@@ -0,0 +1,81 @@
using Content.Shared.Trigger.Components.Effects;
using Robust.Shared.Containers;
namespace Content.Shared.Trigger.Systems;
/// <summary>
/// Empty containers trigger system.
/// </summary>
public sealed class EmptyContainersOnTriggerSystem : XOnTriggerSystem<EmptyContainersOnTriggerComponent>
{
[Dependency] private readonly SharedContainerSystem _container = default!;
protected override void OnTrigger(Entity<EmptyContainersOnTriggerComponent> ent, EntityUid target, ref TriggerEvent args)
{
if (!TryComp<ContainerManagerComponent>(target, out var containerComp))
return;
// Empty everything. Make sure a player isn't the target because they will get removed from their body along with their organs
if (ent.Comp.Container is null)
{
foreach (var container in _container.GetAllContainers(target, containerComp))
{
_container.EmptyContainer(container);
}
args.Handled = true;
}
// Empty containers in a sane way
else
{
foreach (var containerId in ent.Comp.Container)
{
if (!_container.TryGetContainer(target, containerId, out var container, containerComp))
continue;
_container.EmptyContainer(container);
args.Handled = true;
}
}
}
}
/// <summary>
/// Empty containers and delete items trigger system.
/// </summary>
public sealed class CleanContainersOnTriggerSystem : XOnTriggerSystem<CleanContainersOnTriggerComponent>
{
[Dependency] private readonly SharedContainerSystem _container = default!;
protected override void OnTrigger(Entity<CleanContainersOnTriggerComponent> ent, EntityUid target, ref TriggerEvent args)
{
if (!TryComp<ContainerManagerComponent>(target, out var containerComp))
return;
// Empty everything. Make sure a player isn't the target because they will get DELETED
if (ent.Comp.Container is null)
{
foreach (var container in _container.GetAllContainers(target, containerComp))
{
_container.CleanContainer(container);
}
args.Handled = true;
}
// Empty containers in a sane way
else
{
foreach (var containerId in ent.Comp.Container)
{
if (!_container.TryGetContainer(target, containerId, out var container, containerComp))
continue;
_container.CleanContainer(container);
args.Handled = true;
}
}
}
}

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