Cherrypicks 4 (#393)
* Immovable Rod changes (#26757) * Adds non randomized rod velocity (#27123) * adds non randomized rod velocity * Adds despawn suffix to despawn rod * make fire spreading scale with mass (#27202) * make fire spreading scale with mass * realer --------- Co-authored-by: deltanedas <@deltanedas:kde.org> * lower max firestacks to 10, refactor flammable (#27159) * lower max firestacks to 10, refactor flammable * fix * uncap fire stack damage, lower fire stack damage * fix fire spread round removal (#27986) * fix a resolve debug assert * rewrite fire spread --------- Co-authored-by: deltanedas <@deltanedas:kde.org> * fire troll fix (#28034) Co-authored-by: deltanedas <@deltanedas:kde.org> * Hide doafters if you're in a container (#29487) * Hide doafters if you're in a container * Out of the loop --------- Co-authored-by: plykiya <plykiya@protonmail.com> * 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 * Make ERT use short raffle timer (#27830) Co-authored-by: plykiya <plykiya@protonmail.com> * gives loneops a proper ghost role raffle (#27841) * shorten short raffle (#28685) * - fix: Conflicts. * - fix. --------- Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com> Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com> Co-authored-by: plykiya <plykiya@protonmail.com> Co-authored-by: no <165581243+pissdemon@users.noreply.github.com> Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com> Co-authored-by: HS <81934438+HolySSSS@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Roles;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Components
|
||||
@@ -87,5 +88,12 @@ namespace Content.Server.Ghost.Roles.Components
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("reregister")]
|
||||
public bool ReregisterOnGhost { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
|
||||
/// </summary>
|
||||
[DataField("raffle")]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a ghost role is currently being raffled, and stores data about the raffle in progress.
|
||||
/// Raffles start when the first player joins a raffle.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(GhostRoleSystem))]
|
||||
public sealed partial class GhostRoleRaffleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier of the <see cref="GhostRoleComponent">Ghost Role</see> this raffle is for.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField]
|
||||
public uint Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of sessions that are currently in the raffle.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public HashSet<ICommonSession> CurrentMembers = [];
|
||||
|
||||
/// <summary>
|
||||
/// List of sessions that are currently or were previously in the raffle.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public HashSet<ICommonSession> AllMembers = [];
|
||||
|
||||
/// <summary>
|
||||
/// Time left in the raffle in seconds. This must be initialized to a positive value.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField]
|
||||
public TimeSpan Countdown = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The cumulative time, i.e. how much time the raffle will take in total. Added to when the time is extended
|
||||
/// by someone joining the raffle.
|
||||
/// Must be set to the same value as <see cref="Countdown"/> on initialization.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("cumulativeTime")]
|
||||
public TimeSpan CumulativeTime = TimeSpan.MaxValue;
|
||||
|
||||
/// <inheritdoc cref="GhostRoleRaffleSettings.JoinExtendsDurationBy"/>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("joinExtendsDurationBy")]
|
||||
public TimeSpan JoinExtendsDurationBy { get; set; }
|
||||
|
||||
/// <inheritdoc cref="GhostRoleRaffleSettings.MaxDuration"/>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("maxDuration")]
|
||||
public TimeSpan MaxDuration { get; set; }
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
using Content.Server._Miracle.GulagSystem;
|
||||
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.Server.Popups;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Follower;
|
||||
@@ -22,7 +26,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;
|
||||
|
||||
namespace Content.Server.Ghost.Roles
|
||||
@@ -39,11 +45,16 @@ namespace Content.Server.Ghost.Roles
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly GulagSystem _gulagSystem = 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();
|
||||
|
||||
@@ -60,10 +71,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);
|
||||
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
|
||||
@@ -161,17 +174,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)
|
||||
@@ -179,6 +293,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)
|
||||
@@ -197,12 +316,65 @@ namespace Content.Server.Ghost.Roles
|
||||
return;
|
||||
|
||||
_ghostRoles.Remove(comp.Identifier);
|
||||
UpdateAllEui();
|
||||
if (TryComp(role.Owner, out GhostRoleRaffleComponent? raffle))
|
||||
{
|
||||
// if a raffle is still running, get rid of it
|
||||
RemoveRaffleAndUpdateEui(role.Owner, raffle);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateAllEui();
|
||||
}
|
||||
}
|
||||
|
||||
public void Takeover(ICommonSession player, uint identifier)
|
||||
// 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 (!_ghostRoles.TryGetValue(identifier, out var role))
|
||||
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;
|
||||
|
||||
if (_gulagSystem.IsUserGulagged(player.UserId, out _))
|
||||
@@ -210,16 +382,114 @@ namespace Content.Server.Ghost.Roles
|
||||
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();
|
||||
}
|
||||
|
||||
/// <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 false;
|
||||
|
||||
if (_gulagSystem.IsUserGulagged(player.UserId, out _))
|
||||
{
|
||||
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)
|
||||
@@ -248,7 +518,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>();
|
||||
@@ -258,7 +543,39 @@ 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();
|
||||
@@ -273,6 +590,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);
|
||||
}
|
||||
|
||||
@@ -307,6 +628,7 @@ namespace Content.Server.Ghost.Roles
|
||||
|
||||
_openUis.Clear();
|
||||
_ghostRoles.Clear();
|
||||
_ghostRoleRaffles.Clear();
|
||||
_nextRoleIdentifier = 0;
|
||||
}
|
||||
|
||||
@@ -332,12 +654,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);
|
||||
}
|
||||
|
||||
127
Content.Server/Ghost/Roles/MakeRaffledGhostRoleCommand.cs
Normal file
127
Content.Server/Ghost/Roles/MakeRaffledGhostRoleCommand.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Ghost.Roles
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class MakeRaffledGhostRoleCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "makeghostroleraffled";
|
||||
public string Description => "Turns an entity into a raffled ghost role.";
|
||||
public string Help => $"Usage: {Command} <entity uid> <name> <description> (<settings prototype> | <initial duration> <extend by> <max duration>) [<rules>]\n" +
|
||||
$"Durations are in seconds.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length is < 4 or > 7)
|
||||
{
|
||||
shell.WriteLine($"Invalid amount of arguments.\n{Help}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var uidNet) || !_entManager.TryGetEntity(uidNet, out var uid))
|
||||
{
|
||||
shell.WriteLine($"{args[0]} is not a valid entity uid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.TryGetComponent(uid, out MetaDataComponent? metaData))
|
||||
{
|
||||
shell.WriteLine($"No entity found with uid {uid}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entManager.TryGetComponent(uid, out MindContainerComponent? mind) &&
|
||||
mind.HasMind)
|
||||
{
|
||||
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a mind.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entManager.TryGetComponent(uid, out GhostRoleComponent? ghostRole))
|
||||
{
|
||||
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a {nameof(GhostRoleComponent)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entManager.HasComponent<GhostTakeoverAvailableComponent>(uid))
|
||||
{
|
||||
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a {nameof(GhostTakeoverAvailableComponent)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[1];
|
||||
var description = args[2];
|
||||
|
||||
// if the rules are specified then use those, otherwise use the default
|
||||
var rules = args.Length switch
|
||||
{
|
||||
5 => args[4],
|
||||
7 => args[6],
|
||||
_ => Loc.GetString("ghost-role-component-default-rules"),
|
||||
};
|
||||
|
||||
// is it an invocation with a prototype ID and optional rules?
|
||||
var isProto = args.Length is 4 or 5;
|
||||
GhostRoleRaffleSettings settings;
|
||||
|
||||
if (isProto)
|
||||
{
|
||||
if (!_protoManager.TryIndex<GhostRoleRaffleSettingsPrototype>(args[4], out var proto))
|
||||
{
|
||||
var validProtos = string.Join(", ",
|
||||
_protoManager.EnumeratePrototypes<GhostRoleRaffleSettingsPrototype>().Select(p => p.ID)
|
||||
);
|
||||
|
||||
shell.WriteLine($"{args[4]} is not a valid raffle settings prototype. Valid options: {validProtos}");
|
||||
return;
|
||||
}
|
||||
|
||||
settings = proto.Settings;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!uint.TryParse(args[3], out var initial)
|
||||
|| !uint.TryParse(args[4], out var extends)
|
||||
|| !uint.TryParse(args[5], out var max)
|
||||
|| initial == 0 || max == 0)
|
||||
{
|
||||
shell.WriteLine($"The raffle initial/extends/max settings must be positive numbers.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (initial > max)
|
||||
{
|
||||
shell.WriteLine("The initial duration must be smaller than or equal to the maximum duration.");
|
||||
return;
|
||||
}
|
||||
|
||||
settings = new GhostRoleRaffleSettings()
|
||||
{
|
||||
InitialDuration = initial,
|
||||
JoinExtendsDurationBy = extends,
|
||||
MaxDuration = max
|
||||
};
|
||||
}
|
||||
|
||||
ghostRole = _entManager.AddComponent<GhostRoleComponent>(uid.Value);
|
||||
_entManager.AddComponent<GhostTakeoverAvailableComponent>(uid.Value);
|
||||
ghostRole.RoleName = name;
|
||||
ghostRole.RoleDescription = description;
|
||||
ghostRole.RoleRules = rules;
|
||||
ghostRole.RaffleConfig = new GhostRoleRaffleConfig(settings);
|
||||
|
||||
shell.WriteLine($"Made entity {metaData.EntityName} a raffled ghost role.");
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleConfig.cs
Normal file
35
Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleConfig.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Raffle configuration.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed partial class GhostRoleRaffleConfig
|
||||
{
|
||||
public GhostRoleRaffleConfig(GhostRoleRaffleSettings settings)
|
||||
{
|
||||
SettingsOverride = settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the raffle settings to use.
|
||||
/// </summary>
|
||||
[DataField("settings", required: true)]
|
||||
public ProtoId<GhostRoleRaffleSettingsPrototype> Settings { get; set; } = "default";
|
||||
|
||||
/// <summary>
|
||||
/// If not null, the settings from <see cref="Settings"/> are ignored and these settings are used instead.
|
||||
/// Intended for allowing admins to set custom raffle settings for admeme ghost roles.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public GhostRoleRaffleSettings? SettingsOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets which <see cref="IGhostRoleRaffleDecider"/> is used.
|
||||
/// </summary>
|
||||
[DataField("decider")]
|
||||
public ProtoId<GhostRoleRaffleDeciderPrototype> Decider { get; set; } = "default";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Allows getting a <see cref="IGhostRoleRaffleDecider"/> as prototype.
|
||||
/// </summary>
|
||||
[Prototype("ghostRoleRaffleDecider")]
|
||||
public sealed class GhostRoleRaffleDeciderPrototype : IPrototype
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IGhostRoleRaffleDecider"/> instance that chooses the winner of a raffle.
|
||||
/// </summary>
|
||||
[DataField("decider", required: true)]
|
||||
public IGhostRoleRaffleDecider Decider { get; private set; } = new RngGhostRoleRaffleDecider();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Chooses a winner of a ghost role raffle.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public partial interface IGhostRoleRaffleDecider
|
||||
{
|
||||
/// <summary>
|
||||
/// Chooses a winner of a ghost role raffle draw from the given pool of candidates.
|
||||
/// </summary>
|
||||
/// <param name="candidates">The players in the session at the time of drawing.</param>
|
||||
/// <param name="tryTakeover">
|
||||
/// Call this with the chosen winner as argument.
|
||||
/// <ul><li>If <c>true</c> is returned, your winner was able to take over the ghost role, and the drawing is complete.
|
||||
/// <b>Do not call <see cref="tryTakeover"/> again after true is returned.</b></li>
|
||||
/// <li>If <c>false</c> is returned, your winner was not able to take over the ghost role,
|
||||
/// and you must choose another winner, and call <see cref="tryTakeover"/> with the new winner as argument.</li>
|
||||
/// </ul>
|
||||
///
|
||||
/// If <see cref="tryTakeover"/> is not called, or only returns false, the raffle will end without a winner.
|
||||
/// Do not call <see cref="tryTakeover"/> with the same player several times.
|
||||
/// </param>
|
||||
void PickWinner(IEnumerable<ICommonSession> candidates, Func<ICommonSession, bool> tryTakeover);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the winner of a ghost role raffle entirely randomly, without any weighting.
|
||||
/// </summary>
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public sealed partial class RngGhostRoleRaffleDecider : IGhostRoleRaffleDecider
|
||||
{
|
||||
public void PickWinner(IEnumerable<ICommonSession> candidates, Func<ICommonSession, bool> tryTakeover)
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
|
||||
var choices = candidates.ToList();
|
||||
random.Shuffle(choices); // shuffle the list so we can pick a lucky winner!
|
||||
|
||||
foreach (var candidate in choices)
|
||||
{
|
||||
if (tryTakeover(candidate))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,16 @@ namespace Content.Server.Ghost.Roles.UI
|
||||
{
|
||||
public sealed class GhostRolesEui : BaseEui
|
||||
{
|
||||
[Dependency] private readonly GhostRoleSystem _ghostRoleSystem;
|
||||
|
||||
public GhostRolesEui()
|
||||
{
|
||||
_ghostRoleSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GhostRoleSystem>();
|
||||
}
|
||||
|
||||
public override GhostRolesEuiState GetNewState()
|
||||
{
|
||||
return new(EntitySystem.Get<GhostRoleSystem>().GetGhostRolesInfo());
|
||||
return new(_ghostRoleSystem.GetGhostRolesInfo(Player));
|
||||
}
|
||||
|
||||
public override void HandleMessage(EuiMessageBase msg)
|
||||
@@ -17,11 +24,14 @@ namespace Content.Server.Ghost.Roles.UI
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case GhostRoleTakeoverRequestMessage req:
|
||||
EntitySystem.Get<GhostRoleSystem>().Takeover(Player, req.Identifier);
|
||||
case RequestGhostRoleMessage req:
|
||||
_ghostRoleSystem.Request(Player, req.Identifier);
|
||||
break;
|
||||
case GhostRoleFollowRequestMessage req:
|
||||
EntitySystem.Get<GhostRoleSystem>().Follow(Player, req.Identifier);
|
||||
case FollowGhostRoleMessage req:
|
||||
_ghostRoleSystem.Follow(Player, req.Identifier);
|
||||
break;
|
||||
case LeaveGhostRoleRaffleMessage req:
|
||||
_ghostRoleSystem.LeaveRaffle(Player, req.Identifier);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user