Skip to content

Commit

Permalink
refactor event schedulers to use explicit game rules (space-wizards#2…
Browse files Browse the repository at this point in the history
…9320)

* works, still has testing values, im sure I did stupid shit.

* shitvent crapfactor

* snap extra word out of existence

* shit I died of old

* remove useless inaccurate design comments

* Oopsie, handle requirement params in RandomRuleSystem too

* I'm a slash slinging hasher

* Address reviews, add admin alerts I forgor

* EntityMan saves the day

* address reviews 1

* eh, I actually don't care about the cargo gifts thing.

* started

* Do reviews

* you actually meant 1.2 lmao

* dependency inheritance is a fickle bitch

* I have no idea.

* Threads are for sheets not computers.

* fix traitor rule test

* fix round type tattling

* break things

* It worky

* Toolshed makes we want to drink depresso.

* Finished?

* remove debug values

* timings

* use defaults

* alphabetize

* bobby drop tables

* Float required fr fr

* continue

* more continence

* uno mas

* obsolution

* cleanup and documentations

* Yell at self

* use the right value defaults

* housekeeping
  • Loading branch information
IProduceWidgets authored and V committed Aug 14, 2024
1 parent b39eb5b commit b4f0659
Show file tree
Hide file tree
Showing 21 changed files with 600 additions and 241 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using Content.Server.GameTicking;
using Content.Server.GameTicking.Commands;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.CCVar;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;

Expand All @@ -27,8 +24,12 @@ public async Task RestartTest()
var sGameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
var sGameTiming = server.ResolveDependency<IGameTiming>();

sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
Assert.That(entityManager.TryGetComponent<MaxTimeRestartRuleComponent>(ruleEntity, out var maxTime));
MaxTimeRestartRuleComponent maxTime = null;
await server.WaitPost(() =>
{
sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
Assert.That(entityManager.TryGetComponent<MaxTimeRestartRuleComponent>(ruleEntity, out maxTime));
});

Assert.That(server.EntMan.Count<GameRuleComponent>(), Is.EqualTo(1));
Assert.That(server.EntMan.Count<ActiveGameRuleComponent>(), Is.EqualTo(1));
Expand Down
17 changes: 9 additions & 8 deletions Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,17 @@ await server.WaitAssertion(() =>
await pair.SetAntagPreference(TraitorAntagRoleName, true);

// Add the game rule
var gameRuleEnt = ticker.AddGameRule(TraitorGameRuleProtoId);
Assert.That(entMan.TryGetComponent<TraitorRuleComponent>(gameRuleEnt, out var traitorRule));

// Ready up
ticker.ToggleReadyAll(true);
Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.ReadyToPlay));

// Start the round
TraitorRuleComponent traitorRule = null;
await server.WaitPost(() =>
{
var gameRuleEnt = ticker.AddGameRule(TraitorGameRuleProtoId);
Assert.That(entMan.TryGetComponent<TraitorRuleComponent>(gameRuleEnt, out traitorRule));

// Ready up
ticker.ToggleReadyAll(true);
Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.ReadyToPlay));

// Start the round
ticker.StartRound();
// Force traitor mode to start (skip the delay)
ticker.StartGameRule(gameRuleEnt);
Expand Down
11 changes: 11 additions & 0 deletions Content.Server/GameTicking/GameTicker.GameRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Localization;

namespace Content.Server.GameTicking;

Expand Down Expand Up @@ -71,6 +72,16 @@ public EntityUid AddGameRule(string ruleId)
var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace);
_sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}");
_adminLogger.Add(LogType.EventStarted, $"Added game rule {ToPrettyString(ruleEntity)}");
var str = Loc.GetString("station-event-system-run-event", ("eventName", ToPrettyString(ruleEntity)));
#if DEBUG
_chatManager.SendAdminAlert(str);
#else
if (RunLevel == GameRunLevel.InRound) // avoids telling admins the round type before it starts so that can be handled elsewhere.
{
_chatManager.SendAdminAlert(str);
}
#endif
Log.Info(str);

var ev = new GameRuleAddedEvent(ruleEntity, ruleId);
RaiseLocalEvent(ruleEntity, ref ev, true);
Expand Down
85 changes: 63 additions & 22 deletions Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
using Content.Server.Administration;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Shared.Administration;
using Content.Shared.EntityTable;
using Content.Shared.GameTicking.Components;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
Expand All @@ -23,13 +24,17 @@ public sealed class BasicStationEventSchedulerSystem : GameRuleSystem<BasicStati
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EventManagerSystem _event = default!;

