Skip to content

Commit

Permalink
Basic framework done
Browse files Browse the repository at this point in the history
  • Loading branch information
Mnemotechnician committed Dec 16, 2024
1 parent 55d7352 commit 9c5b9b3
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 0 deletions.
140 changes: 140 additions & 0 deletions Content.Server/FloofStation/GameTicking/StationEventCondition.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents an abstract condition required for a station event to be chosen from the random event pool.
/// </summary>
/// <remarks>
/// Implementations should avoid performing expensive checks.
/// Any data that may be expensive to compute should instead be precomputed and stored in <see cref="Dependencies"/>
/// </remarks>
[Serializable, ImplicitDataDefinitionForInheritors]
public abstract partial class StationEventCondition
{
/// <summary>
/// If true, the event will only be run if this condition is NOT met.
/// </summary>
[DataField]
public bool Inverted = false;

public abstract bool IsMet(EntityPrototype proto, StationEventComponent component, Dependencies dependencies);

/// <summary>
/// Entity system and other dependencies used by station event conditions.
/// GameTicker allocates an instance of this before passing it to all events.
/// </summary>
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!;

/// <summary>
/// List of all players along with their jobs. The dept field may have the default value if it could not be determined.
/// </summary>
public List<(ICommonSession session, EntityUid uid, ProtoId<JobPrototype> job, ProtoId<DepartmentPrototype> dept)> Players = new();
public Dictionary<ProtoId<JobPrototype>, int> JobCounts = new();
public Dictionary<ProtoId<DepartmentPrototype>, int> DeptCounts = new();

// Lookups
private readonly Dictionary<string, ProtoId<JobPrototype>> _jobTitleToPrototype = new();
private readonly Dictionary<ProtoId<JobPrototype>, ProtoId<DepartmentPrototype>> _jobToDept = new();

/// <summary>
/// Called once after the instantiation of the class.
/// </summary>
public void Initialize()
{
IoCManager.InjectDependencies(this);

// We cannot use entity system dependencies outside of ESC context.
IdCard = EntMan.System<SharedIdCardSystem>();
Minds = EntMan.System<MindSystem>();

// 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<JobPrototype>())
{
_jobTitleToPrototype[job.LocalizedName] = job.ID;

foreach (var dept in ProtoMan.EnumeratePrototypes<DepartmentPrototype>())
{
if (!dept.Primary || !dept.Roles.Contains(job.ID))
continue;

_jobToDept[job.ID] = dept.ID;
break;
}
}
}

/// <summary>
/// Called once shortly before passing this object to IsMet() to collect the necessary data about the round.
/// </summary>
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<JobPrototype> 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<JobComponent>(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
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A condition that requires a number of players to be present in a specific department.
/// </summary>
/// <example><code>
/// - !type:DepartmentCountCondition
/// department: Security
/// range: {min: 5}
/// </code></example>
[Serializable]
public sealed partial class DepartmentCountCondition : StationEventCondition
{
[DataField(required: true)]
public ProtoId<DepartmentPrototype> 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);
}
}

/// <summary>
/// Same as <see cref="DepartmentCountCondition"/>, but for specific jobs.
/// </summary>
[Serializable]
public sealed partial class JobCountCondition : StationEventCondition
{
[DataField(required: true)]
public ProtoId<JobPrototype> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Linq;
using Content.Server.StationEvents.Components;
using Robust.Shared.Prototypes;

namespace Content.Server.FloofStation.GameTicking;

/// <summary>
/// Combines a number of other conditions in a boolean AND or a boolean OR.
/// </summary>
/// <example>
/// <code>
/// - !type:ComplexCondition
/// requireAll: true
/// conditions:
/// - !type:SomeCondition1
/// ...
/// - !type:SomeCondition2
/// ...
/// </code>
/// </example>
[Serializable]
public sealed partial class ComplexCondition : StationEventCondition
{
/// <summary>
/// If true, this condition acts as a boolean AND. If false, it acts as a boolean OR.
/// </summary>
[DataField]
public bool RequireAll = false;

[DataField(required: true)]
public List<StationEventCondition> 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));
}
11 changes: 11 additions & 0 deletions Content.Server/StationEvents/Components/StationEventComponent.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Content.Server.FloofStation.GameTicking;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;

Expand Down Expand Up @@ -82,4 +83,14 @@ public sealed partial class StationEventComponent : Component
[DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan? EndTime;

// Floof section - custom conditions

/// <summary>
/// A list of conditions that must be met for the event to run.
/// </summary>
[DataField]
public List<StationEventCondition>? Conditions;

// Floof section end
}
15 changes: 15 additions & 0 deletions Content.Server/StationEvents/EventManagerSystem.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
}

/// <summary>
Expand Down Expand Up @@ -113,6 +119,8 @@ private Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(bool

var result = new Dictionary<EntityPrototype, StationEventComponent>();

_eventConditionDeps.Update(); // Floof

foreach (var (proto, stationEvent) in AllEvents())
{
if (CanRun(proto, stationEvent, playerCount, currentTime))
Expand Down Expand Up @@ -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;
}
}

0 comments on commit 9c5b9b3

Please sign in to comment.