Port respawning from Frontierstation/Corvax (#3021)

This commit is contained in:
pathetic meowmeow 2025-02-24 01:33:22 -05:00 committed by GitHub
parent d5379b17f2
commit 07e11dda25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 439 additions and 0 deletions

View File

@ -3,8 +3,13 @@ using Content.Client.Ghost;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Client.UserInterface.Systems.Ghost.Widgets;
using Content.Shared.Ghost;
using Robust.Client.Console; // Frontier
using Robust.Shared.Console; // Frontier
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Content.Client._Corvax.Respawn; // Frontier
using Content.Shared._NF.CCVar; // Frontier
using Robust.Shared.Configuration; // Frontier
namespace Content.Client.UserInterface.Systems.Ghost;
@ -12,8 +17,11 @@ namespace Content.Client.UserInterface.Systems.Ghost;
public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSystem>
{
[Dependency] private readonly IEntityNetworkManager _net = default!;
[Dependency] private readonly IConsoleHost _consoleHost = default!; // Frontier
[Dependency] private readonly IConfigurationManager _cfg = default!; // Frontier
[UISystemDependency] private readonly GhostSystem? _system = default;
[UISystemDependency] private readonly RespawnSystem? _respawn = default; // Frontier
private GhostGui? Gui => UIManager.GetActiveUIWidgetOrNull<GhostGui>();
@ -56,6 +64,24 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
system.GhostRoleCountUpdated -= OnRoleCountUpdated;
}
// Begin Frontier
public void OnSystemLoaded(RespawnSystem system)
{
system.RespawnReseted += OnRespawnReseted;
}
public void OnSystemUnloaded(RespawnSystem system)
{
system.RespawnReseted -= OnRespawnReseted;
}
private void OnRespawnReseted()
{
UpdateGui();
UpdateRespawn(_respawn?.RespawnResetTime);
}
// End Frontier
public void UpdateGui()
{
if (Gui == null)
@ -67,6 +93,13 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
Gui.Update(_system?.AvailableGhostRoleCount, _system?.Player?.CanReturnToBody);
}
// Begin Frontier
private void UpdateRespawn(TimeSpan? timeOfDeath)
{
Gui?.UpdateRespawn(timeOfDeath);
}
// End Frontier
private void OnPlayerRemoved(GhostComponent component)
{
Gui?.Hide();
@ -83,6 +116,7 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
return;
Gui.Visible = true;
UpdateRespawn(_respawn?.RespawnResetTime); // Frontier
UpdateGui();
}
@ -127,10 +161,18 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
Gui.GhostRolesPressed += GhostRolesPressed;
Gui.TargetWindow.WarpClicked += OnWarpClicked;
Gui.TargetWindow.OnGhostnadoClicked += OnGhostnadoClicked;
Gui.GhostRespawnPressed += GuiOnGhostRespawnPressed; // Frontier
UpdateGui();
}
// Begin Frontier
private void GuiOnGhostRespawnPressed()
{
_consoleHost.ExecuteCommand("ghostrespawn");
}
// End Frontier
public void UnloadGui()
{
if (Gui == null)
@ -140,6 +182,7 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
Gui.ReturnToBodyPressed -= ReturnToBody;
Gui.GhostRolesPressed -= GhostRolesPressed;
Gui.TargetWindow.WarpClicked -= OnWarpClicked;
Gui.GhostRespawnPressed -= GuiOnGhostRespawnPressed; // Frontier
Gui.Hide();
}
@ -160,4 +203,11 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
{
_system?.OpenGhostRoles();
}
// Begin Frontier
private void RespawnPressed()
{
IoCManager.Resolve<IClientConsoleHost>().RemoteExecuteCommand(null, "ghostrespawn");
}
// End Frontier
}

View File

@ -5,5 +5,6 @@
<Button Name="ReturnToBodyButton" Text="{Loc ghost-gui-return-to-body-button}" />
<Button Name="GhostWarpButton" Text="{Loc ghost-gui-ghost-warp-button}" />
<Button Name="GhostRolesButton" />
<Button Name="GhostRespawnButton" Text="{Loc ghost-gui-respawn}" /> <!-- Frontier -->
</BoxContainer>
</widgets:GhostGui>

View File