public const float MinEventTime = 60 * 3;
public const float MaxEventTime = 60 * 10;
protected override void Started(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
// A little starting variance so schedulers dont all proc at once.
component.TimeUntilNextEvent = RobustRandom.NextFloat(component.MinimumTimeUntilFirstEvent, component.MinimumTimeUntilFirstEvent + 120);
}

protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
GameRuleEndedEvent args)
{
component.TimeUntilNextEvent = BasicStationEventSchedulerComponent.MinimumTimeUntilFirstEvent;
component.TimeUntilNextEvent = component.MinimumTimeUntilFirstEvent;
}


Expand All @@ -49,10 +54,10 @@ public override void Update(float frameTime)
if (eventScheduler.TimeUntilNextEvent > 0)
{
eventScheduler.TimeUntilNextEvent -= frameTime;
return;
continue;
}

_event.RunRandomEvent();
_event.RunRandomEvent(eventScheduler.ScheduledGameRules);
ResetTimer(eventScheduler);
}
}
Expand All @@ -62,15 +67,16 @@ public override void Update(float frameTime)
/// </summary>
private void ResetTimer(BasicStationEventSchedulerComponent component)
{
component.TimeUntilNextEvent = _random.NextFloat(MinEventTime, MaxEventTime);
component.TimeUntilNextEvent = component.MinMaxEventTiming.Next(_random);
}
}

[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
public sealed class StationEventCommand : ToolshedCommand
{
private EventManagerSystem? _stationEvent;
private BasicStationEventSchedulerSystem? _basicScheduler;
private EntityTableSystem? _entityTable;
private IComponentFactory? _compFac;
private IRobustRandom? _random;

/// <summary>
Expand All @@ -88,10 +94,11 @@ public sealed class StationEventCommand : ToolshedCommand
/// to even exist) so I think it's fine.
/// </remarks>
[CommandImplementation("simulate")]
public IEnumerable<(string, float)> Simulate([CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev)
public IEnumerable<(string, float)> Simulate([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev)
{
_stationEvent ??= GetSys<EventManagerSystem>();
_basicScheduler ??= GetSys<BasicStationEventSchedulerSystem>();
_entityTable ??= GetSys<EntityTableSystem>();
_compFac ??= IoCManager.Resolve<IComponentFactory>();
_random ??= IoCManager.Resolve<IRobustRandom>();

var occurrences = new Dictionary<string, int>();
Expand All @@ -101,6 +108,13 @@ public sealed class StationEventCommand : ToolshedCommand
occurrences.Add(ev.Key.ID, 0);
}

if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
{
return occurrences.Select(p => (p.Key, (float)p.Value)).OrderByDescending(p => p.Item2);
}

var compMinMax = basicScheduler.MinMaxEventTiming; // we gotta do this since we cant execute on comp w/o an ent.

for (var i = 0; i < rounds; i++)
{
var curTime = TimeSpan.Zero;
Expand All @@ -111,9 +125,16 @@ public sealed class StationEventCommand : ToolshedCommand
while (curTime.TotalSeconds < randomEndTime)
{
// sim an event
curTime += TimeSpan.FromSeconds(_random.NextFloat(BasicStationEventSchedulerSystem.MinEventTime, BasicStationEventSchedulerSystem.MaxEventTime));
curTime += TimeSpan.FromSeconds(compMinMax.Next(_random));

if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var selectedEvents))
{
continue; // doesnt break because maybe the time is preventing events being available.
}
var available = _stationEvent.AvailableEvents(false, playerCount, curTime);
var ev = _stationEvent.FindEvent(available);
var plausibleEvents = new Dictionary<EntityPrototype, StationEventComponent>(available.Intersect(selectedEvents)); // C# makes me sad

var ev = _stationEvent.FindEvent(plausibleEvents);
if (ev == null)
continue;

Expand All @@ -125,26 +146,40 @@ public sealed class StationEventCommand : ToolshedCommand
}

[CommandImplementation("lsprob")]
public IEnumerable<(string, float)> LsProb()
public IEnumerable<(string, float)> LsProb([CommandArgument] EntityPrototype eventScheduler)
{
_compFac ??= IoCManager.Resolve<IComponentFactory>();
_stationEvent ??= GetSys<EventManagerSystem>();
var events = _stationEvent.AllEvents();

var totalWeight = events.Sum(x => x.Value.Weight);
if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
yield break;

foreach (var (proto, comp) in events)
if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var events))
yield break;

var totalWeight = events.Sum(x => x.Value.Weight); // Well this shit definitely isnt correct now, and I see no way to make it correct.
// Its probably *fine* but it wont be accurate if the EntityTableSelector does any subsetting.
foreach (var (proto, comp) in events) // The only solution I see is to do a simulation, and we already have that, so...!
{
yield return (proto.ID, comp.Weight / totalWeight);
}
}

[CommandImplementation("lsprobtime")]
public IEnumerable<(string, float)> LsProbTime([CommandArgument] float time)
public IEnumerable<(string, float)> LsProbTime([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] float time)
{
_compFac ??= IoCManager.Resolve<IComponentFactory>();
_stationEvent ??= GetSys<EventManagerSystem>();
var events = _stationEvent.AllEvents().Where(pair => pair.Value.EarliestStart <= time).ToList();

var totalWeight = events.Sum(x => x.Value.Weight);
if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
yield break;

if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var untimedEvents))
yield break;

