From 60be38595a759c69cef6227ada35e6aa4d2b914f Mon Sep 17 00:00:00 2001 From: DDrakov Date: Mon, 6 Jul 2026 16:50:14 +0700 Subject: [PATCH 01/11] Port Coyote Expedition Retreival --- .../TriggerOnMobstateChangeComponent.cs | 11 +- .../EntitySystems/TriggerSystem.Mobstate.cs | 122 +++++- .../Explosion/EntitySystems/TriggerSystem.cs | 28 +- .../Expeditions/SalvageExpeditionComponent.cs | 6 + .../Salvage/SalvageSystem.Runner.cs | 366 +++++++++++++++++- .../Systems/ShuttleSystem.FasterThanLight.cs | 20 + .../Implants/Components/RattleComponent.cs | 9 + .../Implants/SharedSubdermalImplantSystem.cs | 15 + .../en-US/_NF/procedural/expeditions.ftl | 14 + Resources/Locale/en-US/implant/implant.ftl | 3 + 10 files changed, 581 insertions(+), 13 deletions(-) diff --git a/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs b/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs index a6cdda247c3..9c939433e39 100644 --- a/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs +++ b/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs @@ -1,4 +1,5 @@ -using Content.Shared.Mobs; +using System.Threading; +using Content.Shared.Mobs; namespace Content.Server.Explosion.Components; @@ -21,4 +22,12 @@ public sealed partial class TriggerOnMobstateChangeComponent : Component [ViewVariables] [DataField("preventSuicide")] public bool PreventSuicide = false; + + // the timer cancel token + [ViewVariables] + public CancellationTokenSource RattleCancelToken = new(); + + // The delay before the implant sends the message again + [DataField] + public TimeSpan RattleRefireDelay = TimeSpan.FromMinutes(20); } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs index ccd2a6e3df0..9e781da6fa4 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs @@ -1,8 +1,14 @@ -using Content.Server.Explosion.Components; +using System.Threading; +using Content.Server.Explosion.Components; using Content.Shared.Explosion.Components; using Content.Shared.Implants; using Content.Shared.Interaction.Events; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Verbs; +using Robust.Shared.Timing; namespace Content.Server.Explosion.EntitySystems; @@ -15,10 +21,14 @@ private void InitializeMobstate() SubscribeLocalEvent>(OnSuicideRelay); SubscribeLocalEvent>(OnMobStateRelay); + SubscribeLocalEvent>>(OnVerbRelay); + SubscribeLocalEvent>(OnFtlArriveRelay); } private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent component, MobStateChangedEvent args) { + component.RattleCancelToken.Cancel(); + component.RattleCancelToken = new CancellationTokenSource(); if (!component.MobState.Contains(args.NewMobState)) return; @@ -28,14 +38,64 @@ private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent c { HandleTimerTrigger( uid, - args.Origin, + stateChangerUid, timerTrigger.Delay, timerTrigger.BeepInterval, timerTrigger.InitialBeepDelay, timerTrigger.BeepSound); } else - Trigger(uid); + { + Dictionary extraData = new() + { + { "isRetry", retry } + }; + Trigger(uid, extras: extraData); + } + + // but only repeat if their mind has a people behind it + if (!TryComp(changedStateMobUid, out var mindContainer)) + return; + var mind = CompOrNull(mindContainer.Mind); + var hasUserId = mind?.UserId; + if (hasUserId == null) + return; + + // then do it AGAIN + component.RattleCancelToken.Cancel(); + component.RattleCancelToken = new CancellationTokenSource(); + Robust.Shared.Timing.Timer.Spawn(component.RattleRefireDelay, () => CheckAndTryRefire(uid, component, changedStateMobUid), component.RattleCancelToken.Token); + } + + /// + /// Check if the trigger can be retriggered and does so if possible + /// + private void CheckAndTryRefire( + EntityUid uid, + TriggerOnMobstateChangeComponent component, + EntityUid changedStateMobUid) + { + if (!Exists(uid) + || !Exists(changedStateMobUid)) + return; + if (Deleted(uid) + || Deleted(changedStateMobUid)) + return; + if (!HasComp(changedStateMobUid)) + return; + if (!component.Enabled) + return; + var stat = Comp(changedStateMobUid).CurrentState; + if (component.MobState.Contains(stat)) + { + TryRunTrigger( + uid, + component, + changedStateMobUid, + stat, + null, + true); + } } /// @@ -61,6 +121,60 @@ private void OnSuicideRelay(EntityUid uid, TriggerOnMobstateChangeComponent comp private void OnMobStateRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent args) { - OnMobStateChanged(uid, component, args.Event); + OnMobStateChanged( + uid, + component, + args.Event); + } + + /// + /// When ftl arrives, and they are fucked, do the needful + /// + private void OnFtlArriveRelay(EntityUid uid, + TriggerOnMobstateChangeComponent component, + ImplantRelayEvent args) + { + TryRunTrigger( + uid, + component, + args.Event.Implanted, + args.Event.CurrentState, + null); + } + + + private void OnVerbRelay(EntityUid uid, + TriggerOnMobstateChangeComponent component, + ImplantRelayEvent> args) + { + OnGetVerbs(uid, component, args.Event); + } + + private void OnGetVerbs(EntityUid uid, + TriggerOnMobstateChangeComponent component, + GetVerbsEvent args) + { + if (args.User != args.Target) + return; // Self only, but usable in crit + + var verb = new Verb() + { + Text = Loc.GetString( + "trigger-on-mobstate-verb-text", + ("state", component.Enabled ? "ON" : "OFF")), + Act = () => + { + component.Enabled = !component.Enabled; + _popupSystem.PopupEntity( + Loc.GetString( + "trigger-on-mobstate-verb-popup", + ("state", component.Enabled ? "ENABLED" : "DISABLED")), + args.User, + args.User); + }, + Disabled = false, + Message = "Toggle whether or not this thing tells everyone you are dead both inside and outside." + }; + args.Verbs.Add(verb); } } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index 56bccc79608..d9bf9f017b0 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -1,4 +1,6 @@ using Content.Server._Mono.Planets; +using System.Linq; +using System.Threading; using Content.Server.Administration.Logs; using Content.Server.Body.Systems; using Content.Server.Explosion.Components; @@ -35,6 +37,8 @@ using Content.Shared.Humanoid; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Timing; +using Robust.Shared.Utility; using Content.Shared.Body.Components; // Frontier: Gib organs using Content.Shared.Projectiles; // Frontier: embed triggers using Content.Shared.Mind; @@ -51,6 +55,7 @@ public sealed class TriggerEvent : HandledEntityEventArgs { public EntityUid Triggered { get; } public EntityUid? User { get; } + public Dictionary Extras { get; } = new(); public TriggerEvent(EntityUid triggered, EntityUid? user = null) { @@ -59,6 +64,12 @@ public TriggerEvent(EntityUid triggered, EntityUid? user = null) } } + /// + /// Raised before a trigger is activated. + /// + [ByRefEvent] + public record struct BeforeTriggerEvent(EntityUid Triggered, EntityUid? User, bool Cancelled = false); + /// /// Raised when timer trigger becomes active. /// @@ -246,12 +257,16 @@ private void HandleRattleTrigger(EntityUid uid, RattleComponent component, Trigg if (implanted.ImplantedEntity == null) return; + if (!TryComp(implanted.ImplantedEntity, out var mobstate) + || mobstate.CurrentState == MobState.Alive) + return; + // Gets location of the implant var ownerXform = Transform(uid); var pos = ownerXform.MapPosition; - var x = (int) pos.X; - var y = (int) pos.Y; + var x = (int)pos.X; + var y = (int)pos.Y; var posText = $"({x}, {y})"; // Frontier: Gets station location of the implant @@ -350,9 +365,16 @@ private void OnRepeatInit(Entity ent, ref MapInitEven ent.Comp.NextTrigger = _timing.CurTime + ent.Comp.Delay; } - public bool Trigger(EntityUid trigger, EntityUid? user = null) + public bool Trigger(EntityUid trigger, EntityUid? user = null, Dictionary? extras = null) { var triggerEvent = new TriggerEvent(trigger, user); + if (extras != null) + { + foreach (var (key, value) in extras) + { + triggerEvent.AddExtra(key, value); + } + } EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true); return triggerEvent.Handled; } diff --git a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs index 819eecaac6b..757abd4ed00 100644 --- a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs +++ b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs @@ -71,4 +71,10 @@ public sealed partial class SalvageExpeditionComponent : SharedSalvageExpedition [ViewVariables(VVAccess.ReadWrite), DataField("rewards", customTypeSerializer: typeof(PrototypeIdListSerializer))] public List Rewards = default!; // End Frontier: expedition difficulty and rewards + + /// + /// next time to check for autoabort + /// + [ViewVariables(VVAccess.ReadWrite), DataField] + public TimeSpan NextAutoAbortCheck = TimeSpan.Zero; } diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 6557898788c..86cc6f8515c 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -15,8 +15,17 @@ using Content.Shared.Localizations; using Robust.Shared.Map.Components; using Robust.Shared.Player; -using Robust.Shared.Utility; -using Content.Shared.Coordinates; +using Content.Server.Body.Components; +using Content.Server.Buckle.Systems; +using Content.Server.Temperature.Components; +using Content.Server.Temperature.Systems; +using Content.Shared.Atmos; +using Content.Shared.Buckle.Components; +using Content.Shared.Damage; +using Content.Shared.Mind.Components; +using Robust.Server.Player; +using Robust.Shared.Audio; +using Robust.Shared.Enums; // Frontier namespace Content.Server.Salvage; @@ -27,7 +36,12 @@ public sealed partial class SalvageSystem */ [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; // Frontier + [Dependency] private readonly BuckleSystem _buckle = default!; + [Dependency] private readonly IPlayerManager _players = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly TemperatureSystem _temperature = default!; + private void InitializeRunner() { SubscribeLocalEvent(OnFTLRequest); @@ -186,6 +200,8 @@ private void UpdateRunner() var remaining = comp.EndTime - _timing.CurTime; var audioLength = _audio.GetAudioLength(comp.SelectedSong); + AbortIfWiped(uid, comp); // Frontier + if (comp.Stage < ExpeditionStage.FinalCountdown && remaining < TimeSpan.FromSeconds(45)) { comp.Stage = ExpeditionStage.FinalCountdown; @@ -237,7 +253,38 @@ private void UpdateRunner() continue; } - // Destination generator parameters (move to CVAR?) + // rescue all the losers on the map who arent on the ship for whatever reason + var shuttleGrid = shuttleXform.GridUid; + DestinationPriority? deadLoserDestinations = null; + if (shuttleGrid != null) + { + var mobQuery = EntityQueryEnumerator(); + while (mobQuery.MoveNext( + out var mobUid, + out var mindC, + out var mobXform)) + { + if (mobXform.MapUid != uid) + continue; + if (mobXform.GridUid == shuttleGrid) + continue; // they're already on the shuttle + // only count creatures that have at one point had a player controlling them + if (!mindC.HasMind) + continue; + // move them to the shuttle + deadLoserDestinations ??= GetDeadLoserDestinations(shuttleGrid.Value); + RescueDork( + mobUid, + deadLoserDestinations, + shuttleGrid.Value); + Spawn("EffectSparks", Transform(mobUid).Coordinates); + Spawn("EffectGravityPulse", Transform(mobUid).Coordinates); + SoundSpecifier Sound = new SoundPathSpecifier("/Audio/_COYOTE/ExpedReturnToBed.ogg"); + _audio.PlayPvs(Sound, mobUid); + } + } + + // Destination generator parameters (move to CVAR?) int numRetries = 20; // Maximum number of retries float minDistance = 200f; // Minimum distance from another object, in meters float minRange = 750f; // Minimum distance from sector centre, in meters @@ -272,7 +319,13 @@ private void UpdateRunner() dropLocation = _random.NextVector2(minRange, maxRange); } - _shuttle.FTLToCoordinates(shuttleUid, shuttle, new EntityCoordinates(mapUid.Value, dropLocation), 0f, 5.5f, 50f); + _shuttle.FTLToCoordinates( + shuttleUid, + shuttle, + new EntityCoordinates(mapUid.Value, dropLocation), + 0f, + ftlTime, + TravelTime); // End Frontier: try to find a potential destination for ship that doesn't collide with other grids. } @@ -358,5 +411,308 @@ private void UpdateRunner() Announce(uid, Loc.GetString("salvage-expedition-completed")); } } + // End Frontier: mission-specific logic + } + + /// + /// Takes a mob, and puts them onto this shuttle. + /// + private void RescueDork( + EntityUid mobUid, + DestinationPriority possibleDestinations, + EntityUid shuttleGrid) + { + TendToDork(mobUid); + Spawn("EffectGravityPulse", Transform(mobUid).Coordinates); + Spawn("EffectSparks", Transform(mobUid).Coordinates); + // unbuckle them if they are buckled + _buckle.TryUnbuckle(mobUid, null); + // try beds first + foreach (var bedUid in possibleDestinations.Beds) + { + if (TryTeleportToStrap(mobUid, bedUid)) + return; + } + // then chairs + foreach (var chairUid in possibleDestinations.Chairs) + { + if (TryTeleportToStrap(mobUid, chairUid)) + return; + } + // then consoles + foreach (var consoleUid in possibleDestinations.Consoles) + { + var consoleXform = Transform(consoleUid); + var mobXform = Transform(mobUid); + _transform.SetCoordinates(mobUid, consoleXform.Coordinates); + _transform.AttachToGridOrMap(mobUid, mobXform); + return; + } + // then fallback + foreach (var fallbackUid in possibleDestinations.Fallback) + { + var fallbackXform = Transform(fallbackUid); + var mobXform = Transform(mobUid); + _transform.SetCoordinates(mobUid, fallbackXform.Coordinates); + _transform.AttachToGridOrMap(mobUid, mobXform); + return; + } + } + + /// + /// Gets a list of possible destinations for dead/dying crew to be rescued to. + /// Tries to find a location based on a list of priorities. + /// HERES THE PRIORITIES: + /// 2: Beds with no mobs in them. + /// 3: Chairs with no mobs in them. + /// 4: I dunno the console I guess + /// + private DestinationPriority GetDeadLoserDestinations(EntityUid shuttleGrid) + { + DestinationPriority destinations = new(); + // first, find the exped consoles on the grid + var destQuery = EntityQueryEnumerator(); + while (destQuery.MoveNext( + out var uid, + out var _, + out var xform)) + { + if (xform.GridUid != shuttleGrid) + continue; + destinations.Add(uid, DestinationType.Console); + } + // then, all beds / chairs (theyre both strap components) + var strapQuery = EntityQueryEnumerator(); + while (strapQuery.MoveNext( + out var uid, + out var strap, + out var xform)) + { + if (xform.GridUid != shuttleGrid) + continue; + destinations.Add(uid, strap.Position == StrapPosition.Stand ? DestinationType.Chair : DestinationType.Bed); + } + // then some fallback stuff, find the warp point + // worst case, we just teleport them to the center of the grid. hope its not in a wall!! + var warpQuery = EntityQueryEnumerator(); + while (warpQuery.MoveNext( + out var uid, + out var _, + out var xform)) + { + if (xform.GridUid != shuttleGrid) + continue; + destinations.Add(uid, DestinationType.Fallback); + } + return destinations; + } + + /// + /// Beats the heck out of the dork if they arent dead + /// Then extinguishes them and caps their Heat to 300ish if above that. + /// + private void TendToDork(EntityUid mobUid) + { + if (_mobState.IsAlive(mobUid)) + { + // hey you're alive! stop that! + var hurtEmThisMuch = new DamageSpecifier() + { + DamageDict = { ["Slash"] = 150, ["Heat"] = 150, ["Poison"] = 100 } + }; + _damageable.TryChangeDamage( + mobUid, + hurtEmThisMuch, + true); + } + else if (_mobState.IsCritical(mobUid)) + { + // I saw that, you're still alive! stop that! + var hurtEmThisMuch = new DamageSpecifier() + { + DamageDict = { ["Slash"] = 50, ["Heat"] = 50, ["Poison"] = 25 } + }; + _damageable.TryChangeDamage( + mobUid, + hurtEmThisMuch, + true); + } + + // okay, extinguish them, and clamp their burn damages to a max of 300 + // fire sucks, i hate this game + var ev = new ExtinguishEvent + { + FireStacksAdjustment = 1000, + }; + RaiseLocalEvent(mobUid, ref ev); + if (TryComp(mobUid, out var damageable) + && damageable.Damage.DamageDict.TryGetValue("Heat", out var burnAmount) + && burnAmount > 300) + { + var reduceBy = burnAmount - 300; + var burnDamageSpecifier = new DamageSpecifier() + { + DamageDict = { ["Heat"] = -reduceBy } + }; + _damageable.TryChangeDamage( + mobUid, + burnDamageSpecifier, + true); + } + if (!TryComp(mobUid, out var comp)) + return; + if (TryComp( + mobUid, + out var regulator)) // Frontier: Look for normal body temperature and use it + { + _temperature.ForceChangeTemperature( + mobUid, + regulator.NormalBodyTemperature, + comp); + } + else + { + _temperature.ForceChangeTemperature( + mobUid, + Atmospherics.T20C, + comp); + } + // FIRE SUCKSSSSSSSSSS + } + + /// + /// Tries to teleport the mob to the strap and buckle them in. + /// Returns true on success. + /// + private bool TryTeleportToStrap(EntityUid mobUid, EntityUid strapUid) + { + if (!TryComp(mobUid, out var buckle)) + return false; + if (!TryComp(strapUid, out var strap)) + return false; + if (strap.BuckledEntities.Count > 0) + return false; // already occupied + var strapXform = Transform(strapUid); + var mobXform = Transform(mobUid); + _transform.SetCoordinates(mobUid, strapXform.Coordinates); + _transform.AttachToGridOrMap(mobUid, mobXform); + return _buckle.TryBuckle( + mobUid, + null, + strapUid); + } + + // class that holds a set of destinations with a priority + private sealed class DestinationPriority + { + public List Beds = new(); + public List Chairs = new(); + public List Consoles = new(); + public List Fallback = new(); + public void Add(EntityUid uid, DestinationType type) + { + switch (type) + { + case DestinationType.Bed: + Beds.Add(uid); + break; + case DestinationType.Chair: + Chairs.Add(uid); + break; + case DestinationType.Console: + Consoles.Add(uid); + break; + default: + case DestinationType.Fallback: + Fallback.Add(uid); + break; + } + } + } + + // enum for destination types + private enum DestinationType + { + Bed, + Chair, + Console, + Fallback, + } + + /// + /// Checks if everyone on the map worth caring about is dead, and aborts the expedition if so. + /// Honestly, as long as one person is not in crit and not SSD, we consider the expedition salvageable. + /// + private void AbortIfWiped(EntityUid mapUid, SalvageExpeditionComponent component) + { + // give it a 30 second grade after first check to avoid instant aborts + if (component.NextAutoAbortCheck == TimeSpan.Zero) + { + component.NextAutoAbortCheck = _timing.CurTime + TimeSpan.FromSeconds(30); + return; + } + // its an entity query and idk how expensive it is, so, cooldown + if (_timing.CurTime < component.NextAutoAbortCheck) + return; + component.NextAutoAbortCheck = _timing.CurTime + TimeSpan.FromSeconds(15); + + // okay first look for aghosts, whatever + var aghostQuery = + EntityQueryEnumerator(); + while (aghostQuery.MoveNext( + out var _, + out _, + out var xform)) + { + if (xform.MapUid == mapUid) + return; // aghost found, dont abort + } + + var query = + EntityQueryEnumerator< + HumanoidAppearanceComponent, + MindContainerComponent, + MobStateComponent, + TransformComponent>(); + // prevent abort if: + // - aghosts are present + // - anyone is alive AND connected + while (query.MoveNext( + out var uid, + out _, + out var mindC, + out var mobState, + out var xform)) + { + if (xform.MapUid != mapUid) + continue; + // unidentified humans (loot) dont count + if (!mindC.HasHadMind) + continue; + // if anyone is alive and not in crit, we are good + if (_mobState.IsAlive(uid, mobState)) + { + // okay weve got something alive, is their session? + _players.TryGetSessionByEntity(uid, out var session); + // if no session, check if they are SSD + if (session == null) + continue; + if (session.Status == SessionStatus.Disconnected) + continue; + return; // alive and connected player found, expedition is salvageable + } + } + // everyone is dead or ssd, abort the expedition + const int departTime = 20; + Announce(mapUid, Loc.GetString("salvage-expedition-abort-wipe", ("departTime", departTime))); + component.NextAutoAbortCheck = TimeSpan.FromDays(1); // prevent further checks + var newEndTime = _timing.CurTime + TimeSpan.FromSeconds(departTime); + + if (component.EndTime <= newEndTime) + return; + + component.Stage = ExpeditionStage.FinalCountdown; + component.EndTime = newEndTime; + } } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index ed8b559de28..7e9ec84141f 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -9,7 +9,10 @@ using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.Ghost; +using Content.Shared.Implants; +using Content.Shared.Implants.Components; using Content.Shared.Maps; +using Content.Shared.Mobs.Components; using Content.Shared.Parallax; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; @@ -899,6 +902,23 @@ private void UpdateFTLArriving(Entity entity) Enable(uid, component: body, shuttle: entity.Comp2); } } + + // COYOTE: when the shuttle arrives, go through all the mobs on the grid + // and attempt to set off their deathrattle implants + var shuttleGridId = xform.GridUid; + var implantedQuery = EntityQueryEnumerator(); + while (implantedQuery.MoveNext( + out var mobUid, + out var implanted, + out var mobState, + out var mobXform)) + { + if (mobXform.GridUid != shuttleGridId) + continue; + + var deathrattleEvent = new ReTriggerRattleImplantEvent(mobUid, mobState.CurrentState); + RaiseLocalEvent(mobUid, deathrattleEvent); + } } private void UpdateFTLCooldown(Entity entity) diff --git a/Content.Shared/Implants/Components/RattleComponent.cs b/Content.Shared/Implants/Components/RattleComponent.cs index 6588a9d52f5..e29a64882e4 100644 --- a/Content.Shared/Implants/Components/RattleComponent.cs +++ b/Content.Shared/Implants/Components/RattleComponent.cs @@ -1,4 +1,5 @@ using Content.Shared._EinsteinEngines.Language; +using System.Threading; using Content.Shared.Radio; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -23,4 +24,12 @@ public sealed partial class RattleComponent : Component // The message that the implant will send when dead [DataField("deathMessage")] public LocId DeathMessage = "deathrattle-implant-dead-message"; + + // The message that the implant will send when crit still + [DataField] + public LocId CritRetryMessage = "deathrattle-implant-critical-message-still"; + + // The message that the implant will send when dead still + [DataField] + public LocId DeathRetryMessage = "deathrattle-implant-dead-message-still"; } diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index bb166b3c5cb..aaab8f53af8 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Interaction.Events; using Content.Shared.Mobs; using Content.Shared.Tag; +using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.Network; @@ -30,6 +31,8 @@ public override void Initialize() SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); + SubscribeLocalEvent>(RelayToImplantEvent); + SubscribeLocalEvent(RelayToImplantEvent); } private void OnInsert(EntityUid uid, SubdermalImplantComponent component, EntGotInsertedIntoContainerMessage args) @@ -217,3 +220,15 @@ public ImplantImplantedEvent(EntityUid implant, EntityUid? implanted) Implanted = implanted; } } + +/// +/// Event used to re-trigger implant events, if needed. +/// Raised on the implanted entity. +/// +public sealed class ReTriggerRattleImplantEvent( + EntityUid implanted, + MobState currentState) : EventArgs +{ + public readonly EntityUid Implanted = implanted; + public readonly MobState CurrentState = currentState; +} diff --git a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl index bdea86044bb..eeb79e7cd45 100644 --- a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl +++ b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl @@ -1,5 +1,7 @@ salvage-expedition-window-finish = Finish expedition salvage-expedition-announcement-early-finish = The expedition was completed ahead of schedule. Shuttle will depart in {$departTime} seconds. +salvage-expedition-abort-wipe = [color=Red]Oh dear, everyone's dead.[/color] Aborting mission and recovering the bodies! Shuttle will depart in {$departTime} seconds. + salvage-expedition-shuttle-not-found = Cannot locate shuttle. salvage-expedition-not-everyone-aboard = Not all crew aboard! {CAPITALIZE(THE($target))} is still out there! salvage-expedition-ftl-cooldown = The FTL drive is still spooling down from the jump in. Wait for it to cool down before returning. @@ -51,3 +53,15 @@ salvage-air-mod-16 = 34 CO2, 34 NH3, 34 N2O salvage-air-mod-17 = 34 H2O, 34 NH3, 34 N2O salvage-air-mod-18 = 34 H2O, 34 N2O, 17 NH3, 17 CO2 salvage-air-mod-unknown = Unknown atmosphere + +salvage-expedition-difficulty-NFModerate = Moderate +salvage-expedition-difficulty-NFHazardous = Hazardous +salvage-expedition-difficulty-NFExtreme = Extreme + +salvage-expedition-megafauna-remaining = {$count -> + [one] {$count} target remaining. + *[other] {$count} targets remaining. +} + +salvage-expedition-type-Destruction = Destruction +salvage-expedition-type-Elimination = Elimination diff --git a/Resources/Locale/en-US/implant/implant.ftl b/Resources/Locale/en-US/implant/implant.ftl index 06e1909c9f2..04296c1ae82 100644 --- a/Resources/Locale/en-US/implant/implant.ftl +++ b/Resources/Locale/en-US/implant/implant.ftl @@ -34,3 +34,6 @@ scramble-implant-activated-popup = Your appearance shifts and changes! deathrattle-implant-dead-message = {$user}{$specie} has died at {$grid}{$position}. deathrattle-implant-critical-message = {$user}{$specie} life signs critical, immediate assistance required at {$grid}{$position}. + +deathrattle-implant-dead-message-still = {$user}{$specie} is STILL dead at {$grid}{$position}! +deathrattle-implant-critical-message-still = {$user}{$specie} life signs are still critical, immediate assistance is STILL required at {$grid}{$position}. From 8669314d293a053c11152435c97ced3a3c50241b Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 14 Apr 2025 11:00:47 +0200 Subject: [PATCH 02/11] Fire extinguishers can now extinguish items, including when held/worn (#36267) * Fire extinguishers now put out candles This did not actually require any changes to flammable or extinguishers directly, the only necessary changes were to make the collision actually work. Vapor entities (also used for fire extinguishers) now have a collision layer, so they can hit items. Added a new FlammableSetCollisionWake component to actually enable collision on candles while they are lit, because otherwise CollisionWake on entities gets in the way too. * Extinguishing items is now relayed to held/worn items This means held candles get extinguished too. Involved moving the core logic of ExtinguishReaction into an event so that it can be relayed via the existing hand/inventory relay logic. * Add helper functions for subscribing to relayed events. Use these in FlammableSystem * Make extinguishers work on cigarettes too A bunch of renaming to make the rest of my code work with SmokableComponent --------- Co-authored-by: metalgearsloth --- .../Atmos/EntitySystems/FlammableSystem.cs | 16 +++ .../Effects/ExtinguishReaction.cs | 17 ++- .../Nutrition/EntitySystems/SmokingSystem.cs | 18 +++ ...ExtinguishableSetCollisionWakeComponent.cs | 11 ++ .../ExtinguishableSetCollisionWakeSystem.cs | 30 +++++ Content.Shared/Atmos/FireEvents.cs | 42 ++++++ .../EntitySystems/SharedHandsSystem.Relay.cs | 18 +++ .../Inventory/InventorySystem.Relay.cs | 2 + .../Inventory/RelaySubscriptionHelpers.cs | 123 ++++++++++++++++++ .../Consumable/Smokeables/base_smokeables.yml | 4 + .../Entities/Objects/Misc/candles.yml | 1 + .../Objects/Specific/Janitorial/spray.yml | 2 + 12 files changed, 275 insertions(+), 9 deletions(-) create mode 100644 Content.Shared/Atmos/Components/ExtinguishableSetCollisionWakeComponent.cs create mode 100644 Content.Shared/Atmos/EntitySystems/ExtinguishableSetCollisionWakeSystem.cs create mode 100644 Content.Shared/Atmos/FireEvents.cs create mode 100644 Content.Shared/Inventory/RelaySubscriptionHelpers.cs diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index fb716d0ff39..0125c0a26bd 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -23,6 +23,7 @@ using Content.Shared.Toggleable; using Content.Shared.Weapons.Melee.Events; using Content.Shared.FixedPoint; +using Content.Shared.Hands; using Robust.Server.Audio; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; @@ -75,6 +76,7 @@ public override void Initialize() SubscribeLocalEvent(OnTileFire); SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnResistFireAlert); + Subs.SubscribeWithRelay(OnExtinguishEvent); SubscribeLocalEvent(IgniteOnCollide); SubscribeLocalEvent(OnIgniteLand); @@ -88,6 +90,14 @@ public override void Initialize() SubscribeLocalEvent(OnDamageChanged); } + private void OnExtinguishEvent(Entity ent, ref ExtinguishEvent args) + { + // You know I'm really not sure if having AdjustFireStacks *after* Extinguish, + // but I'm just moving this code, not questioning it. + Extinguish(ent, ent.Comp); + AdjustFireStacks(ent, args.FireStacksAdjustment, ent.Comp); + } + private void OnMeleeHit(EntityUid uid, IgniteOnMeleeHitComponent component, MeleeHitEvent args) { foreach (var entity in args.HitEntities) @@ -331,6 +341,9 @@ public void Extinguish(EntityUid uid, FlammableComponent? flammable = null) _ignitionSourceSystem.SetIgnited(uid, false); + var extinguished = new ExtinguishedEvent(); + RaiseLocalEvent(uid, ref extinguished); + UpdateAppearance(uid, flammable); } @@ -361,6 +374,9 @@ public void Ignite(EntityUid uid, EntityUid ignitionSource, FlammableComponent? else _adminLogger.Add(LogType.Flammable, $"{ToPrettyString(uid):target} set on fire by {ToPrettyString(ignitionSource):actor}"); flammable.OnFire = true; + + var extinguished = new IgnitedEvent(); + RaiseLocalEvent(uid, ref extinguished); } UpdateAppearance(uid, flammable); diff --git a/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs b/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs index 6d7e7c2fcd8..d36ac9a576f 100644 --- a/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs +++ b/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs @@ -1,5 +1,4 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos; using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; @@ -20,17 +19,17 @@ public sealed partial class ExtinguishReaction : EntityEffect public override void Effect(EntityEffectBaseArgs args) { - if (!args.EntityManager.TryGetComponent(args.TargetEntity, out FlammableComponent? flammable)) return; + var ev = new ExtinguishEvent + { + FireStacksAdjustment = FireStacksAdjustment, + }; - var flammableSystem = args.EntityManager.System(); - flammableSystem.Extinguish(args.TargetEntity, flammable); if (args is EntityEffectReagentArgs reagentArgs) { - flammableSystem.AdjustFireStacks(reagentArgs.TargetEntity, FireStacksAdjustment * (float) reagentArgs.Quantity, flammable); - } else - { - flammableSystem.AdjustFireStacks(args.TargetEntity, FireStacksAdjustment, flammable); + ev.FireStacksAdjustment *= (float)reagentArgs.Quantity; } + + args.EntityManager.EventBus.RaiseLocalEvent(args.TargetEntity, ref ev); } } } diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs index 0d637139d82..d2074a3d82d 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs @@ -17,6 +17,7 @@ using Robust.Server.GameObjects; using Robust.Shared.Containers; using System.Linq; +using Content.Shared.Atmos; namespace Content.Server.Nutrition.EntitySystems { @@ -48,12 +49,19 @@ public override void Initialize() SubscribeLocalEvent(OnSmokableIsHotEvent); SubscribeLocalEvent(OnSmokableShutdownEvent); SubscribeLocalEvent(OnSmokeableEquipEvent); + Subs.SubscribeWithRelay(OnExtinguishEvent); InitializeCigars(); InitializePipes(); InitializeVapes(); } + private void OnExtinguishEvent(Entity ent, ref ExtinguishEvent args) + { + if (ent.Comp.State == SmokableState.Lit) + SetSmokableState(ent, SmokableState.Burnt, ent); + } + public void SetSmokableState(EntityUid uid, SmokableState state, SmokableComponent? smokable = null, AppearanceComponent? appearance = null, ClothingComponent? clothing = null) { @@ -74,9 +82,19 @@ public void SetSmokableState(EntityUid uid, SmokableState state, SmokableCompone _items.SetHeldPrefix(uid, newState); if (state == SmokableState.Lit) + { + var igniteEvent = new IgnitedEvent(); + RaiseLocalEvent(uid, ref igniteEvent); + _active.Add(uid); + } else + { + var igniteEvent = new ExtinguishedEvent(); + RaiseLocalEvent(uid, ref igniteEvent); + _active.Remove(uid); + } } private void OnSmokableIsHotEvent(Entity entity, ref IsHotEvent args) diff --git a/Content.Shared/Atmos/Components/ExtinguishableSetCollisionWakeComponent.cs b/Content.Shared/Atmos/Components/ExtinguishableSetCollisionWakeComponent.cs new file mode 100644 index 00000000000..19e471f0e58 --- /dev/null +++ b/Content.Shared/Atmos/Components/ExtinguishableSetCollisionWakeComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Atmos.Components; + +/// +/// Makes entities with extinguishing behavior automatically enable/disable , +/// so they can be extinguished with fire extinguishers. +/// +[RegisterComponent] +[NetworkedComponent] +public sealed partial class ExtinguishableSetCollisionWakeComponent : Component; diff --git a/Content.Shared/Atmos/EntitySystems/ExtinguishableSetCollisionWakeSystem.cs b/Content.Shared/Atmos/EntitySystems/ExtinguishableSetCollisionWakeSystem.cs new file mode 100644 index 00000000000..107ac5efd76 --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/ExtinguishableSetCollisionWakeSystem.cs @@ -0,0 +1,30 @@ +using Content.Shared.Atmos.Components; + +namespace Content.Shared.Atmos.EntitySystems; + +/// +/// Implements . +/// +public sealed class ExtinguishableSetCollisionWakeSystem : EntitySystem +{ + [Dependency] + private readonly CollisionWakeSystem _collisionWake = null!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandleExtinguished); + SubscribeLocalEvent(HandleIgnited); + } + + private void HandleExtinguished(Entity ent, ref ExtinguishedEvent args) + { + _collisionWake.SetEnabled(ent, true); + } + + private void HandleIgnited(Entity ent, ref IgnitedEvent args) + { + _collisionWake.SetEnabled(ent, false); + } +} diff --git a/Content.Shared/Atmos/FireEvents.cs b/Content.Shared/Atmos/FireEvents.cs new file mode 100644 index 00000000000..4e19ef61478 --- /dev/null +++ b/Content.Shared/Atmos/FireEvents.cs @@ -0,0 +1,42 @@ +using Content.Shared.Inventory; +using Content.Shared.Nutrition.Components; + +namespace Content.Shared.Atmos; + +// NOTE: These components are currently not raised on the client, only on the server. + +/// +/// An entity has had an existing effect applied to it. +/// +/// +/// This does not necessarily mean the effect is strong enough to fully extinguish the entity in one go. +/// +[ByRefEvent] +public struct ExtinguishEvent : IInventoryRelayEvent +{ + /// + /// Amount of firestacks changed. Should be a negative number. + /// + public float FireStacksAdjustment; + + SlotFlags IInventoryRelayEvent.TargetSlots => SlotFlags.WITHOUT_POCKET; +} + +/// +/// A flammable entity has been extinguished. +/// +/// +/// This can occur on both Flammable entities as well as . +/// +/// +[ByRefEvent] +public struct ExtinguishedEvent; + +/// +/// A flammable entity has been ignited. +/// +/// +/// This can occur on both Flammable entities as well as . +/// +[ByRefEvent] +public struct IgnitedEvent; diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs index a0e02cf2e1a..742e6325010 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs @@ -1,3 +1,4 @@ +using Content.Shared.Atmos; using Content.Shared.Camera; using Content.Shared.Hands.Components; using Content.Shared.Movement.Systems; @@ -11,14 +12,31 @@ private void InitializeRelay() SubscribeLocalEvent(RelayEvent); SubscribeLocalEvent(RelayEvent); SubscribeLocalEvent(RelayEvent); + + // By-ref events. + SubscribeLocalEvent(RefRelayEvent); } private void RelayEvent(Entity entity, ref T args) where T : EntityEventArgs + { + CoreRelayEvent(entity, ref args); + } + + private void RefRelayEvent(Entity entity, ref T args) + { + var ev = CoreRelayEvent(entity, ref args); + args = ev.Args; + } + + private HeldRelayedEvent CoreRelayEvent(Entity entity, ref T args) { var ev = new HeldRelayedEvent(args); + foreach (var held in EnumerateHeld(entity, entity.Comp)) { RaiseLocalEvent(held, ref ev); } + + return ev; } } diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 80472fa5683..b0e533e9377 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -1,6 +1,7 @@ using Content.Shared._Goobstation.Flashbang; using Content.Shared._White.Overlays; using Content.Shared.Armor; +using Content.Shared.Atmos; using Content.Shared.Chat; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Hypospray.Events; @@ -56,6 +57,7 @@ public void InitializeRelay() SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); + SubscribeLocalEvent(RefRelayInventoryEvent); // Eye/vision events SubscribeLocalEvent(RelayInventoryEvent); diff --git a/Content.Shared/Inventory/RelaySubscriptionHelpers.cs b/Content.Shared/Inventory/RelaySubscriptionHelpers.cs new file mode 100644 index 00000000000..e9052315394 --- /dev/null +++ b/Content.Shared/Inventory/RelaySubscriptionHelpers.cs @@ -0,0 +1,123 @@ +using Content.Shared.Hands; + +namespace Content.Shared.Inventory; + +/// +/// Helper functions for subscribing to component events that are also relayed via hands/inventory. +/// +public static class RelaySubscriptionHelpers +{ + /// + /// Subscribe to an event, along with different relayed event wrappers, in one call. + /// + /// Subscriptions for the entity system we're subscribing on. + /// The event handler to be called for the event. + /// Whether to subscribe the base event type. + /// Whether to subscribe for . + /// Whether to subscribe for . + /// + public static void SubscribeWithRelay( + this EntitySystem.Subscriptions subs, + EntityEventRefHandler handler, + bool baseEvent = true, + bool inventory = true, + bool held = true) + where TEvent : notnull + where TComp : IComponent + { + if (baseEvent) + subs.SubscribeLocalEvent(handler); + + if (inventory) + { + subs.SubscribeLocalEvent((Entity ent, ref InventoryRelayedEvent ev) => + { + handler(ent, ref ev.Args); + }); + } + + if (held) + { + subs.SubscribeLocalEvent((Entity ent, ref HeldRelayedEvent ev) => + { + handler(ent, ref ev.Args); + }); + } + } + + /// + /// Subscribe to an event, along with different relayed event wrappers, in one call. + /// + /// Subscriptions for the entity system we're subscribing on. + /// The event handler to be called for the event. + /// Whether to subscribe the base event type. + /// Whether to subscribe for . + /// Whether to subscribe for . + /// + public static void SubscribeWithRelay( + this EntitySystem.Subscriptions subs, + ComponentEventHandler handler, + bool baseEvent = true, + bool inventory = true, + bool held = true) + where TEvent : notnull + where TComp : IComponent + { + if (baseEvent) + subs.SubscribeLocalEvent(handler); + + if (inventory) + { + subs.SubscribeLocalEvent((EntityUid uid, TComp component, InventoryRelayedEvent args) => + { + handler(uid, component, args.Args); + }); + } + + if (held) + { + subs.SubscribeLocalEvent((EntityUid uid, TComp component, HeldRelayedEvent args) => + { + handler(uid, component, args.Args); + }); + } + } + + /// + /// Subscribe to an event, along with different relayed event wrappers, in one call. + /// + /// Subscriptions for the entity system we're subscribing on. + /// The event handler to be called for the event. + /// Whether to subscribe the base event type. + /// Whether to subscribe for . + /// Whether to subscribe for . + /// + public static void SubscribeWithRelay( + this EntitySystem.Subscriptions subs, + ComponentEventRefHandler handler, + bool baseEvent = true, + bool inventory = true, + bool held = true) + where TEvent : notnull + where TComp : IComponent + { + if (baseEvent) + subs.SubscribeLocalEvent(handler); + + if (inventory) + { + subs.SubscribeLocalEvent((EntityUid uid, TComp component, ref InventoryRelayedEvent args) => + { + handler(uid, component, ref args.Args); + }); + } + + if (held) + { + subs.SubscribeLocalEvent((EntityUid uid, TComp component, ref HeldRelayedEvent args) => + { + handler(uid, component, ref args.Args); + }); + } + } +} diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/base_smokeables.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/base_smokeables.yml index 0d88c149e48..247468a1f16 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/base_smokeables.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/base_smokeables.yml @@ -4,6 +4,10 @@ parent: BaseItem abstract: true components: + - type: Reactive + groups: + Extinguish: [ Touch ] + - type: ExtinguishableSetCollisionWake - type: Smokable - type: Sprite - type: Appearance diff --git a/Resources/Prototypes/Entities/Objects/Misc/candles.yml b/Resources/Prototypes/Entities/Objects/Misc/candles.yml index c5a4fd65ac4..5f6624c923b 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/candles.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/candles.yml @@ -27,6 +27,7 @@ variation: 0.05 volume: 10 - type: UseDelay + - type: ExtinguishableSetCollisionWake - type: Flammable fireSpread: false canResistFire: false diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml index 0ce36c938f7..766acdf6a94 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml @@ -130,6 +130,8 @@ mask: - FullTileMask - Opaque + layer: + - ItemMask - type: Appearance - type: VaporVisuals From d00f75e8f205ade1ccacb615ea49d33d99698541 Mon Sep 17 00:00:00 2001 From: DDrakov Date: Thu, 9 Jul 2026 21:01:21 +0700 Subject: [PATCH 03/11] Yay it compiles --- .../EntitySystems/TriggerSystem.Mobstate.cs | 58 ++++++------------- .../Explosion/EntitySystems/TriggerSystem.cs | 9 +-- .../Salvage/SalvageSystem.Runner.cs | 16 ++--- Content.Shared/_Triad/AdminGhostComponent.cs | 10 ++++ 4 files changed, 39 insertions(+), 54 deletions(-) create mode 100644 Content.Shared/_Triad/AdminGhostComponent.cs diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs index 9e781da6fa4..69faa561ba4 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using Content.Server.Explosion.Components; using Content.Shared.Explosion.Components; using Content.Shared.Implants; @@ -21,7 +21,6 @@ private void InitializeMobstate() SubscribeLocalEvent>(OnSuicideRelay); SubscribeLocalEvent>(OnMobStateRelay); - SubscribeLocalEvent>>(OnVerbRelay); SubscribeLocalEvent>(OnFtlArriveRelay); } @@ -32,6 +31,23 @@ private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent c if (!component.MobState.Contains(args.NewMobState)) return; + TryRunTrigger( + uid, + component, + args.Target, + args.NewMobState, + args.Origin); + } + + private void TryRunTrigger( + EntityUid uid, + TriggerOnMobstateChangeComponent component, + EntityUid changedStateMobUid, + MobState coolState, + EntityUid? stateChangerUid = null, + bool retry = false) + { + //This chains Mobstate Changed triggers with OnUseTimerTrigger if they have it //Very useful for things that require a mobstate change and a timer if (TryComp(uid, out var timerTrigger)) @@ -83,8 +99,6 @@ private void CheckAndTryRefire( return; if (!HasComp(changedStateMobUid)) return; - if (!component.Enabled) - return; var stat = Comp(changedStateMobUid).CurrentState; if (component.MobState.Contains(stat)) { @@ -141,40 +155,4 @@ private void OnFtlArriveRelay(EntityUid uid, args.Event.CurrentState, null); } - - - private void OnVerbRelay(EntityUid uid, - TriggerOnMobstateChangeComponent component, - ImplantRelayEvent> args) - { - OnGetVerbs(uid, component, args.Event); - } - - private void OnGetVerbs(EntityUid uid, - TriggerOnMobstateChangeComponent component, - GetVerbsEvent args) - { - if (args.User != args.Target) - return; // Self only, but usable in crit - - var verb = new Verb() - { - Text = Loc.GetString( - "trigger-on-mobstate-verb-text", - ("state", component.Enabled ? "ON" : "OFF")), - Act = () => - { - component.Enabled = !component.Enabled; - _popupSystem.PopupEntity( - Loc.GetString( - "trigger-on-mobstate-verb-popup", - ("state", component.Enabled ? "ENABLED" : "DISABLED")), - args.User, - args.User); - }, - Disabled = false, - Message = "Toggle whether or not this thing tells everyone you are dead both inside and outside." - }; - args.Verbs.Add(verb); - } } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index d9bf9f017b0..efc40b5ed73 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -62,6 +62,11 @@ public TriggerEvent(EntityUid triggered, EntityUid? user = null) Triggered = triggered; User = user; } + + public void AddExtra(string extra, object value) + { + Extras[extra] = value; + } } /// @@ -257,10 +262,6 @@ private void HandleRattleTrigger(EntityUid uid, RattleComponent component, Trigg if (implanted.ImplantedEntity == null) return; - if (!TryComp(implanted.ImplantedEntity, out var mobstate) - || mobstate.CurrentState == MobState.Alive) - return; - // Gets location of the implant var ownerXform = Transform(uid); diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 86cc6f8515c..03c2ddc3f35 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -26,6 +26,9 @@ using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Enums; // Frontier +using Robust.Shared.Utility; +using Content.Shared._Triad; +using Content.Server.Warps; namespace Content.Server.Salvage; @@ -200,7 +203,7 @@ private void UpdateRunner() var remaining = comp.EndTime - _timing.CurTime; var audioLength = _audio.GetAudioLength(comp.SelectedSong); - AbortIfWiped(uid, comp); // Frontier + AbortIfWiped(uid, comp); // Coyote if (comp.Stage < ExpeditionStage.FinalCountdown && remaining < TimeSpan.FromSeconds(45)) { @@ -318,14 +321,7 @@ private void UpdateRunner() // No good position yet, pick another random position. dropLocation = _random.NextVector2(minRange, maxRange); } - - _shuttle.FTLToCoordinates( - shuttleUid, - shuttle, - new EntityCoordinates(mapUid.Value, dropLocation), - 0f, - ftlTime, - TravelTime); + _shuttle.FTLToCoordinates(shuttleUid, shuttle, new EntityCoordinates(mapUid.Value, dropLocation), 0f, 5.5f, 50f); // End Frontier: try to find a potential destination for ship that doesn't collide with other grids. } @@ -687,7 +683,7 @@ private void AbortIfWiped(EntityUid mapUid, SalvageExpeditionComponent component if (xform.MapUid != mapUid) continue; // unidentified humans (loot) dont count - if (!mindC.HasHadMind) + if (!mindC.HasMind) continue; // if anyone is alive and not in crit, we are good if (_mobState.IsAlive(uid, mobState)) diff --git a/Content.Shared/_Triad/AdminGhostComponent.cs b/Content.Shared/_Triad/AdminGhostComponent.cs new file mode 100644 index 00000000000..11726957046 --- /dev/null +++ b/Content.Shared/_Triad/AdminGhostComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Shared._Triad; + +/// +/// Determined that it's admin ghost +/// +[RegisterComponent] +public sealed partial class AdminGhostComponent : Component +{ + +} From 0afd58fbd99e453ea29190cfe136ff6c1d3bfaaa Mon Sep 17 00:00:00 2001 From: DDrakov Date: Fri, 10 Jul 2026 04:51:12 +0700 Subject: [PATCH 04/11] Add adminghost comp to admin ghost --- Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 65719c1c041..7a1ef786684 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -19,6 +19,7 @@ context: "aghost" - type: Ghost canInteract: true + - type: AdminGhost - type: GhostHearing - type: Hands - type: ComplexInteraction From a6bda04bc8d493203776ab4c589f4601eed42f0a Mon Sep 17 00:00:00 2001 From: DDrakov <157034866+DDrakov@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:39:04 +0700 Subject: [PATCH 05/11] Stamcrit instead of kill --- .../Salvage/SalvageSystem.Runner.cs | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 03c2ddc3f35..b391c415983 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -29,6 +29,8 @@ using Robust.Shared.Utility; using Content.Shared._Triad; using Content.Server.Warps; +using Content.Shared.Inventory; +using Content.Shared.Damage.Systems; namespace Content.Server.Salvage; @@ -44,6 +46,8 @@ public sealed partial class SalvageSystem [Dependency] private readonly IPlayerManager _players = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly TemperatureSystem _temperature = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; private void InitializeRunner() { @@ -418,7 +422,7 @@ private void RescueDork( DestinationPriority possibleDestinations, EntityUid shuttleGrid) { - TendToDork(mobUid); + PrepareRescue(mobUid); Spawn("EffectGravityPulse", Transform(mobUid).Coordinates); Spawn("EffectSparks", Transform(mobUid).Coordinates); // unbuckle them if they are buckled @@ -507,35 +511,20 @@ private DestinationPriority GetDeadLoserDestinations(EntityUid shuttleGrid) /// Beats the heck out of the dork if they arent dead /// Then extinguishes them and caps their Heat to 300ish if above that. /// - private void TendToDork(EntityUid mobUid) + private void PrepareRescue(EntityUid mobUid) { if (_mobState.IsAlive(mobUid)) { - // hey you're alive! stop that! - var hurtEmThisMuch = new DamageSpecifier() - { - DamageDict = { ["Slash"] = 150, ["Heat"] = 150, ["Poison"] = 100 } - }; - _damageable.TryChangeDamage( - mobUid, - hurtEmThisMuch, - true); + // Force stamcrit when rescue + _stamina.TakeStaminaDamage(mobUid, 200); } - else if (_mobState.IsCritical(mobUid)) + if (!_mobState.IsAlive(mobUid)) { - // I saw that, you're still alive! stop that! - var hurtEmThisMuch = new DamageSpecifier() - { - DamageDict = { ["Slash"] = 50, ["Heat"] = 50, ["Poison"] = 25 } - }; - _damageable.TryChangeDamage( - mobUid, - hurtEmThisMuch, - true); + // Force strip backpack and outerClothing if they're not alive + _inventorySystem.TryUnequip(mobUid, "back", true, true, false); + _inventorySystem.TryUnequip(mobUid, "outerClothing", true, true, false); } - // okay, extinguish them, and clamp their burn damages to a max of 300 - // fire sucks, i hate this game var ev = new ExtinguishEvent { FireStacksAdjustment = 1000, @@ -573,7 +562,6 @@ private void TendToDork(EntityUid mobUid) Atmospherics.T20C, comp); } - // FIRE SUCKSSSSSSSSSS } /// From 0c0e4059ee4be5de18e7aa5018304c8712a09e33 Mon Sep 17 00:00:00 2001 From: DDrakov <157034866+DDrakov@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:40:14 +0700 Subject: [PATCH 06/11] Change the function name because I don't like it --- Content.Server/Salvage/SalvageSystem.Runner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index b391c415983..8f7eb49159e 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -280,7 +280,7 @@ private void UpdateRunner() continue; // move them to the shuttle deadLoserDestinations ??= GetDeadLoserDestinations(shuttleGrid.Value); - RescueDork( + RescuePlayer( mobUid, deadLoserDestinations, shuttleGrid.Value); @@ -417,7 +417,7 @@ private void UpdateRunner() /// /// Takes a mob, and puts them onto this shuttle. /// - private void RescueDork( + private void RescuePlayer( EntityUid mobUid, DestinationPriority possibleDestinations, EntityUid shuttleGrid) From 920fd9a4ea2c9a984a126bc70c85e7d2305b0e42 Mon Sep 17 00:00:00 2001 From: DDrakov <157034866+DDrakov@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:31:56 +0700 Subject: [PATCH 07/11] The fuck you doing there --- Resources/Locale/en-US/_NF/procedural/expeditions.ftl | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl index eeb79e7cd45..46378edf04c 100644 --- a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl +++ b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl @@ -57,11 +57,3 @@ salvage-air-mod-unknown = Unknown atmosphere salvage-expedition-difficulty-NFModerate = Moderate salvage-expedition-difficulty-NFHazardous = Hazardous salvage-expedition-difficulty-NFExtreme = Extreme - -salvage-expedition-megafauna-remaining = {$count -> - [one] {$count} target remaining. - *[other] {$count} targets remaining. -} - -salvage-expedition-type-Destruction = Destruction -salvage-expedition-type-Elimination = Elimination From fcf65830ba951d742a8f068b5f3a9c3df8b72db6 Mon Sep 17 00:00:00 2001 From: DDrakov <157034866+DDrakov@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:34:32 +0700 Subject: [PATCH 08/11] Fix name, I hate it. --- Resources/Locale/en-US/_NF/procedural/expeditions.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl index 46378edf04c..a4d8b1cbcf1 100644 --- a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl +++ b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl @@ -1,6 +1,6 @@ salvage-expedition-window-finish = Finish expedition salvage-expedition-announcement-early-finish = The expedition was completed ahead of schedule. Shuttle will depart in {$departTime} seconds. -salvage-expedition-abort-wipe = [color=Red]Oh dear, everyone's dead.[/color] Aborting mission and recovering the bodies! Shuttle will depart in {$departTime} seconds. +salvage-expedition-abort-wipe = [color=Red]No life signs detected[/color]. Expedition aborted. Commencing personnel recovery. Shuttle departure in {$departTime} seconds. salvage-expedition-shuttle-not-found = Cannot locate shuttle. salvage-expedition-not-everyone-aboard = Not all crew aboard! {CAPITALIZE(THE($target))} is still out there! From ccd4067c831ee96485e6648d0857e5b6d5789ee1 Mon Sep 17 00:00:00 2001 From: DDrakov <157034866+DDrakov@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:37:14 +0700 Subject: [PATCH 09/11] Remove all the spawn stuff, don't need that they are not there to see anyway. Set depart time for exped retreival into 1 minute . Also check if the FTL is still cooldown or nah --- .../Salvage/SalvageSystem.Runner.cs | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 8f7eb49159e..3dc3bd40a92 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -260,10 +260,10 @@ private void UpdateRunner() continue; } - // rescue all the losers on the map who arent on the ship for whatever reason var shuttleGrid = shuttleXform.GridUid; DestinationPriority? deadLoserDestinations = null; - if (shuttleGrid != null) + // Need to check if the shuttle have any FTLComponent or not. + if (shuttleGrid != null && !HasComp(shuttleGrid)) { var mobQuery = EntityQueryEnumerator(); while (mobQuery.MoveNext( @@ -284,22 +284,20 @@ private void UpdateRunner() mobUid, deadLoserDestinations, shuttleGrid.Value); - Spawn("EffectSparks", Transform(mobUid).Coordinates); - Spawn("EffectGravityPulse", Transform(mobUid).Coordinates); - SoundSpecifier Sound = new SoundPathSpecifier("/Audio/_COYOTE/ExpedReturnToBed.ogg"); - _audio.PlayPvs(Sound, mobUid); + SoundSpecifier sound = new SoundPathSpecifier("/Audio/_COYOTE/ExpedReturnToBed.ogg"); + _audio.PlayPvs(sound, mobUid); } } - // Destination generator parameters (move to CVAR?) - int numRetries = 20; // Maximum number of retries - float minDistance = 200f; // Minimum distance from another object, in meters - float minRange = 750f; // Minimum distance from sector centre, in meters - float maxRange = 3500f; // Maximum distance from sector centre, in meters + // Destination generator parameters (move to CVAR?) + var numRetries = 20; // Maximum number of retries + var minDistance = 200f; // Minimum distance from another object, in meters + var minRange = 750f; // Minimum distance from sector centre, in meters + var maxRange = 3500f; // Maximum distance from sector centre, in meters // Get a list of all grid positions on the destination map List gridCoords = new(); - var gridQuery = EntityManager.AllEntityQueryEnumerator(); + var gridQuery = AllEntityQuery(); while (gridQuery.MoveNext(out var _, out _, out var xform)) { if (xform.MapID == mapId) @@ -423,8 +421,6 @@ private void RescuePlayer( EntityUid shuttleGrid) { PrepareRescue(mobUid); - Spawn("EffectGravityPulse", Transform(mobUid).Coordinates); - Spawn("EffectSparks", Transform(mobUid).Coordinates); // unbuckle them if they are buckled _buckle.TryUnbuckle(mobUid, null); // try beds first @@ -529,21 +525,6 @@ private void PrepareRescue(EntityUid mobUid) { FireStacksAdjustment = 1000, }; - RaiseLocalEvent(mobUid, ref ev); - if (TryComp(mobUid, out var damageable) - && damageable.Damage.DamageDict.TryGetValue("Heat", out var burnAmount) - && burnAmount > 300) - { - var reduceBy = burnAmount - 300; - var burnDamageSpecifier = new DamageSpecifier() - { - DamageDict = { ["Heat"] = -reduceBy } - }; - _damageable.TryChangeDamage( - mobUid, - burnDamageSpecifier, - true); - } if (!TryComp(mobUid, out var comp)) return; if (TryComp( @@ -687,7 +668,7 @@ private void AbortIfWiped(EntityUid mapUid, SalvageExpeditionComponent component } } // everyone is dead or ssd, abort the expedition - const int departTime = 20; + const int departTime = 60; Announce(mapUid, Loc.GetString("salvage-expedition-abort-wipe", ("departTime", departTime))); component.NextAutoAbortCheck = TimeSpan.FromDays(1); // prevent further checks var newEndTime = _timing.CurTime + TimeSpan.FromSeconds(departTime); From eec6520900d78094415e8120a11966282a1c842f Mon Sep 17 00:00:00 2001 From: DDrakov <157034866+DDrakov@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:23:13 +0700 Subject: [PATCH 10/11] I AM READY, HOW ABOUT YOU --- Content.Server/Salvage/SalvageSystem.Runner.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 3dc3bd40a92..93d12ede51a 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -513,12 +513,14 @@ private void PrepareRescue(EntityUid mobUid) { // Force stamcrit when rescue _stamina.TakeStaminaDamage(mobUid, 200); + _inventorySystem.TryUnequip(mobUid, "suitStorage", true, true, false); } if (!_mobState.IsAlive(mobUid)) { - // Force strip backpack and outerClothing if they're not alive + // Force strip backpack, outerClothing, and suitStorage if they're not alive _inventorySystem.TryUnequip(mobUid, "back", true, true, false); _inventorySystem.TryUnequip(mobUid, "outerClothing", true, true, false); + _inventorySystem.TryUnequip(mobUid, "suitStorage", true, true, false); } var ev = new ExtinguishEvent @@ -668,7 +670,7 @@ private void AbortIfWiped(EntityUid mapUid, SalvageExpeditionComponent component } } // everyone is dead or ssd, abort the expedition - const int departTime = 60; + const int departTime = 30; Announce(mapUid, Loc.GetString("salvage-expedition-abort-wipe", ("departTime", departTime))); component.NextAutoAbortCheck = TimeSpan.FromDays(1); // prevent further checks var newEndTime = _timing.CurTime + TimeSpan.FromSeconds(departTime); From d522098cc144f396ee3f8f16f42a9bb9466d4316 Mon Sep 17 00:00:00 2001 From: DDrakov <157034866+DDrakov@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:31:45 +0700 Subject: [PATCH 11/11] change function and remove the sound because we don't have those --- Content.Server/Salvage/SalvageSystem.Runner.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 93d12ede51a..8d3f6d37990 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -279,13 +279,11 @@ private void UpdateRunner() if (!mindC.HasMind) continue; // move them to the shuttle - deadLoserDestinations ??= GetDeadLoserDestinations(shuttleGrid.Value); + deadLoserDestinations ??= GetDeadPersonDestinations(shuttleGrid.Value); RescuePlayer( mobUid, deadLoserDestinations, shuttleGrid.Value); - SoundSpecifier sound = new SoundPathSpecifier("/Audio/_COYOTE/ExpedReturnToBed.ogg"); - _audio.PlayPvs(sound, mobUid); } } @@ -463,7 +461,7 @@ private void RescuePlayer( /// 3: Chairs with no mobs in them. /// 4: I dunno the console I guess /// - private DestinationPriority GetDeadLoserDestinations(EntityUid shuttleGrid) + private DestinationPriority GetDeadPersonDestinations(EntityUid shuttleGrid) { DestinationPriority destinations = new(); // first, find the exped consoles on the grid