Mega Antag Refactor (#25786)

* Mega Antag Refactor

* last minute delta save

* more workshopping

* more shit

* ok tested this for once

* okkkkk sure

* generic delays for starting rules

* well darn

* nukies partially

* ouagh

* ballin' faded and smonkin wed

* obliterated the diff

* Spread my arms and soak up congratulations

* I've got plenty of love, but nothing to show for it

* but there’s too much sunlight
Shining on my laptop monitor, so I
Can’t see anything with any amount of clarity

* ok this junk

* OOK!

* fubar

* most of sloth's review

* oh boy

* eek

* hell yea!

* ASDFJASDJFvsakcvjkzjnhhhyh
This commit is contained in:
Nemanja
2024-04-24 21:31:45 -04:00
committed by GitHub
parent 771661f478
commit 161fd6c83c
99 changed files with 1931 additions and 2310 deletions

View File

@@ -108,8 +108,11 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
/// 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;

View File

@@ -112,22 +112,38 @@ public sealed class NukeOpsTest
// 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();
@@ -135,8 +151,8 @@ public sealed class NukeOpsTest
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.
@@ -149,8 +165,8 @@ public sealed class NukeOpsTest
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.

View File

@@ -1,5 +1,6 @@
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;

View File

@@ -17,6 +17,7 @@ public sealed class SecretStartsTest
var server = pair.Server;
await server.WaitIdleAsync();
var entMan = server.ResolveDependency<IEntityManager>();
var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
await server.WaitAssertion(() =>
@@ -32,10 +33,7 @@ public sealed class SecretStartsTest
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();

View File

@@ -8,6 +8,7 @@ using System.Text.Json.Nodes;
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;

View File

@@ -1,23 +1,37 @@
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)
@@ -40,9 +54,7 @@ public sealed partial class AdminVerbSystem
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"),
@@ -71,7 +83,7 @@ public sealed partial class AdminVerbSystem
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"),
@@ -85,14 +97,14 @@ public sealed partial class AdminVerbSystem
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"),
@@ -100,7 +112,7 @@ public sealed partial class AdminVerbSystem
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"),
@@ -114,7 +126,7 @@ public sealed partial class AdminVerbSystem
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"),

View File

@@ -0,0 +1,29 @@
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);
}

View File

@@ -0,0 +1,302 @@
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);
}
}

View File

