From 5b203f708102820ab708a38d02d17113ab940e4d Mon Sep 17 00:00:00 2001 From: AlexDev_ <56083016+alexdev03@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:35:16 +0200 Subject: [PATCH 1/4] [verified] fix: harden proxy messaging and request handling --- .../BukkitPAPIProxyBridge.java | 1 + .../BungeePAPIProxyBridge.java | 8 +- .../messenger/PluginMessageMessenger.java | 9 + common/build.gradle | 5 + .../papiproxybridge/PAPIProxyBridge.java | 20 +- .../papiproxybridge/api/PlaceholderAPI.java | 61 +++--- .../messenger/redis/RedisMessenger.java | 34 ++-- .../papiproxybridge/user/OnlineUser.java | 3 + .../papiproxybridge/PAPIProxyBridgeTest.java | 148 ++++++++++++++ .../api/PlaceholderAPITest.java | 185 ++++++++++++++++++ .../messenger/PluginMessageMessenger.java | 2 +- .../payload/ComponentPayload.java | 4 +- proxy/build.gradle | 2 + .../papiproxybridge/ProxyPAPIProxyBridge.java | 9 +- .../ProxyPAPIProxyBridgeTest.java | 130 ++++++++++++ .../messenger/PluginMessageMessenger.java | 6 +- 16 files changed, 567 insertions(+), 60 deletions(-) create mode 100644 common/src/test/java/net/william278/papiproxybridge/PAPIProxyBridgeTest.java create mode 100644 common/src/test/java/net/william278/papiproxybridge/api/PlaceholderAPITest.java create mode 100644 proxy/src/test/java/net/william278/papiproxybridge/ProxyPAPIProxyBridgeTest.java diff --git a/bukkit/src/main/java/net/william278/papiproxybridge/BukkitPAPIProxyBridge.java b/bukkit/src/main/java/net/william278/papiproxybridge/BukkitPAPIProxyBridge.java index f19b16a..2b3667b 100644 --- a/bukkit/src/main/java/net/william278/papiproxybridge/BukkitPAPIProxyBridge.java +++ b/bukkit/src/main/java/net/william278/papiproxybridge/BukkitPAPIProxyBridge.java @@ -88,6 +88,7 @@ public void onEnable() { @Override public void onDisable() { messenger.onDisable(); + executorService.shutdownNow(); } private void setupMetrics() { diff --git a/bungee/src/main/java/net/william278/papiproxybridge/BungeePAPIProxyBridge.java b/bungee/src/main/java/net/william278/papiproxybridge/BungeePAPIProxyBridge.java index 4ee720f..00aa3c2 100644 --- a/bungee/src/main/java/net/william278/papiproxybridge/BungeePAPIProxyBridge.java +++ b/bungee/src/main/java/net/william278/papiproxybridge/BungeePAPIProxyBridge.java @@ -22,7 +22,7 @@ import com.google.common.collect.Maps; import lombok.Getter; import lombok.Setter; -import net.md_5.bungee.api.event.PluginMessageEvent; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Plugin; @@ -79,10 +79,6 @@ public void onDisable() { getProxy().getPluginManager().unregisterListener(this); } - @EventHandler - public void onPluginMessageReceived(PluginMessageEvent event) { - this.handleMessage(this, event.getTag(), event.getData(), false); - } @EventHandler public void onJoin(PostLoginEvent event) { @@ -91,7 +87,7 @@ public void onJoin(PostLoginEvent event) { } @EventHandler - public void onQuit(PostLoginEvent event) { + public void onQuit(PlayerDisconnectEvent event) { final BungeeUser user = BungeeUser.adapt(event.getPlayer()); users.remove(user.getUniqueId()); PlaceholderAPI.clearCache(event.getPlayer().getUniqueId()); diff --git a/bungee/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java b/bungee/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java index dbdeaa7..d78a3bd 100644 --- a/bungee/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java +++ b/bungee/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java @@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor; import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.connection.Server; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; import net.william278.papiproxybridge.BungeePAPIProxyBridge; @@ -61,6 +62,14 @@ public void sendMessage(@NotNull UUID uuid, @NotNull String channel, byte @NotNu @EventHandler public void onPluginMessageReceived(net.md_5.bungee.api.event.PluginMessageEvent event) { + if (!event.getTag().equals(PAPIProxyBridge.getChannel(false)) + && !event.getTag().equals(PAPIProxyBridge.getComponentChannel(false))) { + return; + } + event.setCancelled(true); + if (!(event.getSender() instanceof Server)) { + return; + } plugin.handleMessage(plugin, event.getTag(), event.getData(), false); } diff --git a/common/build.gradle b/common/build.gradle index 5f20db0..a9a56b9 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -18,4 +18,9 @@ dependencies { compileOnly "net.kyori:adventure-text-serializer-gson:4.23.0" compileOnly 'io.lettuce:lettuce-core:6.5.5.RELEASE' compileOnly 'de.exlll:configlib-yaml:4.6.1' + + testImplementation 'com.google.guava:guava:33.4.8-jre' + testImplementation 'net.kyori:adventure-api:4.23.0' + testImplementation 'net.kyori:adventure-text-serializer-gson:4.23.0' + testImplementation 'de.exlll:configlib-yaml:4.6.1' } \ No newline at end of file diff --git a/common/src/main/java/net/william278/papiproxybridge/PAPIProxyBridge.java b/common/src/main/java/net/william278/papiproxybridge/PAPIProxyBridge.java index 91252dd..6bf8eb6 100644 --- a/common/src/main/java/net/william278/papiproxybridge/PAPIProxyBridge.java +++ b/common/src/main/java/net/william278/papiproxybridge/PAPIProxyBridge.java @@ -97,21 +97,19 @@ default void handleMessage(@NotNull PAPIProxyBridge plugin, @NotNull String chan return; } - final ByteArrayDataInput inputStream = ByteStreams.newDataInput(message); - final long mostSignificantBits = inputStream.readLong(); - final long leastSignificantBits = inputStream.readLong(); - final UUID uuid = new UUID(mostSignificantBits, leastSignificantBits); - final OnlineUser user = plugin.findPlayer(uuid).orElse(null); - if (user == null) { - return; - } - try { - final short messageLength = inputStream.readShort(); + final ByteArrayDataInput inputStream = ByteStreams.newDataInput(message); + final UUID uuid = new UUID(inputStream.readLong(), inputStream.readLong()); + final OnlineUser user = plugin.findPlayer(uuid).orElse(null); + if (user == null) { + return; + } + + final int messageLength = inputStream.readUnsignedShort(); final byte[] messageBody = new byte[messageLength]; inputStream.readFully(messageBody); user.handleMessage(plugin, Request.deserialize(messageBody), channel.equals(getComponentChannel(isRequest))); - } catch (IOException | ClassNotFoundException | IllegalStateException e) { + } catch (IOException | ClassNotFoundException | RuntimeException e) { plugin.log(Level.SEVERE, "Failed to fully read plugin message. Is PAPIProxyBridge up-to-date and installed on all servers?", e); } } diff --git a/common/src/main/java/net/william278/papiproxybridge/api/PlaceholderAPI.java b/common/src/main/java/net/william278/papiproxybridge/api/PlaceholderAPI.java index 1d2c0ee..fccae2f 100644 --- a/common/src/main/java/net/william278/papiproxybridge/api/PlaceholderAPI.java +++ b/common/src/main/java/net/william278/papiproxybridge/api/PlaceholderAPI.java @@ -57,8 +57,8 @@ public final class PlaceholderAPI { private static PAPIProxyBridge plugin; private final static ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1, r -> new Thread(r, "PAPIProxyBridge-PlaceholderAPI-ScheduledThread")); private final static String PLACEHOLDER_DELIMITER = "%%%-%%%"; - private final ConcurrentMap> cache; - private final ConcurrentMap> componentCache; + private final ConcurrentMap> cache; + private final ConcurrentMap> componentCache; private long requestTimeout = 1000; private long cacheExpiry = 30000; private int retryTimes = 3; @@ -116,11 +116,18 @@ public static void register(@NotNull PAPIProxyBridge plugin) { @ApiStatus.Internal public static void clearCache(@NotNull UUID player) { instances.forEach(instance -> { - instance.cache.remove(player); - instance.componentCache.remove(player); + instance.cache.keySet().removeIf(key -> key.contains(player)); + instance.componentCache.keySet().removeIf(key -> key.contains(player)); }); } + private record CacheKey(@NotNull UUID requester, @NotNull UUID formatFor) { + + private boolean contains(@NotNull UUID player) { + return requester.equals(player) || formatFor.equals(player); + } + } + @SuppressWarnings("unchecked") private static CompletableFuture orTimeoutAsync(CompletableFuture future, long timeout) { final CompletableFuture timeoutFuture = new CompletableFuture<>(); @@ -161,28 +168,33 @@ private CompletableFuture formatPlaceholders(@NotNull String text, @NotN if (!requester.isConnected()) { return CompletableFuture.completedFuture(text); } - if (cacheExpiry > 0 && cache.containsKey(formatFor) && cache.get(formatFor).containsKey(text)) { - return CompletableFuture.completedFuture(cache.get(formatFor).get(text)); + final CacheKey cacheKey = new CacheKey(requester.getUniqueId(), formatFor); + if (cacheExpiry > 0 && cache.containsKey(cacheKey) && cache.get(cacheKey).containsKey(text)) { + return CompletableFuture.completedFuture(cache.get(cacheKey).get(text)); } final CompletableFuture future = plugin.createRequest(text, requester, formatFor, false, requestTimeout); return orTimeoutAsync(future, requestTimeout).thenApply(formatted -> { - cache.computeIfAbsent(requester.getUniqueId(), uuid -> ExpiringMap.builder() + cache.computeIfAbsent(cacheKey, key -> ExpiringMap.builder() .expiration(cacheExpiry, TimeUnit.MILLISECONDS) .build()) .put(text, formatted); return formatted; - }).exceptionally(e -> { + }).exceptionallyCompose(e -> { if (!requester.isConnected()) { - return text; + return CompletableFuture.completedFuture(text); + } + + if (times > 0) { + return formatPlaceholders(text, requester, formatFor, times - 1); } if (checkLastError()) { - return text; + return CompletableFuture.completedFuture(text); } // Handle failed to format exceptions - if (e instanceof CompletionException || Arrays.stream(e.getSuppressed()) - .anyMatch(TimeoutException.class::isInstance)) { + final Throwable cause = e instanceof CompletionException ? e.getCause() : e; + if (cause instanceof TimeoutException) { plugin.log(Level.WARNING, ("Timed out formatting placeholders for %s after %sms." + "Is PAPIProxyBridge up-to-date and installed on all backend servers?") .formatted(requester.getUsername(), getRequestTimeout())); @@ -190,7 +202,7 @@ private CompletableFuture formatPlaceholders(@NotNull String text, @NotN plugin.log(Level.WARNING, "Failed to format placeholders for %s".formatted(requester.getUsername()), e); } - return text; + return CompletableFuture.completedFuture(text); }); } @@ -261,29 +273,34 @@ private CompletableFuture formatComponentPlaceholders(@NotNull String if (!requester.isConnected()) { return CompletableFuture.completedFuture(Component.text(text)); } - if (cacheExpiry > 0 && componentCache.containsKey(formatFor) && componentCache.get(formatFor).containsKey(text)) { - return CompletableFuture.completedFuture(componentCache.get(formatFor).get(text)); + final CacheKey cacheKey = new CacheKey(requester.getUniqueId(), formatFor); + if (cacheExpiry > 0 && componentCache.containsKey(cacheKey) && componentCache.get(cacheKey).containsKey(text)) { + return CompletableFuture.completedFuture(componentCache.get(cacheKey).get(text)); } final CompletableFuture future = plugin.createRequest(text, requester, formatFor, true, requestTimeout); return orTimeoutAsync(future, requestTimeout).thenApply(formatted -> { final Component deserialized = GsonComponentSerializer.gson().deserializeOr(formatted, Component.text(formatted)); - componentCache.computeIfAbsent(requester.getUniqueId(), uuid -> ExpiringMap.builder() + componentCache.computeIfAbsent(cacheKey, key -> ExpiringMap.builder() .expiration(cacheExpiry, TimeUnit.MILLISECONDS) .build()) .put(text, deserialized); return deserialized; - }).exceptionally(e -> { + }).exceptionallyCompose(e -> { if (!requester.isConnected()) { - return Component.text(text); + return CompletableFuture.completedFuture(Component.text(text)); + } + + if (times > 0) { + return formatComponentPlaceholders(text, requester, formatFor, times - 1); } if (checkLastError()) { - return Component.text(text); + return CompletableFuture.completedFuture(Component.text(text)); } // Handle failed to format exceptions - if (e instanceof CompletionException || Arrays.stream(e.getSuppressed()) - .anyMatch(TimeoutException.class::isInstance)) { + final Throwable cause = e instanceof CompletionException ? e.getCause() : e; + if (cause instanceof TimeoutException) { plugin.log(Level.WARNING, ("Timed out formatting placeholders for %s after %sms." + "Is PAPIProxyBridge up-to-date and installed on all backend servers?") .formatted(requester.getUsername(), getRequestTimeout())); @@ -291,7 +308,7 @@ private CompletableFuture formatComponentPlaceholders(@NotNull String plugin.log(Level.WARNING, "Failed to format placeholders for %s".formatted(requester.getUsername()), e); } - return Component.text(text); + return CompletableFuture.completedFuture(Component.text(text)); }); } diff --git a/common/src/main/java/net/william278/papiproxybridge/messenger/redis/RedisMessenger.java b/common/src/main/java/net/william278/papiproxybridge/messenger/redis/RedisMessenger.java index b6d569a..cf758fa 100644 --- a/common/src/main/java/net/william278/papiproxybridge/messenger/redis/RedisMessenger.java +++ b/common/src/main/java/net/william278/papiproxybridge/messenger/redis/RedisMessenger.java @@ -59,14 +59,13 @@ public RedisMessenger(@NotNull PAPIProxyBridge plugin, @NotNull Settings.RedisSe public void onEnable() { try { createClient(); + connection = client.connect(StringByteArrayCodec.INSTANCE); + listen(); } catch (Throwable e) { + onDisable(); plugin.log(Level.SEVERE, "Failed to establish connection with Redis. " + "Please check the supplied credentials in the config file", e); - return; } - - connection = client.connect(StringByteArrayCodec.INSTANCE); - listen(); } private void createClient() { @@ -74,13 +73,15 @@ private void createClient() { final Settings.RedisSettings.RedisSentinel sentinel = redisSettings.getSentinel(); if (sentinel.getNodes().isEmpty()) { - client = RedisClient.create(RedisURI.builder() + final RedisURI.Builder builder = RedisURI.builder() .withHost(credentials.getHost()) .withPort(credentials.getPort()) - .withPassword(credentials.getPassword() == null ? null : credentials.getPassword().toCharArray()) .withClientName(CLIENT_NAME) - .withSsl(credentials.isUseSsl()) - .build()); + .withSsl(credentials.isUseSsl()); + if (credentials.getPassword() != null && !credentials.getPassword().isEmpty()) { + builder.withPassword(credentials.getPassword()); + } + client = RedisClient.create(builder.build()); return; } @@ -93,27 +94,36 @@ private void createClient() { if (split.length != 2) { throw new IllegalArgumentException("Invalid sentinel node: " + node); } - builder.withSentinel(split[0], Integer.parseInt(split[1])); + if (sentinel.getPassword() == null || sentinel.getPassword().isEmpty()) { + builder.withSentinel(split[0], Integer.parseInt(split[1])); + } else { + builder.withSentinel(split[0], Integer.parseInt(split[1]), sentinel.getPassword()); + } }); builder.withClientName(CLIENT_NAME) .withSsl(credentials.isUseSsl()); + if (credentials.getPassword() != null && !credentials.getPassword().isEmpty()) { + builder.withPassword(credentials.getPassword()); + } client = RedisClient.create(builder.build()); } @Override public void onDisable() { + closed = true; try { - client.close(); + if (client != null) { + client.close(); + } } catch (Throwable ignored) { } - closed = true; } @Override public void sendMessage(@NotNull UUID uuid, @NotNull String channel, byte @NotNull [] message) { - if (closed) { + if (closed || connection == null) { return; } connection.async().publish(channel, message); diff --git a/common/src/main/java/net/william278/papiproxybridge/user/OnlineUser.java b/common/src/main/java/net/william278/papiproxybridge/user/OnlineUser.java index b2d39af..b3f1c38 100644 --- a/common/src/main/java/net/william278/papiproxybridge/user/OnlineUser.java +++ b/common/src/main/java/net/william278/papiproxybridge/user/OnlineUser.java @@ -44,6 +44,9 @@ default void sendMessage(@NotNull PAPIProxyBridge plugin, @NotNull Request reque try { final byte[] serializedRequest = request.serialize(); + if (serializedRequest.length > 0xffff) { + throw new IOException("Serialized request exceeds 65535 bytes"); + } messageWriter.writeShort(serializedRequest.length); messageWriter.write(serializedRequest); } catch (IOException e) { diff --git a/common/src/test/java/net/william278/papiproxybridge/PAPIProxyBridgeTest.java b/common/src/test/java/net/william278/papiproxybridge/PAPIProxyBridgeTest.java new file mode 100644 index 0000000..85f2738 --- /dev/null +++ b/common/src/test/java/net/william278/papiproxybridge/PAPIProxyBridgeTest.java @@ -0,0 +1,148 @@ +/* + * This file is part of PAPIProxyBridge, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * 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.william278.papiproxybridge; + +import net.william278.papiproxybridge.config.Settings; +import net.william278.papiproxybridge.messenger.Messenger; +import net.william278.papiproxybridge.user.OnlineUser; +import net.william278.papiproxybridge.user.Request; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PAPIProxyBridgeTest { + + @Test + void ignoresMalformedFrames() { + final TestBridge bridge = new TestBridge(); + assertDoesNotThrow(() -> bridge.handleMessage(bridge, PAPIProxyBridge.getChannel(true), new byte[0], true)); + } + + @Test + void readsUnsignedPayloadLengths() throws Exception { + final TestBridge bridge = new TestBridge(); + final String text = "x".repeat(32_720); + final byte[] request = new Request(text, bridge.user.getUniqueId()).serialize(); + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + final DataOutputStream output = new DataOutputStream(bytes); + final UUID userId = bridge.user.getUniqueId(); + output.writeLong(userId.getMostSignificantBits()); + output.writeLong(userId.getLeastSignificantBits()); + output.writeShort(request.length); + output.write(request); + + assertDoesNotThrow(() -> bridge.handleMessage(bridge, PAPIProxyBridge.getChannel(true), bytes.toByteArray(), true)); + assertEquals(text, bridge.user.message.getMessage()); + } + + private static final class TestBridge implements PAPIProxyBridge { + private final TestUser user = new TestUser(UUID.randomUUID()); + + @Override + public String getServerType() { + return "test"; + } + + @Override + public @NotNull Collection getOnlineUsers() { + return List.of(user); + } + + @Override + public Optional findPlayer(@NotNull UUID uuid) { + return user.getUniqueId().equals(uuid) ? Optional.of(user) : Optional.empty(); + } + + @Override + public CompletableFuture createRequest(@NotNull String text, @NotNull OnlineUser requester, + @NotNull UUID formatFor, boolean wantsJson, long requestTimeout) { + return CompletableFuture.completedFuture(text); + } + + @Override + public CompletableFuture> getServers(long requestTimeout) { + return CompletableFuture.completedFuture(Set.of()); + } + + @Override + public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... exceptions) { + } + + @Override + public File getDataFolder() { + return new File("."); + } + + @Override + public void setSettings(Settings settings) { + } + + @Override + public void loadMessenger() { + } + + @Override + public Messenger getMessenger() { + return null; + } + + @Override + public Settings getSettings() { + return new Settings(); + } + } + + private static final class TestUser implements OnlineUser { + private final UUID uuid; + private Request message; + + private TestUser(UUID uuid) { + this.uuid = uuid; + } + + @Override + public @NotNull String getUsername() { + return "user"; + } + + @Override + public @NotNull UUID getUniqueId() { + return uuid; + } + + @Override + public void handleMessage(@NotNull PAPIProxyBridge plugin, @NotNull Request message, boolean wantsJson) { + this.message = message; + } + } +} diff --git a/common/src/test/java/net/william278/papiproxybridge/api/PlaceholderAPITest.java b/common/src/test/java/net/william278/papiproxybridge/api/PlaceholderAPITest.java new file mode 100644 index 0000000..6cbc62f --- /dev/null +++ b/common/src/test/java/net/william278/papiproxybridge/api/PlaceholderAPITest.java @@ -0,0 +1,185 @@ +/* + * This file is part of PAPIProxyBridge, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * 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.william278.papiproxybridge.api; + +import net.kyori.adventure.text.Component; +import net.william278.papiproxybridge.PAPIProxyBridge; +import net.william278.papiproxybridge.config.Settings; +import net.william278.papiproxybridge.messenger.Messenger; +import net.william278.papiproxybridge.user.OnlineUser; +import net.william278.papiproxybridge.user.Request; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PlaceholderAPITest { + + private final TestUser requester = new TestUser(UUID.randomUUID()); + private TestBridge bridge; + private PlaceholderAPI api; + + @BeforeEach + void setUp() { + bridge = new TestBridge(requester); + PlaceholderAPI.register(bridge); + api = PlaceholderAPI.createInstance(); + api.setRequestTimeout(100); + } + + @Test + void cachesByRequesterAndFormattingTarget() { + final UUID target = UUID.randomUUID(); + bridge.response = "target"; + assertEquals("target", api.formatPlaceholders("%name%", requester, target).join()); + + bridge.response = "cached"; + assertEquals("target", api.formatPlaceholders("%name%", requester, target).join()); + + bridge.response = "requester"; + assertEquals("requester", api.formatPlaceholders("%name%", requester, requester.getUniqueId()).join()); + + bridge.response = "other requester"; + assertEquals("other requester", api.formatPlaceholders("%name%", new TestUser(UUID.randomUUID()), target).join()); + assertEquals(3, bridge.requests.get()); + } + + @Test + void componentCacheUsesRequesterAndFormattingTarget() { + final UUID target = UUID.randomUUID(); + bridge.response = "{\"text\":\"target\"}"; + assertEquals(Component.text("target"), api.formatComponentPlaceholders("%name%", requester, target).join()); + + bridge.response = "{\"text\":\"cached\"}"; + assertEquals(Component.text("target"), api.formatComponentPlaceholders("%name%", requester, target).join()); + + bridge.response = "{\"text\":\"requester\"}"; + assertEquals(Component.text("requester"), api.formatComponentPlaceholders("%name%", requester, requester.getUniqueId()).join()); + + bridge.response = "{\"text\":\"other requester\"}"; + assertEquals(Component.text("other requester"), api.formatComponentPlaceholders("%name%", new TestUser(UUID.randomUUID()), target).join()); + assertEquals(3, bridge.requests.get()); + } + + @Test + void retriesFailedRequests() { + api.setRetryTimes(1); + bridge.failures = 1; + bridge.response = "formatted"; + + assertEquals("formatted", api.formatPlaceholders("%name%", requester).join()); + assertEquals(2, bridge.requests.get()); + } + + private static final class TestBridge implements PAPIProxyBridge { + private final TestUser user; + private final AtomicInteger requests = new AtomicInteger(); + private String response; + private int failures; + + private TestBridge(TestUser user) { + this.user = user; + } + + @Override + public String getServerType() { + return "test"; + } + + @Override + public @NotNull Collection getOnlineUsers() { + return List.of(user); + } + + @Override + public Optional findPlayer(@NotNull UUID uuid) { + return user.getUniqueId().equals(uuid) ? Optional.of(user) : Optional.empty(); + } + + @Override + public CompletableFuture createRequest(@NotNull String text, @NotNull OnlineUser requester, + @NotNull UUID formatFor, boolean wantsJson, long requestTimeout) { + requests.incrementAndGet(); + if (failures-- > 0) { + return CompletableFuture.failedFuture(new IllegalStateException("failed")); + } + return CompletableFuture.completedFuture(response); + } + + @Override + public CompletableFuture> getServers(long requestTimeout) { + return CompletableFuture.completedFuture(Set.of()); + } + + @Override + public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... exceptions) { + } + + @Override + public File getDataFolder() { + return new File("."); + } + + @Override + public void setSettings(Settings settings) { + } + + @Override + public void loadMessenger() { + } + + @Override + public Messenger getMessenger() { + return null; + } + + @Override + public Settings getSettings() { + return new Settings(); + } + } + + private record TestUser(UUID uuid) implements OnlineUser { + @Override + public @NotNull String getUsername() { + return "requester"; + } + + @Override + public @NotNull UUID getUniqueId() { + return uuid; + } + + @Override + public void handleMessage(@NotNull PAPIProxyBridge plugin, @NotNull Request message, boolean wantsJson) { + } + } +} diff --git a/fabric/1.20.1/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java b/fabric/1.20.1/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java index bf2e16b..a730ed6 100644 --- a/fabric/1.20.1/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java +++ b/fabric/1.20.1/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java @@ -59,7 +59,7 @@ public void sendMessage(@NotNull UUID uuid, @NotNull String channel, byte @NotNu } final FabricUser user = optionalFabricUser.get(); - final TemplatePayload templatePayload = channel.equals(ComponentPayload.REQUEST_ID.toString()) ? + final TemplatePayload templatePayload = channel.equals(ComponentPayload.RESPONSE_ID.toString()) ? new ComponentPayload(message, false) : new LiteralPayload(message, false); diff --git a/fabric/src/main/java/net/william278/papiproxybridge/payload/ComponentPayload.java b/fabric/src/main/java/net/william278/papiproxybridge/payload/ComponentPayload.java index 7dea497..a41da8f 100644 --- a/fabric/src/main/java/net/william278/papiproxybridge/payload/ComponentPayload.java +++ b/fabric/src/main/java/net/william278/papiproxybridge/payload/ComponentPayload.java @@ -27,8 +27,8 @@ public final class ComponentPayload extends TemplatePayload { - public static final CustomPayload.Id REQUEST_ID = new CustomPayload.Id<>(Identifier.of(PAPIProxyBridge.getComponentChannel(false))); - public static final CustomPayload.Id RESPONSE_ID = new CustomPayload.Id<>(Identifier.of(PAPIProxyBridge.getComponentChannel(true))); + public static final CustomPayload.Id REQUEST_ID = new CustomPayload.Id<>(Identifier.of(PAPIProxyBridge.getComponentChannel(true))); + public static final CustomPayload.Id RESPONSE_ID = new CustomPayload.Id<>(Identifier.of(PAPIProxyBridge.getComponentChannel(false))); public static final PacketCodec CODEC = PacketCodec.of((value, buf) -> writeBytes(buf, value.bytes), ComponentPayload::new); private final boolean isRequest; diff --git a/proxy/build.gradle b/proxy/build.gradle index d87a9f1..bf393d6 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -5,6 +5,8 @@ dependencies { compileOnly 'org.jetbrains:annotations:26.0.2' compileOnly 'io.lettuce:lettuce-core:6.5.5.RELEASE' compileOnly 'de.exlll:configlib-yaml:4.6.1' + + testImplementation 'com.google.guava:guava:33.4.8-jre' } shadowJar { diff --git a/proxy/src/main/java/net/william278/papiproxybridge/ProxyPAPIProxyBridge.java b/proxy/src/main/java/net/william278/papiproxybridge/ProxyPAPIProxyBridge.java index 1598a99..492f575 100644 --- a/proxy/src/main/java/net/william278/papiproxybridge/ProxyPAPIProxyBridge.java +++ b/proxy/src/main/java/net/william278/papiproxybridge/ProxyPAPIProxyBridge.java @@ -53,10 +53,8 @@ default CompletableFuture createRequest(@NotNull String text, @NotNull O final Request request = new Request(text, formatFor); final CompletableFuture future = new CompletableFuture<>(); getRequests().put(request.getUuid(), future); - future.exceptionallyAsync(throwable -> { - getRequests().remove(request.getUuid()); - return text; - }); + future.orTimeout(requestTimeout, TimeUnit.MILLISECONDS) + .whenComplete((result, throwable) -> getRequests().remove(request.getUuid())); requester.sendMessage(this, request, wantsJson, true); return future; } @@ -69,7 +67,8 @@ default CompletableFuture> getServers(long requestTimeout) { .filter(OnlineUser::isConnected) .collect(() -> Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet), (map, user) -> map.put(user.getServerName(), createRequest(HANDSHAKE_PLACEHOLDER, user, user.getUniqueId(), false, requestTimeout) - .thenApply(message -> message.equals(HANDSHAKE_RESPONSE))), + .thenApply(message -> message.equals(HANDSHAKE_RESPONSE)) + .exceptionally(throwable -> false)), Multimap::putAll); CompletableFuture.allOf(serverMap.values().toArray(new CompletableFuture[0])) diff --git a/proxy/src/test/java/net/william278/papiproxybridge/ProxyPAPIProxyBridgeTest.java b/proxy/src/test/java/net/william278/papiproxybridge/ProxyPAPIProxyBridgeTest.java new file mode 100644 index 0000000..a108cc0 --- /dev/null +++ b/proxy/src/test/java/net/william278/papiproxybridge/ProxyPAPIProxyBridgeTest.java @@ -0,0 +1,130 @@ +/* + * This file is part of PAPIProxyBridge, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * 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.william278.papiproxybridge; + +import net.william278.papiproxybridge.config.Settings; +import net.william278.papiproxybridge.messenger.Messenger; +import net.william278.papiproxybridge.user.OnlineUser; +import net.william278.papiproxybridge.user.ProxyUser; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ProxyPAPIProxyBridgeTest { + + @Test + void serverLookupHonorsRequestTimeout() throws Exception { + final TestBridge bridge = new TestBridge(); + + assertEquals(Set.of(), bridge.getServers(10).get(1, TimeUnit.SECONDS)); + assertTrue(bridge.requests.isEmpty()); + } + + private static final class TestBridge implements ProxyPAPIProxyBridge { + private final ConcurrentMap> requests = new ConcurrentHashMap<>(); + private final TestUser user = new TestUser(); + private final Settings settings = new Settings(); + private final Messenger messenger = new Messenger() { + @Override + public void sendMessage(@NotNull UUID uuid, @NotNull String channel, byte @NotNull [] message) { + } + }; + + @Override + public @NotNull ConcurrentMap> getRequests() { + return requests; + } + + @Override + public String getServerType() { + return "test"; + } + + @Override + public @NotNull Collection getOnlineUsers() { + return List.of(user); + } + + @Override + public Optional findPlayer(@NotNull UUID uuid) { + return user.getUniqueId().equals(uuid) ? Optional.of(user) : Optional.empty(); + } + + @Override + public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... exceptions) { + } + + @Override + public File getDataFolder() { + return new File("."); + } + + @Override + public void setSettings(Settings settings) { + } + + @Override + public void loadMessenger() { + } + + @Override + public Messenger getMessenger() { + return messenger; + } + + @Override + public Settings getSettings() { + return settings; + } + } + + private static final class TestUser implements ProxyUser { + private final UUID uuid = UUID.randomUUID(); + + @Override + public @NotNull String getUsername() { + return "user"; + } + + @Override + public @NotNull UUID getUniqueId() { + return uuid; + } + + @Override + public @NotNull String getServerName() { + return "backend"; + } + } +} diff --git a/velocity/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java b/velocity/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java index 2fb5879..17e547c 100644 --- a/velocity/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java +++ b/velocity/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java @@ -22,6 +22,7 @@ import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import lombok.RequiredArgsConstructor; @@ -75,8 +76,11 @@ public void onPluginMessageReceived(@NotNull PluginMessageEvent event) { return; } - plugin.handleMessage(plugin, event.getIdentifier().getId(), event.getData(), false); event.setResult(PluginMessageEvent.ForwardResult.handled()); + if (!(event.getSource() instanceof ServerConnection)) { + return; + } + plugin.handleMessage(plugin, event.getIdentifier().getId(), event.getData(), false); } @Override From bdb252a19d3579697e7a763e04b3b417091ded17 Mon Sep 17 00:00:00 2001 From: AlexDev_ <56083016+alexdev03@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:16:14 +0200 Subject: [PATCH 2/4] [verified] feat: add Fabric 1.21.10 through 26.2 support --- build.gradle | 41 +++++++++---- fabric/1.21.10/gradle.properties | 6 ++ fabric/1.21.11/gradle.properties | 6 ++ fabric/26.1.2/gradle.properties | 8 +++ fabric/26.2/gradle.properties | 8 +++ fabric/build.gradle | 55 ++++++++++++++---- fabric/mainProject | 2 +- fabric/root.gradle | 16 +++-- .../FabricPAPIProxyBridge.java | 29 ++++++++-- .../messenger/PluginMessageMessenger.java | 35 ++++++----- .../payload/ComponentPayload.java | 39 ++++++++++--- .../payload/LiteralPayload.java | 42 ++++++++++---- .../payload/TemplatePayload.java | 27 +++++++-- .../papiproxybridge/user/FabricUser.java | 58 ++++++++++++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 2 +- 16 files changed, 290 insertions(+), 86 deletions(-) create mode 100644 fabric/1.21.10/gradle.properties create mode 100644 fabric/1.21.11/gradle.properties create mode 100644 fabric/26.1.2/gradle.properties create mode 100644 fabric/26.2/gradle.properties diff --git a/build.gradle b/build.gradle index 791bad0..819a972 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { id 'com.gradleup.shadow' version '9.0.0-beta17' - id 'org.cadixdev.licenser' version '0.6.1' apply false - id 'dev.architectury.loom' version '1.9-SNAPSHOT' apply false + id 'dev.architectury.loom' version '1.14-SNAPSHOT' apply false + id 'dev.architectury.loom-no-remap' version '1.14-SNAPSHOT' apply false id 'gg.essential.multi-version.root' apply false id 'org.ajoberstar.grgit' version '5.3.2' id 'maven-publish' @@ -57,7 +57,6 @@ allprojects { } apply plugin: 'com.gradleup.shadow' - apply plugin: 'org.cadixdev.licenser' apply plugin: 'java' compileJava.options.encoding = 'UTF-8' @@ -93,10 +92,26 @@ allprojects { useJUnitPlatform() } - license { - header = rootProject.file('HEADER') - include '**/*.java' - newLine = true + def licenseHeader = '/*\n' + rootProject.file('HEADER').readLines().collect { + " *${it ? " $it" : ''}" + }.join('\n') + '\n */' + def javaSources = { fileTree(projectDir) { include 'src/**/*.java' } } + + tasks.register('licenseCheck') { + doLast { + def missing = javaSources().findAll { !it.text.startsWith(licenseHeader) } + if (!missing.isEmpty()) { + throw new GradleException("Missing license header: ${missing*.path.join(', ')}") + } + } + } + + tasks.register('licenseFormat') { + doLast { + javaSources().findAll { !it.text.startsWith(licenseHeader) }.each { + it.text = "${licenseHeader}\n\n${it.text}" + } + } } processResources { @@ -127,11 +142,17 @@ subprojects { } else { name += "-${project.name.capitalize()}" } - archivesBaseName = name + base { + archivesName = name + } if (project.parent?.name?.equals('fabric')) { - apply plugin: 'dev.architectury.loom' - compileJava.options.release.set (project.name == '1.20.1' ? 17 : 21) // 1.20.1 requires Java 17 + def fabricMajorVersion = project.name.split('\\.')[0] as int + apply plugin: fabricMajorVersion >= 26 ? 'dev.architectury.loom-no-remap' : 'dev.architectury.loom' + compileJava.options.release.set(project.name == '1.20.1' ? 17 : fabricMajorVersion >= 26 ? 25 : 21) + if (fabricMajorVersion >= 26) { + java.toolchain.languageVersion.set(JavaLanguageVersion.of(25)) + } version += "+mc.${project.name}" } diff --git a/fabric/1.21.10/gradle.properties b/fabric/1.21.10/gradle.properties new file mode 100644 index 0000000..c86896e --- /dev/null +++ b/fabric/1.21.10/gradle.properties @@ -0,0 +1,6 @@ +essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.10+build.3:v2 + +fabric_loader_version=0.19.3 +fabric_api_version=0.138.4+1.21.10 +fabric_adventure_platform_version=6.7.0 +fabric_placeholder_api_version=2.8.2+1.21.10 diff --git a/fabric/1.21.11/gradle.properties b/fabric/1.21.11/gradle.properties new file mode 100644 index 0000000..c559311 --- /dev/null +++ b/fabric/1.21.11/gradle.properties @@ -0,0 +1,6 @@ +essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.11+build.6:v2 + +fabric_loader_version=0.19.3 +fabric_api_version=0.141.4+1.21.11 +fabric_adventure_platform_version=6.8.0 +fabric_placeholder_api_version=2.8.2+1.21.10 diff --git a/fabric/26.1.2/gradle.properties b/fabric/26.1.2/gradle.properties new file mode 100644 index 0000000..e7776b1 --- /dev/null +++ b/fabric/26.1.2/gradle.properties @@ -0,0 +1,8 @@ +essential.defaults.loom.minecraft=com.mojang:minecraft:26.1.2 +essential.defaults.loom.mappings= +fabric.loom.disableObfuscation=true + +fabric_loader_version=0.19.3 +fabric_api_version=0.154.2+26.1.2 +fabric_adventure_platform_version=6.9.0 +fabric_placeholder_api_version=3.0.0+26.1 diff --git a/fabric/26.2/gradle.properties b/fabric/26.2/gradle.properties new file mode 100644 index 0000000..5591983 --- /dev/null +++ b/fabric/26.2/gradle.properties @@ -0,0 +1,8 @@ +essential.defaults.loom.minecraft=com.mojang:minecraft:26.2 +essential.defaults.loom.mappings= +fabric.loom.disableObfuscation=true + +fabric_loader_version=0.19.3 +fabric_api_version=0.154.2+26.2 +fabric_adventure_platform_version=7.1.0 +fabric_placeholder_api_version=3.1.0-beta.1+26.2 diff --git a/fabric/build.gradle b/fabric/build.gradle index ed36b29..d39acbd 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -14,9 +14,15 @@ dependencies { shadow implementation ('io.lettuce:lettuce-core:6.5.5.RELEASE') shadow implementation ('de.exlll:configlib-yaml:4.6.1') - modImplementation include("net.kyori:adventure-platform-fabric:${fabric_adventure_platform_version}") - modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" - modImplementation include("eu.pb4:placeholder-api:${fabric_placeholder_api_version}") + if (platform.isUnobfuscated) { + implementation include("net.kyori:adventure-platform-fabric:${fabric_adventure_platform_version}") + compileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" + implementation include("eu.pb4:placeholder-api:${fabric_placeholder_api_version}") + } else { + modImplementation include("net.kyori:adventure-platform-fabric:${fabric_adventure_platform_version}") + modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" + modImplementation include("eu.pb4:placeholder-api:${fabric_placeholder_api_version}") + } compileOnly "net.kyori:adventure-text-serializer-gson:4.23.0" @@ -31,6 +37,7 @@ processResources { fabric_minecraft_version: project.name, ]) } + } shadowJar { @@ -51,14 +58,38 @@ shadowJar { minimize() } -remapJar { - dependsOn tasks.shadowJar - mustRunAfter tasks.shadowJar - inputFile = shadowJar.archiveFile.get() - addNestedDependencies = true +if (!platform.isUnobfuscated) { + remapJar { + dependsOn tasks.shadowJar + mustRunAfter tasks.shadowJar + inputFile = shadowJar.archiveFile.get() + addNestedDependencies = true - destinationDirectory.set(file("$rootDir/target/")) - archiveClassifier.set('') -} + destinationDirectory.set(file("$rootDir/target/")) + archiveClassifier.set('') + } -shadowJar.finalizedBy(remapJar) \ No newline at end of file + shadowJar.finalizedBy(remapJar) +} else { + // ponytail: 26.x ships shadowJar directly; Loom's redundant raw jar re-nests include dependencies. + tasks.jar.enabled = false + + shadowJar { + dependsOn tasks.processIncludeJars + destinationDirectory.set(file("$rootDir/target/")) + archiveClassifier.set('') + + from(tasks.processIncludeJars.outputs.files) { + into 'META-INF/jars' + } + + filesMatching('fabric.mod.json') { + filter { line -> + line == '}' ? " ,\"jars\": [\n" + + " {\"file\": \"META-INF/jars/adventure-platform-fabric-${fabric_adventure_platform_version}.jar\"},\n" + + " {\"file\": \"META-INF/jars/placeholder-api-${fabric_placeholder_api_version}.jar\"}\n" + + " ]\n}" : line + } + } + } +} \ No newline at end of file diff --git a/fabric/mainProject b/fabric/mainProject index db98d22..4ae8c75 100644 --- a/fabric/mainProject +++ b/fabric/mainProject @@ -1,2 +1,2 @@ -1.21.8 +26.2 diff --git a/fabric/root.gradle b/fabric/root.gradle index 4ce1656..71c6122 100644 --- a/fabric/root.gradle +++ b/fabric/root.gradle @@ -3,6 +3,10 @@ plugins { } preprocess { + def fabric260200 = createNode("26.2", 260200, "yarn") + def fabric260102 = createNode("26.1.2", 260102, "yarn") + def fabric12111 = createNode("1.21.11", 12111, "yarn") + def fabric12110 = createNode("1.21.10", 12110, "yarn") def fabric12108 = createNode("1.21.8", 12108, "yarn") def fabric12105 = createNode("1.21.5", 12105, "yarn") def fabric12104 = createNode("1.21.4", 12104, "yarn") @@ -10,8 +14,12 @@ preprocess { def fabric12100 = createNode("1.20.1", 12100, "yarn") strictExtraMappings.set(true) - fabric12105.link(fabric12108, null) - fabric12104.link(fabric12108, null) - fabric12101.link(fabric12108, null) - fabric12100.link(fabric12108, null) + fabric260102.link(fabric260200, null) + fabric12111.link(fabric260200, null) + fabric12110.link(fabric260200, null) + fabric12108.link(fabric260200, null) + fabric12105.link(fabric260200, null) + fabric12104.link(fabric260200, null) + fabric12101.link(fabric260200, null) + fabric12100.link(fabric260200, null) } \ No newline at end of file diff --git a/fabric/src/main/java/net/william278/papiproxybridge/FabricPAPIProxyBridge.java b/fabric/src/main/java/net/william278/papiproxybridge/FabricPAPIProxyBridge.java index d841f3e..4c2587a 100644 --- a/fabric/src/main/java/net/william278/papiproxybridge/FabricPAPIProxyBridge.java +++ b/fabric/src/main/java/net/william278/papiproxybridge/FabricPAPIProxyBridge.java @@ -27,7 +27,11 @@ import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.text.Text; +//#if MC>=260102 +import net.minecraft.network.chat.Component; +//#else +//$$ import net.minecraft.text.Text; +//#endif import net.william278.papiproxybridge.api.PlaceholderAPI; import net.william278.papiproxybridge.config.Settings; import net.william278.papiproxybridge.messenger.Messenger; @@ -72,7 +76,11 @@ private void handleEvents() { FabricUser user = FabricUser.adapt(handler.player); fabricUsers.put(user.getUniqueId(), user); }); - ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> fabricUsers.remove(handler.player.getUuid())); +//#if MC>=260102 + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> fabricUsers.remove(handler.player.getUUID())); +//#else +//$$ ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> fabricUsers.remove(handler.player.getUuid())); +//#endif } @Override @@ -140,10 +148,19 @@ public void loadMessenger() { } @NotNull - public final Text formatPlaceholders(@NotNull UUID formatFor, @NotNull FabricUser requester, @NotNull String text) { +//#if MC>=260102 + public final Component formatPlaceholders(@NotNull UUID formatFor, @NotNull FabricUser requester, @NotNull String text) { +//#else +//$$ public final Text formatPlaceholders(@NotNull UUID formatFor, @NotNull FabricUser requester, @NotNull String text) { +//#endif text = text.replaceAll(HANDSHAKE_PLACEHOLDER, HANDSHAKE_RESPONSE); - return Placeholders.parseText(Text.of(text), PlaceholderContext.of( - findPlayer(formatFor).orElse(requester).player()) - ); +//#if MC>=260102 + return Placeholders.SERVER_PLACEHOLDER_PARSER.parseComponent(text, PlaceholderContext.of( + findPlayer(formatFor).orElse(requester).player()).asParserContext()); +//#else +//$$ return Placeholders.parseText(Text.of(text), PlaceholderContext.of( +//$$ findPlayer(formatFor).orElse(requester).player()) +//$$ ); +//#endif } } diff --git a/fabric/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java b/fabric/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java index c66fee7..b32e372 100644 --- a/fabric/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java +++ b/fabric/src/main/java/net/william278/papiproxybridge/messenger/PluginMessageMessenger.java @@ -22,9 +22,6 @@ import lombok.RequiredArgsConstructor; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; -import net.minecraft.network.packet.CustomPayload; -import net.minecraft.network.packet.Packet; -import net.minecraft.network.packet.s2c.common.CustomPayloadS2CPacket; import net.william278.papiproxybridge.FabricPAPIProxyBridge; import net.william278.papiproxybridge.payload.ComponentPayload; import net.william278.papiproxybridge.payload.LiteralPayload; @@ -41,14 +38,25 @@ public class PluginMessageMessenger extends Messenger { @Override public void onEnable() { - PayloadTypeRegistry.playC2S().register(LiteralPayload.RESPONSE_ID, LiteralPayload.CODEC); - PayloadTypeRegistry.playS2C().register(LiteralPayload.RESPONSE_ID, LiteralPayload.CODEC); - PayloadTypeRegistry.playS2C().register(LiteralPayload.REQUEST_ID, LiteralPayload.CODEC); - PayloadTypeRegistry.playC2S().register(LiteralPayload.REQUEST_ID, LiteralPayload.CODEC); - PayloadTypeRegistry.playC2S().register(ComponentPayload.RESPONSE_ID, ComponentPayload.CODEC); - PayloadTypeRegistry.playS2C().register(ComponentPayload.RESPONSE_ID, ComponentPayload.CODEC); - PayloadTypeRegistry.playS2C().register(ComponentPayload.REQUEST_ID, ComponentPayload.CODEC); - PayloadTypeRegistry.playC2S().register(ComponentPayload.REQUEST_ID, ComponentPayload.CODEC); +//#if MC>=260102 + PayloadTypeRegistry.serverboundPlay().register(LiteralPayload.RESPONSE_ID, LiteralPayload.CODEC); + PayloadTypeRegistry.clientboundPlay().register(LiteralPayload.RESPONSE_ID, LiteralPayload.CODEC); + PayloadTypeRegistry.clientboundPlay().register(LiteralPayload.REQUEST_ID, LiteralPayload.CODEC); + PayloadTypeRegistry.serverboundPlay().register(LiteralPayload.REQUEST_ID, LiteralPayload.CODEC); + PayloadTypeRegistry.serverboundPlay().register(ComponentPayload.RESPONSE_ID, ComponentPayload.CODEC); + PayloadTypeRegistry.clientboundPlay().register(ComponentPayload.RESPONSE_ID, ComponentPayload.CODEC); + PayloadTypeRegistry.clientboundPlay().register(ComponentPayload.REQUEST_ID, ComponentPayload.CODEC); + PayloadTypeRegistry.serverboundPlay().register(ComponentPayload.REQUEST_ID, ComponentPayload.CODEC); +//#else +//$$ PayloadTypeRegistry.playC2S().register(LiteralPayload.RESPONSE_ID, LiteralPayload.CODEC); +//$$ PayloadTypeRegistry.playS2C().register(LiteralPayload.RESPONSE_ID, LiteralPayload.CODEC); +//$$ PayloadTypeRegistry.playS2C().register(LiteralPayload.REQUEST_ID, LiteralPayload.CODEC); +//$$ PayloadTypeRegistry.playC2S().register(LiteralPayload.REQUEST_ID, LiteralPayload.CODEC); +//$$ PayloadTypeRegistry.playC2S().register(ComponentPayload.RESPONSE_ID, ComponentPayload.CODEC); +//$$ PayloadTypeRegistry.playS2C().register(ComponentPayload.RESPONSE_ID, ComponentPayload.CODEC); +//$$ PayloadTypeRegistry.playS2C().register(ComponentPayload.REQUEST_ID, ComponentPayload.CODEC); +//$$ PayloadTypeRegistry.playC2S().register(ComponentPayload.REQUEST_ID, ComponentPayload.CODEC); +//#endif ServerPlayNetworking.registerGlobalReceiver(LiteralPayload.REQUEST_ID, (payload, context) -> plugin.handleMessage(plugin, LiteralPayload.REQUEST_ID.id().toString(), payload.getBytes(), true)); ServerPlayNetworking.registerGlobalReceiver(ComponentPayload.REQUEST_ID, (payload, context) -> plugin.handleMessage(plugin, ComponentPayload.REQUEST_ID.id().toString(), payload.getBytes(), true)); @@ -62,10 +70,9 @@ public void sendMessage(@NotNull UUID uuid, @NotNull String channel, byte @NotNu } final FabricUser user = optionalFabricUser.get(); - final CustomPayload payload = channel.equals(ComponentPayload.RESPONSE_ID.id().toString()) ? + final var payload = channel.equals(ComponentPayload.RESPONSE_ID.id().toString()) ? new ComponentPayload(message, false) : new LiteralPayload(message, false); - final Packet packet = new CustomPayloadS2CPacket(payload); - user.player().networkHandler.sendPacket(packet); + ServerPlayNetworking.send(user.player(), payload); } } diff --git a/fabric/src/main/java/net/william278/papiproxybridge/payload/ComponentPayload.java b/fabric/src/main/java/net/william278/papiproxybridge/payload/ComponentPayload.java index a41da8f..187ed7a 100644 --- a/fabric/src/main/java/net/william278/papiproxybridge/payload/ComponentPayload.java +++ b/fabric/src/main/java/net/william278/papiproxybridge/payload/ComponentPayload.java @@ -19,17 +19,30 @@ package net.william278.papiproxybridge.payload; -import net.minecraft.network.PacketByteBuf; -import net.minecraft.network.codec.PacketCodec; -import net.minecraft.network.packet.CustomPayload; -import net.minecraft.util.Identifier; +//#if MC>=260102 +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +//#else +//$$ import net.minecraft.network.PacketByteBuf; +//$$ import net.minecraft.network.codec.PacketCodec; +//$$ import net.minecraft.network.packet.CustomPayload; +//$$ import net.minecraft.util.Identifier; +//#endif import net.william278.papiproxybridge.PAPIProxyBridge; public final class ComponentPayload extends TemplatePayload { - public static final CustomPayload.Id REQUEST_ID = new CustomPayload.Id<>(Identifier.of(PAPIProxyBridge.getComponentChannel(true))); - public static final CustomPayload.Id RESPONSE_ID = new CustomPayload.Id<>(Identifier.of(PAPIProxyBridge.getComponentChannel(false))); - public static final PacketCodec CODEC = PacketCodec.of((value, buf) -> writeBytes(buf, value.bytes), ComponentPayload::new); +//#if MC>=260102 + public static final CustomPacketPayload.Type REQUEST_ID = new CustomPacketPayload.Type<>(Identifier.parse(PAPIProxyBridge.getComponentChannel(true))); + public static final CustomPacketPayload.Type RESPONSE_ID = new CustomPacketPayload.Type<>(Identifier.parse(PAPIProxyBridge.getComponentChannel(false))); + public static final StreamCodec CODEC = StreamCodec.of((buf, value) -> writeBytes(buf, value.bytes), ComponentPayload::new); +//#else +//$$ public static final CustomPayload.Id REQUEST_ID = new CustomPayload.Id<>(Identifier.of(PAPIProxyBridge.getComponentChannel(true))); +//$$ public static final CustomPayload.Id RESPONSE_ID = new CustomPayload.Id<>(Identifier.of(PAPIProxyBridge.getComponentChannel(false))); +//$$ public static final PacketCodec CODEC = PacketCodec.of((value, buf) -> writeBytes(buf, value.bytes), ComponentPayload::new); +//#endif private final boolean isRequest; @@ -38,12 +51,20 @@ public ComponentPayload(byte[] bytes, boolean isRequest) { this.isRequest = isRequest; } - private ComponentPayload(PacketByteBuf buf) { +//#if MC>=260102 + private ComponentPayload(FriendlyByteBuf buf) { +//#else +//$$ private ComponentPayload(PacketByteBuf buf) { +//#endif this(getWrittenBytes(buf), true); } @Override - public Id getId() { +//#if MC>=260102 + public CustomPacketPayload.Type type() { +//#else +//$$ public Id getId() { +//#endif return isRequest ? REQUEST_ID : RESPONSE_ID; } } diff --git a/fabric/src/main/java/net/william278/papiproxybridge/payload/LiteralPayload.java b/fabric/src/main/java/net/william278/papiproxybridge/payload/LiteralPayload.java index 0d282e1..b153c92 100644 --- a/fabric/src/main/java/net/william278/papiproxybridge/payload/LiteralPayload.java +++ b/fabric/src/main/java/net/william278/papiproxybridge/payload/LiteralPayload.java @@ -19,18 +19,32 @@ package net.william278.papiproxybridge.payload; -import net.minecraft.network.PacketByteBuf; -import net.minecraft.network.RegistryByteBuf; -import net.minecraft.network.codec.PacketCodec; -import net.minecraft.network.packet.CustomPayload; -import net.minecraft.util.Identifier; +//#if MC>=260102 +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +//#else +//$$ import net.minecraft.network.PacketByteBuf; +//$$ import net.minecraft.network.RegistryByteBuf; +//$$ import net.minecraft.network.codec.PacketCodec; +//$$ import net.minecraft.network.packet.CustomPayload; +//$$ import net.minecraft.util.Identifier; +//#endif import net.william278.papiproxybridge.PAPIProxyBridge; public final class LiteralPayload extends TemplatePayload { - public static final Id REQUEST_ID = new Id<>(Identifier.of(PAPIProxyBridge.getChannel(true))); - public static final Id RESPONSE_ID = new Id<>(Identifier.of(PAPIProxyBridge.getChannel(false))); - public static final PacketCodec CODEC = PacketCodec.of((value, buf) -> writeBytes(buf, value.bytes), LiteralPayload::new); +//#if MC>=260102 + public static final CustomPacketPayload.Type REQUEST_ID = new CustomPacketPayload.Type<>(Identifier.parse(PAPIProxyBridge.getChannel(true))); + public static final CustomPacketPayload.Type RESPONSE_ID = new CustomPacketPayload.Type<>(Identifier.parse(PAPIProxyBridge.getChannel(false))); + public static final StreamCodec CODEC = StreamCodec.of((buf, value) -> writeBytes(buf, value.bytes), LiteralPayload::new); +//#else +//$$ public static final Id REQUEST_ID = new Id<>(Identifier.of(PAPIProxyBridge.getChannel(true))); +//$$ public static final Id RESPONSE_ID = new Id<>(Identifier.of(PAPIProxyBridge.getChannel(false))); +//$$ public static final PacketCodec CODEC = PacketCodec.of((value, buf) -> writeBytes(buf, value.bytes), LiteralPayload::new); +//#endif private final boolean isRequest; @@ -39,12 +53,20 @@ public LiteralPayload(byte[] bytes, boolean isRequest) { this.isRequest = isRequest; } - private LiteralPayload(PacketByteBuf buf) { +//#if MC>=260102 + private LiteralPayload(FriendlyByteBuf buf) { +//#else +//$$ private LiteralPayload(PacketByteBuf buf) { +//#endif this(getWrittenBytes(buf), true); } @Override - public Id getId() { +//#if MC>=260102 + public CustomPacketPayload.Type type() { +//#else +//$$ public Id getId() { +//#endif return isRequest ? REQUEST_ID : RESPONSE_ID; } } \ No newline at end of file diff --git a/fabric/src/main/java/net/william278/papiproxybridge/payload/TemplatePayload.java b/fabric/src/main/java/net/william278/papiproxybridge/payload/TemplatePayload.java index a7a1065..ec75424 100644 --- a/fabric/src/main/java/net/william278/papiproxybridge/payload/TemplatePayload.java +++ b/fabric/src/main/java/net/william278/papiproxybridge/payload/TemplatePayload.java @@ -21,22 +21,39 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; -import net.minecraft.network.PacketByteBuf; -import net.minecraft.network.packet.CustomPayload; +//#if MC>=260102 +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +//#else +//$$ import net.minecraft.network.PacketByteBuf; +//$$ import net.minecraft.network.packet.CustomPayload; +//#endif @Getter @RequiredArgsConstructor -public abstract class TemplatePayload implements CustomPayload { +//#if MC>=260102 +public abstract class TemplatePayload implements CustomPacketPayload { +//#else +//$$ public abstract class TemplatePayload implements CustomPayload { +//#endif protected final byte[] bytes; - protected static byte[] getWrittenBytes(PacketByteBuf buf) { +//#if MC>=260102 + protected static byte[] getWrittenBytes(FriendlyByteBuf buf) { +//#else +//$$ protected static byte[] getWrittenBytes(PacketByteBuf buf) { +//#endif byte[] bs = new byte[buf.readableBytes()]; buf.readBytes(bs); return bs; } - protected static void writeBytes(PacketByteBuf buf, byte[] v) { +//#if MC>=260102 + protected static void writeBytes(FriendlyByteBuf buf, byte[] v) { +//#else +//$$ protected static void writeBytes(PacketByteBuf buf, byte[] v) { +//#endif buf.writeBytes(v); } } diff --git a/fabric/src/main/java/net/william278/papiproxybridge/user/FabricUser.java b/fabric/src/main/java/net/william278/papiproxybridge/user/FabricUser.java index 14e75ac..36e201f 100644 --- a/fabric/src/main/java/net/william278/papiproxybridge/user/FabricUser.java +++ b/fabric/src/main/java/net/william278/papiproxybridge/user/FabricUser.java @@ -22,15 +22,23 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -//#if MC>=12107 -import net.minecraft.text.TextCodecs; +//#if MC>=260102 import com.mojang.serialization.JsonOps; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.server.level.ServerPlayer; +//#elseif MC>=12107 +//$$ import net.minecraft.text.TextCodecs; +//$$ import com.mojang.serialization.JsonOps; +//$$ import net.minecraft.server.network.ServerPlayerEntity; +//$$ import net.minecraft.text.Text; +//$$ import net.minecraft.util.Language; //#else //$$ import net.minecraft.registry.DynamicRegistryManager; +//$$ import net.minecraft.server.network.ServerPlayerEntity; +//$$ import net.minecraft.text.Text; +//$$ import net.minecraft.util.Language; //#endif -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.text.Text; -import net.minecraft.util.Language; import net.william278.papiproxybridge.FabricPAPIProxyBridge; import net.william278.papiproxybridge.PAPIProxyBridge; import org.jetbrains.annotations.NotNull; @@ -38,10 +46,18 @@ import java.util.*; -public record FabricUser(ServerPlayerEntity player) implements OnlineUser { +//#if MC>=260102 +public record FabricUser(ServerPlayer player) implements OnlineUser { +//#else +//$$ public record FabricUser(ServerPlayerEntity player) implements OnlineUser { +//#endif @NotNull - public static FabricUser adapt(@NotNull ServerPlayerEntity player) { +//#if MC>=260102 + public static FabricUser adapt(@NotNull ServerPlayer player) { +//#else +//$$ public static FabricUser adapt(@NotNull ServerPlayerEntity player) { +//#endif return new FabricUser(player); } @@ -54,13 +70,21 @@ public String getUsername() { @Override @NotNull public UUID getUniqueId() { - return player.getUuid(); +//#if MC>=260102 + return player.getUUID(); +//#else +//$$ return player.getUuid(); +//#endif } - private Component getComponent(Text text) { -//#if MC>=12107 - return GsonComponentSerializer.gson().deserialize(TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, text).getOrThrow().getAsString()); +//#if MC>=260102 + private Component getComponent(net.minecraft.network.chat.Component text) { + return GsonComponentSerializer.gson().deserialize(ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, text).getOrThrow().getAsString()); +//#elseif MC>=12107 +//$$ private Component getComponent(Text text) { +//$$ return GsonComponentSerializer.gson().deserialize(TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, text).getOrThrow().getAsString()); //#else +//$$ private Component getComponent(Text text) { //$$ return GsonComponentSerializer.gson().deserialize(Text.Serialization.toJsonString(text, new DynamicRegistryManager.ImmutableImpl(List.of()))); //#endif } @@ -68,7 +92,11 @@ private Component getComponent(Text text) { private Component translateKeys(TranslatableComponent translatable) { final String key = translatable.key(); final @Nullable String translated = Objects.requireNonNullElse( - Language.getInstance().get(key, translatable.fallback()), +//#if MC>=260102 + Language.getInstance().getOrDefault(key, translatable.fallback()), +//#else +//$$ Language.getInstance().get(key, translatable.fallback()), +//#endif key ); return translatable.fallback(translated); @@ -77,7 +105,11 @@ private Component translateKeys(TranslatableComponent translatable) { @Override public void handleMessage(@NotNull PAPIProxyBridge plugin, @NotNull Request message, boolean wantsJson) { FabricPAPIProxyBridge bridge = (FabricPAPIProxyBridge) plugin; - Text formatted = bridge.formatPlaceholders(message.getFormatFor(), this, message.getMessage()); +//#if MC>=260102 + net.minecraft.network.chat.Component formatted = bridge.formatPlaceholders(message.getFormatFor(), this, message.getMessage()); +//#else +//$$ Text formatted = bridge.formatPlaceholders(message.getFormatFor(), this, message.getMessage()); +//#endif String response; if (wantsJson) { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca025c8..c61a118 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index 1ba4d16..9e82ab6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,7 +8,7 @@ pluginManagement { } plugins { - def egtVersion = "0.6.5" + def egtVersion = "0.7.0" id("gg.essential.defaults") version egtVersion id("gg.essential.multi-version.root") version egtVersion } From d298c7486bed1de4c3117f06fc538234d7a08849 Mon Sep 17 00:00:00 2001 From: AlexDev_ <56083016+alexdev03@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:20:58 +0200 Subject: [PATCH 3/4] [verified] fix(fabric): relocate bundled Netty --- fabric/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/fabric/build.gradle b/fabric/build.gradle index d39acbd..dc1e069 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -49,6 +49,7 @@ shadowJar { exclude '/mappings/*' relocate 'net.jodah', 'net.william278.papiproxybridge.libraries' + relocate 'io.netty', 'net.william278.papiproxybridge.libraries.netty' relocate 'org.snakeyaml', 'net.william278.papiproxybridge.libraries.snakeyaml' relocate 'io.lettuce', 'net.william278.papiproxybridge.libraries.lettuce' relocate 'de.exlll', 'net.william278.papiproxybridge.libraries.configlib' From 1b932655fc45cac04d67eeceadff72a00a5955fc Mon Sep 17 00:00:00 2001 From: AlexDev_ <56083016+alexdev03@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:24:55 +0200 Subject: [PATCH 4/4] [verified] ci: build Minecraft 26.x with Java 25 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/pr_tests.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08d7d0c..d4d9436 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,10 @@ jobs: name: 'Build' runs-on: ubuntu-latest steps: - - name: 'Setup JDK 21 📦' + - name: 'Setup JDK 25 📦' uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - name: 'Setup Gradle 8.10 🏗️' uses: gradle/actions/setup-gradle@v4 diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 07218b7..1ae29e6 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v3 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - name: Test Pull Request uses: gradle/gradle-build-action@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e24a318..804b0d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v3 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - name: Build with Gradle uses: gradle/gradle-build-action@v2