diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 71d719c523..0ff2077933 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -97,6 +97,8 @@ 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 58e8648d56..e5b7714e95 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -4,12 +4,14 @@ 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(RuleMaxTimeRestart))] + [TestOf(typeof(MaxTimeRestartRuleSystem))] public class RuleMaxTimeRestartTest : ContentIntegrationTest { [Test] @@ -19,7 +21,9 @@ namespace Content.IntegrationTests.Tests.GameRules { CVarOverrides = { - ["game.lobbyenabled"] = "true" + ["game.lobbyenabled"] = "true", + ["game.dummyticker"] = "false", + ["game.defaultpreset"] = "", // No preset. } }; var server = StartServer(options); @@ -27,16 +31,15 @@ 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)); - maxTimeRule = sGameTicker.AddGameRule(); - maxTimeRule.RoundMaxTime = TimeSpan.FromSeconds(3); + sGameTicker.AddGameRule(IoCManager.Resolve().Index(maxTimeMaxTimeRestartRuleSystem.Prototype)); + maxTimeMaxTimeRestartRuleSystem.RoundMaxTime = TimeSpan.FromSeconds(3); sGameTicker.StartRound(); }); @@ -46,7 +49,7 @@ namespace Content.IntegrationTests.Tests.GameRules Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound)); }); - var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeRule.RoundMaxTime.TotalSeconds * 1.1f); + var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundMaxTime.TotalSeconds * 1.1f); await server.WaitRunTicks(ticks); await server.WaitAssertion(() => @@ -54,7 +57,7 @@ namespace Content.IntegrationTests.Tests.GameRules Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PostRound)); }); - ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeRule.RoundEndDelay.TotalSeconds * 1.1f); + ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.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 60d73a4060..8c17235781 100644 --- a/Content.Server/GameTicking/Commands/ForcePresetCommand.cs +++ b/Content.Server/GameTicking/Commands/ForcePresetCommand.cs @@ -1,4 +1,5 @@ using Content.Server.Administration; +using Content.Server.GameTicking.Presets; using Content.Shared.Administration; using Robust.Shared.Console; using Robust.Shared.GameObjects; @@ -29,13 +30,13 @@ namespace Content.Server.GameTicking.Commands } var name = args[0]; - if (!ticker.TryGetPreset(name, out var type)) + if (!ticker.TryFindGamePreset(name, out var type)) { shell.WriteLine($"No preset exists with name {name}."); return; } - ticker.SetStartPreset(type, true); + ticker.SetGamePreset(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 28aaeb5d1d..adaaff11e1 100644 --- a/Content.Server/GameTicking/Commands/GoLobbyCommand.cs +++ b/Content.Server/GameTicking/Commands/GoLobbyCommand.cs @@ -1,5 +1,6 @@ using System; using Content.Server.Administration; +using Content.Server.GameTicking.Presets; using Content.Shared; using Content.Shared.Administration; using Content.Shared.CCVar; @@ -18,14 +19,14 @@ namespace Content.Server.GameTicking.Commands public string Help => $"Usage: {Command} / {Command} "; public void Execute(IConsoleShell shell, string argStr, string[] args) { - Type? preset = null; + GamePresetPrototype? preset = null; var presetName = string.Join(" ", args); var ticker = EntitySystem.Get(); if (args.Length > 0) { - if (!ticker.TryGetPreset(presetName, out preset)) + if (!ticker.TryFindGamePreset(presetName, out preset)) { shell.WriteLine($"No preset found with name {presetName}"); return; @@ -39,7 +40,7 @@ namespace Content.Server.GameTicking.Commands if (preset != null) { - ticker.SetStartPreset(preset); + ticker.SetGamePreset(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 6c8a313f79..3c9c15e7f0 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.SetStartPreset(args[0]); + ticker.SetGamePreset(args[0]); } } } diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index e7861c7a5d..c40ffedc31 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.Preferences; -using Robust.Shared.Network; -using Robust.Shared.ViewVariables; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.MobState.Components; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; namespace Content.Server.GameTicking @@ -16,60 +16,20 @@ namespace Content.Server.GameTicking { public const float PresetFailedCooldownIncrease = 30f; - [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 GamePresetPrototype? _preset; private void InitializeGamePreset() { - 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)); + SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset)); } - 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) + public void SetGamePreset(GamePresetPrototype preset, bool force = false) { // Do nothing if this game ticker is a dummy! if (DummyTicker) return; - if (!typeof(GamePreset).IsAssignableFrom(type)) throw new ArgumentException("type must inherit GamePreset"); - - _presetType = type; + _preset = preset; UpdateInfoText(); if (force) @@ -78,21 +38,138 @@ namespace Content.Server.GameTicking } } - public void SetStartPreset(string name, bool force = false) + public void SetGamePreset(string preset, bool force = false) { - if (!TryGetPreset(name, out var type)) - { - throw new NotSupportedException($"No preset found with name {name}"); - } - - SetStartPreset(type, force); + var proto = FindGamePreset(preset); + if(proto != null) + SetGamePreset(proto, force); } - private GamePreset MakeGamePreset(Dictionary readyProfiles) + public GamePresetPrototype? FindGamePreset(string preset) { - var preset = _dynamicTypeFactory.CreateInstance(_presetType ?? typeof(PresetSandbox)); - preset.ReadyProfiles = readyProfiles; - return preset; + if (_prototypeManager.TryIndex(preset, out GamePresetPrototype? presetProto)) + return presetProto; + + foreach (var proto in _prototypeManager.EnumeratePrototypes()) + { + foreach (var alias in proto.Alias) + { + if (preset.Equals(alias, StringComparison.InvariantCultureIgnoreCase)) + return proto; + } + } + + return null; + } + + public bool TryFindGamePreset(string preset, [NotNullWhen(true)] out GamePresetPrototype? prototype) + { + 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; } } } diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index c0a9f06db9..83b962fa92 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,66 +7,68 @@ namespace Content.Server.GameTicking { public partial class GameTicker { - [ViewVariables] private readonly List _gameRules = new(); - public IEnumerable ActiveGameRules => _gameRules; + // No duplicates. + [ViewVariables] private readonly HashSet _gameRules = new(); + public IEnumerable ActiveGameRules => _gameRules; - public T AddGameRule() where T : GameRule, new() + public bool AddGameRule(GameRulePrototype rule) { - var instance = _dynamicTypeFactory.CreateInstance(); - - _gameRules.Add(instance); - instance.Added(); - - RaiseLocalEvent(new GameRuleAddedEvent(instance)); - - return instance; - } - - public bool HasGameRule(string? name) - { - if (name == null) + if (!_gameRules.Add(rule)) return false; - foreach (var rule in _gameRules) - { - if (rule.GetType().Name == name) - { - return true; - } - } - - return false; + RaiseLocalEvent(new GameRuleAddedEvent(rule)); + return true; } - public bool HasGameRule(Type? type) + public bool RemoveGameRule(GameRulePrototype rule) { - if (type == null || !typeof(GameRule).IsAssignableFrom(type)) + if (!_gameRules.Remove(rule)) return false; - foreach (var rule in _gameRules) + 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) { - if (rule.GetType().IsAssignableFrom(type)) + if (ruleProto.ID.Equals(rule)) return true; } return false; } - public void RemoveGameRule(GameRule rule) + public void ClearGameRules() { - if (_gameRules.Contains(rule)) return; - - rule.Removed(); - - _gameRules.Remove(rule); + foreach (var rule in _gameRules.ToArray()) + { + RemoveGameRule(rule); + } } } public class GameRuleAddedEvent { - public GameRule Rule { get; } + public GameRulePrototype Rule { get; } - public GameRuleAddedEvent(GameRule rule) + public GameRuleAddedEvent(GameRulePrototype rule) + { + Rule = rule; + } + } + + public class GameRuleRemovedEvent + { + public GameRulePrototype Rule { get; } + + public GameRuleRemovedEvent(GameRulePrototype rule) { Rule = rule; } diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs index 7d65e6cb37..369495d9c3 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 = Preset.ModeTitle; - var desc = Preset.Description; + var gmTitle = Loc.GetString(_preset.ModeTitle); + var desc = Loc.GetString(_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 bcf027b3b3..63e51b9845 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; } - private void PlayerJoinGame(IPlayerSession session) + public 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 1cd48d0180..d420a4ebdd 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -18,6 +18,7 @@ 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; @@ -64,6 +65,7 @@ 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); @@ -108,6 +110,8 @@ namespace Content.Server.GameTicking SendServerMessage(Loc.GetString("game-ticker-start-round")); + AddGamePresetRules(); + List readyPlayers; if (LobbyEnabled) { @@ -118,9 +122,6 @@ 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(); @@ -143,10 +144,58 @@ 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 readyPlayers) + foreach (var player in origReadyPlayers) { if (assignedJobs.ContainsKey(player)) { @@ -188,39 +237,15 @@ namespace Content.Server.GameTicking SpawnPlayer(player, profiles[player.UserId], station, job, false); } - // Time to start the preset. - Preset = MakeGamePreset(profiles); + RefreshLateJoinAllowed(); - DisallowLateJoin |= Preset.DisallowLateJoin; + // 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)); - 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!"); - } + _pauseManager.DoMapInitialize(DefaultMap); - 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(); + _roundStartDateTime = DateTime.UtcNow; + RunLevel = GameRunLevel.InRound; _roundStartTimeSpan = _gameTiming.RealTime; SendStatusToAll(); @@ -229,6 +254,13 @@ 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! @@ -241,8 +273,13 @@ namespace Content.Server.GameTicking RunLevel = GameRunLevel.PostRound; //Tell every client the round has ended. - var gamemodeTitle = Preset?.ModeTitle ?? string.Empty; - var roundEndText = text + $"\n{Preset?.GetRoundEndDescription() ?? string.Empty}"; + 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}"; //Get the timespan of the round. var roundDuration = RoundDuration(); @@ -332,8 +369,6 @@ namespace Content.Server.GameTicking } else { - Preset = null; - if (_playerManager.PlayerCount == 0) _roundStartCountdownHasNotStartedYetDueToNoPlayers = true; else @@ -375,10 +410,7 @@ namespace Content.Server.GameTicking _mapManager.Restart(); // Clear up any game rules. - foreach (var rule in _gameRules) - { - rule.Removed(); - } + ClearGameRules(); _gameRules.Clear(); @@ -452,4 +484,102 @@ 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 c93d803727..1cb89d6243 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -63,6 +63,10 @@ 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(); @@ -73,16 +77,23 @@ 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. @@ -143,7 +154,9 @@ 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}."); - Preset?.OnSpawnPlayerCompleted(player, mob, lateJoin); + // 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); } public void Respawn(IPlayerSession player) @@ -355,4 +368,52 @@ 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 25fecfc3d5..b03752139c 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -1,12 +1,14 @@ 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; @@ -83,17 +85,18 @@ 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 deleted file mode 100644 index f9cb77de42..0000000000 --- a/Content.Server/GameTicking/Presets/GamePreset.cs +++ /dev/null @@ -1,107 +0,0 @@ -#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 deleted file mode 100644 index 83473cea27..0000000000 --- a/Content.Server/GameTicking/Presets/GamePresetAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 new file mode 100644 index 0000000000..f511535780 --- /dev/null +++ b/Content.Server/GameTicking/Presets/GamePresetPrototype.cs @@ -0,0 +1,34 @@ +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 deleted file mode 100644 index 39844f7362..0000000000 --- a/Content.Server/GameTicking/Presets/PresetDeathMatch.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 6989656bf2..0000000000 --- a/Content.Server/GameTicking/Presets/PresetExtended.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index fe17cdb69b..0000000000 --- a/Content.Server/GameTicking/Presets/PresetSandbox.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index d68aa75611..0000000000 --- a/Content.Server/GameTicking/Presets/PresetSuspicion.cs +++ /dev/null @@ -1,146 +0,0 @@ -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 deleted file mode 100644 index fb85f34af7..0000000000 --- a/Content.Server/GameTicking/Presets/PresetTraitor.cs +++ /dev/null @@ -1,244 +0,0 @@ -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 deleted file mode 100644 index b30fbf506b..0000000000 --- a/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs +++ /dev/null @@ -1,244 +0,0 @@ -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 new file mode 100644 index 0000000000..20d12fd682 --- /dev/null +++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs @@ -0,0 +1,127 @@ +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 deleted file mode 100644 index 4d89345ac4..0000000000 --- a/Content.Server/GameTicking/Rules/GameRule.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 new file mode 100644 index 0000000000..57edcbe5dc --- /dev/null +++ b/Content.Server/GameTicking/Rules/GameRulePrototype.cs @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000000..ec90a74bb6 --- /dev/null +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs @@ -0,0 +1,61 @@ +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 new file mode 100644 index 0000000000..c903558d88 --- /dev/null +++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs @@ -0,0 +1,97 @@ +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 new file mode 100644 index 0000000000..3f3d30b46e --- /dev/null +++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs @@ -0,0 +1,80 @@ +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 deleted file mode 100644 index 0a51de949c..0000000000 --- a/Content.Server/GameTicking/Rules/RuleDeathMatch.cs +++ /dev/null @@ -1,110 +0,0 @@ -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 deleted file mode 100644 index f17bd27c9c..0000000000 --- a/Content.Server/GameTicking/Rules/RuleInactivityTimeRestart.cs +++ /dev/null @@ -1,94 +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 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 deleted file mode 100644 index 494b303ec2..0000000000 --- a/Content.Server/GameTicking/Rules/RuleMaxTimeRestart.cs +++ /dev/null @@ -1,71 +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 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 deleted file mode 100644 index cb6f34735a..0000000000 --- a/Content.Server/GameTicking/Rules/RuleSuspicion.cs +++ /dev/null @@ -1,168 +0,0 @@ -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 deleted file mode 100644 index 83d1df18c1..0000000000 --- a/Content.Server/GameTicking/Rules/RuleTraitor.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 8873d870c6..0000000000 --- a/Content.Server/GameTicking/Rules/RuleTraitorDeathMatch.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 new file mode 100644 index 0000000000..1b9c5af5da --- /dev/null +++ b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000000..269b6d0ee8 --- /dev/null +++ b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs @@ -0,0 +1,413 @@ +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 new file mode 100644 index 0000000000..2941d6a44a --- /dev/null +++ b/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs @@ -0,0 +1,270 @@ +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 new file mode 100644 index 0000000000..149de869f7 --- /dev/null +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -0,0 +1,262 @@ +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 2d625a6b14..ef0c93ae02 100644 --- a/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs +++ b/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs @@ -28,6 +28,10 @@ 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 84b3c6bb02..c8bdbd38bb 100644 --- a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs +++ b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs @@ -1,10 +1,14 @@ 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 @@ -18,11 +22,11 @@ namespace Content.Server.Spawners.Components public override string Name => "ConditionalSpawner"; [ViewVariables(VVAccess.ReadWrite)] - [DataField("prototypes")] + [DataField("prototypes", customTypeSerializer:typeof(PrototypeIdListSerializer))] public List Prototypes { get; set; } = new(); [ViewVariables(VVAccess.ReadWrite)] - [DataField("gameRules")] + [DataField("gameRules", customTypeSerializer:typeof(PrototypeIdListSerializer))] private readonly List _gameRules = new(); [ViewVariables(VVAccess.ReadWrite)] @@ -31,7 +35,7 @@ namespace Content.Server.Spawners.Components public void RuleAdded(GameRuleAddedEvent obj) { - if(_gameRules.Contains(obj.Rule.GetType().Name)) + if(_gameRules.Contains(obj.Rule.ID)) Spawn(); } diff --git a/Content.Server/Suspicion/EntitySystems/SuspicionEndTimerSystem.cs b/Content.Server/Suspicion/EntitySystems/SuspicionEndTimerSystem.cs deleted file mode 100644 index 8a6af0e51b..0000000000 --- a/Content.Server/Suspicion/EntitySystems/SuspicionEndTimerSystem.cs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index b2a914f2dd..0000000000 --- a/Content.Server/Suspicion/EntitySystems/SuspicionRoleSystem.cs +++ /dev/null @@ -1,96 +0,0 @@ -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 df51ff8537..7fcfc01b2a 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,7 +11,6 @@ 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; @@ -43,17 +42,17 @@ namespace Content.Server.Suspicion Dirty(); - var suspicionRoleSystem = EntitySystem.Get(); + var sus = EntitySystem.Get(); if (value == null || !value.Antagonist) { ClearAllies(); - suspicionRoleSystem.RemoveTraitor(this); + sus.RemoveTraitor(this); } else if (value.Antagonist) { - SetAllies(suspicionRoleSystem.Traitors); - suspicionRoleSystem.AddTraitor(this); + SetAllies(sus.Traitors); + sus.AddTraitor(this); } } } diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index b7eef2588d..5fc5e65842 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -2,6 +2,7 @@ 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; @@ -11,6 +12,7 @@ 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 @@ -105,13 +107,15 @@ namespace Content.Server.Voting.Managers private void CreatePresetVote(IPlayerSession? initiator) { - var presets = new Dictionary + var presets = new Dictionary(); + + foreach (var preset in _prototypeManager.EnumeratePrototypes()) { - ["traitor"] = "mode-traitor", - ["extended"] = "mode-extended", - ["sandbox"] = "mode-sandbox", - ["suspicion"] = "mode-suspicion", - }; + if(!preset.ShowInVote) + continue; + + presets[preset.ID] = preset.ModeTitle; + } var alone = _playerManager.PlayerCount == 1 && initiator != null; var options = new VoteOptions @@ -150,7 +154,7 @@ namespace Content.Server.Voting.Managers Loc.GetString("ui-vote-gamemode-win", ("winner", Loc.GetString(presets[picked])))); } - EntitySystem.Get().SetStartPreset(picked); + EntitySystem.Get().SetGamePreset(picked); }; } diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs index 0e9920561d..813f0a4d68 100644 --- a/Content.Server/Voting/Managers/VoteManager.cs +++ b/Content.Server/Voting/Managers/VoteManager.cs @@ -36,6 +36,7 @@ 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 c5095bf568..3bc05bb0bc 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 new file mode 100644 index 0000000000..ee41fcefea --- /dev/null +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-deathmatch.ftl @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..56223aef40 --- /dev/null +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-extended.ftl @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..6e26073d34 --- /dev/null +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-sandbox.ftl @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..d70a8cdffb --- /dev/null +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-suspicion.ftl @@ -0,0 +1,2 @@ +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 dcb06af34a..f15743b696 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,6 +25,7 @@ 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. @@ -40,4 +41,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} \ No newline at end of file +traitor-role-codewords = Your codewords are: {$codewords} 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 51bed9f128..fc536cf44a 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-trators-victory = The traitors have won! -rule-suspicion-end-round-nobody-victory = Nobody wins! \ No newline at end of file +rule-suspicion-end-round-traitors-victory = The traitors have won! +rule-suspicion-end-round-nobody-victory = Nobody wins! 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 7c2623196f..0562dc3652 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}. \ No newline at end of file + [other] Your partners in crime are {$partnerNames}. diff --git a/Resources/Locale/en-US/voting/managers/vote-manager.ftl b/Resources/Locale/en-US/voting/managers/vote-manager.ftl index 2f349acfe3..d4534fbdc4 100644 --- a/Resources/Locale/en-US/voting/managers/vote-manager.ftl +++ b/Resources/Locale/en-US/voting/managers/vote-manager.ftl @@ -16,8 +16,3 @@ 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 269bdbfb09..61a9e44b68 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: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Pistol Spawner @@ -45,7 +45,7 @@ - PistolPaco chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Melee Spawner @@ -66,7 +66,7 @@ - Stunbaton chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Revolver Spawner @@ -84,7 +84,7 @@ - RevolverMateba chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Shotgun Spawner @@ -106,7 +106,7 @@ - ShotgunSawn chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion SMG Spawner @@ -126,7 +126,7 @@ - SmgZoric chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Sniper Spawner @@ -144,7 +144,7 @@ - SniperHeavy chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Hitscan Spawner @@ -166,7 +166,7 @@ - TaserGun chance: 0.85 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Launchers Spawner @@ -183,7 +183,7 @@ - LauncherRocket chance: 0.75 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Grenades Spawner @@ -206,7 +206,7 @@ - SoapSyndie # shhh! chance: 0.75 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Rifle Ammo Spawner @@ -227,7 +227,7 @@ - MagazinePistolCalicoTopMounted chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Shotgun Ammo Spawner @@ -243,7 +243,7 @@ - MagazineShotgun chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Pistol Ammo Spawner @@ -260,7 +260,7 @@ - MagazineHCPistol chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Magnum Ammo Spawner @@ -277,7 +277,7 @@ - MagazineMagnumSmg chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion - type: entity name: Suspicion Launcher Ammo Spawner @@ -294,4 +294,4 @@ - GrenadeFrag chance: 0.95 gameRules: - - RuleSuspicion + - Suspicion diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml b/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml index 331c5502a9..3c607f9034 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: - - RuleTraitorDeathMatch + - TraitorDeathMatch diff --git a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml index 1d2168232c..c70b160f2f 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: "traitor" + name: "suspect" antagonist: true setPreference: true objective: "Kill the innocents." diff --git a/Resources/Prototypes/Roles/Antags/traitor.yml b/Resources/Prototypes/Roles/Antags/traitor.yml new file mode 100644 index 0000000000..346b434347 --- /dev/null +++ b/Resources/Prototypes/Roles/Antags/traitor.yml @@ -0,0 +1,6 @@ +- 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 new file mode 100644 index 0000000000..d57ddf18c1 --- /dev/null +++ b/Resources/Prototypes/game_presets.yml @@ -0,0 +1,60 @@ +- 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 new file mode 100644 index 0000000000..8c9b78ac4f --- /dev/null +++ b/Resources/Prototypes/game_rules.yml @@ -0,0 +1,20 @@ +- 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