From 116a9217bf38baba2ec0e0fc371229ed58de55fc Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 13 Apr 2025 17:18:00 +0100 Subject: [PATCH 01/25] feat: slightly less turbo trash lunar impl --- bin/development/build.gradle.kts | 1 + .../mapmaker/dev/DevServerRunner.java | 8 ++ .../discord/DiscordRichPresenceManager.java | 14 +++ .../discord/DiscordRichPresenceProvider.java | 14 +++ .../impl/DiscordRichPresenceManagerImpl.java | 39 ++++++++ .../compat/lunar/LunarCompatProvider.java | 99 +++++++++++++++++++ .../lunar/packets/ClientboundLunarPacket.java | 28 ++++++ .../lunar/packets/ServerboundLunarPacket.java | 25 +++++ 8 files changed, 228 insertions(+) create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/impl/DiscordRichPresenceManagerImpl.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java diff --git a/bin/development/build.gradle.kts b/bin/development/build.gradle.kts index b4b076c28..2e0fc9570 100644 --- a/bin/development/build.gradle.kts +++ b/bin/development/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(project(":modules:map")) implementation(project(":modules:terraform")) implementation(project(":modules:script-engine")) + implementation(project(":modules:compat")) implementation(libs.minestom) implementation(libs.bundles.adventure) diff --git a/bin/development/src/main/java/net/hollowcube/mapmaker/dev/DevServerRunner.java b/bin/development/src/main/java/net/hollowcube/mapmaker/dev/DevServerRunner.java index 70d205c46..e7f63cbab 100644 --- a/bin/development/src/main/java/net/hollowcube/mapmaker/dev/DevServerRunner.java +++ b/bin/development/src/main/java/net/hollowcube/mapmaker/dev/DevServerRunner.java @@ -5,6 +5,8 @@ import net.hollowcube.common.util.FutureUtil; import net.hollowcube.common.util.MojangUtil; import net.hollowcube.common.util.OpUtils; +import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; +import net.hollowcube.compat.impl.DiscordRichPresenceManagerImpl; import net.hollowcube.mapmaker.config.ConfigLoaderV3; import net.hollowcube.mapmaker.hub.HubMapWorld; import net.hollowcube.mapmaker.hub.HubServerRunner; @@ -87,6 +89,8 @@ public DevServerRunner(@NotNull ConfigLoaderV3 config) { protected void prepareStart() { super.prepareStart(); + DiscordRichPresenceManager.load(); + MinecraftServer.getConnectionManager().setPlayerProvider((connection, gameProfile) -> new MapPlayerImplImpl(connection, gameProfile) { @Override public @NotNull CommandManager getCommandManager() { @@ -222,6 +226,10 @@ protected void handleServerListPing(@NotNull ServerListPingEvent event) { }); }, ""); + dbg.createPermissionlessSubcommand("richpresence", (player, ignored) -> { + DiscordRichPresenceManagerImpl.getInstance().setRichPresence(player, "BLOSSOM", "/play 484-555-599", "Playing"); + }, ""); + return dbg; } } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java new file mode 100644 index 000000000..b76506f83 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java @@ -0,0 +1,14 @@ +package net.hollowcube.compat.api.discord; + +import net.minestom.server.entity.Player; + +import java.util.ServiceLoader; + +public interface DiscordRichPresenceManager { + + void setRichPresence(Player player, String gameName, String gameVariantName, String playerState); + + static void load() { + ServiceLoader.load(DiscordRichPresenceManager.class).findFirst().orElseThrow(); + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java new file mode 100644 index 000000000..a602d5002 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java @@ -0,0 +1,14 @@ +package net.hollowcube.compat.api.discord; + +import net.minestom.server.entity.Player; + +public interface DiscordRichPresenceProvider { + void setRichPresence( + final Player player, + final String gameName, + final String gameVariantName, + final String playerState + ); + + boolean isRichPresenceSupportedFor(final Player player); +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/impl/DiscordRichPresenceManagerImpl.java b/modules/compat/src/main/java/net/hollowcube/compat/impl/DiscordRichPresenceManagerImpl.java new file mode 100644 index 000000000..7957bf491 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/impl/DiscordRichPresenceManagerImpl.java @@ -0,0 +1,39 @@ +package net.hollowcube.compat.impl; + +import com.google.auto.service.AutoService; +import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; +import net.hollowcube.compat.api.discord.DiscordRichPresenceProvider; +import net.minestom.server.entity.Player; + +import java.util.HashSet; +import java.util.ServiceLoader; +import java.util.Set; + +@AutoService(DiscordRichPresenceManager.class) +public class DiscordRichPresenceManagerImpl implements DiscordRichPresenceManager { + private static final Set PROVIDERS = new HashSet<>(); + + private static DiscordRichPresenceManagerImpl INSTANCE; + + public DiscordRichPresenceManagerImpl() { + INSTANCE = this; + ServiceLoader.load(DiscordRichPresenceProvider.class).forEach(PROVIDERS::add); + } + + public static DiscordRichPresenceManager getInstance() { + if (INSTANCE != null) return INSTANCE; + throw new IllegalStateException("DiscordRichPresenceManagerImpl not initialized"); + } + + + @Override + public void setRichPresence(final Player player, final String gameName, final String gameVariantName, final String playerState) { + for (DiscordRichPresenceProvider provider : PROVIDERS) { + System.out.println(provider.getClass().getName()); + if (provider.isRichPresenceSupportedFor(player)) { + provider.setRichPresence(player, gameName, gameVariantName, playerState); + return; + } + } + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java new file mode 100644 index 000000000..ddb0b25f0 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -0,0 +1,99 @@ +package net.hollowcube.compat.lunar; + +import com.google.auto.service.AutoService; +import net.hollowcube.compat.api.CompatProvider; +import net.hollowcube.compat.api.discord.DiscordRichPresenceProvider; +import net.hollowcube.compat.api.packet.PacketRegistry; +import net.hollowcube.compat.lunar.packets.ClientboundLunarPacket; +import net.hollowcube.compat.lunar.packets.ServerboundLunarPacket; +import net.minestom.server.entity.Player; +import net.minestom.server.event.GlobalEventHandler; +import net.minestom.server.event.player.PlayerPluginMessageEvent; +import net.minestom.server.tag.Tag; + +import java.util.Map; + + +@AutoService({CompatProvider.class, DiscordRichPresenceProvider.class}) +public class LunarCompatProvider implements CompatProvider, DiscordRichPresenceProvider { + public static final Tag LUNAR_SUPPORT_ENABLED = Tag.Transient("mapmaker:lunar/enabled"); + private static final ClientboundLunarPacket MOD_SETTINGS = new ClientboundLunarPacket( + Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", + "apollo_module", "mod_setting", + "enable", true, + // todo there is probably a cleaner and more configurable way to do this + "properties", Map.of( + "bossbar.enabled", false, + "saturation.enabled", false, + "direction-hud.enabled", false, + "armorstatus.enabled", false, + "title.enabled", false + ) + )); + + @Override + public void registerListeners(GlobalEventHandler events) { + events.addListener(PlayerPluginMessageEvent.class, this::handleLunarPacket); + } + + @Override + public void registerPackets(PacketRegistry registry) { + registry.register(ClientboundLunarPacket.TYPE); + registry.register(ServerboundLunarPacket.LUNAR_APOLLO_TYPE, (player, packet) -> { + }); + registry.register(ServerboundLunarPacket.APOLLO_JSON_TYPE, (player, packet) -> { + }); + + } + + public static void sendRichPresencePacket(final Player player) { + // todo combine this with the other enable packet + new ClientboundLunarPacket( + Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", + "apollo_module", "rich_presence", + "enable", true + ) + ).send(player); + } + + private void handleLunarPacket(final PlayerPluginMessageEvent event) { + // todo this probably isn't needed anymore, although the tag is probably still useful + if (event.getPlayer().hasTag(LUNAR_SUPPORT_ENABLED)) { + return; + } + + if (event.getIdentifier().equalsIgnoreCase("minecraft:register")) { + if (event.getMessageString().contains("lunar:apollo") || event.getMessageString().contains("apollo:json")) { + event.getPlayer().setTag(LUNAR_SUPPORT_ENABLED, true); + MOD_SETTINGS.send(event.getPlayer()); + sendRichPresencePacket(event.getPlayer()); + } + } + } + + @Override + public void setRichPresence( + Player player, + String gameName, + String gameVariantName, + String playerState + ) { + new ClientboundLunarPacket( + Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", + "game_name", gameName, + // This is a lunar bug, it seems to escape the / character and discord doesn't undo it, so we need to replace it with another character + "game_variant_name", gameVariantName.replace("/", "∕"), + "player_state", playerState + ) + ).send(player); + } + + @Override + public boolean isRichPresenceSupportedFor(Player player) { + // todo we probably don't need this tag, we can just check if the channel is registered + return player.hasTag(LUNAR_SUPPORT_ENABLED); + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java new file mode 100644 index 000000000..fdd648cec --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java @@ -0,0 +1,28 @@ +package net.hollowcube.compat.lunar.packets; + +import com.google.gson.Gson; +import net.hollowcube.compat.api.packet.ClientboundModPacket; +import net.minestom.server.network.NetworkBuffer; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public record ClientboundLunarPacket( + Map data +) implements ClientboundModPacket { + public static final String TYPE_PREFIX = "type.googleapis.com/lunarclient.apollo."; + + public static final Type TYPE = Type.of( + "apollo", + "json", + (buffer, packet) -> { + final var json = new Gson().toJson(packet.data); + buffer.write(NetworkBuffer.RAW_BYTES, json.getBytes(StandardCharsets.UTF_8)); + } + ); + + @Override + public Type getType() { + return TYPE; + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java new file mode 100644 index 000000000..d1291c853 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java @@ -0,0 +1,25 @@ +package net.hollowcube.compat.lunar.packets; + +import net.hollowcube.compat.api.packet.ServerboundModPacket; + +// For lunar to advertise itself we have to ensure we register the channels +// Rather than send another register message, we'll just register some dummy handlers +// Who knows, maybe they'll be useful in the future if lunar add more serverbound packets +public record ServerboundLunarPacket() implements ServerboundModPacket { + public static final Type LUNAR_APOLLO_TYPE = Type.of( + "lunar", + "apollo", + buffer -> new ServerboundLunarPacket() + ); + + public static final Type APOLLO_JSON_TYPE = Type.of( + "apollo", + "json", + buffer -> new ServerboundLunarPacket() + ); + + @Override + public Type getType() { + return null; + } +} From 8fd9ca93455e7e919996608af89cd520a4970236 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 14 Apr 2025 22:48:29 +0100 Subject: [PATCH 02/25] feat: feather support for rich presence --- .../kotlin/mapmaker.java-binary.gradle.kts | 6 + .../kotlin/mapmaker.java-library.gradle.kts | 6 + gradle/libs.versions.toml | 3 + modules/compat/build.gradle.kts | 1 + .../compat/feather/FeatherCompatProvider.java | 146 ++++++++++++++++++ .../packets/ClientboundFeatherPacket.java | 31 ++++ .../packets/ServerboundFeatherPacket.java | 31 ++++ 7 files changed, 224 insertions(+) create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java diff --git a/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts b/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts index 16ad5b155..7f34ff39a 100644 --- a/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts +++ b/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts @@ -16,6 +16,12 @@ repositories { includeGroup("com.noxcrew.noxesium") } } + + maven(url = "https://repo.feathermc.net/artifactory/maven-releases") { + content { + includeGroup("net.digitalingot.feather-server-api") + } + } } dependencies { diff --git a/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts b/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts index 59fb52713..572841a02 100644 --- a/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts +++ b/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts @@ -12,6 +12,12 @@ repositories { includeGroup("com.noxcrew.noxesium") } } + + maven(url = "https://repo.feathermc.net/artifactory/maven-releases") { + content { + includeGroup("net.digitalingot.feather-server-api") + } + } } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b06f627a..bce4a58ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,8 @@ json5 = "2.0.0" graalvm = "24.2.0" blossom = "2.1.0" velocity = "3.4.0-SNAPSHOT" +feather = "0.0.5" + [libraries] annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } @@ -48,6 +50,7 @@ caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version mql = { group = "dev.hollowcube", name = "mql", version.ref = "mql" } json5 = { group = "de.marhali", name = "json5-java", version.ref = "json5" } velocity-api = { group = "com.velocitypowered", name = "velocity-api", version.ref = "velocity" } +feather = { group = "net.digitalingot.feather-server-api", name = "messaging", version.ref = "feather" } graal-polyglot = { group = "org.graalvm.polyglot", name = "polyglot", version.ref = "graalvm" } graal-js-language = { group = "org.graalvm.js", name = "js-language", version.ref = "graalvm" } diff --git a/modules/compat/build.gradle.kts b/modules/compat/build.gradle.kts index b194a4f50..4372e70a9 100644 --- a/modules/compat/build.gradle.kts +++ b/modules/compat/build.gradle.kts @@ -11,4 +11,5 @@ dependencies { implementation(libs.posthog) implementation(libs.noxesium) implementation(libs.zstd) + implementation(libs.feather) } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java new file mode 100644 index 000000000..031445817 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java @@ -0,0 +1,146 @@ +package net.hollowcube.compat.feather; + +import com.google.auto.service.AutoService; +import net.digitalingot.feather.serverapi.messaging.*; +import net.digitalingot.feather.serverapi.messaging.messages.client.S2CHandshake; +import net.digitalingot.feather.serverapi.messaging.messages.client.S2CSetDiscordActivity; +import net.digitalingot.feather.serverapi.messaging.messages.server.C2SClientHello; +import net.digitalingot.feather.serverapi.messaging.messages.server.C2SHandshake; +import net.hollowcube.compat.api.CompatProvider; +import net.hollowcube.compat.api.discord.DiscordRichPresenceProvider; +import net.hollowcube.compat.api.packet.PacketRegistry; +import net.hollowcube.compat.feather.packets.ClientboundFeatherPacket; +import net.hollowcube.compat.feather.packets.ServerboundFeatherPacket; +import net.minestom.server.entity.Player; +import net.minestom.server.event.GlobalEventHandler; +import net.minestom.server.event.player.PlayerDisconnectEvent; +import net.minestom.server.tag.Tag; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@AutoService({CompatProvider.class, DiscordRichPresenceProvider.class}) +public class FeatherCompatProvider implements CompatProvider, DiscordRichPresenceProvider { + private static final String IMAGE_URL = "https://servermappings.lunarclientcdn.com/logos/hollowcube.png"; + private static final String FEATHER_CHANNEL = "feather:client"; + private static final String FEATHER_CHANNEL_FRAGMENTED = FEATHER_CHANNEL + "/frag"; + private static final Tag FEATHER_SUPPORT_ENABLED = Tag.Transient("mapmaker:feather/enabled"); + private static final Handshaking HANDSHAKING = new Handshaking(); + + public static void sendMessage(final Player player, final Message message) { + final var bytes = MessageEncoder.CLIENT_BOUND.encode(message); + if (bytes.length > 1048576 /* 1MiB */) { + for (byte[] data : MessageFragmenter.CLIENT_BOUND.fragment(message)) { + player.sendPluginMessage(FEATHER_CHANNEL_FRAGMENTED, data); + } + } else { + player.sendPluginMessage(FEATHER_CHANNEL, bytes); + } + } + + @Override + public void registerListeners(GlobalEventHandler events) { + events.addListener(PlayerDisconnectEvent.class, e -> HANDSHAKING.finish(e.getPlayer())); + + } + + @Override + public void registerPackets(PacketRegistry registry) { + registry.register(ClientboundFeatherPacket.TYPE); + registry.register(ServerboundFeatherPacket.TYPE, (player, packet) -> { + if (!player.hasTag(FEATHER_SUPPORT_ENABLED)) { + final var hello = HANDSHAKING.handle(player, packet.message()); + + if (hello != null) { + player.setTag(FEATHER_SUPPORT_ENABLED, true); + } + + } + }); + } + + @Override + public void setRichPresence(Player player, String gameName, String gameVariantName, String playerState) { + // this is probably turbo trash, but it creates consistency between Lunar and Feather + final var details = playerState + " " + gameName + " on Hollow Cube"; + sendMessage(player, new S2CSetDiscordActivity(IMAGE_URL, "Hollow Cube", gameVariantName, details, null, null, null, null)); + } + + @Override + public boolean isRichPresenceSupportedFor(Player player) { + return player.hasTag(FEATHER_SUPPORT_ENABLED); + } + + // This is directly copied from https://github.com/FeatherMC/feather-server-api/blob/main/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/messaging/BukkitMessagingService.java + // but with some modifications to make it work with Minestom + private static class Handshaking { + private final Map handshakes = new HashMap<>(); + + + private HandshakeState getState(Player player) { + return this.handshakes.getOrDefault(player.getUuid(), HandshakeState.EXPECTING_HANDSHAKE); + } + + private void setState(UUID playerId, HandshakeState state) { + this.handshakes.put(playerId, state); + } + + private void accept(Player player) { + setState(player.getUuid(), HandshakeState.EXPECTING_HELLO); + sendMessage(player, new S2CHandshake()); + } + + private void reject(Player player) { + setState(player.getUuid(), HandshakeState.REJECTED); + } + + private void finish(Player player) { + this.handshakes.remove(player.getUuid()); + } + + private C2SClientHello handle(Player player, Message message) { + HandshakeState state = getState(player); + + if (state == HandshakeState.REJECTED) { + return null; + } + + + if (state == HandshakeState.EXPECTING_HANDSHAKE) { + if (handleExpectingHandshake(message)) { + accept(player); + } else { + reject(player); + } + } else if (state == HandshakeState.EXPECTING_HELLO) { + if ((message instanceof C2SClientHello)) { + finish(player); + return (C2SClientHello) message; + } + reject(player); + } + + return null; + } + + private boolean handleExpectingHandshake(Message message) { + if (!(message instanceof C2SHandshake handshake)) { + return false; + } + int protocolVersion = handshake.getProtocolVersion(); + if (protocolVersion > MessageConstants.VERSION) { + // todo this doesn't actually do anything + } + return true; + } + + + private enum HandshakeState { + EXPECTING_HANDSHAKE, + EXPECTING_HELLO, + REJECTED + } + } + +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java new file mode 100644 index 000000000..5f9ffae64 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java @@ -0,0 +1,31 @@ +package net.hollowcube.compat.feather.packets; + +import net.digitalingot.feather.serverapi.messaging.ClientMessageHandler; +import net.digitalingot.feather.serverapi.messaging.Message; +import net.digitalingot.feather.serverapi.messaging.MessageDecoder; +import net.digitalingot.feather.serverapi.messaging.MessageEncoder; +import net.hollowcube.compat.api.packet.ClientboundModPacket; +import net.minestom.server.network.NetworkBuffer; + +public record ClientboundFeatherPacket( + Message message +) implements ClientboundModPacket { + public static final Type TYPE = Type.of( + "feather", + "client", + NetworkBuffer.RAW_BYTES.transform(ClientboundFeatherPacket::new, ClientboundFeatherPacket::toBytes) + ); + + public ClientboundFeatherPacket(byte[] bytes) { + this(MessageDecoder.CLIENT_BOUND.decode(bytes)); + } + + public byte[] toBytes() { + return MessageEncoder.CLIENT_BOUND.encode(message); + } + + @Override + public Type getType() { + return TYPE; + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java new file mode 100644 index 000000000..9c2fd4572 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java @@ -0,0 +1,31 @@ +package net.hollowcube.compat.feather.packets; + +import net.digitalingot.feather.serverapi.messaging.Message; +import net.digitalingot.feather.serverapi.messaging.MessageDecoder; +import net.digitalingot.feather.serverapi.messaging.MessageEncoder; +import net.digitalingot.feather.serverapi.messaging.ServerMessageHandler; +import net.hollowcube.compat.api.packet.ServerboundModPacket; +import net.minestom.server.network.NetworkBuffer; + +public record ServerboundFeatherPacket( + Message message +) implements ServerboundModPacket { + public static final Type TYPE = Type.of( + "feather", + "client", + NetworkBuffer.RAW_BYTES.transform(ServerboundFeatherPacket::new, ServerboundFeatherPacket::toBytes) + ); + + public ServerboundFeatherPacket(byte[] bytes) { + this(MessageDecoder.SERVER_BOUND.decode(bytes)); + } + + public byte[] toBytes() { + return MessageEncoder.SERVER_BOUND.encode(message); + } + + @Override + public Type getType() { + return TYPE; + } +} From f3831fad10e097b33dc250f3885d6e5c6199bb32 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 18 Apr 2025 15:38:52 +0100 Subject: [PATCH 03/25] feat: discord presence updates on state changes (mostly) --- .../mapmaker/dev/DevServerRunner.java | 9 ---- .../discord/DiscordRichPresenceManager.java | 29 ++++++++-- .../discord/DiscordRichPresenceProvider.java | 2 + .../compat/feather/FeatherCompatProvider.java | 6 +++ .../impl/DiscordRichPresenceManagerImpl.java | 39 -------------- .../compat/lunar/LunarCompatProvider.java | 9 ++++ modules/hub/build.gradle.kts | 1 + .../hollowcube/mapmaker/hub/HubMapWorld.java | 9 ++++ .../DiscordRichPresenceFeatureProvider.java | 53 +++++++++++++++++++ .../mapmaker/map/world/EditingMapWorld.java | 3 ++ 10 files changed, 108 insertions(+), 52 deletions(-) delete mode 100644 modules/compat/src/main/java/net/hollowcube/compat/impl/DiscordRichPresenceManagerImpl.java create mode 100644 modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java diff --git a/bin/development/src/main/java/net/hollowcube/mapmaker/dev/DevServerRunner.java b/bin/development/src/main/java/net/hollowcube/mapmaker/dev/DevServerRunner.java index e7f63cbab..a0b2cc45f 100644 --- a/bin/development/src/main/java/net/hollowcube/mapmaker/dev/DevServerRunner.java +++ b/bin/development/src/main/java/net/hollowcube/mapmaker/dev/DevServerRunner.java @@ -5,8 +5,6 @@ import net.hollowcube.common.util.FutureUtil; import net.hollowcube.common.util.MojangUtil; import net.hollowcube.common.util.OpUtils; -import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; -import net.hollowcube.compat.impl.DiscordRichPresenceManagerImpl; import net.hollowcube.mapmaker.config.ConfigLoaderV3; import net.hollowcube.mapmaker.hub.HubMapWorld; import net.hollowcube.mapmaker.hub.HubServerRunner; @@ -89,8 +87,6 @@ public DevServerRunner(@NotNull ConfigLoaderV3 config) { protected void prepareStart() { super.prepareStart(); - DiscordRichPresenceManager.load(); - MinecraftServer.getConnectionManager().setPlayerProvider((connection, gameProfile) -> new MapPlayerImplImpl(connection, gameProfile) { @Override public @NotNull CommandManager getCommandManager() { @@ -225,11 +221,6 @@ protected void handleServerListPing(@NotNull ServerListPingEvent event) { scriptEngine().guiManager().openGui(player, URI.create("guilib:///map_browser/map-browser-view.js"), Map.of(), Map.of()); }); }, ""); - - dbg.createPermissionlessSubcommand("richpresence", (player, ignored) -> { - DiscordRichPresenceManagerImpl.getInstance().setRichPresence(player, "BLOSSOM", "/play 484-555-599", "Playing"); - }, ""); - return dbg; } } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java index b76506f83..a050caa20 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java @@ -2,13 +2,34 @@ import net.minestom.server.entity.Player; +import java.util.HashSet; import java.util.ServiceLoader; +import java.util.Set; -public interface DiscordRichPresenceManager { - void setRichPresence(Player player, String gameName, String gameVariantName, String playerState); +public class DiscordRichPresenceManager { + private static final Set PROVIDERS = new HashSet<>(); - static void load() { - ServiceLoader.load(DiscordRichPresenceManager.class).findFirst().orElseThrow(); + static { + ServiceLoader.load(DiscordRichPresenceProvider.class).forEach(PROVIDERS::add); + } + + + public static void setRichPresence(final Player player, final String gameName, final String gameVariantName, final String playerState) { + for (DiscordRichPresenceProvider provider : PROVIDERS) { + if (provider.isRichPresenceSupportedFor(player)) { + provider.setRichPresence(player, gameName, gameVariantName, playerState); + return; + } + } + } + + public static void clearRichPresence(Player player) { + for (DiscordRichPresenceProvider provider : PROVIDERS) { + if (provider.isRichPresenceSupportedFor(player)) { + provider.clearRichPresence(player); + return; + } + } } } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java index a602d5002..461ed43ce 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java @@ -10,5 +10,7 @@ void setRichPresence( final String playerState ); + void clearRichPresence(final Player player); + boolean isRichPresenceSupportedFor(final Player player); } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java index 031445817..d665fb2f3 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java @@ -2,6 +2,7 @@ import com.google.auto.service.AutoService; import net.digitalingot.feather.serverapi.messaging.*; +import net.digitalingot.feather.serverapi.messaging.messages.client.S2CClearDiscordActivity; import net.digitalingot.feather.serverapi.messaging.messages.client.S2CHandshake; import net.digitalingot.feather.serverapi.messaging.messages.client.S2CSetDiscordActivity; import net.digitalingot.feather.serverapi.messaging.messages.server.C2SClientHello; @@ -67,6 +68,11 @@ public void setRichPresence(Player player, String gameName, String gameVariantNa sendMessage(player, new S2CSetDiscordActivity(IMAGE_URL, "Hollow Cube", gameVariantName, details, null, null, null, null)); } + @Override + public void clearRichPresence(Player player) { + sendMessage(player, new S2CClearDiscordActivity()); + } + @Override public boolean isRichPresenceSupportedFor(Player player) { return player.hasTag(FEATHER_SUPPORT_ENABLED); diff --git a/modules/compat/src/main/java/net/hollowcube/compat/impl/DiscordRichPresenceManagerImpl.java b/modules/compat/src/main/java/net/hollowcube/compat/impl/DiscordRichPresenceManagerImpl.java deleted file mode 100644 index 7957bf491..000000000 --- a/modules/compat/src/main/java/net/hollowcube/compat/impl/DiscordRichPresenceManagerImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.hollowcube.compat.impl; - -import com.google.auto.service.AutoService; -import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; -import net.hollowcube.compat.api.discord.DiscordRichPresenceProvider; -import net.minestom.server.entity.Player; - -import java.util.HashSet; -import java.util.ServiceLoader; -import java.util.Set; - -@AutoService(DiscordRichPresenceManager.class) -public class DiscordRichPresenceManagerImpl implements DiscordRichPresenceManager { - private static final Set PROVIDERS = new HashSet<>(); - - private static DiscordRichPresenceManagerImpl INSTANCE; - - public DiscordRichPresenceManagerImpl() { - INSTANCE = this; - ServiceLoader.load(DiscordRichPresenceProvider.class).forEach(PROVIDERS::add); - } - - public static DiscordRichPresenceManager getInstance() { - if (INSTANCE != null) return INSTANCE; - throw new IllegalStateException("DiscordRichPresenceManagerImpl not initialized"); - } - - - @Override - public void setRichPresence(final Player player, final String gameName, final String gameVariantName, final String playerState) { - for (DiscordRichPresenceProvider provider : PROVIDERS) { - System.out.println(provider.getClass().getName()); - if (provider.isRichPresenceSupportedFor(player)) { - provider.setRichPresence(player, gameName, gameVariantName, playerState); - return; - } - } - } -} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index ddb0b25f0..eff918f88 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -91,6 +91,15 @@ public void setRichPresence( ).send(player); } + @Override + public void clearRichPresence(Player player) { + new ClientboundLunarPacket( + Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.ResetServerRichPresenceMessage" + ) + ).send(player); + } + @Override public boolean isRichPresenceSupportedFor(Player player) { // todo we probably don't need this tag, we can just check if the channel is registered diff --git a/modules/hub/build.gradle.kts b/modules/hub/build.gradle.kts index 2272a090d..27868c8f5 100644 --- a/modules/hub/build.gradle.kts +++ b/modules/hub/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { implementation(project(":modules:canvas:api")) implementation(project(":modules:terraform")) // Included for schematics, todo both terraform and this module should get schem from central implementation(project(":modules:script-engine")) + implementation(project(":modules:compat")) implementation(libs.minestom) implementation(libs.gson) diff --git a/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java b/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java index 4179116fa..6aab97fa6 100644 --- a/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java +++ b/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java @@ -2,6 +2,7 @@ import net.hollowcube.common.util.FontUtil; import net.hollowcube.common.util.Uuids; +import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; import net.hollowcube.mapmaker.CoreFeatureFlags; import net.hollowcube.mapmaker.hub.entity.marker.HubMarkerLoader; import net.hollowcube.mapmaker.hub.feature.misc.DoubleJumpFeature; @@ -44,6 +45,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.channels.Channels; +import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Objects; @@ -177,6 +179,13 @@ public void addPlayer(@NotNull Player player) { BossBars.clear(player); BOSS_BARS.forEach(player::showBossBar); + + // todo does this need to be a feature? + logger.info("setting discord presence"); + // we have to delay this by a little bit otherwise the presence will try to be set before the module enable message is sent to the client, + // which causes the client to just ignore the presence update. + player.scheduler().buildTask(() -> DiscordRichPresenceManager.setRichPresence(player, "the lobby", "", "In")).delay(Duration.ofSeconds(2)).schedule(); + } @Override diff --git a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java new file mode 100644 index 000000000..645a54342 --- /dev/null +++ b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java @@ -0,0 +1,53 @@ +package net.hollowcube.mapmaker.map.feature.common; + +import com.google.auto.service.AutoService; +import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; +import net.hollowcube.mapmaker.map.MapWorld; +import net.hollowcube.mapmaker.map.event.MapPlayerInitEvent; +import net.hollowcube.mapmaker.map.feature.FeatureProvider; +import net.hollowcube.mapmaker.map.world.EditingMapWorld; +import net.hollowcube.mapmaker.map.world.PlayingMapWorld; +import net.minestom.server.event.EventFilter; +import net.minestom.server.event.EventNode; +import net.minestom.server.event.trait.InstanceEvent; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@AutoService(FeatureProvider.class) +public class DiscordRichPresenceFeatureProvider implements FeatureProvider { + private static final Logger logger = LoggerFactory.getLogger(DiscordRichPresenceFeatureProvider.class); + private final EventNode eventNode = EventNode.type("mapmaker:map/discord-rich-presence", EventFilter.INSTANCE) + .addListener(MapPlayerInitEvent.class, this::handleMapInit); + + @Override + public boolean initMap(@NotNull MapWorld world) { + if (!(world instanceof PlayingMapWorld || world instanceof EditingMapWorld)) + return false; + logger.info("adding rich presence to map"); + world.eventNode().addChild(eventNode); + return true; + } + + private void handleMapInit(@NotNull final MapPlayerInitEvent event) { + if (event.isMapJoin()) { + if (event.mapWorld() instanceof PlayingMapWorld) { + logger.info("saying we are playing"); + DiscordRichPresenceManager.setRichPresence(event.player(), event.mapWorld().map().name(), "/play " + event.mapWorld().map().publishedIdString(), "Playing"); + } else if (event.mapWorld() instanceof EditingMapWorld) { + logger.info("saying we are building"); + final var variant = event.mapWorld().map().settings().getVariant().name().toLowerCase(); + DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "a map", "Building " + indefiniteArticle(variant) + " map", "Building"); + } + } + } + + private static String indefiniteArticle(final String word) { + return switch (word.charAt(0)) { + case 'a', 'e', 'i', 'o', 'u' -> "an"; + default -> "a"; + } + " " + word; + } + +} + diff --git a/modules/map/src/main/java/net/hollowcube/mapmaker/map/world/EditingMapWorld.java b/modules/map/src/main/java/net/hollowcube/mapmaker/map/world/EditingMapWorld.java index a6d956e70..3e06a35c5 100644 --- a/modules/map/src/main/java/net/hollowcube/mapmaker/map/world/EditingMapWorld.java +++ b/modules/map/src/main/java/net/hollowcube/mapmaker/map/world/EditingMapWorld.java @@ -6,6 +6,7 @@ import net.hollowcube.mapmaker.ExceptionReporter; import net.hollowcube.mapmaker.instance.generation.MapGenerators; import net.hollowcube.mapmaker.map.*; +import net.hollowcube.mapmaker.map.event.MapPlayerInitEvent; import net.hollowcube.mapmaker.map.feature.edit.TeleportHistoryFeatureProvider; import net.hollowcube.mapmaker.map.instance.MapInstance; import net.hollowcube.mapmaker.map.item.ItemTags; @@ -311,6 +312,8 @@ public void addPlayer(@NotNull Player player) { // If there is no position stored then this is a fresh edit state so add the builder menu player.getInventory().addItemStack(itemRegistry().getItemStack("mapmaker:builder_menu", null)); } + // FIXME: i don't know if calling this here breaks something or if there is a specific reason it currently isn't + callEvent(new MapPlayerInitEvent(this, player, true, true)); } private @NotNull SaveState getOrCreateSaveState(@NotNull Player player) { From fe828891a35a4c728c2c3ed31cd57dc0d7b90b1e Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 20 Apr 2025 13:29:42 +0100 Subject: [PATCH 04/25] chore: notnull annotations --- .../compat/feather/packets/ClientboundFeatherPacket.java | 3 ++- .../compat/feather/packets/ServerboundFeatherPacket.java | 3 ++- .../compat/lunar/packets/ClientboundLunarPacket.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java index 5f9ffae64..32634d14f 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java @@ -6,9 +6,10 @@ import net.digitalingot.feather.serverapi.messaging.MessageEncoder; import net.hollowcube.compat.api.packet.ClientboundModPacket; import net.minestom.server.network.NetworkBuffer; +import org.jetbrains.annotations.NotNull; public record ClientboundFeatherPacket( - Message message + @NotNull Message message ) implements ClientboundModPacket { public static final Type TYPE = Type.of( "feather", diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java index 9c2fd4572..63bfff8da 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java @@ -6,9 +6,10 @@ import net.digitalingot.feather.serverapi.messaging.ServerMessageHandler; import net.hollowcube.compat.api.packet.ServerboundModPacket; import net.minestom.server.network.NetworkBuffer; +import org.jetbrains.annotations.NotNull; public record ServerboundFeatherPacket( - Message message + @NotNull Message message ) implements ServerboundModPacket { public static final Type TYPE = Type.of( "feather", diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java index fdd648cec..6b0172b1e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java @@ -3,12 +3,13 @@ import com.google.gson.Gson; import net.hollowcube.compat.api.packet.ClientboundModPacket; import net.minestom.server.network.NetworkBuffer; +import org.jetbrains.annotations.NotNull; import java.nio.charset.StandardCharsets; import java.util.Map; public record ClientboundLunarPacket( - Map data + @NotNull Map data ) implements ClientboundModPacket { public static final String TYPE_PREFIX = "type.googleapis.com/lunarclient.apollo."; From e01de3d7631bbdb20de250558dbd38a22e514f26 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 20 Apr 2025 15:49:40 +0100 Subject: [PATCH 05/25] chore: update comment --- .../net/hollowcube/compat/feather/FeatherCompatProvider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java index d665fb2f3..49bc7542f 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java @@ -136,7 +136,9 @@ private boolean handleExpectingHandshake(Message message) } int protocolVersion = handshake.getProtocolVersion(); if (protocolVersion > MessageConstants.VERSION) { - // todo this doesn't actually do anything + // TODO: In the official API Implementation, a mismatched API version just alerts players with a permission that it is out of date. It still processed packets fine. + // There is no indication of what versioning compatibility we can expect since they've only released one version. + // For now, we can probably just ignore this. } return true; } From 7ac5ac522ce1461ff2f981669da834494ba8147f Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 21 Apr 2025 00:48:14 +0100 Subject: [PATCH 06/25] chore: split util into its own class --- .../java/net/hollowcube/common/util/WordUtil.java | 12 ++++++++++++ .../common/DiscordRichPresenceFeatureProvider.java | 10 ++-------- 2 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 modules/common/src/main/java/net/hollowcube/common/util/WordUtil.java diff --git a/modules/common/src/main/java/net/hollowcube/common/util/WordUtil.java b/modules/common/src/main/java/net/hollowcube/common/util/WordUtil.java new file mode 100644 index 000000000..759261609 --- /dev/null +++ b/modules/common/src/main/java/net/hollowcube/common/util/WordUtil.java @@ -0,0 +1,12 @@ +package net.hollowcube.common.util; + +public class WordUtil { + public static String indefiniteArticle(final String word) { + // building -> a building + // adventure -> an adventure + return switch (word.charAt(0)) { + case 'a', 'e', 'i', 'o', 'u' -> "an"; + default -> "a"; + } + " " + word; + } +} diff --git a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java index 645a54342..018c5e314 100644 --- a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java +++ b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java @@ -1,6 +1,7 @@ package net.hollowcube.mapmaker.map.feature.common; import com.google.auto.service.AutoService; +import net.hollowcube.common.util.WordUtil; import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; import net.hollowcube.mapmaker.map.MapWorld; import net.hollowcube.mapmaker.map.event.MapPlayerInitEvent; @@ -37,17 +38,10 @@ private void handleMapInit(@NotNull final MapPlayerInitEvent event) { } else if (event.mapWorld() instanceof EditingMapWorld) { logger.info("saying we are building"); final var variant = event.mapWorld().map().settings().getVariant().name().toLowerCase(); - DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "a map", "Building " + indefiniteArticle(variant) + " map", "Building"); + DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "a map", "Building " + WordUtil.indefiniteArticle(variant) + " map", "Building"); } } } - private static String indefiniteArticle(final String word) { - return switch (word.charAt(0)) { - case 'a', 'e', 'i', 'o', 'u' -> "an"; - default -> "a"; - } + " " + word; - } - } From 2e9236c6dba8a64ba1c383f8e1af73d1470c300a Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 21 Apr 2025 20:46:47 +0100 Subject: [PATCH 07/25] feat: labymod RPC impl --- .../kotlin/mapmaker.java-binary.gradle.kts | 6 +++ .../kotlin/mapmaker.java-library.gradle.kts | 7 +++ gradle/libs.versions.toml | 3 +- modules/compat/build.gradle.kts | 1 + .../compat/laby/LabyCompatProvider.java | 48 +++++++++++++++++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java diff --git a/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts b/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts index 7f34ff39a..7ead9b9da 100644 --- a/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts +++ b/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts @@ -22,6 +22,12 @@ repositories { includeGroup("net.digitalingot.feather-server-api") } } + maven(url = "https://dist.labymod.net/api/v1/maven/release/") { + content { + includeGroup("net.labymod.serverapi") + includeGroup("net.labymod.serverapi.integration") + } + } } dependencies { diff --git a/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts b/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts index 572841a02..b3eb9cd7e 100644 --- a/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts +++ b/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts @@ -18,6 +18,13 @@ repositories { includeGroup("net.digitalingot.feather-server-api") } } + + maven(url = "https://dist.labymod.net/api/v1/maven/release/") { + content { + includeGroup("net.labymod.serverapi") + includeGroup("net.labymod.serverapi.integration") + } + } } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bce4a58ea..5bbdcbe9c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ graalvm = "24.2.0" blossom = "2.1.0" velocity = "3.4.0-SNAPSHOT" feather = "0.0.5" - +labymod = "1.0.6" [libraries] annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } @@ -51,6 +51,7 @@ mql = { group = "dev.hollowcube", name = "mql", version.ref = "mql" } json5 = { group = "de.marhali", name = "json5-java", version.ref = "json5" } velocity-api = { group = "com.velocitypowered", name = "velocity-api", version.ref = "velocity" } feather = { group = "net.digitalingot.feather-server-api", name = "messaging", version.ref = "feather" } +labymod = { group = "net.labymod.serverapi", name = "server-minestom", version.ref="labymod"} graal-polyglot = { group = "org.graalvm.polyglot", name = "polyglot", version.ref = "graalvm" } graal-js-language = { group = "org.graalvm.js", name = "js-language", version.ref = "graalvm" } diff --git a/modules/compat/build.gradle.kts b/modules/compat/build.gradle.kts index 4372e70a9..39f461cb2 100644 --- a/modules/compat/build.gradle.kts +++ b/modules/compat/build.gradle.kts @@ -12,4 +12,5 @@ dependencies { implementation(libs.noxesium) implementation(libs.zstd) implementation(libs.feather) + implementation(libs.labymod) } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java new file mode 100644 index 000000000..3bae55593 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java @@ -0,0 +1,48 @@ +package net.hollowcube.compat.laby; + +import com.google.auto.service.AutoService; +import net.hollowcube.compat.api.CompatProvider; +import net.hollowcube.compat.api.discord.DiscordRichPresenceProvider; +import net.labymod.serverapi.core.model.feature.DiscordRPC; +import net.labymod.serverapi.server.minestom.LabyModProtocolService; +import net.minestom.server.entity.Player; +import net.minestom.server.event.GlobalEventHandler; + +@AutoService({CompatProvider.class, DiscordRichPresenceProvider.class}) +public class LabyCompatProvider implements CompatProvider, DiscordRichPresenceProvider { + + @Override + public void registerListeners(GlobalEventHandler events) { + // todo this probably adds a load of junk we don't need. should probably just implement the packets we need ourselves. + // however I am lazy so for now, this will do. + LabyModProtocolService.initialize(); + } + + @Override + public void setRichPresence(Player player, String gameName, String gameVariantName, String playerState) { + var labyModPlayer = LabyModProtocolService.get().getPlayer(player.getUuid()); + if (labyModPlayer == null) { + return; + } + + // labymod sucks and we can only control one little bit + final var details = playerState + " " + gameName + (gameVariantName != null && !gameVariantName.isEmpty() ? " (" + gameVariantName + ")" : ""); + labyModPlayer.sendDiscordRPC(DiscordRPC.create(details)); + } + + @Override + public void clearRichPresence(Player player) { + var labyModPlayer = LabyModProtocolService.get().getPlayer(player.getUuid()); + if (labyModPlayer == null) { + return; + } + labyModPlayer.sendDiscordRPC(DiscordRPC.createReset()); + } + + @Override + public boolean isRichPresenceSupportedFor(Player player) { + return LabyModProtocolService.get().isUsingLabyMod(player.getUuid()); + } + + +} From f95804938bc75a870e6a59a8370e6c10b956e350 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 27 Apr 2025 15:34:36 +0100 Subject: [PATCH 08/25] chore: cleanup --- .../discord/DiscordRichPresenceManager.java | 4 +- .../discord/DiscordRichPresenceProvider.java | 11 ++--- .../compat/feather/FeatherCompatProvider.java | 41 +++++++++---------- .../packets/ClientboundFeatherPacket.java | 5 ++- .../packets/ServerboundFeatherPacket.java | 2 +- .../compat/laby/LabyCompatProvider.java | 2 +- .../compat/lunar/LunarCompatProvider.java | 31 +++++--------- .../hollowcube/mapmaker/hub/HubMapWorld.java | 4 +- .../DiscordRichPresenceFeatureProvider.java | 10 +---- .../mapmaker/map/world/EditingMapWorld.java | 2 +- 10 files changed, 44 insertions(+), 68 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java index a050caa20..fca767f44 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java @@ -15,10 +15,10 @@ public class DiscordRichPresenceManager { } - public static void setRichPresence(final Player player, final String gameName, final String gameVariantName, final String playerState) { + public static void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) { for (DiscordRichPresenceProvider provider : PROVIDERS) { if (provider.isRichPresenceSupportedFor(player)) { - provider.setRichPresence(player, gameName, gameVariantName, playerState); + provider.setRichPresence(player, playerState, gameName, gameVariantName); return; } } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java index 461ed43ce..df654007a 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java @@ -3,14 +3,9 @@ import net.minestom.server.entity.Player; public interface DiscordRichPresenceProvider { - void setRichPresence( - final Player player, - final String gameName, - final String gameVariantName, - final String playerState - ); + void setRichPresence(Player player, String playerState, String gameName, String gameVariantName); - void clearRichPresence(final Player player); + void clearRichPresence(Player player); - boolean isRichPresenceSupportedFor(final Player player); + boolean isRichPresenceSupportedFor(Player player); } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java index 49bc7542f..fa129b717 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java @@ -24,22 +24,9 @@ @AutoService({CompatProvider.class, DiscordRichPresenceProvider.class}) public class FeatherCompatProvider implements CompatProvider, DiscordRichPresenceProvider { private static final String IMAGE_URL = "https://servermappings.lunarclientcdn.com/logos/hollowcube.png"; - private static final String FEATHER_CHANNEL = "feather:client"; - private static final String FEATHER_CHANNEL_FRAGMENTED = FEATHER_CHANNEL + "/frag"; private static final Tag FEATHER_SUPPORT_ENABLED = Tag.Transient("mapmaker:feather/enabled"); private static final Handshaking HANDSHAKING = new Handshaking(); - public static void sendMessage(final Player player, final Message message) { - final var bytes = MessageEncoder.CLIENT_BOUND.encode(message); - if (bytes.length > 1048576 /* 1MiB */) { - for (byte[] data : MessageFragmenter.CLIENT_BOUND.fragment(message)) { - player.sendPluginMessage(FEATHER_CHANNEL_FRAGMENTED, data); - } - } else { - player.sendPluginMessage(FEATHER_CHANNEL, bytes); - } - } - @Override public void registerListeners(GlobalEventHandler events) { events.addListener(PlayerDisconnectEvent.class, e -> HANDSHAKING.finish(e.getPlayer())); @@ -62,15 +49,24 @@ public void registerPackets(PacketRegistry registry) { } @Override - public void setRichPresence(Player player, String gameName, String gameVariantName, String playerState) { - // this is probably turbo trash, but it creates consistency between Lunar and Feather - final var details = playerState + " " + gameName + " on Hollow Cube"; - sendMessage(player, new S2CSetDiscordActivity(IMAGE_URL, "Hollow Cube", gameVariantName, details, null, null, null, null)); + public void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) { + new ClientboundFeatherPacket( + new S2CSetDiscordActivity( + IMAGE_URL, + "Hollow Cube", + gameVariantName, + playerState + " " + gameName + " on Hollow Cube", + null, + null, + null, + null + ) + ).send(player); } @Override public void clearRichPresence(Player player) { - sendMessage(player, new S2CClearDiscordActivity()); + new ClientboundFeatherPacket(new S2CClearDiscordActivity()).send(player); } @Override @@ -94,7 +90,7 @@ private void setState(UUID playerId, HandshakeState state) { private void accept(Player player) { setState(player.getUuid(), HandshakeState.EXPECTING_HELLO); - sendMessage(player, new S2CHandshake()); + new ClientboundFeatherPacket(new S2CHandshake()).send(player); } private void reject(Player player) { @@ -136,9 +132,10 @@ private boolean handleExpectingHandshake(Message message) } int protocolVersion = handshake.getProtocolVersion(); if (protocolVersion > MessageConstants.VERSION) { - // TODO: In the official API Implementation, a mismatched API version just alerts players with a permission that it is out of date. It still processed packets fine. - // There is no indication of what versioning compatibility we can expect since they've only released one version. - // For now, we can probably just ignore this. + // In the official API Implementation, a mismatched API version just alerts players with a permission that it is out of date. + // It still processes packets fine. + // There is no indication of what versioning compatibility we can expect since they've only released one version. + // For now, we can probably just ignore this. } return true; } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java index 32634d14f..71430dd6c 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ClientboundFeatherPacket.java @@ -8,6 +8,9 @@ import net.minestom.server.network.NetworkBuffer; import org.jetbrains.annotations.NotNull; +// Feather supports a fragmented packet channel for messages that exceed the packet size limit, which was set lower on older versions of Bukkit +// We don't ever send messages that get that big, so for now this can be ignored. +// If we ever utilise more of their UI features, this may be needed in the future. public record ClientboundFeatherPacket( @NotNull Message message ) implements ClientboundModPacket { @@ -17,7 +20,7 @@ public record ClientboundFeatherPacket( NetworkBuffer.RAW_BYTES.transform(ClientboundFeatherPacket::new, ClientboundFeatherPacket::toBytes) ); - public ClientboundFeatherPacket(byte[] bytes) { + private ClientboundFeatherPacket(byte[] bytes) { this(MessageDecoder.CLIENT_BOUND.decode(bytes)); } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java index 63bfff8da..8c66a83d7 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/packets/ServerboundFeatherPacket.java @@ -17,7 +17,7 @@ public record ServerboundFeatherPacket( NetworkBuffer.RAW_BYTES.transform(ServerboundFeatherPacket::new, ServerboundFeatherPacket::toBytes) ); - public ServerboundFeatherPacket(byte[] bytes) { + private ServerboundFeatherPacket(byte[] bytes) { this(MessageDecoder.SERVER_BOUND.decode(bytes)); } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java index 3bae55593..b1551e88e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java @@ -19,7 +19,7 @@ public void registerListeners(GlobalEventHandler events) { } @Override - public void setRichPresence(Player player, String gameName, String gameVariantName, String playerState) { + public void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) { var labyModPlayer = LabyModProtocolService.get().getPlayer(player.getUuid()); if (labyModPlayer == null) { return; diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index eff918f88..74a5cf1db 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -31,6 +31,13 @@ public class LunarCompatProvider implements CompatProvider, DiscordRichPresenceP "title.enabled", false ) )); + private static final ClientboundLunarPacket ENABLE_RICH_PRESENCE = new ClientboundLunarPacket( + Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", + "apollo_module", "rich_presence", + "enable", true + ) + ); @Override public void registerListeners(GlobalEventHandler events) { @@ -47,19 +54,7 @@ public void registerPackets(PacketRegistry registry) { } - public static void sendRichPresencePacket(final Player player) { - // todo combine this with the other enable packet - new ClientboundLunarPacket( - Map.of( - "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", - "apollo_module", "rich_presence", - "enable", true - ) - ).send(player); - } - - private void handleLunarPacket(final PlayerPluginMessageEvent event) { - // todo this probably isn't needed anymore, although the tag is probably still useful + private void handleLunarPacket(PlayerPluginMessageEvent event) { if (event.getPlayer().hasTag(LUNAR_SUPPORT_ENABLED)) { return; } @@ -68,18 +63,13 @@ private void handleLunarPacket(final PlayerPluginMessageEvent event) { if (event.getMessageString().contains("lunar:apollo") || event.getMessageString().contains("apollo:json")) { event.getPlayer().setTag(LUNAR_SUPPORT_ENABLED, true); MOD_SETTINGS.send(event.getPlayer()); - sendRichPresencePacket(event.getPlayer()); + ENABLE_RICH_PRESENCE.send(event.getPlayer()); } } } @Override - public void setRichPresence( - Player player, - String gameName, - String gameVariantName, - String playerState - ) { + public void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) { new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", @@ -102,7 +92,6 @@ public void clearRichPresence(Player player) { @Override public boolean isRichPresenceSupportedFor(Player player) { - // todo we probably don't need this tag, we can just check if the channel is registered return player.hasTag(LUNAR_SUPPORT_ENABLED); } } diff --git a/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java b/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java index 6aab97fa6..10a8b8aba 100644 --- a/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java +++ b/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java @@ -180,11 +180,9 @@ public void addPlayer(@NotNull Player player) { BossBars.clear(player); BOSS_BARS.forEach(player::showBossBar); - // todo does this need to be a feature? - logger.info("setting discord presence"); // we have to delay this by a little bit otherwise the presence will try to be set before the module enable message is sent to the client, // which causes the client to just ignore the presence update. - player.scheduler().buildTask(() -> DiscordRichPresenceManager.setRichPresence(player, "the lobby", "", "In")).delay(Duration.ofSeconds(2)).schedule(); + player.scheduler().buildTask(() -> DiscordRichPresenceManager.setRichPresence(player, "In", "the lobby", "")).delay(Duration.ofSeconds(2)).schedule(); } diff --git a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java index 018c5e314..61e37f432 100644 --- a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java +++ b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java @@ -12,12 +12,9 @@ import net.minestom.server.event.EventNode; import net.minestom.server.event.trait.InstanceEvent; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @AutoService(FeatureProvider.class) public class DiscordRichPresenceFeatureProvider implements FeatureProvider { - private static final Logger logger = LoggerFactory.getLogger(DiscordRichPresenceFeatureProvider.class); private final EventNode eventNode = EventNode.type("mapmaker:map/discord-rich-presence", EventFilter.INSTANCE) .addListener(MapPlayerInitEvent.class, this::handleMapInit); @@ -25,7 +22,6 @@ public class DiscordRichPresenceFeatureProvider implements FeatureProvider { public boolean initMap(@NotNull MapWorld world) { if (!(world instanceof PlayingMapWorld || world instanceof EditingMapWorld)) return false; - logger.info("adding rich presence to map"); world.eventNode().addChild(eventNode); return true; } @@ -33,12 +29,10 @@ public boolean initMap(@NotNull MapWorld world) { private void handleMapInit(@NotNull final MapPlayerInitEvent event) { if (event.isMapJoin()) { if (event.mapWorld() instanceof PlayingMapWorld) { - logger.info("saying we are playing"); - DiscordRichPresenceManager.setRichPresence(event.player(), event.mapWorld().map().name(), "/play " + event.mapWorld().map().publishedIdString(), "Playing"); + DiscordRichPresenceManager.setRichPresence(event.player(), "Playing", event.mapWorld().map().name(), "/play " + event.mapWorld().map().publishedIdString()); } else if (event.mapWorld() instanceof EditingMapWorld) { - logger.info("saying we are building"); final var variant = event.mapWorld().map().settings().getVariant().name().toLowerCase(); - DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "a map", "Building " + WordUtil.indefiniteArticle(variant) + " map", "Building"); + DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "Building", "a map", "Building " + WordUtil.indefiniteArticle(variant) + " map"); } } } diff --git a/modules/map/src/main/java/net/hollowcube/mapmaker/map/world/EditingMapWorld.java b/modules/map/src/main/java/net/hollowcube/mapmaker/map/world/EditingMapWorld.java index 3e06a35c5..07b1efffe 100644 --- a/modules/map/src/main/java/net/hollowcube/mapmaker/map/world/EditingMapWorld.java +++ b/modules/map/src/main/java/net/hollowcube/mapmaker/map/world/EditingMapWorld.java @@ -312,7 +312,7 @@ public void addPlayer(@NotNull Player player) { // If there is no position stored then this is a fresh edit state so add the builder menu player.getInventory().addItemStack(itemRegistry().getItemStack("mapmaker:builder_menu", null)); } - // FIXME: i don't know if calling this here breaks something or if there is a specific reason it currently isn't + callEvent(new MapPlayerInitEvent(this, player, true, true)); } From 3966f343338b8290d786081b223ac5d36cb99cb2 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 27 Apr 2025 15:40:33 +0100 Subject: [PATCH 09/25] chore: remove labymod for now it can come back when I can be bothered to do the packet impl --- .../kotlin/mapmaker.java-binary.gradle.kts | 6 --- .../kotlin/mapmaker.java-library.gradle.kts | 7 --- gradle/libs.versions.toml | 2 - modules/compat/build.gradle.kts | 1 - .../compat/laby/LabyCompatProvider.java | 48 ------------------- 5 files changed, 64 deletions(-) delete mode 100644 modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java diff --git a/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts b/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts index 7ead9b9da..7f34ff39a 100644 --- a/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts +++ b/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts @@ -22,12 +22,6 @@ repositories { includeGroup("net.digitalingot.feather-server-api") } } - maven(url = "https://dist.labymod.net/api/v1/maven/release/") { - content { - includeGroup("net.labymod.serverapi") - includeGroup("net.labymod.serverapi.integration") - } - } } dependencies { diff --git a/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts b/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts index b3eb9cd7e..572841a02 100644 --- a/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts +++ b/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts @@ -18,13 +18,6 @@ repositories { includeGroup("net.digitalingot.feather-server-api") } } - - maven(url = "https://dist.labymod.net/api/v1/maven/release/") { - content { - includeGroup("net.labymod.serverapi") - includeGroup("net.labymod.serverapi.integration") - } - } } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5bbdcbe9c..fe9106aa1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,6 @@ graalvm = "24.2.0" blossom = "2.1.0" velocity = "3.4.0-SNAPSHOT" feather = "0.0.5" -labymod = "1.0.6" [libraries] annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } @@ -51,7 +50,6 @@ mql = { group = "dev.hollowcube", name = "mql", version.ref = "mql" } json5 = { group = "de.marhali", name = "json5-java", version.ref = "json5" } velocity-api = { group = "com.velocitypowered", name = "velocity-api", version.ref = "velocity" } feather = { group = "net.digitalingot.feather-server-api", name = "messaging", version.ref = "feather" } -labymod = { group = "net.labymod.serverapi", name = "server-minestom", version.ref="labymod"} graal-polyglot = { group = "org.graalvm.polyglot", name = "polyglot", version.ref = "graalvm" } graal-js-language = { group = "org.graalvm.js", name = "js-language", version.ref = "graalvm" } diff --git a/modules/compat/build.gradle.kts b/modules/compat/build.gradle.kts index 39f461cb2..4372e70a9 100644 --- a/modules/compat/build.gradle.kts +++ b/modules/compat/build.gradle.kts @@ -12,5 +12,4 @@ dependencies { implementation(libs.noxesium) implementation(libs.zstd) implementation(libs.feather) - implementation(libs.labymod) } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java deleted file mode 100644 index b1551e88e..000000000 --- a/modules/compat/src/main/java/net/hollowcube/compat/laby/LabyCompatProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.hollowcube.compat.laby; - -import com.google.auto.service.AutoService; -import net.hollowcube.compat.api.CompatProvider; -import net.hollowcube.compat.api.discord.DiscordRichPresenceProvider; -import net.labymod.serverapi.core.model.feature.DiscordRPC; -import net.labymod.serverapi.server.minestom.LabyModProtocolService; -import net.minestom.server.entity.Player; -import net.minestom.server.event.GlobalEventHandler; - -@AutoService({CompatProvider.class, DiscordRichPresenceProvider.class}) -public class LabyCompatProvider implements CompatProvider, DiscordRichPresenceProvider { - - @Override - public void registerListeners(GlobalEventHandler events) { - // todo this probably adds a load of junk we don't need. should probably just implement the packets we need ourselves. - // however I am lazy so for now, this will do. - LabyModProtocolService.initialize(); - } - - @Override - public void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) { - var labyModPlayer = LabyModProtocolService.get().getPlayer(player.getUuid()); - if (labyModPlayer == null) { - return; - } - - // labymod sucks and we can only control one little bit - final var details = playerState + " " + gameName + (gameVariantName != null && !gameVariantName.isEmpty() ? " (" + gameVariantName + ")" : ""); - labyModPlayer.sendDiscordRPC(DiscordRPC.create(details)); - } - - @Override - public void clearRichPresence(Player player) { - var labyModPlayer = LabyModProtocolService.get().getPlayer(player.getUuid()); - if (labyModPlayer == null) { - return; - } - labyModPlayer.sendDiscordRPC(DiscordRPC.createReset()); - } - - @Override - public boolean isRichPresenceSupportedFor(Player player) { - return LabyModProtocolService.get().isUsingLabyMod(player.getUuid()); - } - - -} From eec1502f97ae801b2a23d9efb866db80b6012bf7 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 27 Apr 2025 15:49:33 +0100 Subject: [PATCH 10/25] chore: annotations --- .../compat/api/discord/DiscordRichPresenceManager.java | 5 +++-- .../compat/api/discord/DiscordRichPresenceProvider.java | 7 ++++--- .../hollowcube/compat/feather/FeatherCompatProvider.java | 7 ++++--- .../net/hollowcube/compat/lunar/LunarCompatProvider.java | 7 ++++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java index fca767f44..b25e186b7 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java @@ -1,6 +1,7 @@ package net.hollowcube.compat.api.discord; import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; import java.util.HashSet; import java.util.ServiceLoader; @@ -15,7 +16,7 @@ public class DiscordRichPresenceManager { } - public static void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) { + public static void setRichPresence(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName) { for (DiscordRichPresenceProvider provider : PROVIDERS) { if (provider.isRichPresenceSupportedFor(player)) { provider.setRichPresence(player, playerState, gameName, gameVariantName); @@ -24,7 +25,7 @@ public static void setRichPresence(Player player, String playerState, String gam } } - public static void clearRichPresence(Player player) { + public static void clearRichPresence(@NotNull Player player) { for (DiscordRichPresenceProvider provider : PROVIDERS) { if (provider.isRichPresenceSupportedFor(player)) { provider.clearRichPresence(player); diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java index df654007a..204f73776 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java @@ -1,11 +1,12 @@ package net.hollowcube.compat.api.discord; import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; public interface DiscordRichPresenceProvider { - void setRichPresence(Player player, String playerState, String gameName, String gameVariantName); + void setRichPresence(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName); - void clearRichPresence(Player player); + void clearRichPresence(@NotNull Player player); - boolean isRichPresenceSupportedFor(Player player); + boolean isRichPresenceSupportedFor(@NotNull Player player); } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java index fa129b717..6028bbc06 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java @@ -16,6 +16,7 @@ import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.event.player.PlayerDisconnectEvent; import net.minestom.server.tag.Tag; +import org.jetbrains.annotations.NotNull; import java.util.HashMap; import java.util.Map; @@ -49,7 +50,7 @@ public void registerPackets(PacketRegistry registry) { } @Override - public void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) { + public void setRichPresence(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName) { new ClientboundFeatherPacket( new S2CSetDiscordActivity( IMAGE_URL, @@ -65,12 +66,12 @@ public void setRichPresence(Player player, String playerState, String gameName, } @Override - public void clearRichPresence(Player player) { + public void clearRichPresence(@NotNull Player player) { new ClientboundFeatherPacket(new S2CClearDiscordActivity()).send(player); } @Override - public boolean isRichPresenceSupportedFor(Player player) { + public boolean isRichPresenceSupportedFor(@NotNull Player player) { return player.hasTag(FEATHER_SUPPORT_ENABLED); } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index 74a5cf1db..d5c18f7ab 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -10,6 +10,7 @@ import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.event.player.PlayerPluginMessageEvent; import net.minestom.server.tag.Tag; +import org.jetbrains.annotations.NotNull; import java.util.Map; @@ -69,7 +70,7 @@ private void handleLunarPacket(PlayerPluginMessageEvent event) { } @Override - public void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) { + public void setRichPresence(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName) { new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", @@ -82,7 +83,7 @@ public void setRichPresence(Player player, String playerState, String gameName, } @Override - public void clearRichPresence(Player player) { + public void clearRichPresence(@NotNull Player player) { new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.ResetServerRichPresenceMessage" @@ -91,7 +92,7 @@ public void clearRichPresence(Player player) { } @Override - public boolean isRichPresenceSupportedFor(Player player) { + public boolean isRichPresenceSupportedFor(@NotNull Player player) { return player.hasTag(LUNAR_SUPPORT_ENABLED); } } From 8f235ba28fcf77bb4f9ebeba193ab3bcdabda359 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 27 Apr 2025 16:06:18 +0100 Subject: [PATCH 11/25] chore: static gson (oops) --- .../compat/lunar/packets/ClientboundLunarPacket.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java index 6b0172b1e..820096443 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java @@ -11,15 +11,14 @@ public record ClientboundLunarPacket( @NotNull Map data ) implements ClientboundModPacket { + private static final Gson GSON = new Gson(); public static final String TYPE_PREFIX = "type.googleapis.com/lunarclient.apollo."; + public static final Type TYPE = Type.of( "apollo", "json", - (buffer, packet) -> { - final var json = new Gson().toJson(packet.data); - buffer.write(NetworkBuffer.RAW_BYTES, json.getBytes(StandardCharsets.UTF_8)); - } + (buffer, packet) -> buffer.write(NetworkBuffer.RAW_BYTES, GSON.toJson(packet.data).getBytes(StandardCharsets.UTF_8)) ); @Override From 2ca181021f3291b58ede494c598deed9fd4e02e4 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 27 Apr 2025 16:17:04 +0100 Subject: [PATCH 12/25] fix: hub rich presence to feature and event rather than delay --- .../hollowcube/mapmaker/hub/HubMapWorld.java | 7 ------- .../misc/DiscordRichPresenceFeature.java | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 modules/hub/src/main/java/net/hollowcube/mapmaker/hub/feature/misc/DiscordRichPresenceFeature.java diff --git a/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java b/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java index 10a8b8aba..4179116fa 100644 --- a/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java +++ b/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/HubMapWorld.java @@ -2,7 +2,6 @@ import net.hollowcube.common.util.FontUtil; import net.hollowcube.common.util.Uuids; -import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; import net.hollowcube.mapmaker.CoreFeatureFlags; import net.hollowcube.mapmaker.hub.entity.marker.HubMarkerLoader; import net.hollowcube.mapmaker.hub.feature.misc.DoubleJumpFeature; @@ -45,7 +44,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.channels.Channels; -import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Objects; @@ -179,11 +177,6 @@ public void addPlayer(@NotNull Player player) { BossBars.clear(player); BOSS_BARS.forEach(player::showBossBar); - - // we have to delay this by a little bit otherwise the presence will try to be set before the module enable message is sent to the client, - // which causes the client to just ignore the presence update. - player.scheduler().buildTask(() -> DiscordRichPresenceManager.setRichPresence(player, "In", "the lobby", "")).delay(Duration.ofSeconds(2)).schedule(); - } @Override diff --git a/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/feature/misc/DiscordRichPresenceFeature.java b/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/feature/misc/DiscordRichPresenceFeature.java new file mode 100644 index 000000000..5b5225b25 --- /dev/null +++ b/modules/hub/src/main/java/net/hollowcube/mapmaker/hub/feature/misc/DiscordRichPresenceFeature.java @@ -0,0 +1,17 @@ +package net.hollowcube.mapmaker.hub.feature.misc; + +import com.google.auto.service.AutoService; +import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; +import net.hollowcube.mapmaker.hub.HubMapWorld; +import net.hollowcube.mapmaker.hub.feature.HubFeature; +import net.hollowcube.mapmaker.map.MapServer; +import net.minestom.server.event.player.PlayerLoadedEvent; +import org.jetbrains.annotations.NotNull; + +@AutoService(HubFeature.class) +public class DiscordRichPresenceFeature implements HubFeature { + @Override + public void load(@NotNull MapServer server, @NotNull HubMapWorld world) { + world.eventNode().addListener(PlayerLoadedEvent.class, event -> DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "In", "the lobby", "")); + } +} From c9eefcf7355d14f446cc37e3627989b14401acc8 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 28 Apr 2025 00:00:56 +0100 Subject: [PATCH 13/25] chore: rename wordutil --- .../util/{WordUtil.java => StringUtil.java} | 24 +++++++++---------- .../DiscordRichPresenceFeatureProvider.java | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) rename modules/common/src/main/java/net/hollowcube/common/util/{WordUtil.java => StringUtil.java} (89%) diff --git a/modules/common/src/main/java/net/hollowcube/common/util/WordUtil.java b/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java similarity index 89% rename from modules/common/src/main/java/net/hollowcube/common/util/WordUtil.java rename to modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java index 759261609..3476fbca6 100644 --- a/modules/common/src/main/java/net/hollowcube/common/util/WordUtil.java +++ b/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java @@ -1,12 +1,12 @@ -package net.hollowcube.common.util; - -public class WordUtil { - public static String indefiniteArticle(final String word) { - // building -> a building - // adventure -> an adventure - return switch (word.charAt(0)) { - case 'a', 'e', 'i', 'o', 'u' -> "an"; - default -> "a"; - } + " " + word; - } -} +package net.hollowcube.common.util; + +public class StringUtil { + public static String indefiniteArticle(final String word) { + // building -> a building + // adventure -> an adventure + return switch (word.charAt(0)) { + case 'a', 'e', 'i', 'o', 'u' -> "an"; + default -> "a"; + } + " " + word; + } +} diff --git a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java index 61e37f432..1e35e11a6 100644 --- a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java +++ b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java @@ -1,7 +1,7 @@ package net.hollowcube.mapmaker.map.feature.common; import com.google.auto.service.AutoService; -import net.hollowcube.common.util.WordUtil; +import net.hollowcube.common.util.StringUtil; import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; import net.hollowcube.mapmaker.map.MapWorld; import net.hollowcube.mapmaker.map.event.MapPlayerInitEvent; @@ -32,7 +32,7 @@ private void handleMapInit(@NotNull final MapPlayerInitEvent event) { DiscordRichPresenceManager.setRichPresence(event.player(), "Playing", event.mapWorld().map().name(), "/play " + event.mapWorld().map().publishedIdString()); } else if (event.mapWorld() instanceof EditingMapWorld) { final var variant = event.mapWorld().map().settings().getVariant().name().toLowerCase(); - DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "Building", "a map", "Building " + WordUtil.indefiniteArticle(variant) + " map"); + DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "Building", "a map", "Building " + StringUtil.indefiniteArticle(variant) + " map"); } } } From 6a07cb6935a97981297399e6497ea4616077d27d Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Sat, 28 Mar 2026 19:38:52 -0230 Subject: [PATCH 14/25] chore: temporarily include apollo protobufs while lunar is fixing json channel --- .../kotlin/mapmaker.java-binary.gradle.kts | 2 + .../kotlin/mapmaker.java-library.gradle.kts | 2 + gradle/libs.versions.toml | 2 + modules/compat/build.gradle.kts | 1 + .../api/packet/ServerboundModPacket.java | 9 +- .../compat/lunar/LunarCompatProvider.java | 91 +++++++++---------- .../lunar/packets/ServerboundLunarPacket.java | 34 +++++-- 7 files changed, 82 insertions(+), 59 deletions(-) diff --git a/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts b/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts index a7f06a38c..fc909cba1 100644 --- a/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts +++ b/build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts @@ -35,6 +35,8 @@ repositories { includeGroup("net.digitalingot.feather-server-api") } } + + maven(url = "https://repo.lunarclient.dev") } dependencies { diff --git a/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts b/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts index 17cae1c60..492a7cf68 100644 --- a/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts +++ b/build-src/src/main/kotlin/mapmaker.java-library.gradle.kts @@ -31,6 +31,8 @@ repositories { includeGroup("net.digitalingot.feather-server-api") } } + + maven(url = "https://repo.lunarclient.dev") } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4ff183550..4dc037b89 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,6 +43,8 @@ slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } slf4j-jul = { group = "org.slf4j", name = "jul-to-slf4j", version.ref = "slf4j" } logback = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } noxesium = { group = "com.noxcrew.noxesium", name = "api", version.ref = "noxesium" } +feather = { group = "net.digitalingot.feather-server-api", name = "messaging", version = "0.0.5" } +apollo = { group = "com.lunarclient", name = "apollo-protos", version = "0.0.6" } zstd = { group = "com.github.luben", name = "zstd-jni", version.ref = "zstd" } polar = { group = "dev.hollowcube", name = "polar", version.ref = "polar" } similarity = { group = "info.debatty", name = "java-string-similarity", version.ref = "similarity" } diff --git a/modules/compat/build.gradle.kts b/modules/compat/build.gradle.kts index ccb60e23b..b783fb0ff 100644 --- a/modules/compat/build.gradle.kts +++ b/modules/compat/build.gradle.kts @@ -11,4 +11,5 @@ dependencies { implementation(libs.posthog) implementation(libs.zstd) implementation(libs.feather) + implementation(libs.apollo) } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/packet/ServerboundModPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/api/packet/ServerboundModPacket.java index 32d1ef7d1..ecb2f3a3e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/packet/ServerboundModPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/packet/ServerboundModPacket.java @@ -1,6 +1,7 @@ package net.hollowcube.compat.api.packet; import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.utils.ThrowingFunction; import org.jetbrains.annotations.NotNull; import java.util.function.Function; @@ -15,7 +16,7 @@ public static > Type of(String namespace, S return new Type<>("%s:%s".formatted(namespace, path), codec); } - public static > ServerboundModPacket.Type of(String namespace, String path, Function reader) { + public static > ServerboundModPacket.Type of(String namespace, String path, ThrowingFunction reader) { var type = new NetworkBuffer.Type() { @Override public void write(@NotNull NetworkBuffer buffer, T value) { @@ -24,7 +25,11 @@ public void write(@NotNull NetworkBuffer buffer, T value) { @Override public T read(@NotNull NetworkBuffer buffer) { - return reader.apply(buffer); + try { + return reader.apply(buffer); + } catch (Exception e) { + throw new RuntimeException(e); + } } }; return new ServerboundModPacket.Type<>("%s:%s".formatted(namespace, path), type); diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index d5c18f7ab..1801df6d6 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -2,6 +2,7 @@ import com.google.auto.service.AutoService; import net.hollowcube.compat.api.CompatProvider; +import net.hollowcube.compat.api.ModChannelRegisterEvent; import net.hollowcube.compat.api.discord.DiscordRichPresenceProvider; import net.hollowcube.compat.api.packet.PacketRegistry; import net.hollowcube.compat.lunar.packets.ClientboundLunarPacket; @@ -18,76 +19,72 @@ @AutoService({CompatProvider.class, DiscordRichPresenceProvider.class}) public class LunarCompatProvider implements CompatProvider, DiscordRichPresenceProvider { public static final Tag LUNAR_SUPPORT_ENABLED = Tag.Transient("mapmaker:lunar/enabled"); - private static final ClientboundLunarPacket MOD_SETTINGS = new ClientboundLunarPacket( - Map.of( - "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", - "apollo_module", "mod_setting", - "enable", true, - // todo there is probably a cleaner and more configurable way to do this - "properties", Map.of( - "bossbar.enabled", false, - "saturation.enabled", false, - "direction-hud.enabled", false, - "armorstatus.enabled", false, - "title.enabled", false - ) - )); - private static final ClientboundLunarPacket ENABLE_RICH_PRESENCE = new ClientboundLunarPacket( - Map.of( - "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", - "apollo_module", "rich_presence", - "enable", true - ) - ); + private static final ClientboundLunarPacket MOD_SETTINGS = new ClientboundLunarPacket(Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", + "apollo_module", "mod_setting", + "enable", true, + "properties", Map.of( + "replaymod.enabled", true, + "minimap.enabled", false, + "weather-changer.enabled", false, + "day-counter.enabled", false, + "bossbar.enabled", false, + "saturation.enabled", false, + "direction-hud.enabled", false, + "armorstatus.enabled", false, + "titles.enabled", false + ) + )); + private static final ClientboundLunarPacket ENABLE_RICH_PRESENCE = new ClientboundLunarPacket(Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", + "apollo_module", "rich_presence", + "enable", true + )); @Override public void registerListeners(GlobalEventHandler events) { - events.addListener(PlayerPluginMessageEvent.class, this::handleLunarPacket); + events.addListener(ModChannelRegisterEvent.class, this::handleLunarPacket); } @Override public void registerPackets(PacketRegistry registry) { registry.register(ClientboundLunarPacket.TYPE); - registry.register(ServerboundLunarPacket.LUNAR_APOLLO_TYPE, (player, packet) -> { - }); registry.register(ServerboundLunarPacket.APOLLO_JSON_TYPE, (player, packet) -> { + System.out.println("Received lunar json packet: " + packet.message()); }); - } - private void handleLunarPacket(PlayerPluginMessageEvent event) { - if (event.getPlayer().hasTag(LUNAR_SUPPORT_ENABLED)) { - return; - } + private void handleLunarPacket(ModChannelRegisterEvent event) { + if (!event.getChannels().contains("lunar:apollo")) return; + if (event.getPlayer().hasTag(LUNAR_SUPPORT_ENABLED)) return; - if (event.getIdentifier().equalsIgnoreCase("minecraft:register")) { - if (event.getMessageString().contains("lunar:apollo") || event.getMessageString().contains("apollo:json")) { - event.getPlayer().setTag(LUNAR_SUPPORT_ENABLED, true); - MOD_SETTINGS.send(event.getPlayer()); - ENABLE_RICH_PRESENCE.send(event.getPlayer()); - } - } + event.getPlayer().setTag(LUNAR_SUPPORT_ENABLED, true); + MOD_SETTINGS.send(event.getPlayer()); + ENABLE_RICH_PRESENCE.send(event.getPlayer()); } @Override - public void setRichPresence(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName) { + public void setRichPresence( + @NotNull Player player, @NotNull String playerState, @NotNull String gameName, + @NotNull String gameVariantName + ) { new ClientboundLunarPacket( - Map.of( - "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", - "game_name", gameName, - // This is a lunar bug, it seems to escape the / character and discord doesn't undo it, so we need to replace it with another character - "game_variant_name", gameVariantName.replace("/", "∕"), - "player_state", playerState - ) + Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", + "game_name", gameName, + // This is a lunar bug, it seems to escape the / character and discord doesn't undo it, so we need to replace it with another character + "game_variant_name", gameVariantName.replace("/", "∕"), + "player_state", playerState + ) ).send(player); } @Override public void clearRichPresence(@NotNull Player player) { new ClientboundLunarPacket( - Map.of( - "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.ResetServerRichPresenceMessage" - ) + Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.ResetServerRichPresenceMessage" + ) ).send(player); } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java index d1291c853..5ae47f91e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java @@ -1,22 +1,36 @@ package net.hollowcube.compat.lunar.packets; +import com.google.protobuf.Any; +import com.google.protobuf.Message; +import com.lunarclient.apollo.player.v1.PlayerHandshakeMessage; import net.hollowcube.compat.api.packet.ServerboundModPacket; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.utils.ThrowingFunction; + +import java.util.List; // For lunar to advertise itself we have to ensure we register the channels // Rather than send another register message, we'll just register some dummy handlers // Who knows, maybe they'll be useful in the future if lunar add more serverbound packets -public record ServerboundLunarPacket() implements ServerboundModPacket { - public static final Type LUNAR_APOLLO_TYPE = Type.of( - "lunar", - "apollo", - buffer -> new ServerboundLunarPacket() - ); +public record ServerboundLunarPacket( + Message message +) implements ServerboundModPacket { - public static final Type APOLLO_JSON_TYPE = Type.of( - "apollo", - "json", - buffer -> new ServerboundLunarPacket() + private static final List> SUPPORTED_MESSAGES = List.of( + PlayerHandshakeMessage.class ); + private static final ThrowingFunction READER = buffer -> { + var bytes = buffer.read(NetworkBuffer.RAW_BYTES); + var any = Any.parseFrom(bytes); + for (var type : SUPPORTED_MESSAGES) { + if (any.is(type)) { + return new ServerboundLunarPacket(any.unpack(type)); + } + } + return new ServerboundLunarPacket(any); + }; + + public static final Type APOLLO_JSON_TYPE = Type.of("apollo", "json", READER); @Override public Type getType() { From c50c53aac6560167eea0166b26188d04ab60feb7 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Sun, 29 Mar 2026 00:34:29 -0230 Subject: [PATCH 15/25] fix: bad merge --- bin/hub/build.gradle.kts | 1 + .../mapmaker/hub/HubPlayerState.java | 5 +++++ .../discord/DiscordRichPresenceManager.java | 15 +++++++++------ .../mapmaker/editor/EditorState.java | 6 ++++++ .../mapmaker/runtime/parkour/ParkourState.java | 18 ++++++++++++++++++ 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/bin/hub/build.gradle.kts b/bin/hub/build.gradle.kts index 919e90083..4ebbc0e2e 100644 --- a/bin/hub/build.gradle.kts +++ b/bin/hub/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(project(":modules:map-core")) implementation(project(":modules:map-runtime")) implementation(project(":modules:terraform")) + implementation(project(":modules:compat")) implementation(libs.minestom) implementation(libs.bundles.adventure) diff --git a/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java b/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java index 33ab3d2cf..874ebd2b1 100644 --- a/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java +++ b/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java @@ -2,6 +2,7 @@ import net.hollowcube.common.util.FontUtil; import net.hollowcube.common.util.FutureUtil; +import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; import net.hollowcube.mapmaker.CoreFeatureFlags; import net.hollowcube.mapmaker.PlayerSettings; import net.hollowcube.mapmaker.hub.feature.event.christmas.AdventCalendarItem; @@ -64,6 +65,10 @@ public void configurePlayer(HubMapWorld world, Player player, @Nullable HubPlaye if (player instanceof MapPlayer mp) { mp.setCanSendPose(false); } + + DiscordRichPresenceManager.queueRichPresenceUpdate( + player, "the lobby", "", "In" + ); } @Override diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java index b25e186b7..9136186b2 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java @@ -3,6 +3,7 @@ import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; +import java.time.Duration; import java.util.HashSet; import java.util.ServiceLoader; import java.util.Set; @@ -16,13 +17,15 @@ public class DiscordRichPresenceManager { } - public static void setRichPresence(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName) { - for (DiscordRichPresenceProvider provider : PROVIDERS) { - if (provider.isRichPresenceSupportedFor(player)) { - provider.setRichPresence(player, playerState, gameName, gameVariantName); - return; + public static void queueRichPresenceUpdate(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName) { + player.scheduler().buildTask(() -> { + for (DiscordRichPresenceProvider provider : PROVIDERS) { + if (provider.isRichPresenceSupportedFor(player)) { + provider.setRichPresence(player, playerState, gameName, gameVariantName); + break; + } } - } + }).delay(Duration.ofMillis(2500)).schedule(); } public static void clearRichPresence(@NotNull Player player) { diff --git a/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java b/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java index 77d81da7d..e28d15185 100644 --- a/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java +++ b/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java @@ -1,5 +1,6 @@ package net.hollowcube.mapmaker.editor; +import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; import net.hollowcube.compat.axiom.AxiomPlayer; import net.hollowcube.compat.noxesium.components.NoxesiumGameComponents; import net.hollowcube.compat.noxesium.handshake.NoxesiumPlayer; @@ -108,4 +109,9 @@ public void resetPlayer(EditorMapWorld world, Player player, @Nullable EditorSta } } + @Override + default void configurePlayer(EditorMapWorld world, Player player, @Nullable EditorState lastState) { + PlayerState.super.configurePlayer(world, player, lastState); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, "a map", "", "Building"); + } } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java index 0b85e979d..cd34eb734 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java @@ -2,6 +2,7 @@ import net.hollowcube.common.util.FutureUtil; import net.hollowcube.common.util.ProtocolVersions; +import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; import net.hollowcube.compat.noxesium.components.NoxesiumGameComponents; import net.hollowcube.compat.noxesium.handshake.NoxesiumPlayer; import net.hollowcube.mapmaker.ExceptionReporter; @@ -221,6 +222,8 @@ public void configurePlayer(ParkourMapWorld world, Player player, @Nullable Park // the touching state immediately because we should already be inside. ((MapPlayer) player).updateTouchingState(world, false); } + + queueRichPresenceUpdate(world, player, "Playing"); } @Override @@ -253,6 +256,8 @@ public void configurePlayer(ParkourMapWorld world, Player player, @Nullable Park ((MapPlayer) player).resetTouchingState(); ((MapPlayer) player).updateTouchingState(world, true); + + queueRichPresenceUpdate(world, player, "Testing"); } } @@ -296,6 +301,8 @@ public void configurePlayer(ParkourMapWorld world, Player player, @Nullable Park gameState.set(SpectateHelper.GAME_STATE_SAVED, true); } + + queueRichPresenceUpdate(world, player, "Spectating"); } @Override @@ -336,6 +343,8 @@ public void configurePlayer(ParkourMapWorld world, Player player, @Nullable Park // This is delegated back to the world kinda stupidly. We need to be able // to override the behavior for testing worlds world.performFinishEffects(player, saveState); + + queueRichPresenceUpdate(world, player, "Playing"); } @Override @@ -379,4 +388,13 @@ private static void writeSaveState(ParkourMapWorld world, Player player, SaveSta } } + private static void queueRichPresenceUpdate(ParkourMapWorld world, Player player, String activity) { + var map = world.map(); + if (map.isPublished()) { + DiscordRichPresenceManager.queueRichPresenceUpdate(player, map.name(), "/play " + map.publishedIdString(), activity); + } else { + DiscordRichPresenceManager.queueRichPresenceUpdate(player, "a map", "", activity); + } + } + } From 54b3506655ac4c13dcd2b12663f53cb0a6899407 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Sun, 29 Mar 2026 01:17:39 -0230 Subject: [PATCH 16/25] fix: discord rich presence on lunar --- .../net/hollowcube/mapmaker/hub/HubPlayerState.java | 4 +--- .../api/discord/DiscordRichPresenceManager.java | 4 ++-- .../api/discord/DiscordRichPresenceProvider.java | 2 +- .../compat/feather/FeatherCompatProvider.java | 6 +++--- .../hollowcube/compat/lunar/LunarCompatProvider.java | 11 +++++------ .../net/hollowcube/mapmaker/editor/EditorState.java | 2 +- .../mapmaker/runtime/parkour/ParkourState.java | 4 ++-- 7 files changed, 15 insertions(+), 18 deletions(-) diff --git a/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java b/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java index 874ebd2b1..58a91c8a0 100644 --- a/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java +++ b/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java @@ -66,9 +66,7 @@ public void configurePlayer(HubMapWorld world, Player player, @Nullable HubPlaye mp.setCanSendPose(false); } - DiscordRichPresenceManager.queueRichPresenceUpdate( - player, "the lobby", "", "In" - ); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, "In the lobby", ""); } @Override diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java index 9136186b2..b0d570c18 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java @@ -17,11 +17,11 @@ public class DiscordRichPresenceManager { } - public static void queueRichPresenceUpdate(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName) { + public static void queueRichPresenceUpdate(@NotNull Player player, @NotNull String line1, @NotNull String line2) { player.scheduler().buildTask(() -> { for (DiscordRichPresenceProvider provider : PROVIDERS) { if (provider.isRichPresenceSupportedFor(player)) { - provider.setRichPresence(player, playerState, gameName, gameVariantName); + provider.setRichPresence(player, line1, line2); break; } } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java index 204f73776..493f8cf2e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java @@ -4,7 +4,7 @@ import org.jetbrains.annotations.NotNull; public interface DiscordRichPresenceProvider { - void setRichPresence(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName); + void setRichPresence(@NotNull Player player, @NotNull String line1, @NotNull String line2); void clearRichPresence(@NotNull Player player); diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java index 6028bbc06..4892e090e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java @@ -50,13 +50,13 @@ public void registerPackets(PacketRegistry registry) { } @Override - public void setRichPresence(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull String gameVariantName) { + public void setRichPresence(@NotNull Player player, @NotNull String line1, @NotNull String line2) { new ClientboundFeatherPacket( new S2CSetDiscordActivity( IMAGE_URL, "Hollow Cube", - gameVariantName, - playerState + " " + gameName + " on Hollow Cube", + line2, + line1, null, null, null, diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index 1801df6d6..7b0e418fa 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -65,16 +65,15 @@ private void handleLunarPacket(ModChannelRegisterEvent event) { @Override public void setRichPresence( - @NotNull Player player, @NotNull String playerState, @NotNull String gameName, - @NotNull String gameVariantName + @NotNull Player player, + @NotNull String line1, + @NotNull String line2 ) { new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", - "game_name", gameName, - // This is a lunar bug, it seems to escape the / character and discord doesn't undo it, so we need to replace it with another character - "game_variant_name", gameVariantName.replace("/", "∕"), - "player_state", playerState + "game_name", line1, + "player_state", line2 ) ).send(player); } diff --git a/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java b/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java index e28d15185..8932013fb 100644 --- a/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java +++ b/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java @@ -112,6 +112,6 @@ public void resetPlayer(EditorMapWorld world, Player player, @Nullable EditorSta @Override default void configurePlayer(EditorMapWorld world, Player player, @Nullable EditorState lastState) { PlayerState.super.configurePlayer(world, player, lastState); - DiscordRichPresenceManager.queueRichPresenceUpdate(player, "a map", "", "Building"); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, "Building", "a map"); } } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java index cd34eb734..96da90f4b 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java @@ -391,9 +391,9 @@ private static void writeSaveState(ParkourMapWorld world, Player player, SaveSta private static void queueRichPresenceUpdate(ParkourMapWorld world, Player player, String activity) { var map = world.map(); if (map.isPublished()) { - DiscordRichPresenceManager.queueRichPresenceUpdate(player, map.name(), "/play " + map.publishedIdString(), activity); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, activity + " " + map.name(), "/play " + map.publishedIdString()); } else { - DiscordRichPresenceManager.queueRichPresenceUpdate(player, "a map", "", activity); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, activity, "a map"); } } From 32c1db891653ebdda26efedcfc5b8810990f5377 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Sun, 29 Mar 2026 01:29:39 -0230 Subject: [PATCH 17/25] fix: lunar discord rich presence (there is no docs on what the client does with these values) --- .../compat/api/discord/DiscordRichPresenceManager.java | 4 ++-- .../compat/api/discord/DiscordRichPresenceProvider.java | 2 +- .../hollowcube/compat/feather/FeatherCompatProvider.java | 6 +++--- .../net/hollowcube/compat/lunar/LunarCompatProvider.java | 8 ++++---- .../java/net/hollowcube/mapmaker/editor/EditorState.java | 2 +- .../hollowcube/mapmaker/runtime/parkour/ParkourState.java | 8 ++++++-- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java index b0d570c18..e7b18455a 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java @@ -17,11 +17,11 @@ public class DiscordRichPresenceManager { } - public static void queueRichPresenceUpdate(@NotNull Player player, @NotNull String line1, @NotNull String line2) { + public static void queueRichPresenceUpdate(@NotNull Player player, @NotNull String activity, @NotNull String map) { player.scheduler().buildTask(() -> { for (DiscordRichPresenceProvider provider : PROVIDERS) { if (provider.isRichPresenceSupportedFor(player)) { - provider.setRichPresence(player, line1, line2); + provider.setRichPresence(player, activity, map); break; } } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java index 493f8cf2e..20f7fdfc1 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java @@ -4,7 +4,7 @@ import org.jetbrains.annotations.NotNull; public interface DiscordRichPresenceProvider { - void setRichPresence(@NotNull Player player, @NotNull String line1, @NotNull String line2); + void setRichPresence(@NotNull Player player, @NotNull String activity, @NotNull String map); void clearRichPresence(@NotNull Player player); diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java index 4892e090e..405645533 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java @@ -50,13 +50,13 @@ public void registerPackets(PacketRegistry registry) { } @Override - public void setRichPresence(@NotNull Player player, @NotNull String line1, @NotNull String line2) { + public void setRichPresence(@NotNull Player player, @NotNull String activity, @NotNull String map) { new ClientboundFeatherPacket( new S2CSetDiscordActivity( IMAGE_URL, "Hollow Cube", - line2, - line1, + map, + "%s on Hollow Cube".formatted(activity), null, null, null, diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index 7b0e418fa..022b75c7b 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -66,14 +66,14 @@ private void handleLunarPacket(ModChannelRegisterEvent event) { @Override public void setRichPresence( @NotNull Player player, - @NotNull String line1, - @NotNull String line2 + @NotNull String activity, + @NotNull String map ) { new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", - "game_name", line1, - "player_state", line2 + "game_name", activity, + "map_name", map ) ).send(player); } diff --git a/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java b/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java index 8932013fb..0228984fe 100644 --- a/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java +++ b/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java @@ -112,6 +112,6 @@ public void resetPlayer(EditorMapWorld world, Player player, @Nullable EditorSta @Override default void configurePlayer(EditorMapWorld world, Player player, @Nullable EditorState lastState) { PlayerState.super.configurePlayer(world, player, lastState); - DiscordRichPresenceManager.queueRichPresenceUpdate(player, "Building", "a map"); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, "Building", ""); } } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java index 96da90f4b..2ba5dc323 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java @@ -391,9 +391,13 @@ private static void writeSaveState(ParkourMapWorld world, Player player, SaveSta private static void queueRichPresenceUpdate(ParkourMapWorld world, Player player, String activity) { var map = world.map(); if (map.isPublished()) { - DiscordRichPresenceManager.queueRichPresenceUpdate(player, activity + " " + map.name(), "/play " + map.publishedIdString()); + DiscordRichPresenceManager.queueRichPresenceUpdate( + player, + activity, + "%s (/play %s)".formatted(map.name(), map.publishedIdString()) + ); } else { - DiscordRichPresenceManager.queueRichPresenceUpdate(player, activity, "a map"); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, activity, ""); } } From e1c1e4be070983827043860ecfbbeb8097c0575f Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Sun, 29 Mar 2026 01:40:07 -0230 Subject: [PATCH 18/25] lunar sucks so time to see where these fields show up to --- .../net/hollowcube/compat/lunar/LunarCompatProvider.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index 022b75c7b..72100ee16 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -72,8 +72,12 @@ public void setRichPresence( new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", - "game_name", activity, - "map_name", map + "game_name", "game_name", + "game_variant_name", "game_variant_name", + "game_state", "game_state", + "player_state", "player_state", + "map_name", map, + "sub_server", "sub_server" ) ).send(player); } From 333ea70d7a019092d9c63eb8019ffa00df6c4b69 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Sun, 29 Mar 2026 01:58:20 -0230 Subject: [PATCH 19/25] lets see if this is good enough for lunar --- .../net/hollowcube/compat/lunar/LunarCompatProvider.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index 72100ee16..04d6de28e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -72,12 +72,8 @@ public void setRichPresence( new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", - "game_name", "game_name", - "game_variant_name", "game_variant_name", - "game_state", "game_state", - "player_state", "player_state", - "map_name", map, - "sub_server", "sub_server" + "game_name", activity, + "game_variant_name", map ) ).send(player); } From b71ae6c4555360ce0a6c32a0bab65d4d9bd00174 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Sun, 29 Mar 2026 02:05:10 -0230 Subject: [PATCH 20/25] lets see if this is good enough for lunar round 2 --- .../net/hollowcube/compat/lunar/LunarCompatProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index 04d6de28e..ea9cdf7b6 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -9,7 +9,6 @@ import net.hollowcube.compat.lunar.packets.ServerboundLunarPacket; import net.minestom.server.entity.Player; import net.minestom.server.event.GlobalEventHandler; -import net.minestom.server.event.player.PlayerPluginMessageEvent; import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; @@ -72,8 +71,9 @@ public void setRichPresence( new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", - "game_name", activity, - "game_variant_name", map + "player_state", activity, + // This is a lunar bug, it seems to escape the / character and discord doesn't undo it, so we need to replace it with another character + "game_variant_name", map.replace("/", "∕") ) ).send(player); } From 009a157125e8e34b3a0c8f5f8942ac9bf6aed1d0 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Sun, 29 Mar 2026 02:14:39 -0230 Subject: [PATCH 21/25] lunar doesnt like our special slash --- .../java/net/hollowcube/compat/lunar/LunarCompatProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index ea9cdf7b6..caf8e8faa 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -72,8 +72,8 @@ public void setRichPresence( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", "player_state", activity, - // This is a lunar bug, it seems to escape the / character and discord doesn't undo it, so we need to replace it with another character - "game_variant_name", map.replace("/", "∕") + // This is a lunar bug, it seems to escape the / character and discord doesn't undo it + "game_variant_name", map.replace("/", "") ) ).send(player); } From b990ca995130c7d5420802f238f3c4e93fe952f8 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Mon, 30 Mar 2026 06:30:01 -0230 Subject: [PATCH 22/25] I was finally told how lunar uses these magic values and its dumb --- .../hollowcube/mapmaker/hub/HubPlayerState.java | 2 +- .../api/discord/DiscordRichPresenceManager.java | 9 +++++++-- .../api/discord/DiscordRichPresenceProvider.java | 14 +++++++++++++- .../compat/feather/FeatherCompatProvider.java | 11 ++++++++--- .../compat/lunar/LunarCompatProvider.java | 12 ++++++++---- .../lunar/packets/ClientboundLunarPacket.java | 3 ++- .../hollowcube/mapmaker/editor/EditorState.java | 2 +- .../mapmaker/runtime/parkour/ParkourState.java | 5 +++-- 8 files changed, 43 insertions(+), 15 deletions(-) diff --git a/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java b/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java index 58a91c8a0..dc13531ac 100644 --- a/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java +++ b/bin/hub/src/main/java/net/hollowcube/mapmaker/hub/HubPlayerState.java @@ -66,7 +66,7 @@ public void configurePlayer(HubMapWorld world, Player player, @Nullable HubPlaye mp.setCanSendPose(false); } - DiscordRichPresenceManager.queueRichPresenceUpdate(player, "In the lobby", ""); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, "In the", "lobby", "play.hollowcube.net"); } @Override diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java index e7b18455a..e2c949d02 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceManager.java @@ -2,6 +2,7 @@ import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.time.Duration; import java.util.HashSet; @@ -17,11 +18,15 @@ public class DiscordRichPresenceManager { } - public static void queueRichPresenceUpdate(@NotNull Player player, @NotNull String activity, @NotNull String map) { + public static void queueRichPresenceUpdate( + @NotNull Player player, + @NotNull String activity, @NotNull String name, + @Nullable String details + ) { player.scheduler().buildTask(() -> { for (DiscordRichPresenceProvider provider : PROVIDERS) { if (provider.isRichPresenceSupportedFor(player)) { - provider.setRichPresence(player, activity, map); + provider.setRichPresence(player, activity, name, details); break; } } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java index 20f7fdfc1..0e8aaa4bb 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/api/discord/DiscordRichPresenceProvider.java @@ -2,9 +2,21 @@ import net.minestom.server.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public interface DiscordRichPresenceProvider { - void setRichPresence(@NotNull Player player, @NotNull String activity, @NotNull String map); + + /** + * Sets the rich presence for a player. + * Format: + * 0: " on Hollow Cube" + * 1: "" If details is null or empty, this line will default to whaterver the provide wants, on lunar this is the mc version, on feather nothing. + */ + void setRichPresence( + @NotNull Player player, + @NotNull String activity, @NotNull String name, + @Nullable String details + ); void clearRichPresence(@NotNull Player player); diff --git a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java index 405645533..d909d9e6c 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/feather/FeatherCompatProvider.java @@ -17,6 +17,7 @@ import net.minestom.server.event.player.PlayerDisconnectEvent; import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; @@ -50,13 +51,17 @@ public void registerPackets(PacketRegistry registry) { } @Override - public void setRichPresence(@NotNull Player player, @NotNull String activity, @NotNull String map) { + public void setRichPresence( + @NotNull Player player, + @NotNull String activity, @NotNull String name, + @Nullable String details + ) { new ClientboundFeatherPacket( new S2CSetDiscordActivity( IMAGE_URL, "Hollow Cube", - map, - "%s on Hollow Cube".formatted(activity), + details, + "%s %s on Hollow Cube".formatted(activity, name), null, null, null, diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index caf8e8faa..b7d4d935e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -11,6 +11,7 @@ import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Map; @@ -65,15 +66,18 @@ private void handleLunarPacket(ModChannelRegisterEvent event) { @Override public void setRichPresence( @NotNull Player player, - @NotNull String activity, - @NotNull String map + @NotNull String activity, @NotNull String name, + @Nullable String details ) { + // This is a lunar bug, it seems to escape the / character and discord doesn't undo it + details = details != null ? details : ""; + new ClientboundLunarPacket( Map.of( "@type", ClientboundLunarPacket.TYPE_PREFIX + "richpresence.v1.OverrideServerRichPresenceMessage", "player_state", activity, - // This is a lunar bug, it seems to escape the / character and discord doesn't undo it - "game_variant_name", map.replace("/", "") + "game_name", name, + "game_variant_name", details ) ).send(player); } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java index 820096443..62402281d 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ClientboundLunarPacket.java @@ -1,6 +1,7 @@ package net.hollowcube.compat.lunar.packets; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import net.hollowcube.compat.api.packet.ClientboundModPacket; import net.minestom.server.network.NetworkBuffer; import org.jetbrains.annotations.NotNull; @@ -11,7 +12,7 @@ public record ClientboundLunarPacket( @NotNull Map data ) implements ClientboundModPacket { - private static final Gson GSON = new Gson(); + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); public static final String TYPE_PREFIX = "type.googleapis.com/lunarclient.apollo."; diff --git a/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java b/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java index 0228984fe..a8494517d 100644 --- a/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java +++ b/modules/map-editor/src/main/java/net/hollowcube/mapmaker/editor/EditorState.java @@ -112,6 +112,6 @@ public void resetPlayer(EditorMapWorld world, Player player, @Nullable EditorSta @Override default void configurePlayer(EditorMapWorld world, Player player, @Nullable EditorState lastState) { PlayerState.super.configurePlayer(world, player, lastState); - DiscordRichPresenceManager.queueRichPresenceUpdate(player, "Building", ""); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, "Building", "a map", ""); } } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java index 2ba5dc323..7f9281fbc 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java @@ -394,10 +394,11 @@ private static void queueRichPresenceUpdate(ParkourMapWorld world, Player player DiscordRichPresenceManager.queueRichPresenceUpdate( player, activity, - "%s (/play %s)".formatted(map.name(), map.publishedIdString()) + map.name(), + "/play %s".formatted(map.publishedIdString()) ); } else { - DiscordRichPresenceManager.queueRichPresenceUpdate(player, activity, ""); + DiscordRichPresenceManager.queueRichPresenceUpdate(player, activity, "a map", ""); } } From 055327748f6df1cc457eda5cce6c8c5502f9aea0 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Mon, 30 Mar 2026 06:42:06 -0230 Subject: [PATCH 23/25] lunar still needs a special slash --- .../java/net/hollowcube/compat/lunar/LunarCompatProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index b7d4d935e..1791c8f1c 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -70,7 +70,7 @@ public void setRichPresence( @Nullable String details ) { // This is a lunar bug, it seems to escape the / character and discord doesn't undo it - details = details != null ? details : ""; + details = details != null ? details.replace("/", "∕") : ""; new ClientboundLunarPacket( Map.of( From 3e1768cb8fca53daf345f8d7f78b8d9701c0e3f9 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Tue, 5 May 2026 12:54:19 -0230 Subject: [PATCH 24/25] feat: json lunar responses --- modules/compat/build.gradle.kts | 2 +- .../compat/lunar/LunarCompatProvider.java | 49 ++++++++++--------- .../hollowcube/compat/lunar/LunarPackets.java | 47 ++++++++++++++++++ .../compat/lunar/events/LunarPlayerEvent.java | 14 ++++++ .../lunar/events/LunarPlayerInitEvent.java | 11 +++++ .../compat/lunar/events/package-info.java | 4 ++ .../lunar/packets/ServerboundLunarPacket.java | 34 ++++++------- .../InstalledModsResponsePagePayload.java | 29 +++++++++++ .../payload/InstalledModsResponsePayload.java | 46 +++++++++++++++++ .../compat/lunar/payload/LunarPayload.java | 22 +++++++++ .../lunar/payload/LunarPayloadType.java | 20 ++++++++ .../payload/PaginatedPayloadHandler.java | 29 +++++++++++ .../DiscordRichPresenceFeatureProvider.java | 41 ---------------- 13 files changed, 262 insertions(+), 86 deletions(-) create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarPackets.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/events/LunarPlayerEvent.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/events/LunarPlayerInitEvent.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/events/package-info.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/InstalledModsResponsePagePayload.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/InstalledModsResponsePayload.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/LunarPayload.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/LunarPayloadType.java create mode 100644 modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/PaginatedPayloadHandler.java delete mode 100644 modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java diff --git a/modules/compat/build.gradle.kts b/modules/compat/build.gradle.kts index b783fb0ff..712fe6091 100644 --- a/modules/compat/build.gradle.kts +++ b/modules/compat/build.gradle.kts @@ -11,5 +11,5 @@ dependencies { implementation(libs.posthog) implementation(libs.zstd) implementation(libs.feather) - implementation(libs.apollo) + implementation(libs.caffeine) } diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java index 1791c8f1c..12874a93e 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarCompatProvider.java @@ -5,9 +5,14 @@ import net.hollowcube.compat.api.ModChannelRegisterEvent; import net.hollowcube.compat.api.discord.DiscordRichPresenceProvider; import net.hollowcube.compat.api.packet.PacketRegistry; +import net.hollowcube.compat.lunar.events.LunarPlayerInitEvent; import net.hollowcube.compat.lunar.packets.ClientboundLunarPacket; import net.hollowcube.compat.lunar.packets.ServerboundLunarPacket; +import net.hollowcube.compat.lunar.payload.InstalledModsResponsePayload; +import net.hollowcube.compat.lunar.payload.LunarPayload; +import net.hollowcube.compat.lunar.payload.PaginatedPayloadHandler; import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; @@ -18,28 +23,9 @@ @AutoService({CompatProvider.class, DiscordRichPresenceProvider.class}) public class LunarCompatProvider implements CompatProvider, DiscordRichPresenceProvider { + public static final Tag LUNAR_SUPPORT_ENABLED = Tag.Transient("mapmaker:lunar/enabled"); - private static final ClientboundLunarPacket MOD_SETTINGS = new ClientboundLunarPacket(Map.of( - "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", - "apollo_module", "mod_setting", - "enable", true, - "properties", Map.of( - "replaymod.enabled", true, - "minimap.enabled", false, - "weather-changer.enabled", false, - "day-counter.enabled", false, - "bossbar.enabled", false, - "saturation.enabled", false, - "direction-hud.enabled", false, - "armorstatus.enabled", false, - "titles.enabled", false - ) - )); - private static final ClientboundLunarPacket ENABLE_RICH_PRESENCE = new ClientboundLunarPacket(Map.of( - "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", - "apollo_module", "rich_presence", - "enable", true - )); + private static final PaginatedPayloadHandler PAGINATED_PAYLOAD_HANDLER = new PaginatedPayloadHandler(); @Override public void registerListeners(GlobalEventHandler events) { @@ -50,7 +36,15 @@ public void registerListeners(GlobalEventHandler events) { public void registerPackets(PacketRegistry registry) { registry.register(ClientboundLunarPacket.TYPE); registry.register(ServerboundLunarPacket.APOLLO_JSON_TYPE, (player, packet) -> { - System.out.println("Received lunar json packet: " + packet.message()); + var payload = packet.payload(); + + if (payload instanceof LunarPayload.Paginated paginated) { + payload = PAGINATED_PAYLOAD_HANDLER.handle(paginated); + } + + if (payload != null) { + handleLunarPayload(player, payload); + } }); } @@ -59,8 +53,15 @@ private void handleLunarPacket(ModChannelRegisterEvent event) { if (event.getPlayer().hasTag(LUNAR_SUPPORT_ENABLED)) return; event.getPlayer().setTag(LUNAR_SUPPORT_ENABLED, true); - MOD_SETTINGS.send(event.getPlayer()); - ENABLE_RICH_PRESENCE.send(event.getPlayer()); + LunarPackets.getModSettingsPacket().send(event.getPlayer()); + LunarPackets.getEnableRichPresencePacket().send(event.getPlayer()); + LunarPackets.getRequestModsPacket().send(event.getPlayer()); + } + + private void handleLunarPayload(Player player, LunarPayload payload) { + if (payload instanceof InstalledModsResponsePayload response) { + EventDispatcher.call(new LunarPlayerInitEvent(player, response)); + } } @Override diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarPackets.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarPackets.java new file mode 100644 index 000000000..ec12b222a --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/LunarPackets.java @@ -0,0 +1,47 @@ +package net.hollowcube.compat.lunar; + +import net.hollowcube.compat.lunar.packets.ClientboundLunarPacket; + +import java.util.Map; +import java.util.UUID; + +public class LunarPackets { + + private static final ClientboundLunarPacket MOD_SETTINGS = new ClientboundLunarPacket(Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", + "apollo_module", "mod_setting", + "enable", true, + "properties", Map.of( + "replaymod.enabled", true, + "minimap.enabled", false, + "weather-changer.enabled", false, + "day-counter.enabled", false, + "bossbar.enabled", false, + "saturation.enabled", false, + "direction-hud.enabled", false, + "armorstatus.enabled", false, + "titles.enabled", false + ) + )); + + private static final ClientboundLunarPacket ENABLE_RICH_PRESENCE = new ClientboundLunarPacket(Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "configurable.v1.ConfigurableSettings", + "apollo_module", "rich_presence", + "enable", true + )); + + public static ClientboundLunarPacket getModSettingsPacket() { + return MOD_SETTINGS; + } + + public static ClientboundLunarPacket getEnableRichPresencePacket() { + return ENABLE_RICH_PRESENCE; + } + + public static ClientboundLunarPacket getRequestModsPacket() { + return new ClientboundLunarPacket(Map.of( + "@type", ClientboundLunarPacket.TYPE_PREFIX + "modsetting.v1.InstalledModsRequest", + "request_id", UUID.randomUUID().toString() + )); + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/LunarPlayerEvent.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/LunarPlayerEvent.java new file mode 100644 index 000000000..952e2b575 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/LunarPlayerEvent.java @@ -0,0 +1,14 @@ +package net.hollowcube.compat.lunar.events; + +import net.minestom.server.entity.Player; +import net.minestom.server.event.trait.PlayerEvent; + +public interface LunarPlayerEvent extends PlayerEvent { + + Player player(); + + @Override + default Player getPlayer() { + return player(); + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/LunarPlayerInitEvent.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/LunarPlayerInitEvent.java new file mode 100644 index 000000000..eadb41333 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/LunarPlayerInitEvent.java @@ -0,0 +1,11 @@ +package net.hollowcube.compat.lunar.events; + +import net.hollowcube.compat.lunar.payload.InstalledModsResponsePayload; +import net.minestom.server.entity.Player; + +public record LunarPlayerInitEvent( + Player player, + InstalledModsResponsePayload payload +) implements LunarPlayerEvent { + +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/package-info.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/package-info.java new file mode 100644 index 000000000..6e17f17e7 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/events/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.compat.lunar.events; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java index 5ae47f91e..69eeb70b5 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/packets/ServerboundLunarPacket.java @@ -1,33 +1,27 @@ package net.hollowcube.compat.lunar.packets; -import com.google.protobuf.Any; -import com.google.protobuf.Message; -import com.lunarclient.apollo.player.v1.PlayerHandshakeMessage; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import net.hollowcube.compat.api.packet.ServerboundModPacket; +import net.hollowcube.compat.lunar.payload.LunarPayload; +import net.hollowcube.compat.lunar.payload.LunarPayloadType; +import net.minestom.server.codec.Transcoder; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.utils.ThrowingFunction; -import java.util.List; +public record ServerboundLunarPacket(LunarPayload payload) implements ServerboundModPacket { -// For lunar to advertise itself we have to ensure we register the channels -// Rather than send another register message, we'll just register some dummy handlers -// Who knows, maybe they'll be useful in the future if lunar add more serverbound packets -public record ServerboundLunarPacket( - Message message -) implements ServerboundModPacket { - - private static final List> SUPPORTED_MESSAGES = List.of( - PlayerHandshakeMessage.class - ); private static final ThrowingFunction READER = buffer -> { - var bytes = buffer.read(NetworkBuffer.RAW_BYTES); - var any = Any.parseFrom(bytes); - for (var type : SUPPORTED_MESSAGES) { - if (any.is(type)) { - return new ServerboundLunarPacket(any.unpack(type)); + var string = new String(buffer.read(NetworkBuffer.RAW_BYTES)); + var json = JsonParser.parseString(string); + if (json instanceof JsonObject object) { + var type = object.get("@type").getAsString(); + var payloadType = LunarPayloadType.REGISTRY.get(type); + if (payloadType != null) { + return new ServerboundLunarPacket(payloadType.codec().decode(Transcoder.JSON, object).orElseThrow()); } } - return new ServerboundLunarPacket(any); + return new ServerboundLunarPacket(new LunarPayload.Unhandled(json)); }; public static final Type APOLLO_JSON_TYPE = Type.of("apollo", "json", READER); diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/InstalledModsResponsePagePayload.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/InstalledModsResponsePagePayload.java new file mode 100644 index 000000000..90368642a --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/InstalledModsResponsePagePayload.java @@ -0,0 +1,29 @@ +package net.hollowcube.compat.lunar.payload; + +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; + +import java.util.List; + +public record InstalledModsResponsePagePayload( + String id, + int page, + int totalPages, + List groups +) implements LunarPayload.Paginated { + + public static final String ID = "lunarclient.apollo.modsetting.v1.InstalledModsResponse"; + public static final StructCodec CODEC = StructCodec.struct( + "request_id", Codec.STRING, InstalledModsResponsePagePayload::id, + "page", Codec.INT, InstalledModsResponsePagePayload::page, + "total_pages", Codec.INT, InstalledModsResponsePagePayload::totalPages, + "mod_groups", InstalledModsResponsePayload.ModGroup.CODEC.list(), InstalledModsResponsePagePayload::groups, + InstalledModsResponsePagePayload::new + ); + public static final LunarPayloadType TYPE = new LunarPayloadType<>(ID, CODEC); + + @Override + public LunarPayload group(List pages) { + return InstalledModsResponsePayload.fromPages(pages); + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/InstalledModsResponsePayload.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/InstalledModsResponsePayload.java new file mode 100644 index 000000000..8baf806ae --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/InstalledModsResponsePayload.java @@ -0,0 +1,46 @@ +package net.hollowcube.compat.lunar.payload; + +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public record InstalledModsResponsePayload(List groups) implements LunarPayload { + + public static InstalledModsResponsePayload fromPages(List pages) { + Map> groups = new HashMap<>(); + for (var page : pages) { + for (var group : page.groups()) { + groups.computeIfAbsent(group.type(), _ -> new ArrayList<>()).addAll(group.mods()); + } + } + + return new InstalledModsResponsePayload( + groups.entrySet() + .stream() + .map(e -> new ModGroup(e.getKey(), e.getValue())) + .toList() + ); + } + + public record ModGroup(String type, List mods) { + + public static final StructCodec CODEC = StructCodec.struct( + "type", Codec.STRING, ModGroup::type, + "mods", Mod.CODEC.list(), ModGroup::mods, + ModGroup::new + ); + } + + public record Mod(String id, String version) { + + public static final StructCodec CODEC = StructCodec.struct( + "id", Codec.STRING, Mod::id, + "version", Codec.STRING, Mod::version, + Mod::new + ); + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/LunarPayload.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/LunarPayload.java new file mode 100644 index 000000000..cab543aea --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/LunarPayload.java @@ -0,0 +1,22 @@ +package net.hollowcube.compat.lunar.payload; + +import com.google.gson.JsonElement; + +import java.util.List; + +public interface LunarPayload { + + interface Paginated> extends LunarPayload { + String id(); + int page(); + int totalPages(); + + LunarPayload group(List pages); + + default boolean isLastPage() { + return this.page() >= this.totalPages() - 1; + } + } + + record Unhandled(JsonElement json) implements LunarPayload {} +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/LunarPayloadType.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/LunarPayloadType.java new file mode 100644 index 000000000..86dccb809 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/LunarPayloadType.java @@ -0,0 +1,20 @@ +package net.hollowcube.compat.lunar.payload; + +import net.minestom.server.codec.StructCodec; +import org.jetbrains.annotations.NotNullByDefault; + +import java.util.Map; + +@NotNullByDefault +public record LunarPayloadType(String name, StructCodec codec) { + + private static final String QUALIFIER = "type.googleapis.com/"; + + public static final Map> REGISTRY = Map.of( + InstalledModsResponsePagePayload.TYPE.fullyQualifiedName(), InstalledModsResponsePagePayload.TYPE + ); + + public String fullyQualifiedName() { + return QUALIFIER + this.name; + } +} diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/PaginatedPayloadHandler.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/PaginatedPayloadHandler.java new file mode 100644 index 000000000..45d6b0474 --- /dev/null +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/PaginatedPayloadHandler.java @@ -0,0 +1,29 @@ +package net.hollowcube.compat.lunar.payload; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.jspecify.annotations.Nullable; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public class PaginatedPayloadHandler { + + private final Cache>> received = Caffeine.newBuilder() + .maximumSize(10_000) + .expireAfterAccess(Duration.ofMinutes(1)) + .build(); + + @SuppressWarnings("unchecked") + public > @Nullable LunarPayload handle(LunarPayload.Paginated payload) { + var payloads = this.received.get(payload.id(), _ -> new ArrayList<>()); + payloads.add(payload); + if (payload.isLastPage()) { + this.received.invalidate(payload.id()); + + return payload.group((List) payloads); + } + return null; + } +} diff --git a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java b/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java deleted file mode 100644 index 1e35e11a6..000000000 --- a/modules/map/src/main/java/net/hollowcube/mapmaker/map/feature/common/DiscordRichPresenceFeatureProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.hollowcube.mapmaker.map.feature.common; - -import com.google.auto.service.AutoService; -import net.hollowcube.common.util.StringUtil; -import net.hollowcube.compat.api.discord.DiscordRichPresenceManager; -import net.hollowcube.mapmaker.map.MapWorld; -import net.hollowcube.mapmaker.map.event.MapPlayerInitEvent; -import net.hollowcube.mapmaker.map.feature.FeatureProvider; -import net.hollowcube.mapmaker.map.world.EditingMapWorld; -import net.hollowcube.mapmaker.map.world.PlayingMapWorld; -import net.minestom.server.event.EventFilter; -import net.minestom.server.event.EventNode; -import net.minestom.server.event.trait.InstanceEvent; -import org.jetbrains.annotations.NotNull; - -@AutoService(FeatureProvider.class) -public class DiscordRichPresenceFeatureProvider implements FeatureProvider { - private final EventNode eventNode = EventNode.type("mapmaker:map/discord-rich-presence", EventFilter.INSTANCE) - .addListener(MapPlayerInitEvent.class, this::handleMapInit); - - @Override - public boolean initMap(@NotNull MapWorld world) { - if (!(world instanceof PlayingMapWorld || world instanceof EditingMapWorld)) - return false; - world.eventNode().addChild(eventNode); - return true; - } - - private void handleMapInit(@NotNull final MapPlayerInitEvent event) { - if (event.isMapJoin()) { - if (event.mapWorld() instanceof PlayingMapWorld) { - DiscordRichPresenceManager.setRichPresence(event.player(), "Playing", event.mapWorld().map().name(), "/play " + event.mapWorld().map().publishedIdString()); - } else if (event.mapWorld() instanceof EditingMapWorld) { - final var variant = event.mapWorld().map().settings().getVariant().name().toLowerCase(); - DiscordRichPresenceManager.setRichPresence(event.getPlayer(), "Building", "a map", "Building " + StringUtil.indefiniteArticle(variant) + " map"); - } - } - } - -} - From b6ff6ed1e8163f8f5af44755a16285f79c32f955 Mon Sep 17 00:00:00 2001 From: Gravy Boat Date: Mon, 18 May 2026 23:22:47 -0230 Subject: [PATCH 25/25] chore: use expireAfterWrite instead to avoid graal reflect-config changes --- .../compat/lunar/payload/PaginatedPayloadHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/PaginatedPayloadHandler.java b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/PaginatedPayloadHandler.java index 45d6b0474..f75909d18 100644 --- a/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/PaginatedPayloadHandler.java +++ b/modules/compat/src/main/java/net/hollowcube/compat/lunar/payload/PaginatedPayloadHandler.java @@ -4,15 +4,15 @@ import com.github.benmanes.caffeine.cache.Caffeine; import org.jspecify.annotations.Nullable; -import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; public class PaginatedPayloadHandler { private final Cache>> received = Caffeine.newBuilder() .maximumSize(10_000) - .expireAfterAccess(Duration.ofMinutes(1)) + .expireAfterWrite(1, TimeUnit.MINUTES) .build(); @SuppressWarnings("unchecked")