Revert "Gamerule Entities" (#15724)

This commit is contained in:
metalgearsloth
2023-04-24 16:21:05 +10:00
committed by GitHub
parent 39cc02b8f9
commit d3552dae00
124 changed files with 4328 additions and 3083 deletions

View File

@@ -1,33 +0,0 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Simple GameRule that will do a free-for-all death match.
/// Kill everybody else to win.
/// </summary>
[RegisterComponent, Access(typeof(DeathMatchRuleSystem))]
public sealed class DeathMatchRuleComponent : Component
{
/// <summary>
/// How long until the round restarts
/// </summary>
[DataField("restartDelay"), ViewVariables(VVAccess.ReadWrite)]
public float RestartDelay = 10f;
/// <summary>
/// How long after a person dies will the restart be checked
/// </summary>
[DataField("deadCheckDelay"), ViewVariables(VVAccess.ReadWrite)]
public float DeadCheckDelay = 5f;
/// <summary>
/// A timer for checking after a death
/// </summary>
[DataField("deadCheckTimer"), ViewVariables(VVAccess.ReadWrite)]
public float? DeadCheckTimer;
/// <summary>
/// A timer for the restart.
/// </summary>
[DataField("restartTimer"), ViewVariables(VVAccess.ReadWrite)]
public float? RestartTimer;
}

View File

@@ -1,44 +0,0 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Component attached to all gamerule entities.
/// Used to both track the entity as well as store basic data
/// </summary>
[RegisterComponent]
public sealed class GameRuleComponent : Component
{
/// <summary>
/// Whether or not the rule is active.
/// Is enabled after <see cref="GameRuleStartedEvent"/> and disabled after <see cref="GameRuleEndedEvent"/>
/// </summary>
[DataField("active")]
public bool Active;
/// <summary>
/// Whether or not the gamerule finished.
/// Used for tracking whether a non-active gamerule has been started before.
/// </summary>
[DataField("ended")]
public bool Ended;
}
/// <summary>
/// Raised when a rule is added but hasn't formally begun yet.
/// Good for announcing station events and other such things.
/// </summary>
[ByRefEvent]
public readonly record struct GameRuleAddedEvent(EntityUid RuleEntity, string RuleId);
/// <summary>
/// Raised when the rule actually begins.
/// Player-facing logic should begin here.
/// </summary>
[ByRefEvent]
public readonly record struct GameRuleStartedEvent(EntityUid RuleEntity, string RuleId);
/// <summary>
/// Raised when the rule ends.
/// Do cleanup and other such things here.
/// </summary>
[ByRefEvent]
public readonly record struct GameRuleEndedEvent(EntityUid RuleEntity, string RuleId);

View File

@@ -1,24 +0,0 @@
using System.Threading;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Gamerule that ends the round after a period of inactivity.
/// </summary>
[RegisterComponent, Access(typeof(InactivityTimeRestartRuleSystem))]
public sealed class InactivityRuleComponent : Component
{
/// <summary>
/// How long the round must be inactive to restart
/// </summary>
[DataField("inactivityMaxTime", required: true)]
public TimeSpan InactivityMaxTime = TimeSpan.FromMinutes(10);
/// <summary>
/// The delay between announcing round end and the lobby.
/// </summary>
[DataField("roundEndDelay", required: true)]
public TimeSpan RoundEndDelay = TimeSpan.FromSeconds(10);
public CancellationTokenSource TimerCancel = new();
}

View File

@@ -1,24 +0,0 @@
using System.Threading;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Configures the <see cref="InactivityTimeRestartRuleSystem"/> game rule.
/// </summary>
[RegisterComponent]
public sealed class MaxTimeRestartRuleComponent : Component
{
/// <summary>
/// The max amount of time the round can last
/// </summary>
[DataField("roundMaxTime", required: true)]
public TimeSpan RoundMaxTime = TimeSpan.FromMinutes(5);
/// <summary>
/// The amount of time between the round completing and the lobby appearing.
/// </summary>
[DataField("roundEndDelay", required: true)]
public TimeSpan RoundEndDelay = TimeSpan.FromSeconds(10);
public CancellationTokenSource TimerCancel = new();
}

View File

@@ -6,6 +6,7 @@ namespace Content.Server.GameTicking.Rules.Components;
/// TODO: Remove once systems can request spawns from the ghost role system directly.
/// </summary>
[RegisterComponent]
[Access(typeof(NukeopsRuleSystem))]
public sealed class NukeOperativeSpawnerComponent : Component
{
[DataField("name")]

View File

@@ -1,15 +0,0 @@
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(PiratesRuleSystem))]
public sealed class PiratesRuleComponent : Component
{
[ViewVariables]
public List<Mind.Mind> Pirates = new();
[ViewVariables]
public EntityUid PirateShip = EntityUid.Invalid;
[ViewVariables]
public HashSet<EntityUid> InitialItems = new();
[ViewVariables]
public double InitialShipValue;
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(SandboxRuleSystem))]
public sealed class SandboxRuleComponent : Component
{
}

View File

@@ -1,11 +0,0 @@
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(SecretRuleSystem))]
public sealed class SecretRuleComponent : Component
{
/// <summary>
/// The gamerules that get added by secret.
/// </summary>
[DataField("additionalGameRules")]
public HashSet<EntityUid> AdditionalGameRules = new();
}

View File

@@ -1,32 +0,0 @@
using Content.Server.Traitor;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(TraitorRuleSystem))]
public sealed class TraitorRuleComponent : Component
{
public readonly SoundSpecifier AddedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg");
public List<TraitorRole> Traitors = new();
[DataField("traitorPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string TraitorPrototypeId = "Traitor";
public int TotalTraitors => Traitors.Count;
public string[] Codewords = new string[3];
public enum SelectionState
{
WaitingForSpawn = 0,
ReadyToSelect = 1,
SelectionMade = 2,
}
public SelectionState SelectionStatus = SelectionState.WaitingForSpawn;
public TimeSpan AnnounceAt = TimeSpan.Zero;
public Dictionary<IPlayerSession, HumanoidCharacterProfile> StartCandidates = new();
}

View File

@@ -1,12 +0,0 @@
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(ZombieRuleSystem))]
public sealed class ZombieRuleComponent : Component
{
public Dictionary<string, string> InitialInfectedNames = new();
public string PatientZeroPrototypeID = "InitialInfected";
public string InitialZombieVirusPrototype = "PassiveZombieVirus";
public const string ZombifySelfActionPrototype = "TurnUndead";
}

View File

@@ -0,0 +1,13 @@
namespace Content.Server.GameTicking.Rules.Configurations;
/// <summary>
/// Configures a game rule, providing information like what maps to use or how long to run.
/// </summary>
[ImplicitDataDefinitionForInheritors]
public abstract class GameRuleConfiguration
{
/// <summary>
/// The game rule this configuration is intended for.
/// </summary>
public abstract string Id { get; }
}

View File

@@ -0,0 +1,14 @@
using JetBrains.Annotations;
namespace Content.Server.GameTicking.Rules.Configurations;
/// <summary>
/// A generic configuration, for game rules that don't have special config data.
/// </summary>
[UsedImplicitly]
public sealed class GenericGameRuleConfiguration : GameRuleConfiguration
{
[DataField("id", required: true)]
private string _id = default!;
public override string Id => _id;
}

View File

@@ -0,0 +1,17 @@
using JetBrains.Annotations;
namespace Content.Server.GameTicking.Rules.Configurations;
/// <summary>
/// Configures the <see cref="InactivityTimeRestartRuleSystem"/> game rule.
/// </summary>
[UsedImplicitly]
public sealed class InactivityGameRuleConfiguration : GameRuleConfiguration
{
public override string Id => "InactivityTimeRestart"; // The value for this in the system isn't static and can't be made static. RIP.
[DataField("inactivityMaxTime", required: true)]
public TimeSpan InactivityMaxTime { get; }
[DataField("roundEndDelay", required: true)]
public TimeSpan RoundEndDelay { get; }
}

View File

@@ -0,0 +1,17 @@
using JetBrains.Annotations;
namespace Content.Server.GameTicking.Rules.Configurations;
/// <summary>
/// Configures the <see cref="InactivityTimeRestartRuleSystem"/> game rule.
/// </summary>
[UsedImplicitly]
public sealed class MaxTimeRestartRuleConfiguration : GameRuleConfiguration
{
public override string Id => "MaxTimeRestart"; // The value for this in the system isn't static and can't be made static. RIP.
[DataField("roundMaxTime", required: true)]
public TimeSpan RoundMaxTime { get; }
[DataField("roundEndDelay", required: true)]
public TimeSpan RoundEndDelay { get; }
}

View File

