diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 720bdcd2e4..f23e624d57 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -65,7 +65,6 @@ namespace Content.Client.Entry
"Paper",
"Write",
"Bloodstream",
- "PAI",
"TransformableContainer",
"Mind",
"StorageFill",
diff --git a/Content.Client/PAI/PAISystem.cs b/Content.Client/PAI/PAISystem.cs
new file mode 100644
index 0000000000..a363d7d7d9
--- /dev/null
+++ b/Content.Client/PAI/PAISystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.PAI;
+
+namespace Content.Client.PAI
+{
+ public class PAISystem : SharedPAISystem
+ {
+ }
+}
diff --git a/Content.Server/PAI/PAISystem.cs b/Content.Server/PAI/PAISystem.cs
index 79d6838555..5c5aabd409 100644
--- a/Content.Server/PAI/PAISystem.cs
+++ b/Content.Server/PAI/PAISystem.cs
@@ -14,16 +14,7 @@ using Robust.Shared.Player;
namespace Content.Server.PAI
{
- ///
- /// pAIs, or Personal AIs, are essentially portable ghost role generators.
- /// In their current implementation, they create a ghost role anyone can access,
- /// and that a player can also "wipe" (reset/kick out player).
- /// Theoretically speaking pAIs are supposed to use a dedicated "offer and select" system,
- /// with the player holding the pAI being able to choose one of the ghosts in the round.
- /// This seems too complicated for an initial implementation, though,
- /// and there's not always enough players and ghost roles to justify it.
- ///
- public class PAISystem : EntitySystem
+ public class PAISystem : SharedPAISystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -78,6 +69,9 @@ namespace Content.Server.PAI
return;
}
+ // Ownership tag
+ component.Owner.Name = Loc.GetString("pai-system-pai-name", ("owner", args.User));
+
var ghostFinder = EntityManager.EnsureComponent(uid);
ghostFinder.RoleName = Loc.GetString("pai-system-role-name");
@@ -90,7 +84,7 @@ namespace Content.Server.PAI
private void OnMindRemoved(EntityUid uid, PAIComponent component, MindRemovedMessage args)
{
// Mind was removed, shutdown the PAI.
- UpdatePAIAppearance(uid, PAIStatus.Off);
+ PAITurningOff(uid);
}
private void OnMindAdded(EntityUid uid, PAIComponent pai, MindAddedMessage args)
@@ -101,6 +95,17 @@ namespace Content.Server.PAI
UpdatePAIAppearance(uid, PAIStatus.On);
}
+ private void PAITurningOff(EntityUid uid)
+ {
+ UpdatePAIAppearance(uid, PAIStatus.Off);
+ if (EntityManager.TryGetComponent(uid, out var metadata))
+ {
+ var proto = metadata.EntityPrototype;
+ if (proto != null)
+ metadata.EntityName = proto.Name;
+ }
+ }
+
private void UpdatePAIAppearance(EntityUid uid, PAIStatus status)
{
if (EntityManager.TryGetComponent(uid, out var appearance))
@@ -128,7 +133,7 @@ namespace Content.Server.PAI
{
EntityManager.RemoveComponent(uid);
_popupSystem.PopupEntity(Loc.GetString("pai-system-wiped-device"), uid, Filter.Entities(args.User.Uid));
- UpdatePAIAppearance(uid, PAIStatus.Off);
+ PAITurningOff(uid);
}
};
args.Verbs.Add(verb);
@@ -144,7 +149,7 @@ namespace Content.Server.PAI
{
EntityManager.RemoveComponent(uid);
_popupSystem.PopupEntity(Loc.GetString("pai-system-stopped-searching"), uid, Filter.Entities(args.User.Uid));
- UpdatePAIAppearance(uid, PAIStatus.Off);
+ PAITurningOff(uid);
}
};
args.Verbs.Add(verb);
diff --git a/Content.Server/PAI/PAIComponent.cs b/Content.Shared/PAI/PAIComponent.cs
similarity index 84%
rename from Content.Server/PAI/PAIComponent.cs
rename to Content.Shared/PAI/PAIComponent.cs
index 4b8c572586..ad255783da 100644
--- a/Content.Server/PAI/PAIComponent.cs
+++ b/Content.Shared/PAI/PAIComponent.cs
@@ -1,6 +1,8 @@
using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
-namespace Content.Server.PAI
+namespace Content.Shared.PAI
{
///
/// pAIs, or Personal AIs, are essentially portable ghost role generators.
@@ -12,7 +14,7 @@ namespace Content.Server.PAI
/// and there's not always enough players and ghost roles to justify it.
/// All logic in PAISystem.
///
- [RegisterComponent]
+ [RegisterComponent, NetworkedComponent]
public class PAIComponent : Component
{
public override string Name => "PAI";
diff --git a/Content.Shared/PAI/PAIShared.cs b/Content.Shared/PAI/PAIShared.cs
deleted file mode 100644
index 6820733f42..0000000000
--- a/Content.Shared/PAI/PAIShared.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Shared.PAI
-{
- [Serializable, NetSerializable]
- public enum PAIVisuals : byte
- {
- Status
- }
-
- [Serializable, NetSerializable]
- public enum PAIStatus : byte
- {
- Off,
- Searching,
- On
- }
-}
-
diff --git a/Content.Shared/PAI/SharedPAISystem.cs b/Content.Shared/PAI/SharedPAISystem.cs
new file mode 100644
index 0000000000..2d7d2f7ee0
--- /dev/null
+++ b/Content.Shared/PAI/SharedPAISystem.cs
@@ -0,0 +1,74 @@
+using System;
+using Content.Shared.DragDrop;
+using Content.Shared.Emoting;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Item;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Shared.PAI
+{
+ ///
+ /// pAIs, or Personal AIs, are essentially portable ghost role generators.
+ /// In their current implementation, they create a ghost role anyone can access,
+ /// and that a player can also "wipe" (reset/kick out player).
+ /// Theoretically speaking pAIs are supposed to use a dedicated "offer and select" system,
+ /// with the player holding the pAI being able to choose one of the ghosts in the round.
+ /// This seems too complicated for an initial implementation, though,
+ /// and there's not always enough players and ghost roles to justify it.
+ ///
+ public abstract class SharedPAISystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnUseAttempt);
+ SubscribeLocalEvent(OnInteractAttempt);
+ SubscribeLocalEvent(OnAttackAttempt);
+ SubscribeLocalEvent(OnDropAttempt);
+ SubscribeLocalEvent(OnPickupAttempt);
+ }
+
+ private void OnUseAttempt(EntityUid uid, PAIComponent component, UseAttemptEvent args)
+ {
+ args.Cancel();
+ }
+
+ private void OnInteractAttempt(EntityUid uid, PAIComponent component, InteractionAttemptEvent args)
+ {
+ args.Cancel();
+ }
+
+ private void OnAttackAttempt(EntityUid uid, PAIComponent component, AttackAttemptEvent args)
+ {
+ args.Cancel();
+ }
+
+ private void OnDropAttempt(EntityUid uid, PAIComponent component, DropAttemptEvent args)
+ {
+ args.Cancel();
+ }
+
+ private void OnPickupAttempt(EntityUid uid, PAIComponent component, PickupAttemptEvent args)
+ {
+ args.Cancel();
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public enum PAIVisuals : byte
+ {
+ Status
+ }
+
+ [Serializable, NetSerializable]
+ public enum PAIStatus : byte
+ {
+ Off,
+ Searching,
+ On
+ }
+}
+
diff --git a/Resources/Locale/en-US/pai/pai-system.ftl b/Resources/Locale/en-US/pai/pai-system.ftl
index 1406e9b5fd..bb03dd051e 100644
--- a/Resources/Locale/en-US/pai/pai-system.ftl
+++ b/Resources/Locale/en-US/pai/pai-system.ftl
@@ -13,3 +13,5 @@ pai-system-wiped-device = The pAI was wiped from the device.
pai-system-stop-searching-verb-text = Stop searching
pai-system-stopped-searching = The device stopped searching for a pAI.
+pai-system-pai-name = { CAPITALIZE(THE($owner)) }'s pAI
+