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. -->
## 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. -->
## Requirements
<!-- 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 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 -->
## Breaking changes

View File

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

View File

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

View File

@@ -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()
{
base.Initialize();
SubscribeLocalEvent<SharedGravityGeneratorComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<GravityGeneratorComponent, AppearanceChangeEvent>(OnAppearanceChange);
InitializeShake();
}
/// <summary>
/// Ensures that the visible state of gravity generators are synced with their sprites.
/// </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)
return;

View File

@@ -38,6 +38,8 @@ namespace Content.Client.Input
common.AddFunction(ContentKeyFunctions.ZoomIn);
common.AddFunction(ContentKeyFunctions.ResetZoom);
common.AddFunction(ContentKeyFunctions.InspectEntity);
common.AddFunction(ContentKeyFunctions.InspectServerComponent);
common.AddFunction(ContentKeyFunctions.InspectClientComponent);
common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow);
// 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.HideUI);
AddButton(ContentKeyFunctions.InspectEntity);
AddButton(ContentKeyFunctions.InspectServerComponent);
AddButton(ContentKeyFunctions.InspectClientComponent);
AddHeader("ui-options-header-text-cursor");
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.Shared.Gravity;
using Robust.Shared.GameObjects;

View File

@@ -402,8 +402,8 @@ namespace Content.IntegrationTests.Tests.Power
battery = entityManager.GetComponent<BatteryComponent>(generatorEnt);
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
batterySys.SetMaxCharge(generatorEnt, startingCharge, battery);
batterySys.SetCharge(generatorEnt, startingCharge, battery);
batterySys.SetMaxCharge((generatorEnt, battery), startingCharge);
batterySys.SetCharge((generatorEnt, battery), startingCharge);
netBattery.MaxSupply = 400;
netBattery.SupplyRampRate = 400;
netBattery.SupplyRampTolerance = 100;
@@ -513,8 +513,8 @@ namespace Content.IntegrationTests.Tests.Power
supplier.SupplyRampRate = rampRate;
supplier.SupplyRampTolerance = rampTol;
batterySys.SetMaxCharge(batteryEnt, 100_000, battery);
batterySys.SetCharge(batteryEnt, 100_000, battery);
batterySys.SetMaxCharge((batteryEnt, battery), 100_000);
batterySys.SetCharge((batteryEnt, battery), 100_000);
netBattery.MaxSupply = draw / 2;
netBattery.SupplyRampRate = rampRate;
netBattery.SupplyRampTolerance = rampTol;
@@ -600,7 +600,7 @@ namespace Content.IntegrationTests.Tests.Power
supplier.MaxSupply = 500;
supplier.SupplyRampTolerance = 500;
batterySys.SetMaxCharge(batteryEnt, 100_000, battery);
batterySys.SetMaxCharge((batteryEnt, battery), 100_000);
netBattery.MaxChargeRate = 1_000;
netBattery.Efficiency = 0.5f;
});
@@ -670,8 +670,8 @@ namespace Content.IntegrationTests.Tests.Power
netBattery.MaxSupply = 400;
netBattery.SupplyRampTolerance = 400;
netBattery.SupplyRampRate = 100_000;
batterySys.SetMaxCharge(batteryEnt, 100_000, battery);
batterySys.SetCharge(batteryEnt, 100_000, battery);
batterySys.SetMaxCharge((batteryEnt, battery), 100_000);
batterySys.SetCharge((batteryEnt, battery), 100_000);
});
// Run some ticks so everything is stable.
@@ -750,8 +750,8 @@ namespace Content.IntegrationTests.Tests.Power
netBattery.SupplyRampTolerance = 400;
netBattery.SupplyRampRate = 100_000;
netBattery.Efficiency = 0.5f;
batterySys.SetMaxCharge(batteryEnt, 1_000_000, battery);
batterySys.SetCharge(batteryEnt, 1_000_000, battery);
batterySys.SetMaxCharge((batteryEnt, battery), 1_000_000);
batterySys.SetCharge((batteryEnt, battery), 1_000_000);
});
// Run some ticks so everything is stable.
@@ -841,8 +841,8 @@ namespace Content.IntegrationTests.Tests.Power
supplier.MaxSupply = 1000;
supplier.SupplyRampTolerance = 1000;
batterySys.SetMaxCharge(batteryEnt1, 1_000_000, battery1);
batterySys.SetMaxCharge(batteryEnt2, 1_000_000, battery2);
batterySys.SetMaxCharge((batteryEnt1, battery1), 1_000_000);
batterySys.SetMaxCharge((batteryEnt2, battery2), 1_000_000);
netBattery1.MaxChargeRate = 1_000;
netBattery2.MaxChargeRate = 1_000;
@@ -945,10 +945,10 @@ namespace Content.IntegrationTests.Tests.Power
netBattery2.SupplyRampTolerance = 1000;
netBattery1.SupplyRampRate = 100_000;
netBattery2.SupplyRampRate = 100_000;
batterySys.SetMaxCharge(batteryEnt1, 100_000, battery1);
batterySys.SetMaxCharge(batteryEnt2, 100_000, battery2);
batterySys.SetCharge(batteryEnt1, 100_000, battery1);
batterySys.SetCharge(batteryEnt2, 100_000, battery2);
batterySys.SetMaxCharge((batteryEnt1, battery1), 100_000);
batterySys.SetMaxCharge((batteryEnt2, battery2), 100_000);
batterySys.SetCharge((batteryEnt1, battery1), 100_000);
batterySys.SetCharge((batteryEnt2, battery2), 100_000);
});
// Run some ticks so everything is stable.
@@ -1031,8 +1031,8 @@ namespace Content.IntegrationTests.Tests.Power
supplier.MaxSupply = 1000;
supplier.SupplyRampTolerance = 1000;
batterySys.SetMaxCharge(batteryEnt1, 1_000_000, battery1);
batterySys.SetMaxCharge(batteryEnt2, 1_000_000, battery2);
batterySys.SetMaxCharge((batteryEnt1, battery1), 1_000_000);
batterySys.SetMaxCharge((batteryEnt2, battery2), 1_000_000);
netBattery1.MaxChargeRate = 20;
netBattery2.MaxChargeRate = 20;
@@ -1107,8 +1107,8 @@ namespace Content.IntegrationTests.Tests.Power
netBattery.MaxSupply = 1000;
netBattery.SupplyRampTolerance = 200;
netBattery.SupplyRampRate = 10;
batterySys.SetMaxCharge(batteryEnt, 100_000, battery);
batterySys.SetCharge(batteryEnt, 100_000, battery);
batterySys.SetMaxCharge((batteryEnt, battery), 100_000);
batterySys.SetCharge((batteryEnt, battery), 100_000);
});
// Run some ticks so everything is stable.
@@ -1253,7 +1253,7 @@ namespace Content.IntegrationTests.Tests.Power
generatorSupplier.MaxSupply = 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
@@ -1314,8 +1314,8 @@ namespace Content.IntegrationTests.Tests.Power
extensionCableSystem.SetProviderTransferRange(apcExtensionEnt, range);
extensionCableSystem.SetReceiverReceptionRange(powerReceiverEnt, range);
batterySys.SetMaxCharge(apcEnt, 10000, battery); //arbitrary nonzero amount of charge
batterySys.SetCharge(apcEnt, battery.MaxCharge, battery); //fill battery
batterySys.SetMaxCharge((apcEnt, battery), 10000); //arbitrary nonzero amount of charge
batterySys.SetCharge((apcEnt, battery), battery.MaxCharge); //fill battery
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")),
Act = () =>
{
_batterySystem.SetCharge(args.Target, battery.MaxCharge, battery);
_batterySystem.SetCharge((args.Target, battery), battery.MaxCharge);
},
Impact = LogImpact.Medium,
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")),
Act = () =>
{
_batterySystem.SetCharge(args.Target, 0, battery);
_batterySystem.SetCharge((args.Target, battery), 0);
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-drain-battery-description"),
@@ -200,9 +200,8 @@ public sealed partial class AdminVerbSystem
Act = () =>
{
var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target);
recharger.AutoRecharge = true;
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePause = false; // No delay.
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
@@ -553,7 +552,7 @@ public sealed partial class AdminVerbSystem
if (!HasComp<StationInfiniteBatteryTargetComponent>(ent))
continue;
var battery = EnsureComp<BatteryComponent>(ent);
_batterySystem.SetCharge(ent, battery.MaxCharge, battery);
_batterySystem.SetCharge((ent, battery), battery.MaxCharge);
}
},
Impact = LogImpact.Extreme,
@@ -574,7 +573,7 @@ public sealed partial class AdminVerbSystem
if (!HasComp<StationInfiniteBatteryTargetComponent>(ent))
continue;
var battery = EnsureComp<BatteryComponent>(ent);
_batterySystem.SetCharge(ent, 0, battery);
_batterySystem.SetCharge((ent, battery), 0);
}
},
Impact = LogImpact.Extreme,
@@ -599,9 +598,8 @@ public sealed partial class AdminVerbSystem
var recharger = EnsureComp<BatterySelfRechargerComponent>(ent);
var battery = EnsureComp<BatteryComponent>(ent);
recharger.AutoRecharge = true;
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePause = false; // No delay.
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
}
},
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