@@ -1,22 +1,19 @@
using Content.Server.StationEvents.Events;
using Content.Server.GameTicking.Rules.Configurations;
using Content.Shared.Dataset;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules.Components;
namespace Content.Server.GameTicking.Rules.Configurations;
[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
public sealed class NukeopsRuleComponent : Component
public sealed class NukeopsRuleConfiguration : GameRuleConfiguration
{
/// <summary>
/// The minimum needed amount of players
/// </summary>
public override string Id => "Nukeops";
[DataField("minPlayers")]
public int MinPlayers = 15;
@@ -41,6 +38,15 @@ public sealed class NukeopsRuleComponent : Component
[DataField("spawnOutpost")]
public bool SpawnOutpost = true;
/// <summary>
/// Whether or not loneops can spawn. Set to false if a normal nukeops round is occurring.
/// </summary>
[DataField("canLoneOpsSpawn")]
public bool CanLoneOpsSpawn = true;
[DataField("randomHumanoidSettings", customTypeSerializer: typeof(PrototypeIdSerializer<RandomHumanoidSettingsPrototype>))]
public string RandomHumanoidSettingsPrototype = "NukeOp";
[DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
public string SpawnPointPrototype = "SpawnPointNukies";
@@ -76,86 +82,4 @@ public sealed class NukeopsRuleComponent : Component
[DataField("greetingSound", customTypeSerializer: typeof(SoundSpecifierTypeSerializer))]
public SoundSpecifier? GreetSound = new SoundPathSpecifier("/Audio/Misc/nukeops.ogg");
[DataField("winType")]
public WinType WinType = WinType.Neutral;
[DataField("winConditions")]
public List<WinCondition> WinConditions = new ();
public MapId? NukiePlanet;
// TODO: use components, don't just cache entity UIDs
// There have been (and probably still are) bugs where these refer to deleted entities from old rounds.
public EntityUid? NukieOutpost;
public EntityUid? NukieShuttle;
public EntityUid? TargetStation;
/// <summary>
/// Cached starting gear prototypes.
/// </summary>
[DataField("startingGearPrototypes")]
public readonly Dictionary<string, StartingGearPrototype> StartingGearPrototypes = new ();
/// <summary>
/// Cached operator name prototypes.
/// </summary>
[DataField("operativeNames")]
public readonly Dictionary<string, List<string>> OperativeNames = new();
/// <summary>
/// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
/// </summary>
[DataField("operativeMindPendingData")]
public readonly Dictionary<EntityUid, string> OperativeMindPendingData = new();
/// <summary>
/// Players who played as an operative at some point in the round.
/// Stores the session as well as the entity name
/// </summary>
/// todo: don't store sessions, dingus
[DataField("operativePlayers")]
public readonly Dictionary<string, IPlayerSession> OperativePlayers = new();
}
public enum WinType : byte
{
/// <summary>
/// Operative major win. This means they nuked the station.
/// </summary>
OpsMajor,
/// <summary>
/// Minor win. All nukies were alive at the end of the round.
/// Alternatively, some nukies were alive, but the disk was left behind.
/// </summary>
OpsMinor,
/// <summary>
/// Neutral win. The nuke exploded, but on the wrong station.
/// </summary>
Neutral,
/// <summary>
/// Crew minor win. The nuclear authentication disk escaped on the shuttle,
/// but some nukies were alive.
/// </summary>
CrewMinor,
/// <summary>
/// Crew major win. This means they either killed all nukies,
/// or the bomb exploded too far away from the station, or on the nukie moon.
/// </summary>
CrewMajor
}
public enum WinCondition : byte
{
NukeExplodedOnCorrectStation,
NukeExplodedOnNukieOutpost,
NukeExplodedOnIncorrectLocation,
NukeActiveInStation,
NukeActiveAtCentCom,
NukeDiskOnCentCom,
NukeDiskNotOnCentCom,
NukiesAbandoned,
AllNukiesDead,
SomeNukiesAlive,
AllNukiesAlive
}

View File

@@ -0,0 +1,46 @@
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.GameTicking.Rules.Configurations;
/// <summary>
/// Solar Flare event specific configuration
/// </summary>
public sealed class SolarFlareEventRuleConfiguration : StationEventRuleConfiguration
{
/// <summary>
/// In seconds, most early moment event can end
/// </summary>
[DataField("minEndAfter")]
public int MinEndAfter;
/// <summary>
/// In seconds, most late moment event can end
/// </summary>
[DataField("maxEndAfter")]
public int MaxEndAfter;
/// <summary>
/// If true, only headsets affected, but e.g. handheld radio will still work
/// </summary>
[DataField("onlyJamHeadsets")]
public bool OnlyJamHeadsets;
/// <summary>
/// Channels that will be disabled for a duration of event
/// </summary>
[DataField("affectedChannels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public readonly HashSet<string> AffectedChannels = new();
/// <summary>
/// Chance light bulb breaks per second during event
/// </summary>
[DataField("lightBreakChancePerSecond")]
public float LightBreakChancePerSecond;
/// <summary>
/// Chance door toggles per second during event
/// </summary>
[DataField("doorToggleChancePerSecond")]
public float DoorToggleChancePerSecond;
}

View File

@@ -0,0 +1,76 @@
using JetBrains.Annotations;
using Robust.Shared.Audio;
namespace Content.Server.GameTicking.Rules.Configurations;
/// <summary>
/// Defines a configuration for a given station event game rule, since all station events are just
/// game rules.
/// </summary>
[UsedImplicitly]
public class StationEventRuleConfiguration : GameRuleConfiguration
{
[DataField("id", required: true)]
private string _id = default!;
public override string Id => _id;
public const float WeightVeryLow = 0.0f;
public const float WeightLow = 5.0f;
public const float WeightNormal = 10.0f;
public const float WeightHigh = 15.0f;
public const float WeightVeryHigh = 20.0f;
[DataField("weight")]
public float Weight = WeightNormal;
[DataField("startAnnouncement")]
public string? StartAnnouncement;
[DataField("endAnnouncement")]
public string? EndAnnouncement;
[DataField("startAudio")]
public SoundSpecifier? StartAudio;
[DataField("endAudio")]
public SoundSpecifier? EndAudio;
/// <summary>
/// In minutes, when is the first round time this event can start
/// </summary>
[DataField("earliestStart")]
public int EarliestStart = 5;
/// <summary>
/// In minutes, the amount of time before the same event can occur again
/// </summary>
[DataField("reoccurrenceDelay")]
public int ReoccurrenceDelay = 30;
/// <summary>
/// When in the lifetime to start the event.
/// </summary>
[DataField("startAfter")]
public float StartAfter;
/// <summary>
/// When in the lifetime to end the event..
/// </summary>
[DataField("endAfter")]
public float EndAfter = float.MaxValue;
/// <summary>
/// How many players need to be present on station for the event to run
/// </summary>
/// <remarks>
/// To avoid running deadly events with low-pop
/// </remarks>
[DataField("minimumPlayers")]
public int MinimumPlayers;
/// <summary>
/// How many times this even can occur in a single round
/// </summary>
[DataField("maxOccurrences")]
public int? MaxOccurrences;
}

View File

@@ -1,5 +1,5 @@
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GameTicking.Rules.Configurations;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Mobs.Components;
@@ -11,42 +11,44 @@ using Robust.Shared.Enums;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Manages <see cref="DeathMatchRuleComponent"/>
/// Simple GameRule that will do a free-for-all death match.
/// Kill everybody else to win.
/// </summary>
public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponent>
public sealed class DeathMatchRuleSystem : GameRuleSystem
{
public override string Prototype => "DeathMatch";
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private const float RestartDelay = 10f;
private const float DeadCheckDelay = 5f;
private float? _deadCheckTimer = null;
private float? _restartTimer = null;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DamageChangedEvent>(OnHealthChanged);
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
protected override void Started(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
public override void Started()
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement"));
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
protected override void Ended(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
public override void Ended()
{
base.Ended(uid, component, gameRule, args);
component.DeadCheckTimer = null;
component.RestartTimer = null;
_deadCheckTimer = null;
_restartTimer = null;
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnHealthChanged(DamageChangedEvent _)
@@ -54,7 +56,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
RunDelayedCheck();
}
private void OnPlayerStatusChanged(object? ojb, SessionStatusEventArgs e)
private void OnPlayerStatusChanged(object? _, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Disconnected)
{
@@ -64,27 +66,24 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
private void RunDelayedCheck()
{
var query = EntityQueryEnumerator<DeathMatchRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var deathMatch, out var gameRule))
{
if (!GameTicker.IsGameRuleActive(uid, gameRule) || deathMatch.DeadCheckTimer != null)
continue;
if (!RuleAdded || _deadCheckTimer != null)
return;
deathMatch.DeadCheckTimer = deathMatch.DeadCheckDelay;
}
_deadCheckTimer = DeadCheckDelay;
}
protected override void ActiveTick(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, float frameTime)
public override void Update(float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (!RuleAdded)
return;
// If the restart timer is active, that means the round is ending soon, no need to check for winners.
// TODO: We probably want a sane, centralized round end thingie in GameTicker, RoundEndSystem is no good...
if (component.RestartTimer != null)
if (_restartTimer != null)
{
component.RestartTimer -= frameTime;
_restartTimer -= frameTime;
if (component.RestartTimer > 0f)
if (_restartTimer > 0f)
return;
GameTicker.EndRound();
@@ -92,20 +91,20 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
return;
}
if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin) || component.DeadCheckTimer == null)
if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin) || _deadCheckTimer == null)
return;
component.DeadCheckTimer -= frameTime;
_deadCheckTimer -= frameTime;
if (component.DeadCheckTimer > 0)
if (_deadCheckTimer > 0)
return;
component.DeadCheckTimer = null;
_deadCheckTimer = null;
IPlayerSession? winner = null;
foreach (var playerSession in _playerManager.ServerSessions)
{
if (playerSession.AttachedEntity is not { Valid: true } playerEntity
if (playerSession.AttachedEntity is not {Valid: true} playerEntity
|| !TryComp(playerEntity, out MobStateComponent? state))
continue;
@@ -121,10 +120,9 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
_chatManager.DispatchServerAnnouncement(winner == null
? Loc.GetString("rule-death-match-check-winner-stalemate")
: Loc.GetString("rule-death-match-check-winner", ("winner", winner)));
: Loc.GetString("rule-death-match-check-winner",("winner", winner)));
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",
("seconds", component.RestartDelay)));
component.RestartTimer = component.RestartDelay;
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", RestartDelay)));
_restartTimer = RestartDelay;
}
}

View File

@@ -1,8 +1,8 @@
using Content.Server.GameTicking.Rules.Configurations;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules;
/*
[Prototype("gameRule")]
public sealed class GameRulePrototype : IPrototype
{
@@ -12,4 +12,3 @@ public sealed class GameRulePrototype : IPrototype
[DataField("config", required: true)]
public GameRuleConfiguration Configuration { get; } = default!;
}
*/

View File

