tangled
alpha
login
or
join now
theclashfruit.me
/
lattice
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
0
fork
atom
overview
issues
pulls
pipelines
Compare changes
Choose any two refs to compare.
base:
refactor/jda
main
feat/player_data
feat/lua
feat/linking
v1.0.0
v1.0.0-rc.2
v1.0.0-rc.1
compare:
refactor/jda
main
feat/player_data
feat/lua
feat/linking
v1.0.0
v1.0.0-rc.2
v1.0.0-rc.1
go
+265
-11
10 changed files
expand all
collapse all
unified
split
build.gradle
src
main
java
me
theclashfruit
lattice
LatticePlugin.java
discord
BotEventListener.java
events
PlayerEvents.java
scripting
LuaHandler.java
globals
Discord.java
Events.java
util
Handler.java
LatticeConfig.java
TwoArgFunctionWithEvents.java
+2
build.gradle
···
28
dependencies {
29
compileOnly("com.hypixel.hytale:Server:2026.01.22-6f8bdbdc4")
30
0
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'
0
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;
0
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
0
26
public static Config<DiscordDataStore> connections;
27
0
0
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);
0
0
37
}
38
39
@Override
···
62
return;
63
}
64
0
0
0
65
jda = JDABuilder
66
.createDefault(config.get().discord.token)
67
.enableIntents(GatewayIntent.MESSAGE_CONTENT)
···
79
super.shutdown();
80
81
jda.shutdown();
0
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;
0
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;
0
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
}
0
0
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()));
0
0
103
}
104
105
@Deprecated(forRemoval = true)
···
172
event.reply("You don't have a Hytale account linked.").setEphemeral(true).queue();
173
}
174
}
0
0
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;
0
9
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
0
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();
0
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();
0
0
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
···
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(
0
0
0
0
0
0
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();
0
0
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.";
0
0
112
113
-
public boolean useHyvatarAvatars = false;
114
-
}
0
0
0
0
0
0
0
0
0
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()
0
0
0
0
0
0
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
}