A simple (for now) Discord chat bridge for Hytale. modtale.net/mod/lattice
java discord discord-bot hytale hytale-plugin hytale-mod hytale-modding hytale-mods

Compare changes

Choose any two refs to compare.

+265 -11
+2
build.gradle
··· 28 dependencies { 29 compileOnly("com.hypixel.hytale:Server:2026.01.22-6f8bdbdc4") 30 31 implementation("net.dv8tion:JDA:$jda_version") { 32 exclude module: 'opus-java' 33 } ··· 53 archiveClassifier = '' 54 55 relocate 'net.dv8tion.jda', 'me.theclashfruit.shadow.jda' 56 } 57 58 java {
··· 28 dependencies { 29 compileOnly("com.hypixel.hytale:Server:2026.01.22-6f8bdbdc4") 30 31 + implementation("org.luaj:luaj-jse:3.0.1") 32 implementation("net.dv8tion:JDA:$jda_version") { 33 exclude module: 'opus-java' 34 } ··· 54 archiveClassifier = '' 55 56 relocate 'net.dv8tion.jda', 'me.theclashfruit.shadow.jda' 57 + relocate 'org.luaj', 'me.theclashfruit.shadow.luaj' 58 } 59 60 java {
+10 -1
src/main/java/me/theclashfruit/lattice/LatticePlugin.java
··· 11 import me.theclashfruit.lattice.commands.LatticeCommand; 12 import me.theclashfruit.lattice.discord.BotEventListener; 13 import me.theclashfruit.lattice.events.PlayerEvents; 14 import me.theclashfruit.lattice.util.store.DiscordDataStore; 15 import me.theclashfruit.lattice.util.LatticeConfig; 16 import net.dv8tion.jda.api.JDA; ··· 21 22 public class LatticePlugin extends JavaPlugin { 23 public static JDA jda; 24 - public static Config<LatticeConfig> config; 25 26 public static Config<DiscordDataStore> connections; 27 28 public static HytaleLogger LOGGER; 29 30 public LatticePlugin(@Nonnull JavaPluginInit init) { ··· 34 35 config = this.withConfig("Lattice", LatticeConfig.CODEC); 36 connections = this.withConfig("DiscordData", DiscordDataStore.CODEC); 37 } 38 39 @Override ··· 62 return; 63 } 64 65 jda = JDABuilder 66 .createDefault(config.get().discord.token) 67 .enableIntents(GatewayIntent.MESSAGE_CONTENT) ··· 79 super.shutdown(); 80 81 jda.shutdown(); 82 } 83 }
··· 11 import me.theclashfruit.lattice.commands.LatticeCommand; 12 import me.theclashfruit.lattice.discord.BotEventListener; 13 import me.theclashfruit.lattice.events.PlayerEvents; 14 + import me.theclashfruit.lattice.scripting.LuaHandler; 15 import me.theclashfruit.lattice.util.store.DiscordDataStore; 16 import me.theclashfruit.lattice.util.LatticeConfig; 17 import net.dv8tion.jda.api.JDA; ··· 22 23 public class LatticePlugin extends JavaPlugin { 24 public static JDA jda; 25 26 + public static Config<LatticeConfig> config; 27 public static Config<DiscordDataStore> connections; 28 29 + public static LuaHandler luaHandler; 30 + 31 public static HytaleLogger LOGGER; 32 33 public LatticePlugin(@Nonnull JavaPluginInit init) { ··· 37 38 config = this.withConfig("Lattice", LatticeConfig.CODEC); 39 connections = this.withConfig("DiscordData", DiscordDataStore.CODEC); 40 + 41 + luaHandler = new LuaHandler(); 42 } 43 44 @Override ··· 67 return; 68 } 69 70 + if (conf.features.scripting) 71 + luaHandler.setup(this.getDataDirectory().resolve("scripts")); 72 + 73 jda = JDABuilder 74 .createDefault(config.get().discord.token) 75 .enableIntents(GatewayIntent.MESSAGE_CONTENT) ··· 87 super.shutdown(); 88 89 jda.shutdown(); 90 + luaHandler.shutdown(); 91 } 92 }
+7
src/main/java/me/theclashfruit/lattice/discord/BotEventListener.java
··· 5 import com.hypixel.hytale.server.core.universe.PlayerRef; 6 import com.hypixel.hytale.server.core.universe.Universe; 7 import me.theclashfruit.lattice.LatticePlugin; 8 import me.theclashfruit.lattice.util.LinkUtil; 9 import net.dv8tion.jda.api.entities.User; 10 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; ··· 53 LOGGER.atWarning().withCause(e).log("Failed to create guild commands, you may have provided an invalid guild id."); 54 } 55 } 56 } 57 58 @Override ··· 100 101 Universe.get().sendMessage(joined); 102 LOGGER.atInfo().log("[Discord] %s:%s %s", user.getEffectiveName(), builder.toString(), String.join(" ", attachments.stream().map(a -> "[" + a.getFileName() + "]").toList())); 103 } 104 105 @Deprecated(forRemoval = true) ··· 172 event.reply("You don't have a Hytale account linked.").setEphemeral(true).queue(); 173 } 174 } 175 } 176 }
··· 5 import com.hypixel.hytale.server.core.universe.PlayerRef; 6 import com.hypixel.hytale.server.core.universe.Universe; 7 import me.theclashfruit.lattice.LatticePlugin; 8 + import me.theclashfruit.lattice.scripting.globals.Discord; 9 import me.theclashfruit.lattice.util.LinkUtil; 10 import net.dv8tion.jda.api.entities.User; 11 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; ··· 54 LOGGER.atWarning().withCause(e).log("Failed to create guild commands, you may have provided an invalid guild id."); 55 } 56 } 57 + 58 + Discord.callEvents("ready"); 59 } 60 61 @Override ··· 103 104 Universe.get().sendMessage(joined); 105 LOGGER.atInfo().log("[Discord] %s:%s %s", user.getEffectiveName(), builder.toString(), String.join(" ", attachments.stream().map(a -> "[" + a.getFileName() + "]").toList())); 106 + 107 + Discord.callEvents("message_received", event); 108 } 109 110 @Deprecated(forRemoval = true) ··· 177 event.reply("You don't have a Hytale account linked.").setEphemeral(true).queue(); 178 } 179 } 180 + 181 + Discord.callEvents("slash_command_interaction", event); 182 } 183 }
+3 -2
src/main/java/me/theclashfruit/lattice/events/PlayerEvents.java
··· 6 import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent; 7 import com.hypixel.hytale.server.core.universe.PlayerRef; 8 import me.theclashfruit.lattice.LatticePlugin; 9 import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; 10 11 import java.net.URI; 12 import java.util.Map; ··· 52 } 53 54 hook.flatMap(h -> h.sendMessage(content).setUsername(user.getEffectiveName()).setAvatarUrl(user.getAvatarUrl())).queue(); 55 - } else if (LatticePlugin.config.get().discord.messages.useHyvatarAvatars) { 56 - hook.flatMap(h -> h.sendMessage(content).setUsername(sender.getUsername()).setAvatarUrl("https://hyvatar.io/render/%s?size=256".formatted(sender.getUsername()))).queue(); 57 } else { 58 hook.flatMap(h -> h.sendMessage(content).setUsername(sender.getUsername())).queue(); 59 } ··· 61 62 public static void onPlayerReady(PlayerReadyEvent event) { 63 Player player = event.getPlayer(); 64 65 if(player.getWorld() != null) { 66 String joinMessage = String.format(LatticePlugin.config.get().discord.messages.join, player.getDisplayName(), player.getWorld().getName());
··· 6 import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent; 7 import com.hypixel.hytale.server.core.universe.PlayerRef; 8 import me.theclashfruit.lattice.LatticePlugin; 9 + import me.theclashfruit.lattice.scripting.globals.Events; 10 import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; 11 + import org.luaj.vm2.LuaValue; 12 13 import java.net.URI; 14 import java.util.Map; ··· 54 } 55 56 hook.flatMap(h -> h.sendMessage(content).setUsername(user.getEffectiveName()).setAvatarUrl(user.getAvatarUrl())).queue(); 57 } else { 58 hook.flatMap(h -> h.sendMessage(content).setUsername(sender.getUsername())).queue(); 59 } ··· 61 62 public static void onPlayerReady(PlayerReadyEvent event) { 63 Player player = event.getPlayer(); 64 + Events.callEvents("player_ready", player); 65 66 if(player.getWorld() != null) { 67 String joinMessage = String.format(LatticePlugin.config.get().discord.messages.join, player.getDisplayName(), player.getWorld().getName());
+65
src/main/java/me/theclashfruit/lattice/scripting/LuaHandler.java
···
··· 1 + package me.theclashfruit.lattice.scripting; 2 + 3 + import me.theclashfruit.lattice.scripting.globals.Discord; 4 + import me.theclashfruit.lattice.scripting.globals.Events; 5 + import org.jetbrains.annotations.NotNull; 6 + import org.luaj.vm2.Globals; 7 + import org.luaj.vm2.LuaValue; 8 + import org.luaj.vm2.lib.jse.JsePlatform; 9 + 10 + import java.io.File; 11 + import java.nio.file.Files; 12 + import java.nio.file.Path; 13 + import java.util.Arrays; 14 + import java.util.List; 15 + 16 + import static me.theclashfruit.lattice.LatticePlugin.LOGGER; 17 + 18 + public class LuaHandler { 19 + public LuaHandler() {} 20 + 21 + public void setup(@NotNull Path path) { 22 + File pathFile = path.toFile(); 23 + 24 + if (!pathFile.exists()) 25 + return; 26 + if (!pathFile.isDirectory()) 27 + return; 28 + 29 + List<File> files = Arrays.stream(pathFile.listFiles()) 30 + .filter(f -> 31 + f.isFile() && 32 + f.getName().endsWith(".lua") && 33 + !f.getName().startsWith("_") 34 + ) 35 + .toList(); 36 + 37 + if (files.isEmpty()) 38 + return; 39 + for (File file : files) 40 + loadScript(file); 41 + } 42 + 43 + public void shutdown() { 44 + Events.callEvents("shutdown"); 45 + } 46 + 47 + private Globals getGlobals() { 48 + Globals globals = JsePlatform.standardGlobals(); 49 + 50 + globals.load(new Discord()); 51 + globals.load(new Events()); 52 + 53 + return globals; 54 + } 55 + 56 + private void loadScript(File file) { 57 + try { 58 + Globals globals = getGlobals(); 59 + LuaValue chunk = globals.load(Files.readString(file.toPath())); 60 + chunk.call(); 61 + } catch (Exception e) { 62 + LOGGER.atSevere().withCause(e).log("Failed to load script `%s`.", file.getName()); 63 + } 64 + } 65 + }
+59
src/main/java/me/theclashfruit/lattice/scripting/globals/Discord.java
···
··· 1 + package me.theclashfruit.lattice.scripting.globals; 2 + 3 + import me.theclashfruit.lattice.util.TwoArgFunctionWithEvents; 4 + import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 5 + import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 6 + import org.luaj.vm2.LuaTable; 7 + import org.luaj.vm2.LuaValue; 8 + import org.luaj.vm2.Varargs; 9 + import org.luaj.vm2.lib.VarArgFunction; 10 + 11 + import static me.theclashfruit.lattice.LatticePlugin.LOGGER; 12 + 13 + public class Discord extends TwoArgFunctionWithEvents { 14 + private static final String[] functions = { 15 + "set_activity" 16 + }; 17 + 18 + public Discord() { 19 + this.packageName = "discord"; 20 + 21 + this.addEvent("ready", (fn, args) -> fn.call()); 22 + this.addEvent("message_received", (fn, args) -> { 23 + var event = (MessageReceivedEvent) args[0]; 24 + var msg = event.getMessage(); 25 + 26 + fn.call(LuaValue.valueOf(msg.getContentRaw())); 27 + }); 28 + this.addEvent("slash_command_interaction", (fn, args) -> { 29 + var event = (SlashCommandInteractionEvent) args[0]; 30 + var name = event.getName(); 31 + 32 + fn.call(LuaValue.valueOf(name)); 33 + }); 34 + } 35 + 36 + @Override 37 + public LuaValue call(LuaValue value, LuaValue env) { 38 + LuaTable discord = super.call(value, env).checktable(); 39 + 40 + for (String function : functions) { 41 + discord.set(function, new DiscordFunction(function)); 42 + } 43 + 44 + return discord; 45 + } 46 + 47 + static class DiscordFunction extends VarArgFunction { 48 + public DiscordFunction(String name) { 49 + this.name = name; 50 + } 51 + } 52 + 53 + @Override 54 + public Varargs invoke(Varargs args) { 55 + LOGGER.atInfo().log("get called %s", name); 56 + 57 + return NONE; 58 + } 59 + }
+20
src/main/java/me/theclashfruit/lattice/scripting/globals/Events.java
···
··· 1 + package me.theclashfruit.lattice.scripting.globals; 2 + 3 + import com.hypixel.hytale.server.core.entity.entities.Player; 4 + import me.theclashfruit.lattice.util.TwoArgFunctionWithEvents; 5 + import org.luaj.vm2.LuaTable; 6 + 7 + public class Events extends TwoArgFunctionWithEvents { 8 + public Events() { 9 + this.packageName = "events"; 10 + 11 + this.addEvent("player_ready", (fn, args) -> { 12 + Player player = (Player) args[0]; 13 + LuaTable params = new LuaTable(); 14 + params.set("display_name", player.getDisplayName()); 15 + fn.call(params); 16 + }); 17 + 18 + this.addEvent("shutdown", (fn, _) -> fn.call()); 19 + } 20 + }
+7
src/main/java/me/theclashfruit/lattice/util/Handler.java
···
··· 1 + package me.theclashfruit.lattice.util; 2 + 3 + import org.luaj.vm2.LuaValue; 4 + 5 + public interface Handler { 6 + void run(LuaValue fn, Object... args); 7 + }
+21 -8
src/main/java/me/theclashfruit/lattice/util/LatticeConfig.java
··· 18 ) 19 .add() 20 .append( 21 new KeyedCodec<>("ChatPrefix", BuilderCodec.STRING), 22 (config, value, info) -> config.chat_prefix = value, 23 (config, info) -> config.chat_prefix ··· 40 public String chat_prefix_colour = "#5865F2"; 41 42 public DiscordConfig discord = new DiscordConfig(); 43 44 public static class DiscordConfig { 45 public static final BuilderCodec<DiscordConfig> CODEC = BuilderCodec.builder(DiscordConfig.class, DiscordConfig::new) ··· 99 (config, info) -> config.leave 100 ) 101 .add() 102 - .append( 103 - new KeyedCodec<>("UseHyvatarAvatars", BuilderCodec.BOOLEAN), 104 - (config, value, info) -> config.useHyvatarAvatars = value, 105 - (config, info) -> config.useHyvatarAvatars 106 - ) 107 - .add() 108 .build(); 109 110 public String join = "%s joined %s."; 111 public String leave = "%s left."; 112 113 - public boolean useHyvatarAvatars = false; 114 - } 115 } 116 }
··· 18 ) 19 .add() 20 .append( 21 + new KeyedCodec<>("Features", FeaturesConfig.CODEC), 22 + (config, value, info) -> config.features = value, 23 + (config, info) -> config.features 24 + ) 25 + .add() 26 + .append( 27 new KeyedCodec<>("ChatPrefix", BuilderCodec.STRING), 28 (config, value, info) -> config.chat_prefix = value, 29 (config, info) -> config.chat_prefix ··· 46 public String chat_prefix_colour = "#5865F2"; 47 48 public DiscordConfig discord = new DiscordConfig(); 49 + 50 + public FeaturesConfig features = new FeaturesConfig(); 51 52 public static class DiscordConfig { 53 public static final BuilderCodec<DiscordConfig> CODEC = BuilderCodec.builder(DiscordConfig.class, DiscordConfig::new) ··· 107 (config, info) -> config.leave 108 ) 109 .add() 110 .build(); 111 112 public String join = "%s joined %s."; 113 public String leave = "%s left."; 114 + } 115 + } 116 117 + public static class FeaturesConfig { 118 + public static BuilderCodec<FeaturesConfig> CODEC = BuilderCodec.builder(FeaturesConfig.class, FeaturesConfig::new) 119 + .append( 120 + new KeyedCodec<>("Scripting", BuilderCodec.BOOLEAN), 121 + (config, value, info) -> config.scripting = value, 122 + (config, info) -> config.scripting 123 + ) 124 + .add() 125 + .build(); 126 + 127 + public boolean scripting = false; 128 } 129 }
+71
src/main/java/me/theclashfruit/lattice/util/TwoArgFunctionWithEvents.java
···
··· 1 + package me.theclashfruit.lattice.util; 2 + 3 + import org.luaj.vm2.Globals; 4 + import org.luaj.vm2.LuaTable; 5 + import org.luaj.vm2.LuaValue; 6 + import org.luaj.vm2.lib.TwoArgFunction; 7 + 8 + import java.util.ArrayList; 9 + import java.util.HashMap; 10 + import java.util.List; 11 + import java.util.Map; 12 + 13 + /** 14 + * A {@link TwoArgFunction} with event functionalities. 15 + */ 16 + public class TwoArgFunctionWithEvents extends TwoArgFunction { 17 + private static final Map<String, List<LuaValue>> events = new HashMap<>(); 18 + private static final Map<String, Handler> handlers = new HashMap<>(); 19 + 20 + protected String packageName; 21 + 22 + /** 23 + * A function to add an event that can be handled. 24 + * 25 + * @param name The events name, ex. `player_join`. 26 + * @param handler A lambda function to convert stuff to {@link LuaValue}. 27 + */ 28 + protected void addEvent(String name, Handler handler) { 29 + events.put(name, new ArrayList<>()); 30 + handlers.put(name, handler); 31 + } 32 + 33 + public static void callEvents(String eventName, Object... args) { 34 + if (!events.containsKey(eventName)) 35 + return; 36 + 37 + List<LuaValue> functions = events.get(eventName); 38 + Handler handler = handlers.get(eventName); 39 + 40 + for (LuaValue function : functions) { 41 + handler.run(function, args); 42 + } 43 + } 44 + 45 + @Override 46 + public LuaValue call(LuaValue value, LuaValue env) { 47 + Globals globals = env.checkglobals(); 48 + 49 + LuaTable eventTable = new LuaTable(); 50 + 51 + eventTable.set("on", new TwoArgFunction() { 52 + @Override 53 + public LuaValue call(LuaValue name, LuaValue fn) { 54 + String n = name.checkjstring(); 55 + 56 + if (!events.containsKey(n)) 57 + return NONE; 58 + 59 + events.get(n).add(fn); 60 + int len = events.get(n).size() - 1; 61 + 62 + return LuaValue.valueOf(len); 63 + } 64 + }); 65 + 66 + env.set(packageName, eventTable); 67 + env.get("package").get("loaded").set(packageName, eventTable); 68 + 69 + return eventTable; 70 + } 71 + }