@@ -1,84 +1,94 @@
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GameTicking.Rules.Configurations;
using JetBrains.Annotations;
namespace Content.Server.GameTicking.Rules;
public abstract class GameRuleSystem<T> : EntitySystem where T : Component
[PublicAPI]
public abstract class GameRuleSystem : EntitySystem
{
[Dependency] protected GameTicker GameTicker = default!;
/// <summary>
/// Whether this GameRule is currently added or not.
/// Be sure to check this before doing anything rule-specific.
/// </summary>
public bool RuleAdded { get; protected set; }
/// <summary>
/// Whether this game rule has been started after being added.
/// You probably want to check this before doing any update loop stuff.
/// </summary>
public bool RuleStarted { get; protected set; }
/// <summary>
/// When the GameRule prototype with this ID is added, this system will be enabled.
/// When it gets removed, this system will be disabled.
/// </summary>
public new abstract string Prototype { get; }
/// <summary>
/// Holds the current configuration after the event has been added.
/// This should not be getting accessed before the event is enabled, as usual.
/// </summary>
public GameRuleConfiguration Configuration = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
SubscribeLocalEvent<GameRuleAddedEvent>(OnGameRuleAdded);
SubscribeLocalEvent<GameRuleStartedEvent>(OnGameRuleStarted);
SubscribeLocalEvent<GameRuleEndedEvent>(OnGameRuleEnded);
}
private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args)
private void OnGameRuleAdded(GameRuleAddedEvent ev)
{
if (!TryComp<GameRuleComponent>(uid, out var ruleData))
if (ev.Rule.Configuration.Id != Prototype)
return;
Added(uid, component, ruleData, args);
Configuration = ev.Rule.Configuration;
RuleAdded = true;
Added();
}
private void OnGameRuleStarted(EntityUid uid, T component, ref GameRuleStartedEvent args)
private void OnGameRuleStarted(GameRuleStartedEvent ev)
{
if (!TryComp<GameRuleComponent>(uid, out var ruleData))
if (ev.Rule.Configuration.Id != Prototype)
return;
Started(uid, component, ruleData, args);
RuleStarted = true;
Started();
}
private void OnGameRuleEnded(EntityUid uid, T component, ref GameRuleEndedEvent args)
private void OnGameRuleEnded(GameRuleEndedEvent ev)
{
if (!TryComp<GameRuleComponent>(uid, out var ruleData))
if (ev.Rule.Configuration.Id != Prototype)
return;
Ended(uid, component, ruleData, args);
RuleAdded = false;
RuleStarted = false;
Ended();
}
/// <summary>
/// Called when the gamerule is added
/// Called when the game rule has been added.
/// You should avoid using this in favor of started--they are not the same thing.
/// </summary>
protected virtual void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
}
/// <remarks>
/// This is virtual because it doesn't actually have to be used, and most of the time shouldn't be.
/// </remarks>
public virtual void Added() { }
/// <summary>
/// Called when the gamerule begins
/// Called when the game rule has been started.
/// </summary>
protected virtual void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
}
public abstract void Started();
/// <summary>
/// Called when the gamerule ends
/// Called when the game rule has ended.
/// </summary>
protected virtual void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args)
{
}
/// <summary>
/// Called on an active gamerule entity in the Update function
/// </summary>
protected virtual void ActiveTick(EntityUid uid, T component, GameRuleComponent gameRule, float frameTime)
{
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<T, GameRuleComponent>();
while (query.MoveNext(out var uid, out var comp1, out var comp2))
{
if (!GameTicker.IsGameRuleActive(uid, comp2))
continue;
ActiveTick(uid, comp1, comp2, frameTime);
}
}
public abstract void Ended();
}

View File

@@ -1,109 +1,98 @@
using System.Threading;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GameTicking.Rules.Configurations;
using Robust.Server.Player;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.GameTicking.Rules;
public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem<InactivityRuleComponent>
public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem
{
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override string Prototype => "InactivityTimeRestart";
private CancellationTokenSource _timerCancel = new();
public TimeSpan InactivityMaxTime { get; set; } = TimeSpan.FromMinutes(10);
public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10);
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GameRunLevelChangedEvent>(RunLevelChanged);
}
public override void Started()
{
if (Configuration is not InactivityGameRuleConfiguration inactivityConfig)
return;
InactivityMaxTime = inactivityConfig.InactivityMaxTime;
RoundEndDelay = inactivityConfig.RoundEndDelay;
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
}
public override void Shutdown()
public override void Ended()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= PlayerStatusChanged;
StopTimer();
}
protected override void Ended(EntityUid uid, InactivityRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
public void RestartTimer()
{
base.Ended(uid, component, gameRule, args);
StopTimer(uid, component);
_timerCancel.Cancel();
_timerCancel = new CancellationTokenSource();
Timer.Spawn(InactivityMaxTime, TimerFired, _timerCancel.Token);
}
public void RestartTimer(EntityUid uid, InactivityRuleComponent? component = null)
public void StopTimer()
{
if (!Resolve(uid, ref component))
return;
component.TimerCancel.Cancel();
component.TimerCancel = new CancellationTokenSource();
Timer.Spawn(component.InactivityMaxTime, () => TimerFired(uid, component), component.TimerCancel.Token);
_timerCancel.Cancel();
}
public void StopTimer(EntityUid uid, InactivityRuleComponent? component = null)
private void TimerFired()
{
if (!Resolve(uid, ref component))
return;
component.TimerCancel.Cancel();
}
private void TimerFired(EntityUid uid, InactivityRuleComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
GameTicker.EndRound(Loc.GetString("rule-time-has-run-out"));
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds",(int) component.RoundEndDelay.TotalSeconds)));
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds",(int) RoundEndDelay.TotalSeconds)));
Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound());
Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
}
private void RunLevelChanged(GameRunLevelChangedEvent args)
{
var query = EntityQueryEnumerator<InactivityRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var inactivity, out var gameRule))
{
if (!GameTicker.IsGameRuleActive(uid, gameRule))
return;
if (!RuleAdded)
return;
switch (args.New)
{
case GameRunLevel.InRound:
RestartTimer(uid, inactivity);
break;
case GameRunLevel.PreRoundLobby:
case GameRunLevel.PostRound:
StopTimer(uid, inactivity);
break;
}
switch (args.New)
{
case GameRunLevel.InRound:
RestartTimer();
break;
case GameRunLevel.PreRoundLobby:
case GameRunLevel.PostRound:
StopTimer();
break;
}
}
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
var query = EntityQueryEnumerator<InactivityRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var inactivity, out var gameRule))
if (GameTicker.RunLevel != GameRunLevel.InRound)
{
if (!GameTicker.IsGameRuleActive(uid, gameRule))
return;
return;
}
if (GameTicker.RunLevel != GameRunLevel.InRound)
{
return;
}
if (_playerManager.PlayerCount == 0)
{
RestartTimer(uid, inactivity);
}
else
{
StopTimer(uid, inactivity);
}
if (_playerManager.PlayerCount == 0)
{
RestartTimer();
}
else
{
StopTimer();
}
}
}

View File

