Event unification and touch damage#2144
Conversation
|
@Argmaster I would like to hear your opinion on this. Is this in line with what you wanted to have in #2083? |
Argmaster
left a comment
There was a problem hiding this comment.
@Argmaster I would like to hear your opinion on this. Is this in line with what you wanted to have in #2083?
Partially, yes. I did a rough review for now and I have some thoughts, but I would want to make clear what is a simplification to avoid doing to much at once and what is a design choice. So, are we aiming to have full event loop setup, or are you planning to stop with what we have now (basically callbacks but more consistent)?
| } | ||
|
|
||
| pub fn touchBlocks(entity: main.server.Entity, hitBox: Box, side: main.utils.Side) void { | ||
| pub fn touchBlocks(entity: *main.server.Entity, hitBox: Box, side: main.utils.Side, deltaTime: f64) void { |
There was a problem hiding this comment.
So, a touch happens when you collide with block hitbox, hm?
I "touch" immediately brings "touch with your finger" instead of "touch with your face" idk about you.
There was a problem hiding this comment.
The sense of touch is present on your whole body.
For colliding into things (as in moving towards them) I would like to have separate functions. This could in theory also allow to move some physics effects like bouncing into the event handler.
There was a problem hiding this comment.
Hm, I guess I will let you cook.
| return result; | ||
| } | ||
|
|
||
| pub fn run(self: *@This(), params: main.events.ServerBlockEvent.Params) main.events.EventResult { |
There was a problem hiding this comment.
How is this event supposed to work? When I am allowed to use it? Can I use it in WE commands?
There was a problem hiding this comment.
You can use it in any context that uses a ServerBlockEvent, currently it's only used in random tick events, but you could make a world edit command that triggers a ServerBlockEvent based on a user-provided zon if you think that's useful.
There was a problem hiding this comment.
Is there any other replace event in blocks category? Why not replace.zig
There was a problem hiding this comment.
I could see a replace callback for bigger areas, like spheres or cubes. Like maybe you have a corruption block and want to spread it in all directions on random tick.
There was a problem hiding this comment.
Ok, its a valid argument.
src/events/events.zig
Outdated
| fn GenericEvent(_Params: type, list: type) type { | ||
| return struct { | ||
| data: *anyopaque, | ||
| runFunction: *const fn(self: *anyopaque, params: Params) main.events.EventResult, |
There was a problem hiding this comment.
There is a well established name for such things: callback
There was a problem hiding this comment.
Isn't callback generally the name of the entire object that holds the function pointer? At least that's how it was in java.
There was a problem hiding this comment.
There was a problem hiding this comment.
How would I call run then? It would seem inconsistent to have it called run in all but this one place.
There was a problem hiding this comment.
Well, with current implementation it is no longer an event, more of an event handler according to definition on Wikipedia. Event is just a piece of data without callback. If we rename this to event handler then run as a function to trigger it makes sense. Additionally I don't see a problem with runing a callback as an interpretation of what is happening.
There was a problem hiding this comment.
I'm wondering if maybe we should call the whole thing a Callback then. It's conceptually no different from a lambda callback that also captures extra parameters.
I'm not sure yet. I would like to avoid an event loop, since it does have a performance overhead (→needs to allocate and store the event data in a data structure), but we might need it anyways. |
|
Ok, then I need to gather my thoughts and write them down here, I will do that after work. |
src/events/events.zig
Outdated
| .runFunction = &ignoredRun, | ||
| }; | ||
|
|
||
| fn ignoredRun(_: *anyopaque, _: Params) EventResult { |
There was a problem hiding this comment.
Is that noop with a fancy name?
Argmaster
left a comment
There was a problem hiding this comment.
Ok, so this is quite different than the direction #2083 is proposing.
TL;DR; Event loop please, so callback is always called the same way.
Current Event is "just" an implementation of a stateful lambda. It unifies the interface of callbacks for blocks, which is great, they really needed that, but at the same time has typical callback issues - those callbacks have no guarantees where they will be called from (as in, which thread they are called from). Thus callee can never be sure what it can and what it cannot do. I best case scenario this means a bunch more mutex locks and unlocks, in worst case it means we won't be able to reuse that code, as depending on context necessary locks exclude each other.
Consider following case: You have an entity in ECS that can place or break a block (eg. upon death). Can you call place block from entity AI thread? Why does entity AI thread lock a chunk mutex? Even worse, why entity AI code has to bother with which replace block to call (since you have separation between server and client implementation). Entity AI would be happy if it would just schedule a block placement and would wait until that blocks appears where it asked it to appear after some time, checking for it every tick.
Same applies to playing sounds, spawning particles etc. Why does X care about action being immediate? Delaying it for 1, 5, 10 frames doesn't bother anyone, even better, it may allow us to optimize things automagically if events can be batched into single thing. We are not talking about actions in a tight loop in the middle of the rendering thread, we are talking about things that happen at human perception speed.
TL;DR; Events and Listeners as a separate system from Block system and storage.
Now back to genericness of the design. Current solution is closely coupled with block system for no reason at all. An alternative implementation would be to define Events to contain no static data (not zon configurable, only Params in current implementation terms) and Listeners to be configured with necessary zon configuration.
Regardless of looped or loop-less implementation, when an event is triggered it would be delivered to a listener dedicated to that event and said listener would check which block was delivered to it and act accordingly. Block would no longer know that it opens a GUI. Instead you would define listener as code and include a zon configuration for it that would contain all the necessary information for it to function, in format preferred by the listener implementation.
This means we can finally stop adding more and more complexity to blocks.zig and distribute it between smaller systems with smaller interfaces. My experience shows that spaghetti factor of a module grows exponentially with its size.
Final notes:
- what you are implementing is different design pattern called Command (as opposed to Observer I have suggested)
- I am far from saying PR is all bad, although my comments may have suggest otherwise. In current state its still a step in a good direction, but I think its too small of a step.
- I am convinced that this is not performance critical code and it should value readability and extensibility over performance. If someone is dedicated enough, this will always be a weak spot, for one reason or another (eg. inventory lock). I do not think we should throw performance out the window with O(n!) complexity, but we should split the responsibilities between different systems and in that case make the system as generic as possible. To my best knowledge it it possible to generalize it so its not directly tied to any of existing systems.
- I did recently listen to non-pessimisation and "Clean" Code, Horrible Performance and I do see he has a point. There are two "but" I would like point out tho:
- this is just a guideline. Guidelines should be always tested against priorities of the project and reality. Here reality is that for 99% of the time opening a chest or touching a cactus is not a performance critical path in the code (or rather it shouldn't be!)
- There is a big difference between using "clean code" practices at high level, when managing big systems and communication between them (like here) where overhead from the cleanness of the code is in single digits of execution time, compared to using vtables in the middle of rendering thread. Again, guidelines should be tested against reality, not followed blindly.
|
I think you are underestimating the performance impact of this. Yes touching a cactus is rare, but if we have a thousand entities touching the ground, and 10 different listeners that listen to this on-touch event, then that would be quite expensive. I agree that we should be careful about what context an event gets executed.
I do think the block type is the right place to put block-type specific event data. In my opinion the block should know what window it opens, and the user should be able to specify that in the block's zon file. |
|
Then I suppose I have nothing else to add. |
| pub fn getChild(self: *const ZonElement, key: []const u8) ZonElement { | ||
| if(self.* != .object) { | ||
| return .null; | ||
| } else { | ||
| if(self.* == .object) { | ||
| if(self.object.get(key)) |elem| { | ||
| return elem; | ||
| } | ||
| } | ||
| return .null; | ||
| } | ||
|
|
||
| pub fn getChildOrNull(self: *const ZonElement, key: []const u8) ?ZonElement { | ||
| if(self.* == .object) { | ||
| if(self.object.get(key)) |elem| { | ||
| return elem; | ||
| } else { | ||
| return .null; | ||
| } | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
| pub fn getChild(self: *const ZonElement, key: []const u8) ZonElement { | |
| if(self.* != .object) { | |
| return .null; | |
| } else { | |
| if(self.* == .object) { | |
| if(self.object.get(key)) |elem| { | |
| return elem; | |
| } | |
| } | |
| return .null; | |
| } | |
| pub fn getChildOrNull(self: *const ZonElement, key: []const u8) ?ZonElement { | |
| if(self.* == .object) { | |
| if(self.object.get(key)) |elem| { | |
| return elem; | |
| } else { | |
| return .null; | |
| } | |
| } | |
| return null; | |
| } | |
| pub fn getChild(self: *const ZonElement, key: []const u8) ZonElement { | |
| return self.getChildOrNull(key) orelse .null; | |
| } | |
| pub fn getChildOrNull(self: *const ZonElement, key: []const u8) ?ZonElement { | |
| if(self.* == .object) { | |
| return self.object.get(key) orelse .null; | |
| } | |
| return null; | |
| } |
| } | ||
|
|
||
| pub fn run(self: *@This(), params: main.events.BlockTouchCallback.Params) main.events.EventResult { | ||
| std.debug.assert(params.entity == &main.game.Player.super); // TODO: Implement on the server side |
There was a problem hiding this comment.
Todos are forbidden. Make an issue.
I think that for documentation purposes it would be appropriate to link it here in a comment, but that depends on the preference.
Argmaster
left a comment
There was a problem hiding this comment.
Based on my current state of knowledge it should be good to merge.
One thing to consider is having a thread local variable that identifies threads as client side and server side, so we can assert that things like server callbacks and client callbacks are invoked only on threads they are allowed to be invoked on.
src/callbacks/callbacks.zig
Outdated
| fn Callback(_Params: type, list: type) type { | ||
| return struct { | ||
| data: *anyopaque, | ||
| runFunction: *const fn(self: *anyopaque, params: Params) Result, |
There was a problem hiding this comment.
I see runFunction persists. I suspect that is your preference, and the decision is final.
If that is the case, I would want to make
a poll on discord to see what ideas wider audience would have (for scientific purposes ofc).
Given code snippet:
fn Callback(_Params: type, list: type) type { return struct { data: *anyopaque, <field-name>: *const fn(self: *anyopaque, params: Params) Result, ...What field name would you chose for
<field-name>
There was a problem hiding this comment.
go make the poll, we can wait a bit longer on this,
Unifies gui opening, tick events and on touch events to use the same system. This makes it more future proof and establishes a clear pattern to be used for events. Also while I was at it I decided to implement lava damage. - [x] Implement damage for more blocks: Cactus and magma brings us one step closer to PixelGuys#2083
Unifies gui opening, tick events and on touch events to use the same system. This makes it more future proof and establishes a clear pattern to be used for events. Also while I was at it I decided to implement lava damage. - [x] Implement damage for more blocks: Cactus and magma brings us one step closer to PixelGuys#2083

Unifies gui opening, tick events and on touch events to use the same system.
This makes it more future proof and establishes a clear pattern to be used for events.
Also while I was at it I decided to implement lava damage.
brings us one step closer to #2083