Station AI ability to electricute doors (#32012)

* Boom! Emergency access!

* Emergency access sound

* locale

* Updated sounds

* bleh

* Door electrify base

* feat: popups on attempt to activate AI action when wires cut

* refactor: use SharedApcPowerReceiverComponent to check if AI can interact with door

* refactor: added icon and sound for door overcharge

* meta.json should use tabs not spaces

* refactor: extracted sounds for airlock overcharge to static field in system

* refactor: cleanup, ScarKy0 mentions for resources

* refactor: removed unused textures

* feat: now notification is displayed when AI attempting to interact with door which have wire cut

* StationAiWhitelistComponent is properly gating  BUI OnMessageAttempt, SharedPowerReceiverSystem.IsPowered is now used to check if device powered

* refactor: use PlayLocal to play electrify sound only for AI player

* refactor: SetBoltsDown now uses TrySetBoltDown, checks for power.

* bolts now check for power using SharedPowerReceiverSystem

* electrify localization and louder electrify sounds

* extracted ShowDeviceNotRespondingPopup, reverted airlocks not opening/closing when ai wire was cut

* refactor: cleanup

* New sprites and fixes

* Copyright

* even more sprite changes

* refactore: cleanup, rename overcharge => electrify

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Fildrance
2024-09-27 10:22:17 +03:00
committed by GitHub
parent 0d26bb0320
commit 4bcf3c3c0e
30 changed files with 371 additions and 109 deletions

View File

@@ -1,4 +1,5 @@
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Content.Shared.Electrocution;
using Content.Shared.Silicons.StationAi; using Content.Shared.Silicons.StationAi;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -6,25 +7,69 @@ namespace Content.Client.Silicons.StationAi;
public sealed partial class StationAiSystem public sealed partial class StationAiSystem
{ {
private readonly ResPath _aiActionsRsi = new ResPath("/Textures/Interface/Actions/actions_ai.rsi");
private void InitializeAirlock() private void InitializeAirlock()
{ {
SubscribeLocalEvent<DoorBoltComponent, GetStationAiRadialEvent>(OnDoorBoltGetRadial); SubscribeLocalEvent<DoorBoltComponent, GetStationAiRadialEvent>(OnDoorBoltGetRadial);
SubscribeLocalEvent<AirlockComponent, GetStationAiRadialEvent>(OnEmergencyAccessGetRadial);
SubscribeLocalEvent<ElectrifiedComponent, GetStationAiRadialEvent>(OnDoorElectrifiedGetRadial);
} }
private void OnDoorBoltGetRadial(Entity<DoorBoltComponent> ent, ref GetStationAiRadialEvent args) private void OnDoorBoltGetRadial(Entity<DoorBoltComponent> ent, ref GetStationAiRadialEvent args)
{ {
args.Actions.Add(new StationAiRadial() args.Actions.Add(
new StationAiRadial
{ {
Sprite = ent.Comp.BoltsDown ? Sprite = ent.Comp.BoltsDown
new SpriteSpecifier.Rsi( ? new SpriteSpecifier.Rsi(_aiActionsRsi, "unbolt_door")
new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "open") : : new SpriteSpecifier.Rsi(_aiActionsRsi, "bolt_door"),
new SpriteSpecifier.Rsi( Tooltip = ent.Comp.BoltsDown
new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "closed"), ? Loc.GetString("bolt-open")
Tooltip = ent.Comp.BoltsDown ? Loc.GetString("bolt-open") : Loc.GetString("bolt-close"), : Loc.GetString("bolt-close"),
Event = new StationAiBoltEvent() Event = new StationAiBoltEvent
{ {
Bolted = !ent.Comp.BoltsDown, Bolted = !ent.Comp.BoltsDown,
} }
}); }
);
}
private void OnEmergencyAccessGetRadial(Entity<AirlockComponent> ent, ref GetStationAiRadialEvent args)
{
args.Actions.Add(
new StationAiRadial
{
Sprite = ent.Comp.EmergencyAccess
? new SpriteSpecifier.Rsi(_aiActionsRsi, "emergency_off")
: new SpriteSpecifier.Rsi(_aiActionsRsi, "emergency_on"),
Tooltip = ent.Comp.EmergencyAccess
? Loc.GetString("emergency-access-off")
: Loc.GetString("emergency-access-on"),
Event = new StationAiEmergencyAccessEvent
{
EmergencyAccess = !ent.Comp.EmergencyAccess,
}
}
);
}
private void OnDoorElectrifiedGetRadial(Entity<ElectrifiedComponent> ent, ref GetStationAiRadialEvent args)
{
args.Actions.Add(
new StationAiRadial
{
Sprite = ent.Comp.Enabled
? new SpriteSpecifier.Rsi(_aiActionsRsi, "door_overcharge_off")
: new SpriteSpecifier.Rsi(_aiActionsRsi, "door_overcharge_on"),
Tooltip = ent.Comp.Enabled
? Loc.GetString("electrify-door-off")
: Loc.GetString("electrify-door-on"),
Event = new StationAiElectrifiedEvent
{
Electrified = !ent.Comp.Enabled,
}
}
);
} }
} }

View File

@@ -90,22 +90,22 @@ public sealed partial class AdminVerbSystem
args.Verbs.Add(bolt); args.Verbs.Add(bolt);
} }
if (TryComp<AirlockComponent>(args.Target, out var airlock)) if (TryComp<AirlockComponent>(args.Target, out var airlockComp))
{ {
Verb emergencyAccess = new() Verb emergencyAccess = new()
{ {
Text = airlock.EmergencyAccess ? "Emergency Access Off" : "Emergency Access On", Text = airlockComp.EmergencyAccess ? "Emergency Access Off" : "Emergency Access On",
Category = VerbCategory.Tricks, Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/emergency_access.png")), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/emergency_access.png")),
Act = () => Act = () =>
{ {
_airlockSystem.ToggleEmergencyAccess(args.Target, airlock); _airlockSystem.SetEmergencyAccess((args.Target, airlockComp), !airlockComp.EmergencyAccess);
}, },
Impact = LogImpact.Medium, Impact = LogImpact.Medium,
Message = Loc.GetString(airlock.EmergencyAccess Message = Loc.GetString(airlockComp.EmergencyAccess
? "admin-trick-emergency-access-off-description" ? "admin-trick-emergency-access-off-description"
: "admin-trick-emergency-access-on-description"), : "admin-trick-emergency-access-on-description"),
Priority = (int) (airlock.EmergencyAccess ? TricksVerbPriorities.EmergencyAccessOff : TricksVerbPriorities.EmergencyAccessOn), Priority = (int) (airlockComp.EmergencyAccess ? TricksVerbPriorities.EmergencyAccessOff : TricksVerbPriorities.EmergencyAccessOn),
}; };
args.Verbs.Add(emergencyAccess); args.Verbs.Add(emergencyAccess);
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Electrocution; using Content.Server.Electrocution;
using Content.Shared.Electrocution;
using Content.Shared.Construction; using Content.Shared.Construction;
namespace Content.Server.Construction.Completions; namespace Content.Server.Construction.Completions;

