* Improve active game rule querying, add generic try round start attempt method, move minPlayers to GameRuleComponent
* Nukeops todo and cleanup
* Remove Active field
* Add EndedGameRuleComponent
ruleData ??= EnsureComp<GameRuleComponent>(ruleEntity);
// can't start an already active rule
- if (ruleData.Active || ruleData.Ended)
+ if (HasComp<ActiveGameRuleComponent>(ruleEntity) || HasComp<EndedGameRuleComponent>(ruleEntity))
return false;
if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
_allPreviousGameRules.Add((RoundDuration(), id));
_sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
- ruleData.Active = true;
+ EnsureComp<ActiveGameRuleComponent>(ruleEntity);
ruleData.ActivatedAt = _gameTiming.CurTime;
+
var ev = new GameRuleStartedEvent(ruleEntity, id);
RaiseLocalEvent(ruleEntity, ref ev, true);
return true;
return false;
// don't end it multiple times
- if (ruleData.Ended)
+ if (HasComp<EndedGameRuleComponent>(ruleEntity))
return false;
if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
return false;
- ruleData.Active = false;
- ruleData.Ended = true;
+ RemComp<ActiveGameRuleComponent>(ruleEntity);
+ EnsureComp<EndedGameRuleComponent>(ruleEntity);
+
_sawmill.Info($"Ended game rule {ToPrettyString(ruleEntity)}");
var ev = new GameRuleEndedEvent(ruleEntity, id);
public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null)
{
- return Resolve(ruleEntity, ref component) && !component.Ended;
+ return Resolve(ruleEntity, ref component) && !HasComp<EndedGameRuleComponent>(ruleEntity);
}
public bool IsGameRuleAdded(string rule)
public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null)
{
- return Resolve(ruleEntity, ref component) && component.Active;
+ return Resolve(ruleEntity, ref component) && HasComp<ActiveGameRuleComponent>(ruleEntity);
}
public bool IsGameRuleActive(string rule)
/// </summary>
public IEnumerable<EntityUid> GetActiveGameRules()
{
- var query = EntityQueryEnumerator<GameRuleComponent>();
- while (query.MoveNext(out var uid, out var ruleData))
+ var query = EntityQueryEnumerator<ActiveGameRuleComponent, GameRuleComponent>();
+ while (query.MoveNext(out var uid, out _, out _))
{
- if (ruleData.Active)
- yield return uid;
+ yield return uid;
}
}
--- /dev/null
+namespace Content.Server.GameTicking.Rules.Components;
+
+/// <summary>
+/// Added to game rules before <see cref="GameRuleStartedEvent"/> and removed before <see cref="GameRuleEndedEvent"/>.
+/// Mutually exclusive with <seealso cref="EndedGameRuleComponent"/>.
+/// </summary>
+[RegisterComponent]
+public sealed partial class ActiveGameRuleComponent : Component
+{
+}
--- /dev/null
+namespace Content.Server.GameTicking.Rules.Components;
+
+/// <summary>
+/// Added to game rules before <see cref="GameRuleEndedEvent"/>.
+/// Mutually exclusive with <seealso cref="ActiveGameRuleComponent"/>.
+/// </summary>
+[RegisterComponent]
+public sealed partial class EndedGameRuleComponent : Component
+{
+}
[RegisterComponent]
public sealed partial 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>
/// Game time when game rule was activated
/// </summary>
- [DataField("activatedAt", customTypeSerializer:typeof(TimeOffsetSerializer))]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan ActivatedAt;
/// <summary>
- /// Whether or not the gamerule finished.
- /// Used for tracking whether a non-active gamerule has been started before.
+ /// The minimum amount of players needed for this game rule.
/// </summary>
- [DataField("ended")]
- public bool Ended;
+ [DataField]
+ public int MinPlayers;
}
/// <summary>
using Content.Shared.Dataset;
using Content.Shared.Roles;
using Robust.Shared.Map;
-using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
public sealed partial class NukeopsRuleComponent : Component
{
+ // TODO Replace with GameRuleComponent.minPlayers
/// <summary>
/// The minimum needed amount of players
/// </summary>
- [DataField("minPlayers")]
+ [DataField]
public int MinPlayers = 20;
/// <summary>
/// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
/// </summary>
- [DataField("playersPerOperative")]
+ [DataField]
public int PlayersPerOperative = 10;
- [DataField("maxOps")]
- public int MaxOperatives = 5;
+ [DataField]
+ public int MaxOps = 5;
/// <summary>
/// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
/// </summary>
- [DataField("roundEndBehavior")]
+ [DataField]
public RoundEndBehavior RoundEndBehavior = RoundEndBehavior.ShuttleCall;
/// <summary>
/// Text for shuttle call if RoundEndBehavior is ShuttleCall.
/// </summary>
- [DataField("roundEndTextSender")]
+ [DataField]
public string RoundEndTextSender = "comms-console-announcement-title-centcom";
/// <summary>
/// Text for shuttle call if RoundEndBehavior is ShuttleCall.
/// </summary>
- [DataField("roundEndTextShuttleCall")]
+ [DataField]
public string RoundEndTextShuttleCall = "nuke-ops-no-more-threat-announcement-shuttle-call";
/// <summary>
/// Text for announcement if RoundEndBehavior is ShuttleCall. Used if shuttle is already called
/// </summary>
- [DataField("roundEndTextAnnouncement")]
+ [DataField]
public string RoundEndTextAnnouncement = "nuke-ops-no-more-threat-announcement";
/// <summary>
/// Time to emergency shuttle to arrive if RoundEndBehavior is ShuttleCall.
/// </summary>
- [DataField("evacShuttleTime")]
+ [DataField]
public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(10);
/// <summary>
/// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event.
/// </summary>
- [DataField("spawnOutpost")]
+ [DataField]
public bool SpawnOutpost = true;
/// <summary>
/// Whether or not nukie left their outpost
/// </summary>
- [DataField("leftOutpost")]
- public bool LeftOutpost = false;
+ [DataField]
+ public bool LeftOutpost;
/// <summary>
/// Enables opportunity to get extra TC for war declaration
/// </summary>
- [DataField("canEnableWarOps")]
+ [DataField]
public bool CanEnableWarOps = true;
/// <summary>
/// Indicates time when war has been declared, null if not declared
/// </summary>
- [DataField("warDeclaredTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? WarDeclaredTime;
/// <summary>
/// This amount of TC will be given to each nukie
/// </summary>
- [DataField("warTCAmountPerNukie")]
+ [DataField]
public int WarTCAmountPerNukie = 40;
/// <summary>
/// <summary>
/// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare
/// </summary>
- [DataField("warNukieArriveDelay")]
+ [DataField]
public TimeSpan? WarNukieArriveDelay = TimeSpan.FromMinutes(15);
/// <summary>
/// Minimal operatives count for war declaration
/// </summary>
- [DataField("warDeclarationMinOps")]
+ [DataField]
public int WarDeclarationMinOps = 4;
- [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string SpawnPointPrototype = "SpawnPointNukies";
+ [DataField]
+ public EntProtoId SpawnPointProto = "SpawnPointNukies";
- [DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
+ [DataField]
+ public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
- [DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
- public string CommanderRolePrototype = "NukeopsCommander";
+ [DataField]
+ public ProtoId<AntagPrototype> CommanderRoleProto = "NukeopsCommander";
- [DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
- public string OperativeRoleProto = "Nukeops";
+ [DataField]
+ public ProtoId<AntagPrototype> OperativeRoleProto = "Nukeops";
- [DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
- public string MedicRoleProto = "NukeopsMedic";
+ [DataField]
+ public ProtoId<AntagPrototype> MedicRoleProto = "NukeopsMedic";
- [DataField("commanderStartingGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
- public string CommanderStartGearPrototype = "SyndicateCommanderGearFull";
+ [DataField]
+ public ProtoId<StartingGearPrototype> CommanderStartGearProto = "SyndicateCommanderGearFull";
- [DataField("medicStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
- public string MedicStartGearPrototype = "SyndicateOperativeMedicFull";
+ [DataField]
+ public ProtoId<StartingGearPrototype> MedicStartGearProto = "SyndicateOperativeMedicFull";
- [DataField("operativeStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
- public string OperativeStartGearPrototype = "SyndicateOperativeGearFull";
+ [DataField]
+ public ProtoId<StartingGearPrototype> OperativeStartGearProto = "SyndicateOperativeGearFull";
- [DataField("eliteNames", customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
public string EliteNames = "SyndicateNamesElite";
- [DataField("normalNames", customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
public string NormalNames = "SyndicateNamesNormal";
- [DataField("outpostMap", customTypeSerializer: typeof(ResPathSerializer))]
- public ResPath NukieOutpostMap = new("/Maps/nukieplanet.yml");
+ [DataField(customTypeSerializer: typeof(ResPathSerializer))]
+ public ResPath OutpostMap = new("/Maps/nukieplanet.yml");
- [DataField("shuttleMap", customTypeSerializer: typeof(ResPathSerializer))]
- public ResPath NukieShuttleMap = new("/Maps/infiltrator.yml");
+ [DataField(customTypeSerializer: typeof(ResPathSerializer))]
+ public ResPath ShuttleMap = new("/Maps/infiltrator.yml");
- [DataField("winType")]
+ [DataField]
public WinType WinType = WinType.Neutral;
- [DataField("winConditions")]
+ [DataField]
public List<WinCondition> WinConditions = new ();
public MapId? NukiePlanet;
/// <summary>
/// Cached starting gear prototypes.
/// </summary>
- [DataField("startingGearPrototypes")]
+ [DataField]
public Dictionary<string, StartingGearPrototype> StartingGearPrototypes = new ();
/// <summary>
/// Cached operator name prototypes.
/// </summary>
- [DataField("operativeNames")]
+ [DataField]
public 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")]
+ [DataField]
public Dictionary<EntityUid, string> OperativeMindPendingData = new();
/// <summary>
/// Players who played as an operative at some point in the round.
/// Stores the mind as well as the entity name
/// </summary>
- [DataField("operativePlayers")]
+ [DataField]
public Dictionary<string, EntityUid> OperativePlayers = new();
- [DataField("faction", customTypeSerializer: typeof(PrototypeIdSerializer<NpcFactionPrototype>), required: true)]
- public string Faction = default!;
+ [DataField(required: true)]
+ public ProtoId<NpcFactionPrototype> Faction = default!;
}
public enum WinType : byte
+using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
namespace Content.Server.GameTicking.Rules;
public abstract partial class GameRuleSystem<T> : EntitySystem where T : Component
{
+ [Dependency] protected readonly IChatManager ChatManager = default!;
[Dependency] protected readonly GameTicker GameTicker = default!;
public override void Initialize()
Ended(uid, component, ruleData, args);
}
+
/// <summary>
/// Called when the gamerule is added
/// </summary>
}
+ protected EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent> QueryActiveRules()
+ {
+ return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
+ }
+
+ protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
+ {
+ var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
+ while (query.MoveNext(out _, out _, out _, out var gameRule))
+ {
+ var minPlayers = gameRule.MinPlayers;
+ if (!ev.Forced && ev.Players.Length < minPlayers)
+ {
+ ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
+ ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers),
+ ("presetName", localizedPresetName)));
+ ev.Cancel();
+ continue;
+ }
+
+ if (ev.Players.Length == 0)
+ {
+ ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready"));
+ ev.Cancel();
+ }
+ }
+
+ return !ev.Cancelled;
+ }
+
public override void Update(float frameTime)
{
base.Update(frameTime);
// Basically copied verbatim from traitor code
var playersPerOperative = nukeops.PlayersPerOperative;
- var maxOperatives = nukeops.MaxOperatives;
+ var maxOperatives = nukeops.MaxOps;
// Dear lord what is happening HERE.
var everyone = new List<IPlayerSession>(ev.PlayerPool);
}
var profile = ev.Profiles[player.UserId];
- if (profile.AntagPreferences.Contains(nukeops.OperativeRoleProto))
+ if (profile.AntagPreferences.Contains(nukeops.OperativeRoleProto.Id))
{
prefList.Add(player);
}
- if (profile.AntagPreferences.Contains(nukeops.MedicRoleProto))
+ if (profile.AntagPreferences.Contains(nukeops.MedicRoleProto.Id))
{
medPrefList.Add(player);
}
- if (profile.AntagPreferences.Contains(nukeops.CommanderRolePrototype))
+ if (profile.AntagPreferences.Contains(nukeops.CommanderRoleProto.Id))
{
cmdrPrefList.Add(player);
}
if (!component.SpawnOutpost)
return true;
- var path = component.NukieOutpostMap;
- var shuttlePath = component.NukieShuttleMap;
+ var path = component.OutpostMap;
+ var shuttlePath = component.ShuttleMap;
var mapId = _mapManager.CreateMap();
var options = new MapLoadOptions
{
case 0:
name = Loc.GetString("nukeops-role-commander") + " " + _random.PickAndTake(component.OperativeNames[component.EliteNames]);
- role = component.CommanderRolePrototype;
- gear = component.CommanderStartGearPrototype;
+ role = component.CommanderRoleProto;
+ gear = component.CommanderStartGearProto;
break;
case 1:
name = Loc.GetString("nukeops-role-agent") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]);
role = component.MedicRoleProto;
- gear = component.MedicStartGearPrototype;
+ gear = component.MedicStartGearProto;
break;
default:
name = Loc.GetString("nukeops-role-operator") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]);
role = component.OperativeRoleProto;
- gear = component.OperativeStartGearPrototype;
+ gear = component.OperativeStartGearProto;
break;
}
// Forgive me for hardcoding prototypes
foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
{
- if (meta.EntityPrototype?.ID != component.SpawnPointPrototype)
+ if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id)
continue;
if (xform.ParentUid != component.NukieOutpost)
}
// Basically copied verbatim from traitor code
var playersPerOperative = component.PlayersPerOperative;
- var maxOperatives = component.MaxOperatives;
+ var maxOperatives = component.MaxOps;
var playerPool = _playerManager.ServerSessions.ToList();
var numNukies = MathHelper.Clamp(playerPool.Count / playersPerOperative, 1, maxOperatives);
// TODO: Loot table or something
foreach (var proto in new[]
{
- component.CommanderStartGearPrototype,
- component.MedicStartGearPrototype,
- component.OperativeStartGearPrototype
+ component.CommanderStartGearProto,
+ component.MedicStartGearProto,
+ component.OperativeStartGearProto
})
{
component.StartingGearPrototypes.Add(proto, _prototypeManager.Index<StartingGearPrototype>(proto));