@@ -396,7 +396,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!antagEnt.HasValue)
{
var getEntEv = new AntagSelectEntityEvent(session, ent);
var getEntEv = new AntagSelectEntityEvent(session, ent, def.PrefRoles);
RaiseLocalEvent(ent, ref getEntEv, true);
antagEnt = getEntEv.Entity;
}
@@ -419,7 +420,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
// Therefore any component subscribing to this has to make sure both subscriptions return the same value
// or the ghost role raffle location preview will be wrong.
var getPosEv = new AntagSelectLocationEvent(session, ent);
var getPosEv = new AntagSelectLocationEvent(session, ent, player);
RaiseLocalEvent(ent, ref getPosEv, true);
if (getPosEv.Handled)
{
@@ -607,10 +608,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// Only raised if the selected player's current entity is invalid.
/// </summary>
[ByRefEvent]
public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule, List<ProtoId<AntagPrototype>> AntagRoles)
{
public readonly ICommonSession? Session = Session;
/// list of antag role prototypes associated with a entity. used by the <see cref="AntagMultipleRoleSpawnerComponent"/>
public readonly List<ProtoId<AntagPrototype>> AntagRoles = AntagRoles;
public bool Handled => Entity != null;
public EntityUid? Entity;
@@ -620,12 +624,15 @@ public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<Anta
/// Event raised on a game rule entity to determine the location for the antagonist.
/// </summary>
[ByRefEvent]
public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule, EntityUid Entity)
{
public readonly ICommonSession? Session = Session;
public bool Handled => Coordinates.Any();
// the entity of the antagonist
public EntityUid Entity = Entity;
public List<MapCoordinates> Coordinates = new();
}

