Open
Conversation
Rewrites event dispatch around per-event `EventChannel` subclasses so
plugins can subscribe to typed handlers that take the event's fields as
positional parameters instead of a wrapper object. The hot path is
volatile read + direct method invoke, no allocation, no reflection.
### New subscribe shape
```java
GrimAbstractAPI api = GrimAPIProvider.get();
EventBus bus = api.getEventBus();
GrimPlugin grim = api.getGrimPlugin(this); // resolve once at plugin enable
bus.get(FlagEvent.class).onFlag(grim, (user, check, verbose, cancelled) -> {
if (shouldSuppress(user, check)) return true; // cancel
return cancelled; // leave alone
});
```
Every subscribe is tied to a `GrimPlugin` — `bus.unregisterAllListeners(grim)`
sweeps every typed handler the same way Bukkit's `HandlerList` does. For
authors holding a Bukkit `JavaPlugin` / Fabric `ModContainer` / Class,
each channel also exposes a `@Deprecated onX(Object ctx, Handler, …)`
overload that resolves the context through the bus's extension manager.
### Hot-path caching
Fire sites cache the channel once:
```java
private static final FlagEvent.Channel FLAG =
GrimAPIProvider.get().getEventBus().get(FlagEvent.class);
FLAG.fire(user, check, verbose); // static-final → JIT folds the getfield
```
### Priority ordering
Lower number fires first, higher gets the final say — same direction as
Bukkit `EventPriority` (LOWEST → … → MONITOR). This is a break from
1.2.x which sorted highest-first. Default priority (0) users are
unaffected; callers with explicit numbers on `@GrimEventHandler` or the
deprecated class-keyed subscribe need to re-evaluate them.
### No reflection on the hot path
`OptimizedEventBus` constructor explicitly news up each channel:
```java
installChannel(FlagEvent.class, new FlagEvent.Channel());
installChannel(CommandExecuteEvent.class, new CommandExecuteEvent.Channel());
...
```
Obfuscator-safe — renaming `FlagEvent.Channel` breaks the constructor at
compile time, not at runtime. `bus.get(E.class)` is a ConcurrentHashMap
lookup keyed on the class identity. The only reflection is in the
`@Deprecated registerAnnotatedListeners` path which only runs for
plugins that opt into `@GrimEventHandler` scanning.
### Legacy 1.2.4 paths still work
`bus.post(event)`, `bus.subscribe(ctx, Class, GrimEventListener)`, and
`@GrimEventHandler` are all kept as `@Deprecated` and route through
each channel's legacy slot. A `bus.post(new FlagEvent(user, check, verbose))`
from an unmigrated plugin reaches both legacy-registered listeners AND
typed handlers (via each channel's `dispatchTypedFromLegacy` unpack
method).
### Tests + benchmark
25 unit tests pass — priority ordering, typed/legacy mixed dispatch,
cancellation threading, plugin-disable sweep, per-thread legacy pool
isolation.
JMH numbers from the `:benchmarks` subproject (kept local, not included
in this PR) on the reviewer's box:
```
Benchmark (mode) (subscribers) Mode Cnt Score Units
EventDispatchBenchmark.fire typed 0 avgt 3 2.17 ns/op
EventDispatchBenchmark.fire typed 1 avgt 3 7.19 ns/op
EventDispatchBenchmark.fire typed 3 avgt 3 13.51 ns/op
EventDispatchBenchmark.fire legacy 1 avgt 3 15.05 ns/op
EventDispatchBenchmark.fire legacy 3 avgt 3 21.99 ns/op
EventDispatchBenchmark.fire mixed 3 avgt 3 18.39 ns/op
```
`gc.count ≈ 0` and `gc.alloc.rate.norm ≈ 10⁻⁵ B/op` across every
configuration — zero-alloc fire() confirmed.
### Related
Grim-side migration PR that consumes this: GrimAnticheat/Grim#<pending>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Picks up where #19 left off. Each event gets its own
EventChannelsubclass that exposes a typed
fire(user, check, verbose, ...)— thehot path is a volatile read + direct method invoke, no per-fire
allocation, no reflection. The legacy
bus.post(...)/bus.subscribe(Class, listener)/@GrimEventHandlerpaths all keepworking as
@Deprecatedshims that route through each channel's legacyslot.
Usage
Every typed subscribe takes a
GrimPluginsobus.unregisterAllListeners(grim)sweeps it on disable, same shape as Bukkit's
HandlerList. Pluginauthors that want to pass a platform-specific context (Bukkit
JavaPlugin, FabricModContainer, aClass) can use the@Deprecated onX(Object ctx, Handler)overload that resolves throughthe bus's extension manager — the warning is there to nudge toward
api.getGrimPlugin(this)-once-and-cache.Hot path / caching
static finalso the JIT folds the getfield.Check.flag()uses avolatile lazy-init pattern instead to sidestep class-load-order worries
across check subclasses.
Priority ordering flips
Lower number fires first, higher gets the final say — Bukkit
EventPriorityconvention (LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR). Pre-1.3 Grim sorted highest-first. Plugins using defaultpriority (0) are unaffected; anyone with explicit numbers on
@GrimEventHandler/bus.subscribe(...)needs to re-evaluate.No reflection on the dispatch path
OptimizedEventBusconstructornews each channel explicitly:So renaming
FlagEvent.Channelbreaks at compile time, not at runtime,and ProGuard/tree-shakers keep everything reachable from the
constructor.
bus.get(E.class)is aConcurrentHashMaplookup keyed onthe Class identity. The only
getDeclaredMethods/MethodHandle.invokelive in the
@Deprecated registerAnnotatedListenerspath — opt-in, forback-compat with plugins using
@GrimEventHandler.JMH
With
-prof gc:gc.count ≈ 0,gc.alloc.rate.norm ≈ 10⁻⁵ B/op— theresidual is JMH infrastructure noise, not dispatch overhead. Zero-alloc
fire confirmed across typed / legacy / mixed configurations.
Tests
25 unit tests in
grim-internal/src/test:@GrimEventHandler+ plugin-disable sweep (13)Related
Grim-side migration PR: GrimAnticheat/Grim#