@@ -1,14 +1,21 @@
using System.Threading;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GameTicking.Rules.Configurations;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.GameTicking.Rules;
public sealed class MaxTimeRestartRuleSystem : GameRuleSystem<MaxTimeRestartRuleComponent>
public sealed class MaxTimeRestartRuleSystem : GameRuleSystem
{
[Dependency] private readonly IChatManager _chatManager = default!;
public override string Prototype => "MaxTimeRestart";
private CancellationTokenSource _timerCancel = new();
public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromMinutes(5);
public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10);
public override void Initialize()
{
base.Initialize();
@@ -16,60 +23,58 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem<MaxTimeRestartRule
SubscribeLocalEvent<GameRunLevelChangedEvent>(RunLevelChanged);
}
protected override void Started(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
public override void Started()
{
base.Started(uid, component, gameRule, args);
if (Configuration is not MaxTimeRestartRuleConfiguration maxTimeRestartConfig)
return;
RoundMaxTime = maxTimeRestartConfig.RoundMaxTime;
RoundEndDelay = maxTimeRestartConfig.RoundEndDelay;
if(GameTicker.RunLevel == GameRunLevel.InRound)
RestartTimer(component);
RestartTimer();
}
protected override void Ended(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
public override void Ended()
{
base.Ended(uid, component, gameRule, args);
StopTimer(component);
StopTimer();
}
public void RestartTimer(MaxTimeRestartRuleComponent component)
public void RestartTimer()
{
component.TimerCancel.Cancel();
component.TimerCancel = new CancellationTokenSource();
Timer.Spawn(component.RoundMaxTime, () => TimerFired(component), component.TimerCancel.Token);
_timerCancel.Cancel();
_timerCancel = new CancellationTokenSource();
Timer.Spawn(RoundMaxTime, TimerFired, _timerCancel.Token);
}
public void StopTimer(MaxTimeRestartRuleComponent component)
public void StopTimer()
{
component.TimerCancel.Cancel();
_timerCancel.Cancel();
}
private void TimerFired(MaxTimeRestartRuleComponent component)
private void TimerFired()
{
GameTicker.EndRound(Loc.GetString("rule-time-has-run-out"));
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) component.RoundEndDelay.TotalSeconds)));
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) RoundEndDelay.TotalSeconds)));
Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound());
Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
}
private void RunLevelChanged(GameRunLevelChangedEvent args)
{
var query = EntityQueryEnumerator<MaxTimeRestartRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var timer, out var gameRule))
{
if (!GameTicker.IsGameRuleActive(uid, gameRule))
return;
if (!RuleAdded)
return;
switch (args.New)
{
case GameRunLevel.InRound:
RestartTimer(timer);
break;
case GameRunLevel.PreRoundLobby:
case GameRunLevel.PostRound:
StopTimer(timer);
break;
}
switch (args.New)
{
case GameRunLevel.InRound:
RestartTimer();
break;
case GameRunLevel.PreRoundLobby:
case GameRunLevel.PostRound:
StopTimer();
break;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ using System.Linq;
using Content.Server.Administration.Commands;
using Content.Server.Cargo.Systems;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Preferences.Managers;
using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
@@ -26,7 +25,7 @@ namespace Content.Server.GameTicking.Rules;
/// <summary>
/// This handles the Pirates minor antag, which is designed to coincide with other modes on occasion.
/// </summary>
public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
public sealed class PiratesRuleSystem : GameRuleSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -40,6 +39,17 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
[Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly NamingSystem _namingSystem = default!;
[ViewVariables]
private List<Mind.Mind> _pirates = new();
[ViewVariables]
private EntityUid _pirateShip = EntityUid.Invalid;
[ViewVariables]
private HashSet<EntityUid> _initialItems = new();
[ViewVariables]
private double _initialShipValue;
public override string Prototype => "Pirates";
/// <inheritdoc/>
public override void Initialize()
{
@@ -47,186 +57,178 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawningEvent);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextEvent);
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
}
private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev)
{
var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var pirates, out var gameRule))
if (!RuleAdded)
return;
if (Deleted(_pirateShip))
{
if (Deleted(pirates.PirateShip))
// Major loss, the ship somehow got annihilated.
ev.AddLine(Loc.GetString("pirates-no-ship"));
}
else
{
List<(double, EntityUid)> mostValuableThefts = new();
var finalValue = _pricingSystem.AppraiseGrid(_pirateShip, uid =>
{
// Major loss, the ship somehow got annihilated.
ev.AddLine(Loc.GetString("pirates-no-ship"));
}
else
foreach (var mind in _pirates)
{
if (mind.CurrentEntity == uid)
return false; // Don't appraise the pirates twice, we count them in separately.
}
return true;
}, (uid, price) =>
{
if (_initialItems.Contains(uid))
return;
List<(double, EntityUid)> mostValuableThefts = new();
mostValuableThefts.Add((price, uid));
mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1));
if (mostValuableThefts.Count > 5)
mostValuableThefts.Pop();
});
var comp1 = pirates;
var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
{
foreach (var mind in comp1.Pirates)
{
if (mind.CurrentEntity == uid)
return false; // Don't appraise the pirates twice, we count them in separately.
}
return true;
}, (uid, price) =>
{
if (comp1.InitialItems.Contains(uid))
return;
mostValuableThefts.Add((price, uid));
mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1));
if (mostValuableThefts.Count > 5)
mostValuableThefts.Pop();
});
foreach (var mind in pirates.Pirates)
{
if (mind.CurrentEntity is not null)
finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value);
}
var score = finalValue - pirates.InitialShipValue;
ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}")));
ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}")));
ev.AddLine("");
ev.AddLine(Loc.GetString("pirates-most-valuable"));
foreach (var (price, obj) in mostValuableThefts)
{
ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}")));
}
if (mostValuableThefts.Count == 0)
ev.AddLine(Loc.GetString("pirates-stole-nothing"));
foreach (var mind in _pirates)
{
if (mind.CurrentEntity is not null)
finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value);
}
var score = finalValue - _initialShipValue;
ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}")));
ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}")));
ev.AddLine("");
ev.AddLine(Loc.GetString("pirates-list-start"));
foreach (var pirate in pirates.Pirates)
ev.AddLine(Loc.GetString("pirates-most-valuable"));
foreach (var (price, obj) in mostValuableThefts)
{
ev.AddLine($"- {pirate.CharacterName} ({pirate.Session?.Name})");
ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}")));
}
if (mostValuableThefts.Count == 0)
ev.AddLine(Loc.GetString("pirates-stole-nothing"));
}
ev.AddLine("");
ev.AddLine(Loc.GetString("pirates-list-start"));
foreach (var pirates in _pirates)
{
ev.AddLine($"- {pirates.CharacterName} ({pirates.Session?.Name})");
}
}
public override void Started() { }
public override void Ended() { }
private void OnPlayerSpawningEvent(RulePlayerSpawningEvent ev)
{
var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var pirates, out var gameRule))
// Forgive me for copy-pasting nukies.
if (!RuleAdded)
{
// Forgive me for copy-pasting nukies.
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
return;
pirates.Pirates.Clear();
pirates.InitialItems.Clear();
// Between 1 and <max pirate count>: needs at least n players per op.
var numOps = Math.Max(1,
(int) Math.Min(
Math.Floor((double) ev.PlayerPool.Count / _cfg.GetCVar(CCVars.PiratesPlayersPerOp)),
_cfg.GetCVar(CCVars.PiratesMaxOps)));
var ops = new IPlayerSession[numOps];
for (var i = 0; i < numOps; i++)
{
ops[i] = _random.PickAndTake(ev.PlayerPool);
}
var map = "/Maps/Shuttles/pirate.yml";
var xformQuery = GetEntityQuery<TransformComponent>();
var aabbs = _stationSystem.Stations.SelectMany(x =>
Comp<StationDataComponent>(x).Grids.Select(x =>
xformQuery.GetComponent(x).WorldMatrix.TransformBox(_mapManager.GetGridComp(x).LocalAABB)))
.ToArray();
var aabb = aabbs[0];
for (var i = 1; i < aabbs.Length; i++)
{
aabb.Union(aabbs[i]);
}
var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
{
Offset = aabb.Center + MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f
});
if (!gridId.HasValue)
{
Logger.ErrorS("pirates", $"Gridid was null when loading \"{map}\", aborting.");
foreach (var session in ops)
{
ev.PlayerPool.Add(session);
}
return;
}
pirates.PirateShip = gridId.Value;
// TODO: Loot table or something
var pirateGear = _prototypeManager.Index<StartingGearPrototype>("PirateGear"); // YARRR
var spawns = new List<EntityCoordinates>();
// Forgive me for hardcoding prototypes
foreach (var (_, meta, xform) in
EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
{
if (meta.EntityPrototype?.ID != "SpawnPointPirates" || xform.ParentUid != pirates.PirateShip)
continue;
spawns.Add(xform.Coordinates);
}
if (spawns.Count == 0)
{
spawns.Add(Transform(pirates.PirateShip).Coordinates);
Logger.WarningS("pirates", $"Fell back to default spawn for pirates!");
}
for (var i = 0; i < ops.Length; i++)
{
var sex = _random.Prob(0.5f) ? Sex.Male : Sex.Female;
var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
var name = _namingSystem.GetName("Human", gender);
var session = ops[i];
var newMind = new Mind.Mind(session.UserId)
{
CharacterName = name
};
newMind.ChangeOwningPlayer(session.UserId);
var mob = Spawn("MobHuman", _random.Pick(spawns));
MetaData(mob).EntityName = name;
newMind.TransferTo(mob);
var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
_stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
pirates.Pirates.Add(newMind);
GameTicker.PlayerJoinGame(session);
}
pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
{
pirates.InitialItems.Add(uid);
return true;
}); // Include the players in the appraisal.
return;
}
_pirates.Clear();
_initialItems.Clear();
// Between 1 and <max pirate count>: needs at least n players per op.
var numOps = Math.Max(1,
(int)Math.Min(
Math.Floor((double)ev.PlayerPool.Count / _cfg.GetCVar(CCVars.PiratesPlayersPerOp)), _cfg.GetCVar(CCVars.PiratesMaxOps)));
var ops = new IPlayerSession[numOps];
for (var i = 0; i < numOps; i++)
{
ops[i] = _random.PickAndTake(ev.PlayerPool);
}
var map = "/Maps/Shuttles/pirate.yml";
var xformQuery = GetEntityQuery<TransformComponent>();
var aabbs = _stationSystem.Stations.SelectMany(x =>
Comp<StationDataComponent>(x).Grids.Select(x => xformQuery.GetComponent(x).WorldMatrix.TransformBox(_mapManager.GetGridComp(x).LocalAABB))).ToArray();
var aabb = aabbs[0];
for (var i = 1; i < aabbs.Length; i++)
{
aabb.Union(aabbs[i]);
}
var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
{
Offset = aabb.Center + MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f
});
if (!gridId.HasValue)
{
Logger.ErrorS("pirates", $"Gridid was null when loading \"{map}\", aborting.");
foreach (var session in ops)
{
ev.PlayerPool.Add(session);
}
return;
}
_pirateShip = gridId.Value;
// TODO: Loot table or something
var pirateGear = _prototypeManager.Index<StartingGearPrototype>("PirateGear"); // YARRR
var spawns = new List<EntityCoordinates>();
// Forgive me for hardcoding prototypes
foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
{
if (meta.EntityPrototype?.ID != "SpawnPointPirates" || xform.ParentUid != _pirateShip) continue;
spawns.Add(xform.Coordinates);
}
if (spawns.Count == 0)
{
spawns.Add(Transform(_pirateShip).Coordinates);
Logger.WarningS("pirates", $"Fell back to default spawn for pirates!");
}
for (var i = 0; i < ops.Length; i++)
{
var sex = _random.Prob(0.5f) ? Sex.Male : Sex.Female;
var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
var name = _namingSystem.GetName("Human", gender);
var session = ops[i];
var newMind = new Mind.Mind(session.UserId)
{
CharacterName = name
};
newMind.ChangeOwningPlayer(session.UserId);
var mob = Spawn("MobHuman", _random.Pick(spawns));
MetaData(mob).EntityName = name;
newMind.TransferTo(mob);
var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
_stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
_pirates.Add(newMind);
GameTicker.PlayerJoinGame(session);
}
_initialShipValue = _pricingSystem.AppraiseGrid(_pirateShip, uid =>
{
_initialItems.Add(uid);
return true;
}); // Include the players in the appraisal.
}
//Forcing one player to be a pirate.
@@ -239,26 +241,21 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var pirates, out var gameRule))
if (!RuleAdded)
return;
var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
if (!GameTicker.IsGameRuleActive(uid, gameRule))
return;
_chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
return;
}
var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
return;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
ev.Cancel();
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
ev.Cancel();
}
}
}

View File