@@ -1,347 +1,444 @@
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.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
using Content.Shared.Chat;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
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)
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
}
private void OnTakeGhostRole(Entity<GhostRoleAntagSpawnerComponent> ent, ref TakeGhostRoleEvent args)
{
if (args.TookRole)
return;
if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } def)
return;
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 (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition))
eligiblePlayers.Add(player.AttachedEntity!.Value);
}
return eligiblePlayers;
}
/// <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)
{
var eligibleSessions = new List<ICommonSession>();
foreach (var session in playerSessions)
{
if (IsSessionEligible(session, antagPrototype, ignorePreferences))
eligibleSessions.Add(session);
}
return eligibleSessions;
}
/// <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)
{
if (!IsSessionEligible(session, antagPrototype, ignorePreferences))
return false;
//Ensure the player has a mind
if (session.GetMind() is not { } playerMind)
return false;
//Ensure the player has an attached entity
if (session.AttachedEntity is not { } playerEntity)
return false;
//Ignore latejoined players, ie those on the arrivals station
if (HasComp<PendingClockInComponent>(playerEntity))
return false;
//Exclude jobs that cannot be antag, unless explicitly allowed
if (!includeAllJobs && !_jobs.CanBeAntag(session))
return false;
//Check if the entity is already an antag
switch (acceptableAntags)
{
//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;
}
}
//Unless explictly allowed, ignore non humanoids (eg pets)
if (!allowNonHumanoids && !HasComp<HumanoidAppearanceComponent>(playerEntity))
return false;
//If a custom condition was provided, test it and exclude the player if it returns true
if (customExcludeCondition != null && customExcludeCondition(playerEntity))
return false;
return true;
}
/// <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)
{
//Exclude disconnected or zombie sessions
//No point giving antag roles to them
if (session.Status == SessionStatus.Disconnected ||
session.Status == SessionStatus.Zombie)
return false;
//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;
return true;
}
#endregion
/// <summary>
/// Helper method to calculate the number of antags to select based upon the number 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)
{
return Math.Clamp(playerCount / playersPerAntag, 1, maxAntags);
}
#region Antag Selection
/// <summary>
/// Selects a set number of entities from several lists, prioritising the first list till its empty, then second list etc
/// </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)
{
var chosenPlayers = new List<EntityUid>();
foreach (var playerList in eligiblePlayerLists)
{
//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)
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
continue;
//Pick and choose a random number of players from this list
chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
if (comp.SelectionsComplete)
return;
ChooseAntags((uid, comp), pool);
comp.SelectionsComplete = true;
foreach (var session in comp.SelectedSessions)
{
args.PlayerPool.Remove(session);
GameTicker.PlayerJoinGame(session);
}
}
return chosenPlayers;
}
/// <summary>
/// Helper method to choose antags from a list
/// </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)
private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
{
var chosenPlayers = new List<EntityUid>();
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
continue;
if (comp.SelectionsComplete)
continue;
ChooseAntags((uid, comp));
comp.SelectionsComplete = true;
}
}
private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
{
if (!args.LateJoin)
return;
// 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.
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var antag, out _))
{
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);
}
}
protected override void Added(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
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);
}
if (def.MaxRange != null)
{
def.Max = def.MaxRange.Value.Next(RobustRandom);
}
}
}
protected override void Started(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
if (component.SelectionsComplete)
return;
if (GameTicker.RunLevel != GameRunLevel.InRound)
return;
if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
return;
ChooseAntags((uid, component));
component.SelectionsComplete = true;
}
/// <summary>
/// Chooses antagonists from the current selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent)
{
var sessions = _playerManager.Sessions.ToList();
ChooseAntags(ent, sessions);
}
/// <summary>
/// Chooses antagonists from the given selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool)
{
foreach (var def in ent.Comp.Definitions)
{
ChooseAntags(ent, pool, def);
}
}
/// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool, AntagSelectionDefinition def)
{
var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, playerPool, def);
for (var i = 0; i < count; i++)
{
if (eligiblePlayers.Count == 0)
break;
chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
}
return chosenPlayers;
}
/// <summary>
/// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc
/// </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)
{
var chosenPlayers = new List<ICommonSession>();
foreach (var playerList in eligiblePlayerLists)
{
//Remove all chosen players from this list, to prevent duplicates
foreach (var chosenPlayer in chosenPlayers)
var session = (ICommonSession?) null;
if (def.PickPlayer)
{
playerList.Remove(chosenPlayer);
if (!playerPool.TryPickAndTake(RobustRandom, out session))
break;
if (ent.Comp.SelectedSessions.Contains(session))
continue;
}
//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));
MakeAntag(ent, session, def);
}
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++)
/// <summary>
/// Makes a given player into the specified antagonist.
/// </summary>
public void MakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
{
var antagEnt = (EntityUid?) null;
var isSpawner = false;
if (session != null)
{
if (eligiblePlayers.Count == 0)
break;
ent.Comp.SelectedSessions.Add(session);
chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
// we shouldn't be blocking the entity if they're just a ghost or smth.
if (!HasComp<GhostComponent>(session.AttachedEntity))
antagEnt = session.AttachedEntity;
}
return chosenPlayers;
}
#endregion
#region Briefings
/// <summary>
/// Helper method to send the briefing text and sound to a list of entities
/// </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)
{
foreach (var entity in entities)
else if (!ignoreSpawner && def.SpawnerPrototype != null) // don't add spawners if we have a player, dummy.
{
SendBriefing(entity, briefing, briefingColor, briefingSound);
antagEnt = Spawn(def.SpawnerPrototype);
isSpawner = true;
}
}
/// <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 (!_mindSystem.TryGetMind(entity, out _, out var mindComponent))
if (!antagEnt.HasValue)
{
var getEntEv = new AntagSelectEntityEvent(session, ent);
RaiseLocalEvent(ent, ref getEntEv, true);
if (!getEntEv.Handled)
{
throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
}
antagEnt = getEntEv.Entity;
}
if (antagEnt is not { } player)
return;
if (mindComponent.Session == null)
return;
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);
}
SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound);
if (isSpawner)
{
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);
}
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);
}
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
RaiseLocalEvent(ent, ref afterEv, true);
}
/// <summary>
/// Helper method to send the briefing text and sound to a list of sessions
/// Gets an ordered player pool based on player preferences and the antagonist definition.
/// </summary>
/// <param name="sessions"></param>
/// <param name="briefing"></param>
/// <param name="briefingColor"></param>
/// <param name="briefingSound"></param>
public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
{
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(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="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>
if (!IsSessionValid(ent, session, def) ||
!IsEntityValid(session.AttachedEntity, def))
{
rawList.Add(session);
continue;
}
public void SendBriefing(ICommonSession session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
{
_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);
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>
/// Checks if a given session is valid for an antagonist.
/// </summary>
public bool IsSessionValid(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def, EntityUid? mind = null)
{
mind ??= session.GetMind();
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
return false;
if (ent.Comp.SelectedSessions.Contains(session))
return false;
//todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
switch (def.MultiAntagSetting)
{
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>
/// Checks if a given entity (mind/session not included) is valid for a given antagonist.
/// </summary>
private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
{
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);

View File

@@ -0,0 +1,189 @@
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;
}

View File

@@ -0,0 +1,14 @@
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;
}

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,4 +1,6 @@
namespace Content.Server.Destructible.Thresholds
using Robust.Shared.Random;
namespace Content.Server.Destructible.Thresholds
{
[Serializable]
[DataDefinition]
@@ -9,5 +11,16 @@
[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);
}
}
}

View File

@@ -1,4 +1,4 @@
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"/>.

View File

@@ -0,0 +1,16 @@
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;
}

