--- /dev/null
+using Content.Shared.CriminalRecords.Systems;
+
+namespace Content.Client.CriminalRecords.Systems;
+
+public sealed class CriminalRecordsHackerSystem : SharedCriminalRecordsHackerSystem
+{
+}
--- /dev/null
+using Content.Server.Chat.Systems;
+using Content.Server.Station.Systems;
+using Content.Server.StationRecords.Systems;
+using Content.Shared.CriminalRecords;
+using Content.Shared.CriminalRecords.Components;
+using Content.Shared.CriminalRecords.Systems;
+using Content.Shared.Dataset;
+using Content.Shared.Security;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.CriminalRecords.Systems;
+
+public sealed class CriminalRecordsHackerSystem : SharedCriminalRecordsHackerSystem
+{
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly StationRecordsSystem _records = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<CriminalRecordsHackerComponent, CriminalRecordsHackDoAfterEvent>(OnDoAfter);
+ }
+
+ private void OnDoAfter(Entity<CriminalRecordsHackerComponent> ent, ref CriminalRecordsHackDoAfterEvent args)
+ {
+ if (args.Cancelled || args.Handled || args.Target == null)
+ return;
+
+ if (_station.GetOwningStation(ent) is not {} station)
+ return;
+
+ var reasons = _proto.Index<DatasetPrototype>(ent.Comp.Reasons);
+ foreach (var (key, record) in _records.GetRecordsOfType<CriminalRecord>(station))
+ {
+ var reason = _random.Pick(reasons.Values);
+ record.Status = SecurityStatus.Wanted;
+ record.Reason = reason;
+ // no radio message since spam
+ // no history since lazy and its easy to remove anyway
+ // main damage with this is existing arrest warrants are lost and to anger beepsky
+ }
+
+ _chat.DispatchGlobalAnnouncement(Loc.GetString(ent.Comp.Announcement), playSound: true, colorOverride: Color.Red);
+
+ // once is enough
+ RemComp<CriminalRecordsHackerComponent>(ent);
+
+ var ev = new CriminalRecordsHackedEvent(ent, args.Target.Value);
+ RaiseLocalEvent(args.User, ref ev);
+ }
+}
+
+/// <summary>
+/// Raised on the user after hacking a criminal records console.
+/// </summary>
+[ByRefEvent]
+public record struct CriminalRecordsHackedEvent(EntityUid User, EntityUid Target);
using Content.Server.Communications;
using Content.Server.Mind;
using Content.Server.Ninja.Events;
-using Content.Server.Objectives.Components;
+using Content.Server.Objectives.Systems;
using Content.Shared.Communications;
+using Content.Shared.CriminalRecords.Components;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Research.Components;
public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem
{
[Dependency] private readonly EmagProviderSystem _emagProvider = default!;
+ [Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly CommsHackerSystem _commsHacker = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedStunProviderSystem _stunProvider = default!;
EnsureComp<ResearchStealerComponent>(user);
// prevent calling in multiple threats by toggling gloves after
- if (_mind.TryGetObjectiveComp<TerrorConditionComponent>(user, out var obj) && !obj.CalledInThreat)
+ if (!_codeCondition.IsCompleted(user, ninja.TerrorObjective))
{
var hacker = EnsureComp<CommsHackerComponent>(user);
var rule = _ninja.NinjaRule(user);
if (rule != null)
_commsHacker.SetThreats(user, rule.Threats, hacker);
}
+ if (!_codeCondition.IsCompleted(user, ninja.MassArrestObjective))
+ {
+ EnsureComp<CriminalRecordsHackerComponent>(user);
+ }
}
}
using Content.Server.Communications;
using Content.Server.Chat.Managers;
+using Content.Server.CriminalRecords.Systems;
using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GenericAntag;
+using Content.Server.Objectives.Components;
+using Content.Server.Objectives.Systems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Server.Research.Systems;
using Content.Server.Roles;
-using Content.Server.GenericAntag;
using Content.Shared.Alert;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Doors.Components;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using System.Diagnostics.CodeAnalysis;
-using Content.Server.Objectives.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Ninja.Systems;
// engi -> saboteur
// medi -> idk reskin it
// other -> assault
-// TODO: when criminal records is merged, hack it to set everyone to arrest
/// <summary>
/// Main ninja system that handles ninja setup, provides helper methods for the rest of the code to use.
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly BatterySystem _battery = default!;
+ [Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly RoleSystem _role = default!;
SubscribeLocalEvent<SpaceNinjaComponent, EmaggedSomethingEvent>(OnDoorjack);
SubscribeLocalEvent<SpaceNinjaComponent, ResearchStolenEvent>(OnResearchStolen);
SubscribeLocalEvent<SpaceNinjaComponent, ThreatCalledInEvent>(OnThreatCalledIn);
+ SubscribeLocalEvent<SpaceNinjaComponent, CriminalRecordsHackedEvent>(OnCriminalRecordsHacked);
}
public override void Update(float frameTime)
Popup.PopupEntity(str, uid, uid, PopupType.Medium);
}
- private void OnThreatCalledIn(EntityUid uid, SpaceNinjaComponent comp, ref ThreatCalledInEvent args)
+ private void OnThreatCalledIn(Entity<SpaceNinjaComponent> ent, ref ThreatCalledInEvent args)
{
- if (_mind.TryGetObjectiveComp<TerrorConditionComponent>(uid, out var obj))
- {
- obj.CalledInThreat = true;
- }
+ _codeCondition.SetCompleted(ent.Owner, ent.Comp.TerrorObjective);
+ }
+
+ private void OnCriminalRecordsHacked(Entity<SpaceNinjaComponent> ent, ref CriminalRecordsHackedEvent args)
+ {
+ _codeCondition.SetCompleted(ent.Owner, ent.Comp.MassArrestObjective);
+ }
+
+ /// <summary>
+ /// Called by <see cref="SpiderChargeSystem"/> when it detonates.
+ /// </summary>
+ public void DetonatedSpiderCharge(Entity<SpaceNinjaComponent> ent)
+ {
+ _codeCondition.SetCompleted(ent.Owner, ent.Comp.SpiderChargeObjective);
}
}
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SpaceNinjaSystem _ninja = default!;
public override void Initialize()
{
/// </summary>
private void OnExplode(EntityUid uid, SpiderChargeComponent comp, TriggerEvent args)
{
- if (comp.Planter == null || !_mind.TryGetObjectiveComp<SpiderChargeConditionComponent>(comp.Planter.Value, out var obj))
+ if (!TryComp<SpaceNinjaComponent>(comp.Planter, out var ninja))
return;
// assumes the target was destroyed, that the charge wasn't moved somehow
- obj.Detonated = true;
+ _ninja.DetonatedSpiderCharge((comp.Planter.Value, ninja));
}
}
--- /dev/null
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// An objective that is set to complete by code in another system.
+/// Use <see cref="CodeConditionSystem"/> to check and set this.
+/// </summary>
+[RegisterComponent, Access(typeof(CodeConditionSystem))]
+public sealed partial class CodeConditionComponent : Component
+{
+ /// <summary>
+ /// Whether the objective is complete or not.
+ /// </summary>
+ [DataField]
+ public bool Completed;
+}
[RegisterComponent, Access(typeof(NinjaConditionsSystem), typeof(SpiderChargeSystem), typeof(SpaceNinjaSystem))]
public sealed partial class SpiderChargeConditionComponent : Component
{
- [DataField, ViewVariables(VVAccess.ReadWrite)]
- public bool Detonated;
-
/// <summary>
/// Warp point that the spider charge has to target
/// </summary>
+++ /dev/null
-using Content.Server.Objectives.Systems;
-using Content.Shared.Ninja.Systems;
-
-namespace Content.Server.Objectives.Components;
-
-/// <summary>
-/// Requires that the player is a ninja and has called in a threat.
-/// </summary>
-[RegisterComponent, Access(typeof(NinjaConditionsSystem), typeof(SharedSpaceNinjaSystem))]
-public sealed partial class TerrorConditionComponent : Component
-{
- /// <summary>
- /// Whether the comms console has been hacked
- /// </summary>
- [DataField("calledInThreat"), ViewVariables(VVAccess.ReadWrite)]
- public bool CalledInThreat;
-}
--- /dev/null
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles <see cref="CodeConditionComponent"/> progress and provides API for systems to use.
+/// </summary>
+public sealed class CodeConditionSystem : EntitySystem
+{
+ [Dependency] private readonly SharedMindSystem _mind = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<CodeConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
+ }
+
+ private void OnGetProgress(Entity<CodeConditionComponent> ent, ref ObjectiveGetProgressEvent args)
+ {
+ args.Progress = ent.Comp.Completed ? 1f : 0f;
+ }
+
+ /// <summary>
+ /// Returns whether an objective is completed.
+ /// </summary>
+ public bool IsCompleted(Entity<CodeConditionComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return false;
+
+ return ent.Comp.Completed;
+ }
+
+ /// <summary>
+ /// Returns true if a mob's objective with a certain prototype is completed.
+ /// </summary>
+ public bool IsCompleted(Entity<MindContainerComponent?> mob, string prototype)
+ {
+ if (_mind.GetMind(mob, mob.Comp) is not {} mindId)
+ return false;
+
+ if (!_mind.TryFindObjective(mindId, prototype, out var obj))
+ return false;
+
+ return IsCompleted(obj.Value);
+ }
+
+ /// <summary>
+ /// Sets an objective's completed field.
+ /// </summary>
+ public void SetCompleted(Entity<CodeConditionComponent?> ent, bool completed = true)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ ent.Comp.Completed = completed;
+ }
+
+ /// <summary>
+ /// Sets a mob's objective to complete.
+ /// </summary>
+ public void SetCompleted(Entity<MindContainerComponent?> mob, string prototype, bool completed = true)
+ {
+ if (_mind.GetMind(mob, mob.Comp) is not {} mindId)
+ return;
+
+ if (!_mind.TryFindObjective(mindId, prototype, out var obj))
+ return;
+
+ SetCompleted(obj.Value, completed);
+ }
+}
SubscribeLocalEvent<SpiderChargeConditionComponent, RequirementCheckEvent>(OnSpiderChargeRequirementCheck);
SubscribeLocalEvent<SpiderChargeConditionComponent, ObjectiveAfterAssignEvent>(OnSpiderChargeAfterAssign);
- SubscribeLocalEvent<SpiderChargeConditionComponent, ObjectiveGetProgressEvent>(OnSpiderChargeGetProgress);
SubscribeLocalEvent<StealResearchConditionComponent, ObjectiveGetProgressEvent>(OnStealResearchGetProgress);
-
- SubscribeLocalEvent<TerrorConditionComponent, ObjectiveGetProgressEvent>(OnTerrorGetProgress);
}
// doorjack
_metaData.SetEntityName(uid, title, args.Meta);
}
- private void OnSpiderChargeGetProgress(EntityUid uid, SpiderChargeConditionComponent comp, ref ObjectiveGetProgressEvent args)
- {
- args.Progress = comp.Detonated ? 1f : 0f;
- }
-
// steal research
private void OnStealResearchGetProgress(EntityUid uid, StealResearchConditionComponent comp, ref ObjectiveGetProgressEvent args)
return MathF.Min(comp.DownloadedNodes.Count / (float) target, 1f);
}
-
- private void OnTerrorGetProgress(EntityUid uid, TerrorConditionComponent comp, ref ObjectiveGetProgressEvent args)
- {
- args.Progress = comp.CalledInThreat ? 1f : 0f;
- }
}
--- /dev/null
+using Content.Shared.CriminalRecords.Systems;
+using Content.Shared.Dataset;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.CriminalRecords.Components;
+
+/// <summary>
+/// Lets the user hack a criminal records console, once.
+/// Everyone is set to wanted with a randomly picked reason.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedCriminalRecordsHackerSystem))]
+public sealed partial class CriminalRecordsHackerComponent : Component
+{
+ /// <summary>
+ /// How long the doafter is for hacking it.
+ /// </summary>
+ public TimeSpan Delay = TimeSpan.FromSeconds(20);
+
+ /// <summary>
+ /// Dataset of random reasons to use.
+ /// </summary>
+ [DataField]
+ public ProtoId<DatasetPrototype> Reasons = "CriminalRecordsWantedReasonPlaceholders";
+
+ /// <summary>
+ /// Announcement made after the console is hacked.
+ /// </summary>
+ [DataField]
+ public LocId Announcement = "ninja-criminal-records-hack-announcement";
+}
--- /dev/null
+using Content.Shared.CriminalRecords.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.Interaction;
+using Content.Shared.Ninja.Systems;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CriminalRecords.Systems;
+
+public abstract class SharedCriminalRecordsHackerSystem : EntitySystem
+{
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<CriminalRecordsHackerComponent, BeforeInteractHandEvent>(OnBeforeInteractHand);
+ }
+
+ private void OnBeforeInteractHand(Entity<CriminalRecordsHackerComponent> ent, ref BeforeInteractHandEvent args)
+ {
+ // TODO: generic event
+ if (args.Handled || !_gloves.AbilityCheck(ent, args, out var target))
+ return;
+
+ if (!HasComp<CriminalRecordsConsoleComponent>(target))
+ return;
+
+ var doAfterArgs = new DoAfterArgs(EntityManager, ent, ent.Comp.Delay, new CriminalRecordsHackDoAfterEvent(), target: target, used: ent, eventTarget: ent)
+ {
+ BreakOnDamage = true,
+ BreakOnUserMove = true,
+ MovementThreshold = 0.5f
+ };
+
+ _doAfter.TryStartDoAfter(doAfterArgs);
+ args.Handled = true;
+ }
+}
+
+/// <summary>
+/// Raised on the user when the doafter completes.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class CriminalRecordsHackDoAfterEvent : SimpleDoAfterEvent
+{
+}
return false;
}
+ /// <summary>
+ /// Tries to find an objective that has the same prototype as the argument.
+ /// </summary>
+ /// <remarks>
+ /// Will not work for objectives that have no prototype, or duplicate objectives with the same prototype.
+ /// <//remarks>
+ public bool TryFindObjective(Entity<MindComponent?> mind, string prototype, [NotNullWhen(true)] out EntityUid? objective)
+ {
+ objective = null;
+ if (!Resolve(mind, ref mind.Comp))
+ return false;
+
+ foreach (var uid in mind.Comp.Objectives)
+ {
+ if (MetaData(uid).EntityPrototype?.ID == prototype)
+ {
+ objective = uid;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public bool TryGetSession(EntityUid? mindId, [NotNullWhen(true)] out ICommonSession? session)
{
session = null;
using Content.Shared.Ninja.Systems;
using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
+using Robust.Shared.Prototypes;
namespace Content.Shared.Ninja.Components;
/// </summary>
[DataField("katana"), AutoNetworkedField]
public EntityUid? Katana;
+
+ /// <summary>
+ /// Objective to complete after calling in a threat.
+ /// </summary>
+ [DataField]
+ public EntProtoId TerrorObjective = "TerrorObjective";
+
+ /// <summary>
+ /// Objective to complete after setting everyone to arrest.
+ /// </summary>
+ [DataField]
+ public EntProtoId MassArrestObjective = "MassArrestObjective";
+
+ /// <summary>
+ /// Objective to complete after the spider charge detonates.
+ /// </summary>
+ [DataField]
+ public EntProtoId SpiderChargeObjective = "SpiderChargeObjective";
}
using Content.Shared.Actions;
using Content.Shared.CombatMode;
using Content.Shared.Communications;
+using Content.Shared.CriminalRecords.Components;
using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
RemComp<StunProviderComponent>(user);
RemComp<ResearchStealerComponent>(user);
RemComp<CommsHackerComponent>(user);
+ RemComp<CriminalRecordsHackerComponent>(user);
}
/// <summary>
ninja-research-steal-fail = No new research nodes were stolen...
ninja-research-steal-success = Stole {$count} new nodes from {THE($server)}.
+
+ninja-criminal-records-hack-announcement = ERROR: Criminal records has detected a [REDACTED] error #*;"
- type: dataset
id: CriminalRecordsWantedReasonPlaceholders
values:
+ - Ate a delicious valid salad
- Ate their own shoes
- Being a clown
- Being a mime
- Breathed the wrong way
- Broke into evac
- Did literally nothing
+ - Did their job
- Didn't say hello to me
- Drank one too many
+ - Had two toolboxes, that's too many
- Lied on common radio
- Looked at me funny
+ - Lubed up the entire way to evac
+ - Set AME up on time
- Slipped the HoS
- Stole the clown's mask
- Told an unfunny joke
- Wore a gasmask
+ - Wore boxing gloves
- DoorjackObjective
- SpiderChargeObjective
- TerrorObjective
+ - MassArrestObjective
- NinjaSurviveObjective
- type: NinjaRule
threats: NinjaThreats
id: BaseSurviveObjective
components:
- type: SurviveCondition
+
+# objective progress is controlled by a system and not the objective itself
+- type: entity
+ abstract: true
+ parent: BaseObjective
+ id: BaseCodeObjective
+ components:
+ - type: CodeCondition
- type: entity
noSpawn: true
- parent: BaseNinjaObjective
+ parent: [BaseNinjaObjective, BaseCodeObjective]
id: SpiderChargeObjective
description: This bomb can be detonated in a specific location. Note that the bomb will not work anywhere else!
components:
icon:
sprite: Objects/Weapons/Bombs/spidercharge.rsi
state: icon
- - type: SpiderChargeCondition
- type: entity
noSpawn: true
- type: entity
noSpawn: true
- parent: BaseNinjaObjective
+ parent: [BaseNinjaObjective, BaseCodeObjective]
id: TerrorObjective
name: Call in a threat
description: Use your gloves on a communication console in order to bring another threat to the station.
icon:
sprite: Objects/Fun/Instruments/otherinstruments.rsi
state: red_phone
- - type: TerrorCondition
+
+- type: entity
+ noSpawn: true
+ parent: [BaseNinjaObjective, BaseCodeObjective]
+ id: MassArrestObjective
+ name: Set everyone to wanted
+ description: Use your gloves to hack a criminal records console, setting the entire station to be wanted!
+ components:
+ - type: Objective
+ icon:
+ sprite: Objects/Weapons/Melee/stunbaton.rsi
+ state: stunbaton_on