View File

@@ -2,7 +2,6 @@ using Content.Server.Doors.Systems;
using Content.Server.Wires; using Content.Server.Wires;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Wires; using Content.Shared.Wires;
namespace Content.Server.Doors; namespace Content.Server.Doors;

View File

@@ -488,4 +488,15 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
} }
_audio.PlayPvs(electrified.ShockNoises, targetUid, AudioParams.Default.WithVolume(electrified.ShockVolume)); _audio.PlayPvs(electrified.ShockNoises, targetUid, AudioParams.Default.WithVolume(electrified.ShockVolume));
} }
public void SetElectrifiedWireCut(Entity<ElectrifiedComponent> ent, bool value)
{
if (ent.Comp.IsWireCut == value)
{
return;
}
ent.Comp.IsWireCut = value;
Dirty(ent);
}
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Electrocution; using Content.Server.Electrocution;
using Content.Shared.Electrocution;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Wires; using Content.Server.Wires;
using Content.Shared.Power; using Content.Shared.Power;
@@ -104,6 +105,7 @@ public sealed partial class PowerWireAction : BaseWireAction
&& !EntityManager.TryGetComponent(used, out electrified)) && !EntityManager.TryGetComponent(used, out electrified))
return; return;
_electrocutionSystem.SetElectrifiedWireCut((used, electrified), setting);
electrified.Enabled = setting; electrified.Enabled = setting;
} }