View File

@@ -1,4 +1,4 @@
namespace Content.Server.GameTicking.Rules.Components;
namespace Content.Server.GameTicking.Components;
/// <summary>
/// Added to game rules before <see cref="GameRuleEndedEvent"/>.

View File

@@ -1,6 +1,7 @@
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.
@@ -20,6 +21,12 @@ public sealed partial class GameRuleComponent : Component
/// </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>

View File

@@ -1,6 +1,6 @@
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;
@@ -102,6 +102,22 @@ public sealed partial class GameTicker
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)}");
@@ -255,6 +271,18 @@ public sealed partial class GameTicker
}
}
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)]
@@ -323,38 +351,3 @@ public sealed partial class GameTicker
#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;
}
}
*/

View File

@@ -133,6 +133,7 @@ namespace Content.Server.GameTicking
return;
base.Update(frameTime);
UpdateRoundFlow(frameTime);
UpdateGameRules();
}
}
}

View File

@@ -0,0 +1,29 @@
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;
}

View File

@@ -8,7 +8,7 @@ namespace Content.Server.GameTicking.Rules.Components;
/// <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

View File

@@ -1,6 +1,3 @@
using Content.Shared.Roles;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
@@ -9,11 +6,5 @@ namespace Content.Server.GameTicking.Rules.Components;
/// 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!;
}

View File

@@ -6,4 +6,6 @@
[RegisterComponent]
public sealed partial class NukeOpsShuttleComponent : Component
{
[DataField]
public EntityUid AssociatedRule;
}

View File

