Add ghost role raffles (#26629)
* Add ghost role raffles * GRR: Fix dialogue sizing, fix merge * GRR: Add raffle deciders (winner picker) * GRR: Make settings prototype based with option to override * GRR: Use Raffles folder and namespace * GRR: DataFieldify and TimeSpanify * GRR: Don't actually DataFieldify HashSet<ICommonSession>s * GRR: add GetGhostRoleCount() + docs * update engine on branch * Ghost role raffles: docs, fix window size, cleanup, etc * GRR: Admin UI * GRR: Admin UI: Display initial/max/ext of selected raffle settings proto * GRR: Make a ton of roles raffled
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Ghost.Roles.Events;
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Content.Server.Ghost.Roles.UI;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Administration;
|
||||
@@ -21,7 +24,9 @@ using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -41,12 +46,16 @@ namespace Content.Server.Ghost.Roles
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private uint _nextRoleIdentifier;
|
||||
private bool _needsUpdateGhostRoleCount = true;
|
||||
|
||||
private readonly Dictionary<uint, Entity<GhostRoleComponent>> _ghostRoles = new();
|
||||
private readonly Dictionary<uint, Entity<GhostRoleRaffleComponent>> _ghostRoleRaffles = new();
|
||||
|
||||
private readonly Dictionary<ICommonSession, GhostRolesEui> _openUis = new();
|
||||
private readonly Dictionary<ICommonSession, MakeGhostRoleEui> _openMakeGhostRoleUis = new();
|
||||
|
||||
@@ -63,10 +72,12 @@ namespace Content.Server.Ghost.Roles
|
||||
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindRemovedMessage>(OnMindRemoved);
|
||||
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<GhostRoleComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnRoleStartup);
|
||||
SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnRoleShutdown);
|
||||
SubscribeLocalEvent<GhostRoleComponent, EntityPausedEvent>(OnPaused);
|
||||
SubscribeLocalEvent<GhostRoleComponent, EntityUnpausedEvent>(OnUnpaused);
|
||||
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentInit>(OnRaffleInit);
|
||||
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentShutdown>(OnRaffleShutdown);
|
||||
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, TakeGhostRoleEvent>(OnSpawnerTakeRole);
|
||||
SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
|
||||
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GetVerbsEvent<Verb>>(OnVerb);
|
||||
@@ -165,17 +176,118 @@ namespace Content.Server.Ghost.Roles
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
if (_needsUpdateGhostRoleCount)
|
||||
|
||||
UpdateGhostRoleCount();
|
||||
UpdateRaffles(frameTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending count update for the ghost role button in ghost UI, if ghost role count changed.
|
||||
/// </summary>
|
||||
private void UpdateGhostRoleCount()
|
||||
{
|
||||
if (!_needsUpdateGhostRoleCount)
|
||||
return;
|
||||
|
||||
_needsUpdateGhostRoleCount = false;
|
||||
var response = new GhostUpdateGhostRoleCountEvent(GetGhostRoleCount());
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
_needsUpdateGhostRoleCount = false;
|
||||
var response = new GhostUpdateGhostRoleCountEvent(GetGhostRolesInfo().Length);
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
RaiseNetworkEvent(response, player.Channel);
|
||||
}
|
||||
RaiseNetworkEvent(response, player.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ghost role raffle logic.
|
||||
/// </summary>
|
||||
private void UpdateRaffles(float frameTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<GhostRoleRaffleComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var entityUid, out var raffle, out var meta))
|
||||
{
|
||||
if (meta.EntityPaused)
|
||||
continue;
|
||||
|
||||
// if all participants leave/were removed from the raffle, the raffle is canceled.
|
||||
if (raffle.CurrentMembers.Count == 0)
|
||||
{
|
||||
RemoveRaffleAndUpdateEui(entityUid, raffle);
|
||||
continue;
|
||||
}
|
||||
|
||||
raffle.Countdown = raffle.Countdown.Subtract(TimeSpan.FromSeconds(frameTime));
|
||||
if (raffle.Countdown.Ticks > 0)
|
||||
continue;
|
||||
|
||||
// the raffle is over! find someone to take over the ghost role
|
||||
if (!TryComp(entityUid, out GhostRoleComponent? ghostRole))
|
||||
{
|
||||
Log.Warning($"Ghost role raffle finished on {entityUid} but {nameof(GhostRoleComponent)} is missing");
|
||||
RemoveRaffleAndUpdateEui(entityUid, raffle);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ghostRole.RaffleConfig is null)
|
||||
{
|
||||
Log.Warning($"Ghost role raffle finished on {entityUid} but RaffleConfig became null");
|
||||
RemoveRaffleAndUpdateEui(entityUid, raffle);
|
||||
continue;
|
||||
}
|
||||
|
||||
var foundWinner = false;
|
||||
var deciderPrototype = _prototype.Index(ghostRole.RaffleConfig.Decider);
|
||||
|
||||
// use the ghost role's chosen winner picker to find a winner
|
||||
deciderPrototype.Decider.PickWinner(
|
||||
raffle.CurrentMembers.AsEnumerable(),
|
||||
session =>
|
||||
{
|
||||
var success = TryTakeover(session, raffle.Identifier);
|
||||
foundWinner |= success;
|
||||
return success;
|
||||
}
|
||||
);
|
||||
|
||||
if (!foundWinner)
|
||||
{
|
||||
Log.Warning($"Ghost role raffle for {entityUid} ({ghostRole.RoleName}) finished without " +
|
||||
$"{ghostRole.RaffleConfig?.Decider} finding a winner");
|
||||
}
|
||||
|
||||
// raffle over
|
||||
RemoveRaffleAndUpdateEui(entityUid, raffle);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryTakeover(ICommonSession player, uint identifier)
|
||||
{
|
||||
// TODO: the following two checks are kind of redundant since they should already be removed
|
||||
// from the raffle
|
||||
// can't win if you are disconnected (although you shouldn't be a candidate anyway)
|
||||
if (player.Status != SessionStatus.InGame)
|
||||
return false;
|
||||
|
||||
// can't win if you are no longer a ghost (e.g. if you returned to your body)
|
||||
if (player.AttachedEntity == null || !HasComp<GhostComponent>(player.AttachedEntity))
|
||||
return false;
|
||||
|
||||
if (Takeover(player, identifier))
|
||||
{
|
||||
// takeover successful, we have a winner! remove the winner from other raffles they might be in
|
||||
LeaveAllRaffles(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RemoveRaffleAndUpdateEui(EntityUid entityUid, GhostRoleRaffleComponent raffle)
|
||||
{
|
||||
_ghostRoleRaffles.Remove(raffle.Identifier);
|
||||
RemComp(entityUid, raffle);
|
||||
UpdateAllEui();
|
||||
}
|
||||
|
||||
private void PlayerStatusChanged(object? blah, SessionStatusEventArgs args)
|
||||
{
|
||||
if (args.NewStatus == SessionStatus.InGame)
|
||||
@@ -183,6 +295,11 @@ namespace Content.Server.Ghost.Roles
|
||||
var response = new GhostUpdateGhostRoleCountEvent(_ghostRoles.Count);
|
||||
RaiseNetworkEvent(response, args.Session.Channel);
|
||||
}
|
||||
else
|
||||
{
|
||||
// people who disconnect are removed from ghost role raffles
|
||||
LeaveAllRaffles(args.Session);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterGhostRole(Entity<GhostRoleComponent> role)
|
||||
@@ -201,24 +318,170 @@ namespace Content.Server.Ghost.Roles
|
||||
return;
|
||||
|
||||
_ghostRoles.Remove(comp.Identifier);
|
||||
if (TryComp(role.Owner, out GhostRoleRaffleComponent? raffle))
|
||||
{
|
||||
// if a raffle is still running, get rid of it
|
||||
RemoveRaffleAndUpdateEui(role.Owner, raffle);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateAllEui();
|
||||
}
|
||||
}
|
||||
|
||||
// probably fine to be init because it's never added during entity initialization, but much later
|
||||
private void OnRaffleInit(Entity<GhostRoleRaffleComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp(ent, out GhostRoleComponent? ghostRole))
|
||||
{
|
||||
// can't have a raffle for a ghost role that doesn't exist
|
||||
RemComp<GhostRoleRaffleComponent>(ent);
|
||||
return;
|
||||
}
|
||||
|
||||
var config = ghostRole.RaffleConfig;
|
||||
if (config is null)
|
||||
return; // should, realistically, never be reached but you never know
|
||||
|
||||
var settings = config.SettingsOverride
|
||||
?? _prototype.Index<GhostRoleRaffleSettingsPrototype>(config.Settings).Settings;
|
||||
|
||||
if (settings.MaxDuration < settings.InitialDuration)
|
||||
{
|
||||
Log.Error($"Ghost role on {ent} has invalid raffle settings (max duration shorter than initial)");
|
||||
ghostRole.RaffleConfig = null; // make it a non-raffle role so stuff isn't entirely broken
|
||||
RemComp<GhostRoleRaffleComponent>(ent);
|
||||
return;
|
||||
}
|
||||
|
||||
var raffle = ent.Comp;
|
||||
raffle.Identifier = ghostRole.Identifier;
|
||||
raffle.Countdown = TimeSpan.FromSeconds(settings.InitialDuration);
|
||||
raffle.CumulativeTime = TimeSpan.FromSeconds(settings.InitialDuration);
|
||||
// we copy these settings into the component because they would be cumbersome to access otherwise
|
||||
raffle.JoinExtendsDurationBy = TimeSpan.FromSeconds(settings.JoinExtendsDurationBy);
|
||||
raffle.MaxDuration = TimeSpan.FromSeconds(settings.MaxDuration);
|
||||
}
|
||||
|
||||
private void OnRaffleShutdown(Entity<GhostRoleRaffleComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_ghostRoleRaffles.Remove(ent.Comp.Identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joins the given player onto a ghost role raffle, or creates it if it doesn't exist.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="identifier">The ID that represents the ghost role or ghost role raffle.
|
||||
/// (A raffle will have the same ID as the ghost role it's for.)</param>
|
||||
private void JoinRaffle(ICommonSession player, uint identifier)
|
||||
{
|
||||
if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
|
||||
return;
|
||||
|
||||
// get raffle or create a new one if it doesn't exist
|
||||
var raffle = _ghostRoleRaffles.TryGetValue(identifier, out var raffleEnt)
|
||||
? raffleEnt.Comp
|
||||
: EnsureComp<GhostRoleRaffleComponent>(roleEnt.Owner);
|
||||
|
||||
_ghostRoleRaffles.TryAdd(identifier, (roleEnt.Owner, raffle));
|
||||
|
||||
if (!raffle.CurrentMembers.Add(player))
|
||||
{
|
||||
Log.Warning($"{player.Name} tried to join raffle for ghost role {identifier} but they are already in the raffle");
|
||||
return;
|
||||
}
|
||||
|
||||
// if this is the first time the player joins this raffle, and the player wasn't the starter of the raffle:
|
||||
// extend the countdown, but only if doing so will not make the raffle take longer than the maximum
|
||||
// duration
|
||||
if (raffle.AllMembers.Add(player) && raffle.AllMembers.Count > 1
|
||||
&& raffle.CumulativeTime.Add(raffle.JoinExtendsDurationBy) <= raffle.MaxDuration)
|
||||
{
|
||||
raffle.Countdown += raffle.JoinExtendsDurationBy;
|
||||
raffle.CumulativeTime += raffle.JoinExtendsDurationBy;
|
||||
}
|
||||
|
||||
UpdateAllEui();
|
||||
}
|
||||
|
||||
public void Takeover(ICommonSession player, uint identifier)
|
||||
/// <summary>
|
||||
/// Makes the given player leave the raffle corresponding to the given ID.
|
||||
/// </summary>
|
||||
public void LeaveRaffle(ICommonSession player, uint identifier)
|
||||
{
|
||||
if (!_ghostRoleRaffles.TryGetValue(identifier, out var raffleEnt))
|
||||
return;
|
||||
|
||||
if (raffleEnt.Comp.CurrentMembers.Remove(player))
|
||||
{
|
||||
UpdateAllEui();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"{player.Name} tried to leave raffle for ghost role {identifier} but they are not in the raffle");
|
||||
}
|
||||
|
||||
// (raffle ending because all players left is handled in update())
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the given player leave all ghost role raffles.
|
||||
/// </summary>
|
||||
public void LeaveAllRaffles(ICommonSession player)
|
||||
{
|
||||
var shouldUpdateEui = false;
|
||||
|
||||
foreach (var raffleEnt in _ghostRoleRaffles.Values)
|
||||
{
|
||||
shouldUpdateEui |= raffleEnt.Comp.CurrentMembers.Remove(player);
|
||||
}
|
||||
|
||||
if (shouldUpdateEui)
|
||||
UpdateAllEui();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request a ghost role. If it's a raffled role starts or joins a raffle, otherwise the player immediately
|
||||
/// takes over the ghost role if possible.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="identifier">ID of the ghost role.</param>
|
||||
public void Request(ICommonSession player, uint identifier)
|
||||
{
|
||||
if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
|
||||
return;
|
||||
|
||||
if (roleEnt.Comp.RaffleConfig is not null)
|
||||
{
|
||||
JoinRaffle(player, identifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
Takeover(player, identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle.
|
||||
/// </summary>
|
||||
/// <returns>True if takeover was successful, otherwise false.</returns>
|
||||
public bool Takeover(ICommonSession player, uint identifier)
|
||||
{
|
||||
if (!_ghostRoles.TryGetValue(identifier, out var role))
|
||||
return;
|
||||
return false;
|
||||
|
||||
var ev = new TakeGhostRoleEvent(player);
|
||||
RaiseLocalEvent(role, ref ev);
|
||||
|
||||
if (!ev.TookRole)
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (player.AttachedEntity != null)
|
||||
_adminLogger.Add(LogType.GhostRoleTaken, LogImpact.Low, $"{player:player} took the {role.Comp.RoleName:roleName} ghost role {ToPrettyString(player.AttachedEntity.Value):entity}");
|
||||
|
||||
CloseEui(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Follow(ICommonSession player, uint identifier)
|
||||
@@ -247,7 +510,22 @@ namespace Content.Server.Ghost.Roles
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
}
|
||||
|
||||
public GhostRoleInfo[] GetGhostRolesInfo()
|
||||
/// <summary>
|
||||
/// Returns the number of available ghost roles.
|
||||
/// </summary>
|
||||
public int GetGhostRoleCount()
|
||||
{
|
||||
var metaQuery = GetEntityQuery<MetaDataComponent>();
|
||||
return _ghostRoles.Count(pair => metaQuery.GetComponent(pair.Value.Owner).EntityPaused == false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about all available ghost roles.
|
||||
/// </summary>
|
||||
/// <param name="player">
|
||||
/// If not null, the <see cref="GhostRoleInfo"/>s will show if the given player is in a raffle.
|
||||
/// </param>
|
||||
public GhostRoleInfo[] GetGhostRolesInfo(ICommonSession? player)
|
||||
{
|
||||
var roles = new List<GhostRoleInfo>();
|
||||
var metaQuery = GetEntityQuery<MetaDataComponent>();
|
||||
@@ -257,7 +535,40 @@ namespace Content.Server.Ghost.Roles
|
||||
if (metaQuery.GetComponent(uid).EntityPaused)
|
||||
continue;
|
||||
|
||||
roles.Add(new GhostRoleInfo { Identifier = id, Name = role.RoleName, Description = role.RoleDescription, Rules = role.RoleRules, Requirements = role.Requirements });
|
||||
|
||||
var kind = GhostRoleKind.FirstComeFirstServe;
|
||||
GhostRoleRaffleComponent? raffle = null;
|
||||
|
||||
if (role.RaffleConfig is not null)
|
||||
{
|
||||
kind = GhostRoleKind.RaffleReady;
|
||||
|
||||
if (_ghostRoleRaffles.TryGetValue(id, out var raffleEnt))
|
||||
{
|
||||
kind = GhostRoleKind.RaffleInProgress;
|
||||
raffle = raffleEnt.Comp;
|
||||
|
||||
if (player is not null && raffle.CurrentMembers.Contains(player))
|
||||
kind = GhostRoleKind.RaffleJoined;
|
||||
}
|
||||
}
|
||||
|
||||
var rafflePlayerCount = (uint?) raffle?.CurrentMembers.Count ?? 0;
|
||||
var raffleEndTime = raffle is not null
|
||||
? _timing.CurTime.Add(raffle.Countdown)
|
||||
: TimeSpan.MinValue;
|
||||
|
||||
roles.Add(new GhostRoleInfo
|
||||
{
|
||||
Identifier = id,
|
||||
Name = role.RoleName,
|
||||
Description = role.RoleDescription,
|
||||
Rules = role.RoleRules,
|
||||
Requirements = role.Requirements,
|
||||
Kind = kind,
|
||||
RafflePlayerCount = rafflePlayerCount,
|
||||
RaffleEndTime = raffleEndTime
|
||||
});
|
||||
}
|
||||
|
||||
return roles.ToArray();
|
||||
@@ -272,6 +583,10 @@ namespace Content.Server.Ghost.Roles
|
||||
if (HasComp<GhostComponent>(message.Entity))
|
||||
return;
|
||||
|
||||
// The player is not a ghost (anymore), so they should not be in any raffles. Remove them.
|
||||
// This ensures player doesn't win a raffle after returning to their (revived) body and ends up being
|
||||
// forced into a ghost role.
|
||||
LeaveAllRaffles(message.Player);
|
||||
CloseEui(message.Player);
|
||||
}
|
||||
|
||||
@@ -306,6 +621,7 @@ namespace Content.Server.Ghost.Roles
|
||||
|
||||
_openUis.Clear();
|
||||
_ghostRoles.Clear();
|
||||
_ghostRoleRaffles.Clear();
|
||||
_nextRoleIdentifier = 0;
|
||||
}
|
||||
|
||||
@@ -331,12 +647,12 @@ namespace Content.Server.Ghost.Roles
|
||||
RemCompDeferred<GhostRoleComponent>(ent);
|
||||
}
|
||||
|
||||
private void OnStartup(Entity<GhostRoleComponent> ent, ref ComponentStartup args)
|
||||
private void OnRoleStartup(Entity<GhostRoleComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
RegisterGhostRole(ent);
|
||||
}
|
||||
|
||||
private void OnShutdown(Entity<GhostRoleComponent> role, ref ComponentShutdown args)
|
||||
private void OnRoleShutdown(Entity<GhostRoleComponent> role, ref ComponentShutdown args)
|
||||
{
|
||||
UnregisterGhostRole(role);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user