View File

@@ -74,7 +74,7 @@ namespace Content.Shared.Remotes
case OperatingMode.ToggleEmergencyAccess: case OperatingMode.ToggleEmergencyAccess:
if (airlockComp != null) if (airlockComp != null)
{ {
_airlock.ToggleEmergencyAccess(args.Target.Value, airlockComp); _airlock.SetEmergencyAccess((args.Target.Value, airlockComp), !airlockComp.EmergencyAccess);
_adminLogger.Add(LogType.Action, LogImpact.Medium, _adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to set emergency access {(airlockComp.EmergencyAccess ? "on" : "off")}"); $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to set emergency access {(airlockComp.EmergencyAccess ? "on" : "off")}");
} }

View File

@@ -1,5 +1,6 @@
using Content.Shared.DeviceLinking; using Content.Shared.DeviceLinking;
using Content.Shared.Doors.Systems; using Content.Shared.Doors.Systems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -24,6 +25,18 @@ public sealed partial class AirlockComponent : Component
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public bool EmergencyAccess = false; public bool EmergencyAccess = false;
/// <summary>
/// Sound to play when the airlock emergency access is turned on.
/// </summary>
[DataField]
public SoundSpecifier EmergencyOnSound = new SoundPathSpecifier("/Audio/Machines/airlock_emergencyon.ogg");
/// <summary>
/// Sound to play when the airlock emergency access is turned off.
/// </summary>
[DataField]
public SoundSpecifier EmergencyOffSound = new SoundPathSpecifier("/Audio/Machines/airlock_emergencyoff.ogg");
/// <summary> /// <summary>
/// Pry modifier for a powered airlock. /// Pry modifier for a powered airlock.
/// Most anything that can pry powered has a pry speed bonus, /// Most anything that can pry powered has a pry speed bonus,

View File

@@ -1,4 +1,5 @@
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Robust.Shared.Audio.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Prying.Components; using Content.Shared.Prying.Components;
using Content.Shared.Wires; using Content.Shared.Wires;
@@ -10,7 +11,9 @@ public abstract class SharedAirlockSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] protected readonly SharedDoorSystem DoorSystem = default!; [Dependency] protected readonly SharedDoorSystem DoorSystem = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedWiresSystem _wiresSystem = default!; [Dependency] private readonly SharedWiresSystem _wiresSystem = default!;
public override void Initialize() public override void Initialize()
@@ -131,11 +134,23 @@ public abstract class SharedAirlockSystem : EntitySystem
Appearance.SetData(uid, DoorVisuals.EmergencyLights, component.EmergencyAccess); Appearance.SetData(uid, DoorVisuals.EmergencyLights, component.EmergencyAccess);
} }
public void ToggleEmergencyAccess(EntityUid uid, AirlockComponent component) public void SetEmergencyAccess(Entity<AirlockComponent> ent, bool value, EntityUid? user = null, bool predicted = false)
{ {
component.EmergencyAccess = !component.EmergencyAccess; if(!ent.Comp.Powered)
Dirty(uid, component); // This only runs on the server apparently so we need this. return;
UpdateEmergencyLightStatus(uid, component);
if (ent.Comp.EmergencyAccess == value)
return;
ent.Comp.EmergencyAccess = value;
Dirty(ent, ent.Comp); // This only runs on the server apparently so we need this.
UpdateEmergencyLightStatus(ent, ent.Comp);
var sound = ent.Comp.EmergencyAccess ? ent.Comp.EmergencyOnSound : ent.Comp.EmergencyOffSound;
if (predicted)
Audio.PlayPredicted(sound, ent, user: user);
else
Audio.PlayPvs(sound, ent);
} }
public void SetAutoCloseDelayModifier(AirlockComponent component, float value) public void SetAutoCloseDelayModifier(AirlockComponent component, float value)