@@ -1,21 +1,21 @@
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GameTicking.Rules.Configurations;
using Content.Server.Sandbox;
namespace Content.Server.GameTicking.Rules;
public sealed class SandboxRuleSystem : GameRuleSystem<SandboxRuleComponent>
public sealed class SandboxRuleSystem : GameRuleSystem
{
[Dependency] private readonly SandboxSystem _sandbox = default!;
protected override void Started(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
public override string Prototype => "Sandbox";
public override void Started()
{
base.Started(uid, component, gameRule, args);
_sandbox.IsSandboxEnabled = true;
}
protected override void Ended(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
public override void Ended()
{
base.Ended(uid, component, gameRule, args);
_sandbox.IsSandboxEnabled = false;
}
}

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.GameTicking.Presets;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GameTicking.Rules.Configurations;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Robust.Shared.Prototypes;
@@ -7,28 +8,25 @@ using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
public sealed class SecretRuleSystem : GameRuleSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly GameTicker _ticker = default!;
protected override void Started(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
public override string Prototype => "Secret";
public override void Started()
{
base.Started(uid, component, gameRule, args);
PickRule(component);
PickRule();
}
protected override void Ended(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
public override void Ended()
{
base.Ended(uid, component, gameRule, args);
foreach (var rule in component.AdditionalGameRules)
{
GameTicker.EndGameRule(rule);
}
// Preset should already handle it.
}
private void PickRule(SecretRuleComponent component)
private void PickRule()
{
// TODO: This doesn't consider what can't start due to minimum player count, but currently there's no way to know anyway.
// as they use cvars.
@@ -37,8 +35,7 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
foreach (var rule in _prototypeManager.Index<GamePresetPrototype>(preset).Rules)
{
GameTicker.StartGameRule(rule, out var ruleEnt);
component.AdditionalGameRules.Add(ruleEnt);
_ticker.StartGameRule(_prototypeManager.Index<GameRulePrototype>(rule));
}
}
}

View File

@@ -0,0 +1,456 @@
using System.Linq;
using System.Threading;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Configurations;
using Content.Server.Players;
using Content.Server.Roles;
using Content.Server.Station.Components;
using Content.Server.Suspicion;
using Content.Server.Suspicion.Roles;
using Content.Server.Traitor.Uplink;
using Content.Shared.CCVar;
using Content.Shared.Doors.Systems;
using Content.Shared.EntityList;
using Content.Shared.GameTicking;
using Content.Shared.Maps;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles;
using Content.Shared.Suspicion;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Simple GameRule that will do a TTT-like gamemode with traitors.
/// </summary>
public sealed class SuspicionRuleSystem : GameRuleSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
public override string Prototype => "Suspicion";
private static readonly TimeSpan DeadCheckDelay = TimeSpan.FromSeconds(1);
private readonly HashSet<SuspicionRoleComponent> _traitors = new();
public IReadOnlyCollection<SuspicionRoleComponent> Traitors => _traitors;
[DataField("addedSound")] private SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg");
private CancellationTokenSource _checkTimerCancel = new();
private TimeSpan? _endTime;
public TimeSpan? EndTime
{
get => _endTime;
set
{
_endTime = value;
SendUpdateToAll();
}
}
public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromSeconds(CCVars.SuspicionMaxTimeSeconds.DefaultValue);
public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10);
private const string TraitorID = "SuspicionTraitor";
private const string InnocentID = "SuspicionInnocent";
private const string SuspicionLootTable = "SuspicionRule";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersAssigned);
SubscribeLocalEvent<RoundStartAttemptEvent>(OnRoundStartAttempt);
SubscribeLocalEvent<RefreshLateJoinAllowedEvent>(OnLateJoinRefresh);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<SuspicionRoleComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<SuspicionRoleComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<SuspicionRoleComponent, RoleAddedEvent>(OnRoleAdded);
SubscribeLocalEvent<SuspicionRoleComponent, RoleRemovedEvent>(OnRoleRemoved);
}
private void OnRoundStartAttempt(RoundStartAttemptEvent ev)
{
if (!RuleAdded)
return;
var minPlayers = _cfg.GetCVar(CCVars.SuspicionMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.DispatchServerAnnouncement($"Not enough players readied up for the game! There were {ev.Players.Length} players readied up out of {minPlayers} needed.");
ev.Cancel();
return;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement("No players readied up! Can't start Suspicion.");
ev.Cancel();
}
}
private void OnPlayersAssigned(RulePlayerJobsAssignedEvent ev)
{
if (!RuleAdded)
return;
var minTraitors = _cfg.GetCVar(CCVars.SuspicionMinTraitors);
var playersPerTraitor = _cfg.GetCVar(CCVars.SuspicionPlayersPerTraitor);
var traitorStartingBalance = _cfg.GetCVar(CCVars.SuspicionStartingBalance);
var list = new List<IPlayerSession>(ev.Players);
var prefList = new List<IPlayerSession>();
foreach (var player in list)
{
if (!ev.Profiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached)
{
continue;
}
prefList.Add(player);
attached.EnsureComponent<SuspicionRoleComponent>();
}
// Max is players-1 so there's always at least one innocent.
var numTraitors = MathHelper.Clamp(ev.Players.Length / playersPerTraitor,
minTraitors, ev.Players.Length-1);
var traitors = new List<SuspicionTraitorRole>();
for (var i = 0; i < numTraitors; i++)
{
IPlayerSession traitor;
if(prefList.Count == 0)
{
if (list.Count == 0)
{
Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection.");
break;
}
traitor = _random.PickAndTake(list);
Logger.InfoS("preset", "Insufficient preferred traitors, picking at random.");
}
else
{
traitor = _random.PickAndTake(prefList);
list.Remove(traitor);
Logger.InfoS("preset", "Selected a preferred traitor.");
}
var mind = traitor.Data.ContentData()?.Mind;
var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorID);
DebugTools.AssertNotNull(mind?.OwnedEntity);
var traitorRole = new SuspicionTraitorRole(mind!, antagPrototype);
mind!.AddRole(traitorRole);
traitors.Add(traitorRole);
// try to place uplink
_uplink.AddUplink(mind.OwnedEntity!.Value, traitorStartingBalance);
}
foreach (var player in list)
{
var mind = player.Data.ContentData()?.Mind;
var antagPrototype = _prototypeManager.Index<AntagPrototype>(InnocentID);
DebugTools.AssertNotNull(mind);
mind!.AddRole(new SuspicionInnocentRole(mind, antagPrototype));
}
foreach (var traitor in traitors)
{
traitor.GreetSuspicion(traitors, _chatManager);
}
}
public override void Started()
{
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
RoundMaxTime = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.SuspicionMaxTimeSeconds));
EndTime = _timing.CurTime + RoundMaxTime;
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-added-announcement"));
var filter = Filter.Empty()
.AddWhere(session => ((IPlayerSession) session).ContentData()?.Mind?.HasRole<SuspicionTraitorRole>() ?? false);
SoundSystem.Play(_addedSound.GetSound(), filter, AudioParams.Default);
_doorSystem.AccessType = SharedDoorSystem.AccessTypes.AllowAllNoExternal;
var susLoot = _prototypeManager.Index<EntityLootTablePrototype>(SuspicionLootTable);
foreach (var (_, mapGrid) in EntityManager.EntityQuery<StationMemberComponent, MapGridComponent>(true))
{
// I'm so sorry.
var tiles = mapGrid.GetAllTiles().ToArray();
Logger.Info($"TILES: {tiles.Length}");
var spawn = susLoot.GetSpawns();
var count = spawn.Count;
// Try to scale spawned amount by station size...
if (tiles.Length < 1000)
{
count = Math.Min(count, tiles.Length / 10);
// Shuffle so we pick items at random.
_random.Shuffle(spawn);
}
for (var i = 0; i < count; i++)
{
var item = spawn[i];
// Maximum number of attempts for trying to find a suitable empty tile.
// We do this because we don't want to hang the server when a devious map has literally no free tiles.
const int maxTries = 100;
for (var j = 0; j < maxTries; j++)
{
var tile = _random.Pick(tiles);
// Let's not spawn things on top of walls.
if (tile.IsBlockedTurf(false, _lookupSystem) || tile.IsSpace(_tileDefMan))
continue;
var uid = Spawn(item, tile.GridPosition(_mapManager));
// Keep track of all suspicion-spawned weapons so we can clean them up once the rule ends.
EnsureComp<SuspicionItemComponent>(uid);
break;
}
}
}
_checkTimerCancel = new CancellationTokenSource();
Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token);
}
public override void Ended()
{
_doorSystem.AccessType = SharedDoorSystem.AccessTypes.Id;
EndTime = null;
_traitors.Clear();
_playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged;
// Clean up all items we spawned before...
foreach (var item in EntityManager.EntityQuery<SuspicionItemComponent>(true))
{
Del(item.Owner);
}
_checkTimerCancel.Cancel();
}
private void CheckWinConditions()
{
if (!RuleAdded || !_cfg.GetCVar(CCVars.GameLobbyEnableWin))
return;
var traitorsAlive = 0;
var innocentsAlive = 0;
foreach (var playerSession in _playerManager.ServerSessions)
{
if (playerSession.AttachedEntity is not {Valid: true} playerEntity
|| !TryComp(playerEntity, out MobStateComponent? mobState)
|| !HasComp<SuspicionRoleComponent>(playerEntity))
{
continue;
}
if (!_mobStateSystem.IsAlive(playerEntity, mobState))
{
continue;
}
var mind = playerSession.ContentData()?.Mind;
if (mind != null && mind.HasRole<SuspicionTraitorRole>())
traitorsAlive++;
else
innocentsAlive++;
}
if (innocentsAlive + traitorsAlive == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-stalemate"));
EndRound(Victory.Stalemate);
}
else if (traitorsAlive == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-station-win"));
EndRound(Victory.Innocents);
}
else if (innocentsAlive == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-traitor-win"));
EndRound(Victory.Traitors);
}
else if (_timing.CurTime > _endTime)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-traitor-time-has-run-out"));
EndRound(Victory.Innocents);
}
}
private enum Victory
{
Stalemate,
Innocents,
Traitors
}
private void EndRound(Victory victory)
{
string text;
switch (victory)
{
case Victory.Innocents:
text = Loc.GetString("rule-suspicion-end-round-innocents-victory");
break;
case Victory.Traitors:
text = Loc.GetString("rule-suspicion-end-round-traitors-victory");
break;
default:
text = Loc.GetString("rule-suspicion-end-round-nobody-victory");
break;
}
GameTicker.EndRound(text);
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", (int) RoundEndDelay.TotalSeconds)));
_checkTimerCancel.Cancel();
Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
}
private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.InGame)
{
SendUpdateTimerMessage(e.Session);
}
}
private void SendUpdateToAll()
{
foreach (var player in _playerManager.ServerSessions.Where(p => p.Status == SessionStatus.InGame))
{
SendUpdateTimerMessage(player);
}
}
private void SendUpdateTimerMessage(IPlayerSession player)
{
var msg = new SuspicionMessages.SetSuspicionEndTimerMessage
{
EndTime = EndTime
};
EntityManager.EntityNetManager?.SendSystemNetworkMessage(msg, player.ConnectedClient);
}
public void AddTraitor(SuspicionRoleComponent role)
{
if (!_traitors.Add(role))
{
return;
}
foreach (var traitor in _traitors)
{
traitor.AddAlly(role);
}
role.SetAllies(_traitors);
}
public void RemoveTraitor(SuspicionRoleComponent role)
{
if (!_traitors.Remove(role))
{
return;
}
foreach (var traitor in _traitors)
{
traitor.RemoveAlly(role);
}
role.ClearAllies();
}
private void Reset(RoundRestartCleanupEvent ev)
{
EndTime = null;
_traitors.Clear();
}
private void OnPlayerDetached(EntityUid uid, SuspicionRoleComponent component, PlayerDetachedEvent args)
{
component.SyncRoles();
}
private void OnPlayerAttached(EntityUid uid, SuspicionRoleComponent component, PlayerAttachedEvent args)
{
component.SyncRoles();
}
private void OnRoleAdded(EntityUid uid, SuspicionRoleComponent component, RoleAddedEvent args)
{
if (args.Role is not SuspicionRole role) return;
component.Role = role;
}
private void OnRoleRemoved(EntityUid uid, SuspicionRoleComponent component, RoleRemovedEvent args)
{
if (args.Role is not SuspicionRole) return;
component.Role = null;
}
private void OnLateJoinRefresh(RefreshLateJoinAllowedEvent ev)
{
if (!RuleAdded)
return;
ev.Disallow();
}
}