View File

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

View File

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

View File

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

View File

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

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(new FormattedMessage(point.Scoreboard).ToMarkup());
args.AddLine("");
}
}

View File

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

View File

@@ -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("");
}
private void OnNukeExploded(NukeExplodedEvent ev)

View File

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

View File

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

View File

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

View File

@@ -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),
("username", username)));
}
args.AddLine("");
}
/// <summary>

View File

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

View File

@@ -1,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;
public sealed class GravityGeneratorSystem : EntitySystem
public sealed class GravityGeneratorSystem : SharedGravityGeneratorSystem
{
[Dependency] private readonly GravitySystem _gravitySystem = 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)
{
ent.Comp.GravityActive = true;
Dirty(ent, ent.Comp);
var xform = Transform(ent);
@@ -48,6 +49,7 @@ public sealed class GravityGeneratorSystem : EntitySystem
private void OnDeactivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineDeactivatedEvent args)
{
ent.Comp.GravityActive = false;
Dirty(ent, ent.Comp);
var xform = Transform(ent);

View File

@@ -145,7 +145,7 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
{
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);
TurnOff(entity);
@@ -153,8 +153,8 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
}
else
{
_battery.SetCharge(entity.Owner, battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency, battery);
if (_battery.IsFull(entity, battery))
_battery.SetCharge((entity.Owner, battery), battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency);
if (_battery.IsFull((entity.Owner, battery)))
{
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);
}
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);
UpdateLevel(uid);

View File

