diff --git a/build.gradle b/build.gradle index 70151df..09ade24 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ allprojects { mavenCentral() maven { url = 'https://repo.william278.net/velocity/' } maven { url = 'https://repo.papermc.io/repository/maven-public/' } + maven { url = 'https://repo.codemc.io/repository/maven-releases/' } maven { url = 'https://repo.william278.net/releases/' } maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } maven { url = 'https://repo.minebench.de/' } diff --git a/common/src/main/java/net/william278/huskchat/config/ConfigProvider.java b/common/src/main/java/net/william278/huskchat/config/ConfigProvider.java index 8c1414d..f8a8536 100644 --- a/common/src/main/java/net/william278/huskchat/config/ConfigProvider.java +++ b/common/src/main/java/net/william278/huskchat/config/ConfigProvider.java @@ -155,7 +155,7 @@ default void loadLocales() { setLocales(store.load(path)); return; } - +String a = String.format("locales/%s.yml", getSettings().getLanguage()); // Otherwise, save and read the default locales try (InputStream input = getResource(String.format("locales/%s.yml", getSettings().getLanguage()))) { final Locales locales = store.read(input); diff --git a/common/src/main/java/net/william278/huskchat/config/Locales.java b/common/src/main/java/net/william278/huskchat/config/Locales.java index 1bd4a8c..541f01e 100644 --- a/common/src/main/java/net/william278/huskchat/config/Locales.java +++ b/common/src/main/java/net/william278/huskchat/config/Locales.java @@ -19,8 +19,12 @@ package net.william278.huskchat.config; +import de.exlll.configlib.Configuration; import de.themoep.minedown.adventure.MineDown; import de.themoep.minedown.adventure.MineDownParser; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.TextColor; @@ -33,6 +37,10 @@ import java.util.*; +@SuppressWarnings("FieldMayBeFinal") +@Getter +@Configuration +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class Locales { static final String CONFIG_HEADER = """ @@ -46,7 +54,7 @@ public class Locales { private static final String SILENT_JOIN_PERMISSION = "huskchat.silent_join"; private static final String SILENT_QUIT_PERMISSION = "huskchat.silent_quit"; - static final String DEFAULT_LOCALE = "en"; + static final String DEFAULT_LOCALE = "en-gb"; // The raw set of locales loaded from yaml Map locales = new TreeMap<>(); diff --git a/velocity/build.gradle b/velocity/build.gradle index 45f6497..7a0d4f7 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -1,15 +1,23 @@ +plugins { + id 'xyz.jpenilla.run-velocity' version '2.2.2' +} + dependencies { implementation project(path: ':common') implementation 'org.bstats:bstats-velocity:3.0.2' + implementation 'com.github.retrooper.packetevents:velocity:2.2.0' compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT" compileOnly "com.velocitypowered:velocity-proxy:${velocity_api_version}-SNAPSHOT" + compileOnly 'io.netty:netty-codec-http:4.1.106.Final' + compileOnly 'it.unimi.dsi:fastutil:8.5.12' compileOnly 'commons-io:commons-io:2.15.1' compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT' compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.projectlombok:lombok:1.18.30' + compileOnly 'net.kyori:adventure-nbt:4.15.0' annotationProcessor 'org.projectlombok:lombok:1.18.30' } @@ -25,6 +33,8 @@ shadowJar { relocate 'org.jetbrains', 'net.william278.huskchat.libraries' relocate 'org.intellij', 'net.william278.huskchat.libraries' relocate 'org.bstats', 'net.william278.huskchat.libraries.bstats' + relocate 'com.github.retrooper', 'net.william278.huskchat.libraries' + relocate 'io.github.retrooper', 'net.william278.huskchat.libraries' dependencies { //noinspection GroovyAssignabilityCheck @@ -32,4 +42,10 @@ shadowJar { } minimize() +} + +tasks { + runVelocity { + velocityVersion("${velocity_api_version}-SNAPSHOT") + } } \ No newline at end of file diff --git a/velocity/src/main/java/net/william278/huskchat/VelocityHuskChat.java b/velocity/src/main/java/net/william278/huskchat/VelocityHuskChat.java index 921207b..7198e7b 100644 --- a/velocity/src/main/java/net/william278/huskchat/VelocityHuskChat.java +++ b/velocity/src/main/java/net/william278/huskchat/VelocityHuskChat.java @@ -44,7 +44,7 @@ import net.william278.huskchat.getter.DataGetter; import net.william278.huskchat.getter.DefaultDataGetter; import net.william278.huskchat.getter.LuckPermsDataGetter; -import net.william278.huskchat.listener.VelocityListener; +import net.william278.huskchat.packet.PacketManager; import net.william278.huskchat.placeholders.DefaultReplacer; import net.william278.huskchat.placeholders.PAPIProxyBridgeReplacer; import net.william278.huskchat.placeholders.PlaceholderReplacer; @@ -104,11 +104,6 @@ public VelocityHuskChat(@NotNull ProxyServer server, @NotNull org.slf4j.Logger l @Subscribe public void onProxyInitialization(@NotNull ProxyInitializeEvent event) { - // Check plugin compat - if (!isSigningPluginInstalled()) { - return; - } - // Load config and locale files this.loadConfig(); @@ -129,7 +124,8 @@ public void onProxyInitialization(@NotNull ProxyInitializeEvent event) { } // Register events - getProxyServer().getEventManager().register(this, new VelocityListener(this)); +// getProxyServer().getEventManager().register(this, new VelocityListener(this)); + new PacketManager(this).load(); // todo wip // Register commands & channel shortcuts VelocityCommand.Type.registerAll(this); @@ -146,27 +142,6 @@ public void onProxyInitialization(@NotNull ProxyInitializeEvent event) { log(Level.INFO, "Enabled HuskChat version " + getVersion()); } - // Ensures a signing plugin is installed - private boolean isSigningPluginInstalled() { - boolean usvPresent = isPluginPresent("unsignedvelocity"); - boolean svPresent = isPluginPresent("signedvelocity"); - if (usvPresent && svPresent) { - log(Level.SEVERE, "Both UnsignedVelocity and SignedVelocity are present!\n" + - "Please uninstall UnsignedVelocity. HuskChat will now be disabled." - ); - return false; - } - if (!(usvPresent || svPresent)) { - log(Level.WARNING, "Neither UnsignedVelocity nor SignedVelocity are present!\n" + - "Install SignedVelocity (https://modrinth.com/plugin/signedvelocity) for 1.19+ support."); - } else if (usvPresent) { - log(Level.WARNING, "UnsignedVelocity is deprecated; please install SignedVelocity " + - " (https://modrinth.com/plugin/signedvelocity) instead for better support."); - } - return true; - } - - @Override public Optional getDiscordHook() { return Optional.ofNullable(discordHook); @@ -236,7 +211,7 @@ public Optional findPlayer(@NotNull String username) { @Nullable @Override public InputStream getResource(@NotNull String path) { - return HuskChat.class.getClassLoader().getResourceAsStream(path); + return getClass().getClassLoader().getResourceAsStream(path); } @Override diff --git a/velocity/src/main/java/net/william278/huskchat/packet/PacketManager.java b/velocity/src/main/java/net/william278/huskchat/packet/PacketManager.java new file mode 100644 index 0000000..66ac779 --- /dev/null +++ b/velocity/src/main/java/net/william278/huskchat/packet/PacketManager.java @@ -0,0 +1,21 @@ +package net.william278.huskchat.packet; + +import com.github.retrooper.packetevents.PacketEvents; +import io.github.retrooper.packetevents.velocity.factory.VelocityPacketEventsBuilder; +import net.william278.huskchat.VelocityHuskChat; +import org.jetbrains.annotations.NotNull; + +public class PacketManager { + + private final VelocityHuskChat plugin; + + public PacketManager(@NotNull VelocityHuskChat plugin) { + this.plugin = plugin; + } + + public void load() { + PacketEvents.setAPI(VelocityPacketEventsBuilder.build(plugin.getProxyServer(), plugin.getContainer())); + PacketEvents.getAPI().load(); + PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketListener(plugin)); + } +} diff --git a/velocity/src/main/java/net/william278/huskchat/packet/PlayerPacketListener.java b/velocity/src/main/java/net/william278/huskchat/packet/PlayerPacketListener.java new file mode 100644 index 0000000..91b25ca --- /dev/null +++ b/velocity/src/main/java/net/william278/huskchat/packet/PlayerPacketListener.java @@ -0,0 +1,187 @@ +package net.william278.huskchat.packet; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; +import com.github.retrooper.packetevents.event.simple.PacketConfigSendEvent; +import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent; +import com.github.retrooper.packetevents.manager.player.PlayerManager; +import com.github.retrooper.packetevents.protocol.chat.ChatTypes; +import com.github.retrooper.packetevents.protocol.chat.LastSeenMessages; +import com.github.retrooper.packetevents.protocol.chat.MessageSignature; +import com.github.retrooper.packetevents.protocol.chat.filter.FilterMask; +import com.github.retrooper.packetevents.protocol.chat.message.ChatMessage_v1_19_1; +import com.github.retrooper.packetevents.protocol.chat.message.ChatMessage_v1_19_3; +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.util.crypto.MessageSignData; +import com.github.retrooper.packetevents.wrapper.configuration.server.WrapperConfigServerRegistryData; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientChatMessage; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChatMessage; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.text.Component; +import net.william278.huskchat.VelocityHuskChat; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; + +public class PlayerPacketListener extends SimplePacketListenerAbstract { + + private static final int MAX_SEEN = 20; + + private final VelocityHuskChat plugin; + private final PlayerManager playerManager = PacketEvents.getAPI().getPlayerManager(); + private final Map> seenMessages = Maps.newConcurrentMap(); + private final List allMessages = Lists.newArrayList(); + + public PlayerPacketListener(@NotNull VelocityHuskChat plugin) { + this.plugin = plugin; + ChatTypes.define(RegistryEditor.HUSKCHAT.getName().asString()); + } + + @Override + public void onPacketPlayReceive(PacketPlayReceiveEvent event) { + if (event.getPacketType() == PacketType.Play.Client.CHAT_MESSAGE) { + try { + final WrapperPlayClientChatMessage packet = new WrapperPlayClientChatMessage(event); + this.sendSignedMessage(event.getUser(), packet); + event.setCancelled(true); + } catch (Throwable e) { + plugin.log(Level.SEVERE, "Failed to handle CHAT_MESSAGE packet", e); + } + } else if (event.getPacketType() == PacketType.Play.Client.CHAT_ACK) { + try { + final ByteBuf byteBuf = (ByteBuf) event.getByteBuf(); + seenMessages.getOrDefault(event.getUser().getUUID(), Lists.newArrayList()) + .add(allMessages.get(byteBuf.readInt())); + plugin.log(Level.INFO, "Received CHAT_ACK packet"); + event.setCancelled(true); + } catch (Throwable e) { + plugin.log(Level.SEVERE, "Failed to handle CHAT_ACK packet", e); + } + } + } + + @Override + public void onPacketConfigSend(PacketConfigSendEvent event) { + if (event.getPacketType() == PacketType.Configuration.Server.REGISTRY_DATA) { + try { + final WrapperConfigServerRegistryData wrapper = new WrapperConfigServerRegistryData(event); + wrapper.setRegistryData(injectChatTypes(wrapper.getRegistryData())); + event.markForReEncode(true); + + // Initialize the user's message history + final UUID uuid = event.getUser().getUUID(); + seenMessages.put(uuid, Lists.newArrayList()); + } catch (Throwable e) { + plugin.log(Level.SEVERE, "Failed to handle SERVERBOUND_REGISTRY_SYNC packet", e); + } + } + } + + @NotNull + private NBTCompound injectChatTypes(@NotNull NBTCompound data) { + final RegistryEditor wrapper = new RegistryEditor(data); + wrapper.injectTypes(List.of(RegistryEditor.HUSKCHAT)); + return wrapper.root(); + } + + private void sendSignedMessage(@NotNull User sender, @NotNull WrapperPlayClientChatMessage packet) { + if (packet.getLastSeenMessages() == null) { + throw new IllegalStateException("LastSeenMessages is null"); + } + + (plugin.getPlayer(sender.getUUID())).get().sendMessage(Component.text( + "Offset: " + packet.getLastSeenMessages().getOffset() + " " + + packet.getLastSeenMessages().getAcknowledged().toString() + )); + + int messagesSinceILastSentOne = packet.getLastSeenMessages().getOffset(); + int messagesSinceILastSaw = packet.getLastSeenMessages().getAcknowledged().size(); + + // todo isAllowed + try { + plugin.log(Level.INFO, "Sending signed message 1"); + final MessageSignData sign = packet.getMessageSignData().orElse(null); + if (sign == null) { + plugin.log(Level.WARNING, "Failed to send signed message (no sign data)"); + return; + } + plugin.log(Level.INFO, "Sending signed message 2"); + + System.out.println("SENDING: " + UUID.nameUUIDFromBytes(sign.getSaltSignature().getSignature()).toString().split("-")[0]); + + ChatMessage_v1_19_3 message = new ChatMessage_v1_19_3( + sender.getUUID(), + 0, + sign.getSaltSignature().getSignature(), + packet.getMessage(), + sign.getTimestamp(), + sign.getSaltSignature().getSalt(), + null, + null, + FilterMask.PASS_THROUGH, + new ChatMessage_v1_19_1.ChatTypeBoundNetwork( +// ChatTypes.getByName(RegistryEditor.HUSKCHAT.getName().asString()), + ChatTypes.CHAT, + Component.text(sender.getName()), //todo chat message format goes here + null + ) + ); + plugin.log(Level.INFO, "Sending signed message 3"); + final LastMessage thisMessage = new LastMessage(sender.getUUID(), sign.getSaltSignature().getSignature()); + plugin.getProxyServer().getAllPlayers().forEach(receiver -> { + final List seen = seenMessages.getOrDefault(receiver.getUniqueId(), Lists.newArrayList()); + playerManager.sendPacket(receiver, getChatPacket(message, seen)); + seen.add(thisMessage); + }); + allMessages.add(0, thisMessage); + + } catch (Throwable e) { + plugin.log(Level.SEVERE, "Failed to dispatch packet (unsupported client or server version)", e); + } + } + + @NotNull + private WrapperPlayServerChatMessage getChatPacket(@NotNull ChatMessage_v1_19_3 message, @NotNull List seen) { + message.setIndex((int) seen.stream().filter(s -> s.sender.equals(message.getSenderUUID())).count()); + message.setLastSeenMessagesPacked(LastMessage.pack(allMessages, seen)); + return new WrapperPlayServerChatMessage(message); + } + + private record LastMessage(UUID sender, byte[] signature) { + + // Pack the last MAX_SEEN messages into an Array of LastSeenMessages.Packed + @NotNull + private static LastSeenMessages.Packed pack(@NotNull List all, @NotNull List seen) { + final List packed = Lists.newArrayList(); + for (int i = Math.min(all.size() - 1, MAX_SEEN - 1); i >= 0; i--) { + final LastMessage message = all.get(i); + if (packed.isEmpty() && !seen.contains(message)) { + continue; + } + packed.add(seen.contains(message) + ? new MessageSignature.Packed(i) + : new MessageSignature.Packed(new MessageSignature(message.signature))); + System.out.println("SENT: " + UUID.nameUUIDFromBytes(message.signature).toString().split("-")[0]); + } + return new LastSeenMessages.Packed(packed); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LastMessage message && message.sender.equals(sender)) { + return Arrays.equals(message.signature, signature); + } + return false; + } + + } + +} \ No newline at end of file diff --git a/velocity/src/main/java/net/william278/huskchat/packet/RegistryEditor.java b/velocity/src/main/java/net/william278/huskchat/packet/RegistryEditor.java new file mode 100644 index 0000000..6ef0c8e --- /dev/null +++ b/velocity/src/main/java/net/william278/huskchat/packet/RegistryEditor.java @@ -0,0 +1,125 @@ +package net.william278.huskchat.packet; + +import com.github.retrooper.packetevents.protocol.nbt.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public record RegistryEditor(NBTCompound root) { + + public static ChatType HUSKCHAT = RegistryEditor.ChatType.builder() + .name(Key.key("huskchat", "format")) + .id(50) + .elements(Map.of( + RegistryEditor.ChatType.ElementType.CHAT, + RegistryEditor.ChatType.ElementData.builder() + .translationKey("%s%s") + .parameters(List.of( + RegistryEditor.ChatType.ElementData.ElementParameter.SENDER, + RegistryEditor.ChatType.ElementData.ElementParameter.CONTENT + )) + .build(), + RegistryEditor.ChatType.ElementType.NARRATION, + RegistryEditor.ChatType.ElementData.builder() + .translationKey("chat.type.text.narrate") + .parameters(List.of( + RegistryEditor.ChatType.ElementData.ElementParameter.SENDER, + RegistryEditor.ChatType.ElementData.ElementParameter.CONTENT + )) + .build() + )) + .build(); + + public void injectTypes(@NotNull List toAdd) { + System.out.println("Adding chat types"); + System.out.println(root.getTags().keySet()); + NBTList values = root + .getCompoundTagOrThrow("minecraft:chat_type") + .getCompoundListTagOrThrow("value"); + for (ChatType chatType : toAdd) { + NBTCompound elements = new NBTCompound(); + for (ChatType.ElementType type : chatType.elements.keySet()) { + elements.setTag(type.id(), chatType.elements.get(type).toTag()); + } + + NBTCompound chatTypeTag = new NBTCompound(); + chatTypeTag.setTag("name", new NBTString(chatType.name.asString())); + chatTypeTag.setTag("id", new NBTInt(chatType.id)); + chatTypeTag.setTag("element", elements); + values.addTag(chatTypeTag); + } + } + + @Getter + @Builder + @AllArgsConstructor + public static class ChatType { + private Key name; + private int id; + private Map elements; + + enum ElementType { + CHAT, + NARRATION; + + @NotNull + public static ElementType fromString(@NotNull String s) { + return ElementType.valueOf(s.toUpperCase(Locale.ENGLISH)); + } + + @NotNull + public String id() { + return name().toLowerCase(Locale.ENGLISH); + } + } + + @Getter + @Builder + public static class ElementData { + private String translationKey; + private NBTCompound style; + private List parameters; + + @NotNull + public NBTCompound toTag() { + NBTCompound compound = new NBTCompound(); + compound.setTag("translation_key", new NBTString(translationKey)); + if (style != null) { + compound.setTag("style", style); + } + if (parameters != null) { + NBTList parameterList = new NBTList<>(NBTType.STRING); + for (ElementParameter parameter : parameters) { + parameterList.addTag(new NBTString(parameter.id())); + } + compound.setTag("parameters", parameterList); + } + return compound; + } + + enum ElementParameter { + TARGET, + SENDER, + CONTENT; + + @NotNull + public static ElementParameter fromString(@NotNull String s) { + return ElementParameter.valueOf(s.toUpperCase(Locale.ENGLISH)); + } + + @NotNull + public String id() { + return name().toLowerCase(Locale.ENGLISH); + } + + } + } + } + +} diff --git a/velocity/src/main/resources/velocity-plugin.json b/velocity/src/main/resources/velocity-plugin.json index 6bce3b5..33fd9e6 100644 --- a/velocity/src/main/resources/velocity-plugin.json +++ b/velocity/src/main/resources/velocity-plugin.json @@ -29,5 +29,5 @@ "optional": true } ], - "main": "net.william278.huskchat.velocity.VelocityHuskChat" + "main": "net.william278.huskchat.VelocityHuskChat" } \ No newline at end of file