View File

@@ -0,0 +1,276 @@
using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.PDA;
using Content.Server.Players;
using Content.Server.Spawners.Components;
using Content.Server.Store.Components;
using Content.Server.Traitor;
using Content.Server.Traitor.Uplink;
using Content.Server.TraitorDeathMatch.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.PDA;
using Content.Shared.Roles;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly MaxTimeRestartRuleSystem _restarter = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
public override string Prototype => "TraitorDeathMatch";
public string PDAPrototypeName => "CaptainPDA";
public string BeltPrototypeName => "ClothingBeltJanitorFilled";
public string BackpackPrototypeName => "ClothingBackpackFilled";
private bool _safeToEndRound = false;
private readonly Dictionary<EntityUid, string> _allOriginalNames = new();
private const string TraitorPrototypeID = "Traitor";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawned);
SubscribeLocalEvent<GhostAttemptHandleEvent>(OnGhostAttempt);
}
private void OnPlayerSpawned(PlayerSpawnCompleteEvent ev)
{
if (!RuleAdded)
return;
var session = ev.Player;
var startingBalance = _cfg.GetCVar(CCVars.TraitorDeathMatchStartingBalance);
// Yup, they're a traitor
var mind = session.Data.ContentData()?.Mind;
if (mind == null)
{
Logger.ErrorS("preset", "Failed getting mind for TDM player.");
return;
}
var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorPrototypeID);
var traitorRole = new TraitorRole(mind, antagPrototype);
mind.AddRole(traitorRole);
// Delete anything that may contain "dangerous" role-specific items.
// (This includes the PDA, as everybody gets the captain PDA in this mode for true-all-access reasons.)
if (mind.OwnedEntity is {Valid: true} owned)
{
var victimSlots = new[] {"id", "belt", "back"};
foreach (var slot in victimSlots)
{
if(_inventory.TryUnequip(owned, slot, out var entityUid, true, true))
Del(entityUid.Value);
}
// Replace their items:
var ownedCoords = Transform(owned).Coordinates;
// pda
var newPDA = Spawn(PDAPrototypeName, ownedCoords);
_inventory.TryEquip(owned, newPDA, "id", true);
// belt
var newTmp = Spawn(BeltPrototypeName, ownedCoords);
_inventory.TryEquip(owned, newTmp, "belt", true);
// backpack
newTmp = Spawn(BackpackPrototypeName, ownedCoords);
_inventory.TryEquip(owned, newTmp, "back", true);
if (!_uplink.AddUplink(owned, startingBalance))
return;
_allOriginalNames[owned] = Name(owned);
// The PDA needs to be marked with the correct owner.
var pda = Comp<PDAComponent>(newPDA);
EntityManager.EntitySysManager.GetEntitySystem<PDASystem>().SetOwner(pda, Name(owned));
EntityManager.AddComponent<TraitorDeathMatchReliableOwnerTagComponent>(newPDA).UserId = mind.UserId;
}
// Finally, it would be preferable if they spawned as far away from other players as reasonably possible.
if (mind.OwnedEntity != null && FindAnyIsolatedSpawnLocation(mind, out var bestTarget))
{
Transform(mind.OwnedEntity.Value).Coordinates = bestTarget;
}
else
{
// The station is too drained of air to safely continue.
if (_safeToEndRound)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-death-match-station-is-too-unsafe-announcement"));
_restarter.RoundMaxTime = TimeSpan.FromMinutes(1);
_restarter.RestartTimer();
_safeToEndRound = false;
}
}
}
private void OnGhostAttempt(GhostAttemptHandleEvent ev)
{
if (!RuleAdded || ev.Handled)
return;
ev.Handled = true;
var mind = ev.Mind;
if (mind.OwnedEntity is {Valid: true} entity && TryComp(entity, out MobStateComponent? mobState))
{
if (_mobStateSystem.IsCritical(entity, mobState))
{
// TODO BODY SYSTEM KILL
var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), 100);
Get<DamageableSystem>().TryChangeDamage(entity, damage, true);
}
else if (!_mobStateSystem.IsDead(entity,mobState))
{
if (HasComp<HandsComponent>(entity))
{
ev.Result = false;
return;
}
}
}
var session = mind.Session;
if (session == null)
{
ev.Result = false;
return;
}
GameTicker.Respawn(session);
ev.Result = true;
}
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
if (!RuleAdded)
return;
var lines = new List<string>();
lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line"));
foreach (var uplink in EntityManager.EntityQuery<StoreComponent>(true))
{
var owner = uplink.AccountOwner;
if (owner != null && _allOriginalNames.ContainsKey(owner.Value))
{
var tcbalance = _uplink.GetTCBalance(uplink);
lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry",
("originalName", _allOriginalNames[owner.Value]),
("tcBalance", tcbalance)));
}
}
ev.AddLine(string.Join('\n', lines));
}
public override void Started()
{
_restarter.RoundMaxTime = TimeSpan.FromMinutes(30);
_restarter.RestartTimer();
_safeToEndRound = true;
}
public override void Ended()
{
}
// It would be nice if this function were moved to some generic helpers class.
private bool FindAnyIsolatedSpawnLocation(Mind.Mind ignoreMe, out EntityCoordinates bestTarget)
{
// Collate people to avoid...
var existingPlayerPoints = new List<EntityCoordinates>();
foreach (var player in _playerManager.ServerSessions)
{
var avoidMeMind = player.Data.ContentData()?.Mind;
if ((avoidMeMind == null) || (avoidMeMind == ignoreMe))
continue;
var avoidMeEntity = avoidMeMind.OwnedEntity;
if (avoidMeEntity == null)
continue;
if (TryComp(avoidMeEntity.Value, out MobStateComponent? mobState))
{
// Does have mob state component; if critical or dead, they don't really matter for spawn checks
if (_mobStateSystem.IsCritical(avoidMeEntity.Value, mobState) || _mobStateSystem.IsDead(avoidMeEntity.Value, mobState))
continue;
}
else
{
// Doesn't have mob state component. Assume something interesting is going on and don't count this as someone to avoid.
continue;
}
existingPlayerPoints.Add(Transform(avoidMeEntity.Value).Coordinates);
}
// Iterate over each possible spawn point, comparing to the existing player points.
// On failure, the returned target is the location that we're already at.
var bestTargetDistanceFromNearest = -1.0f;
// Need the random shuffle or it stuffs the first person into Atmospherics pretty reliably
var ents = EntityManager.EntityQuery<SpawnPointComponent>().Select(x => x.Owner).ToList();
_robustRandom.Shuffle(ents);
var foundATarget = false;
bestTarget = EntityCoordinates.Invalid;
foreach (var entity in ents)
{
var transform = Transform(entity);
if (transform.GridUid == null || transform.MapUid == null)
continue;
var position = _transformSystem.GetGridOrMapTilePosition(entity, transform);
if (!_atmosphereSystem.IsTileMixtureProbablySafe(transform.GridUid.Value, transform.MapUid.Value, position))
continue;
var distanceFromNearest = float.PositiveInfinity;
foreach (var existing in existingPlayerPoints)
{
if (Transform(entity).Coordinates.TryDistance(EntityManager, existing, out var dist))
distanceFromNearest = Math.Min(distanceFromNearest, dist);
}
if (bestTargetDistanceFromNearest < distanceFromNearest)
{
bestTarget = Transform(entity).Coordinates;
bestTargetDistanceFromNearest = distanceFromNearest;
foundATarget = true;
}
}
return foundATarget;
}
}

View File