@@ -340,7 +340,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (!TryComp<BatteryComponent>(battery, out var batteryComp))
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
{
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
var maxDrained = pnb.MaxSupply * comp.DrainTime;
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;
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
Spawn("EffectSparks", Transform(target).Coordinates);
_audio.PlayPvs(comp.SparkSound, target);
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// 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/>
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>

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,10 +9,10 @@ using JetBrains.Annotations;
using Robust.Shared.Utility;
using Robust.Shared.Timing;
namespace Content.Server.Power.EntitySystems
{
namespace Content.Server.Power.EntitySystems;
[UsedImplicitly]
public sealed class BatterySystem : SharedBatterySystem
public sealed partial class BatterySystem : SharedBatterySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
@@ -21,8 +21,8 @@ namespace Content.Server.Power.EntitySystems
base.Initialize();
SubscribeLocalEvent<ExaminableBatteryComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
SubscribeLocalEvent<BatteryComponent, ChangeChargeEvent>(OnChangeCharge);
SubscribeLocalEvent<BatteryComponent, GetChargeEvent>(OnGetCharge);
@@ -31,27 +31,27 @@ namespace Content.Server.Power.EntitySystems
SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync);
}
private void OnNetBatteryRejuvenate(EntityUid uid, PowerNetworkBatteryComponent component, RejuvenateEvent args)
private void OnNetBatteryRejuvenate(Entity<PowerNetworkBatteryComponent> ent, ref RejuvenateEvent args)
{
component.NetworkBattery.CurrentStorage = component.NetworkBattery.Capacity;
ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity;
}
private void OnBatteryRejuvenate(EntityUid uid, BatteryComponent component, RejuvenateEvent args)
private void OnBatteryRejuvenate(Entity<BatteryComponent> ent, ref RejuvenateEvent args)
{
SetCharge(uid, component.MaxCharge, component);
SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
}
private void OnExamine(EntityUid uid, ExaminableBatteryComponent component, ExaminedEvent args)
private void OnExamine(Entity<ExaminableBatteryComponent> ent, ref ExaminedEvent args)
{
if (!TryComp<BatteryComponent>(uid, out var batteryComponent))
if (!args.IsInDetailsRange)
return;
if (args.IsInDetailsRange)
{
var effectiveMax = batteryComponent.MaxCharge;
if (effectiveMax == 0)
effectiveMax = 1;
var chargeFraction = batteryComponent.CurrentCharge / effectiveMax;
var chargePercentRounded = (int)(chargeFraction * 100);
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",
@@ -60,7 +60,6 @@ namespace Content.Server.Power.EntitySystems
)
);
}
}
private void PreSync(NetworkBatteryPreSync ev)
{
@@ -80,41 +79,24 @@ namespace Content.Server.Power.EntitySystems
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);
SetCharge((uid, bat), netBat.NetworkBattery.CurrentStorage);
}
}
/// <summary>
/// Gets the price for the power contained in an entity's battery.
/// </summary>
private void CalculateBatteryPrice(EntityUid uid, BatteryComponent component, ref PriceCalculationEvent args)
private void CalculateBatteryPrice(Entity<BatteryComponent> ent, ref PriceCalculationEvent args)
{
args.Price += component.CurrentCharge * component.PricePerJoule;
args.Price += ent.Comp.CurrentCharge * ent.Comp.PricePerJoule;
}
private void OnChangeCharge(Entity<BatteryComponent> entity, ref ChangeChargeEvent args)
private void OnChangeCharge(Entity<BatteryComponent> ent, ref ChangeChargeEvent args)
{
if (args.ResidualValue == 0)
return;
args.ResidualValue -= ChangeCharge(entity, args.ResidualValue);
args.ResidualValue -= ChangeCharge(ent.AsNullable(), args.ResidualValue);
}
private void OnGetCharge(Entity<BatteryComponent> entity, ref GetChargeEvent args)
@@ -123,118 +105,19 @@ namespace Content.Server.Power.EntitySystems
args.MaxCharge += entity.Comp.MaxCharge;
}
public override float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
public override void Update(float frameTime)
{
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)
var query = EntityQueryEnumerator<BatterySelfRechargerComponent, BatteryComponent>();
var curTime = _timing.CurTime;
while (query.MoveNext(out var uid, out var comp, out var bat))
{
if (!Resolve(uid, ref battery))
return;
if (!comp.AutoRecharge || IsFull((uid, bat)))
continue;
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;
if (comp.NextAutoRecharge > curTime)
continue;
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;
SetCharge((uid, bat), bat.CurrentCharge + comp.AutoRechargeRate * frameTime);
}
}
}

View File

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

View File

@@ -358,13 +358,13 @@ namespace Content.Server.Power.EntitySystems
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
else if (powered && !_battery.IsFull(uid, battery))
else if (powered && !_battery.IsFull((uid, battery)))
{
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

View File

@@ -40,7 +40,7 @@ namespace Content.Server.Power
shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id)));
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
}
}

View File

@@ -28,7 +28,7 @@ public sealed partial class PowerCellSystem
if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, slot))
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;
var ev = new PowerCellSlotEmptyEvent();

View File

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

View File

@@ -66,7 +66,7 @@ namespace Content.Server.PowerSink
if (!transform.Anchored)
continue;
_battery.ChangeCharge(entity, networkLoad.NetworkLoad.ReceivingPower * frameTime, battery);
_battery.ChangeCharge((entity, battery), networkLoad.NetworkLoad.ReceivingPower * frameTime);
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 (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage((uid, jam)) * frameTime, battery))
if (!_battery.TryUseCharge((batteryUid.Value, battery), GetCurrentWattage((uid, jam)) * frameTime))
{
ChangeLEDState(uid, false);
RemComp<ActiveRadioJammerComponent>(uid);

View File

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

View File

@@ -801,7 +801,11 @@ public sealed partial class ShuttleSystem
while (iteration < FTLProximityIterations)
{
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)
{
@@ -834,10 +838,6 @@ public sealed partial class ShuttleSystem
if (nearbyGrids.Contains(uid))
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(
_transform.GetWorldMatrix(uid)
.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 is pretty crude for multiple landings.
if (nearbyGrids.Count >= 1)
if (nearbyGrids.Count > 1 || !HasComp<MapComponent>(targetXform.GridUid))
{
// Pick a random angle
var offsetAngle = _random.NextAngle();
@@ -866,9 +866,13 @@ public sealed partial class ShuttleSystem
var minRadius = MathF.Max(targetAABB.Width / 2f, targetAABB.Height / 2f);
spawnPos = targetAABB.Center + offsetAngle.RotateVec(new Vector2(_random.NextFloat(minRadius + minOffset, minRadius + maxOffset), 0f));
}
else if (shuttleBody != null)
{
(spawnPos, angle) = _transform.GetWorldPositionRotation(targetXform);
}
else
{
spawnPos = _transform.ToWorldPosition(targetCoordinates);
spawnPos = _transform.GetWorldPosition(targetXform);
}
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).
var transform = new Transform(_transform.ToWorldPosition(xform.Coordinates), angle);
var adjustedOffset = Robust.Shared.Physics.Transform.Mul(transform, offset);
var transform = new Transform(spawnPos, angle);
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;
}

View File

@@ -98,7 +98,7 @@ public sealed partial class BorgSystem
if (command == RoboticsConsoleConstants.NET_DISABLE_COMMAND)
Disable(ent);
else if (command == RoboticsConsoleConstants.NET_DESTROY_COMMAND)
Destroy(ent);
Destroy(ent.Owner);
}
private void Disable(Entity<BorgTransponderComponent, BorgChassisComponent?> ent)
@@ -118,8 +118,15 @@ public sealed partial class BorgSystem
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
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.
if (TryComp<BatteryComponent>(ent, out var battery))
{
_battery.SetCharge(ent, battery.MaxCharge);
_battery.SetCharge((ent, battery), battery.MaxCharge);
}
_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)
{
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;
}

