-
Notifications
You must be signed in to change notification settings - Fork 7
Forge Hybrids
What this page covers: what EverNifeCore does on a hybrid Bukkit+Forge server - the API that hands
a Forge listener of yours to the server's own Forge bus, the forge audience that sits on the
event bus, how the adapter for the running hybrid is picked, and - separately, because it is
not the same claim - which hybrids that has actually been run on.
A hybrid runs Bukkit plugins and Forge mods in one process: Cauldron/Crucible on 1.7.10, Mohist, CatServer, Arclight. On everything else - Spigot, Paper, any vanilla-derived server - all of this is inert. No Forge class is loaded, nothing is logged, and the one entry point that could act refuses with a message instead. That is the case that runs on almost every server, so it is the one that had to be free.
The code lives in the minecraft module: br.com.finalcraft.evernifecore.minecraft.listeners.forge for the
inbound half, and br.com.finalcraft.evernifecore.minecraft.eventbus.McForgeAudience for the outbound
one.
| Direction | What it means | Entry point | State |
|---|---|---|---|
| Inbound | a Forge event reaches a listener you wrote | ForgeListener.registerListener(...) |
works on the hybrids measured below |
| Outbound | an ECEvent posted on the bus reaches the Forge side |
the forge audience |
registered and asked on every post - nothing crosses yet |
Read that table before designing anything around it. The inbound half is a working route with a public API; the outbound half is a seam that is in place, is exercised, and currently ends in a log line.
ForgeListener is the whole public surface - three static methods, no instance to hold:
public static boolean isAvailable();
public static void registerListener(Plugin plugin, ECListener listener);
public static void registerListener(Plugin plugin, ECListener listener, Object... eventBus);plugin is org.bukkit.plugin.Plugin; listener is EverNifeCore's own
ECListener. The buses are Object... on purpose: the caller hands
over a bus whose type EverNifeCore never names, and never has to - see
resolved by name.
Ask isAvailable() first. Both overloads throw when there is no hybrid behind the server, which is
the common case:
//in your plugin's enable; `this` is the Bukkit Plugin doing the registering
if (ForgeListener.isAvailable()) {
ForgeListener.registerListener(this, new MyForgeListener());
}java.lang.IllegalStateException: Tried to register ForgeEvents but there is no IForgeListener
available for EverNifeCore on this Server.
isAvailable() is also what runs the detection, so the first caller pays for building the adapter and
everyone after it reads a field.
The class implements ECListener, but the handler methods are ordinary Forge handlers:
public class MyForgeListener implements ECListener {
//@SubscribeEvent and the parameter type both come from the Forge this server runs.
//EverNifeCore names neither and has neither on its classpath - they are yours to import.
@SubscribeEvent
public void onSomeForgeEvent(SomeForgeEvent event) {
//...
}
}The annotation Forge declares changed package between eras, and both are accepted:
net.minecraftforge.eventbus.api.SubscribeEvent from 1.16.5 on, and
net.minecraftforge.fml.common.eventhandler.SubscribeEvent before it.
Your plugin is the one that needs those types at compile time. EverNifeCore does not provide them and does not stand between you and them.
The no-bus overload uses the era's default bus - net.minecraftforge.common.MinecraftForge.EVENT_BUS,
and only that one. The varargs overload takes the buses you already hold. What happens next depends on
the hybrid, and the differences matter:
| Adapter | registerListener(plugin, listener) |
registerListener(plugin, listener, buses...) |
|---|---|---|
CrucibleForgeListener |
CrucibleEventBus.register(plugin, defaultBus, listener) |
one register per bus handed in |
ArclightForgeListener |
Arclight.registerForgeEvent(plugin, defaultBus, listener) |
only for buses that are a net.minecraftforge.eventbus.api.IEventBus; anything else is skipped in silence
|
ModernMohistForgeListener |
MohistEventBus.register(defaultBus, listener) |
one register per bus handed in |
MohistForgeListener |
scans your listener's declared methods for @SubscribeEvent and routes them through Mohist's own Bukkit wrapper event |
ignores the buses entirely - it delegates to the no-bus form |
Two consequences worth carrying:
- Passing a bus does not guarantee it is used. On Mohist the parameter is discarded by design (that hybrid mirrors Forge events into Bukkit rather than exposing a bus to register on); on Arclight a bus of an unexpected type is dropped without a word.
-
Mohist is the adapter that does its work in the constructor. It wires its Bukkit bridge there, so
it is the one that can refuse before you ever call it - naming
SubscribeEventwhen neither annotation resolves. The other three build without touching a Forge type and refuse only when used, each naming the class it could not find.
McForgeAudience is an ECNativeAudience registered under
the name forge. The Bukkit entry point adds it at enable and removes it at shutdown, next to the
Bukkit one - see Architecture Overview.
Its gate is the hybrid detection ForgeListener already does, and deliberately the only one - a second
probe could disagree with the first. What that detection decides is only whether to speak: the gate
itself answers false on every server, because an audience that cannot deliver has no listeners,
whatever the Forge side registered. So:
-
on a server with no Forge,
hasListeners(...)answersfalse,dispatch(...)never runs, and the audience costs nothing. It is registered unconditionally precisely because that case is free; -
on a hybrid,
hasListeners(...)answersfalseall the same, anddispatch(...)never runs either. The first time the bus asks - a post, anECEventBus.hasListeners(...), a listener watch being evaluated - the audience says this, once per run:
[ECEventBus] This server has a Forge side and the 'forge' audience is registered, but no event can
reach the Forge bus yet: posting one needs a compiled carrier extending this era's Forge Event class,
and none is built. Bukkit listeners are unaffected. This is said once.
Answering the hybrid flag instead would be worse than useless: it would hold every listener watch open
and make every gated event be built - on exactly the hybrid servers this project runs on - for a bus
nothing reaches. dispatch(...) stays a no-op rather than being removed, so a caller that bypasses
the gate loses nothing.
Why nothing crosses, and why it is not a matter of effort. A Forge bus takes a subclass of its
era's Event - and Event is a class. Reflection can call a bus and instantiate a class that
exists; it cannot define a subclass, and Proxy only implements interfaces. Each Forge era declares
that base under a different name, so the route out is one compiled carrier per era, not one reflective
call. Until such a carrier exists, the audience is a seam that is wired, asked and idle.
Nothing about this affects Bukkit listeners: the Bukkit audience
mirrors as it always did, and the forge audience runs after it.
One probe per hybrid flavor, first match wins, and the class each looks for is the API class that flavor ships:
| Order | Class probed | Adapter | Eras the branch claims |
|---|---|---|---|
| 1 | io.github.crucible.api.CrucibleEventBus |
CrucibleForgeListener |
1.7.10 |
| 2 | io.izzel.arclight.api.Arclight |
ArclightForgeListener |
1.12.2, 1.16.5, 1.20.x |
| 3 | com.mohistmc.forge.MohistEventBus |
ModernMohistForgeListener |
1.20.x |
| 4 | com.mohistmc.api.event.BukkitHookForgeEvent |
MohistForgeListener |
1.12.2, 1.16.5 |
| 5 | catserver.api.bukkit.ForgeEventV2 |
MohistForgeListener |
1.16.5 - CatServer is served by the Mohist bridge and has no adapter of its own |
No match means no adapter, and isAvailable() answers false for the rest of the run.
The probe answers flavor, not era, and that gap is real. One branch can cover three Minecraft eras (Arclight's does), so a server whose flavor matches still gets an adapter that may speak the wrong era's vocabulary. That is not hypothetical - it is what a 1.21.1 Arclight does, and it is measured below.
The detection never throws. A platform that only half-matches would otherwise fail this class's
initialization and turn every later call - isAvailable() included - into a NoClassDefFoundError;
failing the detection instead costs the Forge route and nothing else.
No Forge type is compiled into EverNifeCore. Every class, field and method on this route starts life as
a string and is looked up at runtime, which is what lets a single adapter serve eras that disagree
about types - MinecraftForge.EVENT_BUS is a cpw.mods.fml.common.eventhandler.EventBus on 1.7.10 and
a net.minecraftforge.eventbus.api.IEventBus from 1.16.5 on, and a by-name read hands back either one
as an opaque handle.
The price is that the compiler checks none of it. Not a class name, not a member name, not an arity. A typo, a renamed method or an era that moved a type is a failure that appears only when a server of that era executes the line - never in a build, never in a unit test. See Version Compatibility for why the Bukkit side of the core refuses reflection for exactly this reason; the Forge side pays the price knowingly, and the mitigations are what is left:
- a lookup that comes back empty is refused with an
IllegalStateExceptionnaming the class and the member it wanted, and telling the reader to report the server brand and version; - arity is checked before invoking, so a by-name lookup cannot quietly bind to an overload taking something else and fail deep inside the call;
- a lookup that throws instead of answering is refused the same way -
RuntimeExceptionandLinkageErrorare caught, andThrowabledeliberately is not, so anOutOfMemoryErrorkeeps its own type instead of being filed as a quirky hybrid; - the refusal is thrown at call time, never at class-initialization time.
Which is the honest limit of it: a good message where the lookup answers, and nothing where the server breaks the lookup itself. The Arclight case below is exactly that hole.
Because none of this is compile-checked, a real server of the right era is the only evidence there is. This table is that evidence, and nothing in it is inferred. The brand column is what the server reported about itself, which is not always what its distribution is called: the 1.7.10 hybrid tested here ships Crucible's API classes and reports itself as Cauldron.
| Server tested | Brand it reported | Adapter chosen | Inbound registration |
forge audience |
|---|---|---|---|---|
| 1.7.10 hybrid | Cauldron 1.7.10-staging-9985c735 |
CrucibleForgeListener |
completed end to end | present after enable, gone after shutdown |
| Mohist 1.12.2 | Mohist git-Mohist-1.12.2-2d59d5a |
MohistForgeListener |
call completed (see below) | present after enable, gone after shutdown |
| Mohist 1.16.5 | Mohist 1.16.5-8c7caaf7 |
MohistForgeListener |
call completed (see below) | present after enable, gone after shutdown |
| Arclight 1.21.1 | Arclight 1.0.1-8ec9529 |
ArclightForgeListener |
fails - see below | present after enable, gone after shutdown |
| Paper 1.21.1 | Paper 1.21.1-133-3cb8529 |
none - not a hybrid | refuses, with the message above | registered and completely inert |
On all five the server log was clean of exceptions from the plugin, and the audience was on the bus after enable; on the four hybrids it was also confirmed gone after shutdown.
The 1.7.10 result is the strong one. The whole chain ran by name on a real Cauldron:
MinecraftForge.EVENT_BUS was read, CrucibleEventBus.register resolved with three parameters and was
invoked, and no NoSuchFieldError or NoClassDefFoundError appeared.
The two Mohist results are weaker, and the difference is worth stating. MohistForgeListener
scans the listener for @SubscribeEvent methods and does nothing when there are none - and the probe
listener had none. So what those two prove is that the adapter builds on both eras (it resolves
BukkitHookForgeEvent.getEvent, resolves the era's @SubscribeEvent, and registers its Bukkit bridge)
and that the call completes. A Forge event actually arriving at a @SubscribeEvent method was not
measured.
Arclight 1.21.1 runs on NeoForge, where net.minecraftforge.* no longer exists. All three by-name
lookups the adapter needs fail there, each differently:
| What it reaches for | On Arclight 1.21.1 | How it fails |
|---|---|---|
Arclight.registerForgeEvent |
class is there, but the lookup dies inside Arclight's own class remapper | a raw NoClassDefFoundError: org/bukkit/plugin/Plugin - the guard never runs, so the message blames a type that is not the problem |
net.minecraftforge.common.MinecraftForge (for EVENT_BUS) |
absent | would be the guarded IllegalStateException, but the failure above happens first |
net.minecraftforge.eventbus.api.IEventBus |
absent |
isModernEventBus answers false, so the varargs overload registers nothing, in silence
|
Nothing was changed in response: which flavor a branch claims, and which vocabulary an adapter speaks, are design decisions. What this page can do is not promise the route works there. It does not.
Two of the five detection branches have never been executed by any server:
-
com.mohistmc.forge.MohistEventBus->ModernMohistForgeListener(a Mohist on 1.20.x); -
catserver.api.bukkit.ForgeEventV2->MohistForgeListener(CatServer).
They are written, they compile, and they are unproven. Given that nothing on this route is compile-checked, treat "unproven" as "unknown", not as "probably fine".
And the outbound direction is unproven everywhere by construction: the audience registered and was asked on all four hybrids, and on all four the only thing that happened was the log line above.
- Events - the bus, the audiences, and how to write one of your own.
- Architecture Overview - where the audiences are added and removed in the Bukkit startup sequence.
- Version Compatibility - the eras this core spans, and why reflection is refused on the Bukkit side.
- Gotchas & Pitfalls - the short version of the by-name trap.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Block Data (SVWorldDataManager)
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Logging & Debug Modules
- Events
- Scheduler & Threading
- Items & NBT
- GUI Framework
- Integrations
- Economy
- Version Compatibility
Architecture & Reference