@ -3,29 +3,42 @@ using Content.Client.UserInterface.Systems.Ghost.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing; // Frontier
using Robust.Shared.Configuration; // Frontier
using Content.Client._NF.UserInterface.Systems.Ghost.Controls; // Frontier
namespace Content.Client.UserInterface.Systems.Ghost.Widgets;
[GenerateTypedNameReferences]
public sealed partial class GhostGui : UIWidget
{
[Dependency] private readonly IGameTiming _gameTiming = default!; // Frontier
[Dependency] private readonly IConfigurationManager _configurationManager = default!; // Frontier
private TimeSpan? _respawnTime; // Frontier
public GhostTargetWindow TargetWindow { get; }
public GhostRespawnRulesWindow RulesWindow { get; } // Frontier
public event Action? RequestWarpsPressed;
public event Action? ReturnToBodyPressed;
public event Action? GhostRolesPressed;
public event Action? GhostRespawnPressed; // Frontier
public GhostGui()
{
RobustXamlLoader.Load(this);
TargetWindow = new GhostTargetWindow();
RulesWindow = new GhostRespawnRulesWindow(); // Frontier
RulesWindow.RespawnButton.OnPressed += _ => GhostRespawnPressed?.Invoke(); // Frontier
MouseFilter = MouseFilterMode.Ignore;
GhostWarpButton.OnPressed += _ => RequestWarpsPressed?.Invoke();
ReturnToBodyButton.OnPressed += _ => ReturnToBodyPressed?.Invoke();
GhostRolesButton.OnPressed += _ => GhostRolesPressed?.Invoke();
GhostRespawnButton.OnPressed += _ => RulesWindow.OpenCentered(); // Frontier
}
public void Hide()
@ -34,6 +47,13 @@ public sealed partial class GhostGui : UIWidget
Visible = false;
}
// Begin Frontier
public void UpdateRespawn(TimeSpan? respawnTime)
{
_respawnTime = respawnTime;
}
// End Frontier
public void Update(int? roles, bool? canReturnToBody)
{
ReturnToBodyButton.Disabled = !canReturnToBody ?? true;
@ -54,6 +74,23 @@ public sealed partial class GhostGui : UIWidget
TargetWindow.Populate();
}
// Begin Frontier
protected override void FrameUpdate(FrameEventArgs args)
{
if (_respawnTime is null || _gameTiming.CurTime > _respawnTime)
{
GhostRespawnButton.Text = Loc.GetString("ghost-gui-respawn-button-allowed");
GhostRespawnButton.Disabled = false;
}
else
{
double delta = (_respawnTime.Value - _gameTiming.CurTime).TotalSeconds;
GhostRespawnButton.Text = Loc.GetString("ghost-gui-respawn-button-denied", ("time", $"{delta:f1}"));
GhostRespawnButton.Disabled = true;
}
}
// End Frontier
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@ -0,0 +1,22 @@
using Content.Shared._Corvax.Respawn;
namespace Content.Client._Corvax.Respawn;
public sealed class RespawnSystem : EntitySystem
{
public TimeSpan? RespawnResetTime { get; private set; }
public event Action? RespawnReseted;
public override void Initialize()
{
SubscribeNetworkEvent<RespawnResetEvent>(OnRespawnReset);
}
private void OnRespawnReset(RespawnResetEvent e)
{
RespawnResetTime = e.Time;
RespawnReseted?.Invoke();
}
}

View File

@ -0,0 +1,25 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'ghost-respawn-rules-window-title'}"
MinSize="500 350">
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False" Margin="5">
<PanelContainer StyleClasses="Inset">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#303133"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" VerticalExpand="True" SeparationOverride="5">
<PanelContainer Name="TextContainer" StyleClasses="Inset" Margin="5">
<PanelContainer.PanelOverride>
<!-- <graphics:StyleBoxFlat BackgroundColor="#464950"/> -->
<graphics:StyleBoxFlat BackgroundColor="#303133"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<Control VerticalExpand="True" VerticalAlignment="Stretch" />
<Button Name="ConfirmRespawnButton" Text="{Loc 'ghost-respawn-rules-window-confirm-button'}" Margin="5" />
</BoxContainer>
</PanelContainer>
</ScrollContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,29 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client._NF.UserInterface.Systems.Ghost.Controls
{
[GenerateTypedNameReferences]
public sealed partial class GhostRespawnRulesWindow : FancyWindow
{
public PanelContainer RulesContainer => TextContainer;
public RichTextLabel RulesLabel = new() { Margin = new Thickness(5, 5, 5, 5) };
public Button RespawnButton => ConfirmRespawnButton;
public GhostRespawnRulesWindow()
{
RobustXamlLoader.Load(this);
var message = new FormattedMessage();
message.AddMarkup(Loc.GetString("ghost-respawn-rules-window-rules"));
RulesLabel.SetMessage(message);
RulesContainer.AddChild(RulesLabel);
RulesLabel.SetPositionFirst();
RespawnButton.OnPressed += _ => Close();
}
}
}