View File

@@ -24,7 +24,7 @@ public sealed class TeslaCoilSystem : EntitySystem
{
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.Dataset;
using Content.Shared.Tips;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -15,10 +12,7 @@ using Robust.Shared.Timing;
namespace Content.Server.Tips;
/// <summary>
/// Handles periodically displaying gameplay tips to all players ingame.
/// </summary>
public sealed class TipsSystem : EntitySystem
public sealed class TipsSystem : SharedTipsSystem
{
[Dependency] private readonly IChatManager _chat = 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 IRobustRandom _random = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private bool _tipsEnabled;
private float _tipTimeOutOfRound;
@@ -35,16 +27,6 @@ public sealed class TipsSystem : EntitySystem
private string _tipsDataset = "";
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)]
private TimeSpan _nextTipTime = TimeSpan.Zero;
@@ -53,110 +35,45 @@ public sealed class TipsSystem : EntitySystem
base.Initialize();
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.TipsDataset, SetDataset, true);
Subs.CVar(_cfg, CCVars.TipsTippyChance, SetTippyChance, true);
Subs.CVar(_cfg, CCVars.TipFrequencyOutOfRound, value => _tipTimeOutOfRound = value, 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();
_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(
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
};
RecalculateNextTipTime();
}
}
private void SendTip(IConsoleShell shell, string argstr, string[] args)
private void SetEnabled(bool value)
{
AnnounceRandomTip();
_tipsEnabled = value;
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"));
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;
}
_nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeInRound);
}
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;
_nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound);
}
}
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
ev.SpeakTime = GetSpeechTime(ev.Msg);
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)
{
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)
{
_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()
public override void AnnounceRandomTip()
{
if (!_prototype.TryIndex<LocalizedDatasetPrototype>(_tipsDataset, out var tips))
return;
@@ -215,35 +121,20 @@ public sealed class TipsSystem : EntitySystem
if (_random.Prob(_tipTippyChance))
{
var ev = new TippyEvent(msg);
ev.SpeakTime = GetSpeechTime(msg);
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);
var speakTime = GetSpeechTime(msg);
SendTippy(msg, speakTime: speakTime);
}
else
{
_nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound);
}
}
private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev)
{
// reset for lobby -> inround
// reset for inround -> post but not post -> lobby
if (ev.New == GameRunLevel.InRound || ev.Old == GameRunLevel.InRound)
{
RecalculateNextTipTime();
_chat.ChatMessageToManyFiltered(
Filter.Broadcast(),
ChatChannel.OOC,
tip,
msg,
EntityUid.Invalid,
false,
false,
Color.MediumPurple);
}
}
}

View File

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

View File

