Refactor Game Modes (#5819)
This commit is contained in:
committed by
GitHub
parent
8be5c25bd4
commit
8831d08229
@@ -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();
|
||||
|
||||
|
||||
@@ -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<IEntitySystemManager>().GetEntitySystem<GameTicker>();
|
||||
var maxTimeMaxTimeRestartRuleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MaxTimeRestartRuleSystem>();
|
||||
var sGameTiming = server.ResolveDependency<IGameTiming>();
|
||||
|
||||
RuleMaxTimeRestart maxTimeRule = null;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
|
||||
|
||||
maxTimeRule = sGameTicker.AddGameRule<RuleMaxTimeRestart>();
|
||||
maxTimeRule.RoundMaxTime = TimeSpan.FromSeconds(3);
|
||||
sGameTicker.AddGameRule(IoCManager.Resolve<IPrototypeManager>().Index<GameRulePrototype>(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(() =>
|
||||
|
||||
@@ -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}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} <preset>";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
Type? preset = null;
|
||||
GamePresetPrototype? preset = null;
|
||||
var presetName = string.Join(" ", args);
|
||||
|
||||
var ticker = EntitySystem.Get<GameTicker>();
|
||||
|
||||
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}")}");
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Content.Server.GameTicking.Commands
|
||||
|
||||
var ticker = EntitySystem.Get<GameTicker>();
|
||||
|
||||
ticker.SetStartPreset(args[0]);
|
||||
ticker.SetGamePreset(args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<NetUserId, HumanoidCharacterProfile>());
|
||||
set => _preset = value;
|
||||
}
|
||||
|
||||
public ImmutableDictionary<string, Type> Presets { get; private set; } = default!;
|
||||
|
||||
private GamePreset? _preset;
|
||||
private GamePresetPrototype? _preset;
|
||||
|
||||
private void InitializeGamePreset()
|
||||
{
|
||||
var presets = new Dictionary<string, Type>();
|
||||
|
||||
foreach (var type in _reflectionManager.FindTypesWithAttribute<GamePresetAttribute>())
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<GamePresetAttribute>();
|
||||
|
||||
presets.Add(attribute!.Id.ToLowerInvariant(), type);
|
||||
|
||||
foreach (var alias in attribute.Aliases)
|
||||
{
|
||||
presets.Add(alias.ToLowerInvariant(), type);
|
||||
}
|
||||
SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset));
|
||||
}
|
||||
|
||||
Presets = presets.ToImmutableDictionary();
|
||||
|
||||
SetStartPreset(_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}");
|
||||
var proto = FindGamePreset(preset);
|
||||
if(proto != null)
|
||||
SetGamePreset(proto, force);
|
||||
}
|
||||
|
||||
SetStartPreset(type, force);
|
||||
public GamePresetPrototype? FindGamePreset(string preset)
|
||||
{
|
||||
if (_prototypeManager.TryIndex(preset, out GamePresetPrototype? presetProto))
|
||||
return presetProto;
|
||||
|
||||
foreach (var proto in _prototypeManager.EnumeratePrototypes<GamePresetPrototype>())
|
||||
{
|
||||
foreach (var alias in proto.Alias)
|
||||
{
|
||||
if (preset.Equals(alias, StringComparison.InvariantCultureIgnoreCase))
|
||||
return proto;
|
||||
}
|
||||
}
|
||||
|
||||
private GamePreset MakeGamePreset(Dictionary<NetUserId, HumanoidCharacterProfile> readyProfiles)
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool TryFindGamePreset(string preset, [NotNullWhen(true)] out GamePresetPrototype? prototype)
|
||||
{
|
||||
var preset = _dynamicTypeFactory.CreateInstance<GamePreset>(_presetType ?? typeof(PresetSandbox));
|
||||
preset.ReadyProfiles = readyProfiles;
|
||||
return preset;
|
||||
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<IEntityManager>();
|
||||
if (entities.HasComponent<GhostComponent>(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<DamageTypePrototype>("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<GhostComponent>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GameRule> _gameRules = new();
|
||||
public IEnumerable<GameRule> ActiveGameRules => _gameRules;
|
||||
// No duplicates.
|
||||
[ViewVariables] private readonly HashSet<GameRulePrototype> _gameRules = new();
|
||||
public IEnumerable<GameRulePrototype> ActiveGameRules => _gameRules;
|
||||
|
||||
public T AddGameRule<T>() where T : GameRule, new()
|
||||
public bool AddGameRule(GameRulePrototype rule)
|
||||
{
|
||||
var instance = _dynamicTypeFactory.CreateInstance<T>();
|
||||
|
||||
_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)
|
||||
{
|
||||
RaiseLocalEvent(new GameRuleAddedEvent(rule));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (rule.GetType().IsAssignableFrom(type))
|
||||
return _gameRules.Contains(rule);
|
||||
}
|
||||
|
||||
public bool HasGameRule(string rule)
|
||||
{
|
||||
foreach (var ruleProto in _gameRules)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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<IPlayerSession> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised to refresh the late join status.
|
||||
/// If you want to disallow late joins, listen to this and call Disallow.
|
||||
/// </summary>
|
||||
public class RefreshLateJoinAllowedEvent
|
||||
{
|
||||
public bool DisallowLateJoin { get; private set; } = false;
|
||||
|
||||
public void Disallow()
|
||||
{
|
||||
DisallowLateJoin = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class RoundStartAttemptEvent : CancellableEntityEventArgs
|
||||
{
|
||||
public IPlayerSession[] Players { get; }
|
||||
public bool Forced { get; }
|
||||
|
||||
public RoundStartAttemptEvent(IPlayerSession[] players, bool forced)
|
||||
{
|
||||
Players = players;
|
||||
Forced = forced;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="GameTicker.PlayerJoinGame"/> on them.
|
||||
/// </summary>
|
||||
public class RulePlayerSpawningEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>If you spawn a player by yourself from this event, don't forget to call <see cref="GameTicker.PlayerJoinGame"/> on them.</remarks>
|
||||
public List<IPlayerSession> PlayerPool { get; }
|
||||
public IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> Profiles { get; }
|
||||
public bool Forced { get; }
|
||||
|
||||
public RulePlayerSpawningEvent(List<IPlayerSession> playerPool, IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles, bool forced)
|
||||
{
|
||||
PlayerPool = playerPool;
|
||||
Profiles = profiles;
|
||||
Forced = forced;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised after players were assigned jobs by the GameTicker.
|
||||
/// You can give on-station people special roles by listening to this event.
|
||||
/// </summary>
|
||||
public class RulePlayerJobsAssignedEvent
|
||||
{
|
||||
public IPlayerSession[] Players { get; }
|
||||
public IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> Profiles { get; }
|
||||
public bool Forced { get; }
|
||||
|
||||
public RulePlayerJobsAssignedEvent(IPlayerSession[] players, IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles, bool forced)
|
||||
{
|
||||
Players = players;
|
||||
Profiles = profiles;
|
||||
Forced = forced;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised to allow subscribers to add text to the round end summary screen.
|
||||
/// </summary>
|
||||
public class RoundEndTextAppendEvent
|
||||
{
|
||||
private bool _doNewLine;
|
||||
|
||||
/// <summary>
|
||||
/// Text to display in the round end summary screen.
|
||||
/// </summary>
|
||||
public string Text { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Invoke this method to add text to the round end summary screen.
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
public void AddLine(string text)
|
||||
{
|
||||
if (_doNewLine)
|
||||
Text += "\n";
|
||||
|
||||
Text += text;
|
||||
_doNewLine = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A round-start setup preset, such as which antagonists to spawn.
|
||||
/// </summary>
|
||||
public abstract class GamePreset
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
public abstract bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false);
|
||||
public virtual string ModeTitle => "Sandbox";
|
||||
public virtual string Description => "Secret!";
|
||||
public virtual bool DisallowLateJoin => false;
|
||||
public Dictionary<NetUserId, HumanoidCharacterProfile> ReadyProfiles = new();
|
||||
|
||||
public virtual void OnGameStarted() { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player is spawned in (this includes, but is not limited to, before Start)
|
||||
/// </summary>
|
||||
public virtual void OnSpawnPlayerCompleted(IPlayerSession session, EntityUid mob, bool lateJoin) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player attempts to ghost.
|
||||
/// </summary>
|
||||
public virtual bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal)
|
||||
{
|
||||
var playerEntity = mind.OwnedEntity;
|
||||
|
||||
var entities = IoCManager.Resolve<IEntityManager>();
|
||||
if (entities.HasComponent<GhostComponent>(playerEntity))
|
||||
return false;
|
||||
|
||||
if (mind.VisitingEntity != default)
|
||||
{
|
||||
mind.UnVisit();
|
||||
}
|
||||
|
||||
var position = playerEntity is {Valid: true}
|
||||
? _entities.GetComponent<TransformComponent>(playerEntity.Value).Coordinates
|
||||
: EntitySystem.Get<GameTicker>().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<IPrototypeManager>().Index<DamageTypePrototype>("Asphyxiation"), 200);
|
||||
EntitySystem.Get<DamageableSystem>().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<MetaDataComponent>(ghost).EntityName = mind.CharacterName;
|
||||
else if (!string.IsNullOrWhiteSpace(mind.Session?.Name))
|
||||
entities.GetComponent<MetaDataComponent>(ghost).EntityName = mind.Session.Name;
|
||||
|
||||
var ghostComponent = entities.GetComponent<GhostComponent>(ghost);
|
||||
|
||||
if (mind.TimeOfDeath.HasValue)
|
||||
{
|
||||
ghostComponent.TimeOfDeath = mind.TimeOfDeath!.Value;
|
||||
}
|
||||
|
||||
EntitySystem.Get<SharedGhostSystem>().SetCanReturnToBody(ghostComponent, canReturn);
|
||||
|
||||
if (canReturn)
|
||||
mind.Visit(ghost);
|
||||
else
|
||||
mind.TransferTo(ghost);
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual string GetRoundEndDescription() => string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute that marks a game preset.
|
||||
/// The id and aliases are registered in lowercase in <see cref="GameTicker"/>.
|
||||
/// A duplicate id or alias will throw an exception.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
[BaseTypeRequired(typeof(GamePreset))]
|
||||
[MeansImplicitUse]
|
||||
public class GamePresetAttribute : Attribute
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public ImmutableList<string> Aliases { get; }
|
||||
|
||||
public GamePresetAttribute(string id, params string[] aliases)
|
||||
{
|
||||
Id = id;
|
||||
Aliases = aliases.ToImmutableList();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Content.Server/GameTicking/Presets/GamePresetPrototype.cs
Normal file
34
Content.Server/GameTicking/Presets/GamePresetPrototype.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A round-start setup preset, such as which antagonists to spawn.
|
||||
/// </summary>
|
||||
[Prototype("gamePreset")]
|
||||
public class GamePresetPrototype : IPrototype
|
||||
{
|
||||
[DataField("id", required:true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField("alias")]
|
||||
public string[] Alias { get; } = Array.Empty<string>();
|
||||
|
||||
[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<GameRulePrototype>))]
|
||||
public IReadOnlyList<string> Rules { get; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -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<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
EntitySystem.Get<GameTicker>().AddGameRule<RuleDeathMatch>();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ModeTitle => "Deathmatch";
|
||||
public override string Description => "Kill anything that moves!";
|
||||
}
|
||||
}
|
||||
@@ -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<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
// We do nothing. This is extended after all...
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
_sandboxManager.IsSandboxEnabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ModeTitle => "Sandbox";
|
||||
public override string Description => "No stress, build something!";
|
||||
}
|
||||
}
|
||||
@@ -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<IPlayerSession> 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<IPlayerSession>(readyPlayers);
|
||||
var prefList = new List<IPlayerSession>();
|
||||
|
||||
foreach (var player in list)
|
||||
{
|
||||
if (!ReadyProfiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
prefList.Add(player);
|
||||
|
||||
attached.EnsureComponent<SuspicionRoleComponent>();
|
||||
}
|
||||
|
||||
var numTraitors = MathHelper.Clamp(readyPlayers.Count / PlayersPerTraitor,
|
||||
MinTraitors, readyPlayers.Count);
|
||||
|
||||
var traitors = new List<SuspicionTraitorRole>();
|
||||
|
||||
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<AntagPrototype>(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<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
// try to place uplink
|
||||
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var player in list)
|
||||
{
|
||||
var mind = player.Data.ContentData()?.Mind;
|
||||
var antagPrototype = _prototypeManager.Index<AntagPrototype>(InnocentID);
|
||||
|
||||
DebugTools.AssertNotNull(mind);
|
||||
|
||||
mind!.AddRole(new SuspicionInnocentRole(mind, antagPrototype));
|
||||
}
|
||||
|
||||
foreach (var traitor in traitors)
|
||||
{
|
||||
traitor.GreetSuspicion(traitors, _chatManager);
|
||||
}
|
||||
|
||||
EntitySystem.Get<GameTicker>().AddGameRule<RuleSuspicion>();
|
||||
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?";
|
||||
}
|
||||
}
|
||||
@@ -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<TraitorRole> _traitors = new ();
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> 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<IPlayerSession>(readyPlayers).Where(x =>
|
||||
x.Data.ContentData()?.Mind?.AllRoles.All(role => role is not Job {CanBeAntag: false}) ?? false
|
||||
).ToList();
|
||||
|
||||
var prefList = new List<IPlayerSession>();
|
||||
|
||||
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<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
|
||||
continue;
|
||||
|
||||
var traitorRole = new TraitorRole(mind);
|
||||
mind.AddRole(traitorRole);
|
||||
_traitors.Add(traitorRole);
|
||||
}
|
||||
|
||||
var adjectives = _prototypeManager.Index<DatasetPrototype>("adjectives").Values;
|
||||
var verbs = _prototypeManager.Index<DatasetPrototype>("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<GameTicker>().AddGameRule<RuleTraitor>();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void OnGameStarted()
|
||||
{
|
||||
var objectivesMgr = IoCManager.Resolve<IObjectivesManager>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<UplinkAccount, string> _allOriginalNames = new();
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
var gameTicker = EntitySystem.Get<GameTicker>();
|
||||
gameTicker.AddGameRule<RuleTraitorDeathMatch>();
|
||||
_restarter = gameTicker.AddGameRule<RuleMaxTimeRestart>();
|
||||
_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<TransformComponent>(owned).Coordinates);
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.IDCARD, _entityManager.GetComponent<ItemComponent>(newPDA));
|
||||
|
||||
// belt
|
||||
var newTmp = _entityManager.SpawnEntity(BeltPrototypeName, _entityManager.GetComponent<TransformComponent>(owned).Coordinates);
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.BELT, _entityManager.GetComponent<ItemComponent>(newTmp));
|
||||
|
||||
// backpack
|
||||
newTmp = _entityManager.SpawnEntity(BackpackPrototypeName, _entityManager.GetComponent<TransformComponent>(owned).Coordinates);
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.BACKPACK, _entityManager.GetComponent<ItemComponent>(newTmp));
|
||||
|
||||
// Like normal traitors, they need access to a traitor account.
|
||||
var uplinkAccount = new UplinkAccount(startingBalance, owned);
|
||||
var accounts = _entityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
_entityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(owned, uplinkAccount, newPDA);
|
||||
|
||||
_allOriginalNames[uplinkAccount] = _entityManager.GetComponent<MetaDataComponent>(owned).EntityName;
|
||||
|
||||
// The PDA needs to be marked with the correct owner.
|
||||
var pda = _entityManager.GetComponent<PDAComponent>(newPDA);
|
||||
_entityManager.EntitySysManager.GetEntitySystem<PDASystem>()
|
||||
.SetOwner(pda, _entityManager.GetComponent<MetaDataComponent>(owned).EntityName);
|
||||
_entityManager.AddComponent<TraitorDeathMatchReliableOwnerTagComponent>(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<TransformComponent>(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<EntityCoordinates>();
|
||||
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<TransformComponent>(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<SpawnPointComponent>().Select(x => x.Owner).ToList();
|
||||
_robustRandom.Shuffle(ents);
|
||||
var foundATarget = false;
|
||||
bestTarget = EntityCoordinates.Invalid;
|
||||
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
|
||||
foreach (var entity in ents)
|
||||
{
|
||||
if (!atmosphereSystem.IsTileMixtureProbablySafe(_entityManager.GetComponent<TransformComponent>(entity).Coordinates))
|
||||
continue;
|
||||
|
||||
var distanceFromNearest = float.PositiveInfinity;
|
||||
foreach (var existing in existingPlayerPoints)
|
||||
{
|
||||
if (_entityManager.GetComponent<TransformComponent>(entity).Coordinates.TryDistance(_entityManager, existing, out var dist))
|
||||
distanceFromNearest = Math.Min(distanceFromNearest, dist);
|
||||
}
|
||||
if (bestTargetDistanceFromNearest < distanceFromNearest)
|
||||
{
|
||||
bestTarget = _entityManager.GetComponent<TransformComponent>(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<DamageTypePrototype>("Asphyxiation"), 100);
|
||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(entity, damage, true);
|
||||
}
|
||||
else if (!mobState.IsDead())
|
||||
{
|
||||
if (_entityManager.HasComponent<HandsComponent>(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
var session = mind.Session;
|
||||
if (session == null)
|
||||
return false;
|
||||
EntitySystem.Get<GameTicker>().Respawn(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string GetRoundEndDescription()
|
||||
{
|
||||
var lines = new List<string>();
|
||||
lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line"));
|
||||
foreach (var uplink in _entityManager.EntityQuery<UplinkComponent>(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");
|
||||
}
|
||||
}
|
||||
127
Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
Normal file
127
Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Simple GameRule that will do a free-for-all death match.
|
||||
/// Kill everybody else to win.
|
||||
/// </summary>
|
||||
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<DamageChangedEvent>(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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Content.Server/GameTicking/Rules/GameRulePrototype.cs
Normal file
11
Content.Server/GameTicking/Rules/GameRulePrototype.cs
Normal file
@@ -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!;
|
||||
}
|
||||
61
Content.Server/GameTicking/Rules/GameRuleSystem.cs
Normal file
61
Content.Server/GameTicking/Rules/GameRuleSystem.cs
Normal file
@@ -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!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this GameRule is currently enabled or not.
|
||||
/// Be sure to check this before doing anything rule-specific.
|
||||
/// </summary>
|
||||
public bool Enabled { get; protected set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// When the GameRule prototype with this ID is added, this system will be enabled.
|
||||
/// When it gets removed, this system will be disabled.
|
||||
/// </summary>
|
||||
public abstract string Prototype { get; }
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GameRuleAddedEvent>(OnGameRuleAdded);
|
||||
SubscribeLocalEvent<GameRuleRemovedEvent>(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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the game rule has been added and this system has been enabled.
|
||||
/// </summary>
|
||||
public abstract void Added();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the game rule has been removed and this system has been disabled.
|
||||
/// </summary>
|
||||
public abstract void Removed();
|
||||
}
|
||||
@@ -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<GameRunLevelChangedEvent>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
Normal file
80
Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
Normal file
@@ -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<GameRunLevelChangedEvent>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple GameRule that will do a free-for-all death match.
|
||||
/// Kill everybody else to win.
|
||||
/// </summary>
|
||||
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<DamageChangedEvent>(EventSource.Local, this, OnHealthChanged);
|
||||
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
public override void Removed()
|
||||
{
|
||||
base.Removed();
|
||||
|
||||
_entityManager.EventBus.UnsubscribeEvent<DamageChangedEvent>(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<GameTicker>().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GameRunLevelChangedEvent>(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>();
|
||||
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<GameTicker>().RunLevel != GameRunLevel.InRound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playerManager.PlayerCount == 0)
|
||||
{
|
||||
RestartTimer();
|
||||
}
|
||||
else
|
||||
{
|
||||
StopTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GameRunLevelChangedEvent>(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<GameTicker>().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<GameTicker>().RestartRound());
|
||||
}
|
||||
|
||||
private void RunLevelChanged(GameRunLevelChangedEvent args)
|
||||
{
|
||||
switch (args.New)
|
||||
{
|
||||
case GameRunLevel.InRound:
|
||||
RestartTimer();
|
||||
break;
|
||||
case GameRunLevel.PreRoundLobby:
|
||||
case GameRunLevel.PostRound:
|
||||
StopTimer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple GameRule that will do a TTT-like gamemode with traitors.
|
||||
/// </summary>
|
||||
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<SuspicionTraitorRole>() ?? false);
|
||||
|
||||
SoundSystem.Play(filter, _addedSound.GetSound(), AudioParams.Default);
|
||||
EntitySystem.Get<SuspicionEndTimerSystem>().EndTime = _endTime;
|
||||
|
||||
EntitySystem.Get<DoorSystem>().AccessType = DoorSystem.AccessTypes.AllowAllNoExternal;
|
||||
|
||||
Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token);
|
||||
}
|
||||
|
||||
public override void Removed()
|
||||
{
|
||||
base.Removed();
|
||||
|
||||
EntitySystem.Get<DoorSystem>().AccessType = DoorSystem.AccessTypes.Id;
|
||||
EntitySystem.Get<SuspicionEndTimerSystem>().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<SuspicionRoleComponent>(playerEntity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mobState.IsAlive())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mind = playerSession.ContentData()?.Mind;
|
||||
|
||||
if (mind != null && mind.HasRole<SuspicionTraitorRole>())
|
||||
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>();
|
||||
gameTicker.EndRound(text);
|
||||
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", (int) RoundEndDelay.TotalSeconds)));
|
||||
_checkTimerCancel.Cancel();
|
||||
|
||||
Timer.Spawn(RoundEndDelay, () => gameTicker.RestartRound());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<TraitorRole>() ?? false);
|
||||
|
||||
SoundSystem.Play(filter, _addedSound.GetSound(), AudioParams.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
21
Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
Normal file
21
Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
413
Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
Normal file
413
Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Simple GameRule that will do a TTT-like gamemode with traitors.
|
||||
/// </summary>
|
||||
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<SuspicionRoleComponent> _traitors = new();
|
||||
|
||||
public IReadOnlyCollection<SuspicionRoleComponent> 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<RulePlayerJobsAssignedEvent>(OnPlayersAssigned);
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnRoundStartAttempt);
|
||||
SubscribeLocalEvent<RefreshLateJoinAllowedEvent>(OnLateJoinRefresh);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, RoleAddedEvent>(OnRoleAdded);
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, RoleRemovedEvent>(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<IPlayerSession>(ev.Players);
|
||||
var prefList = new List<IPlayerSession>();
|
||||
|
||||
foreach (var player in list)
|
||||
{
|
||||
if (!ev.Profiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
prefList.Add(player);
|
||||
|
||||
attached.EnsureComponent<SuspicionRoleComponent>();
|
||||
}
|
||||
|
||||
var numTraitors = MathHelper.Clamp(ev.Players.Length / playersPerTraitor,
|
||||
minTraitors, ev.Players.Length);
|
||||
|
||||
var traitors = new List<SuspicionTraitorRole>();
|
||||
|
||||
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<AntagPrototype>(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<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
// try to place uplink
|
||||
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var player in list)
|
||||
{
|
||||
var mind = player.Data.ContentData()?.Mind;
|
||||
var antagPrototype = _prototypeManager.Index<AntagPrototype>(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<SuspicionTraitorRole>() ?? 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<SuspicionRoleComponent>(playerEntity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mobState.IsAlive())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mind = playerSession.ContentData()?.Mind;
|
||||
|
||||
if (mind != null && mind.HasRole<SuspicionTraitorRole>())
|
||||
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();
|
||||
}
|
||||
}
|
||||
270
Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
Normal file
270
Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
Normal file
@@ -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<UplinkAccount, string> _allOriginalNames = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawned);
|
||||
SubscribeLocalEvent<GhostAttemptHandleEvent>(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<ItemComponent>(newPDA));
|
||||
|
||||
// belt
|
||||
var newTmp = Spawn(BeltPrototypeName, ownedCoords);
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.BELT, Comp<ItemComponent>(newTmp));
|
||||
|
||||
// backpack
|
||||
newTmp = Spawn(BackpackPrototypeName, ownedCoords);
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.BACKPACK, Comp<ItemComponent>(newTmp));
|
||||
|
||||
// Like normal traitors, they need access to a traitor account.
|
||||
var uplinkAccount = new UplinkAccount(startingBalance, owned);
|
||||
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(owned, uplinkAccount, newPDA);
|
||||
|
||||
_allOriginalNames[uplinkAccount] = Name(owned);
|
||||
|
||||
// The PDA needs to be marked with the correct owner.
|
||||
var pda = Comp<PDAComponent>(newPDA);
|
||||
EntityManager.EntitySysManager.GetEntitySystem<PDASystem>().SetOwner(pda, Name(owned));
|
||||
EntityManager.AddComponent<TraitorDeathMatchReliableOwnerTagComponent>(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<DamageTypePrototype>("Asphyxiation"), 100);
|
||||
Get<DamageableSystem>().TryChangeDamage(entity, damage, true);
|
||||
}
|
||||
else if (!mobState.IsDead())
|
||||
{
|
||||
if (HasComp<HandsComponent>(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<string>();
|
||||
lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line"));
|
||||
foreach (var uplink in EntityManager.EntityQuery<UplinkComponent>(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<EntityCoordinates>();
|
||||
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<SpawnPointComponent>().Select(x => x.Owner).ToList();
|
||||
_robustRandom.Shuffle(ents);
|
||||
var foundATarget = false;
|
||||
bestTarget = EntityCoordinates.Invalid;
|
||||
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
262
Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Normal file
262
Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Normal file
@@ -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<TraitorRole> _traitors = new ();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(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<IPlayerSession>(ev.Players).Where(x =>
|
||||
x.Data.ContentData()?.Mind?.AllRoles.All(role => role is not Job {CanBeAntag: false}) ?? false
|
||||
).ToList();
|
||||
|
||||
var prefList = new List<IPlayerSession>();
|
||||
|
||||
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<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
|
||||
continue;
|
||||
|
||||
var traitorRole = new TraitorRole(mind);
|
||||
mind.AddRole(traitorRole);
|
||||
_traitors.Add(traitorRole);
|
||||
}
|
||||
|
||||
var adjectives = _prototypeManager.Index<DatasetPrototype>("adjectives").Values;
|
||||
var verbs = _prototypeManager.Index<DatasetPrototype>("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);
|
||||
}
|
||||
}
|
||||
@@ -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<IRobustRandom>().Pick(allHumans)};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EntityPrototype>))]
|
||||
public List<string> Prototypes { get; set; } = new();
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("gameRules")]
|
||||
[DataField("gameRules", customTypeSerializer:typeof(PrototypeIdListSerializer<GameRulePrototype>))]
|
||||
private readonly List<string> _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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RoundRestartCleanupEvent>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SuspicionRoleComponent> _traitors = new();
|
||||
|
||||
public IReadOnlyCollection<SuspicionRoleComponent> Traitors => _traitors;
|
||||
|
||||
#region Overrides of EntitySystem
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, RoleAddedEvent>(OnRoleAdded);
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, RoleRemovedEvent>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SuspicionRoleSystem>();
|
||||
var sus = EntitySystem.Get<SuspicionRuleSystem>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, string>
|
||||
var presets = new Dictionary<string, string>();
|
||||
|
||||
foreach (var preset in _prototypeManager.EnumeratePrototypes<GamePresetPrototype>())
|
||||
{
|
||||
["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<GameTicker>().SetStartPreset(picked);
|
||||
EntitySystem.Get<GameTicker>().SetGamePreset(picked);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace Content.Shared.CCVar
|
||||
/// Controls the default game preset.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string>
|
||||
GameLobbyDefaultPreset = CVarDef.Create("game.defaultpreset", "Suspicion", CVar.ARCHIVE);
|
||||
GameLobbyDefaultPreset = CVarDef.Create("game.defaultpreset", "suspicion", CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string>
|
||||
GameLobbyFallbackPreset = CVarDef.Create("game.fallbackpreset", "Sandbox", CVar.ARCHIVE);
|
||||
GameLobbyFallbackPreset = CVarDef.Create("game.fallbackpreset", "sandbox", CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Controls if people can win the game in Suspicion or Deathmatch.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
death-match-title = DeathMatch
|
||||
death-match-description = Kill anything that moves!
|
||||
@@ -0,0 +1,2 @@
|
||||
extended-title = Extended
|
||||
extended-description = No antagonists, have fun!
|
||||
@@ -0,0 +1,2 @@
|
||||
sandbox-title = Sandbox
|
||||
sandbox-description = No stress, build something!
|
||||
@@ -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?
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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-traitors-victory = The traitors have won!
|
||||
rule-suspicion-end-round-nobody-victory = Nobody wins!
|
||||
@@ -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}.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
- TraitorDMRedemptionMachine
|
||||
chance: 1.0
|
||||
gameRules:
|
||||
- RuleTraitorDeathMatch
|
||||
- TraitorDeathMatch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- type: antag
|
||||
id: SuspicionTraitor
|
||||
name: "traitor"
|
||||
name: "suspect"
|
||||
antagonist: true
|
||||
setPreference: true
|
||||
objective: "Kill the innocents."
|
||||
|
||||
6
Resources/Prototypes/Roles/Antags/traitor.yml
Normal file
6
Resources/Prototypes/Roles/Antags/traitor.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
- type: antag
|
||||
id: Traitor
|
||||
name: "traitor"
|
||||
antagonist: true
|
||||
setPreference: true
|
||||
objective: "Complete your objectives without being caught."
|
||||
60
Resources/Prototypes/game_presets.yml
Normal file
60
Resources/Prototypes/game_presets.yml
Normal file
@@ -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
|
||||
20
Resources/Prototypes/game_rules.yml
Normal file
20
Resources/Prototypes/game_rules.yml
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user