diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 0ff2077933..71d719c523 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -97,8 +97,6 @@ namespace Content.Client.Entry prototypes.RegisterIgnore("advertisementsPack"); prototypes.RegisterIgnore("metabolizerType"); prototypes.RegisterIgnore("metabolismGroup"); - prototypes.RegisterIgnore("gamePreset"); - prototypes.RegisterIgnore("gameRule"); ClientContentIoC.Register(); diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index e5b7714e95..58e8648d56 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -4,14 +4,12 @@ using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; using NUnit.Framework; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.IntegrationTests.Tests.GameRules { [TestFixture] - [TestOf(typeof(MaxTimeRestartRuleSystem))] + [TestOf(typeof(RuleMaxTimeRestart))] public class RuleMaxTimeRestartTest : ContentIntegrationTest { [Test] @@ -21,9 +19,7 @@ namespace Content.IntegrationTests.Tests.GameRules { CVarOverrides = { - ["game.lobbyenabled"] = "true", - ["game.dummyticker"] = "false", - ["game.defaultpreset"] = "", // No preset. + ["game.lobbyenabled"] = "true" } }; var server = StartServer(options); @@ -31,15 +27,16 @@ namespace Content.IntegrationTests.Tests.GameRules await server.WaitIdleAsync(); var sGameTicker = server.ResolveDependency().GetEntitySystem(); - var maxTimeMaxTimeRestartRuleSystem = server.ResolveDependency().GetEntitySystem(); var sGameTiming = server.ResolveDependency(); + RuleMaxTimeRestart maxTimeRule = null; + await server.WaitAssertion(() => { Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); - sGameTicker.AddGameRule(IoCManager.Resolve().Index(maxTimeMaxTimeRestartRuleSystem.Prototype)); - maxTimeMaxTimeRestartRuleSystem.RoundMaxTime = TimeSpan.FromSeconds(3); + maxTimeRule = sGameTicker.AddGameRule(); + maxTimeRule.RoundMaxTime = TimeSpan.FromSeconds(3); sGameTicker.StartRound(); }); @@ -49,7 +46,7 @@ namespace Content.IntegrationTests.Tests.GameRules Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound)); }); - var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundMaxTime.TotalSeconds * 1.1f); + var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeRule.RoundMaxTime.TotalSeconds * 1.1f); await server.WaitRunTicks(ticks); await server.WaitAssertion(() => @@ -57,7 +54,7 @@ namespace Content.IntegrationTests.Tests.GameRules Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PostRound)); }); - ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundEndDelay.TotalSeconds * 1.1f); + ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeRule.RoundEndDelay.TotalSeconds * 1.1f); await server.WaitRunTicks(ticks); await server.WaitAssertion(() => diff --git a/Content.Server/GameTicking/Commands/ForcePresetCommand.cs b/Content.Server/GameTicking/Commands/ForcePresetCommand.cs index 8c17235781..60d73a4060 100644 --- a/Content.Server/GameTicking/Commands/ForcePresetCommand.cs +++ b/Content.Server/GameTicking/Commands/ForcePresetCommand.cs @@ -1,5 +1,4 @@ using Content.Server.Administration; -using Content.Server.GameTicking.Presets; using Content.Shared.Administration; using Robust.Shared.Console; using Robust.Shared.GameObjects; @@ -30,13 +29,13 @@ namespace Content.Server.GameTicking.Commands } var name = args[0]; - if (!ticker.TryFindGamePreset(name, out var type)) + if (!ticker.TryGetPreset(name, out var type)) { shell.WriteLine($"No preset exists with name {name}."); return; } - ticker.SetGamePreset(type, true); + ticker.SetStartPreset(type, true); shell.WriteLine($"Forced the game to start with preset {name}."); } } diff --git a/Content.Server/GameTicking/Commands/GoLobbyCommand.cs b/Content.Server/GameTicking/Commands/GoLobbyCommand.cs index adaaff11e1..28aaeb5d1d 100644 --- a/Content.Server/GameTicking/Commands/GoLobbyCommand.cs +++ b/Content.Server/GameTicking/Commands/GoLobbyCommand.cs @@ -1,6 +1,5 @@ using System; using Content.Server.Administration; -using Content.Server.GameTicking.Presets; using Content.Shared; using Content.Shared.Administration; using Content.Shared.CCVar; @@ -19,14 +18,14 @@ namespace Content.Server.GameTicking.Commands public string Help => $"Usage: {Command} / {Command} "; public void Execute(IConsoleShell shell, string argStr, string[] args) { - GamePresetPrototype? preset = null; + Type? preset = null; var presetName = string.Join(" ", args); var ticker = EntitySystem.Get(); if (args.Length > 0) { - if (!ticker.TryFindGamePreset(presetName, out preset)) + if (!ticker.TryGetPreset(presetName, out preset)) { shell.WriteLine($"No preset found with name {presetName}"); return; @@ -40,7 +39,7 @@ namespace Content.Server.GameTicking.Commands if (preset != null) { - ticker.SetGamePreset(preset); + ticker.SetStartPreset(preset); } shell.WriteLine($"Enabling the lobby and restarting the round.{(preset == null ? "" : $"\nPreset set to {presetName}")}"); diff --git a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs index 3c9c15e7f0..6c8a313f79 100644 --- a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs +++ b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs @@ -23,7 +23,7 @@ namespace Content.Server.GameTicking.Commands var ticker = EntitySystem.Get(); - ticker.SetGamePreset(args[0]); + ticker.SetStartPreset(args[0]); } } } diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index c40ffedc31..e7861c7a5d 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -1,13 +1,13 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using Content.Server.GameTicking.Presets; -using Content.Server.GameTicking.Rules; -using Content.Server.Ghost.Components; using Content.Shared.CCVar; -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.MobState.Components; -using Robust.Shared.GameObjects; +using Content.Shared.Preferences; +using Robust.Shared.Network; +using Robust.Shared.ViewVariables; using Robust.Shared.IoC; namespace Content.Server.GameTicking @@ -16,20 +16,60 @@ namespace Content.Server.GameTicking { public const float PresetFailedCooldownIncrease = 30f; - private GamePresetPrototype? _preset; + [ViewVariables] private Type? _presetType; + + [ViewVariables] + public GamePreset? Preset + { + get => _preset ?? MakeGamePreset(new Dictionary()); + set => _preset = value; + } + + public ImmutableDictionary Presets { get; private set; } = default!; + + private GamePreset? _preset; private void InitializeGamePreset() { - SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset)); + var presets = new Dictionary(); + + foreach (var type in _reflectionManager.FindTypesWithAttribute()) + { + var attribute = type.GetCustomAttribute(); + + presets.Add(attribute!.Id.ToLowerInvariant(), type); + + foreach (var alias in attribute.Aliases) + { + presets.Add(alias.ToLowerInvariant(), type); + } + } + + Presets = presets.ToImmutableDictionary(); + + SetStartPreset(_configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset)); } - public void SetGamePreset(GamePresetPrototype preset, bool force = false) + public bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal) + { + return Preset?.OnGhostAttempt(mind, canReturnGlobal) ?? false; + } + + public bool TryGetPreset(string name, [NotNullWhen(true)] out Type? type) + { + name = name.ToLowerInvariant(); + return Presets.TryGetValue(name, out type); + } + + public void SetStartPreset(Type type, bool force = false) { // Do nothing if this game ticker is a dummy! if (DummyTicker) return; - _preset = preset; + if (!typeof(GamePreset).IsAssignableFrom(type)) throw new ArgumentException("type must inherit GamePreset"); + + _presetType = type; UpdateInfoText(); if (force) @@ -38,138 +78,21 @@ namespace Content.Server.GameTicking } } - public void SetGamePreset(string preset, bool force = false) + public void SetStartPreset(string name, bool force = false) { - var proto = FindGamePreset(preset); - if(proto != null) - SetGamePreset(proto, force); - } - - public GamePresetPrototype? FindGamePreset(string preset) - { - if (_prototypeManager.TryIndex(preset, out GamePresetPrototype? presetProto)) - return presetProto; - - foreach (var proto in _prototypeManager.EnumeratePrototypes()) + if (!TryGetPreset(name, out var type)) { - foreach (var alias in proto.Alias) - { - if (preset.Equals(alias, StringComparison.InvariantCultureIgnoreCase)) - return proto; - } + throw new NotSupportedException($"No preset found with name {name}"); } - return null; + SetStartPreset(type, force); } - public bool TryFindGamePreset(string preset, [NotNullWhen(true)] out GamePresetPrototype? prototype) + private GamePreset MakeGamePreset(Dictionary readyProfiles) { - prototype = FindGamePreset(preset); - - return prototype != null; - } - - private bool AddGamePresetRules() - { - if (DummyTicker || _preset == null) - return false; - - foreach (var rule in _preset.Rules) - { - if (!_prototypeManager.TryIndex(rule, out GameRulePrototype? ruleProto)) - continue; - - AddGameRule(ruleProto); - } - - return true; - } - - public bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal) - { - var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal); - RaiseLocalEvent(handleEv); - - // Something else has handled the ghost attempt for us! We return its result. - if (handleEv.Handled) - return handleEv.Result; - - var playerEntity = mind.OwnedEntity; - - var entities = IoCManager.Resolve(); - if (entities.HasComponent(playerEntity)) - return false; - - if (mind.VisitingEntity != default) - { - mind.UnVisit(); - } - - var position = playerEntity is {Valid: true} - ? Transform(playerEntity.Value).Coordinates - : GetObserverSpawnPoint(); - - // Ok, so, this is the master place for the logic for if ghosting is "too cheaty" to allow returning. - // There's no reason at this time to move it to any other place, especially given that the 'side effects required' situations would also have to be moved. - // + If CharacterDeadPhysically applies, we're physically dead. Therefore, ghosting OK, and we can return (this is critical for gibbing) - // Note that we could theoretically be ICly dead and still physically alive and vice versa. - // (For example, a zombie could be dead ICly, but may retain memories and is definitely physically active) - // + If we're in a mob that is critical, and we're supposed to be able to return if possible, - // we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK. - // (If the mob survives, that's a bug. Ghosting is kept regardless.) - var canReturn = canReturnGlobal && mind.CharacterDeadPhysically; - - if (canReturnGlobal && TryComp(playerEntity, out MobStateComponent? mobState)) - { - if (mobState.IsCritical()) - { - canReturn = true; - - //todo: what if they dont breathe lol - //cry deeply - DamageSpecifier damage = new(_prototypeManager.Index("Asphyxiation"), 200); - _damageable.TryChangeDamage(playerEntity, damage, true); - } - } - - var ghost = Spawn("MobObserver", position.ToMap(entities)); - - // Try setting the ghost entity name to either the character name or the player name. - // If all else fails, it'll default to the default entity prototype name, "observer". - // However, that should rarely happen. - var meta = MetaData(ghost); - if(!string.IsNullOrWhiteSpace(mind.CharacterName)) - meta.EntityName = mind.CharacterName; - else if (!string.IsNullOrWhiteSpace(mind.Session?.Name)) - meta.EntityName = mind.Session.Name; - - var ghostComponent = Comp(ghost); - - if (mind.TimeOfDeath.HasValue) - { - ghostComponent.TimeOfDeath = mind.TimeOfDeath!.Value; - } - - _ghosts.SetCanReturnToBody(ghostComponent, canReturn); - - if (canReturn) - mind.Visit(ghost); - else - mind.TransferTo(ghost); - return true; - } - } - - public class GhostAttemptHandleEvent : HandledEntityEventArgs - { - public Mind.Mind Mind { get; } - public bool CanReturnGlobal { get; } - public bool Result { get; set; } - - public GhostAttemptHandleEvent(Mind.Mind mind, bool canReturnGlobal) - { - Mind = mind; - CanReturnGlobal = canReturnGlobal; + var preset = _dynamicTypeFactory.CreateInstance(_presetType ?? typeof(PresetSandbox)); + preset.ReadyProfiles = readyProfiles; + return preset; } } } diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index 83b962fa92..c0a9f06db9 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -1,5 +1,5 @@ +using System; using System.Collections.Generic; -using System.Linq; using Content.Server.GameTicking.Rules; using Robust.Shared.ViewVariables; @@ -7,68 +7,66 @@ namespace Content.Server.GameTicking { public partial class GameTicker { - // No duplicates. - [ViewVariables] private readonly HashSet _gameRules = new(); - public IEnumerable ActiveGameRules => _gameRules; + [ViewVariables] private readonly List _gameRules = new(); + public IEnumerable ActiveGameRules => _gameRules; - public bool AddGameRule(GameRulePrototype rule) + public T AddGameRule() where T : GameRule, new() { - if (!_gameRules.Add(rule)) + var instance = _dynamicTypeFactory.CreateInstance(); + + _gameRules.Add(instance); + instance.Added(); + + RaiseLocalEvent(new GameRuleAddedEvent(instance)); + + return instance; + } + + public bool HasGameRule(string? name) + { + if (name == null) return false; - RaiseLocalEvent(new GameRuleAddedEvent(rule)); - return true; - } - - public bool RemoveGameRule(GameRulePrototype rule) - { - if (!_gameRules.Remove(rule)) - return false; - - RaiseLocalEvent(new GameRuleRemovedEvent(rule)); - return true; - } - - public bool HasGameRule(GameRulePrototype rule) - { - return _gameRules.Contains(rule); - } - - public bool HasGameRule(string rule) - { - foreach (var ruleProto in _gameRules) + foreach (var rule in _gameRules) { - if (ruleProto.ID.Equals(rule)) + if (rule.GetType().Name == name) + { + return true; + } + } + + return false; + } + + public bool HasGameRule(Type? type) + { + if (type == null || !typeof(GameRule).IsAssignableFrom(type)) + return false; + + foreach (var rule in _gameRules) + { + if (rule.GetType().IsAssignableFrom(type)) return true; } return false; } - public void ClearGameRules() + public void RemoveGameRule(GameRule rule) { - foreach (var rule in _gameRules.ToArray()) - { - RemoveGameRule(rule); - } + if (_gameRules.Contains(rule)) return; + + rule.Removed(); + + _gameRules.Remove(rule); } } public class GameRuleAddedEvent { - public GameRulePrototype Rule { get; } + public GameRule Rule { get; } - public GameRuleAddedEvent(GameRulePrototype rule) - { - Rule = rule; - } - } - - public class GameRuleRemovedEvent - { - public GameRulePrototype Rule { get; } - - public GameRuleRemovedEvent(GameRulePrototype rule) + public GameRuleAddedEvent(GameRule rule) { Rule = rule; } diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs index 369495d9c3..7d65e6cb37 100644 --- a/Content.Server/GameTicking/GameTicker.Lobby.cs +++ b/Content.Server/GameTicking/GameTicker.Lobby.cs @@ -37,15 +37,15 @@ namespace Content.Server.GameTicking private string GetInfoText() { - if (_preset == null) + if (Preset == null) { return string.Empty; } var map = _gameMapManager.GetSelectedMap(); var mapName = map?.MapName ?? Loc.GetString("game-ticker-no-map-selected"); - var gmTitle = Loc.GetString(_preset.ModeTitle); - var desc = Loc.GetString(_preset.Description); + var gmTitle = Preset.ModeTitle; + var desc = Preset.Description; return Loc.GetString("game-ticker-get-info-text",("mapName", mapName),("gmTitle", gmTitle),("desc", desc)); } diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs index 63e51b9845..bcf027b3b3 100644 --- a/Content.Server/GameTicking/GameTicker.Player.cs +++ b/Content.Server/GameTicking/GameTicker.Player.cs @@ -127,7 +127,7 @@ namespace Content.Server.GameTicking return (HumanoidCharacterProfile) _prefsManager.GetPreferences(p.UserId).SelectedCharacter; } - public void PlayerJoinGame(IPlayerSession session) + private void PlayerJoinGame(IPlayerSession session) { _chatManager.DispatchServerMessage(session, Loc.GetString("game-ticker-player-join-game-message")); diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index d420a4ebdd..1cd48d0180 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -18,7 +18,6 @@ using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Maths; -using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Utility; @@ -65,7 +64,6 @@ namespace Content.Server.GameTicking private void PreRoundSetup() { DefaultMap = _mapManager.CreateMap(); - _pauseManager.AddUninitializedMap(DefaultMap); var startTime = _gameTiming.RealTime; var map = _gameMapManager.GetSelectedMapChecked(true); var grid = _mapLoader.LoadBlueprint(DefaultMap, map.MapPath); @@ -110,8 +108,6 @@ namespace Content.Server.GameTicking SendServerMessage(Loc.GetString("game-ticker-start-round")); - AddGamePresetRules(); - List readyPlayers; if (LobbyEnabled) { @@ -122,6 +118,9 @@ namespace Content.Server.GameTicking readyPlayers = _playersInLobby.Keys.ToList(); } + _roundStartDateTime = DateTime.UtcNow; + RunLevel = GameRunLevel.InRound; + RoundLengthMetric.Set(0); var playerIds = _playersInLobby.Keys.Select(player => player.UserId.UserId).ToArray(); @@ -144,58 +143,10 @@ namespace Content.Server.GameTicking } } - var origReadyPlayers = readyPlayers.ToArray(); - - var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force); - RaiseLocalEvent(startAttempt); - - var presetTitle = _preset != null ? Loc.GetString(_preset.ModeTitle) : string.Empty; - - void FailedPresetRestart() - { - SendServerMessage(Loc.GetString("game-ticker-start-round-cannot-start-game-mode-restart", ("failedGameMode", presetTitle))); - RestartRound(); - DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease)); - } - - if (startAttempt.Cancelled) - { - if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled)) - { - var oldPreset = _preset; - ClearGameRules(); - SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset)); - AddGamePresetRules(); - - startAttempt.Uncancel(); - RaiseLocalEvent(startAttempt); - - _chatManager.DispatchServerAnnouncement( - Loc.GetString("game-ticker-start-round-cannot-start-game-mode-fallback", - ("failedGameMode", presetTitle), - ("fallbackMode", Loc.GetString(_preset!.ModeTitle)))); - - if (startAttempt.Cancelled) - { - FailedPresetRestart(); - } - - RefreshLateJoinAllowed(); - } - else - { - FailedPresetRestart(); - return; - } - } - - // Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard) - RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force)); - var assignedJobs = AssignJobs(readyPlayers, profiles); // For players without jobs, give them the overflow job if they have that set... - foreach (var player in origReadyPlayers) + foreach (var player in readyPlayers) { if (assignedJobs.ContainsKey(player)) { @@ -237,15 +188,39 @@ namespace Content.Server.GameTicking SpawnPlayer(player, profiles[player.UserId], station, job, false); } - RefreshLateJoinAllowed(); + // Time to start the preset. + Preset = MakeGamePreset(profiles); - // Allow rules to add roles to players who have been spawned in. (For example, on-station traitors) - RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.ToArray(), profiles, force)); + DisallowLateJoin |= Preset.DisallowLateJoin; - _pauseManager.DoMapInitialize(DefaultMap); + if (!Preset.Start(assignedJobs.Keys.ToList(), force)) + { + if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled)) + { + SetStartPreset(_configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset)); + var newPreset = MakeGamePreset(profiles); + _chatManager.DispatchServerAnnouncement( + Loc.GetString("game-ticker-start-round-cannot-start-game-mode-fallback", + ("failedGameMode", Preset.ModeTitle), + ("fallbackMode", newPreset.ModeTitle))); + if (!newPreset.Start(readyPlayers, force)) + { + throw new ApplicationException("Fallback preset failed to start!"); + } - _roundStartDateTime = DateTime.UtcNow; - RunLevel = GameRunLevel.InRound; + DisallowLateJoin = false; + DisallowLateJoin |= newPreset.DisallowLateJoin; + Preset = newPreset; + } + else + { + SendServerMessage(Loc.GetString("game-ticker-start-round-cannot-start-game-mode-restart", ("failedGameMode", Preset.ModeTitle))); + RestartRound(); + DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease)); + return; + } + } + Preset.OnGameStarted(); _roundStartTimeSpan = _gameTiming.RealTime; SendStatusToAll(); @@ -254,13 +229,6 @@ namespace Content.Server.GameTicking UpdateJobsAvailable(); } - private void RefreshLateJoinAllowed() - { - var refresh = new RefreshLateJoinAllowedEvent(); - RaiseLocalEvent(refresh); - DisallowLateJoin = refresh.DisallowLateJoin; - } - public void EndRound(string text = "") { // If this game ticker is a dummy, do nothing! @@ -273,13 +241,8 @@ namespace Content.Server.GameTicking RunLevel = GameRunLevel.PostRound; //Tell every client the round has ended. - var gamemodeTitle = _preset != null ? Loc.GetString(_preset.ModeTitle) : string.Empty; - - // Let things add text here. - var textEv = new RoundEndTextAppendEvent(); - RaiseLocalEvent(textEv); - - var roundEndText = $"{text}\n{textEv.Text}"; + var gamemodeTitle = Preset?.ModeTitle ?? string.Empty; + var roundEndText = text + $"\n{Preset?.GetRoundEndDescription() ?? string.Empty}"; //Get the timespan of the round. var roundDuration = RoundDuration(); @@ -369,6 +332,8 @@ namespace Content.Server.GameTicking } else { + Preset = null; + if (_playerManager.PlayerCount == 0) _roundStartCountdownHasNotStartedYetDueToNoPlayers = true; else @@ -410,7 +375,10 @@ namespace Content.Server.GameTicking _mapManager.Restart(); // Clear up any game rules. - ClearGameRules(); + foreach (var rule in _gameRules) + { + rule.Removed(); + } _gameRules.Clear(); @@ -484,102 +452,4 @@ namespace Content.Server.GameTicking New = @new; } } - - /// - /// Event raised to refresh the late join status. - /// If you want to disallow late joins, listen to this and call Disallow. - /// - public class RefreshLateJoinAllowedEvent - { - public bool DisallowLateJoin { get; private set; } = false; - - public void Disallow() - { - DisallowLateJoin = true; - } - } - - /// - /// Attempt event raised on round start. - /// This can be listened to by GameRule systems to cancel round start if some condition is not met, like player count. - /// - public class RoundStartAttemptEvent : CancellableEntityEventArgs - { - public IPlayerSession[] Players { get; } - public bool Forced { get; } - - public RoundStartAttemptEvent(IPlayerSession[] players, bool forced) - { - Players = players; - Forced = forced; - } - } - - /// - /// Event raised before readied up players are spawned and given jobs by the GameTicker. - /// You can use this to spawn people off-station, like in the case of nuke ops or wizard. - /// Remove the players you spawned from the PlayerPool and call on them. - /// - public class RulePlayerSpawningEvent - { - /// - /// Pool of players to be spawned. - /// If you want to handle a specific player being spawned, remove it from this list and do what you need. - /// - /// If you spawn a player by yourself from this event, don't forget to call on them. - public List PlayerPool { get; } - public IReadOnlyDictionary Profiles { get; } - public bool Forced { get; } - - public RulePlayerSpawningEvent(List playerPool, IReadOnlyDictionary profiles, bool forced) - { - PlayerPool = playerPool; - Profiles = profiles; - Forced = forced; - } - } - - /// - /// Event raised after players were assigned jobs by the GameTicker. - /// You can give on-station people special roles by listening to this event. - /// - public class RulePlayerJobsAssignedEvent - { - public IPlayerSession[] Players { get; } - public IReadOnlyDictionary Profiles { get; } - public bool Forced { get; } - - public RulePlayerJobsAssignedEvent(IPlayerSession[] players, IReadOnlyDictionary profiles, bool forced) - { - Players = players; - Profiles = profiles; - Forced = forced; - } - } - - /// - /// Event raised to allow subscribers to add text to the round end summary screen. - /// - public class RoundEndTextAppendEvent - { - private bool _doNewLine; - - /// - /// Text to display in the round end summary screen. - /// - public string Text { get; private set; } = string.Empty; - - /// - /// Invoke this method to add text to the round end summary screen. - /// - /// - public void AddLine(string text) - { - if (_doNewLine) - Text += "\n"; - - Text += text; - _doNewLine = true; - } - } } diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 1cb89d6243..c93d803727 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -63,10 +63,6 @@ namespace Content.Server.GameTicking private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, StationId station, string? jobId = null, bool lateJoin = true) { - // Can't spawn players with a dummy ticker! - if (DummyTicker) - return; - if (station == StationId.Invalid) { var stations = _stationSystem.StationInfo.Keys.ToList(); @@ -77,23 +73,16 @@ namespace Content.Server.GameTicking station = stations[0]; } + // Can't spawn players with a dummy ticker! + if (DummyTicker) + return; + if (lateJoin && DisallowLateJoin) { MakeObserve(player); return; } - // We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc) - var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station); - RaiseLocalEvent(bev); - - // Do nothing, something else has handled spawning this player for us! - if (bev.Handled) - { - PlayerJoinGame(player); - return; - } - // Pick best job best on prefs. jobId ??= PickBestAvailableJob(character, station); // If no job available, just bail out. @@ -154,9 +143,7 @@ namespace Content.Server.GameTicking else _adminLogSystem.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {_stationSystem.StationInfo[station].Name:stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}."); - // We raise this event directed to the mob, but also broadcast it so game rules can do something now. - var aev = new PlayerSpawnCompleteEvent(mob, player, jobId, lateJoin, station, character); - RaiseLocalEvent(mob, aev); + Preset?.OnSpawnPlayerCompleted(player, mob, lateJoin); } public void Respawn(IPlayerSession player) @@ -368,52 +355,4 @@ namespace Content.Server.GameTicking } #endregion } - - /// - /// Event raised broadcast before a player is spawned by the GameTicker. - /// You can use this event to spawn a player off-station on late-join but also at round start. - /// When this event is handled, the GameTicker will not perform its own player-spawning logic. - /// - public class PlayerBeforeSpawnEvent : HandledEntityEventArgs - { - public IPlayerSession Player { get; } - public HumanoidCharacterProfile Profile { get; } - public string? JobId { get; } - public bool LateJoin { get; } - public StationId Station { get; } - - public PlayerBeforeSpawnEvent(IPlayerSession player, HumanoidCharacterProfile profile, string? jobId, bool lateJoin, StationId station) - { - Player = player; - Profile = profile; - JobId = jobId; - LateJoin = lateJoin; - Station = station; - } - } - - /// - /// Event raised both directed and broadcast when a player has been spawned by the GameTicker. - /// You can use this to handle people late-joining, or to handle people being spawned at round start. - /// Can be used to give random players a role, modify their equipment, etc. - /// - public class PlayerSpawnCompleteEvent : EntityEventArgs - { - public EntityUid Mob { get; } - public IPlayerSession Player { get; } - public string? JobId { get; } - public bool LateJoin { get; } - public StationId Station { get; } - public HumanoidCharacterProfile Profile { get; } - - public PlayerSpawnCompleteEvent(EntityUid mob, IPlayerSession player, string? jobId, bool lateJoin, StationId station, HumanoidCharacterProfile profile) - { - Mob = mob; - Player = player; - JobId = jobId; - LateJoin = lateJoin; - Station = station; - Profile = profile; - } - } } diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index b03752139c..25fecfc3d5 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -1,14 +1,12 @@ using Content.Server.Administration.Logs; using Content.Server.CharacterAppearance.Systems; using Content.Server.Chat.Managers; -using Content.Server.Ghost; using Content.Server.Maps; using Content.Server.PDA; using Content.Server.Preferences.Managers; using Content.Server.Roles; using Content.Server.Station; using Content.Shared.Chat; -using Content.Shared.Damage; using Content.Shared.GameTicking; using Content.Shared.GameWindow; using Robust.Server; @@ -85,18 +83,17 @@ namespace Content.Server.GameTicking [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IServerNetManager _netManager = default!; + [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly IServerPreferencesManager _prefsManager = default!; [Dependency] private readonly IBaseServer _baseServer = default!; [Dependency] private readonly IWatchdogApi _watchdogApi = default!; + [Dependency] private readonly IReflectionManager _reflectionManager = default!; [Dependency] private readonly IGameMapManager _gameMapManager = default!; - [Dependency] private readonly IPauseManager _pauseManager = default!; [Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly AdminLogSystem _adminLogSystem = default!; [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearanceSystem = default!; [Dependency] private readonly PDASystem _pdaSystem = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly GhostSystem _ghosts = default!; } } diff --git a/Content.Server/GameTicking/Presets/GamePreset.cs b/Content.Server/GameTicking/Presets/GamePreset.cs new file mode 100644 index 0000000000..f9cb77de42 --- /dev/null +++ b/Content.Server/GameTicking/Presets/GamePreset.cs @@ -0,0 +1,107 @@ +#nullable enable annotations +using System.Collections.Generic; +using Content.Server.Ghost.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Ghost; +using Content.Shared.MobState.Components; +using Content.Shared.Preferences; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Presets +{ + /// + /// A round-start setup preset, such as which antagonists to spawn. + /// + public abstract class GamePreset + { + [Dependency] private readonly IEntityManager _entities = default!; + + public abstract bool Start(IReadOnlyList readyPlayers, bool force = false); + public virtual string ModeTitle => "Sandbox"; + public virtual string Description => "Secret!"; + public virtual bool DisallowLateJoin => false; + public Dictionary ReadyProfiles = new(); + + public virtual void OnGameStarted() { } + + /// + /// Called when a player is spawned in (this includes, but is not limited to, before Start) + /// + public virtual void OnSpawnPlayerCompleted(IPlayerSession session, EntityUid mob, bool lateJoin) { } + + /// + /// Called when a player attempts to ghost. + /// + public virtual bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal) + { + var playerEntity = mind.OwnedEntity; + + var entities = IoCManager.Resolve(); + if (entities.HasComponent(playerEntity)) + return false; + + if (mind.VisitingEntity != default) + { + mind.UnVisit(); + } + + var position = playerEntity is {Valid: true} + ? _entities.GetComponent(playerEntity.Value).Coordinates + : EntitySystem.Get().GetObserverSpawnPoint(); + // Ok, so, this is the master place for the logic for if ghosting is "too cheaty" to allow returning. + // There's no reason at this time to move it to any other place, especially given that the 'side effects required' situations would also have to be moved. + // + If CharacterDeadPhysically applies, we're physically dead. Therefore, ghosting OK, and we can return (this is critical for gibbing) + // Note that we could theoretically be ICly dead and still physically alive and vice versa. + // (For example, a zombie could be dead ICly, but may retain memories and is definitely physically active) + // + If we're in a mob that is critical, and we're supposed to be able to return if possible, + /// we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK. + // (If the mob survives, that's a bug. Ghosting is kept regardless.) + var canReturn = canReturnGlobal && mind.CharacterDeadPhysically; + + if (canReturnGlobal && entities.TryGetComponent(playerEntity, out MobStateComponent? mobState)) + { + if (mobState.IsCritical()) + { + canReturn = true; + + //todo: what if they dont breathe lol + //cry deeply + DamageSpecifier damage = new(IoCManager.Resolve().Index("Asphyxiation"), 200); + EntitySystem.Get().TryChangeDamage(playerEntity, damage, true); + } + } + + var ghost = entities.SpawnEntity("MobObserver", position.ToMap(entities)); + + // Try setting the ghost entity name to either the character name or the player name. + // If all else fails, it'll default to the default entity prototype name, "observer". + // However, that should rarely happen. + if(!string.IsNullOrWhiteSpace(mind.CharacterName)) + entities.GetComponent(ghost).EntityName = mind.CharacterName; + else if (!string.IsNullOrWhiteSpace(mind.Session?.Name)) + entities.GetComponent(ghost).EntityName = mind.Session.Name; + + var ghostComponent = entities.GetComponent(ghost); + + if (mind.TimeOfDeath.HasValue) + { + ghostComponent.TimeOfDeath = mind.TimeOfDeath!.Value; + } + + EntitySystem.Get().SetCanReturnToBody(ghostComponent, canReturn); + + if (canReturn) + mind.Visit(ghost); + else + mind.TransferTo(ghost); + return true; + } + + public virtual string GetRoundEndDescription() => string.Empty; + } +} diff --git a/Content.Server/GameTicking/Presets/GamePresetAttribute.cs b/Content.Server/GameTicking/Presets/GamePresetAttribute.cs new file mode 100644 index 0000000000..83473cea27 --- /dev/null +++ b/Content.Server/GameTicking/Presets/GamePresetAttribute.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Immutable; +using JetBrains.Annotations; + +namespace Content.Server.GameTicking.Presets +{ + /// + /// Attribute that marks a game preset. + /// The id and aliases are registered in lowercase in . + /// A duplicate id or alias will throw an exception. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + [BaseTypeRequired(typeof(GamePreset))] + [MeansImplicitUse] + public class GamePresetAttribute : Attribute + { + public string Id { get; } + + public ImmutableList Aliases { get; } + + public GamePresetAttribute(string id, params string[] aliases) + { + Id = id; + Aliases = aliases.ToImmutableList(); + } + } +} diff --git a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs b/Content.Server/GameTicking/Presets/GamePresetPrototype.cs deleted file mode 100644 index f511535780..0000000000 --- a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using Content.Server.GameTicking.Rules; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Server.GameTicking.Presets -{ - /// - /// A round-start setup preset, such as which antagonists to spawn. - /// - [Prototype("gamePreset")] - public class GamePresetPrototype : IPrototype - { - [DataField("id", required:true)] - public string ID { get; } = default!; - - [DataField("alias")] - public string[] Alias { get; } = Array.Empty(); - - [DataField("name")] - public string ModeTitle { get; } = "????"; - - [DataField("description")] - public string Description { get; } = string.Empty; - - [DataField("showInVote")] - public bool ShowInVote { get; } = false; - - [DataField("rules", customTypeSerializer:typeof(PrototypeIdListSerializer))] - public IReadOnlyList Rules { get; } = Array.Empty(); - } -} diff --git a/Content.Server/GameTicking/Presets/PresetDeathMatch.cs b/Content.Server/GameTicking/Presets/PresetDeathMatch.cs new file mode 100644 index 0000000000..39844f7362 --- /dev/null +++ b/Content.Server/GameTicking/Presets/PresetDeathMatch.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Content.Server.GameTicking.Rules; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Server.GameTicking.Presets +{ + [GamePreset("deathmatch")] + public sealed class PresetDeathMatch : GamePreset + { + public override bool Start(IReadOnlyList readyPlayers, bool force = false) + { + EntitySystem.Get().AddGameRule(); + return true; + } + + public override string ModeTitle => "Deathmatch"; + public override string Description => "Kill anything that moves!"; + } +} diff --git a/Content.Server/GameTicking/Presets/PresetExtended.cs b/Content.Server/GameTicking/Presets/PresetExtended.cs new file mode 100644 index 0000000000..6989656bf2 --- /dev/null +++ b/Content.Server/GameTicking/Presets/PresetExtended.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Robust.Server.Player; + +namespace Content.Server.GameTicking.Presets +{ + [GamePreset("extended")] + public class PresetExtended : GamePreset + { + public override string Description => "No antagonists, have fun!"; + public override string ModeTitle => "Extended"; + + public override bool Start(IReadOnlyList readyPlayers, bool force = false) + { + // We do nothing. This is extended after all... + return true; + } + } +} diff --git a/Content.Server/GameTicking/Presets/PresetSandbox.cs b/Content.Server/GameTicking/Presets/PresetSandbox.cs new file mode 100644 index 0000000000..fe17cdb69b --- /dev/null +++ b/Content.Server/GameTicking/Presets/PresetSandbox.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Content.Server.Sandbox; +using Robust.Server.Player; +using Robust.Shared.IoC; + +namespace Content.Server.GameTicking.Presets +{ + [GamePreset("sandbox")] + public sealed class PresetSandbox : GamePreset + { + [Dependency] private readonly ISandboxManager _sandboxManager = default!; + + public override bool Start(IReadOnlyList readyPlayers, bool force = false) + { + _sandboxManager.IsSandboxEnabled = true; + return true; + } + + public override string ModeTitle => "Sandbox"; + public override string Description => "No stress, build something!"; + } +} diff --git a/Content.Server/GameTicking/Presets/PresetSuspicion.cs b/Content.Server/GameTicking/Presets/PresetSuspicion.cs new file mode 100644 index 0000000000..d68aa75611 --- /dev/null +++ b/Content.Server/GameTicking/Presets/PresetSuspicion.cs @@ -0,0 +1,146 @@ +using System.Collections.Generic; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Rules; +using Content.Server.Players; +using Content.Server.Suspicion; +using Content.Server.Suspicion.Roles; +using Content.Server.Traitor.Uplink; +using Content.Server.Traitor.Uplink.Account; +using Content.Shared.CCVar; +using Content.Shared.Roles; +using Content.Shared.Traitor.Uplink; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.GameTicking.Presets +{ + [GamePreset("suspicion")] + public class PresetSuspicion : GamePreset + { + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] protected readonly IEntityManager EntityManager = default!; + + public int MinPlayers { get; set; } + public int MinTraitors { get; set; } + public int PlayersPerTraitor { get; set; } + + public int TraitorStartingBalance { get; set; } + + + public override bool DisallowLateJoin => true; + + private static string TraitorID = "SuspicionTraitor"; + private static string InnocentID = "SuspicionInnocent"; + + public override bool Start(IReadOnlyList readyPlayers, bool force = false) + { + MinPlayers = _cfg.GetCVar(CCVars.SuspicionMinPlayers); + MinTraitors = _cfg.GetCVar(CCVars.SuspicionMinTraitors); + PlayersPerTraitor = _cfg.GetCVar(CCVars.SuspicionPlayersPerTraitor); + TraitorStartingBalance = _cfg.GetCVar(CCVars.SuspicionStartingBalance); + + if (!force && readyPlayers.Count < MinPlayers) + { + _chatManager.DispatchServerAnnouncement($"Not enough players readied up for the game! There were {readyPlayers.Count} players readied up out of {MinPlayers} needed."); + return false; + } + + if (readyPlayers.Count == 0) + { + _chatManager.DispatchServerAnnouncement("No players readied up! Can't start Suspicion."); + return false; + } + + var list = new List(readyPlayers); + var prefList = new List(); + + foreach (var player in list) + { + if (!ReadyProfiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached) + { + continue; + } + prefList.Add(player); + + attached.EnsureComponent(); + } + + var numTraitors = MathHelper.Clamp(readyPlayers.Count / PlayersPerTraitor, + MinTraitors, readyPlayers.Count); + + var traitors = new List(); + + for (var i = 0; i < numTraitors; i++) + { + IPlayerSession traitor; + if(prefList.Count == 0) + { + if (list.Count == 0) + { + Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection."); + break; + } + traitor = _random.PickAndTake(list); + Logger.InfoS("preset", "Insufficient preferred traitors, picking at random."); + } + else + { + traitor = _random.PickAndTake(prefList); + list.Remove(traitor); + Logger.InfoS("preset", "Selected a preferred traitor."); + } + var mind = traitor.Data.ContentData()?.Mind; + var antagPrototype = _prototypeManager.Index(TraitorID); + + DebugTools.AssertNotNull(mind?.OwnedEntity); + + var traitorRole = new SuspicionTraitorRole(mind!, antagPrototype); + mind!.AddRole(traitorRole); + traitors.Add(traitorRole); + + // creadth: we need to create uplink for the antag. + // PDA should be in place already, so we just need to + // initiate uplink account. + var uplinkAccount = new UplinkAccount(TraitorStartingBalance, mind.OwnedEntity!); + var accounts = EntityManager.EntitySysManager.GetEntitySystem(); + accounts.AddNewAccount(uplinkAccount); + + // try to place uplink + if (!EntityManager.EntitySysManager.GetEntitySystem() + .AddUplink(mind.OwnedEntity!.Value, uplinkAccount)) + continue; + } + + foreach (var player in list) + { + var mind = player.Data.ContentData()?.Mind; + var antagPrototype = _prototypeManager.Index(InnocentID); + + DebugTools.AssertNotNull(mind); + + mind!.AddRole(new SuspicionInnocentRole(mind, antagPrototype)); + } + + foreach (var traitor in traitors) + { + traitor.GreetSuspicion(traitors, _chatManager); + } + + EntitySystem.Get().AddGameRule(); + return true; + } + + public override string ModeTitle => "Suspicion"; + public override string Description => "Suspicion on the Space Station. There are traitors on board... Can you kill them before they kill you?"; + } +} diff --git a/Content.Server/GameTicking/Presets/PresetTraitor.cs b/Content.Server/GameTicking/Presets/PresetTraitor.cs new file mode 100644 index 0000000000..fb85f34af7 --- /dev/null +++ b/Content.Server/GameTicking/Presets/PresetTraitor.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Rules; +using Content.Server.Objectives.Interfaces; +using Content.Server.Players; +using Content.Server.Roles; +using Content.Server.Traitor; +using Content.Server.Traitor.Uplink; +using Content.Server.Traitor.Uplink.Account; +using Content.Shared.CCVar; +using Content.Shared.Dataset; +using Content.Shared.Traitor.Uplink; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.GameTicking.Presets +{ + [GamePreset("traitor")] + public class PresetTraitor : GamePreset + { + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] protected readonly IEntityManager EntityManager = default!; + + public override string ModeTitle => Loc.GetString("traitor-title"); + + private int MinPlayers { get; set; } + private int PlayersPerTraitor { get; set; } + private int MaxTraitors { get; set; } + private int CodewordCount { get; set; } + private int StartingBalance { get; set; } + private float MaxDifficulty { get; set; } + private int MaxPicks { get; set; } + + private readonly List _traitors = new (); + + public override bool Start(IReadOnlyList readyPlayers, bool force = false) + { + MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers); + PlayersPerTraitor = _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor); + MaxTraitors = _cfg.GetCVar(CCVars.TraitorMaxTraitors); + CodewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); + StartingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance); + MaxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty); + MaxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks); + + if (!force && readyPlayers.Count < MinPlayers) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players", ("readyPlayersCount", readyPlayers.Count), ("minimumPlayers", MinPlayers))); + return false; + } + + if (readyPlayers.Count == 0) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready")); + return false; + } + + var list = new List(readyPlayers).Where(x => + x.Data.ContentData()?.Mind?.AllRoles.All(role => role is not Job {CanBeAntag: false}) ?? false + ).ToList(); + + var prefList = new List(); + + foreach (var player in list) + { + if (!ReadyProfiles.ContainsKey(player.UserId)) + { + continue; + } + var profile = ReadyProfiles[player.UserId]; + if (profile.AntagPreferences.Contains("Traitor")) + { + prefList.Add(player); + } + } + + var numTraitors = MathHelper.Clamp(readyPlayers.Count / PlayersPerTraitor, + 1, MaxTraitors); + + for (var i = 0; i < numTraitors; i++) + { + IPlayerSession traitor; + if(prefList.Count < numTraitors) + { + if (list.Count == 0) + { + Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection."); + break; + } + traitor = _random.PickAndTake(list); + Logger.InfoS("preset", "Insufficient preferred traitors, picking at random."); + } + else + { + traitor = _random.PickAndTake(prefList); + list.Remove(traitor); + Logger.InfoS("preset", "Selected a preferred traitor."); + } + var mind = traitor.Data.ContentData()?.Mind; + if (mind == null) + { + Logger.ErrorS("preset", "Failed getting mind for picked traitor."); + continue; + } + + // creadth: we need to create uplink for the antag. + // PDA should be in place already, so we just need to + // initiate uplink account. + DebugTools.AssertNotNull(mind.OwnedEntity); + + var uplinkAccount = new UplinkAccount(StartingBalance, mind.OwnedEntity!); + var accounts = EntityManager.EntitySysManager.GetEntitySystem(); + accounts.AddNewAccount(uplinkAccount); + + if (!EntityManager.EntitySysManager.GetEntitySystem() + .AddUplink(mind.OwnedEntity!.Value, uplinkAccount)) + continue; + + var traitorRole = new TraitorRole(mind); + mind.AddRole(traitorRole); + _traitors.Add(traitorRole); + } + + var adjectives = _prototypeManager.Index("adjectives").Values; + var verbs = _prototypeManager.Index("verbs").Values; + + var codewordPool = adjectives.Concat(verbs).ToList(); + var finalCodewordCount = Math.Min(CodewordCount, codewordPool.Count); + var codewords = new string[finalCodewordCount]; + for (var i = 0; i < finalCodewordCount; i++) + { + codewords[i] = _random.PickAndTake(codewordPool); + } + + foreach (var traitor in _traitors) + { + traitor.GreetTraitor(codewords); + } + + EntitySystem.Get().AddGameRule(); + return true; + } + + public override void OnGameStarted() + { + var objectivesMgr = IoCManager.Resolve(); + foreach (var traitor in _traitors) + { + //give traitors their objectives + var difficulty = 0f; + for (var pick = 0; pick < MaxPicks && MaxDifficulty > difficulty; pick++) + { + var objective = objectivesMgr.GetRandomObjective(traitor.Mind); + if (objective == null) continue; + if (traitor.Mind.TryAddObjective(objective)) + difficulty += objective.Difficulty; + } + } + } + + public override string GetRoundEndDescription() + { + var result = Loc.GetString("traitor-round-end-result", ("traitorCount", _traitors.Count)); + + foreach (var traitor in _traitors) + { + var name = traitor.Mind.CharacterName; + traitor.Mind.TryGetSession(out var session); + var username = session?.Name; + + var objectives = traitor.Mind.AllObjectives.ToArray(); + if (objectives.Length == 0) + { + if (username != null) + { + if (name == null) + result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username)); + else + result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username), ("name", name)); + } + else if (name != null) + result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name)); + + continue; + } + + if (username != null) + { + if (name == null) + result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives", ("user", username)); + else + result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named", ("user", username), ("name", name)); + } + else if (name != null) + result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name)); + + foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer)) + { + result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}"); + + foreach (var objective in objectiveGroup) + { + foreach (var condition in objective.Conditions) + { + var progress = condition.Progress; + if (progress > 0.99f) + { + result += "\n- " + Loc.GetString( + "traitor-objective-condition-success", + ("condition", condition.Title), + ("markupColor", "green") + ); + } + else + { + result += "\n- " + Loc.GetString( + "traitor-objective-condition-fail", + ("condition", condition.Title), + ("progress", (int) (progress * 100)), + ("markupColor", "red") + ); + } + } + } + } + } + + return result; + } + } +} diff --git a/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs b/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs new file mode 100644 index 0000000000..b30fbf506b --- /dev/null +++ b/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Rules; +using Content.Server.Hands.Components; +using Content.Server.Inventory.Components; +using Content.Server.Items; +using Content.Server.PDA; +using Content.Server.Players; +using Content.Server.Spawners.Components; +using Content.Server.Traitor; +using Content.Server.Traitor.Uplink; +using Content.Server.Traitor.Uplink.Account; +using Content.Server.Traitor.Uplink.Components; +using Content.Server.TraitorDeathMatch.Components; +using Content.Shared.CCVar; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Inventory; +using Content.Shared.MobState.Components; +using Content.Shared.PDA; +using Content.Shared.Traitor.Uplink; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.GameTicking.Presets +{ + [GamePreset("traitordm", "traitordeathmatch")] + public sealed class PresetTraitorDeathMatch : GamePreset + { + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public string PDAPrototypeName => "CaptainPDA"; + public string BeltPrototypeName => "ClothingBeltJanitorFilled"; + public string BackpackPrototypeName => "ClothingBackpackFilled"; + + private RuleMaxTimeRestart _restarter = default!; + private bool _safeToEndRound = false; + + private Dictionary _allOriginalNames = new(); + + public override bool Start(IReadOnlyList readyPlayers, bool force = false) + { + var gameTicker = EntitySystem.Get(); + gameTicker.AddGameRule(); + _restarter = gameTicker.AddGameRule(); + _restarter.RoundMaxTime = TimeSpan.FromMinutes(30); + _restarter.RestartTimer(); + _safeToEndRound = true; + return true; + } + + public override void OnSpawnPlayerCompleted(IPlayerSession session, EntityUid mob, bool lateJoin) + { + var startingBalance = _cfg.GetCVar(CCVars.TraitorDeathMatchStartingBalance); + + // Yup, they're a traitor + var mind = session.Data.ContentData()?.Mind; + if (mind == null) + { + Logger.ErrorS("preset", "Failed getting mind for TDM player."); + return; + } + + var traitorRole = new TraitorRole(mind); + mind.AddRole(traitorRole); + + // Delete anything that may contain "dangerous" role-specific items. + // (This includes the PDA, as everybody gets the captain PDA in this mode for true-all-access reasons.) + if (mind.OwnedEntity is {Valid: true} owned && _entityManager.TryGetComponent(owned, out InventoryComponent? inventory)) + { + var victimSlots = new[] {EquipmentSlotDefines.Slots.IDCARD, EquipmentSlotDefines.Slots.BELT, EquipmentSlotDefines.Slots.BACKPACK}; + foreach (var slot in victimSlots) + { + if (inventory.TryGetSlotItem(slot, out ItemComponent? vItem)) + _entityManager.DeleteEntity(vItem.Owner); + } + + // Replace their items: + + // pda + var newPDA = _entityManager.SpawnEntity(PDAPrototypeName, _entityManager.GetComponent(owned).Coordinates); + inventory.Equip(EquipmentSlotDefines.Slots.IDCARD, _entityManager.GetComponent(newPDA)); + + // belt + var newTmp = _entityManager.SpawnEntity(BeltPrototypeName, _entityManager.GetComponent(owned).Coordinates); + inventory.Equip(EquipmentSlotDefines.Slots.BELT, _entityManager.GetComponent(newTmp)); + + // backpack + newTmp = _entityManager.SpawnEntity(BackpackPrototypeName, _entityManager.GetComponent(owned).Coordinates); + inventory.Equip(EquipmentSlotDefines.Slots.BACKPACK, _entityManager.GetComponent(newTmp)); + + // Like normal traitors, they need access to a traitor account. + var uplinkAccount = new UplinkAccount(startingBalance, owned); + var accounts = _entityManager.EntitySysManager.GetEntitySystem(); + accounts.AddNewAccount(uplinkAccount); + + _entityManager.EntitySysManager.GetEntitySystem() + .AddUplink(owned, uplinkAccount, newPDA); + + _allOriginalNames[uplinkAccount] = _entityManager.GetComponent(owned).EntityName; + + // The PDA needs to be marked with the correct owner. + var pda = _entityManager.GetComponent(newPDA); + _entityManager.EntitySysManager.GetEntitySystem() + .SetOwner(pda, _entityManager.GetComponent(owned).EntityName); + _entityManager.AddComponent(newPDA).UserId = mind.UserId; + } + + // Finally, it would be preferrable if they spawned as far away from other players as reasonably possible. + if (mind.OwnedEntity != null && FindAnyIsolatedSpawnLocation(mind, out var bestTarget)) + { + _entityManager.GetComponent(mind.OwnedEntity.Value).Coordinates = bestTarget; + } + else + { + // The station is too drained of air to safely continue. + if (_safeToEndRound) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-death-match-station-is-too-unsafe-announcement")); + _restarter.RoundMaxTime = TimeSpan.FromMinutes(1); + _restarter.RestartTimer(); + _safeToEndRound = false; + } + } + } + + // It would be nice if this function were moved to some generic helpers class. + private bool FindAnyIsolatedSpawnLocation(Mind.Mind ignoreMe, out EntityCoordinates bestTarget) + { + // Collate people to avoid... + var existingPlayerPoints = new List(); + foreach (var player in _playerManager.ServerSessions) + { + var avoidMeMind = player.Data.ContentData()?.Mind; + if ((avoidMeMind == null) || (avoidMeMind == ignoreMe)) + continue; + var avoidMeEntity = avoidMeMind.OwnedEntity; + if (avoidMeEntity == null) + continue; + if (_entityManager.TryGetComponent(avoidMeEntity.Value, out MobStateComponent? mobState)) + { + // Does have mob state component; if critical or dead, they don't really matter for spawn checks + if (mobState.IsCritical() || mobState.IsDead()) + continue; + } + else + { + // Doesn't have mob state component. Assume something interesting is going on and don't count this as someone to avoid. + continue; + } + existingPlayerPoints.Add(_entityManager.GetComponent(avoidMeEntity.Value).Coordinates); + } + + // Iterate over each possible spawn point, comparing to the existing player points. + // On failure, the returned target is the location that we're already at. + var bestTargetDistanceFromNearest = -1.0f; + // Need the random shuffle or it stuffs the first person into Atmospherics pretty reliably + var ents = _entityManager.EntityQuery().Select(x => x.Owner).ToList(); + _robustRandom.Shuffle(ents); + var foundATarget = false; + bestTarget = EntityCoordinates.Invalid; + var atmosphereSystem = EntitySystem.Get(); + foreach (var entity in ents) + { + if (!atmosphereSystem.IsTileMixtureProbablySafe(_entityManager.GetComponent(entity).Coordinates)) + continue; + + var distanceFromNearest = float.PositiveInfinity; + foreach (var existing in existingPlayerPoints) + { + if (_entityManager.GetComponent(entity).Coordinates.TryDistance(_entityManager, existing, out var dist)) + distanceFromNearest = Math.Min(distanceFromNearest, dist); + } + if (bestTargetDistanceFromNearest < distanceFromNearest) + { + bestTarget = _entityManager.GetComponent(entity).Coordinates; + bestTargetDistanceFromNearest = distanceFromNearest; + foundATarget = true; + } + } + return foundATarget; + } + + public override bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal) + { + if (mind.OwnedEntity is {Valid: true} entity && _entityManager.TryGetComponent(entity, out MobStateComponent? mobState)) + { + if (mobState.IsCritical()) + { + // TODO BODY SYSTEM KILL + var damage = new DamageSpecifier(_prototypeManager.Index("Asphyxiation"), 100); + EntitySystem.Get().TryChangeDamage(entity, damage, true); + } + else if (!mobState.IsDead()) + { + if (_entityManager.HasComponent(entity)) + { + return false; + } + } + } + var session = mind.Session; + if (session == null) + return false; + EntitySystem.Get().Respawn(session); + return true; + } + + public override string GetRoundEndDescription() + { + var lines = new List(); + lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line")); + foreach (var uplink in _entityManager.EntityQuery(true)) + { + var uplinkAcc = uplink.UplinkAccount; + if (uplinkAcc != null && _allOriginalNames.ContainsKey(uplinkAcc)) + { + lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry", + ("originalName", _allOriginalNames[uplinkAcc]), + ("tcBalance", uplinkAcc.Balance))); + } + } + return string.Join('\n', lines); + } + + public override string ModeTitle => Loc.GetString("traitor-death-match-title"); + public override string Description => Loc.GetString("traitor-death-match-description"); + } +} diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs deleted file mode 100644 index 20d12fd682..0000000000 --- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Content.Server.Chat.Managers; -using Content.Shared.CCVar; -using Content.Shared.Damage; -using Content.Shared.MobState.Components; -using Robust.Server.Player; -using Robust.Shared.Configuration; -using Robust.Shared.Enums; -using Robust.Shared.IoC; -using Robust.Shared.Localization; - -namespace Content.Server.GameTicking.Rules; - -/// -/// Simple GameRule that will do a free-for-all death match. -/// Kill everybody else to win. -/// -public sealed class DeathMatchRuleSystem : GameRuleSystem -{ - public override string Prototype => "DeathMatch"; - - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - - private const float RestartDelay = 10f; - private const float DeadCheckDelay = 5f; - - private float? _deadCheckTimer = null; - private float? _restartTimer = null; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnHealthChanged); - } - - public override void Added() - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement")); - - _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; - } - - public override void Removed() - { - _deadCheckTimer = null; - _restartTimer = null; - - _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged; - } - - private void OnHealthChanged(DamageChangedEvent _) - { - RunDelayedCheck(); - } - - private void OnPlayerStatusChanged(object? _, SessionStatusEventArgs e) - { - if (e.NewStatus == SessionStatus.Disconnected) - { - RunDelayedCheck(); - } - } - - private void RunDelayedCheck() - { - if (!Enabled || _deadCheckTimer != null) - return; - - _deadCheckTimer = DeadCheckDelay; - } - - public override void Update(float frameTime) - { - if (!Enabled) - return; - - // If the restart timer is active, that means the round is ending soon, no need to check for winners. - // TODO: We probably want a sane, centralized round end thingie in GameTicker, RoundEndSystem is no good... - if (_restartTimer != null) - { - _restartTimer -= frameTime; - - if (_restartTimer > 0f) - return; - - GameTicker.EndRound(); - GameTicker.RestartRound(); - return; - } - - if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin) || _deadCheckTimer == null) - return; - - _deadCheckTimer -= frameTime; - - if (_deadCheckTimer > 0) - return; - - _deadCheckTimer = null; - - IPlayerSession? winner = null; - foreach (var playerSession in _playerManager.ServerSessions) - { - if (playerSession.AttachedEntity is not {Valid: true} playerEntity - || !TryComp(playerEntity, out MobStateComponent? state)) - continue; - - if (!state.IsAlive()) - continue; - - // Found a second person alive, nothing decided yet! - if (winner != null) - return; - - winner = playerSession; - } - - _chatManager.DispatchServerAnnouncement(winner == null - ? Loc.GetString("rule-death-match-check-winner-stalemate") - : Loc.GetString("rule-death-match-check-winner",("winner", winner))); - - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", RestartDelay))); - _restartTimer = RestartDelay; - } -} diff --git a/Content.Server/GameTicking/Rules/GameRule.cs b/Content.Server/GameTicking/Rules/GameRule.cs new file mode 100644 index 0000000000..4d89345ac4 --- /dev/null +++ b/Content.Server/GameTicking/Rules/GameRule.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Server.GameTicking.Rules +{ + [PublicAPI] + public abstract class GameRule : IEntityEventSubscriber + { + public virtual void Added() + { + + } + + public virtual void Removed() + { + + } + } +} diff --git a/Content.Server/GameTicking/Rules/GameRulePrototype.cs b/Content.Server/GameTicking/Rules/GameRulePrototype.cs deleted file mode 100644 index 57edcbe5dc..0000000000 --- a/Content.Server/GameTicking/Rules/GameRulePrototype.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Content.Server.GameTicking.Rules; - -[Prototype("gameRule")] -public class GameRulePrototype : IPrototype -{ - [DataField("id", required:true)] - public string ID { get; } = default!; -} diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs deleted file mode 100644 index ec90a74bb6..0000000000 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using JetBrains.Annotations; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; - -namespace Content.Server.GameTicking.Rules; - -[PublicAPI] -public abstract class GameRuleSystem : EntitySystem -{ - [Dependency] protected GameTicker GameTicker = default!; - - /// - /// Whether this GameRule is currently enabled or not. - /// Be sure to check this before doing anything rule-specific. - /// - public bool Enabled { get; protected set; } = false; - - /// - /// When the GameRule prototype with this ID is added, this system will be enabled. - /// When it gets removed, this system will be disabled. - /// - public abstract string Prototype { get; } - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnGameRuleAdded); - SubscribeLocalEvent(OnGameRuleRemoved); - - } - - private void OnGameRuleAdded(GameRuleAddedEvent ev) - { - if (ev.Rule.ID != Prototype) - return; - - Enabled = true; - Added(); - } - - private void OnGameRuleRemoved(GameRuleRemovedEvent ev) - { - if (ev.Rule.ID != Prototype) - return; - - Enabled = false; - Removed(); - } - - /// - /// Called when the game rule has been added and this system has been enabled. - /// - public abstract void Added(); - - /// - /// Called when the game rule has been removed and this system has been disabled. - /// - public abstract void Removed(); -} diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs deleted file mode 100644 index c903558d88..0000000000 --- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Threading; -using Content.Server.Chat.Managers; -using Robust.Server.Player; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Timer = Robust.Shared.Timing.Timer; - -namespace Content.Server.GameTicking.Rules; - -public class InactivityTimeRestartRuleSystem : GameRuleSystem -{ - [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - - public override string Prototype => "InactivityTimeRestart"; - - private CancellationTokenSource _timerCancel = new(); - - public TimeSpan InactivityMaxTime { get; set; } = TimeSpan.FromMinutes(10); - public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10); - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(RunLevelChanged); - } - - public override void Added() - { - _playerManager.PlayerStatusChanged += PlayerStatusChanged; - } - - public override void Removed() - { - _playerManager.PlayerStatusChanged -= PlayerStatusChanged; - - StopTimer(); - } - - public void RestartTimer() - { - _timerCancel.Cancel(); - _timerCancel = new CancellationTokenSource(); - Timer.Spawn(InactivityMaxTime, TimerFired, _timerCancel.Token); - } - - public void StopTimer() - { - _timerCancel.Cancel(); - } - - private void TimerFired() - { - GameTicker.EndRound(Loc.GetString("rule-time-has-run-out")); - - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds",(int) RoundEndDelay.TotalSeconds))); - - Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound()); - } - - private void RunLevelChanged(GameRunLevelChangedEvent args) - { - if (!Enabled) - return; - - switch (args.New) - { - case GameRunLevel.InRound: - RestartTimer(); - break; - case GameRunLevel.PreRoundLobby: - case GameRunLevel.PostRound: - StopTimer(); - break; - } - } - - private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) - { - if (GameTicker.RunLevel != GameRunLevel.InRound) - { - return; - } - - if (_playerManager.PlayerCount == 0) - { - RestartTimer(); - } - else - { - StopTimer(); - } - } -} diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs deleted file mode 100644 index 3f3d30b46e..0000000000 --- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Threading; -using Content.Server.Chat.Managers; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Timer = Robust.Shared.Timing.Timer; - -namespace Content.Server.GameTicking.Rules; - -public sealed class MaxTimeRestartRuleSystem : GameRuleSystem -{ - [Dependency] private readonly IChatManager _chatManager = default!; - - public override string Prototype => "MaxTimeRestart"; - - private CancellationTokenSource _timerCancel = new(); - - public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromMinutes(5); - public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10); - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(RunLevelChanged); - } - - public override void Added() - { - if(GameTicker.RunLevel == GameRunLevel.InRound) - RestartTimer(); - } - - public override void Removed() - { - RoundMaxTime = TimeSpan.FromMinutes(5); - RoundEndDelay = TimeSpan.FromMinutes(10); - - StopTimer(); - } - - public void RestartTimer() - { - _timerCancel.Cancel(); - _timerCancel = new CancellationTokenSource(); - Timer.Spawn(RoundMaxTime, TimerFired, _timerCancel.Token); - } - - public void StopTimer() - { - _timerCancel.Cancel(); - } - - private void TimerFired() - { - GameTicker.EndRound(Loc.GetString("rule-time-has-run-out")); - - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) RoundEndDelay.TotalSeconds))); - - Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound()); - } - - private void RunLevelChanged(GameRunLevelChangedEvent args) - { - if (!Enabled) - return; - - switch (args.New) - { - case GameRunLevel.InRound: - RestartTimer(); - break; - case GameRunLevel.PreRoundLobby: - case GameRunLevel.PostRound: - StopTimer(); - break; - } - } -} diff --git a/Content.Server/GameTicking/Rules/RuleDeathMatch.cs b/Content.Server/GameTicking/Rules/RuleDeathMatch.cs new file mode 100644 index 0000000000..0a51de949c --- /dev/null +++ b/Content.Server/GameTicking/Rules/RuleDeathMatch.cs @@ -0,0 +1,110 @@ +using System; +using System.Threading; +using Content.Server.Chat.Managers; +using Content.Shared.CCVar; +using Content.Shared.Damage; +using Content.Shared.MobState.Components; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Server.GameTicking.Rules +{ + /// + /// Simple GameRule that will do a free-for-all death match. + /// Kill everybody else to win. + /// + public sealed class RuleDeathMatch : GameRule, IEntityEventSubscriber + { + private static readonly TimeSpan DeadCheckDelay = TimeSpan.FromSeconds(5); + + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private CancellationTokenSource? _checkTimerCancel; + + public override void Added() + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement")); + + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, OnHealthChanged); + _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged; + } + + public override void Removed() + { + base.Removed(); + + _entityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); + _playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged; + } + + private void OnHealthChanged(DamageChangedEvent _) + { + _runDelayedCheck(); + } + + private void _checkForWinner() + { + _checkTimerCancel = null; + + if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin)) + return; + + IPlayerSession? winner = null; + foreach (var playerSession in _playerManager.ServerSessions) + { + if (playerSession.AttachedEntity is not {Valid: true} playerEntity + || !_entityManager.TryGetComponent(playerEntity, out MobStateComponent? state)) + { + continue; + } + + if (!state.IsAlive()) + { + continue; + } + + if (winner != null) + { + // Found a second person alive, nothing decided yet! + return; + } + + winner = playerSession; + } + + _chatManager.DispatchServerAnnouncement(winner == null + ? Loc.GetString("rule-death-match-check-winner-stalemate") + : Loc.GetString("rule-death-match-check-winner",("winner", winner))); + + var restartDelay = 10; + + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", restartDelay))); + + Timer.Spawn(TimeSpan.FromSeconds(restartDelay), () => EntitySystem.Get().RestartRound()); + } + + private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.Disconnected) + { + _runDelayedCheck(); + } + } + + private void _runDelayedCheck() + { + _checkTimerCancel?.Cancel(); + _checkTimerCancel = new CancellationTokenSource(); + + Timer.Spawn(DeadCheckDelay, _checkForWinner, _checkTimerCancel.Token); + } + } +} diff --git a/Content.Server/GameTicking/Rules/RuleInactivityTimeRestart.cs b/Content.Server/GameTicking/Rules/RuleInactivityTimeRestart.cs new file mode 100644 index 0000000000..f17bd27c9c --- /dev/null +++ b/Content.Server/GameTicking/Rules/RuleInactivityTimeRestart.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading; +using Content.Server.Chat.Managers; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Server.GameTicking.Rules +{ + public class RuleInactivityTimeRestart : GameRule + { + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + + private CancellationTokenSource _timerCancel = new(); + + public TimeSpan InactivityMaxTime { get; set; } = TimeSpan.FromMinutes(10); + public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10); + + public override void Added() + { + base.Added(); + + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, RunLevelChanged); + _playerManager.PlayerStatusChanged += PlayerStatusChanged; + } + + public override void Removed() + { + base.Removed(); + + _entityManager.EventBus.UnsubscribeEvents(this); + _playerManager.PlayerStatusChanged -= PlayerStatusChanged; + + StopTimer(); + } + + public void RestartTimer() + { + _timerCancel.Cancel(); + _timerCancel = new CancellationTokenSource(); + Timer.Spawn(InactivityMaxTime, TimerFired, _timerCancel.Token); + } + + public void StopTimer() + { + _timerCancel.Cancel(); + } + + private void TimerFired() + { + var gameticker = EntitySystem.Get(); + gameticker.EndRound(Loc.GetString("rule-time-has-run-out")); + + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds",(int) RoundEndDelay.TotalSeconds))); + + Timer.Spawn(RoundEndDelay, () => gameticker.RestartRound()); + } + + private void RunLevelChanged(GameRunLevelChangedEvent args) + { + switch (args.New) + { + case GameRunLevel.InRound: + RestartTimer(); + break; + case GameRunLevel.PreRoundLobby: + case GameRunLevel.PostRound: + StopTimer(); + break; + } + } + + private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (EntitySystem.Get().RunLevel != GameRunLevel.InRound) + { + return; + } + + if (_playerManager.PlayerCount == 0) + { + RestartTimer(); + } + else + { + StopTimer(); + } + } + } +} diff --git a/Content.Server/GameTicking/Rules/RuleMaxTimeRestart.cs b/Content.Server/GameTicking/Rules/RuleMaxTimeRestart.cs new file mode 100644 index 0000000000..494b303ec2 --- /dev/null +++ b/Content.Server/GameTicking/Rules/RuleMaxTimeRestart.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; +using Content.Server.Chat.Managers; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Server.GameTicking.Rules +{ + public sealed class RuleMaxTimeRestart : GameRule + { + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + + private CancellationTokenSource _timerCancel = new(); + + public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromMinutes(5); + public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10); + + public override void Added() + { + base.Added(); + + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, RunLevelChanged); + } + + public override void Removed() + { + base.Removed(); + + _entityManager.EventBus.UnsubscribeEvents(this); + StopTimer(); + } + + public void RestartTimer() + { + _timerCancel.Cancel(); + _timerCancel = new CancellationTokenSource(); + Timer.Spawn(RoundMaxTime, TimerFired, _timerCancel.Token); + } + + public void StopTimer() + { + _timerCancel.Cancel(); + } + + private void TimerFired() + { + EntitySystem.Get().EndRound(Loc.GetString("rule-time-has-run-out")); + + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) RoundEndDelay.TotalSeconds))); + + Timer.Spawn(RoundEndDelay, () => EntitySystem.Get().RestartRound()); + } + + private void RunLevelChanged(GameRunLevelChangedEvent args) + { + switch (args.New) + { + case GameRunLevel.InRound: + RestartTimer(); + break; + case GameRunLevel.PreRoundLobby: + case GameRunLevel.PostRound: + StopTimer(); + break; + } + } + } +} diff --git a/Content.Server/GameTicking/Rules/RuleSuspicion.cs b/Content.Server/GameTicking/Rules/RuleSuspicion.cs new file mode 100644 index 0000000000..cb6f34735a --- /dev/null +++ b/Content.Server/GameTicking/Rules/RuleSuspicion.cs @@ -0,0 +1,168 @@ +using System; +using System.Threading; +using Content.Server.Chat.Managers; +using Content.Server.Doors; +using Content.Server.Players; +using Content.Server.Suspicion; +using Content.Server.Suspicion.EntitySystems; +using Content.Server.Suspicion.Roles; +using Content.Shared.CCVar; +using Content.Shared.MobState.Components; +using Content.Shared.Sound; +using Robust.Server.Player; +using Robust.Shared.Audio; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Timing; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Server.GameTicking.Rules +{ + /// + /// Simple GameRule that will do a TTT-like gamemode with traitors. + /// + public sealed class RuleSuspicion : GameRule + { + private static readonly TimeSpan DeadCheckDelay = TimeSpan.FromSeconds(1); + + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IEntityManager _entities = default!; + + [DataField("addedSound")] private SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg"); + + private readonly CancellationTokenSource _checkTimerCancel = new(); + private TimeSpan _endTime; + + public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromSeconds(CCVars.SuspicionMaxTimeSeconds.DefaultValue); + public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10); + + public override void Added() + { + RoundMaxTime = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.SuspicionMaxTimeSeconds)); + + _endTime = _timing.CurTime + RoundMaxTime; + + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-added-announcement")); + + var filter = Filter.Empty() + .AddWhere(session => ((IPlayerSession) session).ContentData()?.Mind?.HasRole() ?? false); + + SoundSystem.Play(filter, _addedSound.GetSound(), AudioParams.Default); + EntitySystem.Get().EndTime = _endTime; + + EntitySystem.Get().AccessType = DoorSystem.AccessTypes.AllowAllNoExternal; + + Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token); + } + + public override void Removed() + { + base.Removed(); + + EntitySystem.Get().AccessType = DoorSystem.AccessTypes.Id; + EntitySystem.Get().EndTime = null; + + _checkTimerCancel.Cancel(); + } + + private void Timeout() + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-traitor-time-has-run-out")); + + EndRound(Victory.Innocents); + } + + private void CheckWinConditions() + { + if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin)) + return; + + var traitorsAlive = 0; + var innocentsAlive = 0; + + foreach (var playerSession in _playerManager.ServerSessions) + { + if (playerSession.AttachedEntity is not {Valid: true} playerEntity + || !_entities.TryGetComponent(playerEntity, out MobStateComponent? mobState) + || !_entities.HasComponent(playerEntity)) + { + continue; + } + + if (!mobState.IsAlive()) + { + continue; + } + + var mind = playerSession.ContentData()?.Mind; + + if (mind != null && mind.HasRole()) + traitorsAlive++; + else + innocentsAlive++; + } + + if (innocentsAlive + traitorsAlive == 0) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-stalemate")); + EndRound(Victory.Stalemate); + } + + else if (traitorsAlive == 0) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-station-win")); + EndRound(Victory.Innocents); + } + else if (innocentsAlive == 0) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-traitor-win")); + EndRound(Victory.Traitors); + } + else if (_timing.CurTime > _endTime) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-traitor-time-has-run-out")); + EndRound(Victory.Innocents); + } + } + + private enum Victory + { + Stalemate, + Innocents, + Traitors + } + + private void EndRound(Victory victory) + { + string text; + + switch (victory) + { + case Victory.Innocents: + text = Loc.GetString("rule-suspicion-end-round-innocents-victory"); + break; + case Victory.Traitors: + text = Loc.GetString("rule-suspicion-end-round-trators-victory"); + break; + default: + text = Loc.GetString("rule-suspicion-end-round-nobody-victory"); + break; + } + + var gameTicker = EntitySystem.Get(); + gameTicker.EndRound(text); + + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", (int) RoundEndDelay.TotalSeconds))); + _checkTimerCancel.Cancel(); + + Timer.Spawn(RoundEndDelay, () => gameTicker.RestartRound()); + } + } +} diff --git a/Content.Server/GameTicking/Rules/RuleTraitor.cs b/Content.Server/GameTicking/Rules/RuleTraitor.cs new file mode 100644 index 0000000000..83d1df18c1 --- /dev/null +++ b/Content.Server/GameTicking/Rules/RuleTraitor.cs @@ -0,0 +1,30 @@ +using Content.Server.Chat.Managers; +using Content.Server.Players; +using Content.Server.Traitor; +using Content.Shared.Sound; +using Robust.Server.Player; +using Robust.Shared.Audio; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.GameTicking.Rules +{ + public class RuleTraitor : GameRule + { + [Dependency] private readonly IChatManager _chatManager = default!; + + [DataField("addedSound")] private SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg"); + + public override void Added() + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-traitor-added-announcement")); + + var filter = Filter.Empty() + .AddWhere(session => ((IPlayerSession)session).ContentData()?.Mind?.HasRole() ?? false); + + SoundSystem.Play(filter, _addedSound.GetSound(), AudioParams.Default); + } + } +} diff --git a/Content.Server/GameTicking/Rules/RuleTraitorDeathMatch.cs b/Content.Server/GameTicking/Rules/RuleTraitorDeathMatch.cs new file mode 100644 index 0000000000..8873d870c6 --- /dev/null +++ b/Content.Server/GameTicking/Rules/RuleTraitorDeathMatch.cs @@ -0,0 +1,7 @@ +namespace Content.Server.GameTicking.Rules +{ + public class RuleTraitorDeathMatch : GameRule + { + // This class only exists so that the game rule is available for the conditional spawner. + } +} diff --git a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs deleted file mode 100644 index 1b9c5af5da..0000000000 --- a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Server.Sandbox; -using Robust.Shared.IoC; - -namespace Content.Server.GameTicking.Rules; - -public class SandboxRuleSystem : GameRuleSystem -{ - [Dependency] private readonly ISandboxManager _sandbox = default!; - - public override string Prototype => "Sandbox"; - - public override void Added() - { - _sandbox.IsSandboxEnabled = true; - } - - public override void Removed() - { - _sandbox.IsSandboxEnabled = false; - } -} diff --git a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs deleted file mode 100644 index 269b6d0ee8..0000000000 --- a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs +++ /dev/null @@ -1,413 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Content.Server.Chat.Managers; -using Content.Server.Doors; -using Content.Server.Players; -using Content.Server.Roles; -using Content.Server.Suspicion; -using Content.Server.Suspicion.Roles; -using Content.Server.Traitor.Uplink; -using Content.Server.Traitor.Uplink.Account; -using Content.Shared.CCVar; -using Content.Shared.GameTicking; -using Content.Shared.MobState.Components; -using Content.Shared.Roles; -using Content.Shared.Sound; -using Content.Shared.Suspicion; -using Content.Shared.Traitor.Uplink; -using Robust.Server.GameObjects; -using Robust.Server.Player; -using Robust.Shared.Audio; -using Robust.Shared.Configuration; -using Robust.Shared.Enums; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Log; -using Robust.Shared.Maths; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Timing; -using Robust.Shared.Utility; -using Timer = Robust.Shared.Timing.Timer; - -namespace Content.Server.GameTicking.Rules; - -/// -/// Simple GameRule that will do a TTT-like gamemode with traitors. -/// -public sealed class SuspicionRuleSystem : GameRuleSystem -{ - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly IEntityManager _entities = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly DoorSystem _doorSystem = default!; - - public override string Prototype => "Suspicion"; - - private static readonly TimeSpan DeadCheckDelay = TimeSpan.FromSeconds(1); - - private readonly HashSet _traitors = new(); - - public IReadOnlyCollection Traitors => _traitors; - - [DataField("addedSound")] private SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg"); - - private CancellationTokenSource _checkTimerCancel = new(); - private TimeSpan? _endTime; - - public TimeSpan? EndTime - { - get => _endTime; - set - { - _endTime = value; - SendUpdateToAll(); - } - } - - public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromSeconds(CCVars.SuspicionMaxTimeSeconds.DefaultValue); - public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10); - - private const string TraitorID = "SuspicionTraitor"; - private const string InnocentID = "SuspicionInnocent"; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnPlayersAssigned); - SubscribeLocalEvent(OnRoundStartAttempt); - SubscribeLocalEvent(OnLateJoinRefresh); - SubscribeLocalEvent(Reset); - - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); - SubscribeLocalEvent(OnRoleAdded); - SubscribeLocalEvent(OnRoleRemoved); - } - - private void OnRoundStartAttempt(RoundStartAttemptEvent ev) - { - if (!Enabled) - return; - - var minPlayers = _cfg.GetCVar(CCVars.SuspicionMinPlayers); - - if (!ev.Forced && ev.Players.Length < minPlayers) - { - _chatManager.DispatchServerAnnouncement($"Not enough players readied up for the game! There were {ev.Players.Length} players readied up out of {minPlayers} needed."); - ev.Cancel(); - return; - } - - if (ev.Players.Length == 0) - { - _chatManager.DispatchServerAnnouncement("No players readied up! Can't start Suspicion."); - ev.Cancel(); - return; - } - } - - private void OnPlayersAssigned(RulePlayerJobsAssignedEvent ev) - { - if (!Enabled) - return; - - var minTraitors = _cfg.GetCVar(CCVars.SuspicionMinTraitors); - var playersPerTraitor = _cfg.GetCVar(CCVars.SuspicionPlayersPerTraitor); - var traitorStartingBalance = _cfg.GetCVar(CCVars.SuspicionStartingBalance); - - var list = new List(ev.Players); - var prefList = new List(); - - foreach (var player in list) - { - if (!ev.Profiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached) - { - continue; - } - prefList.Add(player); - - attached.EnsureComponent(); - } - - var numTraitors = MathHelper.Clamp(ev.Players.Length / playersPerTraitor, - minTraitors, ev.Players.Length); - - var traitors = new List(); - - for (var i = 0; i < numTraitors; i++) - { - IPlayerSession traitor; - if(prefList.Count == 0) - { - if (list.Count == 0) - { - Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection."); - break; - } - traitor = _random.PickAndTake(list); - Logger.InfoS("preset", "Insufficient preferred traitors, picking at random."); - } - else - { - traitor = _random.PickAndTake(prefList); - list.Remove(traitor); - Logger.InfoS("preset", "Selected a preferred traitor."); - } - var mind = traitor.Data.ContentData()?.Mind; - var antagPrototype = _prototypeManager.Index(TraitorID); - - DebugTools.AssertNotNull(mind?.OwnedEntity); - - var traitorRole = new SuspicionTraitorRole(mind!, antagPrototype); - mind!.AddRole(traitorRole); - traitors.Add(traitorRole); - - // creadth: we need to create uplink for the antag. - // PDA should be in place already, so we just need to - // initiate uplink account. - var uplinkAccount = new UplinkAccount(traitorStartingBalance, mind.OwnedEntity!); - var accounts = EntityManager.EntitySysManager.GetEntitySystem(); - accounts.AddNewAccount(uplinkAccount); - - // try to place uplink - if (!EntityManager.EntitySysManager.GetEntitySystem() - .AddUplink(mind.OwnedEntity!.Value, uplinkAccount)) - continue; - } - - foreach (var player in list) - { - var mind = player.Data.ContentData()?.Mind; - var antagPrototype = _prototypeManager.Index(InnocentID); - - DebugTools.AssertNotNull(mind); - - mind!.AddRole(new SuspicionInnocentRole(mind, antagPrototype)); - } - - foreach (var traitor in traitors) - { - traitor.GreetSuspicion(traitors, _chatManager); - } - } - - public override void Added() - { - _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged; - - RoundMaxTime = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.SuspicionMaxTimeSeconds)); - - EndTime = _timing.CurTime + RoundMaxTime; - - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-added-announcement")); - - var filter = Filter.Empty() - .AddWhere(session => ((IPlayerSession) session).ContentData()?.Mind?.HasRole() ?? false); - - SoundSystem.Play(filter, _addedSound.GetSound(), AudioParams.Default); - - _doorSystem.AccessType = DoorSystem.AccessTypes.AllowAllNoExternal; - - _checkTimerCancel = new CancellationTokenSource(); - Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token); - } - - public override void Removed() - { - _doorSystem.AccessType = DoorSystem.AccessTypes.Id; - EndTime = null; - _traitors.Clear(); - - _playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged; - - _checkTimerCancel.Cancel(); - } - - private void CheckWinConditions() - { - if (!Enabled || !_cfg.GetCVar(CCVars.GameLobbyEnableWin)) - return; - - var traitorsAlive = 0; - var innocentsAlive = 0; - - foreach (var playerSession in _playerManager.ServerSessions) - { - if (playerSession.AttachedEntity is not {Valid: true} playerEntity - || !_entities.TryGetComponent(playerEntity, out MobStateComponent? mobState) - || !_entities.HasComponent(playerEntity)) - { - continue; - } - - if (!mobState.IsAlive()) - { - continue; - } - - var mind = playerSession.ContentData()?.Mind; - - if (mind != null && mind.HasRole()) - traitorsAlive++; - else - innocentsAlive++; - } - - if (innocentsAlive + traitorsAlive == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-stalemate")); - EndRound(Victory.Stalemate); - } - - else if (traitorsAlive == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-station-win")); - EndRound(Victory.Innocents); - } - else if (innocentsAlive == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-traitor-win")); - EndRound(Victory.Traitors); - } - else if (_timing.CurTime > _endTime) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-traitor-time-has-run-out")); - EndRound(Victory.Innocents); - } - } - - private enum Victory - { - Stalemate, - Innocents, - Traitors - } - - private void EndRound(Victory victory) - { - string text; - - switch (victory) - { - case Victory.Innocents: - text = Loc.GetString("rule-suspicion-end-round-innocents-victory"); - break; - case Victory.Traitors: - text = Loc.GetString("rule-suspicion-end-round-traitors-victory"); - break; - default: - text = Loc.GetString("rule-suspicion-end-round-nobody-victory"); - break; - } - - GameTicker.EndRound(text); - - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", (int) RoundEndDelay.TotalSeconds))); - _checkTimerCancel.Cancel(); - - Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound()); - } - - private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) - { - if (e.NewStatus == SessionStatus.InGame) - { - SendUpdateTimerMessage(e.Session); - } - } - - private void SendUpdateToAll() - { - foreach (var player in _playerManager.ServerSessions.Where(p => p.Status == SessionStatus.InGame)) - { - SendUpdateTimerMessage(player); - } - } - - private void SendUpdateTimerMessage(IPlayerSession player) - { - var msg = new SuspicionMessages.SetSuspicionEndTimerMessage - { - EndTime = EndTime - }; - - EntityManager.EntityNetManager?.SendSystemNetworkMessage(msg, player.ConnectedClient); - } - - public void AddTraitor(SuspicionRoleComponent role) - { - if (!_traitors.Add(role)) - { - return; - } - - foreach (var traitor in _traitors) - { - traitor.AddAlly(role); - } - - role.SetAllies(_traitors); - } - - public void RemoveTraitor(SuspicionRoleComponent role) - { - if (!_traitors.Remove(role)) - { - return; - } - - foreach (var traitor in _traitors) - { - traitor.RemoveAlly(role); - } - - role.ClearAllies(); - } - - private void Reset(RoundRestartCleanupEvent ev) - { - EndTime = null; - _traitors.Clear(); - } - - private void OnPlayerDetached(EntityUid uid, SuspicionRoleComponent component, PlayerDetachedEvent args) - { - component.SyncRoles(); - } - - private void OnPlayerAttached(EntityUid uid, SuspicionRoleComponent component, PlayerAttachedEvent args) - { - component.SyncRoles(); - } - - private void OnRoleAdded(EntityUid uid, SuspicionRoleComponent component, RoleAddedEvent args) - { - if (args.Role is not SuspicionRole role) return; - component.Role = role; - } - - private void OnRoleRemoved(EntityUid uid, SuspicionRoleComponent component, RoleRemovedEvent args) - { - if (args.Role is not SuspicionRole) return; - component.Role = null; - } - - private void OnLateJoinRefresh(RefreshLateJoinAllowedEvent ev) - { - if (!Enabled) - return; - - ev.Disallow(); - } -} diff --git a/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs deleted file mode 100644 index 2941d6a44a..0000000000 --- a/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs +++ /dev/null @@ -1,270 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Chat.Managers; -using Content.Server.Hands.Components; -using Content.Server.Inventory.Components; -using Content.Server.Items; -using Content.Server.PDA; -using Content.Server.Players; -using Content.Server.Spawners.Components; -using Content.Server.Traitor; -using Content.Server.Traitor.Uplink; -using Content.Server.Traitor.Uplink.Account; -using Content.Server.Traitor.Uplink.Components; -using Content.Server.TraitorDeathMatch.Components; -using Content.Shared.CCVar; -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.Inventory; -using Content.Shared.MobState.Components; -using Content.Shared.PDA; -using Content.Shared.Traitor.Uplink; -using Robust.Server.Player; -using Robust.Shared.Configuration; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Log; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.GameTicking.Rules; - -public class TraitorDeathMatchRuleSystem : GameRuleSystem -{ - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly MaxTimeRestartRuleSystem _restarter = default!; - - public override string Prototype => "TraitorDeathMatch"; - - public string PDAPrototypeName => "CaptainPDA"; - public string BeltPrototypeName => "ClothingBeltJanitorFilled"; - public string BackpackPrototypeName => "ClothingBackpackFilled"; - - private bool _safeToEndRound = false; - - private readonly Dictionary _allOriginalNames = new(); - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnRoundEndText); - SubscribeLocalEvent(OnPlayerSpawned); - SubscribeLocalEvent(OnGhostAttempt); - } - - private void OnPlayerSpawned(PlayerSpawnCompleteEvent ev) - { - if (!Enabled) - return; - - var session = ev.Player; - var startingBalance = _cfg.GetCVar(CCVars.TraitorDeathMatchStartingBalance); - - // Yup, they're a traitor - var mind = session.Data.ContentData()?.Mind; - if (mind == null) - { - Logger.ErrorS("preset", "Failed getting mind for TDM player."); - return; - } - - var traitorRole = new TraitorRole(mind); - mind.AddRole(traitorRole); - - // Delete anything that may contain "dangerous" role-specific items. - // (This includes the PDA, as everybody gets the captain PDA in this mode for true-all-access reasons.) - if (mind.OwnedEntity is {Valid: true} owned && TryComp(owned, out InventoryComponent? inventory)) - { - var victimSlots = new[] {EquipmentSlotDefines.Slots.IDCARD, EquipmentSlotDefines.Slots.BELT, EquipmentSlotDefines.Slots.BACKPACK}; - foreach (var slot in victimSlots) - { - if (inventory.TryGetSlotItem(slot, out ItemComponent? vItem)) - Del(vItem.Owner); - } - - // Replace their items: - - var ownedCoords = Transform(owned).Coordinates; - - // pda - var newPDA = Spawn(PDAPrototypeName, ownedCoords); - inventory.Equip(EquipmentSlotDefines.Slots.IDCARD, Comp(newPDA)); - - // belt - var newTmp = Spawn(BeltPrototypeName, ownedCoords); - inventory.Equip(EquipmentSlotDefines.Slots.BELT, Comp(newTmp)); - - // backpack - newTmp = Spawn(BackpackPrototypeName, ownedCoords); - inventory.Equip(EquipmentSlotDefines.Slots.BACKPACK, Comp(newTmp)); - - // Like normal traitors, they need access to a traitor account. - var uplinkAccount = new UplinkAccount(startingBalance, owned); - var accounts = EntityManager.EntitySysManager.GetEntitySystem(); - accounts.AddNewAccount(uplinkAccount); - - EntityManager.EntitySysManager.GetEntitySystem() - .AddUplink(owned, uplinkAccount, newPDA); - - _allOriginalNames[uplinkAccount] = Name(owned); - - // The PDA needs to be marked with the correct owner. - var pda = Comp(newPDA); - EntityManager.EntitySysManager.GetEntitySystem().SetOwner(pda, Name(owned)); - EntityManager.AddComponent(newPDA).UserId = mind.UserId; - } - - // Finally, it would be preferable if they spawned as far away from other players as reasonably possible. - if (mind.OwnedEntity != null && FindAnyIsolatedSpawnLocation(mind, out var bestTarget)) - { - Transform(mind.OwnedEntity.Value).Coordinates = bestTarget; - } - else - { - // The station is too drained of air to safely continue. - if (_safeToEndRound) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-death-match-station-is-too-unsafe-announcement")); - _restarter.RoundMaxTime = TimeSpan.FromMinutes(1); - _restarter.RestartTimer(); - _safeToEndRound = false; - } - } - } - - private void OnGhostAttempt(GhostAttemptHandleEvent ev) - { - if (!Enabled || ev.Handled) - return; - - ev.Handled = true; - - var mind = ev.Mind; - - if (mind.OwnedEntity is {Valid: true} entity && TryComp(entity, out MobStateComponent? mobState)) - { - if (mobState.IsCritical()) - { - // TODO BODY SYSTEM KILL - var damage = new DamageSpecifier(_prototypeManager.Index("Asphyxiation"), 100); - Get().TryChangeDamage(entity, damage, true); - } - else if (!mobState.IsDead()) - { - if (HasComp(entity)) - { - ev.Result = false; - return; - } - } - } - var session = mind.Session; - if (session == null) - { - ev.Result = false; - return; - } - - GameTicker.Respawn(session); - ev.Result = true; - } - - private void OnRoundEndText(RoundEndTextAppendEvent ev) - { - if (!Enabled) - return; - - var lines = new List(); - lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line")); - foreach (var uplink in EntityManager.EntityQuery(true)) - { - var uplinkAcc = uplink.UplinkAccount; - if (uplinkAcc != null && _allOriginalNames.ContainsKey(uplinkAcc)) - { - lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry", - ("originalName", _allOriginalNames[uplinkAcc]), - ("tcBalance", uplinkAcc.Balance))); - } - } - - ev.AddLine(string.Join('\n', lines)); - } - - public override void Added() - { - _restarter.RoundMaxTime = TimeSpan.FromMinutes(30); - _restarter.RestartTimer(); - _safeToEndRound = true; - } - - public override void Removed() - { - } - - // It would be nice if this function were moved to some generic helpers class. - private bool FindAnyIsolatedSpawnLocation(Mind.Mind ignoreMe, out EntityCoordinates bestTarget) - { - // Collate people to avoid... - var existingPlayerPoints = new List(); - foreach (var player in _playerManager.ServerSessions) - { - var avoidMeMind = player.Data.ContentData()?.Mind; - if ((avoidMeMind == null) || (avoidMeMind == ignoreMe)) - continue; - var avoidMeEntity = avoidMeMind.OwnedEntity; - if (avoidMeEntity == null) - continue; - if (TryComp(avoidMeEntity.Value, out MobStateComponent? mobState)) - { - // Does have mob state component; if critical or dead, they don't really matter for spawn checks - if (mobState.IsCritical() || mobState.IsDead()) - continue; - } - else - { - // Doesn't have mob state component. Assume something interesting is going on and don't count this as someone to avoid. - continue; - } - existingPlayerPoints.Add(Transform(avoidMeEntity.Value).Coordinates); - } - - // Iterate over each possible spawn point, comparing to the existing player points. - // On failure, the returned target is the location that we're already at. - var bestTargetDistanceFromNearest = -1.0f; - // Need the random shuffle or it stuffs the first person into Atmospherics pretty reliably - var ents = EntityManager.EntityQuery().Select(x => x.Owner).ToList(); - _robustRandom.Shuffle(ents); - var foundATarget = false; - bestTarget = EntityCoordinates.Invalid; - var atmosphereSystem = EntitySystem.Get(); - foreach (var entity in ents) - { - if (!atmosphereSystem.IsTileMixtureProbablySafe(Transform(entity).Coordinates)) - continue; - - var distanceFromNearest = float.PositiveInfinity; - foreach (var existing in existingPlayerPoints) - { - if (Transform(entity).Coordinates.TryDistance(EntityManager, existing, out var dist)) - distanceFromNearest = Math.Min(distanceFromNearest, dist); - } - if (bestTargetDistanceFromNearest < distanceFromNearest) - { - bestTarget = Transform(entity).Coordinates; - bestTargetDistanceFromNearest = distanceFromNearest; - foundATarget = true; - } - } - return foundATarget; - } - -} diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs deleted file mode 100644 index 149de869f7..0000000000 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ /dev/null @@ -1,262 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Content.Server.Chat.Managers; -using Content.Server.Objectives.Interfaces; -using Content.Server.Players; -using Content.Server.Roles; -using Content.Server.Traitor; -using Content.Server.Traitor.Uplink; -using Content.Server.Traitor.Uplink.Account; -using Content.Shared.CCVar; -using Content.Shared.Dataset; -using Content.Shared.Sound; -using Content.Shared.Traitor.Uplink; -using Robust.Server.Player; -using Robust.Shared.Audio; -using Robust.Shared.Configuration; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Log; -using Robust.Shared.Maths; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.GameTicking.Rules; - -public class TraitorRuleSystem : GameRuleSystem -{ - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IObjectivesManager _objectivesManager = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - - public override string Prototype => "Traitor"; - - private readonly SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg"); - private readonly List _traitors = new (); - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnStartAttempt); - SubscribeLocalEvent(OnPlayersSpawned); - SubscribeLocalEvent(OnRoundEndText); - } - - public override void Added() - { - // This seems silly, but I'll leave it. - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-traitor-added-announcement")); - } - - public override void Removed() - { - _traitors.Clear(); - } - - private void OnStartAttempt(RoundStartAttemptEvent ev) - { - if (!Enabled) - return; - - var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers); - if (!ev.Forced && ev.Players.Length < minPlayers) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); - ev.Cancel(); - return; - } - - if (ev.Players.Length == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready")); - ev.Cancel(); - return; - } - } - - private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) - { - if (!Enabled) - return; - - var playersPerTraitor = _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor); - var maxTraitors = _cfg.GetCVar(CCVars.TraitorMaxTraitors); - var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); - var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance); - var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty); - var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks); - - var list = new List(ev.Players).Where(x => - x.Data.ContentData()?.Mind?.AllRoles.All(role => role is not Job {CanBeAntag: false}) ?? false - ).ToList(); - - var prefList = new List(); - - foreach (var player in list) - { - if (!ev.Profiles.ContainsKey(player.UserId)) - { - continue; - } - var profile = ev.Profiles[player.UserId]; - if (profile.AntagPreferences.Contains("Traitor")) - { - prefList.Add(player); - } - } - - var numTraitors = MathHelper.Clamp(ev.Players.Length / playersPerTraitor, - 1, maxTraitors); - - for (var i = 0; i < numTraitors; i++) - { - IPlayerSession traitor; - if(prefList.Count < numTraitors) - { - if (list.Count == 0) - { - Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection."); - break; - } - traitor = _random.PickAndTake(list); - Logger.InfoS("preset", "Insufficient preferred traitors, picking at random."); - } - else - { - traitor = _random.PickAndTake(prefList); - list.Remove(traitor); - Logger.InfoS("preset", "Selected a preferred traitor."); - } - var mind = traitor.Data.ContentData()?.Mind; - if (mind == null) - { - Logger.ErrorS("preset", "Failed getting mind for picked traitor."); - continue; - } - - // creadth: we need to create uplink for the antag. - // PDA should be in place already, so we just need to - // initiate uplink account. - DebugTools.AssertNotNull(mind.OwnedEntity); - - var uplinkAccount = new UplinkAccount(startingBalance, mind.OwnedEntity!); - var accounts = EntityManager.EntitySysManager.GetEntitySystem(); - accounts.AddNewAccount(uplinkAccount); - - if (!EntityManager.EntitySysManager.GetEntitySystem() - .AddUplink(mind.OwnedEntity!.Value, uplinkAccount)) - continue; - - var traitorRole = new TraitorRole(mind); - mind.AddRole(traitorRole); - _traitors.Add(traitorRole); - } - - var adjectives = _prototypeManager.Index("adjectives").Values; - var verbs = _prototypeManager.Index("verbs").Values; - - var codewordPool = adjectives.Concat(verbs).ToList(); - var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count); - var codewords = new string[finalCodewordCount]; - for (var i = 0; i < finalCodewordCount; i++) - { - codewords[i] = _random.PickAndTake(codewordPool); - } - - foreach (var traitor in _traitors) - { - traitor.GreetTraitor(codewords); - - //give traitors their objectives - var difficulty = 0f; - for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++) - { - var objective = _objectivesManager.GetRandomObjective(traitor.Mind); - if (objective == null) continue; - if (traitor.Mind.TryAddObjective(objective)) - difficulty += objective.Difficulty; - } - } - - SoundSystem.Play(Filter.Empty().AddPlayers(_traitors.Select(t => t.Mind.Session!)), _addedSound.GetSound(), AudioParams.Default); - } - - private void OnRoundEndText(RoundEndTextAppendEvent ev) - { - if (!Enabled) - return; - - var result = Loc.GetString("traitor-round-end-result", ("traitorCount", _traitors.Count)); - - foreach (var traitor in _traitors) - { - var name = traitor.Mind.CharacterName; - traitor.Mind.TryGetSession(out var session); - var username = session?.Name; - - var objectives = traitor.Mind.AllObjectives.ToArray(); - if (objectives.Length == 0) - { - if (username != null) - { - if (name == null) - result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username)); - else - result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username), ("name", name)); - } - else if (name != null) - result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name)); - - continue; - } - - if (username != null) - { - if (name == null) - result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives", ("user", username)); - else - result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named", ("user", username), ("name", name)); - } - else if (name != null) - result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name)); - - foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer)) - { - result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}"); - - foreach (var objective in objectiveGroup) - { - foreach (var condition in objective.Conditions) - { - var progress = condition.Progress; - if (progress > 0.99f) - { - result += "\n- " + Loc.GetString( - "traitor-objective-condition-success", - ("condition", condition.Title), - ("markupColor", "green") - ); - } - else - { - result += "\n- " + Loc.GetString( - "traitor-objective-condition-fail", - ("condition", condition.Title), - ("progress", (int) (progress * 100)), - ("markupColor", "red") - ); - } - } - } - } - } - - ev.AddLine(result); - } -} diff --git a/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs b/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs index ef0c93ae02..2d625a6b14 100644 --- a/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs +++ b/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs @@ -28,10 +28,6 @@ namespace Content.Server.Objectives.Conditions mobState.IsAlive() && mc.Mind != mind; }).Select(mc => mc.Mind).ToList(); - - if (allHumans.Count == 0) - return new DieCondition(); // I guess I'll die - return new KillRandomPersonCondition {Target = IoCManager.Resolve().Pick(allHumans)}; } } diff --git a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs index c8bdbd38bb..84b3c6bb02 100644 --- a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs +++ b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs @@ -1,14 +1,10 @@ using System.Collections.Generic; using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules; -using Content.Server.Holiday.Greet; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; using Robust.Shared.ViewVariables; namespace Content.Server.Spawners.Components @@ -22,11 +18,11 @@ namespace Content.Server.Spawners.Components public override string Name => "ConditionalSpawner"; [ViewVariables(VVAccess.ReadWrite)] - [DataField("prototypes", customTypeSerializer:typeof(PrototypeIdListSerializer))] + [DataField("prototypes")] public List Prototypes { get; set; } = new(); [ViewVariables(VVAccess.ReadWrite)] - [DataField("gameRules", customTypeSerializer:typeof(PrototypeIdListSerializer))] + [DataField("gameRules")] private readonly List _gameRules = new(); [ViewVariables(VVAccess.ReadWrite)] @@ -35,7 +31,7 @@ namespace Content.Server.Spawners.Components public void RuleAdded(GameRuleAddedEvent obj) { - if(_gameRules.Contains(obj.Rule.ID)) + if(_gameRules.Contains(obj.Rule.GetType().Name)) Spawn(); } diff --git a/Content.Server/Suspicion/EntitySystems/SuspicionEndTimerSystem.cs b/Content.Server/Suspicion/EntitySystems/SuspicionEndTimerSystem.cs new file mode 100644 index 0000000000..8a6af0e51b --- /dev/null +++ b/Content.Server/Suspicion/EntitySystems/SuspicionEndTimerSystem.cs @@ -0,0 +1,78 @@ +using System; +using System.Linq; +using Content.Shared.GameTicking; +using Content.Shared.Suspicion; +using JetBrains.Annotations; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + + +namespace Content.Server.Suspicion.EntitySystems +{ + [UsedImplicitly] + public sealed class SuspicionEndTimerSystem : EntitySystem + { + [Dependency] private readonly IPlayerManager _playerManager = null!; + + private TimeSpan? _endTime; + + public TimeSpan? EndTime + { + get => _endTime; + set + { + _endTime = value; + SendUpdateToAll(); + } + } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(Reset); + + _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged; + } + + public override void Shutdown() + { + base.Shutdown(); + + _playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged; + } + + private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.InGame) + { + SendUpdateTimerMessage(e.Session); + } + } + + private void SendUpdateToAll() + { + foreach (var player in _playerManager.ServerSessions.Where(p => p.Status == SessionStatus.InGame)) + { + SendUpdateTimerMessage(player); + } + } + + private void SendUpdateTimerMessage(IPlayerSession player) + { + var msg = new SuspicionMessages.SetSuspicionEndTimerMessage + { + EndTime = EndTime + }; + + EntityManager.EntityNetManager?.SendSystemNetworkMessage(msg, player.ConnectedClient); + } + + private void Reset(RoundRestartCleanupEvent ev) + { + EndTime = null; + } + } +} diff --git a/Content.Server/Suspicion/EntitySystems/SuspicionRoleSystem.cs b/Content.Server/Suspicion/EntitySystems/SuspicionRoleSystem.cs new file mode 100644 index 0000000000..b2a914f2dd --- /dev/null +++ b/Content.Server/Suspicion/EntitySystems/SuspicionRoleSystem.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using Content.Server.Roles; +using Content.Server.Suspicion.Roles; +using Content.Shared.GameTicking; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; + +namespace Content.Server.Suspicion.EntitySystems +{ + [UsedImplicitly] + public class SuspicionRoleSystem : EntitySystem + { + private readonly HashSet _traitors = new(); + + public IReadOnlyCollection Traitors => _traitors; + + #region Overrides of EntitySystem + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(Reset); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnRoleAdded); + SubscribeLocalEvent(OnRoleRemoved); + } + + private void OnPlayerDetached(EntityUid uid, SuspicionRoleComponent component, PlayerDetachedEvent args) + { + component.SyncRoles(); + } + + private void OnPlayerAttached(EntityUid uid, SuspicionRoleComponent component, PlayerAttachedEvent args) + { + component.SyncRoles(); + } + + private void OnRoleAdded(EntityUid uid, SuspicionRoleComponent component, RoleAddedEvent args) + { + if (args.Role is not SuspicionRole role) return; + component.Role = role; + } + + private void OnRoleRemoved(EntityUid uid, SuspicionRoleComponent component, RoleRemovedEvent args) + { + if (args.Role is not SuspicionRole) return; + component.Role = null; + } + + #endregion + + public void AddTraitor(SuspicionRoleComponent role) + { + if (!_traitors.Add(role)) + { + return; + } + + foreach (var traitor in _traitors) + { + traitor.AddAlly(role); + } + + role.SetAllies(_traitors); + } + + public void RemoveTraitor(SuspicionRoleComponent role) + { + if (!_traitors.Remove(role)) + { + return; + } + + foreach (var traitor in _traitors) + { + traitor.RemoveAlly(role); + } + + role.ClearAllies(); + } + + public override void Shutdown() + { + _traitors.Clear(); + base.Shutdown(); + } + + public void Reset(RoundRestartCleanupEvent ev) + { + _traitors.Clear(); + } + } +} diff --git a/Content.Server/Suspicion/SuspicionRoleComponent.cs b/Content.Server/Suspicion/SuspicionRoleComponent.cs index 7fcfc01b2a..df51ff8537 100644 --- a/Content.Server/Suspicion/SuspicionRoleComponent.cs +++ b/Content.Server/Suspicion/SuspicionRoleComponent.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using Content.Server.GameTicking.Rules; using Content.Server.Mind.Components; using Content.Server.Roles; +using Content.Server.Suspicion.EntitySystems; using Content.Server.Suspicion.Roles; using Content.Shared.Examine; using Content.Shared.MobState.Components; @@ -11,6 +11,7 @@ using Content.Shared.Suspicion; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Players; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -42,17 +43,17 @@ namespace Content.Server.Suspicion Dirty(); - var sus = EntitySystem.Get(); + var suspicionRoleSystem = EntitySystem.Get(); if (value == null || !value.Antagonist) { ClearAllies(); - sus.RemoveTraitor(this); + suspicionRoleSystem.RemoveTraitor(this); } else if (value.Antagonist) { - SetAllies(sus.Traitors); - sus.AddTraitor(this); + SetAllies(suspicionRoleSystem.Traitors); + suspicionRoleSystem.AddTraitor(this); } } } diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 5fc5e65842..b7eef2588d 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Content.Server.GameTicking; -using Content.Server.GameTicking.Presets; using Content.Server.Maps; using Content.Server.RoundEnd; using Content.Shared.CCVar; @@ -12,7 +11,6 @@ using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Voting.Managers @@ -107,15 +105,13 @@ namespace Content.Server.Voting.Managers private void CreatePresetVote(IPlayerSession? initiator) { - var presets = new Dictionary(); - - foreach (var preset in _prototypeManager.EnumeratePrototypes()) + var presets = new Dictionary { - if(!preset.ShowInVote) - continue; - - presets[preset.ID] = preset.ModeTitle; - } + ["traitor"] = "mode-traitor", + ["extended"] = "mode-extended", + ["sandbox"] = "mode-sandbox", + ["suspicion"] = "mode-suspicion", + }; var alone = _playerManager.PlayerCount == 1 && initiator != null; var options = new VoteOptions @@ -154,7 +150,7 @@ namespace Content.Server.Voting.Managers Loc.GetString("ui-vote-gamemode-win", ("winner", Loc.GetString(presets[picked])))); } - EntitySystem.Get().SetGamePreset(picked); + EntitySystem.Get().SetStartPreset(picked); }; } diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs index 813f0a4d68..0e9920561d 100644 --- a/Content.Server/Voting/Managers/VoteManager.cs +++ b/Content.Server/Voting/Managers/VoteManager.cs @@ -36,7 +36,6 @@ namespace Content.Server.Voting.Managers [Dependency] private readonly IAdminManager _adminMgr = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IAfkManager _afkManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameMapManager _gameMapManager = default!; private int _nextVoteId = 1; diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 3bc05bb0bc..c5095bf568 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -85,7 +85,7 @@ namespace Content.Shared.CCVar /// Controls the default game preset. /// public static readonly CVarDef - GameLobbyDefaultPreset = CVarDef.Create("game.defaultpreset", "suspicion", CVar.ARCHIVE); + GameLobbyDefaultPreset = CVarDef.Create("game.defaultpreset", "Suspicion", CVar.ARCHIVE); /// /// Controls if the game can force a different preset if the current preset's criteria are not met. @@ -97,7 +97,7 @@ namespace Content.Shared.CCVar /// The preset for the game to fall back to if the selected preset could not be used, and fallback is enabled. /// public static readonly CVarDef - GameLobbyFallbackPreset = CVarDef.Create("game.fallbackpreset", "sandbox", CVar.ARCHIVE); + GameLobbyFallbackPreset = CVarDef.Create("game.fallbackpreset", "Sandbox", CVar.ARCHIVE); /// /// Controls if people can win the game in Suspicion or Deathmatch. diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-deathmatch.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-deathmatch.ftl deleted file mode 100644 index ee41fcefea..0000000000 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-deathmatch.ftl +++ /dev/null @@ -1,2 +0,0 @@ -death-match-title = DeathMatch -death-match-description = Kill anything that moves! diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-extended.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-extended.ftl deleted file mode 100644 index 56223aef40..0000000000 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-extended.ftl +++ /dev/null @@ -1,2 +0,0 @@ -extended-title = Extended -extended-description = No antagonists, have fun! diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-sandbox.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-sandbox.ftl deleted file mode 100644 index 6e26073d34..0000000000 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-sandbox.ftl +++ /dev/null @@ -1,2 +0,0 @@ -sandbox-title = Sandbox -sandbox-description = No stress, build something! diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-suspicion.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-suspicion.ftl deleted file mode 100644 index d70a8cdffb..0000000000 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-suspicion.ftl +++ /dev/null @@ -1,2 +0,0 @@ -suspicion-title = Suspicion -suspicion-description = Suspicion on the Space Station. There are traitors on board... Can you kill them before they kill you? diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl index f15743b696..dcb06af34a 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl @@ -25,7 +25,6 @@ traitor-objective-condition-success = {$condition} | [color={$markupColor}]Succe traitor-objective-condition-fail = {$condition} | [color={$markupColor}]Failure![/color] ({$progress}%) traitor-title = Traitor -traitor-description = There are traitors among us... traitor-not-enough-ready-players = Not enough players readied up for the game! There were {$readyPlayersCount} players readied up out of {$minimumPlayers} needed. traitor-no-one-ready = No players readied up! Can't start Traitor. @@ -41,4 +40,4 @@ traitor-death-match-end-round-description-entry = {$originalName}'s PDA, with {$ # TraitorRole traitor-role-name = Syndicate Agent traitor-role-greeting = Hello Agent -traitor-role-codewords = Your codewords are: {$codewords} +traitor-role-codewords = Your codewords are: {$codewords} \ No newline at end of file diff --git a/Resources/Locale/en-US/game-ticking/game-rules/rule-suspicion.ftl b/Resources/Locale/en-US/game-ticking/game-rules/rule-suspicion.ftl index fc536cf44a..51bed9f128 100644 --- a/Resources/Locale/en-US/game-ticking/game-rules/rule-suspicion.ftl +++ b/Resources/Locale/en-US/game-ticking/game-rules/rule-suspicion.ftl @@ -4,5 +4,5 @@ rule-suspicion-check-winner-stalemate = Everybody is dead, it's a stalemate! rule-suspicion-check-winner-station-win = The traitors are dead! The innocents win. rule-suspicion-check-winner-traitor-win = The innocents are dead! The traitors win. rule-suspicion-end-round-innocents-victory = The innocents have won! -rule-suspicion-end-round-traitors-victory = The traitors have won! -rule-suspicion-end-round-nobody-victory = Nobody wins! +rule-suspicion-end-round-trators-victory = The traitors have won! +rule-suspicion-end-round-nobody-victory = Nobody wins! \ No newline at end of file diff --git a/Resources/Locale/en-US/suspicion/roles/suspicion-traitor-role.ftl b/Resources/Locale/en-US/suspicion/roles/suspicion-traitor-role.ftl index 0562dc3652..7c2623196f 100644 --- a/Resources/Locale/en-US/suspicion/roles/suspicion-traitor-role.ftl +++ b/Resources/Locale/en-US/suspicion/roles/suspicion-traitor-role.ftl @@ -6,6 +6,6 @@ suspicion-objective = Objective: {$objectiveText} # Shown when greeted with the Suspicion role suspicion-partners-in-crime = {$partnersCount -> - [zero] You're on your own. Good luck! + *[zero] You're on your own. Good luck! [one] Your partner in crime is {$partnerNames}. - [other] Your partners in crime are {$partnerNames}. + [other] Your partners in crime are {$partnerNames}. \ No newline at end of file diff --git a/Resources/Locale/en-US/voting/managers/vote-manager.ftl b/Resources/Locale/en-US/voting/managers/vote-manager.ftl index d4534fbdc4..2f349acfe3 100644 --- a/Resources/Locale/en-US/voting/managers/vote-manager.ftl +++ b/Resources/Locale/en-US/voting/managers/vote-manager.ftl @@ -16,3 +16,8 @@ ui-vote-gamemode-win = { $winner } won the gamemode vote! ui-vote-map-title = Next map ui-vote-map-tie = Tie for map vote! Picking... { $picked } ui-vote-map-win = { $winner } won the map vote! + +mode-traitor = Traitor +mode-extended = Extended +mode-sandbox = Sandbox +mode-suspicion = Suspicion diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Conditional/suspicion.yml b/Resources/Prototypes/Entities/Markers/Spawners/Conditional/suspicion.yml index 61a9e44b68..269bdbfb09 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Conditional/suspicion.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Conditional/suspicion.yml @@ -19,7 +19,7 @@ - RifleCalico chance: 0.75 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Pistol Spawner @@ -45,7 +45,7 @@ - PistolPaco chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Melee Spawner @@ -66,7 +66,7 @@ - Stunbaton chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Revolver Spawner @@ -84,7 +84,7 @@ - RevolverMateba chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Shotgun Spawner @@ -106,7 +106,7 @@ - ShotgunSawn chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion SMG Spawner @@ -126,7 +126,7 @@ - SmgZoric chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Sniper Spawner @@ -144,7 +144,7 @@ - SniperHeavy chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Hitscan Spawner @@ -166,7 +166,7 @@ - TaserGun chance: 0.85 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Launchers Spawner @@ -183,7 +183,7 @@ - LauncherRocket chance: 0.75 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Grenades Spawner @@ -206,7 +206,7 @@ - SoapSyndie # shhh! chance: 0.75 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Rifle Ammo Spawner @@ -227,7 +227,7 @@ - MagazinePistolCalicoTopMounted chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Shotgun Ammo Spawner @@ -243,7 +243,7 @@ - MagazineShotgun chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Pistol Ammo Spawner @@ -260,7 +260,7 @@ - MagazineHCPistol chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Magnum Ammo Spawner @@ -277,7 +277,7 @@ - MagazineMagnumSmg chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion - type: entity name: Suspicion Launcher Ammo Spawner @@ -294,4 +294,4 @@ - GrenadeFrag chance: 0.95 gameRules: - - Suspicion + - RuleSuspicion diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml b/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml index 3c607f9034..331c5502a9 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml @@ -12,4 +12,4 @@ - TraitorDMRedemptionMachine chance: 1.0 gameRules: - - TraitorDeathMatch + - RuleTraitorDeathMatch diff --git a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml index c70b160f2f..1d2168232c 100644 --- a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml +++ b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml @@ -1,6 +1,6 @@ - type: antag id: SuspicionTraitor - name: "suspect" + name: "traitor" antagonist: true setPreference: true objective: "Kill the innocents." diff --git a/Resources/Prototypes/Roles/Antags/traitor.yml b/Resources/Prototypes/Roles/Antags/traitor.yml deleted file mode 100644 index 346b434347..0000000000 --- a/Resources/Prototypes/Roles/Antags/traitor.yml +++ /dev/null @@ -1,6 +0,0 @@ -- type: antag - id: Traitor - name: "traitor" - antagonist: true - setPreference: true - objective: "Complete your objectives without being caught." diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml deleted file mode 100644 index d57ddf18c1..0000000000 --- a/Resources/Prototypes/game_presets.yml +++ /dev/null @@ -1,60 +0,0 @@ -- type: gamePreset - id: Extended - alias: - - extended - - shittersafari - name: extended-title - showInVote: true - description: extended-description - -- type: gamePreset - id: Sandbox - alias: - - sandbox - name: sandbox-title - description: sandbox-description - showInVote: true - rules: - - Sandbox - -- type: gamePreset - id: Traitor - alias: - - traitor - name: traitor-title - description: traitor-description - showInVote: true - rules: - - Traitor - -- type: gamePreset - id: Suspicion - alias: - - suspicion - - sus - name: suspicion-title - description: suspicion-description - showInVote: true - rules: - - Suspicion - -- type: gamePreset - id: Deathmatch - alias: - - deathmatch - - dm - name: death-match-title - description: death-match-description - rules: - - DeathMatch - -- type: gamePreset - id: TraitorDeathMatch - alias: - - traitordm - - traitordeathmatch - name: traitor-death-match-title - description: traitor-death-match-description - rules: - - TraitorDeathMatch - - MaxTimeRestart diff --git a/Resources/Prototypes/game_rules.yml b/Resources/Prototypes/game_rules.yml deleted file mode 100644 index 8c9b78ac4f..0000000000 --- a/Resources/Prototypes/game_rules.yml +++ /dev/null @@ -1,20 +0,0 @@ -- type: gameRule - id: DeathMatch - -- type: gameRule - id: InactivityTimeRestart - -- type: gameRule - id: MaxTimeRestart - -- type: gameRule - id: Suspicion - -- type: gameRule - id: Traitor - -- type: gameRule - id: TraitorDeathMatch - -- type: gameRule - id: Sandbox