#Events and EventBus
1 messages · Page 3 of 1
Just make the constructor protected. If someone then makes an anonymous class to fire it anyway, then they're an idiot ¯_(ツ)_/¯
yeah that would work I guess
Doesn't solve the whole "unimplementable abstract methods bit"
But not a bad solution either
I suppose throwing on those methods would work but it also leads to unexpected bugs if a dev doesn't implement them
actually it doesn't solve any problem
people could still try to listen to the event which is the entire problem
No no this is if you make abstract events unlistenable. This is an event that would normally be abstract but that people should be able to listen to
(at least, that's the context XFactHD was replying to)
ah, right
Yup, that was the intended use case for that suggestion.
Another option could be to have the constructor throw on this.getClass() == TheRootEvent.class
The bigger issue is abstract methods in my opinion
As far as I can remember without checking, there is not a single event in Forge with an abstract method, all of them just hold the context that makes sense for the given level of inheritance.
Yes but the worry was that mods use it. In the long run, I don't think it's a big deal though - and using abstract classes as "don't listen" is imo far cleaner than an annotation, even if both are suboptimal
Yeah it’s fields can be identity classes, e.g. optional will become a value class
(If that’s what you’re referring to)
I just realized that we can use methodhandle combinators to place the cancellation checks etc inside the classes that LMF generates
no the question is if you can pass a value type as an argument, have the function modify a field inside the value type, and have that show up in the calling function [via passing the arguument as a pointer to the calling function's stack]. In C# you have to use an explicit "ref" parameter for this.
though the relevancy to events is tangential, since i am skeptical that events would benefit much from being value types even if this works.
Nah that’s not supported as far as I’m aware, they’re completely immutable and I don’t believe there’s any concept of pointers. Value types would only really be useful for fire and forget events which is not many of them haha.
Unless you stored the current fired event somewhere and allowed event handlers to update that and then the updated version is returned to the event firer
Imagine that at the end of each level, there is a bus that takes you to the next level.
and that bus runs on an extremely tight schedule
@outer steeple please seem my comments on https://github.com/neoforged/Bus/pull/17
if you're touching those it's good to clean it up as well
true, true
especially since some form of inheritance is probably here to stay
changes made
@outer steeple pong
methodhandles aren't even that slow
on a test with 100 listeners and an event firing 10 times:
Benchmark Mode Cnt Score Error Units
ComparisonBenchmark.testCl avgt 30 11.535 ▒ 0.689 us/op
ComparisonBenchmark.testCl:Àjfr avgt NaN ---
ComparisonBenchmark.testLmf avgt 30 13.517 ▒ 0.719 us/op
ComparisonBenchmark.testLmf:Àjfr avgt NaN ---
ComparisonBenchmark.testMh avgt 30 19.878 ▒ 2.209 us/op
ComparisonBenchmark.testMh:Àjfr avgt NaN ---
@outer steeple new PR just dropped 😛
can you run a benchmark too
Benchmark Mode Cnt Score Error Units
EventBusBenchmark.testClassLoaderCombined avgt 15 71.410 ▒ 7.078 ns/op
EventBusBenchmark.testClassLoaderDynamic avgt 15 30.531 ▒ 2.803 ns/op
EventBusBenchmark.testClassLoaderLambda avgt 15 27.355 ▒ 2.896 ns/op
EventBusBenchmark.testClassLoaderStatic avgt 15 35.135 ▒ 5.245 ns/op
EventBusBenchmark.testModLauncherCombined avgt 15 71.563 ▒ 4.830 ns/op
EventBusBenchmark.testModLauncherDynamic avgt 15 31.274 ▒ 2.253 ns/op
EventBusBenchmark.testModLauncherLambda avgt 15 27.076 ▒ 2.112 ns/op
EventBusBenchmark.testModLauncherStatic avgt 15 28.678 ▒ 3.190 ns/op
EventBusBenchmarkNoLoader.testNoLoaderCombined avgt 15 74.308 ▒ 6.854 ns/op
EventBusBenchmarkNoLoader.testNoLoaderDynamic avgt 15 27.666 ▒ 3.103 ns/op
EventBusBenchmarkNoLoader.testNoLoaderLambda avgt 15 27.320 ▒ 2.308 ns/op
EventBusBenchmarkNoLoader.testNoLoaderStatic avgt 15 28.644 ▒ 2.260 ns/op
note that this benchmark is not good at measuring the impact of this, there's not enough listeners
another PR for fun: https://github.com/neoforged/Bus/pull/19
getting close to finishing the TODO list now...
did you already stab cancellable?
No I think I'm gonna keep it for now
bad
Would you?
well, not necessarily
but ideally you'd only get a boolean result if the event is cancelable in the first place 😉
just remove the annotation and make cancellable events override setCancelled
pretty much all the annotation does anyway
what would be a good name for "post a cancelable event and return true if the event was canceled"?
the idea is to move isCanceled and setCanceled to a CancelableEvent interface
honestly fuck that whole contract, it should be false if cancelled and true if successful
every call to it is !post(blah)
not necessarily
if (post) return;
and changing that is too much of a pain to port to for no benefit at the end of the day
I'd just like to find a better method name
guess what you're changing the method signature, so the effort is the same™️
maybe postCheckCanceled
and then you can just name it postCancellable
that doesn't say anything about the return value
would be nice if one could tell just from the callsite what it means
because it doesn't need to compensate in the name for its shitty inverse contract
(also it's cancelable :P)
(US usually cancelable)
the eventbus has been not only wrong but used both names for a while
it's still not super clear to me from postCancelable what the return value means
something like postIsCanceled makes more sense
or postAndCanceled
wait hear me out
what if the event itself was returned after post
so you could do post(...).isCanceled()
how did I not think of this, it's so obvious
maty get reviewing so I can PR this! 
or if someone else wants to review my 2 PRs... be my guest
To be fair, the implicit contract of java lib collections that return a boolean status (such as List#add) is to return if it was successful and not add any verbosity to the name
C# usually prepends Try to functions that return a status (but java doesn't, so that's mostly irrelevant here)
returning the event is the most flexible approach
a lot of event factory methods do "return null if canceled otherwise either the event or some result property of the event"
Ultimately post returning the event is probably pointless
because its just going to transform
var event = new MyEvent(...);
post(event)
if(event.isCancelled() { ...
to
var event = new MyEvent(...);
if(post(event).isCancelled()) { ...
honestly the former is probably preferrable
if only Java had the comma operator
if (post(event), event.isCanceled())
hmmmmmmm... 
aww you can't do post(var event = new MyEvent(...))
is if ((event = bus.post(new MyEvent())).isCancelled()) valid
Yes
what about if (bus.post(new MyEvent()) instanceof SomeTypeIDK event && event.isCancelled())
Thats.... clever

stab
Well isn't that better? Could even do post(new MyEvent()).isCanceled()
That looks terrible when you have params and/or need to retain the event
may as well make post be void
I can also keep the current signature even if the event is not cancelable
nobody says you have to use that signature, however it is nice to keep things one-liners
you could also do var event = post(new MyEvent()); return event.isCanceled() ? null : event
Why would you ever want that
that just obscures .isCancelled by a later != null check
Well I thought ForgeEventFactory did that sometimes?
I want to make cancelable events an interface that people can implement on their events instead of an annotation:
public interface CancelableEvent {
default void setCanceled(boolean canceled) { ((Event) this).canceled = canceled; }
default boolean isCanceled() { return ((Event) this).canceled; }
}
public class Event {
/* package-private */ boolean canceled = false;
// ...
}
this will remove the autocomplete of methods that shouldn't be called.
Now, my question for <@&1128776937410670663>s is:
What should the return value of EventBus#post be?
Here are a few options:
- Keep current status quo:
trueif event is cancelable and was canceled. - Flip the boolean return to mean
falseif the event was canceled. - Make
postreturn the event itself so that people can chain an.isCanceled()call if they want. - Make
postreturnvoid. - Make
postreturnvoidand add a newpostCancelableoverload that returns a boolean, the overload only being available for<T extends Event & CancelableEvent>, of course.
What do you think?
I really like 3. myself
I'm divided between 3 and 5
would postCancellable do anything at all besides return cancel status?
nothing more, no
and you still couldn't tell just from the name of the function whether it is true or false that stands for canceled
then 3. IMO
If it does then it's stupid, lol
Its probably a legacy reason for not writing the event classname in the patch files
Performance wise this is not a great idea
At least implementation wise
why?
The cast ((Event) this) is very very very expensive
it's only used by mods though
the eventbus will use a static final VarHandle internally to read canceled
yes but it's not called nearly enough for it to matter
You do not know that!
Be carefull in these kinds of libraries
Modders use events and their features in interesting ways
Some fire multiple events per frame
Forge even has some cancellable events which are fired multiple times per frame
I have nothing against the concept of a CancellableEvent
could we have a CancellableEvent subclass from Event instead of the interface?
no that is terrible for the API
CancellableEventBase implements CancellableEvent then
I am not buying the FUD argument, Orion, events only get canceled once 😛
if someone wants to skip the base class they can declare their own field
It does not matter, the problem is that the compiler and the JVM have no way to know that the implementations can only be Event!
Please for the love of god, we have been trying to get rid of this pattern every where in all client components
Do not add it here
It is not a good pattern
And terrible for performance
it leads to a much nicer API
and I'll bet you it has ZERO impact on performance in practice!
It does not
also how does CancellableEvent extends Event mess up the API?
We tested this to death a year ago
Has anyone ever measured this in practice? I've seen this claim a bunch of times, mainly related to the extension interfaces, but never backed by any numbers
Yes
fine I'll write a JMH profile
wait nevermind, I answered that myself
Me, @rustic kelp and @manic void ran several tests
We discovered this because we used this exact pattern in extension classes
And could not figure out why the new bakedmodel code was got awefully slow
Well turns out
The bytecode that gets generated from that construct is horrible
varhandle both the set and get
done
The varhandle impl sounds like a hacky mess, just leave all the cancellable code on Event and make cancellable events override setCancelled to flip a protected isCancelled field
isn't the point to not have public setCancelled methods on events that are not cancellable?
Yeah
@rigid gust What you can do is put the event and the interface in the same class
Then add a Event getThis() method to the interface
And then access a package private field
why same class?
same package is fine
not very scientific, but still
Benchmark Mode Cnt Score Error Units
CastBenchmark.testDirect avgt 15 55.673 ▒ 17.477 ns/op
CastBenchmark.testDirect:Àjfr avgt NaN ---
CastBenchmark.testInterface avgt 15 47.756 ▒ 3.776 ns/op
CastBenchmark.testInterface:Àjfr avgt NaN ---
error on the first one is too high
but it is nowhere near that 100x you mentioned and in fact looks quite fine
package net.neoforged.bus.benchmarks;
import net.neoforged.bus.api.BusBuilder;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.IEventBus;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;
@State(Scope.Benchmark)
public class CastBenchmark {
public static class TestEvent1 extends Event implements SomeInterface {
private int value = 0;
}
public static class TestEvent2 extends Event {
private int value = 0;
public int getValue() {
return value;
}
public void incrementValue() {
value++;
}
}
public interface SomeInterface {
default int getValue() {
return ((TestEvent1) this).value;
}
default void incrementValue() {
((TestEvent1) this).value++;
}
}
private final IEventBus bus = BusBuilder.builder().build();
@Setup
public void setup() {
for (int i = 0; i < 20; ++i) {
int finalI = i;
bus.addListener(TestEvent1.class, event -> {
if (event.getValue() < finalI) {
event.incrementValue();
}
});
bus.addListener(TestEvent2.class, event -> {
if (event.getValue() < finalI) {
event.incrementValue();
}
});
}
}
@Benchmark
public void testInterface(Blackhole blackhole) {
TestEvent1 event = new TestEvent1();
bus.post(event);
blackhole.consume(event);
}
@Benchmark
public void testDirect(Blackhole blackhole) {
TestEvent2 event = new TestEvent2();
bus.post(event);
blackhole.consume(event);
}
}
Where is that field supposed to be and what's the point of it?
Is the field supposed to be the canceled flag?
in event. same thing TEch is suggesting but instead of casting, use a getThis() that Event implements
Yeah, I realized that after sending 😅
AFAICT, the getThis() method would have to be implemented by every single event implementing that interface though. I don't see how that's better than a CancellableEvent extends Event class. But maybe I'm missing something really obvious here
hmmm...
```java
protected interface AnEvent {
Event self();
}
public interface CancellableEvent extends AnEvent {
default void setCancelled(boolean cancelled) { self().isCancelled = true; }
}
public class Event implements AnEvent {
/* package private */ boolean isCancelled;
public Event self() { return this; }
}
public class SomeCancellableEvent extends SomeEvent implements CancellableEvent {
/* nothing special to do here */
}
CancellableEvent extends Event means you can't have a class further down in the hierarchy be cancellable, without forcing all the parents to also be cancellable
Makes sense, yeah
I mean, you are overthinking this
oww can't make protected interfaces :P
I proved that my original design doesn't have a substantial impact on performance, feel free to copy/paste the benchmark and prove me wrong 😛
I mean
to me it isn't about performance
it's about self() being much nicer to look at than casting
For 2 methods it doesn't make any difference
here is another benchmark which had lower variance:
Benchmark Mode Cnt Score Error Units
CastBenchmark.testDirect avgt 15 48.783 ▒ 4.505 ns/op
CastBenchmark.testDirect:Àjfr avgt NaN ---
CastBenchmark.testInterface avgt 15 47.159 ▒ 3.426 ns/op
CastBenchmark.testInterface:Àjfr avgt NaN ---
what's the impl of the test, out of curiosity?
could the difference between your results and orions be the java version?
^ here is the impl, giga
could be, this is on java 17
ah so it's just one post call per loop
maybe 15 counts is too small for a meaningful result?
each count corresponds to 2 seconds of continuous runs
oh nevermind, thought it counted individual executions of the test code
is the code @inland imp ^
@rigid gust i get a feeling the jvm will devirtualize SomeInterface in that test
Yeah
because there is only one class implementing it
I was thinking that
And also, because the callsite to the increment is on the concrete type
There might be hot code path detection in the jvm that then unfolds it
@manic void This is more your area of expertise
jvm is annoying like this
Do you remember what triggered that stupid casting performance issues during our rendering refactor tests?
who wants to ask jas too
i think she's quite busy atm
counter-point: Fabric has such a cast per model and meshing performance is fine (https://github.com/FabricMC/fabric/blob/670b2ea08de60d952f62d5333f93bfe18c27f8ed/fabric-renderer-api-v1/src/client/java/net/fabricmc/fabric/api/renderer/v1/model/FabricBakedModel.java#L97)
so I really don't know how this happened to you
We literally had these tests
I can't find them at the moment
And we could prove it too
Because the bytecode in the JVM is garbage
It does a bunch of additional checks
To make sure that the cast is safe
In those virtual calls
Is a potential solution having isCancelled in the Event class, having it always return false, then having an abstract CancellableEvent to extend where the setCancelled exists and overrides the isCancelled check? No casting required then?
I mean yeah checkcast isn't free, and you have the problem of devirtualization if there are few impls
Yeah
we did this with many different implementations of IForgeBakedModel
Which was our case
That had such a cast to get self
And then fallback to the vanilla call if the forge custom method was not overriden
Hold on, can we just add setCancelled as protected and make cancellable events override to public and call super?
checkcast isn't free but it should be cheap enough here, especially since setCanceled is only called once per event at most (usually)
That is the point
once per event for an event(s) fired per frame
Because the JVM has no idea about the type hierarchy the checkcast is far from free
protected with an "override to public" idiom would work
did another more elaborate test:
Benchmark Mode Cnt Score Error Units
CastBenchmark.testDirect avgt 15 96.241 ▒ 11.749 ns/op
CastBenchmark.testDirect:Àjfr avgt NaN ---
CastBenchmark.testInterface avgt 15 107.663 ▒ 21.949 ns/op
CastBenchmark.testInterface:Àjfr avgt NaN ---
(source code: https://gist.github.com/Technici4n/a4709f37b90c85e6fab85a1e0d7c8dc1)
yet again there is no visible difference
once per frame is nothing
Can you push your code
no, CancelableEvent should be an interface
I will check this out later
gradlew jmh to run it should do, otherwise lmk
don't get me wrong I think we should be careful with performance, but we should also understand that events that fire once per frame are not a problem per se
atm posting a non-cancelable event seems to be about 10 + 2*listenerCount nanoseconds
This issue only exists if you make it an interface
my claim is that it's not an issue and I want this settled once and for all!
But casting is going to be less performant than not
it does not make a sufficient difference
But there is a difference
So why would you insist on it being an interface when you could go with the other solution and not have the performance loss because you don't need to cast
- making it a class or adding artificial methods leads to a worse API
- I want to stop this argument once and for all
How does it make it worse API when to the modder it looks the same
abstract superclass is not an option
because something like PlayerEvent might have cancelable and non-cancelable subclasses
That's a reasonable explanation - one potential issue with an interface though is you'd have to implement canceled/setCanceled in every class that implements the interface, no?
And then check it out
(note that we can easily change the casts to a varhandle lookup if we want to in the future, so checking this is not urgent)
see #1136448404495532133 message
The varhandle impl does that somewhere too though, even if it's not as obvious, if the thing you're feeding it isn't already the normal receiver type of the handle
And yeah, I'd say an Event getThis() method is probably the most sensible, honestly
Actually I like this. Or heck, make Event an interface. Is there anything that it actually needs to be a concrete class for?
I guess for storing the cancellable field, right... that whole system seems iffy to me honestly
I have some ideas for how else you'd do it but they'd all be pretty intrusive redesigns which I already know people here are against
Idk why protected method that gets overridden to public is not a viable option
It's no better than an interface with a getThis method, as either case requires implementing classes to overload something. The interface, by comparison, has a good separation of functionality that the protected method does not
It's a viable option, there's just other viable options that it doesn't really offer advantages over
The event needs to override something to document the effects of cancellation
right now we just jam it in the class doc
which is bad for organization
oh hey, the self-cast thing was brought up again 
/**
* Cancelling this event does xyz
*/
public void setCancelled(boolean cancel) {
...
Yes, I agree. I'm saying that implementing an interface with a getThis method isn't any worse than overriding said protected methods - and is in fact better, in that it will be shoved into a separate interface where it's obvious what is or isn't cancellable based on that
Implementing getThis is churn
Overriding setCancelled, which must be done to document it, is implicit
This should go in the root event docs imo, as it's relevant to the behavior of the event as a whole, both cancelling and checking for cancellation
iirc covers, ama, and Orion observed through tests that a self-cast has a non-negligible penalty, and that it was recommended that any such self-cast in rendering extension interfaces should be limited to at most once (i.e. in something like IForgeBakedModel, any method which needs the BakedModel more than once should start with a BakedModel model = self() and use that variable onwards)
I think the basis they used for the test was for IForgeTransformation
Okay, an alternative idea: what if that interface just has those two methods. No default implementations or anything
That is also fine
And the events are responsible for grabbing from or shoving to the field in it
Though the event bus has to know the concept of cancellation
Yeah, protected field
Hmm. The whole system seems iffy. I would refer you to this: #1136448404495532133 message
But yeah, the whole "cancellable field on event when not all events are cancellable" kinda smells to me
But no easy way around it without more intrusive changes
Well it doesn't have to be, right
but then you have to instanceof Cancellable to check if propogation should halt
What's going on
Can't you do that with the class itself before generating the listener array, and delegate to a completely different route for cancellable events?
Hell, that would speed up non cancellable events as well I would think
Maybe? I'm not super versed in that part of the internals
Talking about the smelliness of having a cancellable field in the base event class, and about how maybe making events override the cancellable methods isn't a bad thing so that they're documented
Tech is that a thing we can do 
You've certainly got access to the event type so I theory yes. Especially if you yeet listener inheritance
Which I believe has happened?
Listener inheritance is what would hold it up before
Well, sorta
Actually maybe not
Yeah, on second thought that should be possible
But there is no isCanceled filter anymore for non-cancelable events
A non-cancellable event shouldn't subclass a cancellable event
Yeah, we're fine. I think this should definitely be possible then
I don't think anything suggested here is better than my original proposal
Just yeeting the cancellable field entirely that is and using the interface directly
#1136448404495532133 message
[Reference ➤ ](#1136448404495532133 message)Talking about the smelliness of having a cancellable field in the base event class, and about how maybe making events override the cancellable methods isn't a bad thing so that they're documented
You can keep your interface but it doesn't need a default impl
That's why it's better
Still an interface, yeah
No defaults, and no field in event
The method must be overridden in the target to describe the effects of calling it
Well then people need to implement the methods themselves
sucks, its super trivial to do
Class javadoc moment
It is not appropriate for the class doc
You're not solving any problem
The field in event is really smelly
It is an effect of calling setCancelled
Consider it an impl detail 😛
The class doc has been packed with all sorts of garbage because of the annotations, not because it is the proper place for it
Yeah, the effects of cancellation should go in the docs for the methods involving cancellation
Makes the docs way easier to read
Meh reading 3 lines of class javadoc won't kill you
Impl details can still smell
documentation for cancellation is wildly all over the place due to this
Sure, neither will implementing a couple of super simple methods 😛
its just "somewhere" in the class doc
That's good enough
Adding boilerplate is not a goal here
Neither is propagating code smell, and that's what the field in event is
I don't care about code smell if it doesn't have an impact on modders
It also means that, tech, with your system - an event could implement CancellableEvent, override the impl of isCancelled, and not see any change in what EventBus considers cancelled
Indeed, that's why the method is annotated with @NonExtendable
If you're using what are supposed to be effectively final methods... then an interface isn't the right place for them...
The whole thing is ugly. If implementing the interface makes something cancellable... then it should actually do that, not just enable built in functionality
Additionally, is there any reason not to let people implement their own cancellable implementations?
The entire concept of cancelation gets special treatment from the eventbus
Special treatment that should be based on the interface that determines cancellability
After all, the existence of that interface is what determines whether to enable all the cancellable stuff, right?
Due to inheritance it is somewhat more complicated than that
How so? You're constructing a list of listeners for an event type
The listener has to anticipate for cancelable subclasses
Inheritance should only come in in terms of which listeners get shoved in the array
Won't cancellable subclasses get their own listener arrays?
They do, but currently the code works by removing the check, not adding it
How does it remove the check exactly?
Otherwise the receiveCanceled boolean has to be kept somewhere
Anyway I'm off to bed. Remember that goal #1 is to keep the code close to the current version and not add boilerplate, everything else comes after that
Looking over that it seems like you're just filtering stuff down one of two different paths based on the event type. The extra filter information you keep around could be kept around in the outer class, and just not used, but used by the inner class. That all seems doable to a degree
Why must keeping the code close to the current version necessarily be the #1 goal? If the current version sucks shouldn't we use this opportunity to change it?
That's a shit goal lol
It was the self functions in the extension interfaces. Overriding them actually had more performance, i think it was a combination of invokeinterface being god awfully slow, and the 'unnessesary' checkcast. Lex wanted us to make more tests then write some asm to replace self calls with ALOAD 0 at runtime. We obviously never got around to it.
ye, that's what I remember
i wonder if we should make an issue on the NF repo to track that
just so we remember
even though the tests don't seem to show a meaningful difference, the cancelableevent interface can declare an abstract Event getThis() and Event can soft-override it
yes but that's just stupid, we can instead just make the events override setCancelled
Why was cancellable powered by asm anyway?
because annotations™️
Was there a reason or did lex just do it that way?
it's not any les stupid then having half of Forge's events override and implement a boilerplate method
and in any case don't forget that isCancelled is a thing too
it's not just one method you have to implement
I still think we should do this as a compromise. it's less concerning (justified or no) than the cast, and doesn't require dedicated impl on each subclass
isn't it illegal to override a protected method to public? that's a whole thing in access transformers
other way around
oh right brainfart. the issue is making the parent public, without the children
I still don't like the pattern either way cos overriding setCancelled isn't a good visual indicator , but I guess it would work
it's also a compile time only problem, i'm not sure what the override structure being considered is
what if Event were an interface and CancelableEvent extended it?
then everyone needs to impl the field, get and set
lots of duplication
not an issue just annoying
EventBus.cancelCurrentEvent
setCancelled must be overridden to provide documentation on cancellation. it is not appropriate for the class javadoc, it is only shoehorned in there because it is currently annotation-based and can go nowhere else. Same for result stuff
I'm not gonna add boilerplate for cancelable events, so we can probably stop this discussion here and move on to more productive things
I'm sorry but why is your opinion suddenly the one that ends discussions? This is a worthwhile discussion. Even if you don't think it's a good approach, discussing it means that someone else who does think it's good could go implement it themselves
We can continue the discussion if you accept the obvious requirement that code should not be made a lot more verbose for no gain
mfw adding
@Override
public void setCancelled(boolean cancel) {
this.cancelled = true;
}
is "a lot more verbose"
And you need a getter too
"oh no"
its like 3 more lines
+1 for the field
and that's if all of it gets packed into the children
Yet that doesn't bring any benefit to modders?
if it stays protected in Event then only setCancelled is needed
The benfit is having the documentation of cancelling an event on the relevant methods, not jammed in the class doc where it does not belong
the overhead is exceptionally minimal
Lol if that's your only reasoning I am sorry to tell you that it wont fly
You don't get to make the call :p
if we're stuck disagreeing we have to call a full vote
it can't since modders need to be able to check if the event was cancelled (receiveCancelled)
and we are already taking changes that necessitate updating nearly every event
No?
If you're gonna call a full vote because putting documentation in the class is too much of an ask then wtf
only the field and the setter would be protected, isCancelled (the getter) would remain public as-is
even if the getter is protected it's still a trivial amount of code
Removing the annotations means every cancellable event has to be touched already
Yes and it's a trivial replacement by an interface
Not having to copy-paste boilerplate into each event
but then again we would have non cancellable events that have that method
and? Then they are never isCancelled, it's not a huge issue. The listener list is going to query cancelled status anyway to propagate listeners
I just woke up, and am going to do some number running later
If I can't figure it out we can take what tech proposed and call it a day
Orion it might have been the non-private self() method (java 8 didn't have private interface methods)
It won't as an optimization
Shadows I would like you to understand that it is extremely annoying to have people come and bikeshed a single API element and then threaten request a full vote when I tell you that no that design is not practical
and I'm not in the mood for keeping stuff in a legacy spot "just because"
it is only there because the system was annotation-based
If legacy means less boilerplate then so be it
Your only argument is that class javadoc is apparently not suitable for documentation on how the event behaves when canceled, please dude
shadows the easiest solution if we don't have a consensus here is to leave it as it is ... annotation based
retain the status quo if no consensus to change
we can always revisit this discussion later down the road, perhaps when someone makes a proof of concept
They were all none private indeed
for no gain
Sure, I'd accept that. That's not the discussion here though
then what is the gain for modders?
But you shouldn't be acting like your opinion is somehow authoritative here
(well, it should be since I am the one doing the refactor)
His opinion is what matters the most, since he is writing it
For modders? More obvious documentation, better separation of concerns in terms of what the interface does, and the ability to have custom cancellation logic
When is done, it needs to be reviewed
And then his opinion is put up against other moderators
Orion, this was in response to #1136448404495532133 message
I'm talking about Tech shutting down discussion of alternatives. Not about him not implementing alternatives
Understood
Obviously he doesn't have to implement squat
Misread the statement then
But saying "i won't write that so let's not talk about it" is... just not good
I strongly doubt we'll get to a better alternative but I digress
Personal opinion: I dislike the default implementation on the interface.........
I don't think the custom canellation logic has been talked about at all yet
More obvious documentation
why is class documentation not acceptable?
better separation of concerns in terms of what the interface does
?
ability to have custom cancellation logic
that's not gonna go away
However I think we need some kind of common logic
IMHO from the event bus's perspective, a cancellable event, only needs one property:
public interface CancellableEvent {
boolean isCancelled();
}
You misunderstood point 3 - being able to override the cancellable methods is useful, and that means that the listener array should check the interface method, not the field. In terms of point 1 - yes, it's more obvious. Having it on the class is not necessarily easy to read
How the cancel is triggered is really not relevant for the event bus
Yes, this is what I'm proposing
That, and yeeting the cancelled field on event to go with that
if we remove the field, it has to be re-added by every single cancelable event...
i think the idea behind that documentation point might be similar to why the JDK introduced the SequencedCollection interfaces in J21
having that interface -- or in this case, having a declared setCancelled method -- is more powerful in pointing consumers about a property of this collection/event, rather than being relegated simply to a doc comment
Yeah I understand tech
But that can be solved differently.
Like it is technically a different discussion
From the event busses perspective
It does not matter
Whether it is a field
Or something else
For us as implementers
It matters
Because it means less work
well, virtual dispatch is not free either
Sadly no goorvy Trait here
Performance wise it is not really relevant
VDispatch is much cheaper then check cast....
And that had no impact either in your tests
jvm virtual dispatch is quite fast
well idk, removing the isCanceled check for non-cancelable events made a difference when I made a proper benchmark some time ago
Yeah for sure, because it is a useless call
So the trade off there is for sure worth it
But IMHO the API design of the event is much nicer with it going through the method
Instead of straight for the field
the bus just needs a way of figuring out if an event is cancelled or not
Yeah
yes
back to our problem, I am willing to compromise a bit on the design on the eventbus side if it makes the API nicer for users
This is really in the same vein as the result stuff
Documentation should be organized where it is relevant
the problem with the result is that it's a generic enum that is being misused
There are three "weird" things in event class docs: results, cancellation, and the target event bus
the first two we can move
the latter is... weird af and has no home
if you get the bus wrong your listener won't register
so there is some level of protection
same level as calling setCanceled on an event without the @Cancelable annotation
(i.e. you get a runtime check)
I mean that's a reasonable level of protection but I wish it was a bit better
I still sorta think the multi-bus concept is weird
it is but it will allow us to make events parallel in a nice way
Maybe we should just hide the type of bus from the modders
i.e. make the api just "add listener"
How can we tell which bus?
The event class
Same way we do the runtime failure check, lol
Virtual Dispatch (i.e. ((CancelableEvent) event).isCanceled())
Benchmark Mode Cnt Score Error Units
VirtualCallBenchmark.testCanCancel avgt 15 183.083 ▒ 11.032 ns/op
VirtualCallBenchmark.testCanCancel:Àjfr avgt NaN ---
VirtualCallBenchmark.testCannotCancel avgt 15 81.196 ▒ 4.194 ns/op
VirtualCallBenchmark.testCannotCancel:Àjfr avgt NaN ---
VarHandle access to isCanceled
Benchmark Mode Cnt Score Error Units
VirtualCallBenchmark.testCanCancel avgt 15 171.539 ▒ 5.807 ns/op
VirtualCallBenchmark.testCanCancel:Àjfr avgt NaN ---
VirtualCallBenchmark.testCannotCancel avgt 15 82.577 ▒ 5.970 ns/op
VirtualCallBenchmark.testCannotCancel:Àjfr avgt NaN ---
so no real difference
@inland imp btw
Meh
You know what
I trust what ever you come up with
Sorry to have wasted your time
lol don't worry, you did not
this means that we can let people override isCanceled if they want to
I would say that is a great idea
Personally I would not even bother with the field in event at all, at least not in the base library
And maybe make some sort of ForgeBaseEvent
that holds that magic
so basically abstract BaseCancelableEvent?
No
the problem is that e.g. some PlayerEvent subclasses need to be cancelable and some others not
What I am saying is: have the event library not care for how cancellation is done
But then in forge create your system
Yeah and so you don't extend the abstract event there
Potentially with some ASM processing to deal with the downsides
Just use the interface
That way, you have a very clean API in Bus itself
I'm trying to remove ASM, not add more of it 😛
And deal with the ugly side of things, in a place were you have many many many more tools available
I am not saying you need to use ASM
I am saying move that ugly implementation detail to a place were your toolbox is much bigger
And were you have many more possibilities
a lot of cancelable events don't directly extend from Event
due to single inheritance a base class approach doesn't seem to work
keeping it protected in Event works :p
that doesn't remove the boilerplate of needing to override two methods?
I still think you can leave isCancelled public, only the field and the setter need be protected
hell, the field can be private
the setter just needs to call super
it shouldn't be left public otherwise it is too easy to think your event is cancelable when it is not... :/
Is it? I wager most people never check isCancellable unless they are explicitly using a receiveCancelled listener
well the event author checks it at the very least
The author isn't going to accidentially forget their event is cancellable (or the inverse)
it can happen
I'm trying to avoid cancellation-related methods in non-cancelable events
if we can't achieve that then we'll keep the annotation
the only real way is to make that a true interface
And forego any default implementation anywhere
I know, but it means that we'll need to copy/paste the cancellation boilerplate in most PlayerEvent subclasses... unless we also make a CancelablePlayerEvent class lol 🤨
Yeah
That is the problem you are in
Without a trait system like groovy there is no other clean option
You will always need to muddle the water
Somewhere
yeah
That is the beauty of the ASM solution XD
It fixes that mess
Cause it injects the "trait" afterwards 😄
but I removed the subclass transformer, don't make me add it back xD
yeah I don't think one exists
I have the luxury in ngnext when such a problem came up that Mary wrote me a whole groovy Ast processor that generates the implementations for me
Would a annotation processor be able to do this?
Like you add the interface it detects it
And then generates the field and implementations of the methods for you?
At compile time?
well ideally we don't need post-processing of the jars anymore
Aps are fine
They are part of the jdk/eclipse compiler and supported by all of the frameworks we use
However I think an AP needs a custom annotation to make it work
But that would be trivial
I mean, that is a lot more complicated than putting a canceled field in Event
However I don't think it can modify the original class.....
Yeah but it gets you the clean solution
Then again I do not think the AP can be used after all
Since it can't modify a class
Only generate new ones
code transformations should be last-resort imo
Not really...
This is compile time processing
Which in my opinion is fare game
But given that you can not modify the existing class
I think it them becomes more difficult
Ultimately having only part of the methods be available (the getter) is preferrable to both methods and the asm magic of the annotation
Java annotation processors don't always play nicely with other jvm languages
I stand by
/**
* Interface for events that can be canceled.
* Listeners registered to the event bus will not receive {@link #isCanceled() canceled} events,
* unless they were registered with {@code receiveCanceled = true}.
*/
public interface CancelableEvent {
/**
* Sets the cancel state of this event.
*
* <p>This will prevent other listeners from receiving this event unless they were registered with
* {@code receiveCanceled = true}.
* Further effects of setting the cancel state are defined on a per-event basis.
*
* <p>This method may be overridden to react to cancellation of the event,
* however a super call must always be made as follows:
* {@code CancelableEvent.super.setCanceled(canceled);}
*/
@MustBeInvokedByOverriders
default void setCanceled(boolean canceled) {
((Event) this).isCanceled = canceled;
}
/**
* Returns the canceled state of this event.
*/
@ApiStatus.NonExtendable
default boolean isCanceled() {
return ((Event) this).isCanceled;
}
}
which does not need ASM and hides both methods 😛
wtf is MustBeInvokedByOverriders
jb annotations
oh it means must call super
yes
and so you can override setCanceled if you really want to, but you get a reminder to call super
Yet for events with custom logic, that seems like a bad idea
Still of the opinion that setCancelled must be overridden
why as well ^
do you have an example of an event that cannot use that system?
Well if I have custom cancellation logic
That setter is useless
So I override it to make it noop, now I get annoying warnings
That I must override it / call the super
What could you possibly mean by that 
The most I've ever seen an event do on cancellation is set a result when the cancel flag is set
(if you really need to you can always @SuppressWarnings("MethodDoesntCallSuperMethod"))
(also I think the interface could just be called Cancellable)
But that might have weird name conflict resolution with the annotation
no 2 l
isn't the standard in US english to use a single l?
I'm not from the US
cpw's English, hence why there were two Ls before
No they're both legal
according to Merriam-Webster, the correct forms are canceled, cancellable and cancellation
so we should name the interface Cancellable but the methods isCanceled and setCanceled 
That's American
no that's just what the US wants
Seems strange to change dialect of an existing project that was English to American but if we're making breaking changes regardless... Meh
iirc its inconsistent rn
there are uses of single and double L
the ABI uses single L only atm
Almost everything is inconsistent throughout our ecosystem
isn't the param name double L?
Like how "Coordinated Universal Time" is "CUT" and "Temps Universel Coordonné" (french) is "TUC" so they went with "UTC"
mismatched initialisms/acronyms are usually the work of the French
see also nearly every initialism involved with designing the modern metric system:
SI for International System of Units (Système International)
CGPM for General Conference on Weights and Measures (Conférence générale des poids et mesures)
CIPM for International Committee for Weights and Measures (Comité international des poids et mesures)
BIPM for International Bureau of Weights and Measures (Bureau international des poids et mesures)
and so on
as maty would say, vive la France!
That boilerplate isn't a bad thing? It means stuff is documented on the cancellable method
Also American here, canceled is by no means standard in the US. Plenty of Americans, myself included, use cancelled
So if you really want to go with American English don't let that rule out cancelled
Canceled is technically "more common" in American English but it's not almost all one or the other like it is with color/colour or the like
oh, do we keep the I prefix for interfaces?
for the interface name we have a few options: CancellableEvent, Cancellable, ICancellableEvent, ICancellable
I think for consistency we should stick with I for interfaces and bring it up with the whole maintainers team later on
Because I have my reservations about it
But having this one interface not like all the others is silly
well it is in the bus lib not neo
the other thing is that the event bus already contains BusBuilder which is an interface
admittedly BusBuilder is not modder-facing
went for ICancellableEvent
We'll go for a sweeping refactor a later time if the maintainers agree. Though I don't expect many to, given that it's pretty much just a breaking change for breaking change sake, with no real benefit to anyone :p
there was a vote early on about the I prefixes in neo
it was unfortunately decided that we're keeping them
I suppose keeping them for old stuff and not using them for new stuff could work
Isn't consistency something we should strive for in general?
Having a slowly diverging set of interfaces with disjoint naming schemes is pretty silly
Makes it a lot harder to search for stuff :p
we are inconsistent with the rest of the java ecosystem unfortunately
We can at least be self consistent, and not intentionally make the entire framework way less intuitive to use :p
idk, I'm looking at the available options
I don't think the I prefix is fundamental when searching for something
We either make everyone suffer in the short term by changing everything now, or we make slightly less people suffer for the rest of time by slowly changing things, or we keep consistency and intuitiveness by keeping the current system :p
I don't see many "options" here, everything is bad but one option is far less bad than the others
we already decided to make everyone suffer now
By removing all the I prefixes?
Forcing everyone who uses an interface to fix the code :p
by changing packages, making breaking changes and reworking systems
everyone will suffer now one way or another
even if that implies changing a class/package name (we already need to provide migration tools for the package change anyways)
package change doesn't require migration tools?
should still provide a tsrg file for shrimp's tool
or a xml file for intellij migrations
and we can change the name of the class, not just the package in both of those
if you do remove the I prefix; the class
class CancellableEvent extends net.blah.blah.CancellableEvent
is terrible imo. Rename one of them, and if I prefix is what it needs, then that's fine. But class + interface identically named (sans package) seems like a recipe for confusion, incorrect imports, annoyance, etc.
Cancellable should be an interface tho, not an abstract class
Event should be an abstract class
the point is to move away from magic annotations :p
Yeah I really dislike the use of a magic annotation for this. I feel like making unlistenable stuff have to be abstract is fine. After all, if it's unlistenable you shouldn't be able to instantiate it...
The reasoning against that was that then every abstract event would be unlistenable, right?
If you really want to use a magic annotation, it's much cleaner to have a ListenableAbstractEvent one. That way most of the time you never have to use it - and if you design your API to avoid it you actually never have to use it
Basically, is there any use case for having a non-abstract event be unlistenable? No. All concrete types should be listenable - because they can be instantiated, and thus fired on the bus
The problem is that we might have listenable abstract events
Yes #1136448404495532133 message
@UnlistenableEvent requires abstract
I prefer my use of annotations
Overloading the meaning of abstract is also a form of magic
This would all be fixed with a proper event type system... maybe I'll put together a proof of concept of what that would look like.
That's probably not compatible with mod busses
@outer steeple can you look at the ICancellableEvent PR when you have the time?
UnlistenableEvent too ideally... for that we mostly need to decide whether we want the feature
Yeah I have to remove it
I'll remove it with the rest of the ASM stuff once I've done more tests with LMF
It would be. The bus is integrated into the type, as it ought to be since each type is valid for only one bus
I'll try to prototype it once I have less other projects
There should be a single mod bus type but many mod busses
There's probably a way to make it work but I doubt it would be much better than the current eventbus
Idk about this, maty
Well given that it would solve literally all the issues that keep popping up that encourage magic annotations, and allow for generic event like stuff in a sensible form that doesn't rely on types, allowing you to do something like subscribe to an event for a specific entity type or whatnot... yeah I don't buy that. I'm going to see what it would actually need to make it work.
I vote yes
We can't really keep @vernal latch if we don't make the subscription work with java types
I know Quilt has sort of replacement but I prefer Event subclasses to Fabric-style functional interfaces because they are better for forward-compatibility
Btw, Tech, it's worth noting that IEventBus#stop() currently documents that event posting after bus shutdown is no-op, so that would need to be adapted if we do throw on a stopped/not-yet-started bus
I see
I really don't know about that tbh, I don't like the idea of these start and stop methods in the first place heh
Yeah I have a way to make that work. At least for events where there's only one event type per that event class. I tell you what, why don't we delay that discussion till I have a prototype?
You mean, besides that folks will give it fair consideration and weigh it on its merits, and not just go with a status quo bias? 😉
for something as breaking as the bus system we can almost guarantee you that there will be a status quo bias
I don't see why there must be
After all, there wasn't really with the capability overhaul... and I'm not suggesting changes to how people listen to events. Or. Well. Minor changes there. But most of what I'm thinking of would affect new events added, and how you create events
we are in a major breaking changes phase where something like eventbus (now bus) can break and capabilities is also in this window ... yours would be outside that window
I mean I've been suggesting this sort of approach for ages but kept being told "no we're not doing that"; in fact, tech even said a goal of his EventBus PRs was not to make major changes...
So... I don't really know what to say here? Like, if the approach you've had from the very beginning was "don't make big changes to EventBus" you can hardly expect me to have put together my big changes by now
And breaking change windows will pop up again. It's not like I'm advocating for such a large overhaul as, like, switching to Fabric's system or something
If I implement it all the way I'm thinking it wouldn't require changes to anyone using SubscribeEvent or EBS. Heck, I could probably make all the current API for listening to events keep working
I'm just tired of being told "we don't want big changes" and, basically, being told that you're likely not going to consider my proposal on those grounds alone. That's basically you all saying "don't bother if it's a big change, since we'd never merge it anyways". No, you don't get to say that. You wait till it exists, you judge it on its merits, not on the status quo, you see how big of a change it would be, and if it's better than the current system and thus something you'd want you find an appropriate timeline or intermediate system to transition stuff. If it isnt better, you provide the reasoning that means it doesn't work for neo, and that can't just be "changes are too big"
And sure, I might make such a system and you guys might decide it's not what neo needs. But not on the grounds of "if it ain't broke don't fix it"
That's a reason to delay stuff till a better time but not much more, especially when it's someone else putting in the time to do said fixing
the only thing I said is that there is no guarantee that your prototype will be used
but you should feel free to make one
the differences between that and the current system will be weighted against the benefits it brings
My giant wall of text there was targeted at schurli's justification of status quo bias as a legitimate factor when weighing the advantages of something someone else has already made
The "no promises it will actually be used" bit I would assume as a given. I would also assume the stuff I mentioned here: #1136448404495532133 message, which was why I mentioned it
why did that embed
difference with the current status quo is a decision factor
I'm not planning to use that as an excuse to shut down alternative designs
You compare it against the status quo, yes. But when someone else has already done the work of making the version, the status quo version doesn't get any points just for being the existing solution
but if we have multiple "equivalent" designs then we should go for the one that will break the least things
the change has to be done by everyone in the ecosystem, not just by Neo itself... so "the work is already done" is not necessarily an argument
If they're truly equivalent, sure
anyway, it was just a small disclaimer, please have a look and see if you can do something better
So you do the change at a good time. By that argument neo shouldn't have been created to start with, and shouldn't have changed anything from forge
Or you build tools to help people transition, or whatever
I don't want to argue this, my position can be summarized as "no change just for change's sake"
Or you find a way to shoehorn the old API to fit it - which is my plan
Nobody ever wants change for change's sake. If they want change, it's because they think their system or approach is better for whatever reason
And I'm tired of seeing "no change for change's sake" used as a way to try to shut that down. The simple fact that it's change doesn't have a say in whether it's a good idea or not - if the system were built from scratch today, and you'd pick A over B in that scenario, then there should be a plan to move from B to A
The part that actually matters is whether A is actually better than B
the way old designs inform a choice for me is "does the benefit of B compared to A outweigh the ecosystem migration cost from A to B?"
Yeah no. That's relevant in terms of when you make the switch. It determines timeline. It determines figuring out easy ways to transition. It shouldn't determine, or be relevant to, whether the switch is planned to eventually happen
which for me is status quo bias
And I'm saying that's just not how stuff should work, basically. That isn't a relevant factor in terms of whether to make a change. It's relevant in the "how you make the change" bit, but not in whether you should
then status quo bias is fine
status quo bias is fine
status quo is where people have the expertise, it requires the least immediate effort, so it is necessary to weight the long term improvement vs the short them pain
is there any plan to change the way event subclasses work? for example some have .Pre and .Post which are errorprone since one might accidentally register the superclass making the event firing twice. Other events use internal Phases instead like player tick event which if you dont check for it it will fire 4 times, for client, server and phase start and beginning. I've noticed that for a registered event that checks for phase immediately, the other phase still takes like 0.04% of server time which is small but could be better
this is what I suggest to avoid this error for Pre and Post events: https://github.com/neoforged/Bus/pull/21
client/server and pre/post as fields is not something that class type checks can guard against
oh thats nice
thinking more about this, we should probably use abstract as a marker for unlistenable events
I think that in 99% of the cases it will be equivalent to the annotation, so might as well use it for that
yeah, especially given
- it's honestly not very reasonable for event classes (even unlistenable ones) to have any actual unimplemented methods anyway, other than the event machinery itself they're pretty straightforward data containers with simple getters/setters
- there is, therefore, no good reason for a listenable event to be abstract
This post goes over the changes made to the "EventBus" subsystem of NeoForge 20.2.
This does not cover specific events, but rather changes made to the event machinery itself.
would there ever be a reason for an event to be cancellable but a subclass event not be cancellable?
Apologies for silly typo nitpick - thank for you for putting that together, was very helpful! :)
- We will address this in capability rework.
+ We will address this in the capability rework.
should I also bring up the comma splice I noticed
hmmm?
no, that is not allowed because the superclass listeners will also receive subclass events
Benchmark Mode Cnt Score Error Units
EventBusStressTest.testClassLoader avgt 15 124.391 ▒ 15.395 ns/op
EventBusStressTest.testClassLoaderCancellable avgt 15 183.628 ▒ 30.735 ns/op
EventBusStressTest.testLambdaMetaFactory avgt 15 100.639 ▒ 19.911 ns/op
EventBusStressTest.testLambdaMetaFactoryCancellable avgt 15 173.024 ▒ 42.990 ns/op
EventBusStressTest.testMethodHandles avgt 15 511.020 ▒ 70.839 ns/op
EventBusStressTest.testMethodHandlesCancellable avgt 15 583.273 ▒ 57.246 ns/op
I think we have a winner here
LMF ftw
yeah!
Inheritance of @SubscribeEvent methods might have to be deleted
that's the first I've heard of that annotation 
Fixed lol

Atm you can register an object whose superclasses and superinterfaces have methods with @vernal latch
I don't think killing that is necessarily a bad thing
The problem is that this doesn't interact nicely with private/package-private/protected modifiers
For public we can do getMethods, but this doesn't work to get the full list of protected methods, for example
And I just removed the ASM transformer that changes every non-private @vernal latch method to public
As far as I can tell, method overriding isn't done properly atm - the actual method that is used is picked randomly among the candidates
It doesn't even detect methods in interfaces lol, the code is so broken
I'm fine with a range of solutions
The simplest would be to remove this sort of inheritance
We could also pull all methods but disallow overrides
do we know if anyone is actually using this behaviour?
Im in favour of yeeting it unless theres an actual valid usecase someone needs it for
can we detect it and warn/error?
i don't do object event registration at all so idk the use cases
is that something waifu could log?
Maybe? That would be a question for @outer steeple
I think we're already saving annotations and inheritance so we might already be good?
Maybe
I'll remove it and error-proof the whole thing
(will also throw errors when the eventbus expects a static method but the method is not static, etc...)
I'd also be in favor of killing it. To safe-guard against people potentially trying to still use it: what about requiring non-static event handlers to be final?
I'm checking the superclasses for SubscribeEvent annotations and throwing an error if this happens now
Unfortunately my battery ran out so this isn't pushed yet 😛
This is a great idea
getDeclaredMethods() ?
Doesn't include inherited stuff
Hence the issue
I think the final requirement might be a bit cleaner, but eitherway works
you could use it if you manually crawl the ingeritance tree
Resolving overrides is a PITA however, I checked how reflection does it 😛
ik, I didn't say it is a good idea just that it works
(Wait I realized making subscribe methods final doesn't solve this; yeah, tech, your solution makes sense to me)
I hope it will also reduce the amount of confusion caused by SubscribeEvent with novice modders (forgot to make a method static etc)
Can the annotation be removed completely?
Ah yeah we can do that
You'd have to remove every listener registration apart from addListener()
addListener(Consumer<Event>) may work
We already have addListener
You are right though, each listener method would need to be registered separately. Personally I think that is fine; it works like this on Fabric and it would significantly simplify the code. Also, less magic annotations.
I assume their talking about adding addListener, we already have it
And I agree, that's not the best idea to register all methods
I know it already exists; I just forgot what its signature is
Automatically registering every method in a class is significantly worse than a magic annotation in my opinion. Requiring individual listener registration (i.e. removing EBS as well) would prevent hands-off registration of client-only listeners and listeners in other source sets and while the former can be replaced with individual registration behind a side-guard, the latter cannot
If you're allowing register(someInstanceWithMethods) or register(SomeClass.class) at all, you need to keep the annotation
If you're rewriting event bus to the degree that that sort of registration is getting entirely yeeted, then I would want to push for a bunch of other changes at the same time
It can't ... listeners in other source sets is a major problem
While I agree that with that sort of stuff in mind that seems like a good argument for keeping object/class based listener registration around... I would also argue that that's a good argument for allowing multiple mod entrypoints, because even funky source set layouts should never force a mod to use EBS for stuff
Basically: if you want weird magic side effects, please make them use an annotation at least
But we should provide approachs that don't need weird magic stuff
It's not magic, we can clearly document what it does
Consider the following. A user uses the instance of their main @Mod class to hold listeners. This is not an uncommon thing to do. They register that instance, which has some subscribed methods on it, with the forge bus. They've got a couple of extra listeners that need the mod bus though, so they shove those in unannotated methods and register the methods directly to the mod bus. I see no reason to make this into a flawed approach
It's super confusing but indeed we won't prevent that
All of that said, the use of the event also makes stuff explicit. A class could have any number of methods that take a Something as an argument. We're either 1) special casing ones that consume events, and any sort of non-obvious special casing is not good (that's like the definition of "magic" in this context), or 2) not allowing classes to have other non-listener methods that might look sorta like listener methods, which is also not good
Well idk, instance methods could indeed be useful
Yeah, exactly - that's why @SubscribeEvent should still be needed
there we go: https://github.com/neoforged/Bus/pull/24
--add-opens="java.base/java.lang.invoke=net.neoforged.bus" is now required.
Updated @SubscribeEvent semantics
The new behavior is as follows:
Will register all methods (regardless of vis...
sorry for the massive PR (most of it is ripping out ASM and MML), have fun
almost done with the eventbus changes heh - I will need to make FML changes too for better phase support at some point
but what if I want to have a class which has both static listeners and instance listeners 
well don't 😛
I think the ability to detect user mistakes is worth sacrificing whatever ugly patterns "advanced users" have come up with
the new system should be a lot more beginner-friendly
(I hope)
is it actually useful to have non static listeners cause storing stuff between event calls is cursed
As long as the new ways are consistent so we don't need !events I can go with whatever
It's very useful for testing events :p
so we can remove it :p
My event test framework relies on being able to store data between events
To ensure that the data is changed/set/read properly between them
Yeah, but it makes verifying the entire API a lot harder
Not against it, just unsure how it'd work if we had that limitation
Instance listeners are super useful
Especially if you don't store your blocks and whatnot statically but in your mod object
That's exactly my goal!
But this looks great to me
If someone is using static and instance SubscribeEvent on the same class there's bigger issues
that is cursed
Nah, it makes multiloader dev a lot easier in some ways. Turns out the 50% of the pain points in multiloader stuff are solved if you treat the whole mod as operating on a mod instance
And having side effects in static init is never good in my mind anyways
I think you are the only person doing this
That's kept
What do we actually need the IMPL_LOOKUP for? Is that just to call protected methods?
Ahh, because LambdaMetafactory puts the generated class under the method's package, rather than the Lookup's package?
Yes exactly
Well no
The lookup and method must have the same package
The lookup needs private access so it is always of the method's class, but we also need the module flag to define classes
So MethodHandles.lookup() returns something with both those flags set.
I'm sure you're right! I've just forgotten what LambdaMetafactory does under the hood that means one needs it.
If only MethodHandleProxies.asInterfaceInstance was actually fast, would make this much easier to do :).
I spent some time stepping through LMF to understand why it needs full access - that's because it basically generates class files with ASM
IMO there should be a supported way to get package access for open modules, maybe something to bring up with the jdk devs one day 😉
I still have to look at how method handle lookups work with modules. That's the bit I never quite understood, and I should probably make OpenSesame support it
You only need PRIVATE|MODULE flags set for that, not the global trusted one though. I've definitely used Lookup.defineHiddenClass with it before.
I can't get the MODULE bit on a lookup for a class in another module sadly
Okay, I've made it work, but only for public methods.
Personally I'm not a fan of protected/private listener methods, but not clear to me what the tradeoffs are with breaking them.
To explain, there's a couple of fun method handle things:
- Doing
LOOKUP.in(callbackClass)clears the module/private bits for a non-trustedLookupinstance. So we just remove that, and useMethodHandle.lookup()directly. - This then allows us to declare classes, but causes access checks to fail. To fix that, we need to
.addReads(callbackClass.getModule()). - Theoretically we should be able to do
callback.setAccessible(true)too and call that, but LMF usesMethodHandles.revealDirectunder the hood, which does another access check.
Ironically if one just reimplemented LMF, it'd work fine :/.
Happy to put together a PR if that would be helpful, just to demonstrate? Sorry, not trying to undermine your work, just the number of --add-opens Neo uses already makes me sad :D:.
it's actually very useful having non-public listener methods
it avoids other mods calling your event handler or you yourself doing it
my @SubscribeEvent listeners are basically always package-private
If I understand it right this would let them be private-private, right? That would be awesome
Easy way to prevent mistakes
(and clean up your autocomplete)
https://github.com/SquidDev/Bus/commit/39b84cd8609c9f8dc21e5b73cab6980df338641b Right, managed to get that working with protected/private methods.
Obviously this doesn't remove the use of asm (though does remove the mod launcher/asm code), so may not be worth it.

There's a rogue > at the end of the link
Thank you, clearly missed the leading <.
LMF is stupid
Yeah :/. The way it was written made sense when Java 8 came out, but now that we've got fancier methodhandle stuff, it's odd that its retained its limitations.
I wrote that while needing to generate interfaces using LMF but non-direct handles
with somethind like that we could easily build the IEventListener out of handle chains and lmf
Wait, I'm so confused what that code is trying to do. How do you end up consuming that generated invoke method?
(my initial usecase for that was building fabric-style T[] -> T events automatically)
by getting a handle to it
it's basically a direct wrapper around another handle
tho due to the fact that the defined class is hidden i've ran into some classloader pain
Can you not just invoke the original MH directly though? I'd expect the underlying LF to get specialised and the performance to be the same.
Though realise "I expect" without benchmarking is worth nothing :p.
no, LMF wants direct handles and only direct handles
Ooooh, that's what you're trying to do.
Gotcha then.
It feels like it would have been less code just to reimplement LMF rather than implement that :p.
MethodHandles.dropArguments(lookup.findVirtual(), 0, SomeParam.class) can't be used using lmf nor can
public static <T> MethodHandle createArrayIterationHandle(Class<T> type, Method target) throws Throwable {
final var arrayType = type.arrayType();
final var targetHandle = MethodHandles.publicLookup().unreflect(target);
// Get a method handle for the array's getter
MethodHandle getter = MethodHandles.arrayElementGetter(arrayType);
// The array getter but with reversed parameter order
MethodHandle reorderedGetter = MethodHandles.permuteArguments(getter, MethodType.methodType(type, int.class, arrayType), 1, 0);
// The target but with the loop position and array inserted after the instance parameter so that we can fold the array getter into the target
MethodHandle extraTarget = MethodHandles.dropArguments(targetHandle, 1, int.class, arrayType);
// The body of the loop. Calls the targetHandle on the array element at the position
MethodHandle body = MethodHandles.foldArguments(extraTarget, 0, reorderedGetter);
final Class<?>[] additionalArguments = target.getParameterTypes();
return MethodHandles.countedLoop(
MethodHandles.dropArguments(MethodHandles.constant(int.class, 0), 0, Lists.asList(arrayType, additionalArguments).toArray(Class[]::new)),
MethodHandles.dropArguments(MethodHandles.arrayLength(arrayType), 1, additionalArguments),
null,
body
);
}
it's an extremely annoying and arguably somewhat arbitrary limitation
considering lmf already has cases where it uses a classdata-injected handle and invokeexact's it instead of a INVOKEVIRTUAL/STATIC
Oh yeah, they could remove that limitation just by deleting code. It's definitely odd.
inb4 their reason for not doing it is backwards compat 
"we can't remove it because people that expect it to crash will not crash"
I guess the Javadoc does say it only accepts direct handles. The (arguably cleaner) alternative would be to add a version of MethodHandleProxies.asInterfaceInstance which generates classes per-MH rather than per-interface.
It was changed recently by a familiar face (https://github.com/openjdk/jdk/pull/13197), but not on a useful version of Java for us :).
At this point maybe require Java 22 for NeoForge? 😛
oh god
why did I make the mistake of looking at ProxyGenerator
it really shows how old the code is
Ah, interesting
I think the --add-opens is not a big deal heh
I thought that defineInnerClass needed package access, how did you get around that?
The generated class needs to be in the same module as the listener object IMO
Oh, why? IMO it doesn't really matter - it's a hidden class, so is largely unobservable in stack traces, etc...
Aren't there access restrictions if it's not in the right package? I should look at what exactly you're doing heh
Oh, it's not in the right package (it lives in the bus package), but I guess my feeling is that doesn't matter.
Well if it works it works 😛
Oh I just understood what you're doing heh
You're accessing the target via a static final MethodHandle which can be inlined by the jit
Really unfortunate that MethodHandleProxies is too slow atm, it looks like it does exactly what we need
Yep! It's exactly what LMF does for private methods, just for everything.
Oh I didn't notice that LMF was doing that
I think my preference goes to the add-opens arg over your solution, just because I already wrote it, tested it, and benchmarked it. But yours is perfectly valid! 😉
Oh yeah, that's completely fair. I'm irked by the poking of jdk internals, but me being irked is not a good reason to throw things away :p.
you could do benchmarks yourself and check if it'd work
I would eat my hat if the numbers are different between the two, but happy to if that would be useful.
It's more that I don't have too much time this weekend unfortunately heh
Yeah, I hadn't realised quite how long the benchmark suite took to ran :p.
I do in principle! But I think if you replace ASM (nasty) with poking JDK internals (also nasty!) I don't think it's much of a gain :p.
I think for me the main bit is getting rid of the mod launcher/access transformer code, which both approaches do :).
Yeah that's a nice benefit
Either way we can change to MethodHandleProxies in a few years ^^
I suppose ASM is less ugly
(at least it's a supported API)
We could generate the classes at compile time and remove the runtime ASM dependency (/s, I think).
Please no 😛
hmmm an AP for @vernal latch and @Mod.EventBusSubscriber 
can a moderator finally change this persons display name
I think this is Finn's secondary acc 
Just kick it
and it exists purely to spite us
Not especially convinced by how stable the performance is on my machine for benchmarking, but:
My branch (c418eb2b1805e5b71157eed04a26420297a7b5fc)
EventBusBenchmarkNoLoader.testNoLoaderCombined avgt 15 76.987 ± 0.831 ns/op
EventBusBenchmarkNoLoader.testNoLoaderDynamic avgt 15 34.868 ± 0.982 ns/op
EventBusBenchmarkNoLoader.testNoLoaderLambda avgt 15 27.928 ± 0.363 ns/op
EventBusBenchmarkNoLoader.testNoLoaderStatic avgt 15 31.809 ± 0.588 ns/op
Main (86e69cea88d0acb1b108b0ad4f7aa8bc5d08e4f6)
EventBusBenchmarkNoLoader.testNoLoaderCombined avgt 15 78.481 ± 1.641 ns/op
EventBusBenchmarkNoLoader.testNoLoaderDynamic avgt 15 33.836 ± 0.404 ns/op
EventBusBenchmarkNoLoader.testNoLoaderLambda avgt 15 29.738 ± 1.837 ns/op
EventBusBenchmarkNoLoader.testNoLoaderStatic avgt 15 33.147 ± 1.171 ns/op
soo can we have a direct comparison between just your impl and techs LMF impl
Yeah sure! Give me ... uh, 7 minutes.
That benchmark is suboptimal for performance testing
You should copy the "many listeners" benchmark from my PR
https://jmh.morethan.io/?gists=4aa8ef36773a536215d3e20c2cdb1a05,9174e95bf7350a96e0f532feaabe4d10 Well, hat eating is going to have to occur 😀. Fascinating, wonder what's going on.
I guess the main difference is that ASM needs an explicit cast for the instance, while LMF doesn't, and that's not really avoidable. Apologies for the diversion here then!
I'm a little concerned by that benchmark though - because there's only one listener, everything is going to get super specialised. Doing a quick test with interleaving three listener classes shows a 6x slowdown in those benchmarks.
Why would LMF not need a cast?
Yeah this is the peak performance
As in a cast on the instance object - the LMF class has a field of the receiver type (and so doesn't need a class), but the ASM just uses j.l.Object and casts within the method handle.
ASM could use the exact object class heh
Not if that class is package private, alas. You have to avoid all references to external types :/.
Oh yes, it's definitely a useful benchmark to have, but I also think it's worth doing something which has heterogenous listeners.
Can't you adapt the methodhandle to accept objects?
I thought that would elide the cast with the jit
Oh, so the methodhandle does accept objects, but it then needs to do a checked cast within the MH itself.
^ To be clear, I haven't dug out jitwatch to confirm this.
I've been doing some more fiddling, and I'm not sure how much you want to change/break the API here, but if it was okay to change IEventListener to be an abstract class, you can get some pretty reasonable gains in the heterogeneous listeners case (source code, if it's useful).
not an option we use it as lambdas

the fact that event listeners can be lambdas doesn't mean the wrapper can't be an abstract class
^ Oh yeah, to clarify .addListener accepts a Consumer rather than an IEventListener. From what I can tell, we don't construct IEventListener outside of Bus, but I may have missed something.
that looks reasonable to me
yea then try the abstract class thing
I don't think there's even IEventListener lambdas in eventbus atm
Sounds fine to me
If you'd PR to my PR that would be best cause there's also the other changes I made that I'd like to keep
It exists more as auto-complete these days, so I wouldn't worry about pinging it
PR to the PR PR'ing the PR
a lot of time is probably spent just casting the events
i wonder if there's a clean way to reduce the number of casts from the number of listeners to 1
The cast is probably optimized by the JIT
