/// This should not be used if the entity is owned by the server. The server will otherwise
/// override this with the appearance data it sends over.
/// </remarks>
- public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null)
+ public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
{
+ if (profile == null)
+ return;
+
if (!Resolve(uid, ref humanoid))
{
return;
// The game rule exists, and all the stations/shuttles/maps are properly initialized
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single().Component;
- Assert.That(entMan.EntityExists(rule.NukieOutpost));
- Assert.That(entMan.EntityExists(rule.NukieShuttle));
+ var mapRule = entMan.AllComponents<LoadMapRuleComponent>().Single().Component;
+ foreach (var grid in mapRule.MapGrids)
+ {
+ Assert.That(entMan.EntityExists(grid));
+ Assert.That(entMan.HasComponent<MapGridComponent>(grid));
+ Assert.That(entMan.HasComponent<StationMemberComponent>(grid));
+ }
Assert.That(entMan.EntityExists(rule.TargetStation));
- Assert.That(entMan.HasComponent<MapGridComponent>(rule.NukieOutpost));
- Assert.That(entMan.HasComponent<MapGridComponent>(rule.NukieShuttle));
-
- Assert.That(entMan.HasComponent<StationMemberComponent>(rule.NukieOutpost));
Assert.That(entMan.HasComponent<StationDataComponent>(rule.TargetStation));
- var nukieStation = entMan.GetComponent<StationMemberComponent>(rule.NukieOutpost!.Value);
+ var nukieShuttlEnt = entMan.AllComponents<NukeOpsShuttleComponent>().FirstOrDefault().Uid;
+ Assert.That(entMan.EntityExists(nukieShuttlEnt));
+
+ EntityUid? nukieStationEnt = null;
+ foreach (var grid in mapRule.MapGrids)
+ {
+ if (entMan.HasComponent<StationMemberComponent>(grid))
+ {
+ nukieStationEnt = grid;
+ break;
+ }
+ }
+
+ Assert.That(entMan.EntityExists(nukieStationEnt));
+ var nukieStation = entMan.GetComponent<StationMemberComponent>(nukieStationEnt!.Value);
+
Assert.That(entMan.EntityExists(nukieStation.Station));
Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation));
- Assert.That(server.MapMan.MapExists(rule.NukiePlanet));
- var nukieMap = mapSys.GetMap(rule.NukiePlanet!.Value);
+ Assert.That(server.MapMan.MapExists(mapRule.Map));
+ var nukieMap = mapSys.GetMap(mapRule.Map!.Value);
var targetStation = entMan.GetComponent<StationDataComponent>(rule.TargetStation!.Value);
var targetGrid = targetStation.Grids.First();
Assert.That(targetMap, Is.Not.EqualTo(nukieMap));
Assert.That(entMan.GetComponent<TransformComponent>(player).MapUid, Is.EqualTo(nukieMap));
- Assert.That(entMan.GetComponent<TransformComponent>(rule.NukieOutpost!.Value).MapUid, Is.EqualTo(nukieMap));
- Assert.That(entMan.GetComponent<TransformComponent>(rule.NukieShuttle!.Value).MapUid, Is.EqualTo(nukieMap));
+ Assert.That(entMan.GetComponent<TransformComponent>(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap));
+ Assert.That(entMan.GetComponent<TransformComponent>(nukieShuttlEnt).MapUid, Is.EqualTo(nukieMap));
// The maps are all map-initialized, including the player
// Yes, this is necessary as this has repeatedly been broken somehow.
Assert.That(LifeStage(player), Is.GreaterThan(EntityLifeStage.Initialized));
Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized));
Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized));
- Assert.That(LifeStage(rule.NukieOutpost), Is.GreaterThan(EntityLifeStage.Initialized));
- Assert.That(LifeStage(rule.NukieShuttle), Is.GreaterThan(EntityLifeStage.Initialized));
+ Assert.That(LifeStage(nukieStationEnt.Value), Is.GreaterThan(EntityLifeStage.Initialized));
+ Assert.That(LifeStage(nukieShuttlEnt), Is.GreaterThan(EntityLifeStage.Initialized));
Assert.That(LifeStage(rule.TargetStation), Is.GreaterThan(EntityLifeStage.Initialized));
// Make sure the player has hands. We've had fucking disarmed nukies before.
using Content.Server.GameTicking;
using Content.Server.GameTicking.Commands;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.CCVar;
var server = pair.Server;
await server.WaitIdleAsync();
+ var entMan = server.ResolveDependency<IEntityManager>();
var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
- foreach (var rule in gameTicker.GetAddedGameRules())
- {
- Assert.That(gameTicker.GetActiveGameRules(), Does.Contain(rule));
- }
+ Assert.That(gameTicker.GetAddedGameRules().Count(), Is.GreaterThan(1), $"No additional rules started by secret rule.");
// End all rules
gameTicker.ClearGameRules();
using System.Threading.Tasks;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Presets;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Maps;
-using Content.Server.GameTicking.Rules;
+using Content.Server.Administration.Commands;
+using Content.Server.Antag;
+using Content.Server.GameTicking.Rules.Components;
using Content.Server.Zombies;
using Content.Shared.Administration;
using Content.Shared.Database;
-using Content.Shared.Humanoid;
using Content.Shared.Mind.Components;
+using Content.Shared.Roles;
using Content.Shared.Verbs;
using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems;
public sealed partial class AdminVerbSystem
{
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
- [Dependency] private readonly ThiefRuleSystem _thief = default!;
- [Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
- [Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
- [Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
- [Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!;
+
+ [ValidatePrototypeId<EntityPrototype>]
+ private const string DefaultTraitorRule = "Traitor";
+
+ [ValidatePrototypeId<EntityPrototype>]
+ private const string DefaultNukeOpRule = "LoneOpsSpawn";
+
+ [ValidatePrototypeId<EntityPrototype>]
+ private const string DefaultRevsRule = "Revolutionary";
+
+ [ValidatePrototypeId<EntityPrototype>]
+ private const string DefaultThiefRule = "Thief";
+
+ [ValidatePrototypeId<StartingGearPrototype>]
+ private const string PirateGearId = "PirateGear";
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
Act = () =>
{
- // if its a monkey or mouse or something dont give uplink or objectives
- var isHuman = HasComp<HumanoidAppearanceComponent>(args.Target);
- _traitorRule.MakeTraitorAdmin(args.Target, giveUplink: isHuman, giveObjectives: isHuman);
+ _antag.ForceMakeAntag<TraitorRuleComponent>(player, DefaultTraitorRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-traitor"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
Act = () =>
{
- _nukeopsRule.MakeLoneNukie(args.Target);
+ _antag.ForceMakeAntag<NukeopsRuleComponent>(player, DefaultNukeOpRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-nuclear-operative"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
Act = () =>
{
- _piratesRule.MakePirate(args.Target);
+ // pirates just get an outfit because they don't really have logic associated with them
+ SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-pirate"),
};
args.Verbs.Add(pirate);
- //todo come here at some point dear lort.
Verb headRev = new()
{
Text = Loc.GetString("admin-verb-text-make-head-rev"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
Act = () =>
{
- _revolutionaryRule.OnHeadRevAdmin(args.Target);
+ _antag.ForceMakeAntag<RevolutionaryRuleComponent>(player, DefaultRevsRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-head-rev"),
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
Act = () =>
{
- _thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad
+ _antag.ForceMakeAntag<ThiefRuleComponent>(player, DefaultThiefRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-thief"),
--- /dev/null
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+
+namespace Content.Server.Antag;
+
+public sealed class AntagSelectionPlayerPool(params List<ICommonSession>[] sessions)
+{
+ private readonly List<List<ICommonSession>> _orderedPools = sessions.ToList();
+
+ public bool TryPickAndTake(IRobustRandom random, [NotNullWhen(true)] out ICommonSession? session)
+ {
+ session = null;
+
+ foreach (var pool in _orderedPools)
+ {
+ if (pool.Count == 0)
+ continue;
+
+ session = random.PickAndTake(pool);
+ break;
+ }
+
+ return session != null;
+ }
+
+ public int Count => _orderedPools.Sum(p => p.Count);
+}
--- /dev/null
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Server.Antag.Components;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Objectives;
+using Content.Shared.Chat;
+using Content.Shared.Mind;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+
+namespace Content.Server.Antag;
+
+public sealed partial class AntagSelectionSystem
+{
+ /// <summary>
+ /// Tries to get the next non-filled definition based on the current amount of selected minds and other factors.
+ /// </summary>
+ public bool TryGetNextAvailableDefinition(Entity<AntagSelectionComponent> ent,
+ [NotNullWhen(true)] out AntagSelectionDefinition? definition)
+ {
+ definition = null;
+
+ var totalTargetCount = GetTargetAntagCount(ent);
+ var mindCount = ent.Comp.SelectedMinds.Count;
+ if (mindCount >= totalTargetCount)
+ return false;
+
+ foreach (var def in ent.Comp.Definitions)
+ {
+ var target = GetTargetAntagCount(ent, null, def);
+
+ if (mindCount < target)
+ {
+ definition = def;
+ return true;
+ }
+
+ mindCount -= target;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Gets the number of antagonists that should be present for a given rule based on the provided pool.
+ /// A null pool will simply use the player count.
+ /// </summary>
+ public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool = null)
+ {
+ var count = 0;
+ foreach (var def in ent.Comp.Definitions)
+ {
+ count += GetTargetAntagCount(ent, pool, def);
+ }
+
+ return count;
+ }
+
+ /// <summary>
+ /// Gets the number of antagonists that should be present for a given antag definition based on the provided pool.
+ /// A null pool will simply use the player count.
+ /// </summary>
+ public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def)
+ {
+ var poolSize = pool?.Count ?? _playerManager.Sessions.Length;
+ // factor in other definitions' affect on the count.
+ var countOffset = 0;
+ foreach (var otherDef in ent.Comp.Definitions)
+ {
+ countOffset += Math.Clamp(poolSize / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
+ }
+ // make sure we don't double-count the current selection
+ countOffset -= Math.Clamp((poolSize + countOffset) / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
+
+ return Math.Clamp((poolSize - countOffset) / def.PlayerRatio, def.Min, def.Max);
+ }
+
+ /// <summary>
+ /// Returns identifiable information for all antagonists to be used in a round end summary.
+ /// </summary>
+ /// <returns>
+ /// A list containing, in order, the antag's mind, the session data, and the original name stored as a string.
+ /// </returns>
+ public List<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity<AntagSelectionComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return new List<(EntityUid, SessionData, string)>();
+
+ var output = new List<(EntityUid, SessionData, string)>();
+ foreach (var (mind, name) in ent.Comp.SelectedMinds)
+ {
+ if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
+ continue;
+
+ if (!_playerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data))
+ continue;
+
+ output.Add((mind, data, name));
+ }
+ return output;
+ }
+
+ /// <summary>
+ /// Returns all the minds of antagonists.
+ /// </summary>
+ public List<Entity<MindComponent>> GetAntagMinds(Entity<AntagSelectionComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return new();
+
+ var output = new List<Entity<MindComponent>>();
+ foreach (var (mind, _) in ent.Comp.SelectedMinds)
+ {
+ if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
+ continue;
+
+ output.Add((mind, mindComp));
+ }
+ return output;
+ }
+
+ /// <remarks>
+ /// Helper specifically for <see cref="ObjectivesTextGetInfoEvent"/>
+ /// </remarks>
+ public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return new();
+
+ return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
+ }
+
+ /// <summary>
+ /// Returns all the antagonists for this rule who are currently alive
+ /// </summary>
+ public IEnumerable<EntityUid> GetAliveAntags(Entity<AntagSelectionComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ yield break;
+
+ var minds = GetAntagMinds(ent);
+ foreach (var mind in minds)
+ {
+ if (_mind.IsCharacterDeadIc(mind))
+ continue;
+
+ if (mind.Comp.OriginalOwnedEntity != null)
+ yield return GetEntity(mind.Comp.OriginalOwnedEntity.Value);
+ }
+ }
+
+ /// <summary>
+ /// Returns the number of alive antagonists for this rule.
+ /// </summary>
+ public int GetAliveAntagCount(Entity<AntagSelectionComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return 0;
+
+ var numbah = 0;
+ var minds = GetAntagMinds(ent);
+ foreach (var mind in minds)
+ {
+ if (_mind.IsCharacterDeadIc(mind))
+ continue;
+
+ numbah++;
+ }
+
+ return numbah;
+ }
+
+ /// <summary>
+ /// Returns if there are any remaining antagonists alive for this rule.
+ /// </summary>
+ public bool AnyAliveAntags(Entity<AntagSelectionComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return false;
+
+ return GetAliveAntags(ent).Any();
+ }
+
+ /// <summary>
+ /// Checks if all the antagonists for this rule are alive.
+ /// </summary>
+ public bool AllAntagsAlive(Entity<AntagSelectionComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return false;
+
+ return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
+ }
+
+ /// <summary>
+ /// Helper method to send the briefing text and sound to a player entity
+ /// </summary>
+ /// <param name="entity">The entity chosen to be antag</param>
+ /// <param name="briefing">The briefing text to send</param>
+ /// <param name="briefingColor">The color the briefing should be, null for default</param>
+ /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
+ public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+ {
+ if (!_mind.TryGetMind(entity, out _, out var mindComponent))
+ return;
+
+ if (mindComponent.Session == null)
+ return;
+
+ SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound);
+ }
+
+ /// <summary>
+ /// Helper method to send the briefing text and sound to a list of sessions
+ /// </summary>
+ /// <param name="sessions">The sessions that will be sent the briefing</param>
+ /// <param name="briefing">The briefing text to send</param>
+ /// <param name="briefingColor">The color the briefing should be, null for default</param>
+ /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
+ [PublicAPI]
+ public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+ {
+ foreach (var session in sessions)
+ {
+ SendBriefing(session, briefing, briefingColor, briefingSound);
+ }
+ }
+
+ /// <summary>
+ /// Helper method to send the briefing text and sound to a session
+ /// </summary>
+ /// <param name="session">The player chosen to be an antag</param>
+ /// <param name="data">The briefing data</param>
+ public void SendBriefing(
+ ICommonSession? session,
+ BriefingData? data)
+ {
+ if (session == null || data == null)
+ return;
+
+ var text = data.Value.Text == null ? string.Empty : Loc.GetString(data.Value.Text);
+ SendBriefing(session, text, data.Value.Color, data.Value.Sound);
+ }
+
+ /// <summary>
+ /// Helper method to send the briefing text and sound to a session
+ /// </summary>
+ /// <param name="session">The player chosen to be an antag</param>
+ /// <param name="briefing">The briefing text to send</param>
+ /// <param name="briefingColor">The color the briefing should be, null for default</param>
+ /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
+ public void SendBriefing(
+ ICommonSession? session,
+ string briefing,
+ Color? briefingColor,
+ SoundSpecifier? briefingSound)
+ {
+ if (session == null)
+ return;
+
+ _audio.PlayGlobal(briefingSound, session);
+ if (!string.IsNullOrEmpty(briefing))
+ {
+ var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
+ _chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel,
+ briefingColor);
+ }
+ }
+
+ /// <summary>
+ /// This technically is a gamerule-ent-less way to make an entity an antag.
+ /// You should almost never be using this.
+ /// </summary>
+ public void ForceMakeAntag<T>(ICommonSession? player, string defaultRule) where T : Component
+ {
+ var rule = ForceGetGameRuleEnt<T>(defaultRule);
+
+ if (!TryGetNextAvailableDefinition(rule, out var def))
+ def = rule.Comp.Definitions.Last();
+ MakeAntag(rule, player, def.Value);
+ }
+
+ /// <summary>
+ /// Tries to grab one of the weird specific antag gamerule ents or starts a new one.
+ /// This is gross code but also most of this is pretty gross to begin with.
+ /// </summary>
+ public Entity<AntagSelectionComponent> ForceGetGameRuleEnt<T>(string id) where T : Component
+ {
+ var query = EntityQueryEnumerator<T, AntagSelectionComponent>();
+ while (query.MoveNext(out var uid, out _, out var comp))
+ {
+ return (uid, comp);
+ }
+ var ruleEnt = GameTicker.AddGameRule(id);
+ RemComp<LoadMapRuleComponent>(ruleEnt);
+ var antag = Comp<AntagSelectionComponent>(ruleEnt);
+ antag.SelectionsComplete = true; // don't do normal selection.
+ GameTicker.StartGameRule(ruleEnt);
+ return (ruleEnt, antag);
+ }
+}
+using System.Linq;
+using Content.Server.Antag.Components;
+using Content.Server.Chat.Managers;
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Ghost.Roles;
+using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
using Content.Server.Preferences.Managers;
+using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components;
+using Content.Server.Station.Systems;
using Content.Shared.Antag;
+using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Players;
using Content.Shared.Preferences;
-using Content.Shared.Roles;
using Robust.Server.Audio;
-using Robust.Shared.Audio;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
using Robust.Shared.Random;
-using System.Linq;
-using Content.Shared.Chat;
-using Robust.Shared.Enums;
namespace Content.Server.Antag;
-public sealed class AntagSelectionSystem : GameRuleSystem<GameRuleComponent>
+public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
{
- [Dependency] private readonly IServerPreferencesManager _prefs = default!;
- [Dependency] private readonly AudioSystem _audioSystem = default!;
+ [Dependency] private readonly IChatManager _chat = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IServerPreferencesManager _pref = default!;
+ [Dependency] private readonly AudioSystem _audio = default!;
+ [Dependency] private readonly GhostRoleSystem _ghostRole = default!;
[Dependency] private readonly JobSystem _jobs = default!;
- [Dependency] private readonly MindSystem _mindSystem = default!;
- [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
+ [Dependency] private readonly MapSystem _map = default!;
+ [Dependency] private readonly MindSystem _mind = default!;
+ [Dependency] private readonly RoleSystem _role = default!;
+ [Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
+ [Dependency] private readonly TransformSystem _transform = default!;
- #region Eligible Player Selection
- /// <summary>
- /// Get all players that are eligible for an antag role
- /// </summary>
- /// <param name="playerSessions">All sessions from which to select eligible players</param>
- /// <param name="antagPrototype">The prototype to get eligible players for</param>
- /// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
- /// <param name="acceptableAntags">Should players already selected as antags be eligible</param>
- /// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
- /// <param name="customExcludeCondition">A custom condition that each player is tested against, if it returns true the player is excluded from eligibility</param>
- /// <returns>List of all player entities that match the requirements</returns>
- public List<EntityUid> GetEligiblePlayers(IEnumerable<ICommonSession> playerSessions,
- ProtoId<AntagPrototype> antagPrototype,
- bool includeAllJobs = false,
- AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
- bool ignorePreferences = false,
- bool allowNonHumanoids = false,
- Func<EntityUid?, bool>? customExcludeCondition = null)
+ // arbitrary random number to give late joining some mild interest.
+ public const float LateJoinRandomChance = 0.5f;
+
+ /// <inheritdoc/>
+ public override void Initialize()
{
- var eligiblePlayers = new List<EntityUid>();
+ base.Initialize();
- foreach (var player in playerSessions)
- {
- if (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition))
- eligiblePlayers.Add(player.AttachedEntity!.Value);
- }
+ SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
- return eligiblePlayers;
+ SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
+ SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
+ SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
}
- /// <summary>
- /// Get all sessions that are eligible for an antag role, can be run prior to sessions being attached to an entity
- /// This does not exclude sessions that have already been chosen as antags - that must be handled manually
- /// </summary>
- /// <param name="playerSessions">All sessions from which to select eligible players</param>
- /// <param name="antagPrototype">The prototype to get eligible players for</param>
- /// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
- /// <returns>List of all player sessions that match the requirements</returns>
- public List<ICommonSession> GetEligibleSessions(IEnumerable<ICommonSession> playerSessions, ProtoId<AntagPrototype> antagPrototype, bool ignorePreferences = false)
+ private void OnTakeGhostRole(Entity<GhostRoleAntagSpawnerComponent> ent, ref TakeGhostRoleEvent args)
{
- var eligibleSessions = new List<ICommonSession>();
+ if (args.TookRole)
+ return;
+
+ if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } def)
+ return;
- foreach (var session in playerSessions)
+ if (!Exists(rule) || !TryComp<AntagSelectionComponent>(rule, out var select))
+ return;
+
+ MakeAntag((rule, select), args.Player, def, ignoreSpawner: true);
+ args.TookRole = true;
+ _ghostRole.UnregisterGhostRole((ent, Comp<GhostRoleComponent>(ent)));
+ }
+
+ private void OnPlayerSpawning(RulePlayerSpawningEvent args)
+ {
+ var pool = args.PlayerPool;
+
+ var query = QueryActiveRules();
+ while (query.MoveNext(out var uid, out _, out var comp, out _))
{
- if (IsSessionEligible(session, antagPrototype, ignorePreferences))
- eligibleSessions.Add(session);
- }
+ if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
+ continue;
- return eligibleSessions;
+ if (comp.SelectionsComplete)
+ return;
+
+ ChooseAntags((uid, comp), pool);
+ comp.SelectionsComplete = true;
+
+ foreach (var session in comp.SelectedSessions)
+ {
+ args.PlayerPool.Remove(session);
+ GameTicker.PlayerJoinGame(session);
+ }
+ }
}
- /// <summary>
- /// Test eligibility of the player for a specific antag role
- /// </summary>
- /// <param name="session">The player session to test</param>
- /// <param name="antagPrototype">The prototype to get eligible players for</param>
- /// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
- /// <param name="acceptableAntags">Should players already selected as antags be eligible</param>
- /// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
- /// <param name="customExcludeCondition">A function, accepting an EntityUid and returning bool. Each player is tested against this, returning truw will exclude the player from eligibility</param>
- /// <returns>True if the player session matches the requirements, false otherwise</returns>
- public bool IsPlayerEligible(ICommonSession session,
- ProtoId<AntagPrototype> antagPrototype,
- bool includeAllJobs = false,
- AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
- bool ignorePreferences = false,
- bool allowNonHumanoids = false,
- Func<EntityUid?, bool>? customExcludeCondition = null)
+ private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
{
- if (!IsSessionEligible(session, antagPrototype, ignorePreferences))
- return false;
+ var query = QueryActiveRules();
+ while (query.MoveNext(out var uid, out _, out var comp, out _))
+ {
+ if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
+ continue;
- //Ensure the player has a mind
- if (session.GetMind() is not { } playerMind)
- return false;
+ if (comp.SelectionsComplete)
+ continue;
- //Ensure the player has an attached entity
- if (session.AttachedEntity is not { } playerEntity)
- return false;
+ ChooseAntags((uid, comp));
+ comp.SelectionsComplete = true;
+ }
+ }
- //Ignore latejoined players, ie those on the arrivals station
- if (HasComp<PendingClockInComponent>(playerEntity))
- return false;
+ private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
+ {
+ if (!args.LateJoin)
+ return;
- //Exclude jobs that cannot be antag, unless explicitly allowed
- if (!includeAllJobs && !_jobs.CanBeAntag(session))
- return false;
+ // TODO: this really doesn't handle multiple latejoin definitions well
+ // eventually this should probably store the players per definition with some kind of unique identifier.
+ // something to figure out later.
- //Check if the entity is already an antag
- switch (acceptableAntags)
+ var query = QueryActiveRules();
+ while (query.MoveNext(out var uid, out _, out var antag, out _))
{
- //If we dont want to select any antag roles
- case AntagAcceptability.None:
- {
- if (_roleSystem.MindIsAntagonist(playerMind))
- return false;
- break;
- }
- //If we dont want to select exclusive antag roles
- case AntagAcceptability.NotExclusive:
- {
- if (_roleSystem.MindIsExclusiveAntagonist(playerMind))
- return false;
- break;
- }
+ if (!RobustRandom.Prob(LateJoinRandomChance))
+ continue;
+
+ if (!antag.Definitions.Any(p => p.LateJoinAdditional))
+ continue;
+
+ if (!TryGetNextAvailableDefinition((uid, antag), out var def))
+ continue;
+
+ MakeAntag((uid, antag), args.Player, def.Value);
}
+ }
- //Unless explictly allowed, ignore non humanoids (eg pets)
- if (!allowNonHumanoids && !HasComp<HumanoidAppearanceComponent>(playerEntity))
- return false;
+ protected override void Added(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+ {
+ base.Added(uid, component, gameRule, args);
- //If a custom condition was provided, test it and exclude the player if it returns true
- if (customExcludeCondition != null && customExcludeCondition(playerEntity))
- return false;
+ for (var i = 0; i < component.Definitions.Count; i++)
+ {
+ var def = component.Definitions[i];
+ if (def.MinRange != null)
+ {
+ def.Min = def.MinRange.Value.Next(RobustRandom);
+ }
- return true;
+ if (def.MaxRange != null)
+ {
+ def.Max = def.MaxRange.Value.Next(RobustRandom);
+ }
+ }
}
- /// <summary>
- /// Check if the session is eligible for a role, can be run prior to the session being attached to an entity
- /// </summary>
- /// <param name="session">Player session to check</param>
- /// <param name="antagPrototype">Which antag prototype to check for</param>
- /// <param name="ignorePreferences">Ignore if the player has enabled this antag</param>
- /// <returns>True if the session matches the requirements, false otherwise</returns>
- public bool IsSessionEligible(ICommonSession session, ProtoId<AntagPrototype> antagPrototype, bool ignorePreferences = false)
+ protected override void Started(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
- //Exclude disconnected or zombie sessions
- //No point giving antag roles to them
- if (session.Status == SessionStatus.Disconnected ||
- session.Status == SessionStatus.Zombie)
- return false;
+ base.Started(uid, component, gameRule, args);
- //Check the player has this antag preference selected
- //Unless we are ignoring preferences, in which case add them anyway
- var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(session.UserId).SelectedCharacter;
- if (!pref.AntagPreferences.Contains(antagPrototype.Id) && !ignorePreferences)
- return false;
+ if (component.SelectionsComplete)
+ return;
- return true;
+ if (GameTicker.RunLevel != GameRunLevel.InRound)
+ return;
+
+ if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
+ return;
+
+ ChooseAntags((uid, component));
+ component.SelectionsComplete = true;
}
- #endregion
/// <summary>
- /// Helper method to calculate the number of antags to select based upon the number of players
+ /// Chooses antagonists from the current selection of players
/// </summary>
- /// <param name="playerCount">How many players there are on the server</param>
- /// <param name="playersPerAntag">How many players should there be for an additional antag</param>
- /// <param name="maxAntags">Maximum number of antags allowed</param>
- /// <returns>The number of antags that should be chosen</returns>
- public int CalculateAntagCount(int playerCount, int playersPerAntag, int maxAntags)
+ public void ChooseAntags(Entity<AntagSelectionComponent> ent)
{
- return Math.Clamp(playerCount / playersPerAntag, 1, maxAntags);
+ var sessions = _playerManager.Sessions.ToList();
+ ChooseAntags(ent, sessions);
}
- #region Antag Selection
/// <summary>
- /// Selects a set number of entities from several lists, prioritising the first list till its empty, then second list etc
+ /// Chooses antagonists from the given selection of players
/// </summary>
- /// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
- /// <param name="count">How many items to select</param>
- /// <returns>Up to the specified count of elements from all provided lists</returns>
- public List<EntityUid> ChooseAntags(int count, params List<EntityUid>[] eligiblePlayerLists)
+ public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool)
{
- var chosenPlayers = new List<EntityUid>();
- foreach (var playerList in eligiblePlayerLists)
+ foreach (var def in ent.Comp.Definitions)
{
- //Remove all chosen players from this list, to prevent duplicates
- foreach (var chosenPlayer in chosenPlayers)
- {
- playerList.Remove(chosenPlayer);
- }
-
- //If we have reached the desired number of players, skip
- if (chosenPlayers.Count >= count)
- continue;
-
- //Pick and choose a random number of players from this list
- chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
+ ChooseAntags(ent, pool, def);
}
- return chosenPlayers;
}
+
/// <summary>
- /// Helper method to choose antags from a list
+ /// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary>
- /// <param name="eligiblePlayers">List of eligible players</param>
- /// <param name="count">How many to choose</param>
- /// <returns>Up to the specified count of elements from the provided list</returns>
- public List<EntityUid> ChooseAntags(int count, List<EntityUid> eligiblePlayers)
+ public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool, AntagSelectionDefinition def)
{
- var chosenPlayers = new List<EntityUid>();
+ var playerPool = GetPlayerPool(ent, pool, def);
+ var count = GetTargetAntagCount(ent, playerPool, def);
for (var i = 0; i < count; i++)
{
- if (eligiblePlayers.Count == 0)
- break;
+ var session = (ICommonSession?) null;
+ if (def.PickPlayer)
+ {
+ if (!playerPool.TryPickAndTake(RobustRandom, out session))
+ break;
- chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
- }
+ if (ent.Comp.SelectedSessions.Contains(session))
+ continue;
+ }
- return chosenPlayers;
+ MakeAntag(ent, session, def);
+ }
}
/// <summary>
- /// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc
+ /// Makes a given player into the specified antagonist.
/// </summary>
- /// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
- /// <param name="count">How many items to select</param>
- /// <returns>Up to the specified count of elements from all provided lists</returns>
- public List<ICommonSession> ChooseAntags(int count, params List<ICommonSession>[] eligiblePlayerLists)
+ public void MakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
{
- var chosenPlayers = new List<ICommonSession>();
- foreach (var playerList in eligiblePlayerLists)
+ var antagEnt = (EntityUid?) null;
+ var isSpawner = false;
+
+ if (session != null)
+ {
+ ent.Comp.SelectedSessions.Add(session);
+
+ // we shouldn't be blocking the entity if they're just a ghost or smth.
+ if (!HasComp<GhostComponent>(session.AttachedEntity))
+ antagEnt = session.AttachedEntity;
+ }
+ else if (!ignoreSpawner && def.SpawnerPrototype != null) // don't add spawners if we have a player, dummy.
+ {
+ antagEnt = Spawn(def.SpawnerPrototype);
+ isSpawner = true;
+ }
+
+ if (!antagEnt.HasValue)
{
- //Remove all chosen players from this list, to prevent duplicates
- foreach (var chosenPlayer in chosenPlayers)
+ var getEntEv = new AntagSelectEntityEvent(session, ent);
+ RaiseLocalEvent(ent, ref getEntEv, true);
+
+ if (!getEntEv.Handled)
{
- playerList.Remove(chosenPlayer);
+ throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
}
- //If we have reached the desired number of players, skip
- if (chosenPlayers.Count >= count)
- continue;
+ antagEnt = getEntEv.Entity;
+ }
+
+ if (antagEnt is not { } player)
+ return;
- //Pick and choose a random number of players from this list
- chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
+ var getPosEv = new AntagSelectLocationEvent(session, ent);
+ RaiseLocalEvent(ent, ref getPosEv, true);
+ if (getPosEv.Handled)
+ {
+ var playerXform = Transform(player);
+ var pos = RobustRandom.Pick(getPosEv.Coordinates);
+ var mapEnt = _map.GetMap(pos.MapId);
+ _transform.SetMapCoordinates((player, playerXform), pos);
}
- return chosenPlayers;
- }
- /// <summary>
- /// Helper method to choose sessions from a list
- /// </summary>
- /// <param name="eligiblePlayers">List of eligible sessions</param>
- /// <param name="count">How many to choose</param>
- /// <returns>Up to the specified count of elements from the provided list</returns>
- public List<ICommonSession> ChooseAntags(int count, List<ICommonSession> eligiblePlayers)
- {
- var chosenPlayers = new List<ICommonSession>();
- for (int i = 0; i < count; i++)
+ if (isSpawner)
{
- if (eligiblePlayers.Count == 0)
- break;
+ if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
+ {
+ Log.Error("Antag spawner with GhostRoleAntagSpawnerComponent.");
+ return;
+ }
+
+ spawnerComp.Rule = ent;
+ spawnerComp.Definition = def;
+ return;
+ }
+
+ EntityManager.AddComponents(player, def.Components);
+ _stationSpawning.EquipStartingGear(player, def.StartingGear);
+
+ if (session != null)
+ {
+ var curMind = session.GetMind();
+ if (curMind == null)
+ {
+ curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value));
+ _mind.SetUserId(curMind.Value, session.UserId);
+ }
- chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
+ EntityManager.AddComponents(curMind.Value, def.MindComponents);
+ _mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
+ ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
+ }
+
+ if (def.Briefing is { } briefing)
+ {
+ SendBriefing(session, briefing);
}
- return chosenPlayers;
+ var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
+ RaiseLocalEvent(ent, ref afterEv, true);
}
- #endregion
- #region Briefings
/// <summary>
- /// Helper method to send the briefing text and sound to a list of entities
+ /// Gets an ordered player pool based on player preferences and the antagonist definition.
/// </summary>
- /// <param name="entities">The players chosen to be antags</param>
- /// <param name="briefing">The briefing text to send</param>
- /// <param name="briefingColor">The color the briefing should be, null for default</param>
- /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
- public void SendBriefing(List<EntityUid> entities, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+ public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
{
- foreach (var entity in entities)
+ var primaryList = new List<ICommonSession>();
+ var secondaryList = new List<ICommonSession>();
+ var fallbackList = new List<ICommonSession>();
+ var rawList = new List<ICommonSession>();
+ foreach (var session in sessions)
{
- SendBriefing(entity, briefing, briefingColor, briefingSound);
+ if (!IsSessionValid(ent, session, def) ||
+ !IsEntityValid(session.AttachedEntity, def))
+ {
+ rawList.Add(session);
+ continue;
+ }
+
+ var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
+ if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p)))
+ {
+ primaryList.Add(session);
+ }
+ else if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p)))
+ {
+ secondaryList.Add(session);
+ }
+ else
+ {
+ fallbackList.Add(session);
+ }
}
+
+ return new AntagSelectionPlayerPool(primaryList, secondaryList, fallbackList, rawList);
}
/// <summary>
- /// Helper method to send the briefing text and sound to a player entity
+ /// Checks if a given session is valid for an antagonist.
/// </summary>
- /// <param name="entity">The entity chosen to be antag</param>
- /// <param name="briefing">The briefing text to send</param>
- /// <param name="briefingColor">The color the briefing should be, null for default</param>
- /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
- public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+ public bool IsSessionValid(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def, EntityUid? mind = null)
{
- if (!_mindSystem.TryGetMind(entity, out _, out var mindComponent))
- return;
+ mind ??= session.GetMind();
- if (mindComponent.Session == null)
- return;
+ if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
+ return false;
- SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound);
- }
+ if (ent.Comp.SelectedSessions.Contains(session))
+ return false;
- /// <summary>
- /// Helper method to send the briefing text and sound to a list of sessions
- /// </summary>
- /// <param name="sessions"></param>
- /// <param name="briefing"></param>
- /// <param name="briefingColor"></param>
- /// <param name="briefingSound"></param>
+ //todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
- public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
- {
- foreach (var session in sessions)
+ switch (def.MultiAntagSetting)
{
- SendBriefing(session, briefing, briefingColor, briefingSound);
+ case AntagAcceptability.None:
+ {
+ if (_role.MindIsAntagonist(mind))
+ return false;
+ break;
+ }
+ case AntagAcceptability.NotExclusive:
+ {
+ if (_role.MindIsExclusiveAntagonist(mind))
+ return false;
+ break;
+ }
}
+
+ // todo: expand this to allow for more fine antag-selection logic for game rules.
+ if (!_jobs.CanBeAntag(session))
+ return false;
+
+ return true;
}
+
/// <summary>
- /// Helper method to send the briefing text and sound to a session
+ /// Checks if a given entity (mind/session not included) is valid for a given antagonist.
/// </summary>
- /// <param name="session">The player chosen to be an antag</param>
- /// <param name="briefing">The briefing text to send</param>
- /// <param name="briefingColor">The color the briefing should be, null for default</param>
- /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
-
- public void SendBriefing(ICommonSession session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+ private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
{
- _audioSystem.PlayGlobal(briefingSound, session);
- var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
- ChatManager.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor);
+ if (entity == null)
+ return false;
+
+ if (HasComp<PendingClockInComponent>(entity))
+ return false;
+
+ if (!def.AllowNonHumans && !HasComp<HumanoidAppearanceComponent>(entity))
+ return false;
+
+ if (def.Whitelist != null)
+ {
+ if (!def.Whitelist.IsValid(entity.Value, EntityManager))
+ return false;
+ }
+
+ if (def.Blacklist != null)
+ {
+ if (def.Blacklist.IsValid(entity.Value, EntityManager))
+ return false;
+ }
+
+ return true;
}
- #endregion
}
+
+/// <summary>
+/// Event raised on a game rule entity in order to determine what the antagonist entity will be.
+/// Only raised if the selected player's current entity is invalid.
+/// </summary>
+[ByRefEvent]
+public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
+{
+ public readonly ICommonSession? Session = Session;
+
+ public bool Handled => Entity != null;
+
+ public EntityUid? Entity;
+}
+
+/// <summary>
+/// Event raised on a game rule entity to determine the location for the antagonist.
+/// </summary>
+[ByRefEvent]
+public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
+{
+ public readonly ICommonSession? Session = Session;
+
+ public bool Handled => Coordinates.Any();
+
+ public List<MapCoordinates> Coordinates = new();
+}
+
+/// <summary>
+/// Event raised on a game rule entity after the setup logic for an antag is complete.
+/// Used for applying additional more complex setup logic.
+/// </summary>
+[ByRefEvent]
+public readonly record struct AfterAntagEntitySelectedEvent(ICommonSession? Session, EntityUid EntityUid, Entity<AntagSelectionComponent> GameRule, AntagSelectionDefinition Def);
--- /dev/null
+using Content.Server.Administration.Systems;
+using Content.Server.Destructible.Thresholds;
+using Content.Shared.Antag;
+using Content.Shared.Roles;
+using Content.Shared.Storage;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Antag.Components;
+
+[RegisterComponent, Access(typeof(AntagSelectionSystem), typeof(AdminVerbSystem))]
+public sealed partial class AntagSelectionComponent : Component
+{
+ /// <summary>
+ /// Has the primary selection of antagonists finished yet?
+ /// </summary>
+ [DataField]
+ public bool SelectionsComplete;
+
+ /// <summary>
+ /// The definitions for the antagonists
+ /// </summary>
+ [DataField]
+ public List<AntagSelectionDefinition> Definitions = new();
+
+ /// <summary>
+ /// The minds and original names of the players selected to be antagonists.
+ /// </summary>
+ [DataField]
+ public List<(EntityUid, string)> SelectedMinds = new();
+
+ /// <summary>
+ /// When the antag selection will occur.
+ /// </summary>
+ [DataField]
+ public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
+
+ /// <summary>
+ /// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick.
+ /// Is not serialized.
+ /// </summary>
+ public HashSet<ICommonSession> SelectedSessions = new();
+}
+
+[DataDefinition]
+public partial struct AntagSelectionDefinition()
+{
+ /// <summary>
+ /// A list of antagonist roles that are used for selecting which players will be antagonists.
+ /// </summary>
+ [DataField]
+ public List<ProtoId<AntagPrototype>> PrefRoles = new();
+
+ /// <summary>
+ /// Fallback for <see cref="PrefRoles"/>. Useful if you need multiple role preferences for a team antagonist.
+ /// </summary>
+ [DataField]
+ public List<ProtoId<AntagPrototype>> FallbackRoles = new();
+
+ /// <summary>
+ /// Should we allow people who already have an antagonist role?
+ /// </summary>
+ [DataField]
+ public AntagAcceptability MultiAntagSetting = AntagAcceptability.None;
+
+ /// <summary>
+ /// The minimum number of this antag.
+ /// </summary>
+ [DataField]
+ public int Min = 1;
+
+ /// <summary>
+ /// The maximum number of this antag.
+ /// </summary>
+ [DataField]
+ public int Max = 1;
+
+ /// <summary>
+ /// A range used to randomly select <see cref="Min"/>
+ /// </summary>
+ [DataField]
+ public MinMax? MinRange;
+
+ /// <summary>
+ /// A range used to randomly select <see cref="Max"/>
+ /// </summary>
+ [DataField]
+ public MinMax? MaxRange;
+
+ /// <summary>
+ /// a player to antag ratio: used to determine the amount of antags that will be present.
+ /// </summary>
+ [DataField]
+ public int PlayerRatio = 10;
+
+ /// <summary>
+ /// Whether or not players should be picked to inhabit this antag or not.
+ /// </summary>
+ [DataField]
+ public bool PickPlayer = true;
+
+ /// <summary>
+ /// If true, players that latejoin into a round have a chance of being converted into antagonists.
+ /// </summary>
+ [DataField]
+ public bool LateJoinAdditional = false;
+
+ //todo: find out how to do this with minimal boilerplate: filler department, maybe?
+ //public HashSet<ProtoId<JobPrototype>> JobBlacklist = new()
+
+ /// <remarks>
+ /// Mostly just here for legacy compatibility and reducing boilerplate
+ /// </remarks>
+ [DataField]
+ public bool AllowNonHumans = false;
+
+ /// <summary>
+ /// A whitelist for selecting which players can become this antag.
+ /// </summary>
+ [DataField]
+ public EntityWhitelist? Whitelist;
+
+ /// <summary>
+ /// A blacklist for selecting which players can become this antag.
+ /// </summary>
+ [DataField]
+ public EntityWhitelist? Blacklist;
+
+ /// <summary>
+ /// Components added to the player.
+ /// </summary>
+ [DataField]
+ public ComponentRegistry Components = new();
+
+ /// <summary>
+ /// Components added to the player's mind.
+ /// </summary>
+ [DataField]
+ public ComponentRegistry MindComponents = new();
+
+ /// <summary>
+ /// A set of starting gear that's equipped to the player.
+ /// </summary>
+ [DataField]
+ public ProtoId<StartingGearPrototype>? StartingGear;
+
+ /// <summary>
+ /// A briefing shown to the player.
+ /// </summary>
+ [DataField]
+ public BriefingData? Briefing;
+
+ /// <summary>
+ /// A spawner used to defer the selection of this particular definition.
+ /// </summary>
+ /// <remarks>
+ /// Not the cleanest way of doing this code but it's just an odd specific behavior.
+ /// Sue me.
+ /// </remarks>
+ [DataField]
+ public EntProtoId? SpawnerPrototype;
+}
+
+/// <summary>
+/// Contains data used to generate a briefing.
+/// </summary>
+[DataDefinition]
+public partial struct BriefingData
+{
+ /// <summary>
+ /// The text shown
+ /// </summary>
+ [DataField]
+ public LocId? Text;
+
+ /// <summary>
+ /// The color of the text.
+ /// </summary>
+ [DataField]
+ public Color? Color;
+
+ /// <summary>
+ /// The sound played.
+ /// </summary>
+ [DataField]
+ public SoundSpecifier? Sound;
+}
--- /dev/null
+namespace Content.Server.Antag.Components;
+
+/// <summary>
+/// Ghost role spawner that creates an antag for the associated gamerule.
+/// </summary>
+[RegisterComponent, Access(typeof(AntagSelectionSystem))]
+public sealed partial class GhostRoleAntagSpawnerComponent : Component
+{
+ [DataField]
+ public EntityUid? Rule;
+
+ [DataField]
+ public AntagSelectionDefinition? Definition;
+}
using Content.Server.Antag.Mimic;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.VendingMachines;
-namespace Content.Server.Destructible.Thresholds
+using Robust.Shared.Random;
+
+namespace Content.Server.Destructible.Thresholds
{
[Serializable]
[DataDefinition]
[DataField("max")]
public int Max;
+
+ public MinMax(int min, int max)
+ {
+ Min = min;
+ Max = max;
+ }
+
+ public int Next(IRobustRandom random)
+ {
+ return random.Next(Min, Max + 1);
+ }
}
}
-namespace Content.Server.GameTicking.Rules.Components;
+namespace Content.Server.GameTicking.Components;
/// <summary>
/// Added to game rules before <see cref="GameRuleStartedEvent"/> and removed before <see cref="GameRuleEndedEvent"/>.
--- /dev/null
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.GameTicking.Components;
+
+/// <summary>
+/// Generic component used to track a gamerule that's start has been delayed.
+/// </summary>
+[RegisterComponent, AutoGenerateComponentPause]
+public sealed partial class DelayedStartRuleComponent : Component
+{
+ /// <summary>
+ /// The time at which the rule will start properly.
+ /// </summary>
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+ public TimeSpan RuleStartTime;
+}
-namespace Content.Server.GameTicking.Rules.Components;
+namespace Content.Server.GameTicking.Components;
/// <summary>
/// Added to game rules before <see cref="GameRuleEndedEvent"/>.
+using Content.Server.Destructible.Thresholds;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-namespace Content.Server.GameTicking.Rules.Components;
+namespace Content.Server.GameTicking.Components;
/// <summary>
/// Component attached to all gamerule entities.
/// </summary>
[DataField]
public int MinPlayers;
+
+ /// <summary>
+ /// A delay for when the rule the is started and when the starting logic actually runs.
+ /// </summary>
+ [DataField]
+ public MinMax? Delay;
}
/// <summary>
using System.Linq;
using Content.Server.Administration;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Prototypes;
if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
return false;
+ // If we already have it, then we just skip the delay as it has already happened.
+ if (!RemComp<DelayedStartRuleComponent>(ruleEntity) && ruleData.Delay != null)
+ {
+ var delayTime = TimeSpan.FromSeconds(ruleData.Delay.Value.Next(_robustRandom));
+
+ if (delayTime > TimeSpan.Zero)
+ {
+ _sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
+ _adminLogger.Add(LogType.EventStarted, $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
+
+ var delayed = EnsureComp<DelayedStartRuleComponent>(ruleEntity);
+ delayed.RuleStartTime = _gameTiming.CurTime + (delayTime);
+ return true;
+ }
+ }
+
_allPreviousGameRules.Add((RoundDuration(), id));
_sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
_adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}");
}
}
+ private void UpdateGameRules()
+ {
+ var query = EntityQueryEnumerator<DelayedStartRuleComponent, GameRuleComponent>();
+ while (query.MoveNext(out var uid, out var delay, out var rule))
+ {
+ if (_gameTiming.CurTime < delay.RuleStartTime)
+ continue;
+
+ StartGameRule(uid, rule);
+ }
+ }
+
#region Command Implementations
[AdminCommand(AdminFlags.Fun)]
#endregion
}
-
-/*
-/// <summary>
-/// Raised broadcast when a game rule is selected, but not started yet.
-/// </summary>
-public sealed class GameRuleAddedEvent
-{
- public GameRulePrototype Rule { get; }
-
- public GameRuleAddedEvent(GameRulePrototype rule)
- {
- Rule = rule;
- }
-}
-
-public sealed class GameRuleStartedEvent
-{
- public GameRulePrototype Rule { get; }
-
- public GameRuleStartedEvent(GameRulePrototype rule)
- {
- Rule = rule;
- }
-}
-
-public sealed class GameRuleEndedEvent
-{
- public GameRulePrototype Rule { get; }
-
- public GameRuleEndedEvent(GameRulePrototype rule)
- {
- Rule = rule;
- }
-}
-*/
return;
base.Update(frameTime);
UpdateRoundFlow(frameTime);
+ UpdateGameRules();
}
}
}
--- /dev/null
+using Content.Server.Maps;
+using Content.Shared.Whitelist;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Server.GameTicking.Rules.Components;
+
+/// <summary>
+/// This is used for a game rule that loads a map when activated.
+/// </summary>
+[RegisterComponent]
+public sealed partial class LoadMapRuleComponent : Component
+{
+ [DataField]
+ public MapId? Map;
+
+ [DataField]
+ public ProtoId<GameMapPrototype>? GameMap ;
+
+ [DataField]
+ public ResPath? MapPath;
+
+ [DataField]
+ public List<EntityUid> MapGrids = new();
+
+ [DataField]
+ public EntityWhitelist? SpawnerWhitelist;
+}
/// <summary>
/// Stores some configuration used by the ninja system.
-/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent/">.
+/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent"/>.
/// </summary>
[RegisterComponent, Access(typeof(SpaceNinjaSystem))]
public sealed partial class NinjaRuleComponent : Component
-using Content.Shared.Roles;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// TODO: Remove once systems can request spawns from the ghost role system directly.
/// </summary>
[RegisterComponent]
-public sealed partial class NukeOperativeSpawnerComponent : Component
-{
- [DataField("name", required:true)]
- public string OperativeName = default!;
+public sealed partial class NukeOperativeSpawnerComponent : Component;
- [DataField]
- public NukeopSpawnPreset SpawnDetails = default!;
-}
[RegisterComponent]
public sealed partial class NukeOpsShuttleComponent : Component
{
+ [DataField]
+ public EntityUid AssociatedRule;
}
-using Content.Server.Maps;
using Content.Server.RoundEnd;
-using Content.Server.StationEvents.Events;
using Content.Shared.Dataset;
using Content.Shared.NPC.Prototypes;
using Content.Shared.Roles;
-using Robust.Shared.Map;
+using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.GameTicking.Rules.Components;
-[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
+[RegisterComponent, Access(typeof(NukeopsRuleSystem))]
public sealed partial class NukeopsRuleComponent : Component
{
- /// <summary>
- /// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
- /// </summary>
- [DataField]
- public int PlayersPerOperative = 10;
-
- [DataField]
- public int MaxOps = 5;
-
/// <summary>
/// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
/// </summary>
[DataField]
public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(3);
- /// <summary>
- /// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event.
- /// </summary>
- [DataField]
- public bool SpawnOutpost = true;
-
/// <summary>
/// Whether or not nukie left their outpost
/// </summary>
/// This amount of TC will be given to each nukie
/// </summary>
[DataField]
- public int WarTCAmountPerNukie = 40;
+ public int WarTcAmountPerNukie = 40;
/// <summary>
/// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare
[DataField]
public int WarDeclarationMinOps = 4;
- [DataField]
- public EntProtoId SpawnPointProto = "SpawnPointNukies";
-
- [DataField]
- public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
-
- [DataField]
- public string OperationName = "Test Operation";
-
- [DataField]
- public ProtoId<GameMapPrototype> OutpostMapPrototype = "NukieOutpost";
-
[DataField]
public WinType WinType = WinType.Neutral;
[DataField]
public List<WinCondition> WinConditions = new ();
- // TODO full game save
- // 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.
- // Whenever this gets fixed, update NukiesTest.
- public EntityUid? NukieOutpost;
- public EntityUid? NukieShuttle;
- public EntityUid? TargetStation;
- public MapId? NukiePlanet;
-
- /// <summary>
- /// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
- /// </summary>
- [DataField]
- public Dictionary<EntityUid, string> OperativeMindPendingData = new();
-
- [DataField(required: true)]
- public ProtoId<NpcFactionPrototype> Faction;
-
[DataField]
- public NukeopSpawnPreset CommanderSpawnDetails = new() { AntagRoleProto = "NukeopsCommander", GearProto = "SyndicateCommanderGearFull", NamePrefix = "nukeops-role-commander", NameList = "SyndicateNamesElite" };
+ public EntityUid? TargetStation;
[DataField]
- public NukeopSpawnPreset AgentSpawnDetails = new() { AntagRoleProto = "NukeopsMedic", GearProto = "SyndicateOperativeMedicFull", NamePrefix = "nukeops-role-agent", NameList = "SyndicateNamesNormal" };
+ public ProtoId<NpcFactionPrototype> Faction = "Syndicate";
+ /// <summary>
+ /// Path to antagonist alert sound.
+ /// </summary>
[DataField]
- public NukeopSpawnPreset OperativeSpawnDetails = new();
+ public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
}
/// <summary>
+++ /dev/null
-using Robust.Shared.Audio;
-
-namespace Content.Server.GameTicking.Rules.Components;
-
-[RegisterComponent, Access(typeof(PiratesRuleSystem))]
-public sealed partial class PiratesRuleComponent : Component
-{
- [ViewVariables]
- public List<EntityUid> Pirates = new();
- [ViewVariables]
- public EntityUid PirateShip = EntityUid.Invalid;
- [ViewVariables]
- public HashSet<EntityUid> InitialItems = new();
- [ViewVariables]
- public double InitialShipValue;
-
- /// <summary>
- /// Path to antagonist alert sound.
- /// </summary>
- [DataField("pirateAlertSound")]
- public SoundSpecifier PirateAlertSound = new SoundPathSpecifier(
- "/Audio/Ambience/Antag/pirate_start.ogg",
- AudioParams.Default.WithVolume(4));
-}
[DataField]
public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
- /// <summary>
- /// Stores players minds
- /// </summary>
- [DataField]
- public Dictionary<string, EntityUid> HeadRevs = new();
-
- [DataField]
- public ProtoId<AntagPrototype> HeadRevPrototypeId = "HeadRev";
-
- /// <summary>
- /// Min players needed for Revolutionary gamemode to start.
- /// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
- public int MinPlayers = 15;
-
- /// <summary>
- /// Max Head Revs allowed during selection.
- /// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
- public int MaxHeadRevs = 3;
-
- /// <summary>
- /// The amount of Head Revs that will spawn per this amount of players.
- /// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
- public int PlayersPerHeadRev = 15;
-
- /// <summary>
- /// The gear head revolutionaries are given on spawn.
- /// </summary>
- [DataField]
- public List<EntProtoId> StartingGear = new()
- {
- "Flash",
- "ClothingEyesGlassesSunglasses"
- };
-
/// <summary>
/// The time it takes after the last head is killed for the shuttle to arrive.
/// </summary>
using Content.Shared.Random;
-using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
-/// Stores data for <see cref="ThiefRuleSystem/">.
+/// Stores data for <see cref="ThiefRuleSystem"/>.
/// </summary>
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
public sealed partial class ThiefRuleComponent : Component
[DataField]
public float BigObjectiveChance = 0.7f;
- /// <summary>
- /// Add a Pacified comp to thieves
- /// </summary>
- [DataField]
- public bool PacifistThieves = true;
-
- [DataField]
- public ProtoId<AntagPrototype> ThiefPrototypeId = "Thief";
-
[DataField]
public float MaxObjectiveDifficulty = 2.5f;
[DataField]
public int MaxStealObjectives = 10;
-
- /// <summary>
- /// Things that will be given to thieves
- /// </summary>
- [DataField]
- public List<EntProtoId> StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" };
-
- /// <summary>
- /// All Thieves created by this rule
- /// </summary>
- [DataField]
- public List<EntityUid> ThievesMinds = new();
-
- /// <summary>
- /// Max Thiefs created by rule on roundstart
- /// </summary>
- [DataField]
- public int MaxAllowThief = 3;
-
- /// <summary>
- /// Sound played when making the player a thief via antag control or ghost role
- /// </summary>
- [DataField]
- public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/thief_greeting.ogg");
}
/// </summary>
[DataField]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
+
+ /// <summary>
+ /// The amount of codewords that are selected.
+ /// </summary>
+ [DataField]
+ public int CodewordCount = 4;
+
+ /// <summary>
+ /// The amount of TC traitors start with.
+ /// </summary>
+ [DataField]
+ public int StartingBalance = 20;
+
+ [DataField]
+ public int MaxDifficulty = 20;
}
[RegisterComponent, Access(typeof(ZombieRuleSystem))]
public sealed partial class ZombieRuleComponent : Component
{
- [DataField]
- public Dictionary<string, string> InitialInfectedNames = new();
-
- [DataField]
- public ProtoId<AntagPrototype> PatientZeroPrototypeId = "InitialInfected";
-
/// <summary>
/// When the round will next check for round end.
/// </summary>
[DataField]
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
- /// <summary>
- /// The time at which the initial infected will be chosen.
- /// </summary>
- [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan? StartTime;
-
- /// <summary>
- /// The minimum amount of time after the round starts that the initial infected will be chosen.
- /// </summary>
- [DataField]
- public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10);
-
- /// <summary>
- /// The maximum amount of time after the round starts that the initial infected will be chosen.
- /// </summary>
- [DataField]
- public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15);
-
- /// <summary>
- /// The sound that plays when someone becomes an initial infected.
- /// todo: this should have a unique sound instead of reusing the zombie one.
- /// </summary>
- [DataField]
- public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
-
- /// <summary>
- /// The minimum amount of time initial infected have before they start taking infection damage.
- /// </summary>
- [DataField]
- public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
-
- /// <summary>
- /// The maximum amount of time initial infected have before they start taking damage.
- /// </summary>
- [DataField]
- public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
-
- /// <summary>
- /// How many players for each initial infected.
- /// </summary>
- [DataField]
- public int PlayersPerInfected = 10;
-
- /// <summary>
- /// The maximum number of initial infected.
- /// </summary>
- [DataField]
- public int MaxInitialInfected = 6;
-
/// <summary>
/// After this amount of the crew become zombies, the shuttle will be automatically called.
/// </summary>
[DataField]
public float ZombieShuttleCallPercentage = 0.7f;
-
- [DataField]
- public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
}
using System.Linq;
using Content.Server.Administration.Commands;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Server.Mind;
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
SubscribeLocalEvent<DeathMatchRuleComponent, PlayerPointChangedEvent>(OnPointChanged);
- SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
}
private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
_roundEnd.EndRound(component.RestartDelay);
}
- private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev)
+ protected override void AppendRoundEndText(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
{
- var query = EntityQueryEnumerator<DeathMatchRuleComponent, PointManagerComponent, GameRuleComponent>();
- while (query.MoveNext(out var uid, out var dm, out var point, out var rule))
- {
- if (!GameTicker.IsGameRuleAdded(uid, rule))
- continue;
+ if (!TryComp<PointManagerComponent>(uid, out var point))
+ return;
- if (dm.Victor != null && _player.TryGetPlayerData(dm.Victor.Value, out var data))
- {
- ev.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
- ev.AddLine("");
- }
- ev.AddLine(Loc.GetString("point-scoreboard-header"));
- ev.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
+ if (component.Victor != null && _player.TryGetPlayerData(component.Victor.Value, out var data))
+ {
+ args.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
+ args.AddLine("");
}
+ args.AddLine(Loc.GetString("point-scoreboard-header"));
+ args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
}
}
using System.Diagnostics.CodeAnalysis;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Robust.Shared.Collections;
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;
- }
-
/// <summary>
/// Utility function for finding a random event-eligible station entity
/// </summary>
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using Robust.Shared.Timing;
{
base.Initialize();
+ SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
+ SubscribeLocalEvent<T, RoundEndTextAppendEvent>(OnRoundEndTextAppend);
+ }
+
+ private void OnStartAttempt(RoundStartAttemptEvent args)
+ {
+ if (args.Forced || args.Cancelled)
+ return;
+
+ var query = QueryActiveRules();
+ while (query.MoveNext(out var uid, out _, out _, out var gameRule))
+ {
+ var minPlayers = gameRule.MinPlayers;
+ if (args.Players.Length >= minPlayers)
+ continue;
+
+ ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
+ ("readyPlayersCount", args.Players.Length),
+ ("minimumPlayers", minPlayers),
+ ("presetName", ToPrettyString(uid))));
+ args.Cancel();
+ }
}
private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args)
Ended(uid, component, ruleData, args);
}
+ private void OnRoundEndTextAppend(Entity<T> ent, ref RoundEndTextAppendEvent args)
+ {
+ if (!TryComp<GameRuleComponent>(ent, out var ruleData))
+ return;
+ AppendRoundEndText(ent, ent, ruleData, ref args);
+ }
/// <summary>
/// Called when the gamerule is added
}
+ /// <summary>
+ /// Called at the end of a round when text needs to be added for a game rule.
+ /// </summary>
+ protected virtual void AppendRoundEndText(EntityUid uid, T component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
+ {
+
+ }
+
/// <summary>
/// Called on an active gamerule entity in the Update function
/// </summary>
using System.Threading;
using Content.Server.Chat.Managers;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Robust.Server.Player;
using Robust.Shared.Player;
using Content.Server.Chat.Managers;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Shared.Chat;
--- /dev/null
+using Content.Server.Antag;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Spawners.Components;
+using Robust.Server.GameObjects;
+using Robust.Server.Maps;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.GameTicking.Rules;
+
+public sealed class LoadMapRuleSystem : GameRuleSystem<LoadMapRuleComponent>
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly MapSystem _map = default!;
+ [Dependency] private readonly MapLoaderSystem _mapLoader = default!;
+ [Dependency] private readonly TransformSystem _transform = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<LoadMapRuleComponent, AntagSelectLocationEvent>(OnSelectLocation);
+ SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
+ }
+
+ private void OnGridSplit(ref GridSplitEvent args)
+ {
+ var rule = QueryActiveRules();
+ while (rule.MoveNext(out _, out var mapComp, out _))
+ {
+ if (!mapComp.MapGrids.Contains(args.Grid))
+ continue;
+
+ mapComp.MapGrids.AddRange(args.NewGrids);
+ break;
+ }
+ }
+
+ protected override void Added(EntityUid uid, LoadMapRuleComponent comp, GameRuleComponent rule, GameRuleAddedEvent args)
+ {
+ if (comp.Map != null)
+ return;
+
+ _map.CreateMap(out var mapId);
+ comp.Map = mapId;
+
+ if (comp.GameMap != null)
+ {
+ var gameMap = _prototypeManager.Index(comp.GameMap.Value);
+ comp.MapGrids.AddRange(GameTicker.LoadGameMap(gameMap, comp.Map.Value, new MapLoadOptions()));
+ }
+ else if (comp.MapPath != null)
+ {
+ if (_mapLoader.TryLoad(comp.Map.Value, comp.MapPath.Value.ToString(), out var roots, new MapLoadOptions { LoadMap = true }))
+ comp.MapGrids.AddRange(roots);
+ }
+ else
+ {
+ Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}");
+ }
+ }
+
+ private void OnSelectLocation(Entity<LoadMapRuleComponent> ent, ref AntagSelectLocationEvent args)
+ {
+ var query = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
+ while (query.MoveNext(out var uid, out _, out var xform))
+ {
+ if (xform.MapID != ent.Comp.Map)
+ continue;
+
+ if (xform.GridUid == null || !ent.Comp.MapGrids.Contains(xform.GridUid.Value))
+ continue;
+
+ if (ent.Comp.SpawnerWhitelist != null && !ent.Comp.SpawnerWhitelist.IsValid(uid, EntityManager))
+ continue;
+
+ args.Coordinates.Add(_transform.GetMapCoordinates(xform));
+ }
+ }
+}
using System.Threading;
using Content.Server.Chat.Managers;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Timer = Robust.Shared.Timing.Timer;
-using Content.Server.Administration.Commands;
-using Content.Server.Administration.Managers;
using Content.Server.Antag;
using Content.Server.Communications;
using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Ghost.Roles.Components;
-using Content.Server.Ghost.Roles.Events;
using Content.Server.Humanoid;
-using Content.Server.Mind;
using Content.Server.Nuke;
using Content.Server.NukeOps;
using Content.Server.Popups;
using Content.Server.Preferences.Managers;
-using Content.Server.RandomMetadata;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Events;
using Content.Server.Shuttles.Systems;
-using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
-using Content.Server.Station.Systems;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
-using Content.Shared.CCVar;
-using Content.Shared.Dataset;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.NPC.Components;
using Content.Shared.Nuke;
using Content.Shared.NukeOps;
using Content.Shared.Preferences;
-using Content.Shared.Roles;
using Content.Shared.Store;
using Content.Shared.Tag;
using Content.Shared.Zombies;
-using Robust.Server.Player;
-using Robust.Shared.Configuration;
using Robust.Shared.Map;
-using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
+using Content.Server.GameTicking.Components;
namespace Content.Server.GameTicking.Rules;
public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
- [Dependency] private readonly IAdminManager _adminManager = default!;
- [Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
- [Dependency] private readonly MetaDataSystem _metaData = default!;
- [Dependency] private readonly RandomMetadataSystem _randomMetadata = default!;
- [Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
- [Dependency] private readonly SharedRoleSystem _roles = default!;
- [Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly StoreSystem _store = default!;
[Dependency] private readonly TagSystem _tag = default!;
- [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
-
- private ISawmill _sawmill = default!;
[ValidatePrototypeId<CurrencyPrototype>]
private const string TelecrystalCurrencyPrototype = "Telecrystal";
[ValidatePrototypeId<TagPrototype>]
private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink";
- [ValidatePrototypeId<AntagPrototype>]
- public const string NukeopsId = "Nukeops";
-
- [ValidatePrototypeId<DatasetPrototype>]
- private const string OperationPrefixDataset = "operationPrefix";
-
- [ValidatePrototypeId<DatasetPrototype>]
- private const string OperationSuffixDataset = "operationSuffix";
-
public override void Initialize()
{
base.Initialize();
- _sawmill = _logManager.GetSawmill("NukeOps");
-
- SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
- SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayersSpawning);
- SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<NukeExplodedEvent>(OnNukeExploded);
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
SubscribeLocalEvent<NukeDisarmSuccessEvent>(OnNukeDisarm);
SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
- SubscribeLocalEvent<NukeOperativeComponent, GhostRoleSpawnerUsedEvent>(OnPlayersGhostSpawning);
- SubscribeLocalEvent<NukeOperativeComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
+ SubscribeLocalEvent<NukeOpsShuttleComponent, MapInitEvent>(OnMapInit);
+
SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
+
+ SubscribeLocalEvent<NukeopsRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
+ SubscribeLocalEvent<NukeopsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected);
}
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
- base.Started(uid, component, gameRule, args);
-
- if (GameTicker.RunLevel == GameRunLevel.InRound)
- SpawnOperativesForGhostRoles(uid, component);
- }
-
- #region Event Handlers
-
- private void OnStartAttempt(RoundStartAttemptEvent ev)
- {
- TryRoundStartAttempt(ev, Loc.GetString("nukeops-title"));
- }
-
- private void OnPlayersSpawning(RulePlayerSpawningEvent ev)
- {
- var query = QueryActiveRules();
- while (query.MoveNext(out var uid, out _, out var nukeops, out _))
+ var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
+ var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
+ while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member))
{
- if (!SpawnMap((uid, nukeops)))
- {
- _sawmill.Info("Failed to load map for nukeops");
- continue;
- }
-
- //Handle there being nobody readied up
- if (ev.PlayerPool.Count == 0)
+ if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member)))
continue;
- var commanderEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.CommanderSpawnDetails.AntagRoleProto);
- var agentEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.AgentSpawnDetails.AntagRoleProto);
- var operativeEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.OperativeSpawnDetails.AntagRoleProto);
- //Calculate how large the nukeops team needs to be
- var nukiesToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, nukeops.PlayersPerOperative, nukeops.MaxOps);
-
- //Select Nukies
- //Select Commander, priority : commanderEligible, agentEligible, operativeEligible, all players
- var selectedCommander = _antagSelection.ChooseAntags(1, commanderEligible, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault();
- //Select Agent, priority : agentEligible, operativeEligible, all players
- var selectedAgent = _antagSelection.ChooseAntags(1, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault();
- //Select Operatives, priority : operativeEligible, all players
- var selectedOperatives = _antagSelection.ChooseAntags(nukiesToSelect - 2, operativeEligible, ev.PlayerPool);
-
- //Create the team!
- //If the session is null, they will be spawned as ghost roles (provided the cvar is set)
- var operatives = new List<NukieSpawn> { new NukieSpawn(selectedCommander, nukeops.CommanderSpawnDetails) };
- if (nukiesToSelect > 1)
- operatives.Add(new NukieSpawn(selectedAgent, nukeops.AgentSpawnDetails));
-
- for (var i = 0; i < nukiesToSelect - 2; i++)
- {
- //Use up all available sessions first, then spawn the rest as ghost roles (if enabled)
- if (selectedOperatives.Count > i)
- {
- operatives.Add(new NukieSpawn(selectedOperatives[i], nukeops.OperativeSpawnDetails));
- }
- else
- {
- operatives.Add(new NukieSpawn(null, nukeops.OperativeSpawnDetails));
- }
- }
-
- SpawnOperatives(operatives, _cfg.GetCVar(CCVars.NukeopsSpawnGhostRoles), nukeops);
+ eligible.Add((eligibleUid, eligibleComp, member));
+ }
- foreach (var nukieSpawn in operatives)
- {
- if (nukieSpawn.Session == null)
- continue;
+ if (eligible.Count == 0)
+ return;
- GameTicker.PlayerJoinGame(nukieSpawn.Session);
- }
- }
+ component.TargetStation = RobustRandom.Pick(eligible);
}
- private void OnRoundEndText(RoundEndTextAppendEvent ev)
+ #region Event Handlers
+ protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
+ ref RoundEndTextAppendEvent args)
{
- var ruleQuery = QueryActiveRules();
- while (ruleQuery.MoveNext(out _, out _, out var nukeops, out _))
- {
- var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}");
- ev.AddLine(winText);
+ var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
+ args.AddLine(winText);
- foreach (var cond in nukeops.WinConditions)
- {
- var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
- ev.AddLine(text);
- }
+ foreach (var cond in component.WinConditions)
+ {
+ var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
+ args.AddLine(text);
}
- ev.AddLine(Loc.GetString("nukeops-list-start"));
+ args.AddLine(Loc.GetString("nukeops-list-start"));
- var nukiesQuery = EntityQueryEnumerator<NukeopsRoleComponent, MindContainerComponent>();
- while (nukiesQuery.MoveNext(out var nukeopsUid, out _, out var mindContainer))
- {
- if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
- continue;
+ var antags =_antag.GetAntagIdentifiers(uid);
- ev.AddLine(mind.Session != null
- ? Loc.GetString("nukeops-list-name-user", ("name", Name(nukeopsUid)), ("user", mind.Session.Name))
- : Loc.GetString("nukeops-list-name", ("name", Name(nukeopsUid))));
+ foreach (var (_, sessionData, name) in antags)
+ {
+ args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName)));
}
}
{
if (ev.OwningStation != null)
{
- if (ev.OwningStation == nukeops.NukieOutpost)
+ if (ev.OwningStation == GetOutpost(uid))
{
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
- SetWinType(uid, WinType.CrewMajor, nukeops);
+ SetWinType((uid, nukeops), WinType.CrewMajor);
continue;
}
}
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
- SetWinType(uid, WinType.OpsMajor, nukeops);
+ SetWinType((uid, nukeops), WinType.OpsMajor);
correctStation = true;
}
private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
{
+ if (ev.New is not GameRunLevel.PostRound)
+ return;
+
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
{
- switch (ev.New)
+ OnRoundEnd((uid, nukeops));
+ }
+ }
+
+ private void OnRoundEnd(Entity<NukeopsRuleComponent> ent)
+ {
+ // If the win condition was set to operative/crew major win, ignore.
+ if (ent.Comp.WinType == WinType.OpsMajor || ent.Comp.WinType == WinType.CrewMajor)
+ return;
+
+ var nukeQuery = AllEntityQuery<NukeComponent, TransformComponent>();
+ var centcomms = _emergency.GetCentcommMaps();
+
+ while (nukeQuery.MoveNext(out var nuke, out var nukeTransform))
+ {
+ if (nuke.Status != NukeStatus.ARMED)
+ continue;
+
+ // UH OH
+ if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value))
{
- case GameRunLevel.InRound:
- OnRoundStart(uid, nukeops);
- break;
- case GameRunLevel.PostRound:
- OnRoundEnd(uid, nukeops);
- break;
+ ent.Comp.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
+ SetWinType((ent, ent), WinType.OpsMajor);
+ return;
+ }
+
+ if (nukeTransform.GridUid == null || ent.Comp.TargetStation == null)
+ continue;
+
+ if (!TryComp(ent.Comp.TargetStation.Value, out StationDataComponent? data))
+ continue;
+
+ foreach (var grid in data.Grids)
+ {
+ if (grid != nukeTransform.GridUid)
+ continue;
+
+ ent.Comp.WinConditions.Add(WinCondition.NukeActiveInStation);
+ SetWinType(ent, WinType.OpsMajor);
+ return;
}
}
+
+ if (_antag.AllAntagsAlive(ent.Owner))
+ {
+ SetWinType(ent, WinType.OpsMinor);
+ ent.Comp.WinConditions.Add(WinCondition.AllNukiesAlive);
+ return;
+ }
+
+ ent.Comp.WinConditions.Add(_antag.AnyAliveAntags(ent.Owner)
+ ? WinCondition.SomeNukiesAlive
+ : WinCondition.AllNukiesDead);
+
+ var diskAtCentCom = false;
+ var diskQuery = AllEntityQuery<NukeDiskComponent, TransformComponent>();
+ while (diskQuery.MoveNext(out _, out var transform))
+ {
+ diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value);
+
+ // TODO: The target station should be stored, and the nuke disk should store its original station.
+ // This is fine for now, because we can assume a single station in base SS14.
+ break;
+ }
+
+ // If the disk is currently at Central Command, the crew wins - just slightly.
+ // This also implies that some nuclear operatives have died.
+ SetWinType(ent, diskAtCentCom
+ ? WinType.CrewMinor
+ : WinType.OpsMinor);
+ ent.Comp.WinConditions.Add(diskAtCentCom
+ ? WinCondition.NukeDiskOnCentCom
+ : WinCondition.NukeDiskNotOnCentCom);
}
private void OnNukeDisarm(NukeDisarmSuccessEvent ev)
CheckRoundShouldEnd();
}
- private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent args)
+ private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
{
- var spawner = args.Spawner;
-
- if (!TryComp<NukeOperativeSpawnerComponent>(spawner, out var nukeOpSpawner))
- return;
-
- HumanoidCharacterProfile? profile = null;
- if (TryComp(args.Spawned, out ActorComponent? actor))
- profile = _prefs.GetPreferences(actor.PlayerSession.UserId).SelectedCharacter as HumanoidCharacterProfile;
-
- // TODO: this is kinda awful for multi-nukies
- foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
- {
- SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.SpawnDetails, profile);
-
- nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.SpawnDetails.AntagRoleProto);
- }
+ RemCompDeferred(uid, component);
}
- private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args)
+ private void OnMapInit(Entity<NukeOpsShuttleComponent> ent, ref MapInitEvent args)
{
- if (!_mind.TryGetMind(uid, out var mindId, out var mind))
- return;
+ var map = Transform(ent).MapID;
- var query = QueryActiveRules();
- while (query.MoveNext(out _, out _, out var nukeops, out _))
+ var rules = EntityQueryEnumerator<NukeopsRuleComponent, LoadMapRuleComponent>();
+ while (rules.MoveNext(out var uid, out _, out var mapRule))
{
- if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost ||
- nukeops.RoundEndBehavior == RoundEndBehavior.Nothing)
- {
- role ??= nukeops.OperativeSpawnDetails.AntagRoleProto;
- _roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = role });
- nukeops.OperativeMindPendingData.Remove(uid);
- }
-
- if (mind.Session is not { } playerSession)
- return;
-
- if (GameTicker.RunLevel != GameRunLevel.InRound)
- return;
-
- if (nukeops.TargetStation != null && !string.IsNullOrEmpty(Name(nukeops.TargetStation.Value)))
- {
- NotifyNukie(playerSession, component, nukeops);
- }
+ if (map != mapRule.Map)
+ continue;
+ ent.Comp.AssociatedRule = uid;
+ break;
}
}
- private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
- {
- RemCompDeferred(uid, component);
- }
-
private void OnShuttleFTLAttempt(ref ConsoleFTLAttemptEvent ev)
{
var query = QueryActiveRules();
- while (query.MoveNext(out _, out _, out var nukeops, out _))
+ while (query.MoveNext(out var uid, out _, out var nukeops, out _))
{
- if (ev.Uid != nukeops.NukieShuttle)
+ if (ev.Uid != GetShuttle((uid, nukeops)))
continue;
if (nukeops.WarDeclaredTime != null)
{
// TODO: this is VERY awful for multi-nukies
var query = QueryActiveRules();
- while (query.MoveNext(out _, out _, out var nukeops, out _))
+ while (query.MoveNext(out var uid, out _, out var nukeops, out _))
{
if (nukeops.WarDeclaredTime != null)
continue;
- if (Transform(ev.DeclaratorEntity).MapID != nukeops.NukiePlanet)
+ if (TryComp<LoadMapRuleComponent>(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map)
continue;
var newStatus = GetWarCondition(nukeops, ev.Status);
if (!_tag.HasTag(uid, NukeOpsUplinkTagPrototype))
continue;
- if (!nukieRule.NukieOutpost.HasValue)
+ if (GetOutpost(uid) is not {} outpost)
continue;
- if (Transform(uid).MapID != Transform(nukieRule.NukieOutpost.Value).MapID) // Will receive bonus TC only on their start outpost
+ if (Transform(uid).MapID != Transform(outpost).MapID) // Will receive bonus TC only on their start outpost
continue;
- _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTCAmountPerNukie } }, uid, component);
+ _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTcAmountPerNukie } }, uid, component);
var msg = Loc.GetString("store-currency-war-boost-given", ("target", uid));
_popupSystem.PopupEntity(msg, uid);
}
}
- private void OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null)
+ private void SetWinType(Entity<NukeopsRuleComponent> ent, WinType type, bool endRound = true)
{
- if (!Resolve(uid, ref component))
- return;
-
- // TODO: This needs to try and target a Nanotrasen station. At the very least,
- // we can only currently guarantee that NT stations are the only station to
- // exist in the base game.
-
- var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
- var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
- while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member))
- {
- if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member)))
- continue;
-
- eligible.Add((eligibleUid, eligibleComp, member));
- }
-
- if (eligible.Count == 0)
- return;
-
- component.TargetStation = RobustRandom.Pick(eligible);
- component.OperationName = _randomMetadata.GetRandomFromSegments([OperationPrefixDataset, OperationSuffixDataset], " ");
-
- var filter = Filter.Empty();
- var query = EntityQueryEnumerator<NukeOperativeComponent, ActorComponent>();
- while (query.MoveNext(out _, out var nukeops, out var actor))
- {
- NotifyNukie(actor.PlayerSession, nukeops, component);
- filter.AddPlayer(actor.PlayerSession);
- }
- }
-
- private void OnRoundEnd(EntityUid uid, NukeopsRuleComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- // If the win condition was set to operative/crew major win, ignore.
- if (component.WinType == WinType.OpsMajor || component.WinType == WinType.CrewMajor)
- return;
-
- var nukeQuery = AllEntityQuery<NukeComponent, TransformComponent>();
- var centcomms = _emergency.GetCentcommMaps();
-
- while (nukeQuery.MoveNext(out var nuke, out var nukeTransform))
- {
- if (nuke.Status != NukeStatus.ARMED)
- continue;
-
- // UH OH
- if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value))
- {
- component.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
- SetWinType(uid, WinType.OpsMajor, component);
- return;
- }
-
- if (nukeTransform.GridUid == null || component.TargetStation == null)
- continue;
-
- if (!TryComp(component.TargetStation.Value, out StationDataComponent? data))
- continue;
-
- foreach (var grid in data.Grids)
- {
- if (grid != nukeTransform.GridUid)
- continue;
-
- component.WinConditions.Add(WinCondition.NukeActiveInStation);
- SetWinType(uid, WinType.OpsMajor, component);
- return;
- }
- }
-
- var allAlive = true;
- var query = EntityQueryEnumerator<NukeopsRoleComponent, MindContainerComponent, MobStateComponent>();
- while (query.MoveNext(out var nukeopsUid, out _, out var mindContainer, out var mobState))
- {
- // mind got deleted somehow so ignore it
- if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
- continue;
-
- // check if player got gibbed or ghosted or something - count as dead
- if (mind.OwnedEntity != null &&
- // if the player somehow isn't a mob anymore that also counts as dead
- // have to be alive, not crit or dead
- mobState.CurrentState is MobState.Alive)
- {
- continue;
- }
-
- allAlive = false;
- break;
- }
-
- // If all nuke ops were alive at the end of the round,
- // the nuke ops win. This is to prevent people from
- // running away the moment nuke ops appear.
- if (allAlive)
- {
- SetWinType(uid, WinType.OpsMinor, component);
- component.WinConditions.Add(WinCondition.AllNukiesAlive);
- return;
- }
-
- component.WinConditions.Add(WinCondition.SomeNukiesAlive);
-
- var diskAtCentCom = false;
- var diskQuery = AllEntityQuery<NukeDiskComponent, TransformComponent>();
-
- while (diskQuery.MoveNext(out _, out var transform))
- {
- diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value);
-
- // TODO: The target station should be stored, and the nuke disk should store its original station.
- // This is fine for now, because we can assume a single station in base SS14.
- break;
- }
-
- // If the disk is currently at Central Command, the crew wins - just slightly.
- // This also implies that some nuclear operatives have died.
- if (diskAtCentCom)
- {
- SetWinType(uid, WinType.CrewMinor, component);
- component.WinConditions.Add(WinCondition.NukeDiskOnCentCom);
- }
- // Otherwise, the nuke ops win.
- else
- {
- SetWinType(uid, WinType.OpsMinor, component);
- component.WinConditions.Add(WinCondition.NukeDiskNotOnCentCom);
- }
- }
-
- private void SetWinType(EntityUid uid, WinType type, NukeopsRuleComponent? component = null, bool endRound = true)
- {
- if (!Resolve(uid, ref component))
- return;
-
- component.WinType = type;
+ ent.Comp.WinType = type;
if (endRound && (type == WinType.CrewMajor || type == WinType.OpsMajor))
_roundEndSystem.EndRound();
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
{
- if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
- continue;
-
- // If there are any nuclear bombs that are active, immediately return. We're not over yet.
- var armed = false;
- foreach (var nuke in EntityQuery<NukeComponent>())
- {
- if (nuke.Status == NukeStatus.ARMED)
- {
- armed = true;
- break;
- }
- }
- if (armed)
- continue;
-
- MapId? shuttleMapId = Exists(nukeops.NukieShuttle)
- ? Transform(nukeops.NukieShuttle.Value).MapID
- : null;
-
- MapId? targetStationMap = null;
- if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data))
- {
- var grid = data.Grids.FirstOrNull();
- targetStationMap = grid != null
- ? Transform(grid.Value).MapID
- : null;
- }
-
- // Check if there are nuke operatives still alive on the same map as the shuttle,
- // or on the same map as the station.
- // If there are, the round can continue.
- var operatives = EntityQuery<NukeOperativeComponent, MobStateComponent, TransformComponent>(true);
- var operativesAlive = operatives
- .Where(ent =>
- ent.Item3.MapID == shuttleMapId
- || ent.Item3.MapID == targetStationMap)
- .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running);
-
- if (operativesAlive)
- continue; // There are living operatives than can access the shuttle, or are still on the station's map.
-
- // Check that there are spawns available and that they can access the shuttle.
- var spawnsAvailable = EntityQuery<NukeOperativeSpawnerComponent>(true).Any();
- if (spawnsAvailable && shuttleMapId == nukeops.NukiePlanet)
- continue; // Ghost spawns can still access the shuttle. Continue the round.
-
- // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives,
- // and there are no nuclear operatives on the target station's map.
- nukeops.WinConditions.Add(spawnsAvailable
- ? WinCondition.NukiesAbandoned
- : WinCondition.AllNukiesDead);
-
- SetWinType(uid, WinType.CrewMajor, nukeops, false);
- _roundEndSystem.DoRoundEndBehavior(
- nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
-
- // prevent it called multiple times
- nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
+ CheckRoundShouldEnd((uid, nukeops));
}
}
- private bool SpawnMap(Entity<NukeopsRuleComponent> ent)
+ private void CheckRoundShouldEnd(Entity<NukeopsRuleComponent> ent)
{
- if (!ent.Comp.SpawnOutpost
- || ent.Comp.NukiePlanet != null)
- return true;
-
- ent.Comp.NukiePlanet = _mapManager.CreateMap();
- var gameMap = _prototypeManager.Index(ent.Comp.OutpostMapPrototype);
- ent.Comp.NukieOutpost = GameTicker.LoadGameMap(gameMap, ent.Comp.NukiePlanet.Value, null)[0];
- var query = EntityQueryEnumerator<NukeOpsShuttleComponent, TransformComponent>();
- while (query.MoveNext(out var grid, out _, out var shuttleTransform))
- {
- if (shuttleTransform.MapID != ent.Comp.NukiePlanet)
- continue;
-
- ent.Comp.NukieShuttle = grid;
- break;
- }
-
- return true;
- }
-
- /// <summary>
- /// Adds missing nuke operative components, equips starting gear and renames the entity.
- /// </summary>
- private void SetupOperativeEntity(EntityUid mob, string name, NukeopSpawnPreset spawnDetails, HumanoidCharacterProfile? profile)
- {
- _metaData.SetEntityName(mob, name);
- EnsureComp<NukeOperativeComponent>(mob);
-
- if (profile != null)
- _humanoid.LoadProfile(mob, profile);
+ var nukeops = ent.Comp;
- var gear = _prototypeManager.Index(spawnDetails.GearProto);
- _stationSpawning.EquipStartingGear(mob, gear);
-
- _npcFaction.RemoveFaction(mob, "NanoTrasen", false);
- _npcFaction.AddFaction(mob, "Syndicate");
- }
-
- private void SpawnOperatives(List<NukieSpawn> sessions, bool spawnGhostRoles, NukeopsRuleComponent component)
- {
- if (component.NukieOutpost is not { Valid: true } outpostUid)
+ if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
return;
- var spawns = new List<EntityCoordinates>();
- foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
- {
- if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id)
- continue;
- if (xform.ParentUid != component.NukieOutpost)
- continue;
-
- spawns.Add(xform.Coordinates);
- break;
- }
-
- //Fallback, spawn at the centre of the map
- if (spawns.Count == 0)
+ // If there are any nuclear bombs that are active, immediately return. We're not over yet.
+ foreach (var nuke in EntityQuery<NukeComponent>())
{
- spawns.Add(Transform(outpostUid).Coordinates);
- _sawmill.Warning($"Fell back to default spawn for nukies!");
+ if (nuke.Status == NukeStatus.ARMED)
+ return;
}
- //Spawn the team
- foreach (var nukieSession in sessions)
- {
- var name = $"{Loc.GetString(nukieSession.Type.NamePrefix)} {RobustRandom.PickAndTake(_prototypeManager.Index(nukieSession.Type.NameList).Values.ToList())}";
-
- var nukeOpsAntag = _prototypeManager.Index(nukieSession.Type.AntagRoleProto);
-
- //If a session is available, spawn mob and transfer mind into it
- if (nukieSession.Session != null)
- {
- var profile = _prefs.GetPreferences(nukieSession.Session.UserId).SelectedCharacter as HumanoidCharacterProfile;
- if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
- {
- species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
- }
+ var shuttle = GetShuttle((ent, ent));
- var mob = Spawn(species.Prototype, RobustRandom.Pick(spawns));
- SetupOperativeEntity(mob, name, nukieSession.Type, profile);
+ MapId? shuttleMapId = Exists(shuttle)
+ ? Transform(shuttle.Value).MapID
+ : null;
- var newMind = _mind.CreateMind(nukieSession.Session.UserId, name);
- _mind.SetUserId(newMind, nukieSession.Session.UserId);
- _roles.MindAddRole(newMind, new NukeopsRoleComponent { PrototypeId = nukieSession.Type.AntagRoleProto });
-
- _mind.TransferTo(newMind, mob);
- }
- //Otherwise, spawn as a ghost role
- else if (spawnGhostRoles)
- {
- var spawnPoint = Spawn(component.GhostSpawnPointProto, RobustRandom.Pick(spawns));
- var ghostRole = EnsureComp<GhostRoleComponent>(spawnPoint);
- EnsureComp<GhostRoleMobSpawnerComponent>(spawnPoint);
- ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name);
- ghostRole.RoleDescription = Loc.GetString(nukeOpsAntag.Objective);
-
- var nukeOpSpawner = EnsureComp<NukeOperativeSpawnerComponent>(spawnPoint);
- nukeOpSpawner.OperativeName = name;
- nukeOpSpawner.SpawnDetails = nukieSession.Type;
- }
+ MapId? targetStationMap = null;
+ if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data))
+ {
+ var grid = data.Grids.FirstOrNull();
+ targetStationMap = grid != null
+ ? Transform(grid.Value).MapID
+ : null;
}
- }
- /// <summary>
- /// Display a greeting message and play a sound for a nukie
- /// </summary>
- private void NotifyNukie(ICommonSession session, NukeOperativeComponent nukeop, NukeopsRuleComponent nukeopsRule)
- {
- if (nukeopsRule.TargetStation is not { } station)
- return;
-
- _antagSelection.SendBriefing(session, Loc.GetString("nukeops-welcome", ("station", station), ("name", nukeopsRule.OperationName)), Color.Red, nukeop.GreetSoundNotification);
+ // Check if there are nuke operatives still alive on the same map as the shuttle,
+ // or on the same map as the station.
+ // If there are, the round can continue.
+ var operatives = EntityQuery<NukeOperativeComponent, MobStateComponent, TransformComponent>(true);
+ var operativesAlive = operatives
+ .Where(op =>
+ op.Item3.MapID == shuttleMapId
+ || op.Item3.MapID == targetStationMap)
+ .Any(op => op.Item2.CurrentState == MobState.Alive && op.Item1.Running);
+
+ if (operativesAlive)
+ return; // There are living operatives than can access the shuttle, or are still on the station's map.
+
+ // Check that there are spawns available and that they can access the shuttle.
+ var spawnsAvailable = EntityQuery<NukeOperativeSpawnerComponent>(true).Any();
+ if (spawnsAvailable && CompOrNull<LoadMapRuleComponent>(ent)?.Map == shuttleMapId)
+ return; // Ghost spawns can still access the shuttle. Continue the round.
+
+ // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives,
+ // and there are no nuclear operatives on the target station's map.
+ nukeops.WinConditions.Add(spawnsAvailable
+ ? WinCondition.NukiesAbandoned
+ : WinCondition.AllNukiesDead);
+
+ SetWinType(ent, WinType.CrewMajor, false);
+ _roundEndSystem.DoRoundEndBehavior(
+ nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
+
+ // prevent it called multiple times
+ nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
}
- /// <summary>
- /// Spawn nukie ghost roles if this gamerule was started mid round
- /// </summary>
- private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null)
+ // this should really go anywhere else but im tired.
+ private void OnAntagSelectEntity(Entity<NukeopsRuleComponent> ent, ref AntagSelectEntityEvent args)
{
- if (!Resolve(uid, ref component))
+ if (args.Handled)
return;
- if (!SpawnMap((uid, component)))
+ var profile = args.Session != null
+ ? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile
+ : HumanoidCharacterProfile.RandomWithSpecies();
+ if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
{
- _sawmill.Info("Failed to load map for nukeops");
- return;
+ species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
}
- var numNukies = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerOperative, component.MaxOps);
+ args.Entity = Spawn(species.Prototype);
+ _humanoid.LoadProfile(args.Entity.Value, profile);
+ }
- //Dont continue if we have no nukies to spawn
- if (numNukies == 0)
+ private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
+ {
+ if (ent.Comp.TargetStation is not { } station)
return;
- //Fill the ranks, commander first, then agent, then operatives
- //TODO: Possible alternative team compositions? Like multiple commanders or agents
- var operatives = new List<NukieSpawn>();
- if (numNukies >= 1)
- operatives.Add(new NukieSpawn(null, component.CommanderSpawnDetails));
- if (numNukies >= 2)
- operatives.Add(new NukieSpawn(null, component.AgentSpawnDetails));
- if (numNukies >= 3)
- {
- for (var i = 2; i < numNukies; i++)
- {
- operatives.Add(new NukieSpawn(null, component.OperativeSpawnDetails));
- }
- }
-
- SpawnOperatives(operatives, true, component);
+ _antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome",
+ ("station", station),
+ ("name", Name(ent))),
+ Color.Red,
+ ent.Comp.GreetSoundNotification);
}
- //For admins forcing someone to nukeOps.
- public void MakeLoneNukie(EntityUid entity)
+ /// <remarks>
+ /// Is this method the shitty glue holding together the last of my sanity? yes.
+ /// Do i have a better solution? not presently.
+ /// </remarks>
+ private EntityUid? GetOutpost(Entity<LoadMapRuleComponent?> ent)
{
- if (!_mind.TryGetMind(entity, out var mindId, out var mindComponent))
- return;
+ if (!Resolve(ent, ref ent.Comp, false))
+ return null;
- //ok hardcoded value bad but so is everything else here
- _roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = NukeopsId }, mindComponent);
- SetOutfitCommand.SetOutfit(entity, "SyndicateOperativeGearFull", EntityManager);
+ return ent.Comp.MapGrids.FirstOrNull();
}
- private sealed class NukieSpawn
+ /// <remarks>
+ /// Is this method the shitty glue holding together the last of my sanity? yes.
+ /// Do i have a better solution? not presently.
+ /// </remarks>
+ private EntityUid? GetShuttle(Entity<NukeopsRuleComponent?> ent)
{
- public ICommonSession? Session { get; private set; }
- public NukeopSpawnPreset Type { get; private set; }
+ if (!Resolve(ent, ref ent.Comp, false))
+ return null;
- public NukieSpawn(ICommonSession? session, NukeopSpawnPreset type)
+ var query = EntityQueryEnumerator<NukeOpsShuttleComponent>();
+ while (query.MoveNext(out var uid, out var comp))
{
- Session = session;
- Type = type;
+ if (comp.AssociatedRule == ent.Owner)
+ return uid;
}
+
+ return null;
}
}
+++ /dev/null
-using System.Linq;
-using System.Numerics;
-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;
-using Content.Server.Station.Systems;
-using Content.Shared.CCVar;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Mind;
-using Content.Shared.NPC.Prototypes;
-using Content.Shared.NPC.Systems;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Robust.Server.GameObjects;
-using Robust.Server.Maps;
-using Robust.Server.Player;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-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.Utility;
-
-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>
-{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] private readonly IChatManager _chatManager = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly IServerPreferencesManager _prefs = default!;
- [Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!;
- [Dependency] private readonly PricingSystem _pricingSystem = default!;
- [Dependency] private readonly MapLoaderSystem _map = default!;
- [Dependency] private readonly NamingSystem _namingSystem = default!;
- [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
- [Dependency] private readonly SharedMindSystem _mindSystem = default!;
- [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
- [Dependency] private readonly MetaDataSystem _metaData = default!;
-
- [ValidatePrototypeId<EntityPrototype>]
- private const string GameRuleId = "Pirates";
-
- [ValidatePrototypeId<EntityPrototype>]
- private const string MobId = "MobHuman";
-
- [ValidatePrototypeId<SpeciesPrototype>]
- private const string SpeciesId = "Human";
-
- [ValidatePrototypeId<NpcFactionPrototype>]
- private const string PirateFactionId = "Syndicate";
-
- [ValidatePrototypeId<NpcFactionPrototype>]
- private const string EnemyFactionId = "NanoTrasen";
-
- [ValidatePrototypeId<StartingGearPrototype>]
- private const string GearId = "PirateGear";
-
- [ValidatePrototypeId<EntityPrototype>]
- private const string SpawnPointId = "SpawnPointPirates";
-
- /// <inheritdoc/>
- public override void Initialize()
- {
- base.Initialize();
-
- 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 (Deleted(pirates.PirateShip))
- {
- // Major loss, the ship somehow got annihilated.
- ev.AddLine(Loc.GetString("pirates-no-ship"));
- }
- else
- {
- List<(double, EntityUid)> mostValuableThefts = new();
-
- var comp1 = pirates;
- var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
- {
- foreach (var mindId in comp1.Pirates)
- {
- if (TryComp(mindId, out MindComponent? mind) && 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 mindId in pirates.Pirates)
- {
- if (TryComp(mindId, out MindComponent? mind) && 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"));
- }
-
- ev.AddLine("");
- ev.AddLine(Loc.GetString("pirates-list-start"));
- foreach (var pirate in pirates.Pirates)
- {
- if (TryComp(pirate, out MindComponent? mind))
- {
- ev.AddLine($"- {mind.CharacterName} ({mind.Session?.Name})");
- }
- }
- }
- }
-
- 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 (!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 ICommonSession[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 = EntityQuery<StationDataComponent>().SelectMany(x =>
- x.Grids.Select(x =>
- xformQuery.GetComponent(x).WorldMatrix.TransformBox(Comp<MapGridComponent>(x).LocalAABB)))
- .ToArray();
-
- var aabb = aabbs[0];
-
- for (var i = 1; i < aabbs.Length; i++)
- {
- aabb.Union(aabbs[i]);
- }
-
- // (Not commented?)
- var a = MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f;
-
- var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
- {
- Offset = aabb.Center + new Vector2(a, a),
- LoadMap = false,
- });
-
- if (!gridId.HasValue)
- {
- Log.Error($"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>(GearId); // 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 != SpawnPointId || xform.ParentUid != pirates.PirateShip)
- continue;
-
- spawns.Add(xform.Coordinates);
- }
-
- if (spawns.Count == 0)
- {
- spawns.Add(Transform(pirates.PirateShip).Coordinates);
- Log.Warning($"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(SpeciesId, gender);
-
- var session = ops[i];
- var newMind = _mindSystem.CreateMind(session.UserId, name);
- _mindSystem.SetUserId(newMind, session.UserId);
-
- var mob = Spawn(MobId, _random.Pick(spawns));
- _metaData.SetEntityName(mob, name);
-
- _mindSystem.TransferTo(newMind, mob);
- var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
- _stationSpawningSystem.EquipStartingGear(mob, pirateGear);
-
- _npcFaction.RemoveFaction(mob, EnemyFactionId, false);
- _npcFaction.AddFaction(mob, PirateFactionId);
-
- pirates.Pirates.Add(newMind);
-
- // Notificate every player about a pirate antagonist role with sound
- _audioSystem.PlayGlobal(pirates.PirateAlertSound, session);
-
- GameTicker.PlayerJoinGame(session);
- }
-
- pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
- {
- pirates.InitialItems.Add(uid);
- return true;
- }); // Include the players in the appraisal.
- }
- }
-
- //Forcing one player to be a pirate.
- public void MakePirate(EntityUid entity)
- {
- if (!_mindSystem.TryGetMind(entity, out var mindId, out var mind))
- return;
-
- SetOutfitCommand.SetOutfit(entity, GearId, EntityManager);
-
- var pirateRule = EntityQuery<PiratesRuleComponent>().FirstOrDefault();
- if (pirateRule == null)
- {
- //todo fuck me this shit is awful
- GameTicker.StartGameRule(GameRuleId, out var ruleEntity);
- pirateRule = Comp<PiratesRuleComponent>(ruleEntity);
- }
-
- // Notificate every player about a pirate antagonist role with sound
- if (mind.Session != null)
- {
- _audioSystem.PlayGlobal(pirateRule.PirateAlertSound, mind.Session);
- }
- }
-
- private void OnStartAttempt(RoundStartAttemptEvent ev)
- {
- var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
- while (query.MoveNext(out var uid, out var pirates, out var gameRule))
- {
- if (!GameTicker.IsGameRuleActive(uid, gameRule))
- return;
-
- var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
- if (!ev.Forced && ev.Players.Length < minPlayers)
- {
- _chatManager.SendAdminAnnouncement(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();
- }
- }
- }
-}
using Content.Server.Chat.Managers;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Systems;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
-using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mindshield.Components;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.Revolutionary.Components;
-using Content.Shared.Roles;
using Content.Shared.Stunnable;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
-using System.Linq;
+using Content.Server.GameTicking.Components;
namespace Content.Server.GameTicking.Rules;
{
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly EuiManager _euiMan = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
- [Dependency] private readonly InventorySystem _inventory = default!;
//Used in OnPostFlash, no reference to the rule component is available
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
- SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayerJobAssigned);
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
- SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
}
- //Set miniumum players
- protected override void Added(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
- {
- base.Added(uid, component, gameRule, args);
-
- gameRule.MinPlayers = component.MinPlayers;
- }
-
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
}
}
- private void OnRoundEndText(RoundEndTextAppendEvent ev)
+ protected override void AppendRoundEndText(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule,
+ ref RoundEndTextAppendEvent args)
{
+ base.AppendRoundEndText(uid, component, gameRule, ref args);
+
var revsLost = CheckRevsLose();
var commandLost = CheckCommandLose();
- var query = AllEntityQuery<RevolutionaryRuleComponent>();
- while (query.MoveNext(out var headrev))
+ // This is (revsLost, commandsLost) concatted together
+ // (moony wrote this comment idk what it means)
+ var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
+ args.AddLine(Loc.GetString(Outcomes[index]));
+
+ var sessionData = _antag.GetAntagIdentifiers(uid);
+ args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
+ foreach (var (mind, data, name) in sessionData)
{
- // This is (revsLost, commandsLost) concatted together
- // (moony wrote this comment idk what it means)
- var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
- ev.AddLine(Loc.GetString(Outcomes[index]));
-
- ev.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", headrev.HeadRevs.Count)));
- foreach (var player in headrev.HeadRevs)
- {
- // TODO: when role entities are a thing this has to change
- var count = CompOrNull<RevolutionaryRoleComponent>(player.Value)?.ConvertedCount ?? 0;
-
- _mind.TryGetSession(player.Value, out var session);
- var username = session?.Name;
- if (username != null)
- {
- ev.AddLine(Loc.GetString("rev-headrev-name-user",
- ("name", player.Key),
- ("username", username), ("count", count)));
- }
- else
- {
- ev.AddLine(Loc.GetString("rev-headrev-name",
- ("name", player.Key), ("count", count)));
- }
+ var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
+ args.AddLine(Loc.GetString("rev-headrev-name-user",
+ ("name", name),
+ ("username", data.UserName),
+ ("count", count)));
- // TODO: someone suggested listing all alive? revs maybe implement at some point
- }
+ // TODO: someone suggested listing all alive? revs maybe implement at some point
}
}
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
}
- //Check for enough players to start rule
- private void OnStartAttempt(RoundStartAttemptEvent ev)
- {
- TryRoundStartAttempt(ev, Loc.GetString("roles-antag-rev-name"));
- }
-
- private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev)
- {
- var query = QueryActiveRules();
- while (query.MoveNext(out var uid, out var activeGameRule, out var comp, out var gameRule))
- {
- var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.HeadRevPrototypeId);
-
- if (eligiblePlayers.Count == 0)
- continue;
-
- var headRevCount = _antagSelection.CalculateAntagCount(ev.Players.Length, comp.PlayersPerHeadRev, comp.MaxHeadRevs);
-
- var headRevs = _antagSelection.ChooseAntags(headRevCount, eligiblePlayers);
-
- GiveHeadRev(headRevs, comp.HeadRevPrototypeId, comp);
- }
- }
-
- private void GiveHeadRev(IEnumerable<EntityUid> chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
- {
- foreach (var headRev in chosen)
- GiveHeadRev(headRev, antagProto, comp);
- }
- private void GiveHeadRev(EntityUid chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
- {
- RemComp<CommandStaffComponent>(chosen);
-
- var inCharacterName = MetaData(chosen).EntityName;
-
- if (!_mind.TryGetMind(chosen, out var mind, out _))
- return;
-
- if (!_role.MindHasRole<RevolutionaryRoleComponent>(mind))
- {
- _role.MindAddRole(mind, new RevolutionaryRoleComponent { PrototypeId = antagProto }, silent: true);
- }
-
- comp.HeadRevs.Add(inCharacterName, mind);
- _inventory.SpawnItemsOnEntity(chosen, comp.StartingGear);
- var revComp = EnsureComp<RevolutionaryComponent>(chosen);
- EnsureComp<HeadRevolutionaryComponent>(chosen);
-
- _antagSelection.SendBriefing(chosen, Loc.GetString("head-rev-role-greeting"), Color.CornflowerBlue, revComp.RevStartSound);
- }
-
/// <summary>
/// Called when a Head Rev uses a flash in melee to convert somebody else.
/// </summary>
}
if (mind?.Session != null)
- _antagSelection.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
- }
-
- public void OnHeadRevAdmin(EntityUid entity)
- {
- if (HasComp<HeadRevolutionaryComponent>(entity))
- return;
-
- var revRule = EntityQuery<RevolutionaryRuleComponent>().FirstOrDefault();
- if (revRule == null)
- {
- GameTicker.StartGameRule("Revolutionary", out var ruleEnt);
- revRule = Comp<RevolutionaryRuleComponent>(ruleEnt);
- }
-
- GiveHeadRev(entity, revRule.HeadRevPrototypeId, revRule);
+ _antag.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
}
//TODO: Enemies of the revolution
_popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");
- if (!_mind.TryGetMind(uid, out var mindId, out var mind, mc))
+ if (!_mind.TryGetMind(uid, out var mindId, out _, mc))
continue;
// remove their antag role
using System.Linq;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Sandbox;
using Content.Server.Administration.Logs;
+using Content.Server.GameTicking.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Presets;
using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.Storage;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Roles;
-using Content.Shared.Antag;
-using Content.Shared.CombatMode.Pacification;
using Content.Shared.Humanoid;
-using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
-using Content.Shared.Roles;
using Robust.Shared.Random;
-using System.Linq;
namespace Content.Server.GameTicking.Rules;
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
- [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
- [Dependency] private readonly InventorySystem _inventory = default!;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
+ SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected);
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
}
- private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
+ private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
- var query = QueryActiveRules();
- while (query.MoveNext(out var uid, out _, out var comp, out var gameRule))
- {
- //Get all players eligible for this role, allow selecting existing antags
- //TO DO: When voxes specifies are added, increase their chance of becoming a thief by 4 times >:)
- var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.ThiefPrototypeId, acceptableAntags: AntagAcceptability.All, allowNonHumanoids: true);
-
- //Abort if there are none
- if (eligiblePlayers.Count == 0)
- {
- Log.Warning($"No eligible thieves found, ending game rule {ToPrettyString(uid):rule}");
- GameTicker.EndGameRule(uid, gameRule);
- continue;
- }
-
- //Calculate number of thieves to choose
- var thiefCount = _random.Next(1, comp.MaxAllowThief + 1);
-
- //Select our theives
- var thieves = _antagSelection.ChooseAntags(thiefCount, eligiblePlayers);
-
- MakeThief(thieves, comp, comp.PacifistThieves);
- }
- }
-
- public void MakeThief(List<EntityUid> players, ThiefRuleComponent thiefRule, bool addPacified)
- {
- foreach (var thief in players)
- {
- MakeThief(thief, thiefRule, addPacified);
- }
- }
-
- public void MakeThief(EntityUid thief, ThiefRuleComponent thiefRule, bool addPacified)
- {
- if (!_mindSystem.TryGetMind(thief, out var mindId, out var mind))
+ if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind))
return;
- if (HasComp<ThiefRoleComponent>(mindId))
- return;
-
- // Assign thief roles
- _roleSystem.MindAddRole(mindId, new ThiefRoleComponent
- {
- PrototypeId = thiefRule.ThiefPrototypeId,
- }, silent: true);
-
- //Add Pacified
- //To Do: Long-term this should just be using the antag code to add components.
- if (addPacified) //This check is important because some servers may want to disable the thief's pacifism. Do not remove.
- {
- EnsureComp<PacifiedComponent>(thief);
- }
-
//Generate objectives
- GenerateObjectives(mindId, mind, thiefRule);
-
- //Send briefing here to account for humanoid/animal
- _antagSelection.SendBriefing(thief, MakeBriefing(thief), null, thiefRule.GreetingSound);
-
- // Give starting items
- _inventory.SpawnItemsOnEntity(thief, thiefRule.StarterItems);
-
- thiefRule.ThievesMinds.Add(mindId);
- }
-
- public void AdminMakeThief(EntityUid entity, bool addPacified)
- {
- var thiefRule = EntityQuery<ThiefRuleComponent>().FirstOrDefault();
- if (thiefRule == null)
- {
- GameTicker.StartGameRule("Thief", out var ruleEntity);
- thiefRule = Comp<ThiefRuleComponent>(ruleEntity);
- }
-
- if (HasComp<ThiefRoleComponent>(entity))
- return;
-
- MakeThief(entity, thiefRule, addPacified);
+ GenerateObjectives(mindId, mind, ent);
}
private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
private string MakeBriefing(EntityUid thief)
{
var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
- var briefing = "\n";
- briefing = isHuman
+ var briefing = isHuman
? Loc.GetString("thief-role-greeting-human")
: Loc.GetString("thief-role-greeting-animal");
return briefing;
}
- private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> thiefs, ref ObjectivesTextGetInfoEvent args)
+ private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> ent, ref ObjectivesTextGetInfoEvent args)
{
- args.Minds = thiefs.Comp.ThievesMinds;
+ args.Minds = _antag.GetAntagMindEntityUids(ent.Owner);
args.AgentName = Loc.GetString("thief-round-end-agent-name");
}
}
using Content.Server.PDA.Ringer;
using Content.Server.Roles;
using Content.Server.Traitor.Uplink;
-using Content.Shared.CCVar;
-using Content.Shared.Dataset;
using Content.Shared.Mind;
-using Content.Shared.Mobs.Systems;
using Content.Shared.NPC.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.PDA;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
-using Robust.Server.Player;
-using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
-using Robust.Shared.Timing;
using System.Linq;
using System.Text;
+using Content.Server.GameTicking.Components;
namespace Content.Server.GameTicking.Rules;
public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
{
- [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
- [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
- [Dependency] private readonly IGameTiming _timing = default!;
- private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
- private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
+ public const int MaxPicks = 20;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
- SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
- SubscribeLocalEvent<PlayerSpawnCompleteEvent>(HandleLatejoin);
+ SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
}
- //Set min players on game rule
protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
-
- gameRule.MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
- }
-
- protected override void Started(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
- {
- base.Started(uid, component, gameRule, args);
MakeCodewords(component);
}
- protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime)
- {
- base.ActiveTick(uid, component, gameRule, frameTime);
-
- if (component.SelectionStatus < TraitorRuleComponent.SelectionState.Started && component.AnnounceAt < _timing.CurTime)
- {
- DoTraitorStart(component);
- component.SelectionStatus = TraitorRuleComponent.SelectionState.Started;
- }
- }
-
- /// <summary>
- /// Check for enough players
- /// </summary>
- /// <param name="ev"></param>
- private void OnStartAttempt(RoundStartAttemptEvent ev)
+ private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
- TryRoundStartAttempt(ev, Loc.GetString("traitor-title"));
+ MakeTraitor(args.EntityUid, ent);
}
private void MakeCodewords(TraitorRuleComponent component)
{
- var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
- var adjectives = _prototypeManager.Index<DatasetPrototype>(component.CodewordAdjectives).Values;
- var verbs = _prototypeManager.Index<DatasetPrototype>(component.CodewordVerbs).Values;
+ var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values;
+ var verbs = _prototypeManager.Index(component.CodewordVerbs).Values;
var codewordPool = adjectives.Concat(verbs).ToList();
- var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count);
+ var finalCodewordCount = Math.Min(component.CodewordCount, codewordPool.Count);
component.Codewords = new string[finalCodewordCount];
for (var i = 0; i < finalCodewordCount; i++)
{
}
}
- private void DoTraitorStart(TraitorRuleComponent component)
- {
- var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.TraitorPrototypeId);
-
- if (eligiblePlayers.Count == 0)
- return;
-
- var traitorsToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, PlayersPerTraitor, MaxTraitors);
-
- var selectedTraitors = _antagSelection.ChooseAntags(traitorsToSelect, eligiblePlayers);
-
- MakeTraitor(selectedTraitors, component);
- }
-
- private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
- {
- //Start the timer
- var query = QueryActiveRules();
- while (query.MoveNext(out _, out var comp, out var gameRuleComponent))
- {
- var delay = TimeSpan.FromSeconds(
- _cfg.GetCVar(CCVars.TraitorStartDelay) +
- _random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
-
- //Set the delay for choosing traitors
- comp.AnnounceAt = _timing.CurTime + delay;
-
- comp.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToStart;
- }
- }
-
- public bool MakeTraitor(List<EntityUid> traitors, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
- {
- foreach (var traitor in traitors)
- {
- MakeTraitor(traitor, component, giveUplink, giveObjectives);
- }
-
- return true;
- }
-
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
{
//Grab the mind if it wasnt provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
return false;
- if (HasComp<TraitorRoleComponent>(mindId))
- {
- Log.Error($"Player {mind.CharacterName} is already a traitor.");
- return false;
- }
-
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
Note[]? code = null;
if (giveUplink)
{
// Calculate the amount of currency on the uplink.
- var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
+ var startingBalance = component.StartingBalance;
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
}
- _antagSelection.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification);
+ _antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification);
component.TraitorMinds.Add(mindId);
- // Assign traitor roles
- _roleSystem.MindAddRole(mindId, new TraitorRoleComponent
- {
- PrototypeId = component.TraitorPrototypeId
- }, mind, true);
// Assign briefing
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
{
- Briefing = briefing.ToString()
+ Briefing = briefing
}, mind, true);
// Change the faction
// Give traitors their objectives
if (giveObjectives)
{
- var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty);
- var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks);
var difficulty = 0f;
- Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty");
- for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++)
+ for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++)
{
var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
if (objective == null)
return true;
}
- private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
- {
- var query = QueryActiveRules();
- while (query.MoveNext(out _, out var comp, out _))
- {
- if (comp.TotalTraitors >= MaxTraitors)
- continue;
-
- if (!ev.LateJoin)
- continue;
-
- if (!_antagSelection.IsPlayerEligible(ev.Player, comp.TraitorPrototypeId))
- continue;
-
- //If its before we have selected traitors, continue
- if (comp.SelectionStatus < TraitorRuleComponent.SelectionState.Started)
- continue;
-
- // the nth player we adjust our probabilities around
- var target = PlayersPerTraitor * comp.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.Mob, comp);
- }
- }
- }
-
private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
{
- args.Minds = comp.TraitorMinds;
+ args.Minds = _antag.GetAntagMindEntityUids(uid);
args.AgentName = Loc.GetString("traitor-round-end-agent-name");
}
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
}
- /// <summary>
- /// Start this game rule manually
- /// </summary>
- public TraitorRuleComponent StartGameRule()
- {
- var comp = EntityQuery<TraitorRuleComponent>().FirstOrDefault();
- if (comp == null)
- {
- GameTicker.StartGameRule("Traitor", out var ruleEntity);
- comp = Comp<TraitorRuleComponent>(ruleEntity);
- }
-
- return comp;
- }
-
- public void MakeTraitorAdmin(EntityUid entity, bool giveUplink, bool giveObjectives)
- {
- var traitorRule = StartGameRule();
- MakeTraitor(entity, traitorRule, giveUplink, giveObjectives);
- }
-
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode)
{
var sb = new StringBuilder();
public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind)
{
List<(EntityUid Id, MindComponent Mind)> allTraitors = new();
- foreach (var traitor in EntityQuery<TraitorRuleComponent>())
+
+ var query = EntityQueryEnumerator<TraitorRuleComponent>();
+ while (query.MoveNext(out var uid, out var traitor))
{
- foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, traitor))
+ foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, (uid, traitor)))
{
if (!allTraitors.Contains(role))
allTraitors.Add(role);
return allTraitors;
}
- private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, TraitorRuleComponent component)
+ private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, Entity<TraitorRuleComponent> rule)
{
var traitors = new List<(EntityUid Id, MindComponent Mind)>();
- foreach (var traitor in component.TraitorMinds)
+ foreach (var mind in _antag.GetAntagMinds(rule.Owner))
{
- if (TryComp(traitor, out MindComponent? mind) &&
- mind.OwnedEntity != null &&
- mind.Session != null &&
- mind != ourMind &&
- _mobStateSystem.IsAlive(mind.OwnedEntity.Value) &&
- mind.CurrentEntity == mind.OwnedEntity)
- {
- traitors.Add((traitor, mind));
- }
+ if (mind.Comp == ourMind)
+ continue;
+
+ traitors.Add((mind, mind));
}
return traitors;
-using Content.Server.Actions;
using Content.Server.Antag;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Popups;
-using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Zombies;
-using Content.Shared.CCVar;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
-using Content.Shared.Roles;
using Content.Shared.Zombies;
-using Robust.Server.Player;
-using Robust.Shared.Configuration;
using Robust.Shared.Player;
-using Robust.Shared.Random;
using Robust.Shared.Timing;
using System.Globalization;
+using Content.Server.GameTicking.Components;
namespace Content.Server.GameTicking.Rules;
public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly PopupSystem _popup = default!;
- [Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
- [Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly StationSystem _station = default!;
- [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
- SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
}
- /// <summary>
- /// Set the required minimum players for this gamemode to start
- /// </summary>
- protected override void Added(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+ protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
+ ref RoundEndTextAppendEvent args)
{
- base.Added(uid, component, gameRule, args);
-
- gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
- }
-
- private void OnRoundEndText(RoundEndTextAppendEvent ev)
- {
- foreach (var zombie in EntityQuery<ZombieRuleComponent>())
+ base.AppendRoundEndText(uid, component, gameRule, ref args);
+
+ // This is just the general condition thing used for determining the win/lose text
+ var fraction = GetInfectedFraction(true, true);
+
+ if (fraction <= 0)
+ args.AddLine(Loc.GetString("zombie-round-end-amount-none"));
+ else if (fraction <= 0.25)
+ args.AddLine(Loc.GetString("zombie-round-end-amount-low"));
+ else if (fraction <= 0.5)
+ args.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
+ else if (fraction < 1)
+ args.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
+ else
+ args.AddLine(Loc.GetString("zombie-round-end-amount-all"));
+
+ var antags = _antag.GetAntagIdentifiers(uid);
+ args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count)));
+ foreach (var (_, data, entName) in antags)
{
- // This is just the general condition thing used for determining the win/lose text
- var fraction = GetInfectedFraction(true, true);
-
- if (fraction <= 0)
- ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
- else if (fraction <= 0.25)
- ev.AddLine(Loc.GetString("zombie-round-end-amount-low"));
- else if (fraction <= 0.5)
- ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
- else if (fraction < 1)
- ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
- else
- ev.AddLine(Loc.GetString("zombie-round-end-amount-all"));
+ args.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
+ ("name", entName),
+ ("username", data.UserName)));
+ }
- ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", zombie.InitialInfectedNames.Count)));
- foreach (var player in zombie.InitialInfectedNames)
+ var healthy = GetHealthyHumans();
+ // 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 (healthy.Count <= 0 || healthy.Count > 2 * antags.Count)
+ return;
+ args.AddLine("");
+ args.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
+ foreach (var survivor in healthy)
+ {
+ var meta = MetaData(survivor);
+ var username = string.Empty;
+ if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null)
{
- ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
- ("name", player.Key),
- ("username", player.Value)));
+ username = mind.Session.Name;
}
- var healthy = GetHealthyHumans();
- // 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 (healthy.Count <= 0 || healthy.Count > 2 * zombie.InitialInfectedNames.Count)
- continue;
- ev.AddLine("");
- ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
- foreach (var survivor in healthy)
- {
- var meta = MetaData(survivor);
- var username = string.Empty;
- if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null)
- {
- username = mind.Session.Name;
- }
-
- ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
- ("name", meta.EntityName),
- ("username", username)));
- }
+ args.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
+ ("name", meta.EntityName),
+ ("username", username)));
}
}
_roundEnd.EndRound();
}
- /// <summary>
- /// Check we have enough players to start this game mode, if not - cancel and announce
- /// </summary>
- private void OnStartAttempt(RoundStartAttemptEvent ev)
- {
- TryRoundStartAttempt(ev, Loc.GetString("zombie-title"));
- }
-
protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
- var delay = _random.Next(component.MinStartDelay, component.MaxStartDelay);
- component.StartTime = _timing.CurTime + delay;
+ component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
}
protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
-
- if (component.StartTime.HasValue && component.StartTime < _timing.CurTime)
- {
- InfectInitialPlayers(component);
- component.StartTime = null;
- component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
- }
-
- if (component.NextRoundEndCheck.HasValue && component.NextRoundEndCheck < _timing.CurTime)
- {
- CheckRoundEnd(component);
- component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
- }
+ if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime)
+ return;
+ CheckRoundEnd(component);
+ component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
}
private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args)
}
return healthy;
}
-
- /// <summary>
- /// Infects the first players with the passive zombie virus.
- /// Also records their names for the end of round screen.
- /// </summary>
- /// <remarks>
- /// The reason this code is written separately is to facilitate
- /// allowing this gamemode to be started midround. As such, it doesn't need
- /// any information besides just running.
- /// </remarks>
- private void InfectInitialPlayers(ZombieRuleComponent component)
- {
- //Get all players with initial infected enabled, and exclude those with the ZombieImmuneComponent and roles with CanBeAntag = False
- var eligiblePlayers = _antagSelection.GetEligiblePlayers(
- _playerManager.Sessions,
- component.PatientZeroPrototypeId,
- includeAllJobs: false,
- customExcludeCondition: player => HasComp<ZombieImmuneComponent>(player) || HasComp<InitialInfectedExemptComponent>(player)
- );
-
- //And get all players, excluding ZombieImmune and roles with CanBeAntag = False - to fill any leftover initial infected slots
- var allPlayers = _antagSelection.GetEligiblePlayers(
- _playerManager.Sessions,
- component.PatientZeroPrototypeId,
- acceptableAntags: Shared.Antag.AntagAcceptability.All,
- includeAllJobs: false ,
- ignorePreferences: true,
- customExcludeCondition: HasComp<ZombieImmuneComponent>
- );
-
- //If there are no players to choose, abort
- if (allPlayers.Count == 0)
- return;
-
- //How many initial infected should we select
- var initialInfectedCount = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerInfected, component.MaxInitialInfected);
-
- //Choose the required number of initial infected from the eligible players, making up any shortfall by choosing from all players
- var initialInfected = _antagSelection.ChooseAntags(initialInfectedCount, eligiblePlayers, allPlayers);
-
- //Make brain craving
- MakeZombie(initialInfected, component);
-
- //Send the briefing, play greeting sound
- _antagSelection.SendBriefing(initialInfected, Loc.GetString("zombie-patientzero-role-greeting"), Color.Plum, component.InitialInfectedSound);
- }
-
- private void MakeZombie(List<EntityUid> entities, ZombieRuleComponent component)
- {
- foreach (var entity in entities)
- {
- MakeZombie(entity, component);
- }
- }
- private void MakeZombie(EntityUid entity, ZombieRuleComponent component)
- {
- if (!_mindSystem.TryGetMind(entity, out var mind, out var mindComponent))
- return;
-
- //Add the role to the mind silently (to avoid repeating job assignment)
- _roles.MindAddRole(mind, new InitialInfectedRoleComponent { PrototypeId = component.PatientZeroPrototypeId }, silent: true);
- EnsureComp<InitialInfectedComponent>(entity);
-
- //Add the zombie components and grace period
- var pending = EnsureComp<PendingZombieComponent>(entity);
- pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
- EnsureComp<ZombifyOnDeathComponent>(entity);
- EnsureComp<IncurableZombieComponent>(entity);
-
- //Add the zombify action
- _action.AddAction(entity, ref pending.Action, component.ZombifySelfActionPrototype, entity);
-
- //Get names for the round end screen, incase they leave mid-round
- var inCharacterName = MetaData(entity).EntityName;
- var accountName = mindComponent.Session == null ? string.Empty : mindComponent.Session.Name;
- component.InitialInfectedNames.Add(inCharacterName, accountName);
- }
}
using Content.Server.GameTicking;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Mind;
using Content.Server.Shuttles.Systems;
using Content.Shared.Cuffs.Components;
using Content.Shared.Mind;
-using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Content.Shared.Random;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
+using Content.Server.GameTicking.Components;
using System.Text;
+using Robust.Server.Player;
namespace Content.Server.Objectives;
{
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
public override void Initialize()
.ThenByDescending(x => x.completedObjectives);
foreach (var (summary, _, _) in sortedAgents)
+ {
result.AppendLine(summary);
+ }
}
public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
return null;
var name = mind.CharacterName;
- _mind.TryGetSession(mindId, out var session);
- var username = session?.Name;
+ var username = (string?) null;
+
+ if (mind.OriginalOwnerUserId != null &&
+ _player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData))
+ {
+ username = sessionData.UserName;
+ }
+
if (username != null)
{
using Robust.Shared.Utility;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
+using Content.Server.GameTicking.Components;
namespace Content.Server.Power.EntitySystems;
}
}
- // Designates a supplied entity as a 'collection master'. Other entities which share this
- // entities collection name and are attached on the same load network are assigned this entity
+ // Designates a supplied entity as a 'collection master'. Other entities which share this
+ // entities collection name and are attached on the same load network are assigned this entity
// as the master that represents them on the console UI. This way you can have one device
// represent multiple connected devices
private void AssignEntityAsCollectionMaster
bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences);
PlayerPreferences GetPreferences(NetUserId userId);
+ PlayerPreferences? GetPreferencesOrNull(NetUserId? userId);
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
bool HavePreferencesLoaded(ICommonSession session);
}
return prefs;
}
+ /// <summary>
+ /// Retrieves preferences for the given username from storage or returns null.
+ /// Creates and saves default preferences if they are not found, then returns them.
+ /// </summary>
+ public PlayerPreferences? GetPreferencesOrNull(NetUserId? userId)
+ {
+ if (userId == null)
+ return null;
+
+ if (_cachedPlayerPrefs.TryGetValue(userId.Value, out var pref))
+ return pref.Prefs;
+ return null;
+ }
+
private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId)
{
var prefs = await _db.GetPlayerPreferencesAsync(userId);
-using Content.Shared.Dataset;
+using Content.Shared.Dataset;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
var outputSegments = new List<string>();
foreach (var segment in segments)
{
- outputSegments.Add(_prototype.TryIndex<DatasetPrototype>(segment, out var proto)
- ? Loc.GetString(_random.Pick(proto.Values))
- : Loc.GetString(segment));
+ if (_prototype.TryIndex<DatasetPrototype>(segment, out var proto))
+ outputSegments.Add(_random.Pick(proto.Values));
+ else if (Loc.TryGetString(segment, out var localizedSegment))
+ outputSegments.Add(localizedSegment);
+ else
+ outputSegments.Add(segment);
}
return string.Join(separator, outputSegments);
}
using System.Numerics;
using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Spawners.Components;
using JetBrains.Annotations;
using System.Linq;
using Content.Server.Administration;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
+++ /dev/null
-using Content.Server.StationEvents.Events;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(LoneOpsSpawnRule))]
-public sealed partial class LoneOpsSpawnRuleComponent : Component
-{
- [DataField("loneOpsShuttlePath")]
- public string LoneOpsShuttlePath = "Maps/Shuttles/striker.yml";
-
- [DataField("gameRuleProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string GameRuleProto = "Nukeops";
-
- [DataField("additionalRule")]
- public EntityUid? AdditionalRule;
-}
using Content.Server.Anomaly;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Content.Server.StationEvents.Components;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Robust.Shared.Random;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
using Content.Server.Resist;
using Content.Server.Station.Components;
using Content.Server.StationEvents.Components;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Station.Components;
using System.Linq;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Content.Server.StationEvents.Components;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Server.StationRecords;
using Content.Server.StationRecords.Systems;
using System.Linq;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using JetBrains.Annotations;
using Content.Server.Atmos.EntitySystems;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Robust.Shared.Audio;
using System.Numerics;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.ImmovableRod;
using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking.Components;
using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
using Content.Server.Silicons.Laws;
using Content.Server.Station.Components;
using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
+++ /dev/null
-using Robust.Server.GameObjects;
-using Robust.Server.Maps;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
-using Content.Server.RoundEnd;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class LoneOpsSpawnRule : StationEventSystem<LoneOpsSpawnRuleComponent>
-{
- [Dependency] private readonly MapLoaderSystem _map = default!;
-
- protected override void Started(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
- {
- base.Started(uid, component, gameRule, args);
-
- // Loneops can only spawn if there is no nukeops active
- if (GameTicker.IsGameRuleAdded<NukeopsRuleComponent>())
- {
- ForceEndSelf(uid, gameRule);
- return;
- }
-
- var shuttleMap = MapManager.CreateMap();
- var options = new MapLoadOptions
- {
- LoadMap = true,
- };
-
- _map.TryLoad(shuttleMap, component.LoneOpsShuttlePath, out _, options);
-
- var nukeopsEntity = GameTicker.AddGameRule(component.GameRuleProto);
- component.AdditionalRule = nukeopsEntity;
- var nukeopsComp = Comp<NukeopsRuleComponent>(nukeopsEntity);
- nukeopsComp.SpawnOutpost = false;
- nukeopsComp.RoundEndBehavior = RoundEndBehavior.Nothing;
- GameTicker.StartGameRule(nukeopsEntity);
- }
-
- protected override void Ended(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
- {
- base.Ended(uid, component, gameRule, args);
-
- if (component.AdditionalRule != null)
- GameTicker.EndGameRule(component.AdditionalRule.Value);
- }
-}
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Server.Traits.Assorted;
using System.Numerics;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Robust.Shared.Map;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Ninja.Systems;
using Content.Server.Station.Components;
using System.Threading;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Server.Storage.Components;
using System.Linq;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Ghost.Roles.Components;
using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Radio;
using Robust.Shared.Random;
using Content.Server.Administration.Logs;
using Content.Server.Chat.Systems;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Systems;
using Robust.Shared.Random;
using System.Linq;
using Content.Server.Fluids.EntitySystems;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking.Components;
using Content.Server.StationEvents.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
-using Content.Server.GameTicking.Rules;
+using Content.Server.Antag;
using Content.Server.Traitor.Components;
using Content.Shared.Mind.Components;
+using Robust.Shared.Prototypes;
namespace Content.Server.Traitor.Systems;
/// </summary>
public sealed class AutoTraitorSystem : EntitySystem
{
- [Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
+
+ [ValidatePrototypeId<EntityPrototype>]
+ private const string DefaultTraitorRule = "Traitor";
public override void Initialize()
{
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
{
- TryMakeTraitor(uid, comp);
- }
-
- /// <summary>
- /// Sets the GiveUplink field.
- /// </summary>
- public void SetGiveUplink(EntityUid uid, bool giveUplink, AutoTraitorComponent? comp = null)
- {
- if (!Resolve(uid, ref comp))
- return;
-
- comp.GiveUplink = giveUplink;
- }
-
- /// <summary>
- /// Sets the GiveObjectives field.
- /// </summary>
- public void SetGiveObjectives(EntityUid uid, bool giveObjectives, AutoTraitorComponent? comp = null)
- {
- if (!Resolve(uid, ref comp))
- return;
-
- comp.GiveObjectives = giveObjectives;
- }
-
- /// <summary>
- /// Checks if there is a mind, then makes it a traitor using the options.
- /// </summary>
- public bool TryMakeTraitor(EntityUid uid, AutoTraitorComponent? comp = null)
- {
- if (!Resolve(uid, ref comp))
- return false;
-
- //Start the rule if it has not already been started
- var traitorRuleComponent = _traitorRule.StartGameRule();
- _traitorRule.MakeTraitor(uid, traitorRuleComponent, giveUplink: comp.GiveUplink, giveObjectives: comp.GiveObjectives);
- // prevent spamming anything if it fails
- RemComp<AutoTraitorComponent>(uid);
- return true;
+ _antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule);
}
}
uplinkEntity = eUid;
}
- // Get TC count
- var tcCount = _cfgManager.GetCVar(CCVars.TraitorStartingBalance);
- Logger.Debug(_entManager.ToPrettyString(user));
// Finally add uplink
var uplinkSys = _entManager.System<UplinkSystem>();
- if (!uplinkSys.AddUplink(user, FixedPoint2.New(tcCount), uplinkEntity: uplinkEntity))
+ if (!uplinkSys.AddUplink(user, 20, uplinkEntity: uplinkEntity))
{
shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
}
using Content.Shared.Damage;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Zombies;
[DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan GracePeriod = TimeSpan.Zero;
+ /// <summary>
+ /// The minimum amount of time initial infected have before they start taking infection damage.
+ /// </summary>
+ [DataField]
+ public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
+
+ /// <summary>
+ /// The maximum amount of time initial infected have before they start taking damage.
+ /// </summary>
+ [DataField]
+ public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
+
+ [DataField]
+ public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
+
/// <summary>
/// The chance each second that a warning will be shown.
/// </summary>
using System.Linq;
+using Content.Server.Actions;
using Content.Server.Body.Systems;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
}
component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f);
+ component.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
+ _actions.AddAction(uid, ref component.Action, component.ZombifySelfActionPrototype);
}
public override void Update(float frameTime)
All
}
+public enum AntagSelectionTime : byte
+{
+ PrePlayerSpawn,
+ PostPlayerSpawn
+}
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
-
- /*
- * Suspicion
- */
-
- public static readonly CVarDef<int> SuspicionMinPlayers =
- CVarDef.Create("suspicion.min_players", 5);
-
- public static readonly CVarDef<int> SuspicionMinTraitors =
- CVarDef.Create("suspicion.min_traitors", 2);
-
- public static readonly CVarDef<int> SuspicionPlayersPerTraitor =
- CVarDef.Create("suspicion.players_per_traitor", 6);
-
- public static readonly CVarDef<int> SuspicionStartingBalance =
- CVarDef.Create("suspicion.starting_balance", 20);
-
- public static readonly CVarDef<int> SuspicionMaxTimeSeconds =
- CVarDef.Create("suspicion.max_time_seconds", 300);
-
- /*
- * Traitor
- */
-
- public static readonly CVarDef<int> TraitorMinPlayers =
- CVarDef.Create("traitor.min_players", 5);
-
- public static readonly CVarDef<int> TraitorMaxTraitors =
- CVarDef.Create("traitor.max_traitors", 12); // Assuming average server maxes somewhere from like 50-80 people
-
- public static readonly CVarDef<int> TraitorPlayersPerTraitor =
- CVarDef.Create("traitor.players_per_traitor", 10);
-
- public static readonly CVarDef<int> TraitorCodewordCount =
- CVarDef.Create("traitor.codeword_count", 4);
-
- public static readonly CVarDef<int> TraitorStartingBalance =
- CVarDef.Create("traitor.starting_balance", 20);
-
- public static readonly CVarDef<int> TraitorMaxDifficulty =
- CVarDef.Create("traitor.max_difficulty", 5);
-
- public static readonly CVarDef<int> TraitorMaxPicks =
- CVarDef.Create("traitor.max_picks", 20);
-
- public static readonly CVarDef<float> TraitorStartDelay =
- CVarDef.Create("traitor.start_delay", 4f * 60f);
-
- public static readonly CVarDef<float> TraitorStartDelayVariance =
- CVarDef.Create("traitor.start_delay_variance", 3f * 60f);
-
- /*
- * TraitorDeathMatch
- */
-
- public static readonly CVarDef<int> TraitorDeathMatchStartingBalance =
- CVarDef.Create("traitordm.starting_balance", 20);
-
- /*
- * Zombie
- */
-
- public static readonly CVarDef<int> ZombieMinPlayers =
- CVarDef.Create("zombie.min_players", 20);
-
- /*
- * Pirates
- */
-
- public static readonly CVarDef<int> PiratesMinPlayers =
- CVarDef.Create("pirates.min_players", 25);
-
- public static readonly CVarDef<int> PiratesMaxOps =
- CVarDef.Create("pirates.max_pirates", 6);
-
- public static readonly CVarDef<int> PiratesPlayersPerOp =
- CVarDef.Create("pirates.players_per_pirate", 5);
-
- /*
- * Nukeops
- */
-
- public static readonly CVarDef<bool> NukeopsSpawnGhostRoles =
- CVarDef.Create("nukeops.spawn_ghost_roles", false);
-
/*
* Tips
*/
/// <param name="uid">The mob's entity UID.</param>
/// <param name="profile">The character profile to load.</param>
/// <param name="humanoid">Humanoid component of the entity</param>
- public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null)
+ public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
{
+ if (profile == null)
+ return;
+
if (!Resolve(uid, ref humanoid))
{
return;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using Content.Shared.Hands.Components;
using Content.Shared.Storage.EntitySystems;
-using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.Shared.Inventory;
/// </summary>
/// <param name="entity">The entity that you want to spawn an item on</param>
/// <param name="items">A list of prototype IDs that you want to spawn in the bag.</param>
- public void SpawnItemsOnEntity(EntityUid entity, List<EntProtoId> items)
+ public void SpawnItemsOnEntity(EntityUid entity, List<string> items)
{
foreach (var item in items)
{
[RegisterComponent, NetworkedComponent]
public sealed partial class NukeOperativeComponent : Component
{
- /// <summary>
- /// Path to antagonist alert sound.
- /// </summary>
- [DataField("greetSoundNotification")]
- public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
/// <summary>
- ///
+ ///
/// </summary>
[DataField("syndStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))]
public string SyndStatusIcon = "SyndicateFaction";
_antagTypes.Add(typeof(T));
}
+ public void MindAddRole(EntityUid mindId, Component component, MindComponent? mind = null, bool silent = false)
+ {
+ if (!Resolve(mindId, ref mind))
+ return;
+
+ if (HasComp(mindId, component.GetType()))
+ {
+ throw new ArgumentException($"We already have this role: {component}");
+ }
+
+ EntityManager.AddComponent(mindId, component);
+ var antagonist = IsAntagonistRole(component.GetType());
+
+ var mindEv = new MindRoleAddedEvent(silent);
+ RaiseLocalEvent(mindId, ref mindEv);
+
+ var message = new RoleAddedEvent(mindId, mind, antagonist, silent);
+ if (mind.OwnedEntity != null)
+ {
+ RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
+ }
+
+ _adminLogger.Add(LogType.Mind, LogImpact.Low,
+ $"'Role {component}' added to mind of {_minds.MindOwnerLoggingString(mind)}");
+ }
+
/// <summary>
/// Gives this mind a new role.
/// </summary>
return _antagTypes.Contains(typeof(T));
}
+ public bool IsAntagonistRole(Type component)
+ {
+ return _antagTypes.Contains(component);
+ }
+
/// <summary>
/// Play a sound for the mind, if it has a session attached.
/// Use this for role greeting sounds.
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
-using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Collections;
+using Robust.Shared.Prototypes;
namespace Content.Shared.Station;
public abstract class SharedStationSpawningSystem : EntitySystem
{
+ [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[Dependency] protected readonly InventorySystem InventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
/// </summary>
/// <param name="entity">Entity to load out.</param>
/// <param name="startingGear">Starting gear to use.</param>
- public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear)
+ public void EquipStartingGear(EntityUid entity, ProtoId<StartingGearPrototype>? startingGear)
{
+ PrototypeManager.TryIndex(startingGear, out var gearProto);
+ EquipStartingGear(entity, gearProto);
+ }
+
+ /// <summary>
+ /// Equips starting gear onto the given entity.
+ /// </summary>
+ /// <param name="entity">Entity to load out.</param>
+ /// <param name="startingGear">Starting gear to use.</param>
+ public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingGear)
+ {
+ if (startingGear == null)
+ return;
+
if (InventorySystem.TryGetSlots(entity, out var slotDefinitions))
{
foreach (var slot in slotDefinitions)
+++ /dev/null
-pirates-title = Privateers
-pirates-description = A group of privateers has approached your lowly station. Hostile or not, their sole goal is to end the round with as many knicknacks on their ship as they can get.
-
-pirates-no-ship = Through unknown circumstances, the privateer's ship was completely and utterly destroyed. No score.
-pirates-final-score = The privateers successfully obtained {$score} spesos worth
-pirates-final-score-2 = of knicknacks, with a total of {$finalPrice} spesos.
-pirates-list-start = The privateers were:
-pirates-most-valuable = The most valuable stolen items were:
-pirates-stolen-item-entry = {$entity} ({$credits} spesos)
-pirates-stole-nothing = - The pirates stole absolutely nothing at all. Point and laugh.
- type: Transform
pos: 0.5436061,-7.5129323
parent: 325
-- proto: SpawnPointLoneNukeOperative
+- proto: SpawnPointNukies
entities:
- uid: 322
components:
name: ghost-role-information-loneop-name
description: ghost-role-information-loneop-description
rules: ghost-role-information-loneop-rules
- - type: GhostRoleMobSpawner
- prototype: MobHumanLoneNuclearOperative
+ - type: GhostRoleAntagSpawner
- type: Sprite
sprite: Markers/jobs.rsi
layers:
weight: 3
duration: 1
- type: ZombieRule
- minStartDelay: 0 #let them know immediately
- maxStartDelay: 10
- maxInitialInfected: 3 #fewer zombies
- minInitialInfectedGrace: 300 #less time to prepare
- maxInitialInfectedGrace: 450
+ - type: AntagSelection
+ definitions:
+ - prefRoles: [ InitialInfected ]
+ max: 3
+ playerRatio: 10
+ blacklist:
+ components:
+ - ZombieImmune
+ - InitialInfectedExempt
+ briefing:
+ text: zombie-patientzero-role-greeting
+ color: Plum
+ sound: "/Audio/Ambience/Antag/zombie_start.ogg"
+ components:
+ - type: PendingZombie #less time to prepare than normal
+ minInitialInfectedGrace: 300
+ maxInitialInfectedGrace: 450
+ - type: ZombifyOnDeath
+ - type: IncurableZombie
+ mindComponents:
+ - type: InitialInfectedRole
+ prototype: InitialInfected
- type: entity
id: LoneOpsSpawn
minimumPlayers: 20
reoccurrenceDelay: 30
duration: 1
- - type: LoneOpsSpawnRule
+ - type: LoadMapRule
+ mapPath: /Maps/Shuttles/striker.yml
+ - type: NukeopsRule
+ roundEndBehavior: Nothing
+ - type: AntagSelection
+ definitions:
+ - spawnerPrototype: SpawnPointLoneNukeOperative
+ min: 1
+ max: 1
+ pickPlayer: false
+ startingGear: SyndicateLoneOperativeGearFull
+ components:
+ - type: NukeOperative
+ - type: RandomMetadata
+ nameSegments:
+ - SyndicateNamesPrefix
+ - SyndicateNamesNormal
+ - type: NpcFactionMember
+ factions:
+ - Syndicate
+ mindComponents:
+ - type: NukeopsRole
+ prototype: Nukeops
- type: entity
id: MassHallucinations
id: Thief
components:
- type: ThiefRule
+ - type: AntagSelection
+ definitions:
+ - prefRoles: [ Thief ]
+ maxRange:
+ min: 1
+ max: 3
+ playerRatio: 1
+ allowNonHumans: true
+ multiAntagSetting: All
+ startingGear: ThiefGear
+ components:
+ - type: Pacified
+ mindComponents:
+ - type: ThiefRole
+ prototype: Thief
+ briefing:
+ sound: "/Audio/Misc/thief_greeting.ogg"
- type: entity
noSpawn: true
components:
- type: GameRule
minPlayers: 20
+ - type: RandomMetadata #this generates the random operation name cuz it's cool.
+ nameSegments:
+ - operationPrefix
+ - operationSuffix
- type: NukeopsRule
- faction: Syndicate
-
-- type: entity
- id: Pirates
- parent: BaseGameRule
- noSpawn: true
- components:
- - type: PiratesRule
+ - type: LoadMapRule
+ gameMap: NukieOutpost
+ - type: AntagSelection
+ selectionTime: PrePlayerSpawn
+ definitions:
+ - prefRoles: [ NukeopsCommander ]
+ fallbackRoles: [ Nukeops, NukeopsMedic ]
+ max: 1
+ playerRatio: 10
+ startingGear: SyndicateCommanderGearFull
+ components:
+ - type: NukeOperative
+ - type: RandomMetadata
+ nameSegments:
+ - nukeops-role-commander
+ - SyndicateNamesElite
+ - type: NpcFactionMember
+ factions:
+ - Syndicate
+ mindComponents:
+ - type: NukeopsRole
+ prototype: NukeopsCommander
+ - prefRoles: [ NukeopsMedic ]
+ fallbackRoles: [ Nukeops, NukeopsCommander ]
+ max: 1
+ playerRatio: 10
+ startingGear: SyndicateOperativeMedicFull
+ components:
+ - type: NukeOperative
+ - type: RandomMetadata
+ nameSegments:
+ - nukeops-role-agent
+ - SyndicateNamesNormal
+ - type: NpcFactionMember
+ factions:
+ - Syndicate
+ mindComponents:
+ - type: NukeopsRole
+ prototype: NukeopsMedic
+ - prefRoles: [ Nukeops ]
+ fallbackRoles: [ NukeopsCommander, NukeopsMedic ]
+ min: 0
+ max: 3
+ playerRatio: 10
+ startingGear: SyndicateOperativeGearFull
+ components:
+ - type: NukeOperative
+ - type: RandomMetadata
+ nameSegments:
+ - nukeops-role-operator
+ - SyndicateNamesNormal
+ - type: NpcFactionMember
+ factions:
+ - Syndicate
+ mindComponents:
+ - type: NukeopsRole
+ prototype: Nukeops
- type: entity
id: Traitor
parent: BaseGameRule
noSpawn: true
components:
+ - type: GameRule
+ minPlayers: 5
+ delay:
+ min: 240
+ max: 420
- type: TraitorRule
+ - type: AntagSelection
+ definitions:
+ - prefRoles: [ Traitor ]
+ max: 12
+ playerRatio: 10
+ lateJoinAdditional: true
+ mindComponents:
+ - type: TraitorRole
+ prototype: Traitor
- type: entity
id: Revolutionary
parent: BaseGameRule
noSpawn: true
components:
+ - type: GameRule
+ minPlayers: 15
- type: RevolutionaryRule
+ - type: AntagSelection
+ definitions:
+ - prefRoles: [ HeadRev ]
+ max: 3
+ playerRatio: 15
+ briefing:
+ text: head-rev-role-greeting
+ color: CornflowerBlue
+ sound: "/Audio/Ambience/Antag/headrev_start.ogg"
+ startingGear: HeadRevGear
+ components:
+ - type: Revolutionary
+ - type: HeadRevolutionary
+ mindComponents:
+ - type: RevolutionaryRole
+ prototype: HeadRev
- type: entity
id: Sandbox
parent: BaseGameRule
noSpawn: true
components:
+ - type: GameRule
+ minPlayers: 20
+ delay:
+ min: 600
+ max: 900
- type: ZombieRule
+ - type: AntagSelection
+ definitions:
+ - prefRoles: [ InitialInfected ]
+ max: 6
+ playerRatio: 10
+ blacklist:
+ components:
+ - ZombieImmune
+ - InitialInfectedExempt
+ briefing:
+ text: zombie-patientzero-role-greeting
+ color: Plum
+ sound: "/Audio/Ambience/Antag/zombie_start.ogg"
+ components:
+ - type: PendingZombie
+ - type: ZombifyOnDeath
+ - type: IncurableZombie
+ mindComponents:
+ - type: InitialInfectedRole
+ prototype: InitialInfected
# event schedulers
- type: entity
- id: BasicTrashVariationPass
- id: SolidWallRustingVariationPass
- id: ReinforcedWallRustingVariationPass
- - id: CutWireVariationPass
- id: BasicPuddleMessVariationPass
prob: 0.99
orGroup: puddleMess
#Head Rev Gear
- type: startingGear
id: HeadRevGear
- equipment:
- pocket2: Flash
+ storage:
+ back:
+ - Flash
+ - ClothingEyesGlassesSunglasses
+
+#Thief Gear
+- type: startingGear
+ id: ThiefGear
+ storage:
+ back:
+ - ToolboxThief
+ - ClothingHandsChameleonThief
#Gladiator with spear
- type: startingGear
- Zombie
- BasicStationEventScheduler
- BasicRoundstartVariation
-
-- type: gamePreset
- id: Pirates
- alias:
- - pirates
- name: pirates-title
- description: pirates-description
- showInVote: false
- rules:
- - Pirates
- - BasicStationEventScheduler
- - BasicRoundstartVariation