@@ -25,7 +25,7 @@ public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryCompo
_lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _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;
}
if (Values.TryAdd(Logger.ConvertName(format), value)
format = Logger.ConvertName(format);
if (Values.TryAdd(format, value)
|| Values[format] is T val && val.Equals(value) )
{
return;
@@ -50,7 +51,7 @@ public ref struct LogStringHandler
var i = 2;
format = $"{originalFormat}_{i}";
while (!(Values.TryAdd(Logger.ConvertName(format), value)
while (!(Values.TryAdd(format, value)
|| Values[format] is T val2 && val2.Equals(value)))
{
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"/>
/// </summary>
[Pure]
public FixedPoint2 GetTotalPrototypeQuantity(params string[] prototypes)
public FixedPoint2 GetTotalPrototypeQuantity(params ProtoId<ReagentPrototype>[] prototypes)
{
var total = FixedPoint2.Zero;
foreach (var (reagent, quantity) in Contents)
@@ -314,7 +314,7 @@ namespace Content.Shared.Chemistry.Components
return total;
}
public FixedPoint2 GetTotalPrototypeQuantity(string id)
public FixedPoint2 GetTotalPrototypeQuantity(ProtoId<ReagentPrototype> id)
{
var total = FixedPoint2.Zero;
foreach (var (reagent, quantity) in Contents)
@@ -645,7 +645,7 @@ namespace Content.Shared.Chemistry.Components
/// <summary>
/// Splits a solution with only the specified reagent prototypes.
/// </summary>
public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includedPrototypes)
public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params ProtoId<ReagentPrototype>[] includedPrototypes)
{
// First remove the non-included prototypes
List<ReagentQuantity> excluded = new();
@@ -844,7 +844,7 @@ namespace Content.Shared.Chemistry.Components
ValidateSolution();
}
public Color GetColorWithout(IPrototypeManager? protoMan, params string[] without)
public Color GetColorWithout(IPrototypeManager? protoMan, params ProtoId<ReagentPrototype>[] without)
{
if (Volume == FixedPoint2.Zero)
{
@@ -887,7 +887,7 @@ namespace Content.Shared.Chemistry.Components
return GetColorWithout(protoMan);
}
public Color GetColorWithOnly(IPrototypeManager? protoMan, params string[] included)
public Color GetColorWithOnly(IPrototypeManager? protoMan, params ProtoId<ReagentPrototype>[] included)
{
if (Volume == FixedPoint2.Zero)
{

View File

@@ -1217,7 +1217,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
AddComp(uid, relation);
MetaDataSys.SetEntityName(uid, $"solution - {name}");
MetaDataSys.SetEntityName(uid, $"solution - {name}", raiseEvents: false);
ContainerSystem.Insert(uid, container, force: true);
return (uid, solution, relation);

View File

@@ -8,13 +8,41 @@ namespace Content.Shared.Damage.Systems;
public sealed partial class DamageableSystem
{
/// <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>
/// <remarks>
/// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed
/// event is raised.
/// </remarks>
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))
return;

View File

@@ -1,7 +1,9 @@
using System.Linq;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Content.Shared.Fluids.Components;
using Content.Shared.Chemistry.Reagent;
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)
{
if (solProto.EvaporationSpeed > FixedPoint2.Zero)
@@ -89,10 +91,10 @@ public abstract partial class SharedPuddleSystem
return evaporatingReagents.ToArray();
}
public string[] GetAbsorbentReagents(Solution solution)
public ProtoId<ReagentPrototype>[] GetAbsorbentReagents(Solution solution)
{
List<string> absorbentReagents = [];
foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
var absorbentReagents = new List<ProtoId<ReagentPrototype>>();
foreach (ReagentPrototype solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
{
if (solProto.Absorbent)
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.
/// The speed at which a solution evaporates is the average of the speed of all evaporating reagents in it.
/// </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)
{
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 TileFrictionController _tile = default!;
private string[] _standoutReagents = [];
private ProtoId<ReagentPrototype>[] _standoutReagents = [];
/// <summary>
/// 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!;
private static readonly ProtoId<TagPrototype> ForceableFollowTag = "ForceableFollow";
private static readonly ProtoId<TagPrototype> PreventGhostnadoWarpTag = "NotGhostnadoWarpable";
public override void Initialize()
{
@@ -320,11 +321,17 @@ public sealed class FollowerSystem : EntitySystem
var query = EntityQueryEnumerator<FollowerComponent, GhostComponent, ActorComponent>();
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))
continue;
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
followedEnts.TryGetValue(followed, out var currentValue);
followedEnts[followed] = currentValue + 1;

View File

@@ -3,42 +3,45 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Gravity;
[NetworkedComponent()]
[Virtual]
public partial class SharedGravityGeneratorComponent : Component
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class GravityGeneratorComponent : Component
{
[DataField] public float LightRadiusMin { get; set; }
[DataField] public float LightRadiusMax { get; set; }
/// <summary>
/// A map of the sprites used by the gravity generator given its status.
/// </summary>
[DataField("spriteMap")]
[Access(typeof(SharedGravitySystem))]
public Dictionary<PowerChargeStatus, string> SpriteMap = new();
[DataField, Access(typeof(SharedGravitySystem))]
public Dictionary<PowerChargeStatus, string> SpriteMap = [];
/// <summary>
/// The sprite used by the core of the gravity generator when the gravity generator is starting up.
/// </summary>
[DataField("coreStartupState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string CoreStartupState = "startup";
/// <summary>
/// The sprite used by the core of the gravity generator when the gravity generator is idle.
/// </summary>
[DataField("coreIdleState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string CoreIdleState = "idle";
/// <summary>
/// The sprite used by the core of the gravity generator when the gravity generator is activating.
/// </summary>
[DataField("coreActivatingState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string CoreActivatingState = "activating";
/// <summary>
/// The sprite used by the core of the gravity generator when the gravity generator is active.
/// </summary>
[DataField("coreActivatedState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
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()
{
SubscribeLocalEvent<HandsComponent, GetStandUpTimeEvent>(OnStandupArgs);
SubscribeLocalEvent<HandsComponent, KnockedDownRefreshEvent>(OnKnockedDownRefresh);
}
/// <summary>
@@ -28,4 +29,17 @@ public abstract partial class SharedHandsSystem
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 EditorFlipObject = "EditorFlipObject";
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 SaveMap = "SaveMap";
public static readonly BoundKeyFunction MappingEnablePick = "MappingEnablePick";

View File

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

View File

@@ -6,6 +6,7 @@ using Content.Shared.Charges.Systems;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
@@ -66,6 +67,7 @@ public abstract class SharedMagicSystem : EntitySystem
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly ExamineSystemShared _examine= default!;
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
@@ -399,22 +401,30 @@ public abstract class SharedMagicSystem : EntitySystem
#endregion
#region Knock Spells
/// <summary>
/// Opens all doors and locks within range
/// Opens all doors and locks within range.
/// </summary>
/// <param name="args"></param>
private void OnKnockSpell(KnockSpellEvent args)
{
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
return;
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.
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;
if (TryComp<DoorBoltComponent>(target, out var doorBoltComp) && doorBoltComp.BoltsDown)
@@ -424,7 +434,7 @@ public abstract class SharedMagicSystem : EntitySystem
_door.StartOpening(target);
if (TryComp<LockComponent>(target, out var lockComp) && lockComp.Locked)
_lock.Unlock(target, args.Performer, lockComp);
_lock.Unlock(target, performer, lockComp);
}
}
// End Knock Spells

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
@@ -43,6 +44,8 @@ public abstract partial class SharedMindSystem : EntitySystem
private HashSet<Entity<MindComponent>> _pickingMinds = new();
private readonly EntProtoId _mindProto = "MindBase";
public override void Initialize()
{
base.Initialize();
@@ -226,7 +229,7 @@ public abstract partial class SharedMindSystem : EntitySystem
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})");
var mind = EnsureComp<MindComponent>(mindId);
mind.CharacterName = name;

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Content.Shared.Conveyor;
using Content.Shared.Gravity;
using Content.Shared.Movement.Components;
@@ -58,6 +58,9 @@ public abstract class SharedConveyorController : VirtualController
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.
args.Modifier = 0f;
}
@@ -140,7 +143,15 @@ public abstract class SharedConveyorController : VirtualController
continue;
var physics = ent.Entity.Comp3;
if (physics.BodyStatus != BodyStatus.OnGround)
{
SetConveying(ent.Entity.Owner, ent.Entity.Comp1, false);
continue;
}
var velocity = physics.LinearVelocity;
var angularVelocity = physics.AngularVelocity;
var targetDir = ent.Direction;
// 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
// snagged on corners from sliding into the centerline.
_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);
@@ -172,8 +184,10 @@ public abstract class SharedConveyorController : VirtualController
// 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.
_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);
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;
/// <summary>
@@ -7,27 +10,36 @@ namespace Content.Shared.Power;
public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
/// <summary>
/// Event that supports multiple battery types.
/// 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>
[ByRefEvent]
public sealed class GetChargeEvent : EntityEventArgs
public record struct GetChargeEvent
{
public float CurrentCharge;
public float MaxCharge;
}
/// <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>
[ByRefEvent]
public sealed class ChangeChargeEvent : EntityEventArgs
public record struct ChangeChargeEvent(float Amount)
{
public float OriginalValue;
public float ResidualValue;
/// <summary>
/// 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)
{
OriginalValue = value;
ResidualValue = value;
}
/// <summary>
/// The amount of charge that still has to be removed.
/// For cases where there are multiple batteries.
/// </summary>
public float ResidualValue = Amount;
}

View File

@@ -11,10 +11,8 @@ namespace Content.Shared.Power.Components;
[Access(typeof(SharedBatterySystem))]
public partial class BatteryComponent : Component
{
public string SolutionName = "battery";
/// <summary>
/// Maximum charge of the battery in joules (ie. watt seconds)
/// Maximum charge of the battery in joules (i.e. watt seconds)
/// </summary>
[DataField]
[GuidebookData]
@@ -23,11 +21,11 @@ public partial class BatteryComponent : Component
/// <summary>
/// Current charge of the battery in joules (ie. watt seconds)
/// </summary>
[DataField("startingCharge")]
[DataField("startingCharge")] // TODO: rename this datafield to currentCharge
public float CurrentCharge;
/// <summary>
/// The price per one joule. Default is 1 credit for 10kJ.
/// The price per one joule. Default is 1 speso for 10kJ.
/// </summary>
[DataField]
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);
}
private void OnEmpPulse(Entity<BatteryComponent> entity, ref EmpPulseEvent args)
private void OnEmpPulse(Entity<BatteryComponent> ent, ref EmpPulseEvent args)
{
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.
TrySetChargeCooldown(entity);
TrySetChargeCooldown(ent.Owner);
}
public virtual float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
return 0f;
}
public virtual void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null) { }
public virtual float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
/// <summary>
/// Changes the battery's charge by the given amount.
/// A positive value will add charge, a negative value will remove charge.
/// </summary>
/// <returns>The actually changed amount.</returns>
public virtual float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
{
return 0f;
}
/// <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>
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;
}
/// <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]
public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
/// <summary>
/// If the entity can open own UI.
/// </summary>
[DataField]
public bool CanOpenSelfUi;
}
[Serializable, NetSerializable]

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Silicons.Borgs.Components;
@@ -24,6 +25,13 @@ public sealed partial class BorgModuleComponent : Component
[DataField]
[AutoNetworkedField]
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>

View File

@@ -1,6 +1,8 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Localizations;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
@@ -35,10 +37,30 @@ public abstract partial class SharedBorgSystem : EntitySystem
SubscribeLocalEvent<BorgChassisComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
SubscribeLocalEvent<BorgChassisComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
SubscribeLocalEvent<BorgModuleComponent, ExaminedEvent>(OnModuleExamine);
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)
{
if (args.Handled)
@@ -98,8 +120,8 @@ public abstract partial class SharedBorgSystem : EntitySystem
private void OnUIOpenAttempt(EntityUid uid, BorgChassisComponent component, ActivatableUIOpenAttemptEvent args)
{
// borgs can't view their own ui
if (args.User == uid)
// borgs generaly can't view their own ui
if (args.User == uid && !component.CanOpenSelfUi)
args.Cancel();
}

View File

@@ -1,12 +1,12 @@
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Gravity;
using Content.Shared.Hands;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Input;
using Content.Shared.Movement.Events;
@@ -54,7 +54,7 @@ public abstract partial class SharedStunSystem
SubscribeLocalEvent<KnockedDownComponent, BuckleAttemptEvent>(OnBuckleAttempt);
SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt);
// Updating movement a friction
// Updating movement and friction
SubscribeLocalEvent<KnockedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshKnockedSpeed);
SubscribeLocalEvent<KnockedDownComponent, RefreshFrictionModifiersEvent>(OnRefreshFriction);
SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
@@ -66,6 +66,9 @@ public abstract partial class SharedStunSystem
SubscribeLocalEvent<CrawlerComponent, KnockedDownRefreshEvent>(OnKnockdownRefresh);
SubscribeLocalEvent<CrawlerComponent, DamageChangedEvent>(OnDamaged);
SubscribeLocalEvent<KnockedDownComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
SubscribeLocalEvent<KnockedDownComponent, DidEquipHandEvent>(OnHandEquipped);
SubscribeLocalEvent<KnockedDownComponent, DidUnequipHandEvent>(OnHandUnequipped);
SubscribeLocalEvent<KnockedDownComponent, HandCountChangedEvent>(OnHandCountChanged);
SubscribeLocalEvent<GravityAffectedComponent, KnockDownAttemptEvent>(OnKnockdownAttempt);
SubscribeLocalEvent<GravityAffectedComponent, GetStandUpTimeEvent>(OnGetStandUpTime);
@@ -522,6 +525,30 @@ public abstract partial class SharedStunSystem
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)
{
// Directed, targeted moth attack.
@@ -582,6 +609,7 @@ public abstract partial class SharedStunSystem
ent.Comp.SpeedModifier = ev.SpeedModifier;
ent.Comp.FrictionModifier = ev.FrictionModifier;
Dirty(ent);
_movementSpeedModifier.RefreshMovementSpeedModifiers(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;
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]
public sealed class TippyEvent : EntityEventArgs
public sealed class TippyEvent(string msg, EntProtoId? proto, float speakTime, float slideTime, float waddleInterval) : EntityEventArgs
{
public TippyEvent(string msg)
{
Msg = msg;
}
/// <summary>
/// The text to show in the speech bubble.
/// </summary>
public string Msg = msg;
public string Msg;
public string? Proto;
/// <summary>
/// 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.
public float SpeakTime = 5;
public float SlideTime = 3;
public float WaddleInterval = 0.5f;
/// <summary>
/// The time the speech bubble is shown, in seconds.
/// </summary>
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;
}
}
}
}

View File

@@ -0,0 +1,20 @@
using Content.Shared.Jittering;
using Content.Shared.Trigger.Components.Effects;
using Robust.Shared.Network;
namespace Content.Shared.Trigger.Systems;
public sealed class JitterOnTriggerSystem : XOnTriggerSystem<JitterOnTriggerComponent>
{
[Dependency] private readonly SharedJitteringSystem _jittering = default!;
[Dependency] private readonly INetManager _net = default!;
protected override void OnTrigger(Entity<JitterOnTriggerComponent> ent, EntityUid target, ref TriggerEvent args)
{
// DoJitter mispredicts at the moment.
// TODO: Fix this and remove the IsServer check.
if (_net.IsServer)
_jittering.DoJitter(target, ent.Comp.Time, ent.Comp.Refresh, ent.Comp.Amplitude, ent.Comp.Frequency, ent.Comp.ForceValueChange);
args.Handled = true;
}
}

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