@@ -1,27 +1,16 @@
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>
@@ -52,12 +41,6 @@ public sealed partial class NukeopsRuleComponent : Component
[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>
@@ -80,7 +63,7 @@ public sealed partial class NukeopsRuleComponent : Component
/// 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
@@ -94,50 +77,23 @@ public sealed partial class NukeopsRuleComponent : Component
[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;
[DataField]
public EntityUid? TargetStation;
public MapId? NukiePlanet;
[DataField]
public ProtoId<NpcFactionPrototype> Faction = "Syndicate";
/// <summary>
/// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
/// Path to antagonist alert sound.
/// </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" };
[DataField]
public NukeopSpawnPreset AgentSpawnDetails = new() { AntagRoleProto = "NukeopsMedic", GearProto = "SyndicateOperativeMedicFull", NamePrefix = "nukeops-role-agent", NameList = "SyndicateNamesNormal" };
[DataField]
public NukeopSpawnPreset OperativeSpawnDetails = new();
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
}
/// <summary>

View File

@@ -1,24 +0,0 @@
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));
}

View File

@@ -22,43 +22,6 @@ public sealed partial class RevolutionaryRuleComponent : Component
[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>

View File

@@ -1,12 +1,11 @@
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
@@ -23,42 +22,9 @@ 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");
}

View File

@@ -57,4 +57,19 @@ public sealed partial class TraitorRuleComponent : Component
/// </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;
}

View File

@@ -8,12 +8,6 @@ namespace Content.Server.GameTicking.Rules.Components;
[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>
@@ -26,61 +20,9 @@ public sealed partial class ZombieRuleComponent : Component
[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";
}

View File

@@ -1,5 +1,6 @@
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;
@@ -33,7 +34,6 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
SubscribeLocalEvent<DeathMatchRuleComponent, PlayerPointChangedEvent>(OnPointChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
}
private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
@@ -113,21 +113,17 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
_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());
}
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Robust.Shared.Collections;
@@ -15,31 +16,6 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
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>

View File

@@ -1,6 +1,6 @@
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;
@@ -22,9 +22,31 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
{
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)
@@ -48,6 +70,12 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
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
@@ -73,6 +101,14 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
}
/// <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>

View File

@@ -1,5 +1,6 @@
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;

View File

@@ -1,4 +1,5 @@
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Shared.Chat;

View File

@@ -0,0 +1,80 @@
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));
}
}
}

View File

@@ -1,5 +1,6 @@
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;

View File