View File

@@ -77,8 +77,20 @@ public abstract partial class SharedDoorSystem
public void SetBoltsDown(Entity<DoorBoltComponent> ent, bool value, EntityUid? user = null, bool predicted = false) public void SetBoltsDown(Entity<DoorBoltComponent> ent, bool value, EntityUid? user = null, bool predicted = false)
{ {
TrySetBoltDown(ent, value, user, predicted);
}
public bool TrySetBoltDown(
Entity<DoorBoltComponent> ent,
bool value,
EntityUid? user = null,
bool predicted = false
)
{
if (!_powerReceiver.IsPowered(ent.Owner))
return false;
if (ent.Comp.BoltsDown == value) if (ent.Comp.BoltsDown == value)
return; return false;
ent.Comp.BoltsDown = value; ent.Comp.BoltsDown = value;
Dirty(ent, ent.Comp); Dirty(ent, ent.Comp);
@@ -89,6 +101,7 @@ public abstract partial class SharedDoorSystem
Audio.PlayPredicted(sound, ent, user: user); Audio.PlayPredicted(sound, ent, user: user);
else else
Audio.PlayPvs(sound, ent); Audio.PlayPvs(sound, ent);
return true;
} }
private void OnStateChanged(Entity<DoorBoltComponent> entity, ref DoorStateChangedEvent args) private void OnStateChanged(Entity<DoorBoltComponent> entity, ref DoorStateChangedEvent args)

View File

@@ -9,6 +9,7 @@ using Content.Shared.Emag.Systems;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Physics; using Content.Shared.Physics;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Prying.Components; using Content.Shared.Prying.Components;
using Content.Shared.Prying.Systems; using Content.Shared.Prying.Systems;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
@@ -42,6 +43,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
[Dependency] private readonly PryingSystem _pryingSystem = default!; [Dependency] private readonly PryingSystem _pryingSystem = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedPowerReceiverSystem _powerReceiver = default!;
[ValidatePrototypeId<TagPrototype>] [ValidatePrototypeId<TagPrototype>]

View File

