Skip to content
Open
Show file tree
Hide file tree
Changes from 20 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,86 @@
/*
* 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 net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
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 {
/**
* Invoked when any custom click action is received. If you are only interested in listening to events with a
* specific ID, use {@link #customClickActionReceivedEvent(Identifier)}.
*/
public static final Event<CustomClickActionReceived> ON_ANY_CUSTOM_CLICK_ACTION_RECEIVED = EventFactory.createArrayBacked(
Copy link
Member

Choose a reason for hiding this comment

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

What is the example use case for this? Generally it seems like you should only care about your own specific click actions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had a usecase where I tracked where a user was in a particular flow using the ID of the event. But that is probably not a good way to handle that, adding to payload is better (eg with the additions field). Will remove.

CustomClickActionReceived.class,
listeners -> (id, context) -> {
for (CustomClickActionReceived listener : listeners) {
listener.handleCustomClickAction(id, context);
}
}
);

/**
* Gets an event that is invoked on the server when a custom click event is received during the PLAY phase. 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<NamedCustomClickActionReceived> customClickActionReceivedEvent(Identifier id) {
Objects.requireNonNull(id, "ID cannot be null");
return CustomClickActionsRegistry.getOrCreateActionEvent(id);
}

@FunctionalInterface
public interface CustomClickActionReceived {
/**
* Handles any 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.
*/
void handleCustomClickAction(Identifier id, CustomClickEventContext context);
}

@FunctionalInterface
public interface NamedCustomClickActionReceived {
/**
* Handles a custom click event on the server from a given context.
*
* <p>This event only works for click actions with a single ID registered with {@link #customClickActionReceivedEvent(Identifier)},
* for generic events see {@link #ON_ANY_CUSTOM_CLICK_ACTION_RECEIVED}.
*
* @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.
*/
void handleCustomClickAction(CustomClickEventContext context);
}

private CustomClickActionEvents() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.Optional;

import org.jetbrains.annotations.ApiStatus;

import net.minecraft.nbt.NbtElement;
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. If the event is
* received during PLAY, then a player entity will be provided.
*/
public sealed interface CustomClickEventContext permits CustomClickEventContext.Play, CustomClickEventContext.Configuration {
/**
* The handler responsible for the event.
*/
ServerCommonNetworkHandler handler();

/**
* The player entity responsible for the event, if in the play phase.
*/
Optional<ServerPlayerEntity> player();

/**
* The payload received with this event. If no payload is received, then this payload will be {@code null}.
*/
Optional<NbtElement> payload();

/**
* 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 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();

/**
* The configuration phase is too early for an entity to have been created, so an entity is never returned.
*
* @return Returns an empty optional.
*/
default Optional<ServerPlayerEntity> player() {
return Optional.empty();
}
}
}
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,81 @@
/*
* 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.server.network.ServerConfigurationNetworkHandler;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.server.network.ServerPlayerEntity;
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.NamedCustomClickActionReceived>> REGISTRY = new HashMap<>();

public static Event<CustomClickActionEvents.NamedCustomClickActionReceived> getOrCreateActionEvent(Identifier id) {
return REGISTRY.computeIfAbsent(
id,
idx -> {
return EventFactory.createArrayBacked(
CustomClickActionEvents.NamedCustomClickActionReceived.class,
listeners -> context -> {
for (CustomClickActionEvents.NamedCustomClickActionReceived listener : listeners) {
listener.handleCustomClickAction(context);
}
}
);
}
);
}

public static void invokeListenerEvent(Identifier id, CustomClickEventContext context) {
CustomClickActionEvents.ON_ANY_CUSTOM_CLICK_ACTION_RECEIVED.invoker().handleCustomClickAction(id, context);

Event<CustomClickActionEvents.NamedCustomClickActionReceived> event = REGISTRY.get(id);

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

public record PlayContextImpl(
ServerPlayNetworkHandler handler,
Optional<ServerPlayerEntity> player,
Optional<NbtElement> payload
) implements CustomClickEventContext.Play {
public PlayContextImpl(ServerPlayNetworkHandler handler, ServerPlayerEntity player, Optional<NbtElement> payload) {
this(handler, Optional.of(player), payload);
}
}

public record ConfigurationContextImpl(
ServerConfigurationNetworkHandler handler,
Optional<NbtElement> payload
) implements CustomClickEventContext.Configuration {
}

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 @@ -38,6 +40,7 @@
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 +200,14 @@ public void setReconfiguring() {
isReconfiguring = true;
}

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

private enum RegisterState {
NOT_SENT,
SENT,
Expand Down
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 @@ -35,6 +37,7 @@
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 +147,14 @@ public boolean requestedReconfigure() {
return requestedReconfigure;
}

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

private record ContextImpl(MinecraftServer server, ServerPlayNetworkHandler handler, PacketSender responseSender) implements ServerPlayNetworking.Context {
private ContextImpl {
Objects.requireNonNull(server, "server");
Expand Down
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());
}
}
Loading