diff --git a/Content.Server/Cargo/Components/TradeStationComponent.cs b/Content.Server/Cargo/Components/TradeStationComponent.cs
deleted file mode 100644
index 0422470cc9..0000000000
--- a/Content.Server/Cargo/Components/TradeStationComponent.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Content.Server.Cargo.Components;
-
-///
-/// Target for approved orders to spawn at.
-///
-[RegisterComponent]
-public sealed partial class TradeStationComponent : Component
-{
-
-}
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
index 351451123c..635eadea1d 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
@@ -6,6 +6,7 @@ using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Events;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.CCVar;
+using Content.Shared.HijackBeacon;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -14,7 +15,7 @@ namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem
{
/*
- * Handles cargo shuttle / trade mechanics.
+ * Handles automated trade station / trade mechanics.
*/
private static readonly SoundPathSpecifier ApproveSound = new("/Audio/Effects/Cargo/ping.ogg");
diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs
index 9f3a4d5bf3..a35f1e7ea3 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.cs
@@ -7,6 +7,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Cargo;
+using Content.Shared.Cargo.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Mobs.Components;
using Content.Shared.Paper;
diff --git a/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs b/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs
new file mode 100644
index 0000000000..416c422837
--- /dev/null
+++ b/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+///
+/// Objective condition that requires the player to hijack the trade station.
+///
+[RegisterComponent, Access(typeof(HijackTradeStationConditionSystem))]
+public sealed partial class HijackTradeStationConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs b/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs
new file mode 100644
index 0000000000..e5f88a28dd
--- /dev/null
+++ b/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs
@@ -0,0 +1,35 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Cargo.Components;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+using NetCord;
+
+namespace Content.Server.Objectives.Systems;
+
+///
+/// Handles the Hijack Trade Station objective.
+///
+public sealed class HijackTradeStationConditionSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetProgress);
+ }
+
+ private void OnGetProgress(Entity ent, ref ObjectiveGetProgressEvent args)
+ {
+ var enumerator = EntityQueryEnumerator();
+ args.Progress = 0f;
+ // If there's any hacked trade station, succeed.
+ while (enumerator.MoveNext(out var comp))
+ {
+ if (!comp.Hacked)
+ continue;
+
+ args.Progress = 1f;
+ return;
+ }
+ }
+}
diff --git a/Content.Shared/Cargo/Components/TradeStationComponent.cs b/Content.Shared/Cargo/Components/TradeStationComponent.cs
new file mode 100644
index 0000000000..ec0cba5386
--- /dev/null
+++ b/Content.Shared/Cargo/Components/TradeStationComponent.cs
@@ -0,0 +1,17 @@
+using Content.Shared.HijackBeacon;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Cargo.Components;
+
+///
+/// Target for approved orders to spawn at.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TradeStationComponent : Component
+{
+ ///
+ /// The Trade Station's current hijack state. Modified by HijackBeaconSystem.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool Hacked = false;
+}
diff --git a/Content.Shared/Cargo/SharedCargoSystem.cs b/Content.Shared/Cargo/SharedCargoSystem.cs
index 9d044d1850..291d3abbc3 100644
--- a/Content.Shared/Cargo/SharedCargoSystem.cs
+++ b/Content.Shared/Cargo/SharedCargoSystem.cs
@@ -1,5 +1,6 @@
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
+using Content.Shared.HijackBeacon;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
@@ -17,6 +18,7 @@ public abstract class SharedCargoSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnHijackSuccess);
}
private void OnMapInit(Entity ent, ref MapInitEvent args)
@@ -25,6 +27,23 @@ public abstract class SharedCargoSystem : EntitySystem
Dirty(ent);
}
+ private void OnHijackSuccess(ref HijackBeaconSuccessEvent args)
+ {
+ var stationQuery = EntityQueryEnumerator();
+ while (stationQuery.MoveNext(out var uid, out var comp))
+ {
+ foreach (var (account, cash) in comp.Accounts)
+ {
+ comp.Accounts[account] = cash - args.Fine;
+ args.Total += args.Fine;
+ }
+
+ var ev = new BankBalanceUpdatedEvent(uid, comp.Accounts);
+ RaiseLocalEvent(uid, ref ev, true);
+ Dirty(uid, comp);
+ }
+ }
+
///
/// For a given station, retrieves the balance in a specific account.
///
diff --git a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
index 3985bd3051..b290aae404 100644
--- a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
+++ b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
@@ -235,12 +235,8 @@ public sealed partial class AnchorableSystem : EntitySystem
// Log anchor attempt (server only)
_adminLogger.Add(LogType.Anchor, LogImpact.Low, $"{ToPrettyString(userUid):user} is trying to anchor {ToPrettyString(uid):entity} to {transform.Coordinates:targetlocation}");
- if (TryComp(uid, out var anchorBody) &&
- !TileFree(transform.Coordinates, anchorBody))
- {
- _popup.PopupClient(Loc.GetString("anchorable-occupied"), uid, userUid);
+ if (!CanAnchorAt(uid, transform.Coordinates, userUid))
return;
- }
if (AnyUnstackable(uid, transform.Coordinates))
{
@@ -285,6 +281,23 @@ public sealed partial class AnchorableSystem : EntitySystem
return !attempt.Cancelled;
}
+ public bool CanAnchorAt(Entity entity, EntityUid? user = null)
+ {
+ return CanAnchorAt(entity, Transform(entity).Coordinates, user);
+ }
+
+ public bool CanAnchorAt(Entity entity, EntityCoordinates coordinates, EntityUid? user = null)
+ {
+ if (!Resolve(entity, ref entity.Comp))
+ return true;
+
+ if (TileFree(coordinates, entity.Comp))
+ return true;
+
+ _popup.PopupClient(Loc.GetString("anchorable-occupied"), entity, user);
+ return false;
+ }
+
///
/// Returns true if no hard anchored entities exist on the coordinate tile that would collide with the provided physics body.
///
diff --git a/Content.Shared/HijackBeacon/ActiveHijackBeaconComponent.cs b/Content.Shared/HijackBeacon/ActiveHijackBeaconComponent.cs
new file mode 100644
index 0000000000..b6f1d60c79
--- /dev/null
+++ b/Content.Shared/HijackBeacon/ActiveHijackBeaconComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.HijackBeacon;
+
+///
+/// This is used for tracking a that is currently activated or on cooldown.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ActiveHijackBeaconComponent : Component
+{
+ ///
+ /// Remaining time until the hijack is completed.
+ ///
+ [DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))]
+ public TimeSpan CompletionTime = TimeSpan.Zero;
+}
diff --git a/Content.Shared/HijackBeacon/HijackBeaconComponent.cs b/Content.Shared/HijackBeacon/HijackBeaconComponent.cs
new file mode 100644
index 0000000000..ed1d3ca74b
--- /dev/null
+++ b/Content.Shared/HijackBeacon/HijackBeaconComponent.cs
@@ -0,0 +1,53 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.HijackBeacon;
+
+///
+/// Component for hijack beacons, meant to be planted on the ATS to drain station funds.
+///
+///
+/// Status and timer fields are private so the state machine is preserved.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class HijackBeaconComponent : Component
+{
+ ///
+ /// Current state of the beacon.
+ ///
+ [DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))]
+ public HijackBeaconStatus Status = HijackBeaconStatus.AwaitActivate;
+
+ ///
+ /// How long it takes to deactivate the beacon.
+ ///
+ [DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))]
+ public TimeSpan DeactivationLength = TimeSpan.FromSeconds(5);
+
+ ///
+ /// Remaining time until the hijack is completed.
+ ///
+ [DataField, Access(typeof(HijackBeaconSystem))]
+ public TimeSpan RemainingTime = TimeSpan.FromSeconds(200);
+
+ ///
+ /// Default amount of time before the beacon can be re-activated, if it is disarmed.
+ ///
+ [DataField, Access(typeof(HijackBeaconSystem))]
+ public TimeSpan Cooldown = TimeSpan.FromSeconds(20);
+
+ ///
+ /// Remaining cooldown time before the beacon can be reactivated.
+ ///
+ [DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))]
+ public TimeSpan CooldownTime = TimeSpan.Zero;
+
+ ///
+ /// How much cash should be withdrawn from each department account?
+ ///
+ [DataField]
+ public int Fine = 5000;
+}
diff --git a/Content.Shared/HijackBeacon/HijackBeaconSystem.cs b/Content.Shared/HijackBeacon/HijackBeaconSystem.cs
new file mode 100644
index 0000000000..d3e5810fba
--- /dev/null
+++ b/Content.Shared/HijackBeacon/HijackBeaconSystem.cs
@@ -0,0 +1,353 @@
+using Content.Shared.Cargo.Components;
+using Content.Shared.Chat;
+using Content.Shared.Construction.Components;
+using Content.Shared.Construction.EntitySystems;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Audio;
+using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.HijackBeacon;
+
+public sealed class HijackBeaconSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly AnchorableSystem _anchor = default!;
+ [Dependency] private readonly SharedChatSystem _chat = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+ public readonly SoundSpecifier AnnounceSound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
+ public readonly SoundSpecifier DeactivateSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg");
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(OnGetAltVerbs);
+ SubscribeLocalEvent(OnUnanchorAttempt);
+ SubscribeLocalEvent(OnAnchorChanged);
+ SubscribeLocalEvent(OnDeactivateDoAfter);
+ SubscribeLocalEvent(OnExaminedEvent);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var active, out var comp))
+ {
+ switch (comp.Status)
+ {
+ case HijackBeaconStatus.Armed:
+ if (_gameTiming.CurTime < active.CompletionTime)
+ return;
+
+ HijackFinish((uid, comp));
+ Dirty(uid, comp);
+ break;
+ case HijackBeaconStatus.Cooldown:
+ if (comp.CooldownTime < _gameTiming.CurTime)
+ {
+ comp.Status = HijackBeaconStatus.AwaitActivate;
+ RemCompDeferred(uid);
+ Dirty(uid, comp);
+ }
+ break;
+ }
+ }
+ }
+
+ #region Event Subs
+
+ ///
+ /// Deactivate beacon if it gets unanchored(via a bomb or something)
+ ///
+ private void OnAnchorChanged(Entity ent, ref AnchorStateChangedEvent args)
+ {
+ // Unanchoring the beacon deactivates it. This is to prevent people from bombing the tile the beacon is on and running away with it for a free activation.
+ if (!args.Anchored && ent.Comp.Status == HijackBeaconStatus.Armed)
+ DeactivateBeacon(ent);
+ }
+
+ private void OnUnanchorAttempt(Entity entity, ref UnanchorAttemptEvent args)
+ {
+ if (entity.Comp.Status == HijackBeaconStatus.Armed)
+ args.Cancel();
+ }
+
+ ///
+ /// Get the activation and deactivation verbs.
+ ///
+ private void OnGetAltVerbs(Entity ent, ref GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract || args.Hands is null)
+ return;
+
+ if (ent.Comp.Status == HijackBeaconStatus.AwaitActivate)
+ {
+ args.Verbs.Add(new()
+ {
+ Act = () =>
+ {
+ ActivateBeacon(ent);
+ },
+ Text = Loc.GetString("hijack-beacon-verb-activate-text"),
+ Message = Loc.GetString("hijack-beacon-verb-activate-message"),
+ Disabled = ent.Comp.Status != HijackBeaconStatus.AwaitActivate || !CanActivate(ent),
+ TextStyleClass = "InteractionVerb",
+ Impact = LogImpact.High,
+ });
+ }
+
+ if (ent.Comp.Status == HijackBeaconStatus.Armed)
+ {
+ var user = args.User;
+
+ args.Verbs.Add(new()
+ {
+ Act = () =>
+ {
+ DeactivateBeaconDoAfter(ent, user);
+ },
+ Text = Loc.GetString("hijack-beacon-verb-deactivate-text"),
+ Message = Loc.GetString("hijack-beacon-verb-deactivate-message"),
+ TextStyleClass = "InteractionVerb",
+ Impact = LogImpact.High,
+ });
+ }
+ }
+
+ ///
+ /// When it's examined.
+ ///
+ private void OnExaminedEvent(Entity ent, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ switch (ent.Comp.Status)
+ {
+ case HijackBeaconStatus.AwaitActivate:
+ args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-activate"));
+ break;
+ case HijackBeaconStatus.Armed:
+ args.PushMarkup(Loc.GetString("defusable-examine-live",
+ ("name", ent),
+ ("time", GetRemainingTime(ent.Owner))));
+ break;
+ case HijackBeaconStatus.Cooldown:
+ args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-cooldown"));
+ break;
+ case HijackBeaconStatus.HijackComplete:
+ args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-hijack-complete"));
+ break;
+ }
+ }
+
+ ///
+ /// What happens when you deactivate the beacon.
+ ///
+ private void OnDeactivateDoAfter(Entity ent, ref HijackBeaconDeactivateDoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled)
+ return;
+
+ DeactivateBeacon(ent);
+
+ args.Handled = true;
+ }
+
+ #endregion
+
+ ///
+ /// Arming the beacon. Should only occur if on the ATS.
+ ///
+ private void ActivateBeacon(Entity ent)
+ {
+ if (ent.Comp.Status != HijackBeaconStatus.AwaitActivate || !CanActivate(ent))
+ return;
+
+ // Activate and start countdown.
+ // Remaining time is adjusted by current time to simplify the update loop.
+ EnsureComp(ent, out var activeComp);
+ activeComp.CompletionTime = _gameTiming.CurTime + ent.Comp.RemainingTime;
+ ent.Comp.Status = HijackBeaconStatus.Armed;
+
+ //global announcement
+ var sender = Loc.GetString("hijack-beacon-announcement-sender");
+ var message = Loc.GetString("hijack-beacon-announcement-activated", ("time", GetRemainingTime((ent.Owner, activeComp))));
+ _chat.DispatchGlobalAnnouncement(message, sender, true, AnnounceSound, Color.Yellow);
+
+ //Anchor. Anchoring is tied to activation.
+ Anchor(ent);
+
+ Dirty(ent);
+ }
+
+ ///
+ /// Deactivates the beacon.
+ ///
+ private void DeactivateBeacon(Entity ent)
+ {
+ if (ent.Comp.Status != HijackBeaconStatus.Armed)
+ return;
+
+ // Put beacon on cooldown
+ ent.Comp.CooldownTime = ent.Comp.Cooldown + _gameTiming.CurTime;
+ ent.Comp.Status = HijackBeaconStatus.Cooldown;
+
+ //global announcement
+ var sender = Loc.GetString("hijack-beacon-announcement-sender");
+ var message = Loc.GetString("hijack-beacon-announcement-deactivated");
+ _chat.DispatchGlobalAnnouncement(message, sender, true, DeactivateSound, Color.Green);
+
+ // Unanchor. we want anchoring to be tied to activation here so we just call this.
+ Unanchor(ent);
+
+ Dirty(ent);
+ }
+
+ ///
+ /// Complete the hijack. The beacon equivalent to a nuke detonation
+ ///
+ private void HijackFinish(Entity ent)
+ {
+ if (ent.Comp.Status != HijackBeaconStatus.Armed)
+ return;
+
+ // Hijack is completed and can't be reattempted
+ ent.Comp.Status = HijackBeaconStatus.HijackComplete;
+ RemCompDeferred(ent);
+
+ var beaconXForm = Transform(ent);
+
+ // Ensure we are on the trade station still
+ if (!TryComp(beaconXForm.GridUid, out var station))
+ {
+ Log.Warning($"Trade station hijack tried to succeed on non-trade station grid {beaconXForm.GridUid}!");
+ DeactivateBeacon(ent);
+ return;
+ }
+
+ if (station.Hacked)
+ {
+ Log.Warning("Hack succeeded on an already hacked trade station!");
+ return;
+ }
+
+ // Mark the station as hacked.
+ station.Hacked = true;
+ Dirty(beaconXForm.GridUid.Value, station);
+
+ // Broadcast that the ATS has been hacked.
+ var ev = new HijackBeaconSuccessEvent(ent.Comp.Fine);
+ RaiseLocalEvent(ref ev);
+
+ //global announcement
+ var sender = Loc.GetString("hijack-beacon-announcement-sender");
+ var message = Loc.GetString("hijack-beacon-announcement-success", ("fine", ev.Total));
+ _chat.DispatchGlobalAnnouncement(message, sender, true, AnnounceSound, Color.Red);
+
+ // Unanchoring must occur after updating the status, or it will disarm the beacon
+ Unanchor(ent, beaconXForm);
+
+ Dirty(ent);
+ }
+
+ ///
+ /// Starts the deactivation doafter.
+ ///
+ private void DeactivateBeaconDoAfter(Entity beacon, EntityUid user)
+ {
+ var doAfter = new DoAfterArgs(EntityManager, user, beacon.Comp.DeactivationLength, new HijackBeaconDeactivateDoAfterEvent(), beacon)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ NeedHand = true,
+ };
+
+ // (try to) start doafter
+ _doAfter.TryStartDoAfter(doAfter);
+ }
+
+ #region Helpers
+
+ ///
+ /// Check if the beacon is on the Trade Station and if the Trade Station has not been hijacked already.
+ ///
+ private bool CanActivate(Entity ent)
+ {
+ return TryComp(Transform(ent).GridUid, out TradeStationComponent? tradeStation) && !tradeStation.Hacked && _anchor.CanAnchorAt(ent.Owner);
+ }
+
+ ///
+ /// Anchoring helper
+ ///
+ private void Anchor(Entity ent, TransformComponent? beaconXForm = null)
+ {
+ beaconXForm ??= Transform(ent);
+
+ if (beaconXForm.Anchored || beaconXForm.GridUid == null)
+ return;
+
+ _transform.AnchorEntity(ent, beaconXForm);
+ _popup.PopupPredicted(Loc.GetString("hijack-beacon-popup-anchor"), ent, null);
+ }
+
+ ///
+ /// Unanchoring helper
+ ///
+ private void Unanchor(Entity ent, TransformComponent? beaconXForm = null)
+ {
+ beaconXForm ??= Transform(ent);
+
+ if (!beaconXForm.Anchored)
+ return;
+
+ _transform.Unanchor(ent, beaconXForm);
+ _popup.PopupPredicted(Loc.GetString("hijack-beacon-popup-unanchor"), ent, null);
+ }
+
+ ///
+ /// Turns time values into usable values for announcements/examine messages
+ ///
+ private int GetRemainingTime(Entity ent)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return 69420; // Mature error code
+
+ return (int) (ent.Comp.CompletionTime - _gameTiming.CurTime).TotalSeconds;
+ }
+
+ #endregion
+}
+
+public enum HijackBeaconStatus : byte
+{
+ AwaitActivate,
+ Armed,
+ Cooldown,
+ HijackComplete
+}
+
+///
+/// DoAfter event raised when the hijack beacon is deactivated.
+///
+[Serializable, NetSerializable]
+public sealed partial class HijackBeaconDeactivateDoAfterEvent : SimpleDoAfterEvent;
+
+///
+/// Event raised when the hijack beacon succeeds in hijacking the ATS.
+///
+[ByRefEvent]
+public record struct HijackBeaconSuccessEvent(int Fine)
+{
+ public int Total = 0;
+};
diff --git a/Resources/Locale/en-US/hijack-beacon/hijack-beacon.ftl b/Resources/Locale/en-US/hijack-beacon/hijack-beacon.ftl
new file mode 100644
index 0000000000..a98370e891
--- /dev/null
+++ b/Resources/Locale/en-US/hijack-beacon/hijack-beacon.ftl
@@ -0,0 +1,16 @@
+hijack-beacon-announcement-sender = Automated Trade Station
+hijack-beacon-announcement-activated = Attention! An Attempted Breach of the Automated Trade Station's firewall has been detected! Estimated {$time} seconds until firewall breach!
+hijack-beacon-announcement-deactivated = Firewall breach failed. Firewall integrity partially restored. Have a nice day!
+hijack-beacon-announcement-success = Successfully disengaged Automated Trade Station firewall. {$fine} spessos has been transferred from station funds to [%ERROR%]. Your trade station warranty is now void. This incident has been reported.
+
+hijack-beacon-examine-await-activate = The beacon is [color=green]ready to activate[/color].
+hijack-beacon-examine-await-cooldown = The beacon is [color=red]on cooldown[/color].
+hijack-beacon-examine-await-hijack-complete = The beacon is [color=red]spent[/color].
+
+hijack-beacon-popup-anchor = The beacon anchors itself into the ground!
+hijack-beacon-popup-unanchor = The beacon unanchors itself from the ground.
+
+hijack-beacon-verb-activate-text = Activate
+hijack-beacon-verb-activate-message = The beacon can only be armed on the Automated Trade Station, on an unoccupied tile.
+hijack-beacon-verb-deactivate-text = Deactivate
+hijack-beacon-verb-deactivate-message = The beacon isn't going to deactivate itself, you know.
diff --git a/Resources/Locale/en-US/store/categories.ftl b/Resources/Locale/en-US/store/categories.ftl
index 4469a576cd..9ecf4f91d2 100644
--- a/Resources/Locale/en-US/store/categories.ftl
+++ b/Resources/Locale/en-US/store/categories.ftl
@@ -12,6 +12,7 @@ store-category-allies = Allies
store-category-job = Job
store-category-wearables = Wearables
store-category-pointless = Pointless
+store-category-objective = Objective
store-discounted-items = Discounts
# Revenant
diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl
index 4f67fae1cd..685a4d83dd 100644
--- a/Resources/Locale/en-US/store/uplink-catalog.ftl
+++ b/Resources/Locale/en-US/store/uplink-catalog.ftl
@@ -520,3 +520,7 @@ uplink-briefcase-gun-desc = An indistinct briefcase with a highly compact C-20K
uplink-energycrossbow-name = Mini Energy Crossbow
uplink-energycrossbow-desc = The go-to sidearm of any operative who prefers their victims not to be moving. Fires regenerating toxic arrows that floors victims in an instant.
+
+#Objective items
+uplink-hijack-beacon-name = Hijack Beacon
+uplink-hijack-beacon-desc = A syndicate-brand hijack beacon designed to get around the firewalls of Nanotrasen-brand Automated Trade Stations. They take 200 seconds to work and Trade Stations will announce they are being hacked, so prepare accordingly.
diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml
index e232308d01..98c2206f6a 100644
--- a/Resources/Prototypes/Catalog/uplink_catalog.yml
+++ b/Resources/Prototypes/Catalog/uplink_catalog.yml
@@ -2268,3 +2268,20 @@
- !type:BuyerJobCondition
whitelist:
- Lawyer
+
+#Objective items
+
+- type: listing
+ id: uplinkHijackBeacon
+ name: uplink-hijack-beacon-name
+ description: uplink-hijack-beacon-desc
+ productEntity: HijackBeacon
+ categories:
+ - UplinkObjective
+ conditions:
+ - !type:ListingLimitedStockCondition
+ stock: 1
+ - !type:BuyerObjectiveWhitelistCondition
+ whitelist:
+ components:
+ - HijackTradeStationCondition
diff --git a/Resources/Prototypes/Entities/Objects/Tools/hijack_beacon.yml b/Resources/Prototypes/Entities/Objects/Tools/hijack_beacon.yml
new file mode 100644
index 0000000000..d15b845322
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Tools/hijack_beacon.yml
@@ -0,0 +1,34 @@
+- type: entity
+ parent: [ BaseItem, BaseSyndicateContraband ]
+ id: HijackBeacon
+ name: hijack beacon
+ description: A device that bypasses the firewall on Nanotrasen-brand Automated Trade Stations.
+ components:
+ - type: Transform
+ anchored: false
+ noRot: true
+ - type: Physics
+ bodyType: Dynamic
+ - type: Anchorable
+ - type: Item
+ size: Normal
+ - type: HijackBeacon
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.3,-0.3,0.3,0.3"
+ density: 190
+ mask:
+ - MachineMask
+ layer:
+ - MachineLayer
+ - type: Clickable
+ - type: InteractionOutline
+ - type: Appearance
+ - type: Sprite
+ sprite: Objects/Tools/hijack_beacon.rsi
+ noRot: true
+ layers:
+ - state: extraction_point
diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml
index 9aa13fa74a..bbdd6fa236 100644
--- a/Resources/Prototypes/Objectives/objectiveGroups.yml
+++ b/Resources/Prototypes/Objectives/objectiveGroups.yml
@@ -6,6 +6,7 @@
TraitorObjectiveGroupKill: 1
TraitorObjectiveGroupState: 1 #As in, something about your character. Alive, dead, arrested, gained an ability...
TraitorObjectiveGroupSocial: 1 #Involves helping/harming others without killing them or stealing their stuff
+ TraitorObjectiveGroupOther: 1
- type: weightedRandom
id: TraitorObjectiveGroupSteal
@@ -43,6 +44,13 @@
# RandomTraitorAliveObjective: 1 - Removed because the objective was boring and didn't progress the round.
RandomTraitorProgressObjective: 1
+#misc objectives that don't fall other another category
+- type: weightedRandom
+ id: TraitorObjectiveGroupOther
+ weights:
+ HijackTradeStationObjective: 1
+
+
#Thief groups
- type: weightedRandom
id: ThiefObjectiveGroups
diff --git a/Resources/Prototypes/Objectives/traitor.yml b/Resources/Prototypes/Objectives/traitor.yml
index 4825fa4565..9ffc0146a2 100644
--- a/Resources/Prototypes/Objectives/traitor.yml
+++ b/Resources/Prototypes/Objectives/traitor.yml
@@ -352,3 +352,20 @@
- type: StealCondition
stealGroup: NukeDisk
owner: objective-condition-steal-station
+
+# other
+- type: entity
+ parent: BaseTraitorObjective
+ id: HijackTradeStationObjective
+ name: Hijack the Automated Trade Station.
+ description: Your uplink has been authorized one hijack beacon. Deploy it on the Automated Trade Station and defend it whilst it hijacks the trade station.
+ components:
+ - type: Objective
+ difficulty: 3
+ icon:
+ sprite: Objects/Tools/hijack_beacon.rsi
+ state: extraction_point
+ - type: HijackTradeStationCondition
+ - type: ObjectiveLimit
+ # There is only one trade station so there should never be more than one of these objectives.
+ limit: 1
diff --git a/Resources/Prototypes/Store/categories.yml b/Resources/Prototypes/Store/categories.yml
index ccdb349d9c..a4c339d0e7 100644
--- a/Resources/Prototypes/Store/categories.yml
+++ b/Resources/Prototypes/Store/categories.yml
@@ -89,6 +89,11 @@
name: store-category-pointless
priority: 10
+- type: storeCategory
+ id: UplinkObjective
+ name: store-category-objective
+ priority: 11
+
#nukie delivery
- type: storeCategory
diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml
index 37329d4096..13823a40f1 100644
--- a/Resources/Prototypes/Store/presets.yml
+++ b/Resources/Prototypes/Store/presets.yml
@@ -16,6 +16,7 @@
- UplinkWearables
- UplinkJob
- UplinkPointless
+ - UplinkObjective
currencyWhitelist:
- Telecrystal
balance:
diff --git a/Resources/Textures/Objects/Tools/hijack_beacon.rsi/extraction_point.png b/Resources/Textures/Objects/Tools/hijack_beacon.rsi/extraction_point.png
new file mode 100644
index 0000000000..7b8dd4c3af
Binary files /dev/null and b/Resources/Textures/Objects/Tools/hijack_beacon.rsi/extraction_point.png differ
diff --git a/Resources/Textures/Objects/Tools/hijack_beacon.rsi/meta.json b/Resources/Textures/Objects/Tools/hijack_beacon.rsi/meta.json
new file mode 100644
index 0000000000..5177acf42f
--- /dev/null
+++ b/Resources/Textures/Objects/Tools/hijack_beacon.rsi/meta.json
@@ -0,0 +1,20 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from https://github.com/austation/austation/commit/e2a4fefd01e702f48d3d4cc8d6a2686d54d104fa and edited by TheShuEd. Recolored by alexalexmax",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "extraction_point",
+ "delays": [
+ [
+ 0.5,
+ 0.5
+ ]
+ ]
+ }
+ ]
+}