@@ -1,121 +1,131 @@
using Robust.Shared.GameStates;
using Robust.Shared.Audio; using Robust.Shared.Audio;
namespace Content.Server.Electrocution; namespace Content.Shared.Electrocution;
/// <summary> /// <summary>
/// Component for things that shock users on touch. /// Component for things that shock users on touch.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ElectrifiedComponent : Component public sealed partial class ElectrifiedComponent : Component
{ {
[DataField("enabled")] [DataField, AutoNetworkedField]
public bool Enabled = true; public bool Enabled = true;
/// <summary> /// <summary>
/// Should player get damage on collide /// Should player get damage on collide
/// </summary> /// </summary>
[DataField("onBump")] [DataField, AutoNetworkedField]
public bool OnBump = true; public bool OnBump = true;
/// <summary> /// <summary>
/// Should player get damage on attack /// Should player get damage on attack
/// </summary> /// </summary>
[DataField("onAttacked")] [DataField, AutoNetworkedField]
public bool OnAttacked = true; public bool OnAttacked = true;
/// <summary> /// <summary>
/// When true - disables power if a window is present in the same tile /// When true - disables power if a window is present in the same tile
/// </summary> /// </summary>
[DataField("noWindowInTile")] [DataField, AutoNetworkedField]
public bool NoWindowInTile = false; public bool NoWindowInTile = false;
/// <summary> /// <summary>
/// Should player get damage on interact with empty hand /// Should player get damage on interact with empty hand
/// </summary> /// </summary>
[DataField("onHandInteract")] [DataField, AutoNetworkedField]
public bool OnHandInteract = true; public bool OnHandInteract = true;
/// <summary> /// <summary>
/// Should player get damage on interact while holding an object in their hand /// Should player get damage on interact while holding an object in their hand
/// </summary> /// </summary>
[DataField("onInteractUsing")] [DataField, AutoNetworkedField]
public bool OnInteractUsing = true; public bool OnInteractUsing = true;
/// <summary> /// <summary>
/// Indicates if the entity requires power to function /// Indicates if the entity requires power to function
/// </summary> /// </summary>
[DataField("requirePower")] [DataField, AutoNetworkedField]
public bool RequirePower = true; public bool RequirePower = true;
/// <summary> /// <summary>
/// Indicates if the entity uses APC power /// Indicates if the entity uses APC power
/// </summary> /// </summary>
[DataField("usesApcPower")] [DataField, AutoNetworkedField]
public bool UsesApcPower = false; public bool UsesApcPower = false;
/// <summary> /// <summary>
/// Identifier for the high voltage node. /// Identifier for the high voltage node.
/// </summary> /// </summary>
[DataField("highVoltageNode")] [DataField, AutoNetworkedField]
public string? HighVoltageNode; public string? HighVoltageNode;
/// <summary> /// <summary>
/// Identifier for the medium voltage node. /// Identifier for the medium voltage node.
/// </summary> /// </summary>
[DataField("mediumVoltageNode")] [DataField, AutoNetworkedField]
public string? MediumVoltageNode; public string? MediumVoltageNode;
/// <summary> /// <summary>
/// Identifier for the low voltage node. /// Identifier for the low voltage node.
/// </summary> /// </summary>
[DataField("lowVoltageNode")] [DataField, AutoNetworkedField]
public string? LowVoltageNode; public string? LowVoltageNode;
/// <summary> /// <summary>
/// Damage multiplier for HV electrocution /// Damage multiplier for HV electrocution
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public float HighVoltageDamageMultiplier = 3f; public float HighVoltageDamageMultiplier = 3f;
/// <summary> /// <summary>
/// Shock time multiplier for HV electrocution /// Shock time multiplier for HV electrocution
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public float HighVoltageTimeMultiplier = 1.5f; public float HighVoltageTimeMultiplier = 1.5f;
/// <summary> /// <summary>
/// Damage multiplier for MV electrocution /// Damage multiplier for MV electrocution
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public float MediumVoltageDamageMultiplier = 2f; public float MediumVoltageDamageMultiplier = 2f;
/// <summary> /// <summary>
/// Shock time multiplier for MV electrocution /// Shock time multiplier for MV electrocution
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public float MediumVoltageTimeMultiplier = 1.25f; public float MediumVoltageTimeMultiplier = 1.25f;
[DataField("shockDamage")] [DataField, AutoNetworkedField]
public float ShockDamage = 7.5f; public float ShockDamage = 7.5f;
/// <summary> /// <summary>
/// Shock time, in seconds. /// Shock time, in seconds.
/// </summary> /// </summary>
[DataField("shockTime")] [DataField, AutoNetworkedField]
public float ShockTime = 8f; public float ShockTime = 8f;
[DataField("siemensCoefficient")] [DataField, AutoNetworkedField]
public float SiemensCoefficient = 1f; public float SiemensCoefficient = 1f;
[DataField("shockNoises")] [DataField, AutoNetworkedField]
public SoundSpecifier ShockNoises = new SoundCollectionSpecifier("sparks"); public SoundSpecifier ShockNoises = new SoundCollectionSpecifier("sparks");
[DataField("playSoundOnShock")] [DataField, AutoNetworkedField]
public SoundPathSpecifier AirlockElectrifyDisabled = new("/Audio/Machines/airlock_electrify_on.ogg");
[DataField, AutoNetworkedField]
public SoundPathSpecifier AirlockElectrifyEnabled = new("/Audio/Machines/airlock_electrify_off.ogg");
[DataField, AutoNetworkedField]
public bool PlaySoundOnShock = true; public bool PlaySoundOnShock = true;
[DataField("shockVolume")] [DataField, AutoNetworkedField]
public float ShockVolume = 20; public float ShockVolume = 20;
[DataField] [DataField, AutoNetworkedField]
public float Probability = 1f; public float Probability = 1f;
[DataField, AutoNetworkedField]
public bool IsWireCut = false;
} }

