From 9c5b9b3ad20755b8350007e1ef5d071d8f68b94f Mon Sep 17 00:00:00 2001 From: fox Date: Mon, 16 Dec 2024 22:34:05 +0300 Subject: [PATCH 1/4] Basic framework done --- .../GameTicking/StationEventCondition.cs | 140 ++++++++++++++++++ .../StationEventConditions.Players.cs | 49 ++++++ .../StationEventConditions.Utility.cs | 37 +++++ .../Components/StationEventComponent.cs | 11 ++ .../StationEvents/EventManagerSystem.cs | 15 ++ 5 files changed, 252 insertions(+) create mode 100644 Content.Server/FloofStation/GameTicking/StationEventCondition.cs create mode 100644 Content.Server/FloofStation/GameTicking/StationEventConditions.Players.cs create mode 100644 Content.Server/FloofStation/GameTicking/StationEventConditions.Utility.cs diff --git a/Content.Server/FloofStation/GameTicking/StationEventCondition.cs b/Content.Server/FloofStation/GameTicking/StationEventCondition.cs new file mode 100644 index 00000000000..549ada404b9 --- /dev/null +++ b/Content.Server/FloofStation/GameTicking/StationEventCondition.cs @@ -0,0 +1,140 @@ +using Content.Server.GameTicking; +using Content.Server.Mind; +using Content.Server.StationEvents; +using Content.Server.StationEvents.Components; +using Content.Shared.Access.Systems; +using Content.Shared.Roles; +using Content.Shared.Roles.Jobs; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Serilog; + + +namespace Content.Server.FloofStation.GameTicking; + +/// +/// Represents an abstract condition required for a station event to be chosen from the random event pool. +/// +/// +/// Implementations should avoid performing expensive checks. +/// Any data that may be expensive to compute should instead be precomputed and stored in +/// +[Serializable, ImplicitDataDefinitionForInheritors] +public abstract partial class StationEventCondition +{ + /// + /// If true, the event will only be run if this condition is NOT met. + /// + [DataField] + public bool Inverted = false; + + public abstract bool IsMet(EntityPrototype proto, StationEventComponent component, Dependencies dependencies); + + /// + /// Entity system and other dependencies used by station event conditions. + /// GameTicker allocates an instance of this before passing it to all events. + /// + public sealed class Dependencies(IEntityManager entMan, GameTicker ticker, EventManagerSystem eventManager) + { + public ISawmill Log = Logger.GetSawmill("station-event-conditions"); + + public IEntityManager EntMan => entMan; + public GameTicker Ticker => ticker; + public EventManagerSystem EventManager => eventManager; + + public MindSystem Minds = default!; + public SharedIdCardSystem IdCard = default!; + + [Dependency] public IPrototypeManager ProtoMan = default!; + [Dependency] public IRobustRandom Random = default!; + [Dependency] public IPlayerManager PlayerManager = default!; + + /// + /// List of all players along with their jobs. The dept field may have the default value if it could not be determined. + /// + public List<(ICommonSession session, EntityUid uid, ProtoId job, ProtoId dept)> Players = new(); + public Dictionary, int> JobCounts = new(); + public Dictionary, int> DeptCounts = new(); + + // Lookups + private readonly Dictionary> _jobTitleToPrototype = new(); + private readonly Dictionary, ProtoId> _jobToDept = new(); + + /// + /// Called once after the instantiation of the class. + /// + public void Initialize() + { + IoCManager.InjectDependencies(this); + + // We cannot use entity system dependencies outside of ESC context. + IdCard = EntMan.System(); + Minds = EntMan.System(); + + // Build the lookups - SharedJobSystem contains methods that iterate over all of those lists each time, + // Resulting in an O(n^2 * m) performance cost for each update() call. + foreach (var job in ProtoMan.EnumeratePrototypes()) + { + _jobTitleToPrototype[job.LocalizedName] = job.ID; + + foreach (var dept in ProtoMan.EnumeratePrototypes()) + { + if (!dept.Primary || !dept.Roles.Contains(job.ID)) + continue; + + _jobToDept[job.ID] = dept.ID; + break; + } + } + } + + /// + /// Called once shortly before passing this object to IsMet() to collect the necessary data about the round. + /// + public void Update() + { + JobCounts.Clear(); + DeptCounts.Clear(); + + // Collect data about the jobs of the players in the round + Players.Clear(); + foreach (var session in PlayerManager.Sessions) + { + if (session.AttachedEntity is not {} player + || session.Status is SessionStatus.Zombie or SessionStatus.Disconnected + || !Minds.TryGetMind(session, out var mind, out var mindComponent)) + continue; + + ProtoId job = default; + // 1: Try to get the job from the ID the person holds + if (IdCard.TryFindIdCard(player, out var idCard) && idCard.Comp.JobTitle is {} jobTitle) + _jobTitleToPrototype.TryGetValue(jobTitle, out job); + + // 2: If failed, try to fetch it from the mind component instead + if (job == default + && EntMan.TryGetComponent(mind, out var jobComp) + && jobComp.Prototype is {} mindJobProto + ) + job = mindJobProto; + + // If both have failed, skip the player + if (job == default) + continue; + + var dept = _jobToDept.GetValueOrDefault(job); + + Players.Add((session, player, job, dept)); + JobCounts[job] = JobCounts.GetValueOrDefault(job, 0) + 1; + DeptCounts[dept] = DeptCounts.GetValueOrDefault(dept, 0) + 1; + } + + #if DEBUG + Log.Debug($"Event conditions data: Job counts: {string.Join(", ", JobCounts)}"); + Log.Debug($"Dept counts: {string.Join(", ", DeptCounts)}"); + #endif + } + } +} diff --git a/Content.Server/FloofStation/GameTicking/StationEventConditions.Players.cs b/Content.Server/FloofStation/GameTicking/StationEventConditions.Players.cs new file mode 100644 index 00000000000..ef9fdb9504b --- /dev/null +++ b/Content.Server/FloofStation/GameTicking/StationEventConditions.Players.cs @@ -0,0 +1,49 @@ +using Content.Server.StationEvents.Components; +using Content.Shared.InteractionVerbs; +using Content.Shared.Roles; +using Robust.Shared.Prototypes; + +namespace Content.Server.FloofStation.GameTicking; + +/// +/// A condition that requires a number of players to be present in a specific department. +/// +/// +/// - !type:DepartmentCountCondition +/// department: Security +/// range: {min: 5} +/// +[Serializable] +public sealed partial class DepartmentCountCondition : StationEventCondition +{ + [DataField(required: true)] + public ProtoId Department; + + [DataField(required: true)] + public InteractionVerbPrototype.RangeSpecifier Range; + + public override bool IsMet(EntityPrototype proto, StationEventComponent component, Dependencies dependencies) + { + var count = dependencies.DeptCounts.GetValueOrDefault(Department, 0); + return Range.IsInRange(count); + } +} + +/// +/// Same as , but for specific jobs. +/// +[Serializable] +public sealed partial class JobCountCondition : StationEventCondition +{ + [DataField(required: true)] + public ProtoId Job; + + [DataField(required: true)] + public InteractionVerbPrototype.RangeSpecifier Range; + + public override bool IsMet(EntityPrototype proto, StationEventComponent component, Dependencies dependencies) + { + var count = dependencies.JobCounts.GetValueOrDefault(Job, 0); + return Range.IsInRange(count); + } +} diff --git a/Content.Server/FloofStation/GameTicking/StationEventConditions.Utility.cs b/Content.Server/FloofStation/GameTicking/StationEventConditions.Utility.cs new file mode 100644 index 00000000000..ab2b7bc7570 --- /dev/null +++ b/Content.Server/FloofStation/GameTicking/StationEventConditions.Utility.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Content.Server.StationEvents.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.FloofStation.GameTicking; + +/// +/// Combines a number of other conditions in a boolean AND or a boolean OR. +/// +/// +/// +/// - !type:ComplexCondition +/// requireAll: true +/// conditions: +/// - !type:SomeCondition1 +/// ... +/// - !type:SomeCondition2 +/// ... +/// +/// +[Serializable] +public sealed partial class ComplexCondition : StationEventCondition +{ + /// + /// If true, this condition acts as a boolean AND. If false, it acts as a boolean OR. + /// + [DataField] + public bool RequireAll = false; + + [DataField(required: true)] + public List Conditions = new(); + + public override bool IsMet(EntityPrototype proto, StationEventComponent component, Dependencies dependencies) => + RequireAll + ? Conditions.All(it => it.Inverted ^ it.IsMet(proto, component, dependencies)) + : Conditions.Any(it => it.Inverted ^ it.IsMet(proto, component, dependencies)); +} diff --git a/Content.Server/StationEvents/Components/StationEventComponent.cs b/Content.Server/StationEvents/Components/StationEventComponent.cs index 54af1a59d99..6edf3d8ac5a 100644 --- a/Content.Server/StationEvents/Components/StationEventComponent.cs +++ b/Content.Server/StationEvents/Components/StationEventComponent.cs @@ -1,3 +1,4 @@ +using Content.Server.FloofStation.GameTicking; using Robust.Shared.Audio; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -82,4 +83,14 @@ public sealed partial class StationEventComponent : Component [DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] public TimeSpan? EndTime; + + // Floof section - custom conditions + + /// + /// A list of conditions that must be met for the event to run. + /// + [DataField] + public List? Conditions; + + // Floof section end } diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index 9c0005d06db..b219ce20c77 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Chat.Managers; +using Content.Server.FloofStation.GameTicking; using Content.Server.GameTicking; using Content.Server.StationEvents.Components; using Content.Shared.CCVar; @@ -24,11 +25,16 @@ public sealed class EventManagerSystem : EntitySystem public bool EventsEnabled { get; private set; } private void SetEnabled(bool value) => EventsEnabled = value; + private StationEventCondition.Dependencies _eventConditionDeps = default!; // Floof + public override void Initialize() { base.Initialize(); Subs.CVar(_configurationManager, CCVars.EventsEnabled, SetEnabled, true); + + _eventConditionDeps = new(EntityManager, GameTicker, this); + _eventConditionDeps.Initialize(); } /// @@ -113,6 +119,8 @@ private Dictionary AvailableEvents(bool var result = new Dictionary(); + _eventConditionDeps.Update(); // Floof + foreach (var (proto, stationEvent) in AllEvents()) { if (CanRun(proto, stationEvent, playerCount, currentTime)) @@ -201,6 +209,13 @@ private bool CanRun(EntityPrototype prototype, StationEventComponent stationEven } // Nyano - End modified code block. + // Floof section - custom conditions + if (stationEvent.Conditions is { } conditions + && conditions.Any(it => it.Inverted ^ !it.IsMet(prototype, stationEvent, _eventConditionDeps)) + ) + return false; + // Floof section end + return true; } } From 00114655063a95a1714011915287da642ee0650a Mon Sep 17 00:00:00 2001 From: fox Date: Mon, 16 Dec 2024 23:33:15 +0300 Subject: [PATCH 2/4] Fixes --- .../GameTicking/StationEventCondition.cs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/Content.Server/FloofStation/GameTicking/StationEventCondition.cs b/Content.Server/FloofStation/GameTicking/StationEventCondition.cs index 549ada404b9..42418c42cc2 100644 --- a/Content.Server/FloofStation/GameTicking/StationEventCondition.cs +++ b/Content.Server/FloofStation/GameTicking/StationEventCondition.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Server.GameTicking; using Content.Server.Mind; using Content.Server.StationEvents; @@ -10,7 +11,6 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Serilog; namespace Content.Server.FloofStation.GameTicking; @@ -53,15 +53,15 @@ public sealed class Dependencies(IEntityManager entMan, GameTicker ticker, Event [Dependency] public IPlayerManager PlayerManager = default!; /// - /// List of all players along with their jobs. The dept field may have the default value if it could not be determined. + /// The list of all players along with their jobs. /// - public List<(ICommonSession session, EntityUid uid, ProtoId job, ProtoId dept)> Players = new(); + public List<(ICommonSession session, EntityUid uid, ProtoId job)> Players = new(); public Dictionary, int> JobCounts = new(); public Dictionary, int> DeptCounts = new(); // Lookups private readonly Dictionary> _jobTitleToPrototype = new(); - private readonly Dictionary, ProtoId> _jobToDept = new(); + private readonly Dictionary, List>> _jobToDepts = new(); /// /// Called once after the instantiation of the class. @@ -74,20 +74,18 @@ public void Initialize() IdCard = EntMan.System(); Minds = EntMan.System(); - // Build the lookups - SharedJobSystem contains methods that iterate over all of those lists each time, + // Build the inverse lookups - SharedJobSystem contains methods that iterate over all of those lists each time, // Resulting in an O(n^2 * m) performance cost for each update() call. foreach (var job in ProtoMan.EnumeratePrototypes()) { _jobTitleToPrototype[job.LocalizedName] = job.ID; - foreach (var dept in ProtoMan.EnumeratePrototypes()) - { - if (!dept.Primary || !dept.Roles.Contains(job.ID)) - continue; + var depts = ProtoMan.EnumeratePrototypes() + .Where(it => it.Roles.Contains(job.ID)) + .Select(it => new ProtoId(it.ID)) + .ToList(); - _jobToDept[job.ID] = dept.ID; - break; - } + _jobToDepts[job.ID] = depts; } } @@ -124,11 +122,15 @@ public void Update() if (job == default) continue; - var dept = _jobToDept.GetValueOrDefault(job); - - Players.Add((session, player, job, dept)); + // Update the info + Players.Add((session, player, job)); JobCounts[job] = JobCounts.GetValueOrDefault(job, 0) + 1; - DeptCounts[dept] = DeptCounts.GetValueOrDefault(dept, 0) + 1; + // Increment the number of players in each dept this job belongs to + if (_jobToDepts.TryGetValue(job, out var depts)) + { + foreach (var dept in depts) + DeptCounts[dept] = DeptCounts.GetValueOrDefault(dept, 0) + 1; + } } #if DEBUG From b17765a909d068aa09d65776ad7f23607a2a15cb Mon Sep 17 00:00:00 2001 From: fox Date: Mon, 16 Dec 2024 23:33:25 +0300 Subject: [PATCH 3/4] Add conditions to some events --- Resources/Prototypes/Floof/Gamerules/base.yml | 11 +++++++++++ Resources/Prototypes/GameRules/events.yml | 14 +++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 Resources/Prototypes/Floof/Gamerules/base.yml diff --git a/Resources/Prototypes/Floof/Gamerules/base.yml b/Resources/Prototypes/Floof/Gamerules/base.yml new file mode 100644 index 00000000000..8bdbfa44ac7 --- /dev/null +++ b/Resources/Prototypes/Floof/Gamerules/base.yml @@ -0,0 +1,11 @@ +# A game rule base that requires at least 2 security members to be present +- type: entity + id: BaseGameRuleSecurityRequirement + parent: BaseGameRule + abstract: true + components: + - type: StationEvent + conditions: + - !type:DepartmentCountCondition + department: Security + range: {min: 2} diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 35c2fb2805d..7a742bf2de1 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -7,6 +7,10 @@ weight: 8 startDelay: 30 duration: 35 + conditions: # Floof - at least one epi member + - !type:DepartmentCountCondition + department: Epistemics + range: {min: 1} - type: AnomalySpawnRule - type: entity @@ -80,7 +84,7 @@ prototype: MobSkeletonCloset - type: entity - parent: BaseGameRule + parent: BaseGameRuleSecurityRequirement # Floof - changed parent id: DragonSpawn noSpawn: true components: @@ -94,7 +98,7 @@ prototype: SpawnPointGhostDragon - type: entity - parent: BaseGameRule + parent: BaseGameRuleSecurityRequirement # Floof - changed parent id: NinjaSpawn noSpawn: true components: @@ -168,6 +172,10 @@ endAnnouncement: true duration: null #ending is handled by MeteorSwarmRule startDelay: 30 + conditions: # Floof + - !type:DepartmentCountCondition + department: Engineering + range: {min: 2} - type: MeteorSwarmRule - type: entity @@ -371,7 +379,7 @@ - type: entity id: LoneOpsSpawn - parent: BaseGameRule + parent: BaseGameRuleSecurityRequirement # Floof - changed parent noSpawn: true components: - type: StationEvent From 9afb8425ba37bbdda853d0368fc6b09d35fc7567 Mon Sep 17 00:00:00 2001 From: fox Date: Mon, 16 Dec 2024 23:41:12 +0300 Subject: [PATCH 4/4] Minor formatting --- .../FloofStation/GameTicking/StationEventCondition.cs | 1 - Content.Server/StationEvents/EventManagerSystem.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Content.Server/FloofStation/GameTicking/StationEventCondition.cs b/Content.Server/FloofStation/GameTicking/StationEventCondition.cs index 42418c42cc2..060357ef662 100644 --- a/Content.Server/FloofStation/GameTicking/StationEventCondition.cs +++ b/Content.Server/FloofStation/GameTicking/StationEventCondition.cs @@ -12,7 +12,6 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; - namespace Content.Server.FloofStation.GameTicking; /// diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index b219ce20c77..ce62e35213a 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -33,7 +33,7 @@ public override void Initialize() Subs.CVar(_configurationManager, CCVars.EventsEnabled, SetEnabled, true); - _eventConditionDeps = new(EntityManager, GameTicker, this); + _eventConditionDeps = new(EntityManager, GameTicker, this); // Floof _eventConditionDeps.Initialize(); }