Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
d7e05f4
feat(triggers): stable trigger ids, runtime state and pending slots; …
kamronbatman Sep 12, 2026
4e23b23
chore: pin ModernUO to 908e9ffbc (virtual OnTick, DTO group; PR #2640)
kamronbatman Sep 12, 2026
0231ebe
fix(triggers): unique ids on import; accept the legacy triggers JSON …
kamronbatman Sep 12, 2026
758bfe6
refactor(triggers): TriggerSet per spawner, struct context, pure Eval…
kamronbatman Sep 12, 2026
dc095e6
fix(triggers): timeofday whole-day wrap; tolerate a bad speech regex;…
kamronbatman Sep 12, 2026
1f95298
chore: pin ModernUO to c02909e2c (main after #2640)
kamronbatman Sep 12, 2026
5657ad9
feat(triggers): gate set, bounded pending cycles, refractory, per-ent…
kamronbatman Sep 13, 2026
feb2504
fix(triggers): gate-open drain budget; one-shot run-now; group semant…
kamronbatman Sep 13, 2026
d53f6a6
feat(migration): XmlSpawner refractory, SpawnOnTrigger, conjunctive t…
kamronbatman Sep 13, 2026
30916ca
fix(migration): mode:tick precedes when: in emitted definitions
kamronbatman Sep 13, 2026
f209c48
perf(triggers): dispatch benchmarks and a 12k trigger perf harness
kamronbatman Sep 13, 2026
b3410af
docs: D2 trigger state machine as built
kamronbatman Sep 13, 2026
c029314
fix(triggers): game-clock constant; skill triggers on stopped spawner…
kamronbatman Sep 13, 2026
9e12f98
docs: architecture §2-3 and §7 as built after D2
kamronbatman Sep 13, 2026
5142132
fix(triggers): A4 re-arm only on deactivation; no lifted bool in when…
kamronbatman Sep 13, 2026
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
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"modernuoschemagenerator": {
"version": "4.1.0",
"commands": [
"ModernUOSchemaGenerator"
]
}
}
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/Projects/*/bin
/Projects/*/Generated
/Projects/ModernSpawner.Benchmarks/BenchmarkDotNet.Artifacts/
# dotnet run --project writes the artifacts under the working directory, not the project.
/BenchmarkDotNet.Artifacts/
/TestResults/

# Working notes and generated docs live here uncommitted (same convention as ModernUO)
Expand Down
32 changes: 22 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pull requests; until a PR merges, the submodule may be pinned to that PR's head

```sh
dotnet build ModernSpawner.slnx # builds ModernUO Server/UOContent from the submodule too
dotnet test Projects/ModernSpawner.Tests # 427 tests; the lifecycle collection boots a ModernUO test server
dotnet test Projects/ModernSpawner.Tests # 587 tests; the lifecycle collection boots a ModernUO test server
dotnet build -c Analyze # analyzers + Rules.ruleset
```

Expand All @@ -36,7 +36,9 @@ dotnet build -c Analyze # analyzers + Rules.ruleset
World-backed tests (`Projects/ModernSpawner.Tests/Core/ModernSpawnerLifecycleTests.cs`) share a
process-wide ModernUO bootstrap in `Projects/ModernSpawner.Tests/Fixtures/ModernSpawnerTestServer.cs`
and run in a `DisableParallelization` xunit collection; use that fixture for any new test that needs a
live spawner rather than standing up World/Core state by hand.
live spawner rather than standing up World/Core state by hand. The 12k-spawner trigger perf harness
(`Projects/ModernSpawner.Tests/Perf/TriggerPerfHarness.cs`) is part of that count but is a no-op unless
`MODERNSPAWNER_PERF=1` is set, so the default run stays sub-second.

## Rules

Expand All @@ -60,14 +62,24 @@ ModernSpawner-specific:
base contract (`Entries`, `EntrySpan`, `CreateEntry`, `AddEntryCore`, …) runs over it and the lifecycle
hooks (`OnStarted`, `OnSpawned`, `OnSpawnedDeath`, entry-aware `GetSpawnPosition`) carry the modern
behaviour. Never add a parallel entry list or hide base members with `new`.
- Triggers register through `TriggerSystem`; proximity uses `Item.HandlesOnMovement`/`OnMovement`, speech
uses `HandlesOnSpeech`, skill uses `Server.Misc.SkillEvents.SkillUsed` (players only). Extended (beyond
24-tile) proximity is stubbed pending a ModernUO area-movement API.
- Trigger list changes go through the generated helpers (`AddToTriggerDefinitions`,
`RemoveFromTriggerDefinitionsAt`, `ClearTriggerDefinitions`), then call `EnsureTriggersActive()`; the
`TriggerActivated` setter does this for you. Never call `TriggerSystem.ActivateTriggers` directly — it is
not idempotent, and within `Projects/ModernSpawner` `EnsureTriggersActive` is its only caller (tests call
it deliberately, to build the stale registrations teardown has to survive).
- Triggers are a state machine, not a bool: dispatch (`OnMovement`/`OnSpeech`/kill/skill) never spawns —
a match calls `spawner.RequestCycle(trigger, in context)`, which only mutates spawner state (cooldown,
refractory, the pending-cycle queue) and asks `TriggerSystem` for a drain; the outermost dispatch runs
the cycle once it returns. All of that state (the gate set, the queue, cooldowns, kill counts, per-entry
deadlines) lives on the spawner, so a tick never looks anything up. See `dev-docs/architecture.md` §5 for
the tick-precedence and event transition table.
- Trigger definitions are `TriggerDefinition { Id, Text }` with a stable id generated once; mutate the list
only through `AddTriggerDefinition`/`RemoveTriggerDefinitionAt`/`ClearTriggerDefinitions` — they assign
the id and call `EnsureTriggersActive()` for you. Never call `TriggerSystem.ActivateTriggers` directly:
it is not idempotent on its own, and within `Projects/ModernSpawner`, `ModernSpawner.EnsureTriggersActive`
is its only caller (tests call it deliberately, to build the stale-registration cases teardown has to
survive). Registration follows `TriggerActivated` and the definition list, never `Running` — `Start()`/
`Stop()` only arm or disarm the timer.
- Proximity uses `Item.HandlesOnMovement`/`OnMovement`, speech uses `HandlesOnSpeech`, skill uses
`Server.Misc.SkillEvents.SkillUsed` (players only). Extended (beyond 24-tile) proximity is clamped to
`Core.GlobalMaxUpdateRange` with a warning.
- Per-event-trigger tokens (`wake:`, `mode:`, `when:`) are a suffix, recognised only after a grammar's full
positional list — never write one where a positional field could be misread as a token name.

## ModernUO changes

Expand Down
17 changes: 17 additions & 0 deletions Projects/ModernSpawner.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ dotnet run -c Release [options]
--compile Run compilation cost benchmarks
--set Run property set benchmarks
--expr Run XmlSpawner vs ModernSpawner expression comparison
--triggers Run the D2 trigger dispatch benchmarks (set lookup, request/drain)
--quick Run with fewer iterations (for quick testing)
--filter <p> Hand the arguments to BenchmarkDotNet's own switcher, e.g. --filter *TriggerDispatch*
--help, -h Show this help

Examples:
dotnet run -c Release --all
dotnet run -c Release --expr
dotnet run -c Release --spawner --condition
dotnet run -c Release --quick --simple
dotnet run -c Release -- --filter *TriggerDispatch*
""");
return;
}
Expand All @@ -44,6 +47,14 @@ dotnet run -c Release --quick --simple
config = config.WithOptions(ConfigOptions.DisableOptimizationsValidator);
}

// A --filter run is handed straight to BenchmarkDotNet's switcher, which understands globs over the
// whole assembly. The curated flags below stay for the suites that predate it.
if (args.Contains("--filter"))
{
BenchmarkSwitcher.FromAssembly(typeof(TriggerDispatchLookupBenchmarks).Assembly).Run(args, config);
return;
}

// Determine which benchmarks to run
var runAll = args.Contains("--all") || args.Length == 0;
var benchmarkTypes = new List<Type>();
Expand Down Expand Up @@ -80,6 +91,12 @@ dotnet run -c Release --quick --simple
benchmarkTypes.Add(typeof(ComplexExpressionBenchmarks));
}

if (runAll || args.Contains("--triggers"))
{
benchmarkTypes.Add(typeof(TriggerDispatchLookupBenchmarks));
benchmarkTypes.Add(typeof(TriggerDispatchRequestDrainBenchmarks));
}

if (benchmarkTypes.Count == 0)
{
Console.WriteLine("No benchmarks selected. Use --help for options.");
Expand Down
Loading