View File

@@ -23,6 +23,20 @@ namespace Content.Shared.Electrocution
Dirty(uid, insulated); Dirty(uid, insulated);
} }
/// <summary>
/// Sets electrified value of component and marks dirty if required.
/// </summary>
public void SetElectrified(Entity<ElectrifiedComponent> ent, bool value)
{
if (ent.Comp.Enabled == value)
{
return;
}
ent.Comp.Enabled = value;
Dirty(ent, ent.Comp);
}
/// <param name="uid">Entity being electrocuted.</param> /// <param name="uid">Entity being electrocuted.</param>
/// <param name="sourceUid">Source entity of the electrocution.</param> /// <param name="sourceUid">Source entity of the electrocution.</param>
/// <param name="shockDamage">How much shock damage the entity takes.</param> /// <param name="shockDamage">How much shock damage the entity takes.</param>

View File

@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Shared.Examine;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
namespace Content.Shared.Power.EntitySystems; namespace Content.Shared.Power.EntitySystems;
@@ -8,6 +7,9 @@ public abstract class SharedPowerReceiverSystem : EntitySystem
{ {
public abstract bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component); public abstract bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component);
/// <summary>
/// Checks if entity is APC-powered device, and if it have power.
/// </summary>
public bool IsPowered(Entity<SharedApcPowerReceiverComponent?> entity) public bool IsPowered(Entity<SharedApcPowerReceiverComponent?> entity)
{ {
if (!ResolveApc(entity.Owner, ref entity.Comp)) if (!ResolveApc(entity.Owner, ref entity.Comp))

View File

@@ -1,5 +1,6 @@
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Content.Shared.Electrocution;
namespace Content.Shared.Silicons.StationAi; namespace Content.Shared.Silicons.StationAi;
@@ -10,16 +11,84 @@ public abstract partial class SharedStationAiSystem
private void InitializeAirlock() private void InitializeAirlock()
{ {
SubscribeLocalEvent<DoorBoltComponent, StationAiBoltEvent>(OnAirlockBolt); SubscribeLocalEvent<DoorBoltComponent, StationAiBoltEvent>(OnAirlockBolt);
SubscribeLocalEvent<AirlockComponent, StationAiEmergencyAccessEvent>(OnAirlockEmergencyAccess);
SubscribeLocalEvent<ElectrifiedComponent, StationAiElectrifiedEvent>(OnElectrified);
} }
/// <summary>
/// Attempts to bolt door. If wire was cut (AI or for bolts) or its not powered - notifies AI and does nothing.
/// </summary>
private void OnAirlockBolt(EntityUid ent, DoorBoltComponent component, StationAiBoltEvent args) private void OnAirlockBolt(EntityUid ent, DoorBoltComponent component, StationAiBoltEvent args)
{ {
_doors.SetBoltsDown((ent, component), args.Bolted, args.User, predicted: true); if (component.BoltWireCut)
{
ShowDeviceNotRespondingPopup(args.User);
return;
}
var setResult = _doors.TrySetBoltDown((ent, component), args.Bolted, args.User, predicted: true);
if (!setResult)
{
ShowDeviceNotRespondingPopup(args.User);
}
}
/// <summary>
/// Attempts to bolt door. If wire was cut (AI) or its not powered - notifies AI and does nothing.
/// </summary>
private void OnAirlockEmergencyAccess(EntityUid ent, AirlockComponent component, StationAiEmergencyAccessEvent args)
{
if (!PowerReceiver.IsPowered(ent))
{
ShowDeviceNotRespondingPopup(args.User);
return;
}
_airlocks.SetEmergencyAccess((ent, component), args.EmergencyAccess, args.User, predicted: true);
}
/// <summary>
/// Attempts to bolt door. If wire was cut (AI or for one of power-wires) or its not powered - notifies AI and does nothing.
/// </summary>
private void OnElectrified(EntityUid ent, ElectrifiedComponent component, StationAiElectrifiedEvent args)
{
if (
component.IsWireCut
|| !PowerReceiver.IsPowered(ent)
)
{
ShowDeviceNotRespondingPopup(args.User);
return;
}
_electrify.SetElectrified((ent, component), args.Electrified);
var soundToPlay = component.Enabled
? component.AirlockElectrifyDisabled
: component.AirlockElectrifyEnabled;
_audio.PlayLocal(soundToPlay, ent, args.User);
} }
} }
/// <summary> Event for StationAI attempt at bolting/unbolting door. </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class StationAiBoltEvent : BaseStationAiAction public sealed class StationAiBoltEvent : BaseStationAiAction
{ {
/// <summary> Marker, should be door bolted or unbolted. </summary>
public bool Bolted; public bool Bolted;
} }
/// <summary> Event for StationAI attempt at setting emergency access for door on/off. </summary>
[Serializable, NetSerializable]
public sealed class StationAiEmergencyAccessEvent : BaseStationAiAction
{
/// <summary> Marker, should door have emergency access on or off. </summary>
public bool EmergencyAccess;
}
/// <summary> Event for StationAI attempt at electrifying/de-electrifying door. </summary>
[Serializable, NetSerializable]
public sealed class StationAiElectrifiedEvent : BaseStationAiAction
{
/// <summary> Marker, should door be electrified or no. </summary>
public bool Electrified;
}

