Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a3cd3b7
implement a basic event system
TheDeathlyCow Jul 7, 2025
f877689
add sign block entity mixin to config and hopefully fix that weirdly …
TheDeathlyCow Jul 7, 2025
70d352e
consistent registry name
TheDeathlyCow Jul 7, 2025
7159fa7
refactor to use context objects and separate config/play events
TheDeathlyCow Jul 8, 2025
3db85ce
add a dialog test
TheDeathlyCow Jul 8, 2025
5e6b483
apply spotless checks
TheDeathlyCow Jul 8, 2025
745831a
fix blank line checkstyle violation
TheDeathlyCow Jul 8, 2025
517bf0c
add javadoc to api methods
TheDeathlyCow Jul 8, 2025
68fac72
add configuration phase tests
TheDeathlyCow Jul 8, 2025
65c8eba
add missing full stops to javadoc
TheDeathlyCow Jul 8, 2025
6819567
Merge branch '1.21.7' into custom-click-action-registry
TheDeathlyCow Jul 8, 2025
519d68e
use a mock dialog packet system for testing
TheDeathlyCow Jul 8, 2025
9d4734b
remove join event forcing dialog
TheDeathlyCow Jul 8, 2025
52caa7d
Merge branch 'custom-click-action-registry' of https://github.com/The…
TheDeathlyCow Jul 8, 2025
4049294
run spotlessapply
TheDeathlyCow Jul 8, 2025
25cf3a7
move tests into own class and use a command to enable configuration test
TheDeathlyCow Jul 14, 2025
66a1dc4
fix checkstyle
TheDeathlyCow Jul 14, 2025
d2ca80f
convert to a single event for the registry
TheDeathlyCow Jul 14, 2025
2503992
add an event to handle "any" custom click action being received
TheDeathlyCow Jul 14, 2025
4fc69f7
add override annotations to context
TheDeathlyCow Jul 14, 2025
05cc21b
remove player from event context
TheDeathlyCow Jul 27, 2025
3f593f0
move test play handling into same listener as config
TheDeathlyCow Jul 27, 2025
3f9130e
move context impls to respective addons
TheDeathlyCow Jul 27, 2025
2ac4575
remove any event and move create event to own method
TheDeathlyCow Jul 27, 2025
2c4a29f
clear up some javadoc
TheDeathlyCow Jul 27, 2025
991c6bd
move payload into event parameter
TheDeathlyCow Jul 27, 2025
e25b3f6
add player convenience method to the play context
TheDeathlyCow Jul 27, 2025
150a4d0
import style fixes
TheDeathlyCow Jul 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.api.networking.v1;

import java.util.Objects;
import java.util.Optional;

import net.minecraft.nbt.NbtElement;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.impl.networking.CustomClickActionsRegistry;

