From 2df523061b8cd4cf2a8ef0507f7a4578718b2784 Mon Sep 17 00:00:00 2001
From: Red <96445749+TheShuEd@users.noreply.github.com>
Date: Sun, 24 Aug 2025 18:36:24 +0300
Subject: [PATCH] Respawn button (#1702)
* Add respawn action for ghosts with confirmation popup
Introduces a new respawn action for ghosts, allowing them to return to the lobby and respawn as a new character. Adds supporting systems, localization, prototype definitions, and an icon for the action. The action includes a confirmation popup to prevent accidental use.
* Prevent respawning with the same character in a round
Introduces a system to block players from rejoining a round with a character that has already died, notifying them with a localized message. Refactors and moves respawn-related localization strings to new files for both English and Russian.
* Prevent admins from character reuse restriction
Added a check in CP14RespawnSystem to allow admins to bypass the restriction that prevents the same character from re-entering the round. This ensures that admin users are not limited by the character reuse logic.
---
Content.Client/Ghost/GhostSystem.cs | 1 +
.../GameTicking/GameTicker.Spawning.cs | 8 ++
Content.Server/Ghost/GhostSystem.cs | 5 +-
.../_CP14/Respawn/CP14RespawnSystem.cs | 97 ++++++++++++++++++
Content.Shared/Ghost/GhostComponent.cs | 6 ++
.../_CP14/Respawn/CP14SharedRespawnSystem.cs | 7 ++
Resources/Locale/en-US/_CP14/respawn.ftl | 3 +
Resources/Locale/ru-RU/_CP14/respawn.ftl | 3 +
.../_CP14/Entities/Actions/ghost.yml | 17 +++
.../_CP14/Actions/zLevel.rsi/meta.json | 3 +
.../_CP14/Actions/zLevel.rsi/respawn.png | Bin 0 -> 550 bytes
11 files changed, 148 insertions(+), 2 deletions(-)
create mode 100644 Content.Server/_CP14/Respawn/CP14RespawnSystem.cs
create mode 100644 Content.Shared/_CP14/Respawn/CP14SharedRespawnSystem.cs
create mode 100644 Resources/Locale/en-US/_CP14/respawn.ftl
create mode 100644 Resources/Locale/ru-RU/_CP14/respawn.ftl
create mode 100644 Resources/Textures/_CP14/Actions/zLevel.rsi/respawn.png
diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs
index ee80470807..ebe9629a4a 100644
--- a/Content.Client/Ghost/GhostSystem.cs
+++ b/Content.Client/Ghost/GhostSystem.cs
@@ -136,6 +136,7 @@ namespace Content.Client.Ghost
//_actions.RemoveAction(uid, component.ToggleGhostHearingActionEntity); //Dont need in CP14
//CP14
_actions.RemoveAction(uid, component.CP14ToggleRoofActionEntity);
+ _actions.RemoveAction(uid, component.CP14RespawnActionEntity);
//CP14 end
if (uid != _playerManager.LocalEntity)
diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs
index d758b36197..b05572849e 100644
--- a/Content.Server/GameTicking/GameTicker.Spawning.cs
+++ b/Content.Server/GameTicking/GameTicker.Spawning.cs
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Linq;
using System.Numerics;
+using Content.Server._CP14.Respawn;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking.Events;
@@ -212,6 +213,13 @@ namespace Content.Server.GameTicking
character = HumanoidCharacterProfile.RandomWithSpecies(speciesId);
}
+ //CP14 blocking rejoining to game with same character
+ var CP14Ev = new CP14PlayerSpawnAttemptEvent(player, character, jobId, lateJoin, station);
+ RaiseLocalEvent(CP14Ev);
+ if (CP14Ev.Cancelled)
+ return;
+ //CP14 end
+
// We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station);
RaiseLocalEvent(bev);
diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs
index 1c10d08dcd..cda6996053 100644
--- a/Content.Server/Ghost/GhostSystem.cs
+++ b/Content.Server/Ghost/GhostSystem.cs
@@ -224,18 +224,19 @@ namespace Content.Server.Ghost
// Entity can't see ghosts anymore.
_eye.RefreshVisibilityMask(uid);
- _actions.RemoveAction(uid, component.BooActionEntity);
+ //_actions.RemoveAction(uid, component.BooActionEntity); //Dont need in CP14
}
private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent args)
{
- _actions.AddAction(uid, ref component.BooActionEntity, component.BooAction);
+ //_actions.AddAction(uid, ref component.BooActionEntity, component.BooAction); //Dont need in CP14
//_actions.AddAction(uid, ref component.ToggleGhostHearingActionEntity, component.ToggleGhostHearingAction); //Dont need in CP14
_actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleLightingAction);
_actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction);
_actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction);
//CP14
_actions.AddAction(uid, ref component.CP14ToggleRoofActionEntity, component.CP14ToggleRoofAction);
+ _actions.AddAction(uid, ref component.CP14RespawnActionEntity, component.CP14RespawnAction);
//CP14 end
}
diff --git a/Content.Server/_CP14/Respawn/CP14RespawnSystem.cs b/Content.Server/_CP14/Respawn/CP14RespawnSystem.cs
new file mode 100644
index 0000000000..8b50e5a3e9
--- /dev/null
+++ b/Content.Server/_CP14/Respawn/CP14RespawnSystem.cs
@@ -0,0 +1,97 @@
+using Content.Server.Chat.Systems;
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Events;
+using Content.Shared._CP14.Respawn;
+using Content.Shared.Administration.Managers;
+using Content.Shared.GameTicking;
+using Content.Shared.Ghost;
+using Content.Shared.Preferences;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+
+namespace Content.Server._CP14.Respawn;
+
+public sealed partial class CP14RespawnSystem : EntitySystem
+{
+ [Dependency] private readonly GameTicker _gameTicker = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly ISharedAdminManager _admin = default!;
+
+ ///
+ /// We keep a list of all characters connected in a round to prevent the same character from re-entering the round.
+ /// Of course, we only check by character name, and this is easy to circumvent by renaming the character,
+ /// but the main goal here is to make the player understand that this is not the right thing to do.
+ /// If a player abuses the ability to re-enter a round, the administration will take care of it.
+ ///
+ private Dictionary> _usedCharacters = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnRespawnAction);
+ SubscribeLocalEvent(OnBeforePlayerSpawn);
+ SubscribeLocalEvent(OnRoundEnd);
+ }
+
+ private void OnRoundEnd(RoundStartingEvent ev)
+ {
+ _usedCharacters.Clear();
+ }
+
+ private void OnBeforePlayerSpawn(CP14PlayerSpawnAttemptEvent ev)
+ {
+ if (_admin.IsAdmin(ev.Player, true))
+ return;
+
+ if (!_usedCharacters.TryGetValue(ev.Player.UserId, out var usedCharacters))
+ {
+ usedCharacters = [];
+ _usedCharacters[ev.Player.UserId] = usedCharacters;
+ }
+
+ if (usedCharacters.Contains(ev.Profile.Name))
+ {
+ var filter = Filter.Empty().AddPlayer(ev.Player);
+ _chat.DispatchFilteredAnnouncement(
+ filter,
+ Loc.GetString("cp14-respawn-same-char-bloked"),
+ colorOverride: Color.Red,
+ announcementSound: new SoundPathSpecifier("/Audio/Effects/beep1.ogg"),
+ sender: "Server");
+ ev.Cancel();
+ return;
+ }
+
+ usedCharacters.Add(ev.Profile.Name);
+ }
+
+ private void OnRespawnAction(Entity ent, ref CP14RespawnAction args)
+ {
+ if (!TryComp(ent, out var actor))
+ return;
+
+ _gameTicker.Respawn(actor.PlayerSession);
+ }
+}
+
+///
+/// Called before a player spawns for a specific character. Can be canceled by prohibiting connection for that character.
+///
+[PublicAPI]
+public sealed class CP14PlayerSpawnAttemptEvent(
+ ICommonSession player,
+ HumanoidCharacterProfile profile,
+ string? jobId,
+ bool lateJoin,
+ EntityUid station)
+ : CancellableEntityEventArgs
+{
+ public ICommonSession Player { get; } = player;
+ public HumanoidCharacterProfile Profile { get; } = profile;
+ public string? JobId { get; } = jobId;
+ public bool LateJoin { get; } = lateJoin;
+ public EntityUid Station { get; } = station;
+}
diff --git a/Content.Shared/Ghost/GhostComponent.cs b/Content.Shared/Ghost/GhostComponent.cs
index c0a287af0d..ca87871c5c 100644
--- a/Content.Shared/Ghost/GhostComponent.cs
+++ b/Content.Shared/Ghost/GhostComponent.cs
@@ -47,8 +47,14 @@ public sealed partial class GhostComponent : Component
[DataField]
public EntProtoId CP14ToggleRoofAction = "CP14ActionToggleRoofs";
+ [DataField]
+ public EntProtoId CP14RespawnAction = "CP14ActionRespawn";
+
[DataField, AutoNetworkedField]
public EntityUid? CP14ToggleRoofActionEntity;
+
+ [DataField, AutoNetworkedField]
+ public EntityUid? CP14RespawnActionEntity;
//CP14 Ghost entities end
// End actions
diff --git a/Content.Shared/_CP14/Respawn/CP14SharedRespawnSystem.cs b/Content.Shared/_CP14/Respawn/CP14SharedRespawnSystem.cs
new file mode 100644
index 0000000000..09c49d3ed7
--- /dev/null
+++ b/Content.Shared/_CP14/Respawn/CP14SharedRespawnSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared.Actions;
+
+namespace Content.Shared._CP14.Respawn;
+
+public sealed partial class CP14RespawnAction : InstantActionEvent
+{
+}
diff --git a/Resources/Locale/en-US/_CP14/respawn.ftl b/Resources/Locale/en-US/_CP14/respawn.ftl
new file mode 100644
index 0000000000..a7b27721f8
--- /dev/null
+++ b/Resources/Locale/en-US/_CP14/respawn.ftl
@@ -0,0 +1,3 @@
+cp14-respawn-action-popup = Are you sure you want to return to the lobby? You will no longer be able to play as this character!
+
+cp14-respawn-same-char-bloked = This character has died. You can no longer use them in this round.
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/_CP14/respawn.ftl b/Resources/Locale/ru-RU/_CP14/respawn.ftl
new file mode 100644
index 0000000000..cd5e437cc4
--- /dev/null
+++ b/Resources/Locale/ru-RU/_CP14/respawn.ftl
@@ -0,0 +1,3 @@
+cp14-respawn-action-popup = Вы уверены что хотите вернуться в лобби? Игра за этого персонажа станет недоступной!
+
+cp14-respawn-same-char-bloked = Этот персонаж погиб. Вы больше не можете использовать его в этом раунде.
\ No newline at end of file
diff --git a/Resources/Prototypes/_CP14/Entities/Actions/ghost.yml b/Resources/Prototypes/_CP14/Entities/Actions/ghost.yml
index a4aed57606..8acb83eca7 100644
--- a/Resources/Prototypes/_CP14/Entities/Actions/ghost.yml
+++ b/Resources/Prototypes/_CP14/Entities/Actions/ghost.yml
@@ -38,3 +38,20 @@
- type: InstantAction
event: !type:CP14ToggleRoofVisibilityAction
+- type: entity
+ id: CP14ActionRespawn
+ name: Respawn
+ description: Return to the lobby to connect with a new character.
+ components:
+ - type: Action
+ priority: 20
+ startDelay: true
+ useDelay: 300
+ checkCanInteract: false
+ icon:
+ sprite: _CP14/Actions/zLevel.rsi
+ state: respawn
+ - type: ConfirmableAction
+ popup: cp14-respawn-action-popup
+ - type: InstantAction
+ event: !type:CP14RespawnAction
\ No newline at end of file
diff --git a/Resources/Textures/_CP14/Actions/zLevel.rsi/meta.json b/Resources/Textures/_CP14/Actions/zLevel.rsi/meta.json
index 59cc25eb7b..6abdad2bc3 100644
--- a/Resources/Textures/_CP14/Actions/zLevel.rsi/meta.json
+++ b/Resources/Textures/_CP14/Actions/zLevel.rsi/meta.json
@@ -15,6 +15,9 @@
},
{
"name": "roof"
+ },
+ {
+ "name": "respawn"
}
]
}
diff --git a/Resources/Textures/_CP14/Actions/zLevel.rsi/respawn.png b/Resources/Textures/_CP14/Actions/zLevel.rsi/respawn.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0fe4e1abc1c8619a88481cb8d8761296203f22d
GIT binary patch
literal 550
zcmV+>0@?kEP)Px$;7LS5R9J<@R=bhHKnxY0n;|11BZY*(bx;8n(7=^K1FnDyTn7OOq|Tf;yNC-w
zzGwMqS3bD8H{%WTSiVPRCfKhCMl>GNwck&uuo_Y`|X
zA|$c`p7Q#<9Q1c(`zsR3Nv2#CTZL>qLZXN&ug^=V&r9j5W~5vdcU3d+jIK=uu1f$n
z5E1FBM!Kp=iT83I2}Xnxu!6c_G_n~{FMwSSSnH!ulFSQ}2q>8KHUrq07o-*+ImOKS
zu4<${tVUyYJ(BRe=z4%fJI2PYYzLo}!C*|q5Dlu$dV9xn!xEdThA1l{vN)`I>luNO
z2Ux18(nu*rnf-Czd&kQldIY($`%btqfJ#~A%OQGkQ36_WZ7yp$_Mr3Z?(i@=7aIeM
z!z!H9OfD6GlmH{cNZ4p+ag$S?C6FWGA*wt?z>*O0gVI*dx4eif2_#~H<@T*_LXQZ;
z)K~GpW0te%uTh9RQb<46$FbDBZF_%g_m$8V7p!`$3bHCW^|BIy2g$B%pPM#)Uqs{t
z^S00Ab9fN_ksWvdH$jWp9e~9~`w`eXroagVzI3#W3$45*gU4dYp=^Qdt;M#l3+)rj
ou2(DZkMr1mDGjf;%gbr}4V%t!#QVy%#sB~S07*qoM6N<$g0hbMfdBvi
literal 0
HcmV?d00001