View File

@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using Content.Shared.Actions.Events; using Content.Shared.Actions.Events;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -108,41 +109,56 @@ public abstract partial class SharedStationAiSystem
return; return;
if (TryComp(ev.Actor, out StationAiHeldComponent? aiComp) && if (TryComp(ev.Actor, out StationAiHeldComponent? aiComp) &&
(!ValidateAi((ev.Actor, aiComp)) || (!TryComp(ev.Target, out StationAiWhitelistComponent? whitelistComponent) ||
!HasComp<StationAiWhitelistComponent>(ev.Target))) !ValidateAi((ev.Actor, aiComp))))
{ {
if (whitelistComponent is { Enabled: false })
{
ShowDeviceNotRespondingPopup(ev.Actor);
}
ev.Cancel(); ev.Cancel();
} }
} }
private void OnHeldInteraction(Entity<StationAiHeldComponent> ent, ref InteractionAttemptEvent args) private void OnHeldInteraction(Entity<StationAiHeldComponent> ent, ref InteractionAttemptEvent args)
{ {
// Cancel if it's not us or something with a whitelist. // Cancel if it's not us or something with a whitelist, or whitelist is disabled.
args.Cancelled = ent.Owner != args.Target && args.Cancelled = (!TryComp(args.Target, out StationAiWhitelistComponent? whitelistComponent)
args.Target != null && || !whitelistComponent.Enabled)
(!TryComp(args.Target, out StationAiWhitelistComponent? whitelist) || !whitelist.Enabled); && ent.Owner != args.Target
&& args.Target != null;
if (whitelistComponent is { Enabled: false })
{
ShowDeviceNotRespondingPopup(ent.Owner);
}
} }
private void OnTargetVerbs(Entity<StationAiWhitelistComponent> ent, ref GetVerbsEvent<AlternativeVerb> args) private void OnTargetVerbs(Entity<StationAiWhitelistComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{ {
if (!args.CanComplexInteract || if (!args.CanComplexInteract
!ent.Comp.Enabled || || !HasComp<StationAiHeldComponent>(args.User))
!HasComp<StationAiHeldComponent>(args.User) ||
!HasComp<StationAiWhitelistComponent>(args.Target))
{ {
return; return;
} }
var user = args.User; var user = args.User;
var target = args.Target; var target = args.Target;
var isOpen = _uiSystem.IsUiOpen(target, AiUi.Key, user); var isOpen = _uiSystem.IsUiOpen(target, AiUi.Key, user);
args.Verbs.Add(new AlternativeVerb() var verb = new AlternativeVerb
{ {
Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"), Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"),
Act = () => Act = () =>
{ {
// no need to show menu if device is not powered.
if (!PowerReceiver.IsPowered(ent.Owner))
{
ShowDeviceNotRespondingPopup(user);
return;
}
if (isOpen) if (isOpen)
{ {
_uiSystem.CloseUi(ent.Owner, AiUi.Key, user); _uiSystem.CloseUi(ent.Owner, AiUi.Key, user);
@@ -152,7 +168,13 @@ public abstract partial class SharedStationAiSystem
_uiSystem.OpenUi(ent.Owner, AiUi.Key, user); _uiSystem.OpenUi(ent.Owner, AiUi.Key, user);
} }
} }
}); };
args.Verbs.Add(verb);
}
private void ShowDeviceNotRespondingPopup(EntityUid toEntity)
{
_popup.PopupClient(Loc.GetString("ai-device-not-responding"), toEntity, PopupType.MediumCaution);
} }
} }