@@ -1,31 +1,20 @@
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;
@@ -33,45 +22,30 @@ using Content.Shared.NPC.Systems;
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";
@@ -79,141 +53,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
[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);
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;
if (GameTicker.RunLevel == GameRunLevel.InRound)
SpawnOperativesForGhostRoles(uid, component);
eligible.Add((eligibleUid, eligibleComp, member));
}
if (eligible.Count == 0)
return;
component.TargetStation = RobustRandom.Pick(eligible);
}
#region Event Handlers
private void OnStartAttempt(RoundStartAttemptEvent ev)
protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
TryRoundStartAttempt(ev, Loc.GetString("nukeops-title"));
}
var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
args.AddLine(winText);
private void OnPlayersSpawning(RulePlayerSpawningEvent ev)
{
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
foreach (var cond in component.WinConditions)
{
if (!SpawnMap((uid, nukeops)))
{
_sawmill.Info("Failed to load map for nukeops");
continue;
}
//Handle there being nobody readied up
if (ev.PlayerPool.Count == 0)
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);
foreach (var nukieSpawn in operatives)
{
if (nukieSpawn.Session == null)
continue;
GameTicker.PlayerJoinGame(nukieSpawn.Session);
}
}
}
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
var ruleQuery = QueryActiveRules();
while (ruleQuery.MoveNext(out _, out _, out var nukeops, out _))
{
var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}");
ev.AddLine(winText);
foreach (var cond in nukeops.WinConditions)
{
var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
ev.AddLine(text);
}
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))
var antags =_antag.GetAntagIdentifiers(uid);
foreach (var (_, sessionData, name) in antags)
{
if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
continue;
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))));
args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName)));
}
}
@@ -224,10 +124,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
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;
}
@@ -242,7 +142,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
}
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
SetWinType(uid, WinType.OpsMajor, nukeops);
SetWinType((uid, nukeops), WinType.OpsMajor);
correctStation = true;
}
@@ -263,19 +163,85 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
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)
@@ -294,66 +260,31 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
CheckRoundShouldEnd();
}
private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent 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);
}
}
private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args)
{
if (!_mind.TryGetMind(uid, out var mindId, out var mind))
return;
var query = QueryActiveRules();
while (query.MoveNext(out _, out _, out var nukeops, out _))
{
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);
}
}
}
private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
{
RemCompDeferred(uid, component);
}
private void OnMapInit(Entity<NukeOpsShuttleComponent> ent, ref MapInitEvent args)
{
var map = Transform(ent).MapID;
var rules = EntityQueryEnumerator<NukeopsRuleComponent, LoadMapRuleComponent>();
while (rules.MoveNext(out var uid, out _, out var mapRule))
{
if (map != mapRule.Map)
continue;
ent.Comp.AssociatedRule = uid;
break;
}
}
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)
@@ -397,12 +328,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
// 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);
@@ -448,161 +379,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
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();
@@ -613,243 +405,130 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
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;
CheckRoundShouldEnd((uid, nukeops));
}
}
// 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;
private void CheckRoundShouldEnd(Entity<NukeopsRuleComponent> ent)
{
var nukeops = ent.Comp;
MapId? shuttleMapId = Exists(nukeops.NukieShuttle)
? Transform(nukeops.NukieShuttle.Value).MapID
if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
return;
// If there are any nuclear bombs that are active, immediately return. We're not over yet.
foreach (var nuke in EntityQuery<NukeComponent>())
{
if (nuke.Status == NukeStatus.ARMED)
return;
}
var shuttle = GetShuttle((ent, ent));
MapId? shuttleMapId = Exists(shuttle)
? Transform(shuttle.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;
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;
}
}
private bool SpawnMap(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;
// 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>
/// Adds missing nuke operative components, equips starting gear and renames the entity.
/// </summary>
private void SetupOperativeEntity(EntityUid mob, string name, NukeopSpawnPreset spawnDetails, HumanoidCharacterProfile? profile)
// this should really go anywhere else but im tired.
private void OnAntagSelectEntity(Entity<NukeopsRuleComponent> ent, ref AntagSelectEntityEvent args)
{
_metaData.SetEntityName(mob, name);
EnsureComp<NukeOperativeComponent>(mob);
if (profile != null)
_humanoid.LoadProfile(mob, profile);
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 (args.Handled)
return;
var spawns = new List<EntityCoordinates>();
foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
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))
{
if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id)
continue;
if (xform.ParentUid != component.NukieOutpost)
continue;
spawns.Add(xform.Coordinates);
break;
species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
}
//Fallback, spawn at the centre of the map
if (spawns.Count == 0)
{
spawns.Add(Transform(outpostUid).Coordinates);
_sawmill.Warning($"Fell back to default spawn for nukies!");
}
//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 mob = Spawn(species.Prototype, RobustRandom.Pick(spawns));
SetupOperativeEntity(mob, name, nukieSession.Type, profile);
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;
}
}
args.Entity = Spawn(species.Prototype);
_humanoid.LoadProfile(args.Entity.Value, profile);
}
/// <summary>
/// Display a greeting message and play a sound for a nukie
/// </summary>
private void NotifyNukie(ICommonSession session, NukeOperativeComponent nukeop, NukeopsRuleComponent nukeopsRule)
private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (nukeopsRule.TargetStation is not { } station)
if (ent.Comp.TargetStation is not { } station)
return;
_antagSelection.SendBriefing(session, Loc.GetString("nukeops-welcome", ("station", station), ("name", nukeopsRule.OperationName)), Color.Red, nukeop.GreetSoundNotification);
_antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome",
("station", station),
("name", Name(ent))),
Color.Red,
ent.Comp.GreetSoundNotification);
}
/// <summary>
/// Spawn nukie ghost roles if this gamerule was started mid round
/// </summary>
private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null)
/// <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 (!Resolve(uid, ref component))
return;
if (!Resolve(ent, ref ent.Comp, false))
return null;
if (!SpawnMap((uid, component)))
{
_sawmill.Info("Failed to load map for nukeops");
return;
}
var numNukies = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerOperative, component.MaxOps);
//Dont continue if we have no nukies to spawn
if (numNukies == 0)
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);
return ent.Comp.MapGrids.FirstOrNull();
}
//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? GetShuttle(Entity<NukeopsRuleComponent?> 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);
}
private sealed class NukieSpawn
{
public ICommonSession? Session { get; private set; }
public NukeopSpawnPreset Type { get; private set; }
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;
}
}