@@ -1,6 +1,5 @@
using System.Linq;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.NPC.Systems;
using Content.Server.Objectives.Interfaces;
using Content.Server.PDA.Ringer;
@@ -25,7 +24,7 @@ using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules;
public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
public sealed class TraitorRuleSystem : GameRuleSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -33,6 +32,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
[Dependency] private readonly IObjectivesManager _objectivesManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly FactionSystem _faction = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
@@ -40,8 +40,30 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
private ISawmill _sawmill = default!;
private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
public override string Prototype => "Traitor";
private readonly SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg");
public List<TraitorRole> Traitors = new();
private const string TraitorPrototypeID = "Traitor";
private const string TraitorUplinkPresetId = "StorePresetUplink";
public int TotalTraitors => Traitors.Count;
public string[] Codewords = new string[3];
private int _playersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
private int _maxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
public enum SelectionState
{
WaitingForSpawn = 0,
ReadyToSelect = 1,
SelectionMade = 2,
}
public SelectionState SelectionStatus = SelectionState.WaitingForSpawn;
private TimeSpan _announceAt = TimeSpan.Zero;
private Dictionary<IPlayerSession, HumanoidCharacterProfile> _startCandidates = new();
public override void Initialize()
{
@@ -55,101 +77,101 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
}
protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime)
public override void Update(float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
base.Update(frameTime);
if (component.SelectionStatus == TraitorRuleComponent.SelectionState.ReadyToSelect && _gameTiming.CurTime > component.AnnounceAt)
DoTraitorStart(component);
if (SelectionStatus == SelectionState.ReadyToSelect && _gameTiming.CurTime >= _announceAt)
DoTraitorStart();
}
public override void Started(){}
public override void Ended()
{
Traitors.Clear();
_startCandidates.Clear();
SelectionStatus = SelectionState.WaitingForSpawn;
}
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var traitor, out var gameRule))
MakeCodewords();
if (!RuleAdded)
return;
var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
return;
}
MakeCodewords(traitor);
var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
ev.Cancel();
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
ev.Cancel();
}
}
private void MakeCodewords(TraitorRuleComponent component)
private void MakeCodewords()
{
var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
var adjectives = _prototypeManager.Index<DatasetPrototype>("adjectives").Values;
var verbs = _prototypeManager.Index<DatasetPrototype>("verbs").Values;
var codewordPool = adjectives.Concat(verbs).ToList();
var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count);
component.Codewords = new string[finalCodewordCount];
Codewords = new string[finalCodewordCount];
for (var i = 0; i < finalCodewordCount; i++)
{
component.Codewords[i] = _random.PickAndTake(codewordPool);
Codewords[i] = _random.PickAndTake(codewordPool);
}
}
private void DoTraitorStart(TraitorRuleComponent component)
private void DoTraitorStart()
{
if (!component.StartCandidates.Any())
if (!_startCandidates.Any())
{
_sawmill.Error("Tried to start Traitor mode without any candidates.");
return;
}
var numTraitors = MathHelper.Clamp(component.StartCandidates.Count / PlayersPerTraitor, 1, MaxTraitors);
var traitorPool = FindPotentialTraitors(component.StartCandidates, component);
var numTraitors = MathHelper.Clamp(_startCandidates.Count / _playersPerTraitor, 1, _maxTraitors);
var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
var traitorPool = FindPotentialTraitors(_startCandidates);
var selectedTraitors = PickTraitors(numTraitors, traitorPool);
foreach (var traitor in selectedTraitors)
{
MakeTraitor(traitor);
}
component.SelectionStatus = TraitorRuleComponent.SelectionState.SelectionMade;
SelectionStatus = SelectionState.SelectionMade;
}
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
{
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var traitor, out var gameRule))
if (!RuleAdded)
return;
foreach (var player in ev.Players)
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
if (!ev.Profiles.ContainsKey(player.UserId))
continue;
foreach (var player in ev.Players)
{
if (!ev.Profiles.ContainsKey(player.UserId))
continue;
traitor.StartCandidates[player] = ev.Profiles[player.UserId];
}
var delay = TimeSpan.FromSeconds(
_cfg.GetCVar(CCVars.TraitorStartDelay) +
_random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
traitor.AnnounceAt = _gameTiming.CurTime + delay;
traitor.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToSelect;
_startCandidates[player] = ev.Profiles[player.UserId];
}
var delay = TimeSpan.FromSeconds(
_cfg.GetCVar(CCVars.TraitorStartDelay) +
_random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
_announceAt = _gameTiming.CurTime + delay;
SelectionStatus = SelectionState.ReadyToSelect;
}
public List<IPlayerSession> FindPotentialTraitors(in Dictionary<IPlayerSession, HumanoidCharacterProfile> candidates, TraitorRuleComponent component)
public List<IPlayerSession> FindPotentialTraitors(in Dictionary<IPlayerSession, HumanoidCharacterProfile> candidates)
{
var list = new List<IPlayerSession>();
var pendingQuery = GetEntityQuery<PendingClockInComponent>();
@@ -174,7 +196,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
foreach (var player in list)
{
var profile = candidates[player];
if (profile.AntagPreferences.Contains(component.TraitorPrototypeId))
if (profile.AntagPreferences.Contains(TraitorPrototypeID))
{
prefList.Add(player);
}
@@ -206,14 +228,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
public bool MakeTraitor(IPlayerSession traitor)
{
var traitorRule = EntityQuery<TraitorRuleComponent>().FirstOrDefault();
if (traitorRule == null)
{
//todo fuck me this shit is awful
GameTicker.StartGameRule("traitor", out var ruleEntity);
traitorRule = EntityManager.GetComponent<TraitorRuleComponent>(ruleEntity);
}
var mind = traitor.Data.ContentData()?.Mind;
if (mind == null)
{
@@ -240,15 +254,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
if (pda == null || !_uplink.AddUplink(mind.OwnedEntity.Value, startingBalance))
return false;
// add the ringtone uplink and get its code for greeting
var code = AddComp<RingerUplinkComponent>(pda.Value).Code;
var antagPrototype = _prototypeManager.Index<AntagPrototype>(traitorRule.TraitorPrototypeId);
var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorPrototypeID);
var traitorRole = new TraitorRole(mind, antagPrototype);
mind.AddRole(traitorRole);
traitorRule.Traitors.Add(traitorRole);
traitorRole.GreetTraitor(traitorRule.Codewords, code);
Traitors.Add(traitorRole);
traitorRole.GreetTraitor(Codewords, code);
_faction.RemoveFaction(entity, "NanoTrasen", false);
_faction.AddFaction(entity, "Syndicate");
@@ -267,173 +280,147 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
}
//give traitors their codewords and uplink code to keep in their character info menu
traitorRole.Mind.Briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", traitorRule.Codewords)))
traitorRole.Mind.Briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", Codewords)))
+ "\n" + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("", code)));
_audioSystem.PlayGlobal(traitorRule.AddedSound, Filter.Empty().AddPlayer(traitor), false, AudioParams.Default);
_audioSystem.PlayGlobal(_addedSound, Filter.Empty().AddPlayer(traitor), false, AudioParams.Default);
return true;
}
private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
{
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var traitor, out var gameRule))
if (!RuleAdded)
return;
if (TotalTraitors >= _maxTraitors)
return;
if (!ev.LateJoin)
return;
if (!ev.Profile.AntagPreferences.Contains(TraitorPrototypeID))
return;
if (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job))
return;
if (!job.CanBeAntag)
return;
// Before the announcement is made, late-joiners are considered the same as players who readied.
if (SelectionStatus < SelectionState.SelectionMade)
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
_startCandidates[ev.Player] = ev.Profile;
return;
}
if (traitor.TotalTraitors >= MaxTraitors)
continue;
if (!ev.LateJoin)
continue;
if (!ev.Profile.AntagPreferences.Contains(traitor.TraitorPrototypeId))
continue;
// the nth player we adjust our probabilities around
int target = ((_playersPerTraitor * TotalTraitors) + 1);
if (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job))
continue;
float chance = (1f / _playersPerTraitor);
if (!job.CanBeAntag)
continue;
// If we have too many traitors, divide by how many players below target for next traitor we are.
if (ev.JoinOrder < target)
{
chance /= (target - ev.JoinOrder);
} else // Tick up towards 100% chance.
{
chance *= ((ev.JoinOrder + 1) - target);
}
if (chance > 1)
chance = 1;
// Before the announcement is made, late-joiners are considered the same as players who readied.
if (traitor.SelectionStatus < TraitorRuleComponent.SelectionState.SelectionMade)
{
traitor.StartCandidates[ev.Player] = ev.Profile;
continue;
}
// the nth player we adjust our probabilities around
var target = PlayersPerTraitor * traitor.TotalTraitors + 1;
var chance = 1f / PlayersPerTraitor;
// If we have too many traitors, divide by how many players below target for next traitor we are.
if (ev.JoinOrder < target)
{
chance /= (target - ev.JoinOrder);
}
else // Tick up towards 100% chance.
{
chance *= ((ev.JoinOrder + 1) - target);
}
if (chance > 1)
chance = 1;
// Now that we've calculated our chance, roll and make them a traitor if we roll under.
// You get one shot.
if (_random.Prob(chance))
{
MakeTraitor(ev.Player);
}
// Now that we've calculated our chance, roll and make them a traitor if we roll under.
// You get one shot.
if (_random.Prob(chance))
{
MakeTraitor(ev.Player);
}
}
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var traitor, out var gameRule))
if (!RuleAdded)
return;
var result = Loc.GetString("traitor-round-end-result", ("traitorCount", Traitors.Count));
result += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", Codewords))) + "\n";
foreach (var traitor in Traitors)
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
var name = traitor.Mind.CharacterName;
traitor.Mind.TryGetSession(out var session);
var username = session?.Name;
var result = Loc.GetString("traitor-round-end-result", ("traitorCount", traitor.Traitors.Count));
result += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", traitor.Codewords))) +
"\n";
foreach (var t in traitor.Traitors)
var objectives = traitor.Mind.AllObjectives.ToArray();
if (objectives.Length == 0)
{
var name = t.Mind.CharacterName;
t.Mind.TryGetSession(out var session);
var username = session?.Name;
var objectives = t.Mind.AllObjectives.ToArray();
if (objectives.Length == 0)
{
if (username != null)
{
if (name == null)
result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username));
else
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username),
("name", name));
}
else if (name != null)
result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name));
continue;
}
if (username != null)
{
if (name == null)
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives",
("user", username));
result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username));
else
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named",
("user", username), ("name", name));
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username), ("name", name));
}
else if (name != null)
result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name));
result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name));
foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer))
continue;
}
if (username != null)
{
if (name == null)
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives", ("user", username));
else
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named", ("user", username), ("name", name));
}
else if (name != null)
result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name));
foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer))
{
result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}");
foreach (var objective in objectiveGroup)
{
result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}");
foreach (var objective in objectiveGroup)
foreach (var condition in objective.Conditions)
{
foreach (var condition in objective.Conditions)
var progress = condition.Progress;
if (progress > 0.99f)
{
var progress = condition.Progress;
if (progress > 0.99f)
{
result += "\n- " + Loc.GetString(
"traitor-objective-condition-success",
("condition", condition.Title),
("markupColor", "green")
);
}
else
{
result += "\n- " + Loc.GetString(
"traitor-objective-condition-fail",
("condition", condition.Title),
("progress", (int) (progress * 100)),
("markupColor", "red")
);
}
result += "\n- " + Loc.GetString(
"traitor-objective-condition-success",
("condition", condition.Title),
("markupColor", "green")
);
}
else
{
result += "\n- " + Loc.GetString(
"traitor-objective-condition-fail",
("condition", condition.Title),
("progress", (int) (progress * 100)),
("markupColor", "red")
);
}
}
}
}
ev.AddLine(result);
}
ev.AddLine(result);
}
public List<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind)
public IEnumerable<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind)
{
List<TraitorRole> allTraitors = new();
foreach (var traitor in EntityQuery<TraitorRuleComponent>())
{
foreach (var role in GetOtherTraitorsAliveAndConnected(ourMind, traitor))
{
if (!allTraitors.Contains(role))
allTraitors.Add(role);
}
}
var traitors = Traitors;
List<TraitorRole> removeList = new();
return allTraitors;
}
public List<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind, TraitorRuleComponent component)
{
return component.Traitors // don't want
return Traitors // don't want
.Where(t => t.Mind is not null) // no mind
.Where(t => t.Mind.OwnedEntity is not null) // no entity
.Where(t => t.Mind.Session is not null) // player disconnected
.Where(t => t.Mind != ourMind) // ourselves
.Where(t => _mobStateSystem.IsAlive((EntityUid) t.Mind.OwnedEntity!)) // dead
.Where(t => t.Mind.CurrentEntity == t.Mind.OwnedEntity).ToList(); // not in original body
.Where(t => t.Mind.CurrentEntity == t.Mind.OwnedEntity); // not in original body
}
}