/**
* Events for listening to {@linkplain net.minecraft.text.ClickEvent.Custom custom click actions}, such as those invoked
* from a custom dialog.
*/
public final class CustomClickActionEvents {
/**
* Gets an event that is invoked on the server when a custom click event is received. The returned event will only
* be invoked when a click event is received with the given ID.
*
* @param id The of the ID click event to listen to.
* @return Returns an event that will be invoked when a click event with the given ID is received during the PLAY
* phase.
*/
public static Event<CustomClickActionReceived> customClickActionReceivedEvent(Identifier id) {
Objects.requireNonNull(id, "ID cannot be null");
return CustomClickActionsRegistry.getOrCreateActionEvent(id);
}

@FunctionalInterface
public interface CustomClickActionReceived {
/**
* Handles a custom click event on the server from a given context.
*
* @param context The context of the event, contains the handler responsible for the action and the payload.
* Will either be an instance of {@link CustomClickEventContext.Play} or
* {@link CustomClickEventContext.Configuration}, depending on when this event was invoked. This
* can be checked using switch-statement pattern matching (see testmod if unfamiliar with this
* syntax).
* @param payload The payload received with this event. If no payload is received, then this payload will be
* empty.
*/
void handleCustomClickAction(CustomClickEventContext context, Optional<NbtElement> payload);
}

private CustomClickActionEvents() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.api.networking.v1;

import org.jetbrains.annotations.ApiStatus;

import net.minecraft.server.network.ServerCommonNetworkHandler;
import net.minecraft.server.network.ServerConfigurationNetworkHandler;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.server.network.ServerPlayerEntity;

/**
* Contains data about a {@linkplain net.minecraft.text.ClickEvent.Custom custom click event} when one is received on
* the server. Custom click events may be received either during the PLAY or in CONFIGURATION phases.
*/
public sealed interface CustomClickEventContext permits CustomClickEventContext.Play, CustomClickEventContext.Configuration {
/**
* The handler responsible for the event.
*/
ServerCommonNetworkHandler handler();

/**
* The context data when a custom click event is received during the PLAY phase on the server.
*/
@ApiStatus.NonExtendable
non-sealed interface Play extends CustomClickEventContext {
/**
* The play handler responsible for the event.
*/
@Override
ServerPlayNetworkHandler handler();

/**
* The player responsible for the event.
*
* @return Returns exactly the same player as calling {@link ServerPlayNetworkHandler#getPlayer()} on the result
* of {@link #handler()}.
*/
default ServerPlayerEntity player() {
return handler().getPlayer();
}
}

/**
* The context data when a custom click event is received during the CONFIGURATION phase on the server.
*/
@ApiStatus.NonExtendable
non-sealed interface Configuration extends CustomClickEventContext {
/**
* The configuration handler responsible for the event.
*/
@Override
ServerConfigurationNetworkHandler handler();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
Expand All @@ -30,6 +31,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.minecraft.nbt.NbtElement;
import net.minecraft.util.Identifier;

/**
Expand Down Expand Up @@ -172,4 +174,7 @@ public final void handleDisconnect() {
* @return whether the channel is reserved
*/
protected abstract boolean isReservedChannel(Identifier channelName);

public void invokeCustomClickActionEvent(Identifier id, Optional<NbtElement> payload) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the default impl for this throw? Something is wrong if this is called but not overriden.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some other methods in the addon that are just declared as abstract but have empty implementations (like handleUnregistration).

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.impl.networking;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import net.minecraft.nbt.NbtElement;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.fabricmc.fabric.api.networking.v1.CustomClickActionEvents;
import net.fabricmc.fabric.api.networking.v1.CustomClickEventContext;

public final class CustomClickActionsRegistry {
private static final Map<Identifier, Event<CustomClickActionEvents.CustomClickActionReceived>> REGISTRY = new HashMap<>();

public static Event<CustomClickActionEvents.CustomClickActionReceived> getOrCreateActionEvent(Identifier id) {
return REGISTRY.computeIfAbsent(id, idx -> createNewEvent());
}

public static void invokeListenerEvent(Identifier id, CustomClickEventContext context, Optional<NbtElement> payload) {
Event<CustomClickActionEvents.CustomClickActionReceived> event = REGISTRY.get(id);

if (event != null) {
event.invoker().handleCustomClickAction(context, payload);
}
}

private static Event<CustomClickActionEvents.CustomClickActionReceived> createNewEvent() {
return EventFactory.createArrayBacked(
CustomClickActionEvents.CustomClickActionReceived.class,
listeners -> (context, payload) -> {
for (CustomClickActionEvents.CustomClickActionReceived listener : listeners) {
listener.handleCustomClickAction(context, payload);
}
}
);
}

private CustomClickActionsRegistry() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import io.netty.channel.ChannelFutureListener;
import org.jetbrains.annotations.Nullable;

import net.minecraft.nbt.NbtElement;
import net.minecraft.network.NetworkPhase;
import net.minecraft.network.packet.BrandCustomPayload;
import net.minecraft.network.packet.CustomPayload;
Expand All @@ -32,12 +34,14 @@
import net.minecraft.server.network.ServerConfigurationNetworkHandler;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.networking.v1.CustomClickEventContext;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.S2CConfigurationChannelEvents;
import net.fabricmc.fabric.api.networking.v1.ServerConfigurationConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerConfigurationNetworking;
import net.fabricmc.fabric.impl.networking.AbstractChanneledNetworkAddon;
import net.fabricmc.fabric.impl.networking.ChannelInfoHolder;
import net.fabricmc.fabric.impl.networking.CustomClickActionsRegistry;
import net.fabricmc.fabric.impl.networking.NetworkingImpl;
import net.fabricmc.fabric.impl.networking.RegistrationPayload;
import net.fabricmc.fabric.mixin.networking.accessor.ServerCommonNetworkHandlerAccessor;
Expand Down Expand Up @@ -197,6 +201,12 @@ public void setReconfiguring() {
isReconfiguring = true;
}

@Override
public void invokeCustomClickActionEvent(Identifier id, Optional<NbtElement> payload) {
CustomClickEventContext context = new ConfigurationContextImpl(this.handler);
CustomClickActionsRegistry.invokeListenerEvent(id, context, payload);
}

private enum RegisterState {
NOT_SENT,
SENT,
Expand All @@ -215,4 +225,7 @@ private record ContextImpl(MinecraftServer server, ServerConfigurationNetworkHan
Objects.requireNonNull(responseSender, "responseSender");
}
}

private record ConfigurationContextImpl(ServerConfigurationNetworkHandler handler) implements CustomClickEventContext.Configuration {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import net.minecraft.nbt.NbtElement;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.NetworkPhase;
import net.minecraft.network.packet.CustomPayload;
Expand All @@ -29,12 +31,14 @@
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.networking.v1.CustomClickEventContext;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.S2CPlayChannelEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.fabricmc.fabric.impl.networking.AbstractChanneledNetworkAddon;
import net.fabricmc.fabric.impl.networking.ChannelInfoHolder;
import net.fabricmc.fabric.impl.networking.CustomClickActionsRegistry;
import net.fabricmc.fabric.impl.networking.NetworkingImpl;
import net.fabricmc.fabric.impl.networking.RegistrationPayload;

Expand Down Expand Up @@ -144,6 +148,12 @@ public boolean requestedReconfigure() {
return requestedReconfigure;
}

@Override
public void invokeCustomClickActionEvent(Identifier id, Optional<NbtElement> payload) {
CustomClickEventContext context = new PlayContextImpl(this.handler);
CustomClickActionsRegistry.invokeListenerEvent(id, context, payload);
}

private record ContextImpl(MinecraftServer server, ServerPlayNetworkHandler handler, PacketSender responseSender) implements ServerPlayNetworking.Context {
private ContextImpl {
Objects.requireNonNull(server, "server");
Expand All @@ -156,4 +166,7 @@ public ServerPlayerEntity player() {
return handler.getPlayer();
}
}

public record PlayContextImpl(ServerPlayNetworkHandler handler) implements CustomClickEventContext.Play {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import net.minecraft.network.packet.CustomPayload;
import net.minecraft.network.packet.c2s.common.CommonPongC2SPacket;
import net.minecraft.network.packet.c2s.common.CustomClickActionC2SPacket;
import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket;
import net.minecraft.server.network.ServerCommonNetworkHandler;

Expand Down Expand Up @@ -55,4 +56,9 @@ private void onPlayPong(CommonPongC2SPacket packet, CallbackInfo ci) {
addon.onPong(packet.getParameter());
}
}

@Inject(method = "onCustomClickAction", at = @At("TAIL"))
protected void hookCustomClickActionEvent(CustomClickActionC2SPacket packet, CallbackInfo ci) {
getAddon().invokeCustomClickActionEvent(packet.id(), packet.payload());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.mixin.networking;

import java.util.Optional;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;

import net.minecraft.block.entity.SignBlockEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.nbt.NbtElement;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.networking.v1.CustomClickEventContext;
import net.fabricmc.fabric.impl.networking.CustomClickActionsRegistry;
import net.fabricmc.fabric.impl.networking.server.ServerPlayNetworkAddon;

@Mixin(SignBlockEntity.class)
public class SignBlockEntityMixin {
@WrapOperation(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a wrap operation and not just an inject?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an Inject would require capturing more locals

method = "runCommandClickEvent",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/MinecraftServer;handleCustomClickAction(Lnet/minecraft/util/Identifier;Ljava/util/Optional;)V"
)
)
private void hookCustomClickActionListener(MinecraftServer instance, Identifier id, Optional<NbtElement> payload, Operation<Void> original, @Local(argsOnly = true) PlayerEntity player) {
original.call(instance, id, payload);

if (player instanceof ServerPlayerEntity serverPlayer) {
CustomClickEventContext context = new ServerPlayNetworkAddon.PlayContextImpl(serverPlayer.networkHandler);
CustomClickActionsRegistry.invokeListenerEvent(id, context, payload);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"ServerConfigurationNetworkHandlerMixin",
"ServerLoginNetworkHandlerMixin",
"ServerPlayNetworkHandlerMixin",
"SignBlockEntityMixin",
"accessor.EntityTrackerAccessor",
"accessor.ServerCommonNetworkHandlerAccessor",
"accessor.ServerLoginNetworkHandlerAccessor",
Expand Down
Loading