Skip to content

Commit

Permalink
Merge pull request #418 from Mnemotechnician/floof/feat/event-conditions
Browse files Browse the repository at this point in the history
Event Conditions
  • Loading branch information
FoxxoTrystan authored Dec 21, 2024
2 parents dedcdf6 + 9afb842 commit 45849fb
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 3 deletions.
141 changes: 141 additions & 0 deletions Content.Server/FloofStation/GameTicking/StationEventCondition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System.Linq;
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;

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>
/// The list of all players along with their jobs.
/// </summary>
public List<(ICommonSession session, EntityUid uid, ProtoId<JobPrototype> job)> 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>, List<ProtoId<DepartmentPrototype>>> _jobToDepts = 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 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<JobPrototype>())
{
_jobTitleToPrototype[job.LocalizedName] = job.ID;

var depts = ProtoMan.EnumeratePrototypes<DepartmentPrototype>()
.Where(it => it.Roles.Contains(job.ID))
.Select(it => new ProtoId<DepartmentPrototype>(it.ID))
.ToList();

_jobToDepts[job.ID] = depts;
}
}

/// <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;

// Update the info
Players.Add((session, player, job));
JobCounts[job] = JobCounts.GetValueOrDefault(job, 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
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); // Floof
_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;
}
}
11 changes: 11 additions & 0 deletions Resources/Prototypes/Floof/Gamerules/base.yml
Original file line number Diff line number Diff line change
@@ -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}
14 changes: 11 additions & 3 deletions Resources/Prototypes/GameRules/events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,7 +84,7 @@
prototype: MobSkeletonCloset

- type: entity
parent: BaseGameRule
parent: BaseGameRuleSecurityRequirement # Floof - changed parent
id: DragonSpawn
noSpawn: true
components:
Expand All @@ -94,7 +98,7 @@
prototype: SpawnPointGhostDragon

- type: entity
parent: BaseGameRule
parent: BaseGameRuleSecurityRequirement # Floof - changed parent
id: NinjaSpawn
noSpawn: true
components:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -371,7 +379,7 @@

- type: entity
id: LoneOpsSpawn
parent: BaseGameRule
parent: BaseGameRuleSecurityRequirement # Floof - changed parent
noSpawn: true
components:
- type: StationEvent
Expand Down

0 comments on commit 45849fb

Please sign in to comment.