Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event Conditions #418

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
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
Loading