View File

@@ -4,7 +4,7 @@ using Content.Server.Actions;
using Content.Server.Chat.Managers;
using Content.Server.Disease;
using Content.Server.Disease.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Humanoid;
using Content.Server.Mind.Components;
using Content.Server.Players;
using Content.Server.Popups;
@@ -29,7 +29,7 @@ using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules;
public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
public sealed class ZombieRuleSystem : GameRuleSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -44,6 +44,14 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
private Dictionary<string, string> _initialInfectedNames = new();
public override string Prototype => "Zombie";
private const string PatientZeroPrototypeID = "InitialInfected";
private const string InitialZombieVirusPrototype = "PassiveZombieVirus";
private const string ZombifySelfActionPrototype = "TurnUndead";
public override void Initialize()
{
base.Initialize();
@@ -59,61 +67,60 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
foreach (var zombie in EntityQuery<ZombieRuleComponent>())
if (!RuleAdded)
return;
//this is just the general condition thing used for determining the win/lose text
var percent = GetInfectedPercentage(out var livingHumans);
if (percent <= 0)
ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
else if (percent <= 0.25)
ev.AddLine(Loc.GetString("zombie-round-end-amount-low"));
else if (percent <= 0.5)
ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((percent * 100), 2).ToString(CultureInfo.InvariantCulture))));
else if (percent < 1)
ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((percent * 100), 2).ToString(CultureInfo.InvariantCulture))));
else
ev.AddLine(Loc.GetString("zombie-round-end-amount-all"));
ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", _initialInfectedNames.Count)));
foreach (var player in _initialInfectedNames)
{
//this is just the general condition thing used for determining the win/lose text
var percent = GetInfectedPercentage(out var livingHumans);
ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
("name", player.Key),
("username", player.Value)));
}
if (percent <= 0)
ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
else if (percent <= 0.25)
ev.AddLine(Loc.GetString("zombie-round-end-amount-low"));
else if (percent <= 0.5)
ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((percent * 100), 2).ToString(CultureInfo.InvariantCulture))));
else if (percent < 1)
ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((percent * 100), 2).ToString(CultureInfo.InvariantCulture))));
else
ev.AddLine(Loc.GetString("zombie-round-end-amount-all"));
ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", zombie.InitialInfectedNames.Count)));
foreach (var player in zombie.InitialInfectedNames)
//Gets a bunch of the living players and displays them if they're under a threshold.
//InitialInfected is used for the threshold because it scales with the player count well.
if (livingHumans.Count > 0 && livingHumans.Count <= _initialInfectedNames.Count)
{
ev.AddLine("");
ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", livingHumans.Count)));
foreach (var survivor in livingHumans)
{
ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
("name", player.Key),
("username", player.Value)));
}
var meta = MetaData(survivor);
var username = string.Empty;
if (TryComp<MindComponent>(survivor, out var mindcomp))
if (mindcomp.Mind != null && mindcomp.Mind.Session != null)
username = mindcomp.Mind.Session.Name;
//Gets a bunch of the living players and displays them if they're under a threshold.
//InitialInfected is used for the threshold because it scales with the player count well.
if (livingHumans.Count > 0 && livingHumans.Count <= zombie.InitialInfectedNames.Count)
{
ev.AddLine("");
ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", livingHumans.Count)));
foreach (var survivor in livingHumans)
{
var meta = MetaData(survivor);
var username = string.Empty;
if (TryComp<MindComponent>(survivor, out var mindcomp))
if (mindcomp.Mind != null && mindcomp.Mind.Session != null)
username = mindcomp.Mind.Session.Name;
ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
("name", meta.EntityName),
("username", username)));
}
ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
("name", meta.EntityName),
("username", username)));
}
}
}
private void OnJobAssigned(RulePlayerJobsAssignedEvent ev)
{
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var zombies, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
InfectInitialPlayers(zombies);
}
if (!RuleAdded)
return;
_initialInfectedNames = new();
InfectInitialPlayers();
}
/// <remarks>
@@ -122,11 +129,15 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// </remarks>
private void OnMobStateChanged(MobStateChangedEvent ev)
{
if (!RuleAdded)
return;
CheckRoundEnd(ev.Target);
}
private void OnEntityZombified(EntityZombifiedEvent ev)
{
if (!RuleAdded)
return;
CheckRoundEnd(ev.Target);
}
@@ -136,59 +147,50 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// <param name="target">depending on this uid, we should care about the round ending</param>
private void CheckRoundEnd(EntityUid target)
{
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var zombies, out var gameRule))
{
if (GameTicker.IsGameRuleActive(uid, gameRule))
continue;
//we only care about players, not monkeys and such.
if (!HasComp<HumanoidAppearanceComponent>(target))
return;
//we only care about players, not monkeys and such.
if (!HasComp<HumanoidAppearanceComponent>(target))
continue;
var percent = GetInfectedPercentage(out var num);
if (num.Count == 1) //only one human left. spooky
_popup.PopupEntity(Loc.GetString("zombie-alone"), num[0], num[0]);
if (percent >= 1) //oops, all zombies
_roundEndSystem.EndRound();
}
var percent = GetInfectedPercentage(out var num);
if (num.Count == 1) //only one human left. spooky
_popup.PopupEntity(Loc.GetString("zombie-alone"), num[0], num[0]);
if (percent >= 1) //oops, all zombies
_roundEndSystem.EndRound();
}
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var zombies, out var gameRule))
if (!RuleAdded)
return;
var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
_chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
return;
}
var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready"));
ev.Cancel();
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready"));
ev.Cancel();
}
}
protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
public override void Started()
{
base.Started(uid, component, gameRule, args);
InfectInitialPlayers(component);
//this technically will run twice with zombies on roundstart, but it doesn't matter because it fails instantly
InfectInitialPlayers();
}
public override void Ended() { }
private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args)
{
_zombify.ZombifyEntity(uid);
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombifySelfActionPrototype));
_action.RemoveAction(uid, action);
}
@@ -226,7 +228,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// allowing this gamemode to be started midround. As such, it doesn't need
/// any information besides just running.
/// </remarks>
private void InfectInitialPlayers(ZombieRuleComponent component)
private void InfectInitialPlayers()
{
var allPlayers = _playerManager.ServerSessions.ToList();
var playerList = new List<IPlayerSession>();
@@ -238,7 +240,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
playerList.Add(player);
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeID))
if (pref.AntagPreferences.Contains(PatientZeroPrototypeID))
prefList.Add(player);
}
}
@@ -282,15 +284,15 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
DebugTools.AssertNotNull(mind.OwnedEntity);
mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeID)));
mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(PatientZeroPrototypeID)));
var inCharacterName = string.Empty;
if (mind.OwnedEntity != null)
{
_diseaseSystem.TryAddDisease(mind.OwnedEntity.Value, component.InitialZombieVirusPrototype);
_diseaseSystem.TryAddDisease(mind.OwnedEntity.Value, InitialZombieVirusPrototype);
inCharacterName = MetaData(mind.OwnedEntity.Value).EntityName;
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombifySelfActionPrototype));
_action.AddAction(mind.OwnedEntity.Value, action, null);
}
@@ -301,7 +303,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
//gets the names now in case the players leave.
//this gets unhappy if people with the same name get chose. Probably shouldn't happen.
component.InitialInfectedNames.Add(inCharacterName, mind.Session.Name);
_initialInfectedNames.Add(inCharacterName, mind.Session.Name);
// I went all the way to ChatManager.cs and all i got was this lousy T-shirt
// You got a free T-shirt!?!?