View File

@ -0,0 +1,151 @@
using System.Runtime.InteropServices;
using Content.Server.Ghost.Roles.Components;
using Content.Shared._Corvax.Respawn;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Server.Player;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Content.Shared._NF.CCVar; // Frontier
using Robust.Shared.Configuration; // Frontier
using Robust.Shared.Player; // Frontier
using Content.Shared.Ghost; // Frontier
using Content.Server.Administration.Managers; // Frontier
using Content.Server.Administration; // Frontier
using Content.Shared.GameTicking; // Frontier
namespace Content.Server._Corvax.Respawn;
public sealed class RespawnSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IAdminManager _admin = default!;
private float _respawnTime = 0f;
// Frontier: struct for respawn lookup
private sealed class RespawnData
{
public TimeSpan RespawnTime; // The next time the user can respawn.
}
// End Frontier
[ViewVariables]
private Dictionary<NetUserId, RespawnData> _respawnInfo = new(); // Frontier: struct for complete respawn info
public override void Initialize()
{
SubscribeLocalEvent<MindContainerComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<MindContainerComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart); // Frontier
_admin.OnPermsChanged += OnAdminPermsChanged; // Frontier
_player.PlayerStatusChanged += PlayerStatusChanged; // Frontier
Subs.CVar(_cfg, NFCCVars.RespawnTime, OnRespawnCryoTimeChanged, true); // Frontier
}
// Frontier: CVar setters
private void OnRespawnCryoTimeChanged(float value)
{
_respawnTime = value;
}
// End Frontier
private void OnMobStateChanged(EntityUid entity, MindContainerComponent component, MobStateChangedEvent e)
{
if (e.NewMobState != MobState.Dead)
return;
if (!_player.TryGetSessionByEntity(entity, out var session))
return;
var respawnData = GetRespawnData(session.UserId);
SetRespawnTime(session.UserId, ref respawnData, _timing.CurTime + TimeSpan.FromSeconds(_respawnTime));
}
private void OnMindRemoved(EntityUid entity, MindContainerComponent _, MindRemovedMessage e)
{
if (e.Mind.Comp.UserId is null)
return;
// Mob is dead, don't reset spawn timer twice
if (TryComp<MobStateComponent>(entity, out var state) && state.CurrentState == MobState.Dead)
return;
// Frontier: extra conditions for respawn lenience
if (HasComp<GhostRoleComponent>(entity)) // Don't penalize user for exiting ghost roles
return; // Frontier: don't penalize user for exiting ghost roles
if (HasComp<GhostComponent>(entity)) // Don't penalize user for reobserving
return;
if (e.Mind.Comp.Session != null && _admin.IsAdmin(e.Mind.Comp.Session)) // Admins get free respawns
return;
// Get respawn info
var userId = e.Mind.Comp.UserId.Value;
var respawnInfo = GetRespawnData(userId);
// End Frontier
SetRespawnTime(userId, ref respawnInfo, _timing.CurTime + TimeSpan.FromSeconds(_respawnTime));
}
// Frontier: admin permissions handler: clear respawn data for admins
private void OnAdminPermsChanged(AdminPermsChangedEventArgs args)
{
if (args.IsAdmin)
{
var respawnData = GetRespawnData(args.Player.UserId);
SetRespawnTime(args.Player.UserId, ref respawnData, TimeSpan.Zero);
}
}
// Frontier: respawn handler: adjusts respawn and cryo timers.
public void Respawn(ICommonSession session)
{
var respawnData = GetRespawnData(session.UserId);
}
private void SetRespawnTime(NetUserId user, ref RespawnData data, TimeSpan nextSpawn, TimeSpan? cryoTime = null) // Frontier: Reset<Set, added cryoTime, time changed to be time of next respawn, not time of death
{
data.RespawnTime = nextSpawn;
if (_player.TryGetSessionById(user, out var session)) // Frontier: try first, if no valid session, nothing to do.
RaiseNetworkEvent(new RespawnResetEvent(nextSpawn), session);
}
public TimeSpan? GetRespawnTime(NetUserId user) // Frontier: GetRespawnResetTime<GetRespawnTime
{
return _respawnInfo.TryGetValue(user, out var data) ? data.RespawnTime : null;
}
// Frontier: return a writable reference
private ref RespawnData GetRespawnData(NetUserId player)
{
if (!_respawnInfo.ContainsKey(player))
_respawnInfo[player] = new RespawnData();
return ref CollectionsMarshal.GetValueRefOrNullRef(_respawnInfo, player);
}
// Frontier: send ghost timer on player connection
private void PlayerStatusChanged(object? _, SessionStatusEventArgs args)
{
var session = args.Session;
if (args.NewStatus == Robust.Shared.Enums.SessionStatus.InGame &&
_respawnInfo.ContainsKey(session.UserId))
{
RaiseNetworkEvent(new RespawnResetEvent(_respawnInfo[session.UserId].RespawnTime), session);
}
}
// Frontier: reset game state, we have a new round.
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
_respawnInfo.Clear();
}
// End Frontier
}

