Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
116a921
feat: slightly less turbo trash lunar impl
JakeMT04 Apr 13, 2025
8fd9ca9
feat: feather support for rich presence
JakeMT04 Apr 14, 2025
f3831fa
feat: discord presence updates on state changes (mostly)
JakeMT04 Apr 18, 2025
fe82889
chore: notnull annotations
JakeMT04 Apr 20, 2025
e01de3d
chore: update comment
JakeMT04 Apr 20, 2025
7ac5ac5
chore: split util into its own class
JakeMT04 Apr 20, 2025
2e9236c
feat: labymod RPC impl
JakeMT04 Apr 21, 2025
f958049
chore: cleanup
JakeMT04 Apr 27, 2025
3966f34
chore: remove labymod for now
JakeMT04 Apr 27, 2025
eec1502
chore: annotations
JakeMT04 Apr 27, 2025
8f235ba
chore: static gson (oops)
JakeMT04 Apr 27, 2025
2ca1810
fix: hub rich presence to feature and event rather than delay
JakeMT04 Apr 27, 2025
c9eefcf
chore: rename wordutil
JakeMT04 Apr 27, 2025
d9e954b
Merge branch 'main' into feat/lunar
ThatGravyBoat Jul 27, 2025
cdf33dd
Merge remote-tracking branch 'origin/main' into feat/lunar
ThatGravyBoat Mar 27, 2026
6a07cb6
chore: temporarily include apollo protobufs while lunar is fixing jso…
ThatGravyBoat Mar 28, 2026
8b419c3
Merge branch 'main' into feat/lunar
ThatGravyBoat Mar 29, 2026
c50c53a
fix: bad merge
ThatGravyBoat Mar 29, 2026
54b3506
fix: discord rich presence on lunar
ThatGravyBoat Mar 29, 2026
32c1db8
fix: lunar discord rich presence (there is no docs on what the client…
ThatGravyBoat Mar 29, 2026
e1c1e4b
lunar sucks so time to see where these fields show up to
ThatGravyBoat Mar 29, 2026
333ea70
lets see if this is good enough for lunar
ThatGravyBoat Mar 29, 2026
b71ae6c
lets see if this is good enough for lunar round 2
ThatGravyBoat Mar 29, 2026
6b972a9
Merge branch 'main' into feat/lunar
ThatGravyBoat Mar 29, 2026
009a157
lunar doesnt like our special slash
ThatGravyBoat Mar 29, 2026
b990ca9
I was finally told how lunar uses these magic values and its dumb
ThatGravyBoat Mar 30, 2026
0553277
lunar still needs a special slash
ThatGravyBoat Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/development/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +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());
});
}, "");

return dbg;
}
}
12 changes: 12 additions & 0 deletions build-src/src/main/kotlin/mapmaker.java-binary.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ repositories {
includeGroup("com.noxcrew.noxesium")
}
}

maven(url = "https://repo.feathermc.net/artifactory/maven-releases") {
content {
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 {
Expand Down
13 changes: 13 additions & 0 deletions build-src/src/main/kotlin/mapmaker.java-library.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ repositories {
includeGroup("com.noxcrew.noxesium")
}
}

maven(url = "https://repo.feathermc.net/artifactory/maven-releases") {
content {
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 {
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
labymod = "1.0.6"

[libraries]
annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" }
Expand All @@ -48,6 +50,8 @@ 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" }
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" }
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions modules/compat/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ dependencies {
implementation(libs.posthog)
implementation(libs.noxesium)
implementation(libs.zstd)
implementation(libs.feather)
implementation(libs.labymod)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package net.hollowcube.compat.api.discord;

import net.minestom.server.entity.Player;

import java.util.HashSet;
import java.util.ServiceLoader;
import java.util.Set;


public class DiscordRichPresenceManager {
private static final Set<DiscordRichPresenceProvider> PROVIDERS = new HashSet<>();

static {
ServiceLoader.load(DiscordRichPresenceProvider.class).forEach(PROVIDERS::add);
}


public static void setRichPresence(Player player, String playerState, String gameName, String gameVariantName) {
for (DiscordRichPresenceProvider provider : PROVIDERS) {
if (provider.isRichPresenceSupportedFor(player)) {
provider.setRichPresence(player, playerState, gameName, gameVariantName);
return;
}
}
}

public static void clearRichPresence(Player player) {
for (DiscordRichPresenceProvider provider : PROVIDERS) {
if (provider.isRichPresenceSupportedFor(player)) {
provider.clearRichPresence(player);
return;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.hollowcube.compat.api.discord;

import net.minestom.server.entity.Player;

public interface DiscordRichPresenceProvider {
void setRichPresence(Player player, String playerState, String gameName, String gameVariantName);

void clearRichPresence(Player player);

boolean isRichPresenceSupportedFor(Player player);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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.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;
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 Tag<Boolean> FEATHER_SUPPORT_ENABLED = Tag.Transient("mapmaker:feather/enabled");
private static final Handshaking HANDSHAKING = new Handshaking();

@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 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) {
new ClientboundFeatherPacket(new S2CClearDiscordActivity()).send(player);
}

@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<UUID, HandshakeState> 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);
new ClientboundFeatherPacket(new S2CHandshake()).send(player);
}

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<ServerMessageHandler> 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<ServerMessageHandler> message) {
if (!(message instanceof C2SHandshake handshake)) {
return false;
}
int protocolVersion = handshake.getProtocolVersion();
if (protocolVersion > MessageConstants.VERSION) {
// 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;
}


private enum HandshakeState {
EXPECTING_HANDSHAKE,
EXPECTING_HELLO,
REJECTED
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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;
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<ClientMessageHandler> message
) implements ClientboundModPacket<ClientboundFeatherPacket> {
public static final Type<ClientboundFeatherPacket> TYPE = Type.of(
"feather",
"client",
NetworkBuffer.RAW_BYTES.transform(ClientboundFeatherPacket::new, ClientboundFeatherPacket::toBytes)
);

private ClientboundFeatherPacket(byte[] bytes) {
this(MessageDecoder.CLIENT_BOUND.decode(bytes));
}

public byte[] toBytes() {
return MessageEncoder.CLIENT_BOUND.encode(message);
}

@Override
public Type<ClientboundFeatherPacket> getType() {
return TYPE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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;
import org.jetbrains.annotations.NotNull;

public record ServerboundFeatherPacket(
@NotNull Message<ServerMessageHandler> message
) implements ServerboundModPacket<ServerboundFeatherPacket> {
public static final Type<ServerboundFeatherPacket> TYPE = Type.of(
"feather",
"client",
NetworkBuffer.RAW_BYTES.transform(ServerboundFeatherPacket::new, ServerboundFeatherPacket::toBytes)
);

private ServerboundFeatherPacket(byte[] bytes) {
this(MessageDecoder.SERVER_BOUND.decode(bytes));
}

public byte[] toBytes() {
return MessageEncoder.SERVER_BOUND.encode(message);
}

@Override
public Type<ServerboundFeatherPacket> getType() {
return TYPE;
}
}
Loading