View File

@@ -4,14 +4,18 @@ using Content.Shared.Administration.Managers;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Doors.Systems; using Content.Shared.Doors.Systems;
using Content.Shared.Electrocution;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Item.ItemToggle; using Content.Shared.Item.ItemToggle;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Power; using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.StationAi; using Content.Shared.StationAi;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -31,13 +35,18 @@ public abstract partial class SharedStationAiSystem : EntitySystem
[Dependency] private readonly ItemToggleSystem _toggles = default!; [Dependency] private readonly ItemToggleSystem _toggles = default!;
[Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!; [Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedAirlockSystem _airlocks = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!; [Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly SharedDoorSystem _doors = default!; [Dependency] private readonly SharedDoorSystem _doors = default!;
[Dependency] private readonly SharedElectrocutionSystem _electrify = default!;
[Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] protected readonly SharedMapSystem Maps = default!; [Dependency] protected readonly SharedMapSystem Maps = default!;
[Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedMoverController _mover = default!; [Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!; [Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly StationAiVisionSystem _vision = default!; [Dependency] private readonly StationAiVisionSystem _vision = default!;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -171,3 +171,12 @@
license: "CC0-1.0" license: "CC0-1.0"
copyright: "by Ko4erga" copyright: "by Ko4erga"
source: "https://github.com/space-wizards/space-station-14/pull/30431" source: "https://github.com/space-wizards/space-station-14/pull/30431"
- files:
- airlock_emergencyoff.ogg
- airlock_emergencyon.ogg
- airlock_electrify_off.ogg
- airlock_electrify_on.ogg
license: "CC0-1.0"
copyright: "by ScarKy0"
source: "https://github.com/space-wizards/space-station-14/pull/32012"

View File

@@ -11,4 +11,12 @@ ai-close = Close actions
bolt-close = Close bolt bolt-close = Close bolt
bolt-open = Open bolt bolt-open = Open bolt
emergency-access-on = Enable emergency access
emergency-access-off = Disable emergency access
electrify-door-on = Enable overcharge
electrify-door-off = Disable overcharge
toggle-light = Toggle light toggle-light = Toggle light
ai-device-not-responding = Device is not responding

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

View File

@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"license": "CC-BY-SA-3.0", "license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/blob/c473a8bcc28fbd80827dfca5660d81ca6e833e2c/icons/hud/screen_ai.dmi", "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/blob/c473a8bcc28fbd80827dfca5660d81ca6e833e2c/icons/hud/screen_ai.dmi , some sprites updated by ScarKy0(Discord), door actions by ScarKy0(Discord) and @Max_tanuki(Twitter)",
"size": { "size": {
"x": 32, "x": 32,
"y": 32 "y": 32
@@ -33,6 +33,24 @@
}, },
{ {
"name": "comms_console" "name": "comms_console"
},
{
"name": "emergency_off"
},
{
"name": "emergency_on"
},
{
"name": "bolt_door"
},
{
"name": "unbolt_door"
},
{
"name": "door_overcharge_on"
},
{
"name": "door_overcharge_off"
} }
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B