View File

@@ -1,321 +0,0 @@
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();
}
}
}
}

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -14,7 +14,6 @@ using Content.Server.Station.Systems;
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;
@@ -24,12 +23,11 @@ using Content.Shared.Mobs.Systems;
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;
@@ -40,7 +38,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
{
[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!;
@@ -51,7 +49,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
[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";
@@ -60,23 +57,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
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);
@@ -98,40 +84,29 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
}
}
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]));
var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
args.AddLine(Loc.GetString("rev-headrev-name-user",
("name", name),
("username", data.UserName),
("count", count)));
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)));
}
// TODO: someone suggested listing all alive? revs maybe implement at some point
}
// TODO: someone suggested listing all alive? revs maybe implement at some point
}
}
@@ -144,57 +119,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
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>
@@ -232,22 +156,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
}
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
@@ -308,7 +217,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
_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

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Sandbox;

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.Storage;

View File

@@ -3,118 +3,37 @@ using Content.Server.GameTicking.Rules.Components;
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)
@@ -160,8 +79,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
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");
@@ -169,9 +87,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
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");
}
}

View File

@@ -5,97 +5,61 @@ using Content.Server.Objectives;
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)
private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
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)
{
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++)
{
@@ -103,66 +67,19 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
}
}
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);
@@ -180,19 +97,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
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
@@ -202,11 +114,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
// 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)
@@ -222,53 +131,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
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");
}
@@ -277,27 +142,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
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();
@@ -312,9 +156,11 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
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);
@@ -324,20 +170,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
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;

View File

@@ -1,112 +1,90 @@
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);
base.AppendRoundEndText(uid, component, gameRule, ref args);
gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
}
// This is just the general condition thing used for determining the win/lose text
var fraction = GetInfectedFraction(true, true);
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
foreach (var zombie in EntityQuery<ZombieRuleComponent>())
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);
args.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
("name", entName),
("username", data.UserName)));
}
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"));
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)));
}
}
@@ -134,38 +112,20 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
_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)
@@ -232,81 +192,4 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
}
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);
}
}

View File

@@ -1,10 +1,7 @@
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;
@@ -12,7 +9,9 @@ using Content.Shared.Random.Helpers;
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;
@@ -20,8 +19,8 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
{
[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()
@@ -179,7 +178,9 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
.ThenByDescending(x => x.completedObjectives);
foreach (var (summary, _, _) in sortedAgents)
{
result.AppendLine(summary);
}
}
public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
@@ -244,8 +245,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
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)
{

View File

@@ -17,6 +17,7 @@ using Robust.Shared.Player;
using Robust.Shared.Utility;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using Content.Server.GameTicking.Components;
namespace Content.Server.Power.EntitySystems;

View File

@@ -16,6 +16,7 @@ namespace Content.Server.Preferences.Managers
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);
}

View File

@@ -256,6 +256,20 @@ namespace Content.Server.Preferences.Managers
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);

View File

@@ -1,4 +1,4 @@
using Content.Shared.Dataset;
using Content.Shared.Dataset;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -47,9 +47,12 @@ public sealed class RandomMetadataSystem : EntitySystem
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);
}