View File

@ -0,0 +1,71 @@
using Content.Server._Corvax.Respawn;
using Content.Server.GameTicking;
using Content.Server.Mind;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared._NF.CCVar;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._NF.Commands;
[AnyCommand()]
public sealed class GhostRespawnCommand : IConsoleCommand
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IEntitySystemManager _entity = default!;
public string Command => "ghostrespawn";
public string Description => "Allows the player to return to the lobby if they've been dead long enough, allowing re-entering the round AS ANOTHER CHARACTER.";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (!_configurationManager.GetCVar(NFCCVars.RespawnEnabled))
{
shell.WriteLine("Respawning is disabled, ask an admin to respawn you.");
return;
}
if (shell.Player is null)
{
shell.WriteLine("You cannot run this from the console!");
return;
}
if (shell.Player.AttachedEntity is null)
{
shell.WriteLine("You cannot run this in the lobby, or without an entity.");
return;
}
if (!_entityManager.TryGetComponent<GhostComponent>(shell.Player.AttachedEntity, out var ghost))
{
shell.WriteLine("You are not a ghost.");
return;
}
var respawnResetTime = _entity.GetEntitySystem<RespawnSystem>().GetRespawnTime(shell.Player.UserId);
if (respawnResetTime is not null)
{
if (_gameTiming.CurTime < respawnResetTime.Value)
{
var timeLeft = (respawnResetTime.Value - _gameTiming.CurTime).TotalSeconds;
shell.WriteLine($"You haven't been dead long enough. You can respawn in {timeLeft} seconds.");
return;
}
}
var gameTicker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
gameTicker.Respawn(shell.Player);
}
}

View File

@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared._Corvax.Respawn;
[Serializable, NetSerializable]
public sealed class RespawnResetEvent(TimeSpan? time) : EntityEventArgs
{
public readonly TimeSpan? Time = time;
}

View File

@ -0,0 +1,28 @@
using Robust.Shared.Configuration;
namespace Content.Shared._NF.CCVar;
[CVarDefs]
public sealed class NFCCVars
{
/*
* Respawn
*/
/// <summary>
/// Whether or not respawning is enabled.
/// </summary>
public static readonly CVarDef<bool> RespawnEnabled =
CVarDef.Create("nf14.respawn.enabled", true, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Respawn time, how long the player has to wait in seconds after going into cryosleep. Should be small, misclicks happen.
/// </summary>
public static readonly CVarDef<float> RespawnCryoFirstTime =
CVarDef.Create("nf14.respawn.cryo_first_time", 20.0f, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Respawn time, how long the player has to wait in seconds after death, or on subsequent cryo attempts.
/// </summary>
public static readonly CVarDef<float> RespawnTime =
CVarDef.Create("nf14.respawn.time", 1200.0f, CVar.SERVER | CVar.REPLICATED);
}

View File

@ -0,0 +1,16 @@
## UI
ghost-respawn-rules-window-title = Ghost Respawn Rules
ghost-respawn-rules-window-confirm-button = I understand, respawn me
ghost-gui-respawn-button-denied = Respawn ({$time}s)
ghost-gui-respawn-button-allowed = Respawn
# DeltaV - we have our own rules for respawning
ghost-respawn-rules-window-rules = Respawning follows our rule B1.3: Follow the new life
You MAY NOT respawn as a character who has died this round. This applies to variants of that same character; the character you choose must be entirely new to the station's shift.
Your newly spawned character is treated as if they are a fresh arrival to the station.
## COMMMANDS
ghost-respawn-command-desc = Respawns you if you're an eligible ghost.
ghost-respawn-not-a-ghost = You're not currently ghosted.
ghost-respawn-ineligible = You're not currently eligible