Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 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 @@ -28,6 +28,12 @@ repositories {
includeGroup("com.noxcrew.noxesium")
}
}

maven(url = "https://repo.feathermc.net/artifactory/maven-releases") {
content {
includeGroup("net.digitalingot.feather-server-api")
}
}
}

dependencies {
Expand Down
6 changes: 6 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 @@ -24,6 +24,12 @@ repositories {
includeGroup("com.noxcrew.noxesium")
}
}

maven(url = "https://repo.feathermc.net/artifactory/maven-releases") {
content {
includeGroup("net.digitalingot.feather-server-api")
}
}
}

dependencies {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ blossom = "2.1.0"
velocity = "3.4.0-SNAPSHOT"
classgraph = "4.8.179"
included = "-INCLUDED"
feather = "0.0.5"

[libraries]
annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" }
Expand All @@ -50,6 +51,7 @@ jctools = { group = "org.jctools", name = "jctools-core", version.ref = "jctools
caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version.ref = "caffeine" }
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" }

included-molang = { group = "dev.hollowcube", name = "molang", version.ref = "included" }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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;
}
}
1 change: 1 addition & 0 deletions modules/compat/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ dependencies {
implementation(libs.posthog)
implementation(libs.noxesium)
implementation(libs.zstd)
implementation(libs.feather)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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;
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(@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 clearRichPresence(@NotNull 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,12 @@
package net.hollowcube.compat.api.discord;

import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;

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

void clearRichPresence(@NotNull Player player);

boolean isRichPresenceSupportedFor(@NotNull Player player);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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 org.jetbrains.annotations.NotNull;

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(@NotNull Player player, @NotNull String playerState, @NotNull String gameName, @NotNull 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(@NotNull Player player) {
new ClientboundFeatherPacket(new S2CClearDiscordActivity()).send(player);
}

@Override
public boolean isRichPresenceSupportedFor(@NotNull 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