var events = untimedEvents.Where(pair => pair.Value.EarliestStart <= time).ToList();

var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob.

foreach (var (proto, comp) in events)
{
Expand All @@ -153,12 +188,18 @@ public sealed class StationEventCommand : ToolshedCommand
}

[CommandImplementation("prob")]
public float Prob([CommandArgument] string eventId)
public float Prob([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] string eventId)
{
_compFac ??= IoCManager.Resolve<IComponentFactory>();
_stationEvent ??= GetSys<EventManagerSystem>();
var events = _stationEvent.AllEvents();

var totalWeight = events.Sum(x => x.Value.Weight);
if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
return 0f;

if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var events))
return 0f;

var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob.
var weight = 0f;
if (events.TryFirstOrNull(p => p.Key.ID == eventId, out var pair))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
namespace Content.Server.StationEvents.Components;
using Content.Shared.Destructible.Thresholds;
using Content.Shared.EntityTable.EntitySelectors;


namespace Content.Server.StationEvents.Components;

[RegisterComponent, Access(typeof(BasicStationEventSchedulerSystem))]
public sealed partial class BasicStationEventSchedulerComponent : Component
{
public const float MinimumTimeUntilFirstEvent = 300;
/// <summary>
/// How long the the scheduler waits to begin starting rules.
/// </summary>
[DataField]
public float MinimumTimeUntilFirstEvent = 200;

/// <summary>
/// The minimum and maximum time between rule starts in seconds.
/// </summary>
[DataField]
public MinMax MinMaxEventTiming = new(3 * 60, 10 * 60);

/// <summary>
/// How long until the next check for an event runs, is initially set based on MinimumTimeUntilFirstEvent & MinMaxEventTiming.
/// </summary>
[DataField]
public float TimeUntilNextEvent;

/// <summary>
/// How long until the next check for an event runs
/// The gamerules that the scheduler can choose from
/// </summary>
/// Default value is how long until first event is allowed
[ViewVariables(VVAccess.ReadWrite)]
public float TimeUntilNextEvent = MinimumTimeUntilFirstEvent;
/// Reminder that though we could do all selection via the EntityTableSelector, we also need to consider various <see cref="StationEventComponent"/> restrictions.
/// As such, we want to pass a list of acceptable game rules, which are then parsed for restrictions by the <see cref="EventManagerSystem"/>.
[DataField(required: true)]
public EntityTableSelector ScheduledGameRules = default!;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
namespace Content.Server.StationEvents.Components;
using Content.Shared.EntityTable.EntitySelectors;

namespace Content.Server.StationEvents.Components;

[RegisterComponent, Access(typeof(RampingStationEventSchedulerSystem))]
public sealed partial class RampingStationEventSchedulerComponent : Component
{
[DataField("endTime"), ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// Average ending chaos modifier for the ramping event scheduler. Higher means faster.
/// Max chaos chosen for a round will deviate from this
/// </summary>
[DataField]
public float AverageChaos = 6f;

/// <summary>
/// Average time (in minutes) for when the ramping event scheduler should stop increasing the chaos modifier.
/// Close to how long you expect a round to last, so you'll probably have to tweak this on downstreams.
/// </summary>
[DataField]
public float AverageEndTime = 40f;

[DataField]
public float EndTime;

[DataField("maxChaos"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float MaxChaos;

[DataField("startingChaos"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float StartingChaos;

[DataField("timeUntilNextEvent"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float TimeUntilNextEvent;

/// <summary>
/// The gamerules that the scheduler can choose from
/// </summary>
/// Reminder that though we could do all selection via the EntityTableSelector, we also need to consider various <see cref="StationEventComponent"/> restrictions.
/// As such, we want to pass a list of acceptable game rules, which are then parsed for restrictions by the <see cref="EventManagerSystem"/>.
[DataField(required: true)]
public EntityTableSelector ScheduledGameRules = default!;
}
Loading

0 comments on commit b4f0659

Please sign in to comment.