View File

@@ -1,5 +1,6 @@
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;

View File

@@ -1,5 +1,6 @@
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;

View File

@@ -1,18 +0,0 @@
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;
}

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;

View File

@@ -2,6 +2,7 @@ using System.Linq;
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;

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using JetBrains.Annotations;

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.ImmovableRod;
using Content.Server.StationEvents.Components;

View File

@@ -1,5 +1,5 @@
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;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;

View File

@@ -1,47 +0,0 @@
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);
}
}

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Server.Traits.Assorted;

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Robust.Shared.Map;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Ninja.Systems;
using Content.Server.Station.Components;

View File

@@ -1,4 +1,5 @@
using System.Threading;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Server.Storage.Components;

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Radio;
using Robust.Shared.Random;

View File

@@ -1,5 +1,6 @@
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;

View File

@@ -6,6 +6,7 @@ using JetBrains.Annotations;
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;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.StationEvents.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,6 +1,7 @@
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;
@@ -9,7 +10,10 @@ 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()
{
@@ -20,44 +24,6 @@ public sealed class AutoTraitorSystem : EntitySystem
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);
}
}

View File

@@ -83,12 +83,9 @@ namespace Content.Server.Traitor.Uplink.Commands
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"));
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Damage;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Zombies;
@@ -35,6 +36,21 @@ public sealed partial class PendingZombieComponent : Component
[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>

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Server.Actions;
using Content.Server.Body.Systems;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
@@ -30,6 +31,7 @@ namespace Content.Server.Zombies
[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!;
@@ -74,6 +76,8 @@ namespace Content.Server.Zombies
}
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)

View File

@@ -20,3 +20,8 @@ public enum AntagAcceptability
All
}
public enum AntagSelectionTime : byte
{
PrePlayerSpawn,
PostPlayerSpawn
}

View File

@@ -403,91 +403,6 @@ namespace Content.Shared.CCVar
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
*/

View File

@@ -267,8 +267,11 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
/// <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;

View File

@@ -1,8 +1,6 @@
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;
@@ -96,7 +94,7 @@ public partial class InventorySystem
/// </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)
{

View File

@@ -13,11 +13,6 @@ namespace Content.Shared.NukeOps;
[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>
///

View File

@@ -62,6 +62,32 @@ public abstract class SharedRoleSystem : EntitySystem
_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>
@@ -180,6 +206,11 @@ public abstract class SharedRoleSystem : EntitySystem
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.

View File

@@ -1,16 +1,17 @@
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!;
@@ -21,8 +22,22 @@ public abstract class SharedStationSpawningSystem : EntitySystem
/// </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)

View File

@@ -1,10 +0,0 @@
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.

View File

@@ -1771,7 +1771,7 @@ entities:
- type: Transform
pos: 0.5436061,-7.5129323
parent: 325
- proto: SpawnPointLoneNukeOperative
- proto: SpawnPointNukies
entities:
- uid: 322
components:

View File

@@ -84,8 +84,7 @@
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:

View File

@@ -405,11 +405,28 @@
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
@@ -422,7 +439,29 @@
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

View File

@@ -34,6 +34,23 @@
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

View File

@@ -70,29 +70,114 @@
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
@@ -113,7 +198,32 @@
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
@@ -142,7 +252,6 @@
- id: BasicTrashVariationPass
- id: SolidWallRustingVariationPass
- id: ReinforcedWallRustingVariationPass
- id: CutWireVariationPass
- id: BasicPuddleMessVariationPass
prob: 0.99
orGroup: puddleMess

View File

@@ -273,8 +273,18 @@
#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

View File

@@ -153,15 +153,3 @@
- Zombie
- BasicStationEventScheduler
- BasicRoundstartVariation
- type: gamePreset
id: Pirates
alias:
- pirates
name: pirates-title
description: pirates-description
showInVote: false
rules:
- Pirates
- BasicStationEventScheduler
- BasicRoundstartVariation