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
+11
-265
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
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 {
···
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 {
+1
-10
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.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
···
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
}
···
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;
0
24
public static Config<LatticeConfig> config;
0
25
26
+
public static Config<DiscordDataStore> connections;
27
28
public static HytaleLogger LOGGER;
29
···
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
}
-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.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
}
···
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
}
+2
-3
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 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());
···
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());
-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
-
}
···
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
-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
-
}
···
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
-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
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
}
···
0
0
0
0
0
0
0
+8
-21
src/main/java/me/theclashfruit/lattice/util/LatticeConfig.java
···
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.";
0
0
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
}
···
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.";
112
+
113
+
public boolean useHyvatarAvatars = false;
114
}
0
0
0
0
0
0
0
0
0
0
0
0
0
115
}
116
}
-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
-
}
···
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