From 9c5b9b3ad20755b8350007e1ef5d071d8f68b94f Mon Sep 17 00:00:00 2001 From: fox Date: Mon, 16 Dec 2024 22:34:05 +0300 Subject: [PATCH] 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; } }