From 2a2f19ae95bec989dfc6355258a06c2c7a3d2153 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 13 Sep 2025 19:54:34 -0400 Subject: [PATCH 01/22] feat: initial logic (seems wrong) --- bin/development/build.gradle.kts | 4 + .../mapmaker/isolate/MapIsolateServer.java | 6 +- gradle/libs.versions.toml | 9 + .../hollowcube/common/util/StringUtil.java | 19 ++ modules/map-runtime/build.gradle.kts | 7 + .../runtime/freeform/FreeformMapWorld.java | 222 ++++++++++++++++++ .../runtime/freeform/FreeformState.java | 18 ++ .../runtime/freeform/lua/LuaGlobals.java | 35 +++ .../runtime/freeform/lua/LuaTask.java | 99 ++++++++ .../freeform/lua/math/LuaVectorTypeImpl.java | 112 +++++++++ .../freeform/lua/math/package-info.java | 4 + .../runtime/freeform/lua/package-info.java | 4 + .../runtime/freeform/lua/world/LuaBlock.java | 84 +++++++ .../runtime/freeform/lua/world/LuaWorld.java | 98 ++++++++ .../freeform/lua/world/package-info.java | 4 + .../runtime/freeform/package-info.java | 4 + .../runtime/freeform/script/LuaHelpers.java | 83 +++++++ .../freeform/script/LuaScriptState.java | 55 +++++ .../runtime/freeform/script/package-info.java | 4 + 19 files changed, 869 insertions(+), 2 deletions(-) create mode 100644 modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaGlobals.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/math/LuaVectorTypeImpl.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/math/package-info.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/package-info.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlock.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/package-info.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/package-info.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaScriptState.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/package-info.java diff --git a/bin/development/build.gradle.kts b/bin/development/build.gradle.kts index e7affa1d4..f45a93072 100644 --- a/bin/development/build.gradle.kts +++ b/bin/development/build.gradle.kts @@ -3,6 +3,10 @@ plugins { id("mapmaker.packer-data") } +repositories { + mavenLocal() +} + dependencies { implementation(project(":bin:config")) implementation(project(":bin:hub")) diff --git a/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java b/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java index b9bc035d2..b3bd04c26 100644 --- a/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java +++ b/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java @@ -4,9 +4,11 @@ import net.hollowcube.common.util.FutureUtil; import net.hollowcube.common.util.Uuids; import net.hollowcube.mapmaker.config.ConfigLoaderV3; +import net.hollowcube.mapmaker.map.AbstractMapWorld; import net.hollowcube.mapmaker.map.runtime.AbstractMapServer; import net.hollowcube.mapmaker.map.runtime.ServerBridge; import net.hollowcube.mapmaker.misc.ResourcePackManager; +import net.hollowcube.mapmaker.runtime.freeform.FreeformMapWorld; import net.hollowcube.mapmaker.runtime.parkour.ParkourMapWorld; import net.hollowcube.mapmaker.session.Presence; import net.kyori.adventure.text.Component; @@ -34,7 +36,7 @@ public class MapIsolateServer extends AbstractMapServer { // Its only kinda unknown. it's not created in the constructor, but after prepareState // it is always not-null which should cover any reasonable logic. // TODO: pretty sure we could do init in constructor, should investigate. - private @UnknownNullability ParkourMapWorld world; + private @UnknownNullability AbstractMapWorld world; public MapIsolateServer(ConfigLoaderV3 config) { super(config); @@ -78,7 +80,7 @@ protected void prepareStart() { try { var map = mapService().getMap(Uuids.ZERO, this.mapId); - world = new ParkourMapWorld(this, map); + world = new FreeformMapWorld(this, map); world.loadWorld(); // We schedule on first tick end because submitTask invokes the executor immediately to determine diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52557d166..4c84844d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,7 @@ blossom = "2.1.0" velocity = "3.4.0-SNAPSHOT" classgraph = "4.8.179" included = "-INCLUDED" +luau = "0.3.2" [libraries] annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } @@ -63,6 +64,12 @@ adventure-text-minimessage = { group = "net.kyori", name = "adventure-text-minim adventure-text-serializer-plain = { group = "net.kyori", name = "adventure-text-serializer-plain", version.ref = "adventure" } adventure-nbt = { group = "net.kyori", name = "adventure-nbt", version.ref = "adventure" } +luau-lib = { group = "dev.hollowcube", name = "luau", version.ref = "luau" } +luau-natives-macos-x64 = { group = "dev.hollowcube", name = "luau-natives-macos-x64", version.ref = "luau" } +luau-natives-macos-arm64 = { group = "dev.hollowcube", name = "luau-natives-macos-arm64", version.ref = "luau" } +luau-natives-linux-x64 = { group = "dev.hollowcube", name = "luau-natives-linux-x64", version.ref = "luau" } +luau-natives-windows-x64 = { group = "dev.hollowcube", name = "luau-natives-windows-x64", version.ref = "luau" } + prometheus = { group = "io.prometheus", name = "simpleclient", version.ref = "prometheus" } prometheus-hotspot = { group = "io.prometheus", name = "simpleclient_hotspot", version.ref = "prometheus" } prometheus-httpserver = { group = "io.prometheus", name = "simpleclient_httpserver", version.ref = "prometheus" } @@ -86,6 +93,8 @@ junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", ver adventure = ["adventure-api", "adventure-key", "adventure-text-minimessage", "adventure-text-serializer-plain", "adventure-nbt"] prometheus = ["prometheus", "prometheus-hotspot", "prometheus-httpserver"] otel = ["otel-api", "otel-context", "otel-sdk", "otel-sdk-common", "otel-sdk-trace", "otel-extension-trace-propagators", "otel-exporter-logging", "otel-exporter-otlp", "otel-exporter-sender-jdk", "otel-semconv"] +luau = ["luau-natives-macos-x64", "luau-natives-macos-arm64", "luau-natives-linux-x64", "luau-natives-windows-x64"] +#luau = ["luau-lib", "luau-natives-macos-x64", "luau-natives-macos-arm64", "luau-natives-linux-x64", "luau-natives-windows-x64"] [plugins] blossom = { id = "net.kyori.blossom", version.ref = "blossom" } diff --git a/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java b/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java new file mode 100644 index 000000000..635f0bbdf --- /dev/null +++ b/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java @@ -0,0 +1,19 @@ +package net.hollowcube.common.util; + +import org.jetbrains.annotations.NotNull; + +public final class StringUtil { + + public static @NotNull String snakeToPascal(@NotNull String snakeCase) { + StringBuilder pascalCase = new StringBuilder(); + for (String part : snakeCase.split("_")) { + if (!part.isEmpty()) { + pascalCase.append(Character.toUpperCase(part.charAt(0))); + if (part.length() > 1) { + pascalCase.append(part.substring(1).toLowerCase()); + } + } + } + return pascalCase.toString(); + } +} diff --git a/modules/map-runtime/build.gradle.kts b/modules/map-runtime/build.gradle.kts index 7da723033..404eefbdb 100644 --- a/modules/map-runtime/build.gradle.kts +++ b/modules/map-runtime/build.gradle.kts @@ -2,6 +2,10 @@ plugins { id("mapmaker.java-library") } +repositories { + mavenLocal() +} + dependencies { api(project(":modules:map-core")) api(project(":modules:terraform")) //TODO: this exists for entity implementations, but it shouldn't. @@ -13,6 +17,9 @@ dependencies { implementation(libs.included.molang) implementation(libs.bundles.adventure) + implementation(libs.bundles.luau) + implementation("dev.hollowcube:luau:dev") + testImplementation(project(":modules:compat")) testImplementation(project(":modules:test")) testImplementation(libs.bundles.otel) diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java new file mode 100644 index 000000000..46d91fdc8 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java @@ -0,0 +1,222 @@ +package net.hollowcube.mapmaker.runtime.freeform; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.compiler.LuauCompileException; +import net.hollowcube.luau.compiler.LuauCompiler; +import net.hollowcube.mapmaker.map.AbstractMapWorld; +import net.hollowcube.mapmaker.map.MapData; +import net.hollowcube.mapmaker.map.MapServer; +import net.hollowcube.mapmaker.misc.BossBars; +import net.hollowcube.mapmaker.runtime.freeform.lua.LuaGlobals; +import net.hollowcube.mapmaker.runtime.freeform.lua.LuaTask; +import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; +import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaBlock; +import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaWorld; +import net.hollowcube.mapmaker.runtime.freeform.script.LuaScriptState; +import net.kyori.adventure.bossbar.BossBar; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class FreeformMapWorld extends AbstractMapWorld { + + private static final LuauCompiler LUAU_COMPILER = LuauCompiler.builder() + .userdataTypes() // todo + .vectorType("vector") + .vectorCtor("vec") + .build(); + + private final LuaState globalState; + private LuaScriptState worldThread; + + public FreeformMapWorld(MapServer server, MapData map) { + super(server, map, makeMapInstance(map, 'f'), FreeformState.class); + + this.globalState = createGlobalState(); + + // 1, 39, 1 + // -62, 39, -62 + +// eventNode() +// .addListener(PlayerTickEvent.class, this::handlePlayerTick) +// .addChild(EventUtil.READ_ONLY_NODE); + } + + public LuaState globalState() { + return this.globalState; + } + + //region World Lifecycle + + @Override + public void loadWorld() { + super.loadWorld(); + + this.worldThread = LuaScriptState.create(this); + try { + + // We use the roblox pattern of having a global "script" which can be used to access the "owner" of the script. + // For now this is kinda dumb since its just the world/player, HOWEVER it gets around a very cursed optimization. + // Luau will eagerly evaluate all __index-es on globals when the script is loaded, meaning that if the player + // was a global, all occurrences of `player.Position` would be evaluated immediately instead of when they + // actually occur. Gross. + this.worldThread.state().newTable(); + LuaWorld.push(this.worldThread.state(), new LuaWorld(this)); + this.worldThread.state().setField(-2, "Parent"); // Set the world as the parent + this.worldThread.state().setReadOnly(-1, true); // Make it read-only + this.worldThread.state().setGlobal("script"); + + worldThread.state().load("test.luau", LUAU_COMPILER.compile(""" + local world = script.Parent + + function create_bit_board(width, height) + local bits_needed = width * height + local bytes_needed = math.ceil(bits_needed / 8) + return buffer.create(bytes_needed), width, height + end + + function get_cell(board, width, x, y) + local bit_index = y * width + x + return buffer.readbits(board, bit_index, 1) + end + + function set_cell(board, width, x, y, value) + local bit_index = y * width + x + buffer.writebits(board, bit_index, 1, value and 1 or 0) + end + + function count_neighbors_bitwise(board, width, height, x, y) + local count = 0 + local base_bit = y * width + x + + for dy = -1, 1 do + for dx = -1, 1 do + if dx ~= 0 or dy ~= 0 then + local nx, ny = x + dx, y + dy + if nx >= 0 and nx < width and ny >= 0 and ny < height then + local neighbor_bit = (y + dy) * width + (x + dx) + count = count + buffer.readbits(board, neighbor_bit, 1) + end + end + end + end + return count + end + + local worldSpace = create_bit_board(64, 64) + local copySpace = create_bit_board(64, 64) + + -- init world + for x = 0, 63 do + for z = 0, 63 do + local idx = x * 64 + z + local active = world:GetBlock(vec(-x, 39, -z)) == Block.Stone + set_cell(worldSpace, 64, x, z, active) + end + end + + function step() + buffer.copy(copySpace, 0, worldSpace, 0, math.ceil(64 * 64 / 8)) + + for x = 0, 63 do + for z = 0, 63 do + local idx = x * 64 + z + local active = get_cell(copySpace, 64, x, z) + local neighbors = count_neighbors_bitwise(copySpace, 64, 64, x, z) + + if active then + -- A live cell with fewer than two live neighbors dies. + -- A live cell with more than three live neighbors dies. + if neighbors < 2 or neighbors > 3 then + set_cell(worldSpace, 64, x, z, false) + world:SetBlock(vec(-x, 39, -z), Block.Air) + end + -- A live cell with two or three live neighbors continues to live on to the next generation. + else + -- A dead cell with exactly three live neighbors becomes a live cell in the next generation. + if neighbors == 3 then + set_cell(worldSpace, 64, x, z, true) + world:SetBlock(vec(-x, 39, -z), Block.Stone) + end + end + end + end + + end + + task.spawn(function() + print('Hello, Task!') + task.wait(80) + print('should be printed later') + + step() + task.wait(20) + step() + task.wait(20) + step() + task.wait(20) + step() + task.wait(20) + step() + task.wait(20) + step() + end) + """)); + worldThread.state().pcall(0, 0); + } catch (LuauCompileException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() { + super.close(); + + this.worldThread.close(); + this.globalState.close(); + } + + //endregion + + //region Player Lifecycle + + @Override + protected FreeformState configurePlayer(Player player) { + + player.setRespawnPoint(map().settings().getSpawnPoint()); + + return new FreeformState.Playing(); + } + + @Override + protected @Nullable List createBossBars() { + return BossBars.createPlayingBossBar(server().playerService(), map()); + } + + //endregion + + private static LuaState createGlobalState() { + var global = LuaState.newState(); + global.openLibs(); // todo probably dont give all for now + + // 'Standard' Libraries + LuaGlobals.init(global); + LuaTask.init(global); + + // Global APIs + LuaVectorTypeImpl.init(global); +// LuaColor.init(global); +// LuaText.init(global); + +// LuaEventSource.init(global); + LuaBlock.init(global); + LuaWorld.init(global); +// LuaParticle.init(global); +// LuaEntity.init(global); +// LuaPlayer.init(global); + + global.sandbox(); + return global; + } +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java new file mode 100644 index 000000000..89394ad26 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java @@ -0,0 +1,18 @@ +package net.hollowcube.mapmaker.runtime.freeform; + +import net.hollowcube.mapmaker.map.PlayerState; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.Nullable; + +public sealed interface FreeformState extends PlayerState { + + record Playing() implements FreeformState { + + @Override + public void configurePlayer(FreeformMapWorld world, Player player, @Nullable FreeformState lastState) { + FreeformState.super.configurePlayer(world, player, lastState); + } + + } + +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaGlobals.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaGlobals.java new file mode 100644 index 000000000..c6d31514d --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaGlobals.java @@ -0,0 +1,35 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.mapmaker.runtime.freeform.script.LuaScriptState; +import net.kyori.adventure.text.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class LuaGlobals { + private static final Logger LOGGER = LoggerFactory.getLogger(LuaGlobals.class); + + public static void init(LuaState state) { + state.pushCFunction(LuaGlobals::print, "print"); + state.setGlobal("print"); + } + + private static int print(LuaState state) { + var builder = new StringBuilder(); + int top = state.getTop(); + for (int i = 1; i <= top; i++) { + var arg = state.toStringRepr(i); + if (i > 1) builder.append(" "); + builder.append(arg); + } + + // TODO: should include debug info in here later + var script = LuaScriptState.from(state); + var world = script.world(); + + LOGGER.info("[SCRIPT] {}", builder); + world.instance().sendMessage(Component.text("[SCRIPT] " + builder)); + + return 0; + } +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java new file mode 100644 index 000000000..bb638a9db --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java @@ -0,0 +1,99 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.LuaStatus; +import net.hollowcube.luau.LuaType; +import net.hollowcube.mapmaker.runtime.freeform.script.LuaScriptState; +import net.minestom.server.timer.TaskSchedule; + +import java.util.Map; +import java.util.function.Supplier; + +public class LuaTask { + private static final String NAME = "task"; + + public static void init(LuaState state) { + state.registerLib(NAME, Map.of( + "spawn", LuaTask::spawn, + "wait", LuaTask::wait + )); + state.pop(1); + } + + private static int spawn(LuaState state) { + state.checkType(1, LuaType.FUNCTION); // todo should support coroutine being passed here also + + var luaState = LuaScriptState.from(state); + + // Create a new thread + var thread = state.newThread(); + state.xPush(thread, 1); // Push the function onto the thread + + // Begin executing the new thread + // This abuses minestoms behavior of immediately calling the supplier when + // using this form of scheduleTask. + var task = new LuaTaskWrapper(luaState, thread); + thread.setThreadData(task); + luaState.world().scheduler().submitTask(task); + + // TODO: This ref is a straight memory leak + state.ref(-1); // Store the thread in the registry + state.pop(1); // Remove the thread + + return 0; + } + + private static int wait(LuaState state) { + int ticks = state.checkIntegerArg(1); + if (ticks < 0) { + state.argError(1, "must be a non-negative"); + return 0; + } + + if (!(state.getThreadData() instanceof LuaTaskWrapper task)) { + state.argError(1, "must be called from a task"); + return 0; + } + + task.waitTicks = ticks; + return state.yield(0); + } + + private static class LuaTaskWrapper implements Supplier, LuaScriptState.Holder { + private final LuaScriptState state; + private final LuaState thread; + + private int waitTicks = -1; + + private LuaTaskWrapper(LuaScriptState state, LuaState thread) { + this.state = state; + this.thread = thread; + } + + @Override + public LuaScriptState scriptState() { + return state; + } + + @Override + public TaskSchedule get() { + var status = thread.resume(null, 0); // again handle args here + if (status == LuaStatus.OK) { + return TaskSchedule.stop(); + } else if (status == LuaStatus.YIELD) { + if (waitTicks < 0) { + // Probably coroutine was yielded out of band + return TaskSchedule.stop(); + } + + int waitTicks = this.waitTicks; + this.waitTicks = -1; + return TaskSchedule.tick(waitTicks); + } + + // TODO on error this should log to the user + var error = thread.toString(-1); + throw new IllegalStateException("Unexpected Lua status: " + status + " with error: " + error); + } + } +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/math/LuaVectorTypeImpl.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/math/LuaVectorTypeImpl.java new file mode 100644 index 000000000..a5d41a118 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/math/LuaVectorTypeImpl.java @@ -0,0 +1,112 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.math; + +import net.hollowcube.luau.LuaState; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; + +public final class LuaVectorTypeImpl { + public static final String NAME = "vector"; + + public static void init(LuaState state) { + // Put a zero vector on the stack, we will eventually assign the metatable to it + state.pushVector(0f, 0f, 0f); + + // Create metatable + state.newMetaTable(NAME); + state.pushCFunction(LuaVectorTypeImpl::luaIndex, "__index"); + state.setField(-2, "__index"); + state.pushCFunction(LuaVectorTypeImpl::luaToString, "__tostring"); + state.setField(-2, "__tostring"); + state.pushCFunction(LuaVectorTypeImpl::luaAdd, "__add"); + state.setField(-2, "__add"); + state.pushCFunction(LuaVectorTypeImpl::luaSub, "__sub"); + state.setField(-2, "__sub"); + + // Assign to the zero vector and pop (aka all vectors) + state.setMetaTable(-2); + state.pop(1); + + // Create constructor + state.pushCFunction(LuaVectorTypeImpl::vectorCtor, "vec"); + state.setGlobal("vec"); + } + + public static void push(LuaState state, Point point) { + state.pushVector((float) point.x(), (float) point.y(), (float) point.z()); + } + + public static Point checkArg(LuaState state, int index) { + float[] raw = state.checkVectorArg(index); + return new Vec(raw[0], raw[1], raw[2]); + } + + private static int vectorCtor(LuaState state) { + double x = state.checkNumberArg(1); + double y = state.checkNumberArg(2); + double z = state.checkNumberArg(3); + state.pushVector((float) x, (float) y, (float) z); + return 1; + } + + static int luaIndex(LuaState state) { + var vec = state.checkVectorArg(1); + var name = state.checkStringArg(2); + + if ("Length".equals(name)) { + state.pushNumber(Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2])); + return 1; + } + + int elem = name.charAt(0) - 'X'; + if (elem < 0 || elem > 2) { + state.error("No such key: " + name); + return 0; + } + + state.pushNumber(vec[elem]); + return 1; + } + + static int luaToString(LuaState state) { + var vec = state.checkVectorArg(1); + state.pushString(String.format("vec(%f, %f, %f)", vec[0], vec[1], vec[2])); + return 1; + } + + static int luaAdd(LuaState state) { + var lhs = state.checkVectorArg(1); + var rhsType = state.type(2); + switch (rhsType) { + case NUMBER -> { + var n = (float) state.checkNumberArg(2); + state.pushVector(lhs[0] + n, lhs[1] + n, lhs[2] + n); + } + case VECTOR -> { + var rhs = state.checkVectorArg(2); + state.pushVector(lhs[0] + rhs[0], lhs[1] + rhs[1], lhs[2] + rhs[2]); + } + default -> state.error("Expected number or vector, got " + state.typeName(2)); + } + return 1; + } + + static int luaSub(LuaState state) { + var lhs = state.checkVectorArg(1); + var rhsType = state.type(2); + switch (rhsType) { + case NUMBER -> { + var n = (float) state.checkNumberArg(2); + state.pushVector(lhs[0] - n, lhs[1] - n, lhs[2] - n); + } + case VECTOR -> { + var rhs = state.checkVectorArg(2); + state.pushVector(lhs[0] - rhs[0], lhs[1] - rhs[1], lhs[2] - rhs[2]); + } + default -> state.error("Expected number or vector, got " + state.typeName(2)); + } + return 1; + } + + //todo other metamethods +} + diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/math/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/math/package-info.java new file mode 100644 index 000000000..1bb85a899 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/math/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform.lua.math; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/package-info.java new file mode 100644 index 000000000..3135eda6f --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform.lua; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlock.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlock.java new file mode 100644 index 000000000..7d15dffef --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlock.java @@ -0,0 +1,84 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.world; + +import net.hollowcube.common.util.BlockUtil; +import net.hollowcube.common.util.StringUtil; +import net.hollowcube.luau.LuaState; +import net.minestom.server.instance.block.Block; + +import java.util.HashMap; + +public class LuaBlock { + public static final String NAME = "Block"; + + public static void init(LuaState state) { + // Create the metatable for Minestom Block + state.newMetaTable(NAME); + state.pushCFunction(LuaBlock::luaToString, "__tostring"); + state.setField(-2, "__tostring"); + state.pushCFunction(LuaBlock::luaCall, "__call"); + state.setField(-2, "__call"); + state.pushCFunction(LuaBlock::luaEq, "__eq"); + state.setField(-2, "__eq"); + state.pop(1); + + // Global table of all blocks + // todo this should probably just be an index metamethod, theres no need to prealloc this. + state.newTable(); + for (var block : Block.values()) { + var friendlyName = StringUtil.snakeToPascal( + block.key().value().replace("/", "_")); + + push(state, block); + state.setField(-2, friendlyName); + } + state.setReadOnly(-1, true); + state.setGlobal("Block"); + } + + public static void push(LuaState state, Block block) { + state.newUserData(block); + state.getMetaTable(NAME); + state.setMetaTable(-2); + } + + public static Block checkArg(LuaState state, int index) { + return (Block) state.checkUserDataArg(index, NAME); + } + + private static int luaToString(LuaState state) { + var block = checkArg(state, 1); + state.pushString(BlockUtil.toString(block)); + return 1; + } + + private static int luaCall(LuaState state) { + var block = checkArg(state, 1); + var newProps = new HashMap(); + state.pushNil(); + while (state.next(2)) { + // Key is at index -2, value is at index -1 + String key = state.toString(-2); + String value = state.toString(-1); + newProps.put(key, value); + + // Remove the value, keep the key for the next iteration + state.pop(1); + } + + try { + push(state, block.withProperties(newProps)); + return 1; + } catch (IllegalArgumentException e) { + state.error(e.getMessage()); + return 0; + } + } + + private static int luaEq(LuaState state) { + var block1 = checkArg(state, 1); + var block2 = checkArg(state, 2); + state.pushBoolean(block1.stateId() == block2.stateId()); + return 1; + } +} + diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java new file mode 100644 index 000000000..1323b6ac5 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java @@ -0,0 +1,98 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.world; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.mapmaker.runtime.freeform.FreeformMapWorld; +import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; + +import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchKey; +import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchMethod; + +public class LuaWorld { + private static final String NAME = "World"; + + public static void init(LuaState state) { + // Create the metatable for Entity + state.newMetaTable(NAME); + state.pushCFunction(LuaWorld::luaIndex, "__index"); + state.setField(-2, "__index"); + state.pushCFunction(LuaWorld::luaNewIndex, "__newindex"); + state.setField(-2, "__newindex"); + state.pushCFunction(LuaWorld::luaNameCall, "__namecall"); + state.setField(-2, "__namecall"); + state.pop(1); + } + + public static void push(LuaState state, LuaWorld entity) { + state.newUserData(entity); + state.getMetaTable(NAME); + state.setMetaTable(-2); + } + + public static LuaWorld checkArg(LuaState state, int index) { + return (LuaWorld) state.checkUserDataArg(index, NAME); + } + + private final FreeformMapWorld delegate; + + public LuaWorld(FreeformMapWorld world) { + this.delegate = world; + } + + // Properties + + private int getUuid(LuaState state) { + state.pushString(delegate.map().id()); + return 1; + } + + // Methods + + private int getBlock(LuaState state) { + var blockPosition = LuaVectorTypeImpl.checkArg(state, 1); + + var block = delegate.instance().getBlock(blockPosition); + LuaBlock.push(state, block); + return 1; + } + + private int setBlock(LuaState state) { + var blockPosition = LuaVectorTypeImpl.checkArg(state, 1); + var block = LuaBlock.checkArg(state, 2); + + delegate.instance().setBlock(blockPosition, block); + return 0; + } + + // Metamethods + + private static int luaIndex(LuaState state) { + final LuaWorld world = checkArg(state, 1); + final String key = state.checkStringArg(2); + return switch (key) { + case "Uuid" -> world.getUuid(state); + default -> noSuchKey(state, NAME, key); + }; + } + + private static int luaNewIndex(LuaState state) { + final LuaWorld world = checkArg(state, 1); + final String key = state.checkStringArg(2); + state.remove(1); // Remove the userdata from the stack + state.remove(1); // Remove the key from the stack + return switch (key) { + default -> noSuchKey(state, NAME, key); + }; + } + + private static int luaNameCall(LuaState state) { + final LuaWorld world = checkArg(state, 1); + state.remove(1); // Remove the world userdata from the stack (so implementations can pretend they have no self) + final String methodName = state.nameCallAtom(); + return switch (methodName) { + case "GetBlock" -> world.getBlock(state); + case "SetBlock" -> world.setBlock(state); + default -> noSuchMethod(state, NAME, methodName); + }; + } + +} \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/package-info.java new file mode 100644 index 000000000..54525e628 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform.lua.world; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/package-info.java new file mode 100644 index 000000000..1bf31865f --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java new file mode 100644 index 000000000..c7b1409b3 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java @@ -0,0 +1,83 @@ +package net.hollowcube.mapmaker.runtime.freeform.script; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.LuaType; +import net.kyori.adventure.key.InvalidKeyException; +import net.kyori.adventure.key.Key; + +import java.util.function.Consumer; + +public class LuaHelpers { + + public static int noSuchKey(LuaState state, String typeName, String methodName) { + state.error("No such key '" + methodName + "' for " + typeName); + return 0; // Never reached, just to make java happy + } + + public static int noSuchMethod(LuaState state, String typeName, String methodName) { + state.error("No such method '" + methodName + "' for " + typeName); + return 0; // Never reached, just to make java happy + } + + /// Iterates over a table (no checks to ensure its a table) and applies the given function for each key. + /// During the callback, the value is always at index -1 (and the key at -2 if needed). + /// + /// The state should be left _exactly_ as it was before the call (value at -1). + public static void tableForEach(LuaState state, int tableIndex, Consumer func) { + state.pushNil(); + while (state.next(2)) { + // Key is at index -2, value is at index -1 + String key = state.toString(-2); + func.accept(key); + + // Remove the value, keep the key for the next iteration + state.pop(1); + } + } + + // Returns true if the key exists, it is at the top of the stack. + public static boolean tableGet(LuaState state, int tableIndex, String key) { + state.getField(tableIndex, key); + if (state.isNil(-1)) { + state.pop(1); // Pop the nil value + return false; + } + return true; + } + + public static Key checkKeyArg(LuaState state, int index) { + var key = state.checkStringArg(index); + try { + return Key.key(key); + } catch (InvalidKeyException e) { + state.error("Invalid key: " + key); + return null; // Never reached, just to make java happy + } + } + + public static float[] checkFloat4Arg(LuaState state, int index) { + state.checkType(index, LuaType.TABLE); + float[] floats = new float[4]; + for (int i = 0; i < 4; i++) { + state.rawGetI(index, i + 1); + if (state.isNumber(-1)) { + floats[i] = (float) state.toNumber(-1); + } else { + state.argError(index, "Expected a number at index " + (i + 1)); + } + state.pop(1); // Pop the value + } + return floats; + } + + public static void pushFloat4(LuaState state, float[] floats) { + if (floats.length != 4) throw new IllegalArgumentException("Float4 must have exactly 4 elements"); + state.newTable(); + for (int i = 0; i < 4; i++) { + state.pushNumber(floats[i]); + state.rawSetI(-2, i + 1); + } + } + +} + diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaScriptState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaScriptState.java new file mode 100644 index 000000000..74bb5343e --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaScriptState.java @@ -0,0 +1,55 @@ +package net.hollowcube.mapmaker.runtime.freeform.script; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.mapmaker.runtime.freeform.FreeformMapWorld; + +public final class LuaScriptState { + + public static LuaScriptState from(LuaState state) { + return switch (state.getThreadData()) { + case LuaScriptState threadState -> threadState; + case Holder holder -> holder.scriptState(); + case null -> throw new IllegalStateException("No thread data set for LuaState: " + state); + default -> + throw new IllegalArgumentException("Invalid thread data type: " + state.getThreadData().getClass()); + }; + } + + public static LuaScriptState create(FreeformMapWorld world) { + var thread = world.globalState().newThread(); + thread.sandboxThread(); // Create mutable user space + int ref = world.globalState().ref(-1); + + var luaScriptState = new LuaScriptState(world, thread, ref); + thread.setThreadData(luaScriptState); + return luaScriptState; + } + + public interface Holder { + LuaScriptState scriptState(); + } + + private final FreeformMapWorld world; + + private final LuaState state; + private final int stateRef; // A ref in the global state keeping the thread alive. + + private LuaScriptState(FreeformMapWorld world, LuaState state, int stateRef) { + this.world = world; + this.state = state; + this.stateRef = stateRef; + } + + public FreeformMapWorld world() { + return world; + } + + public LuaState state() { + return this.state; + } + + public void close() { + this.world.globalState().unref(this.stateRef); + System.out.println("CLOSING CHILD THREAD WITH STATUS: " + this.state.status()); + } +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/package-info.java new file mode 100644 index 000000000..16edd4e67 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform.script; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file From 3d8be4ec70461173431a57423368409a19bf847e Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 13 Sep 2025 20:27:38 -0400 Subject: [PATCH 02/22] feat: task.cancel, tweaked gol --- .../runtime/freeform/FreeformMapWorld.java | 74 +++++++++---------- .../runtime/freeform/lua/LuaTask.java | 20 ++++- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java index 46d91fdc8..e3dc5811e 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java @@ -88,15 +88,13 @@ function set_cell(board, width, x, y, value) function count_neighbors_bitwise(board, width, height, x, y) local count = 0 - local base_bit = y * width + x for dy = -1, 1 do for dx = -1, 1 do - if dx ~= 0 or dy ~= 0 then + if dx ~= 0 or dy ~= 0 then -- Skip center cell local nx, ny = x + dx, y + dy if nx >= 0 and nx < width and ny >= 0 and ny < height then - local neighbor_bit = (y + dy) * width + (x + dx) - count = count + buffer.readbits(board, neighbor_bit, 1) + count = count + buffer.readbits(board, ny * width + nx, 1) end end end @@ -106,13 +104,14 @@ function count_neighbors_bitwise(board, width, height, x, y) local worldSpace = create_bit_board(64, 64) local copySpace = create_bit_board(64, 64) + local stepTask = nil - -- init world - for x = 0, 63 do - for z = 0, 63 do - local idx = x * 64 + z - local active = world:GetBlock(vec(-x, 39, -z)) == Block.Stone - set_cell(worldSpace, 64, x, z, active) + function init() + for x = 0, 63 do + for z = 0, 63 do + local active = world:GetBlock(vec(-x, 39, -z)) == Block.Stone + set_cell(worldSpace, 64, x, z, active) + end end end @@ -121,47 +120,46 @@ function step() for x = 0, 63 do for z = 0, 63 do - local idx = x * 64 + z - local active = get_cell(copySpace, 64, x, z) + local active = get_cell(copySpace, 64, x, z) == 1 local neighbors = count_neighbors_bitwise(copySpace, 64, 64, x, z) + local new_state = false if active then - -- A live cell with fewer than two live neighbors dies. - -- A live cell with more than three live neighbors dies. - if neighbors < 2 or neighbors > 3 then - set_cell(worldSpace, 64, x, z, false) - world:SetBlock(vec(-x, 39, -z), Block.Air) + if neighbors == 2 or neighbors == 3 then + new_state = true -- Cell survives + else + new_state = false -- Cell dies end - -- A live cell with two or three live neighbors continues to live on to the next generation. else - -- A dead cell with exactly three live neighbors becomes a live cell in the next generation. if neighbors == 3 then - set_cell(worldSpace, 64, x, z, true) - world:SetBlock(vec(-x, 39, -z), Block.Stone) + new_state = true -- Cell becomes alive end end + + if new_state ~= active then + set_cell(worldSpace, 64, x, z, new_state) + world:SetBlock(vec(-x, 39, -z), new_state and Block.Stone or Block.Air) + end end end - end - task.spawn(function() - print('Hello, Task!') - task.wait(80) - print('should be printed later') + function toggleGame() + if stepTask then + task.cancel(stepTask) + stepTask = nil + else + stepTask = task.spawn(function() + init() + while true do + task.wait(10) + step() + end + end) + end + end - step() - task.wait(20) - step() - task.wait(20) - step() - task.wait(20) - step() - task.wait(20) - step() - task.wait(20) - step() - end) + toggleGame() """)); worldThread.state().pcall(0, 0); } catch (LuauCompileException e) { diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java index bb638a9db..34356e1c7 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java @@ -4,6 +4,7 @@ import net.hollowcube.luau.LuaStatus; import net.hollowcube.luau.LuaType; import net.hollowcube.mapmaker.runtime.freeform.script.LuaScriptState; +import net.minestom.server.timer.Task; import net.minestom.server.timer.TaskSchedule; import java.util.Map; @@ -15,6 +16,7 @@ public class LuaTask { public static void init(LuaState state) { state.registerLib(NAME, Map.of( "spawn", LuaTask::spawn, + "cancel", LuaTask::cancel, "wait", LuaTask::wait )); state.pop(1); @@ -34,12 +36,25 @@ private static int spawn(LuaState state) { // using this form of scheduleTask. var task = new LuaTaskWrapper(luaState, thread); thread.setThreadData(task); - luaState.world().scheduler().submitTask(task); + task.selfRef = luaState.world().scheduler().submitTask(task); // TODO: This ref is a straight memory leak state.ref(-1); // Store the thread in the registry - state.pop(1); // Remove the thread + // Return the thread + state.pop(1); + return 1; + } + + private static int cancel(LuaState state) { + state.checkType(1, LuaType.THREAD); + var thread = state.toThread(1); + if (!(thread.getThreadData() instanceof LuaTaskWrapper task)) { + state.argError(1, "must be called with a task"); + return 0; + } + + task.selfRef.cancel(); return 0; } @@ -62,6 +77,7 @@ private static int wait(LuaState state) { private static class LuaTaskWrapper implements Supplier, LuaScriptState.Holder { private final LuaScriptState state; private final LuaState thread; + private Task selfRef; private int waitTicks = -1; From ef432eee49df25b7aaf1939e3ca92e266c5e3e37 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 13 Sep 2025 21:17:21 -0400 Subject: [PATCH 03/22] chore: bump luau to 0.4.0 --- bin/development/build.gradle.kts | 4 ---- bin/example/build.gradle.kts | 5 ----- bin/hub/build.gradle.kts | 5 ----- bin/map-isolate/build.gradle.kts | 5 ----- bin/map/build.gradle.kts | 5 ----- gradle/libs.versions.toml | 14 +++++++------- modules/map-runtime/build.gradle.kts | 4 ---- .../runtime/freeform/FreeformMapWorld.java | 2 +- 8 files changed, 8 insertions(+), 36 deletions(-) diff --git a/bin/development/build.gradle.kts b/bin/development/build.gradle.kts index f45a93072..e7affa1d4 100644 --- a/bin/development/build.gradle.kts +++ b/bin/development/build.gradle.kts @@ -3,10 +3,6 @@ plugins { id("mapmaker.packer-data") } -repositories { - mavenLocal() -} - dependencies { implementation(project(":bin:config")) implementation(project(":bin:hub")) diff --git a/bin/example/build.gradle.kts b/bin/example/build.gradle.kts index a3061e090..b16574b50 100644 --- a/bin/example/build.gradle.kts +++ b/bin/example/build.gradle.kts @@ -3,11 +3,6 @@ plugins { id("mapmaker.packer-data") } -repositories { - mavenLocal() - mavenCentral() -} - dependencies { implementation(project(":bin:config")) diff --git a/bin/hub/build.gradle.kts b/bin/hub/build.gradle.kts index 545a4e5e8..2d50eabd6 100644 --- a/bin/hub/build.gradle.kts +++ b/bin/hub/build.gradle.kts @@ -3,11 +3,6 @@ plugins { id("mapmaker.packer-data") } -repositories { - mavenLocal() - mavenCentral() -} - dependencies { implementation(project(":bin:config")) diff --git a/bin/map-isolate/build.gradle.kts b/bin/map-isolate/build.gradle.kts index 62f6f4dc4..c94aa92f6 100644 --- a/bin/map-isolate/build.gradle.kts +++ b/bin/map-isolate/build.gradle.kts @@ -4,11 +4,6 @@ plugins { id("org.graalvm.buildtools.native") version "0.10.6" } -repositories { - mavenLocal() - mavenCentral() -} - dependencies { implementation(project(":bin:config")) diff --git a/bin/map/build.gradle.kts b/bin/map/build.gradle.kts index abfd9087e..3254302ef 100644 --- a/bin/map/build.gradle.kts +++ b/bin/map/build.gradle.kts @@ -3,11 +3,6 @@ plugins { id("mapmaker.packer-data") } -repositories { - mavenLocal() - mavenCentral() -} - dependencies { implementation(project(":bin:config")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c84844d9..6e85231e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,8 @@ blossom = "2.1.0" velocity = "3.4.0-SNAPSHOT" classgraph = "4.8.179" included = "-INCLUDED" -luau = "0.3.2" +luau = "0.4.0" +luau-natives = "0.4.0" [libraries] annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } @@ -65,10 +66,10 @@ adventure-text-serializer-plain = { group = "net.kyori", name = "adventure-text- adventure-nbt = { group = "net.kyori", name = "adventure-nbt", version.ref = "adventure" } luau-lib = { group = "dev.hollowcube", name = "luau", version.ref = "luau" } -luau-natives-macos-x64 = { group = "dev.hollowcube", name = "luau-natives-macos-x64", version.ref = "luau" } -luau-natives-macos-arm64 = { group = "dev.hollowcube", name = "luau-natives-macos-arm64", version.ref = "luau" } -luau-natives-linux-x64 = { group = "dev.hollowcube", name = "luau-natives-linux-x64", version.ref = "luau" } -luau-natives-windows-x64 = { group = "dev.hollowcube", name = "luau-natives-windows-x64", version.ref = "luau" } +luau-natives-macos-x64 = { group = "dev.hollowcube", name = "luau-natives-macos-x64", version.ref = "luau-natives" } +luau-natives-macos-arm64 = { group = "dev.hollowcube", name = "luau-natives-macos-arm64", version.ref = "luau-natives" } +luau-natives-linux-x64 = { group = "dev.hollowcube", name = "luau-natives-linux-x64", version.ref = "luau-natives" } +luau-natives-windows-x64 = { group = "dev.hollowcube", name = "luau-natives-windows-x64", version.ref = "luau-natives" } prometheus = { group = "io.prometheus", name = "simpleclient", version.ref = "prometheus" } prometheus-hotspot = { group = "io.prometheus", name = "simpleclient_hotspot", version.ref = "prometheus" } @@ -93,8 +94,7 @@ junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", ver adventure = ["adventure-api", "adventure-key", "adventure-text-minimessage", "adventure-text-serializer-plain", "adventure-nbt"] prometheus = ["prometheus", "prometheus-hotspot", "prometheus-httpserver"] otel = ["otel-api", "otel-context", "otel-sdk", "otel-sdk-common", "otel-sdk-trace", "otel-extension-trace-propagators", "otel-exporter-logging", "otel-exporter-otlp", "otel-exporter-sender-jdk", "otel-semconv"] -luau = ["luau-natives-macos-x64", "luau-natives-macos-arm64", "luau-natives-linux-x64", "luau-natives-windows-x64"] -#luau = ["luau-lib", "luau-natives-macos-x64", "luau-natives-macos-arm64", "luau-natives-linux-x64", "luau-natives-windows-x64"] +luau = ["luau-lib", "luau-natives-macos-x64", "luau-natives-macos-arm64", "luau-natives-linux-x64", "luau-natives-windows-x64"] [plugins] blossom = { id = "net.kyori.blossom", version.ref = "blossom" } diff --git a/modules/map-runtime/build.gradle.kts b/modules/map-runtime/build.gradle.kts index 404eefbdb..3a88019e3 100644 --- a/modules/map-runtime/build.gradle.kts +++ b/modules/map-runtime/build.gradle.kts @@ -2,10 +2,6 @@ plugins { id("mapmaker.java-library") } -repositories { - mavenLocal() -} - dependencies { api(project(":modules:map-core")) api(project(":modules:terraform")) //TODO: this exists for entity implementations, but it shouldn't. diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java index e3dc5811e..69622c27d 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java @@ -152,7 +152,7 @@ function toggleGame() stepTask = task.spawn(function() init() while true do - task.wait(10) + task.wait(2) step() end end) From 1a7c23eb97541aca6a0db3932104feac293fb2ba Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 13 Sep 2025 23:00:23 -0400 Subject: [PATCH 04/22] chore: native image slop --- tools/native-image-helper/build.gradle.kts | 1 + .../nativeimage/HCNativeImageFeature.java | 145 +++++++++++++++++- 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/tools/native-image-helper/build.gradle.kts b/tools/native-image-helper/build.gradle.kts index 4aae9e3ea..c8c8c4146 100644 --- a/tools/native-image-helper/build.gradle.kts +++ b/tools/native-image-helper/build.gradle.kts @@ -5,4 +5,5 @@ plugins { dependencies { implementation(libs.nativeimage) implementation(libs.classgraph) + implementation(libs.luau.lib) } diff --git a/tools/native-image-helper/src/main/java/net/hollowcube/nativeimage/HCNativeImageFeature.java b/tools/native-image-helper/src/main/java/net/hollowcube/nativeimage/HCNativeImageFeature.java index 1505390fb..4cd5cdbfc 100644 --- a/tools/native-image-helper/src/main/java/net/hollowcube/nativeimage/HCNativeImageFeature.java +++ b/tools/native-image-helper/src/main/java/net/hollowcube/nativeimage/HCNativeImageFeature.java @@ -3,14 +3,19 @@ import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; import io.github.classgraph.ScanResult; -import org.graalvm.nativeimage.hosted.Feature; -import org.graalvm.nativeimage.hosted.RuntimeReflection; -import org.graalvm.nativeimage.hosted.RuntimeResourceAccess; +import net.hollowcube.luau.util.GlobalRef; +import org.graalvm.nativeimage.hosted.*; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; +import java.lang.foreign.AddressLayout; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.ValueLayout; import java.lang.reflect.Constructor; +import static java.lang.foreign.ValueLayout.JAVA_BYTE; + /// Responsible for doing a bunch of dynamic registration required for native image. /// /// * Record classes in net.hollowcube.mapmaker are automatically registered for reflection (required for gson) @@ -21,6 +26,140 @@ /// * Minestom MetadataDef subclasses are registered for runtime lookup. public class HCNativeImageFeature implements Feature { + private static final ValueLayout.OfShort C_SHORT = ValueLayout.JAVA_SHORT; + private static final ValueLayout.OfInt C_INT = ValueLayout.JAVA_INT; + private static final ValueLayout.OfFloat C_FLOAT = ValueLayout.JAVA_FLOAT; + private static final ValueLayout.OfDouble C_DOUBLE = ValueLayout.JAVA_DOUBLE; + private static final AddressLayout C_POINTER = ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, JAVA_BYTE)); + private static final ValueLayout.OfLong C_LONG = ValueLayout.JAVA_LONG; + + @Override + public void duringSetup(DuringSetupAccess access) { + RuntimeJNIAccess.register(GlobalRef.class); + + // todo probably can set Linker.Option.critical() for lots of the functions + // There are also a lot of duplicates, but i(matt) am lazy to remove them. + RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_POINTER, C_LONG, C_LONG)); + RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.of(C_INT, C_POINTER)); + RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.of(C_SHORT, C_POINTER, C_LONG)); + RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.ofVoid(C_POINTER, C_LONG, C_LONG)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_POINTER, C_LONG, C_LONG)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_LONG, C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_SHORT, C_POINTER, C_LONG)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_LONG, C_LONG)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_DOUBLE, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_DOUBLE)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_FLOAT, C_FLOAT, C_FLOAT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_LONG)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_LONG, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_LONG, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_LONG)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_POINTER, C_LONG, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_LONG, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_DOUBLE)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT, C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_DOUBLE, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_DOUBLE, C_POINTER, C_INT, C_DOUBLE)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER, C_INT)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(C_INT, C_POINTER)); + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(C_POINTER)); + } + @Override public void beforeAnalysis(BeforeAnalysisAccess access) { var canvasClasses = new CanvasClasses(access); From aca773af13db052f918e47408de5cb2912a5d221 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 13 Sep 2025 23:06:32 -0400 Subject: [PATCH 05/22] chore: enable ffm in native image --- bin/map-isolate/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/map-isolate/build.gradle.kts b/bin/map-isolate/build.gradle.kts index c94aa92f6..bccb9dc3b 100644 --- a/bin/map-isolate/build.gradle.kts +++ b/bin/map-isolate/build.gradle.kts @@ -54,6 +54,7 @@ graalvmNative { buildArgs( listOf( "--enable-native-access=ALL-UNNAMED", "--enable-monitoring=jfr", + "-H:+UnlockExperimentalVMOptions", "-H:+ForeignAPISupport", "--features=net.hollowcube.nativeimage.HCNativeImageFeature", "--static-nolibc", "--no-fallback", "--emit build-report", From a853a664b347cd8d4344add593ce4cf95f6ef342 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 13 Sep 2025 23:19:46 -0400 Subject: [PATCH 06/22] chore: graal meta for luau natives --- .../net.hollowcube/luau/resource-config.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/resource-config.json diff --git a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/resource-config.json b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/resource-config.json new file mode 100644 index 000000000..d0aa2f3c3 --- /dev/null +++ b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/resource-config.json @@ -0,0 +1,10 @@ +{ + "bundles": [], + "resources": { + "includes": [ + { + "pattern": "net/hollowcube/luau/(windows|linux|macos)/(x64|arm64)/lib(compiler|globalref|vm).(so|dll|dylib)" + } + ] + } +} \ No newline at end of file From f6b438643778ff62a5239c536a85423ef6bb17ab Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 13 Sep 2025 23:30:38 -0400 Subject: [PATCH 07/22] chore: switch to distroless/cc --- bin/map-isolate/deploy/Dockerfile-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/map-isolate/deploy/Dockerfile-native b/bin/map-isolate/deploy/Dockerfile-native index 3b6315914..c3a176302 100644 --- a/bin/map-isolate/deploy/Dockerfile-native +++ b/bin/map-isolate/deploy/Dockerfile-native @@ -1,4 +1,4 @@ -FROM gcr.io/distroless/base +FROM gcr.io/distroless/cc USER 65532:65532 From 1f8a9ca0f136667c3a41c5f7dda6aefe47a05a3c Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 13 Sep 2025 23:47:05 -0400 Subject: [PATCH 08/22] chore: try using much heavier base image to see if missing libs --- bin/map-isolate/deploy/Dockerfile-native | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/map-isolate/deploy/Dockerfile-native b/bin/map-isolate/deploy/Dockerfile-native index c3a176302..10ceb6cb8 100644 --- a/bin/map-isolate/deploy/Dockerfile-native +++ b/bin/map-isolate/deploy/Dockerfile-native @@ -1,4 +1,5 @@ -FROM gcr.io/distroless/cc +#FROM gcr.io/distroless/cc +FROM debian:bookworm-slim USER 65532:65532 From c183dd51c9338c68f65dcb071c1d3da68a99d207 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 14 Sep 2025 00:00:09 -0400 Subject: [PATCH 09/22] chore: upgrade debian --- bin/map-isolate/deploy/Dockerfile-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/map-isolate/deploy/Dockerfile-native b/bin/map-isolate/deploy/Dockerfile-native index 10ceb6cb8..e8b66a2f0 100644 --- a/bin/map-isolate/deploy/Dockerfile-native +++ b/bin/map-isolate/deploy/Dockerfile-native @@ -1,5 +1,5 @@ #FROM gcr.io/distroless/cc -FROM debian:bookworm-slim +FROM debian:trixie-slim USER 65532:65532 From 36b5678827698d18f083cc3fba50719cd1497f70 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 14 Sep 2025 00:23:32 -0400 Subject: [PATCH 10/22] chore: reflect config for luau function types --- .../net.hollowcube/luau/reflect-config.json | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/reflect-config.json diff --git a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/reflect-config.json b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/reflect-config.json new file mode 100644 index 000000000..e4fb41f24 --- /dev/null +++ b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/reflect-config.json @@ -0,0 +1,36 @@ +[ + { + "name": "net.hollowcube.luau.internal.vm.lua_CFunction$Function", + "methods": [ + { + "name": "apply", + "parameterTypes": [ + "java.lang.foreign.MemorySegment" + ] + } + ] + }, + { + "name": "net.hollowcube.luau.internal.vm.lua_Callbacks$userthread$Function", + "methods": [ + { + "name": "apply", + "parameterTypes": [ + "java.lang.foreign.MemorySegment", + "java.lang.foreign.MemorySegment" + ] + } + ] + }, + { + "name": "net.hollowcube.luau.internal.vm.lua_newuserdatadtor$dtor$Function", + "methods": [ + { + "name": "apply", + "parameterTypes": [ + "java.lang.foreign.MemorySegment" + ] + } + ] + } +] \ No newline at end of file From 4ab4608da9ad4ba900940f3edafefd67836bdb6a Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 14 Sep 2025 00:29:57 -0400 Subject: [PATCH 11/22] fix: more missing reflect metadata --- .../native-image/net.hollowcube/mapmaker/reflect-config.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/mapmaker/reflect-config.json b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/mapmaker/reflect-config.json index a94f43a1a..af5be8071 100644 --- a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/mapmaker/reflect-config.json +++ b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/mapmaker/reflect-config.json @@ -141,5 +141,10 @@ "name": "net.hollowcube.mapmaker.runtime.parkour.ParkourState$AnyPlaying", "allDeclaredClasses": true, "allPermittedSubclasses": true + }, + { + "name": "net.hollowcube.mapmaker.runtime.freeform.FreeformState", + "allDeclaredClasses": true, + "allPermittedSubclasses": true } ] \ No newline at end of file From 206a982c468441503d1605a27fc594aed8e1a64f Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 14 Sep 2025 12:58:47 -0400 Subject: [PATCH 12/22] chore: load scripts dynamically (this breaks prod), ij formatter notebook --- .../mapmaker/isolate/MapIsolateServer.java | 9 +- idea/Luau-Formatter.ipynb | 139 +++++++++++++++ .../runtime/freeform/FreeformMapWorld.java | 163 +++++------------- .../freeform/bundle/LocalFsLoader.java | 85 +++++++++ .../runtime/freeform/bundle/ScriptBundle.java | 28 +++ .../runtime/freeform/bundle/package-info.java | 4 + scripts/button-clicker/map.json | 9 + scripts/button-clicker/world.luau | 3 + scripts/game-of-life/map.json | 9 + scripts/game-of-life/world.luau | 92 ++++++++++ 10 files changed, 418 insertions(+), 123 deletions(-) create mode 100644 idea/Luau-Formatter.ipynb create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/LocalFsLoader.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ScriptBundle.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/package-info.java create mode 100644 scripts/button-clicker/map.json create mode 100644 scripts/button-clicker/world.luau create mode 100644 scripts/game-of-life/map.json create mode 100644 scripts/game-of-life/world.luau diff --git a/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java b/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java index b3bd04c26..b61c0b287 100644 --- a/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java +++ b/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java @@ -9,6 +9,8 @@ import net.hollowcube.mapmaker.map.runtime.ServerBridge; import net.hollowcube.mapmaker.misc.ResourcePackManager; import net.hollowcube.mapmaker.runtime.freeform.FreeformMapWorld; +import net.hollowcube.mapmaker.runtime.freeform.bundle.LocalFsLoader; +import net.hollowcube.mapmaker.runtime.freeform.bundle.ScriptBundle; import net.hollowcube.mapmaker.runtime.parkour.ParkourMapWorld; import net.hollowcube.mapmaker.session.Presence; import net.kyori.adventure.text.Component; @@ -23,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.file.Path; import java.util.Arrays; import java.util.UUID; @@ -31,6 +34,8 @@ public class MapIsolateServer extends AbstractMapServer { private static final Logger logger = LoggerFactory.getLogger(MapIsolateServer.class); + private final ScriptBundle.Loader scriptLoader; + private final String mapId; // Its only kinda unknown. it's not created in the constructor, but after prepareState @@ -51,6 +56,8 @@ public MapIsolateServer(ConfigLoaderV3 config) { .addListener(AsyncPlayerConfigurationEvent.class, this::handleConfigPhase) .addListener(PlayerSpawnEvent.class, this::handleSpawn) .addListener(PlayerDisconnectEvent.class, this::handleDisconnect)); + + this.scriptLoader = new LocalFsLoader(Path.of("../../scripts")); } @Override @@ -80,7 +87,7 @@ protected void prepareStart() { try { var map = mapService().getMap(Uuids.ZERO, this.mapId); - world = new FreeformMapWorld(this, map); + world = new FreeformMapWorld(this, map, scriptLoader); world.loadWorld(); // We schedule on first tick end because submitTask invokes the executor immediately to determine diff --git a/idea/Luau-Formatter.ipynb b/idea/Luau-Formatter.ipynb new file mode 100644 index 000000000..a9907a0dd --- /dev/null +++ b/idea/Luau-Formatter.ipynb @@ -0,0 +1,139 @@ +{ + "cells": [ + { + "cell_type": "code", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-09-14T16:41:46.318731Z", + "start_time": "2025-09-14T16:41:45.419424Z" + } + }, + "source": "%use intellij-platform", + "outputs": [ + { + "data": { + "text/plain": [ + "IntelliJ Platform integration is loaded" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 1 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-14T16:58:11.272099Z", + "start_time": "2025-09-14T16:58:11.025190Z" + } + }, + "cell_type": "code", + "source": [ + "import com.intellij.execution.configurations.GeneralCommandLine\n", + "import com.intellij.execution.process.CapturingProcessAdapter\n", + "import com.intellij.execution.process.OSProcessHandler\n", + "import com.intellij.execution.process.ProcessEvent\n", + "import com.intellij.formatting.service.AsyncDocumentFormattingService\n", + "import com.intellij.formatting.service.AsyncFormattingRequest\n", + "import com.intellij.formatting.service.FormattingService\n", + "import com.intellij.openapi.util.NlsSafe\n", + "import com.intellij.psi.PsiFile\n", + "import java.nio.charset.StandardCharsets\n", + "import java.util.EnumSet\n", + "\n", + "class StyluaFormatter : AsyncDocumentFormattingService() {\n", + "\n", + " override fun getName() = \"StyLua Formatter\"\n", + "\n", + " override fun getNotificationGroupId() = \"StyLua Formatter\"\n", + "\n", + " override fun getFeatures() = EnumSet.noneOf(FormattingService.Feature::class.java)\n", + "\n", + " override fun canFormat(file: PsiFile): Boolean {\n", + " return file.virtualFile.extension == \"luau\"\n", + " }\n", + "\n", + " override fun createFormattingTask(request: AsyncFormattingRequest): FormattingTask? {\n", + " val path = request.ioFile?.toPath() ?: return null\n", + "\n", + " val commandLine = GeneralCommandLine()\n", + " .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)\n", + " .withExePath(\"stylua\")\n", + " .withWorkingDirectory(path.parent)\n", + " .withParameters(\"--no-editorconfig\", \"-\")\n", + " .withInput(request.ioFile)\n", + " val handler = OSProcessHandler(commandLine.withCharset(StandardCharsets.UTF_8))\n", + " return object : FormattingTask {\n", + " override fun run() {\n", + " handler.addProcessListener(object : CapturingProcessAdapter() {\n", + " override fun processTerminated(event: ProcessEvent) {\n", + " if (event.exitCode == 0) {\n", + " request.onTextReady(output.stdout)\n", + " } else {\n", + " request.onTextReady(output.stderr)\n", + " }\n", + " }\n", + " })\n", + " handler.startNotify()\n", + " }\n", + "\n", + " override fun cancel(): Boolean {\n", + " handler.destroyProcess()\n", + " return true\n", + " }\n", + "\n", + " override fun isRunUnderProgress(): Boolean {\n", + " return true\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "registerExtension(FormattingService.EP_NAME, StyluaFormatter())" + ], + "outputs": [], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "2.2.20-Beta2", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "sessionRunMode": "IDE_PROCESS" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java index 69622c27d..709ce10fd 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java @@ -1,12 +1,12 @@ package net.hollowcube.mapmaker.runtime.freeform; import net.hollowcube.luau.LuaState; -import net.hollowcube.luau.compiler.LuauCompileException; import net.hollowcube.luau.compiler.LuauCompiler; import net.hollowcube.mapmaker.map.AbstractMapWorld; import net.hollowcube.mapmaker.map.MapData; import net.hollowcube.mapmaker.map.MapServer; import net.hollowcube.mapmaker.misc.BossBars; +import net.hollowcube.mapmaker.runtime.freeform.bundle.ScriptBundle; import net.hollowcube.mapmaker.runtime.freeform.lua.LuaGlobals; import net.hollowcube.mapmaker.runtime.freeform.lua.LuaTask; import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; @@ -16,8 +16,12 @@ import net.kyori.adventure.bossbar.BossBar; import net.minestom.server.entity.Player; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class FreeformMapWorld extends AbstractMapWorld { @@ -26,21 +30,20 @@ public class FreeformMapWorld extends AbstractMapWorld worldThreads = new ArrayList<>(); - public FreeformMapWorld(MapServer server, MapData map) { + public FreeformMapWorld(MapServer server, MapData map, ScriptBundle.Loader scriptLoader) { super(server, map, makeMapInstance(map, 'f'), FreeformState.class); - this.globalState = createGlobalState(); - - // 1, 39, 1 - // -62, 39, -62 + var scriptBundle = scriptLoader.load(map.id()); + this.scriptBundle = Objects.requireNonNull(scriptBundle, "Failed to load script bundle for map " + map.id()); -// eventNode() -// .addListener(PlayerTickEvent.class, this::handlePlayerTick) -// .addChild(EventUtil.READ_ONLY_NODE); + this.globalState = createGlobalState(); } public LuaState globalState() { @@ -53,117 +56,33 @@ public LuaState globalState() { public void loadWorld() { super.loadWorld(); - this.worldThread = LuaScriptState.create(this); - try { - - // We use the roblox pattern of having a global "script" which can be used to access the "owner" of the script. - // For now this is kinda dumb since its just the world/player, HOWEVER it gets around a very cursed optimization. - // Luau will eagerly evaluate all __index-es on globals when the script is loaded, meaning that if the player - // was a global, all occurrences of `player.Position` would be evaluated immediately instead of when they - // actually occur. Gross. - this.worldThread.state().newTable(); - LuaWorld.push(this.worldThread.state(), new LuaWorld(this)); - this.worldThread.state().setField(-2, "Parent"); // Set the world as the parent - this.worldThread.state().setReadOnly(-1, true); // Make it read-only - this.worldThread.state().setGlobal("script"); - - worldThread.state().load("test.luau", LUAU_COMPILER.compile(""" - local world = script.Parent - - function create_bit_board(width, height) - local bits_needed = width * height - local bytes_needed = math.ceil(bits_needed / 8) - return buffer.create(bytes_needed), width, height - end - - function get_cell(board, width, x, y) - local bit_index = y * width + x - return buffer.readbits(board, bit_index, 1) - end - - function set_cell(board, width, x, y, value) - local bit_index = y * width + x - buffer.writebits(board, bit_index, 1, value and 1 or 0) - end - - function count_neighbors_bitwise(board, width, height, x, y) - local count = 0 - - for dy = -1, 1 do - for dx = -1, 1 do - if dx ~= 0 or dy ~= 0 then -- Skip center cell - local nx, ny = x + dx, y + dy - if nx >= 0 and nx < width and ny >= 0 and ny < height then - count = count + buffer.readbits(board, ny * width + nx, 1) - end - end - end - end - return count - end - - local worldSpace = create_bit_board(64, 64) - local copySpace = create_bit_board(64, 64) - local stepTask = nil - - function init() - for x = 0, 63 do - for z = 0, 63 do - local active = world:GetBlock(vec(-x, 39, -z)) == Block.Stone - set_cell(worldSpace, 64, x, z, active) - end - end - end - - function step() - buffer.copy(copySpace, 0, worldSpace, 0, math.ceil(64 * 64 / 8)) - - for x = 0, 63 do - for z = 0, 63 do - local active = get_cell(copySpace, 64, x, z) == 1 - local neighbors = count_neighbors_bitwise(copySpace, 64, 64, x, z) - - local new_state = false - if active then - if neighbors == 2 or neighbors == 3 then - new_state = true -- Cell survives - else - new_state = false -- Cell dies - end - else - if neighbors == 3 then - new_state = true -- Cell becomes alive - end - end - - if new_state ~= active then - set_cell(worldSpace, 64, x, z, new_state) - world:SetBlock(vec(-x, 39, -z), new_state and Block.Stone or Block.Air) - end - end - end - end - - function toggleGame() - if stepTask then - task.cancel(stepTask) - stepTask = nil - else - stepTask = task.spawn(function() - init() - while true do - task.wait(2) - step() - end - end) - end - end - - toggleGame() - """)); - worldThread.state().pcall(0, 0); - } catch (LuauCompileException e) { - throw new RuntimeException(e); + for (var entrypoint : scriptBundle.entrypoints()) { + if (entrypoint.type() != ScriptBundle.Entrypoint.Type.WORLD) + continue; + + try { + var script = scriptBundle.loadScript(entrypoint.script()); + log.info("Loading world script {}", script.filename()); + + var thread = LuaScriptState.create(this); + this.worldThreads.add(thread); + + // We use the roblox pattern of having a global "script" which can be used to access the "owner" of the script. + // For now this is kinda dumb since its just the world/player, HOWEVER it gets around a very cursed optimization. + // Luau will eagerly evaluate all __index-es on globals when the script is loaded, meaning that if the player + // was a global, all occurrences of `player.Position` would be evaluated immediately instead of when they + // actually occur. Gross. + thread.state().newTable(); + LuaWorld.push(thread.state(), new LuaWorld(this)); + thread.state().setField(-2, "Parent"); // Set the world as the parent + thread.state().setReadOnly(-1, true); // Make it read-only + thread.state().setGlobal("script"); + + thread.state().load(script.filename(), LUAU_COMPILER.compile(script.content())); + thread.state().pcall(0, 0); + } catch (Exception e) { + throw new RuntimeException("Failed to load world script " + entrypoint.script(), e); + } } } @@ -171,7 +90,7 @@ function toggleGame() public void close() { super.close(); - this.worldThread.close(); + this.worldThreads.forEach(LuaScriptState::close); this.globalState.close(); } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/LocalFsLoader.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/LocalFsLoader.java new file mode 100644 index 000000000..3f899e45a --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/LocalFsLoader.java @@ -0,0 +1,85 @@ +package net.hollowcube.mapmaker.runtime.freeform.bundle; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import net.hollowcube.common.util.RuntimeGson; +import net.hollowcube.mapmaker.util.gson.EnumTypeAdapter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/// Script loader for the local file system. +/// +/// Useful for local development until we have a fancier way. +public class LocalFsLoader implements ScriptBundle.Loader { + public static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(ScriptBundle.Entrypoint.Type.class, new EnumTypeAdapter<>(ScriptBundle.Entrypoint.Type.class)) + .disableJdkUnsafe() + .create(); + + @RuntimeGson + public record MapJson(String id, List entrypoints) { + } + + // Mapping of map.json -> id to fs path. + private final Map availableBundles = new HashMap<>(); + + public LocalFsLoader(Path basePath) { + try (var list = Files.list(basePath)) { + for (var file : list.toList()) { + if (!Files.isDirectory(file)) continue; + + var mapJsonFile = file.resolve("map.json"); + if (!Files.exists(mapJsonFile)) continue; + + var mapJson = GSON.fromJson(Files.readString(mapJsonFile), JsonObject.class); + availableBundles.put(mapJson.get("id").getAsString(), file.toRealPath()); + } + } catch (IOException e) { + throw new RuntimeException("failed to discover scripts", e); + } + } + + @Override + public @Nullable ScriptBundle load(String id) { + var path = availableBundles.get(id); + if (path == null) return null; + + try { + var mapJsonFile = path.resolve("map.json"); + var mapJson = GSON.fromJson(Files.readString(mapJsonFile), MapJson.class); + + return new ScriptBundle() { + @Override + public String id() { + return mapJson.id(); + } + + @Override + public List entrypoints() { + return mapJson.entrypoints(); + } + + @Override + public Script loadScript(String name) { + var scriptFile = path.resolve(name); + if (!Files.exists(scriptFile)) + throw new RuntimeException("script " + name + " not found in " + path); + try { + return new Script(scriptFile.getFileName().toString(), Files.readString(scriptFile)); + } catch (IOException e) { + throw new RuntimeException("failed to load script " + name + ":", e); + } + } + }; + } catch (IOException e) { + throw new RuntimeException("failed to load script " + id + ":", e); + } + } +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ScriptBundle.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ScriptBundle.java new file mode 100644 index 000000000..dae3950eb --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ScriptBundle.java @@ -0,0 +1,28 @@ +package net.hollowcube.mapmaker.runtime.freeform.bundle; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public interface ScriptBundle { + + interface Loader { + @Nullable ScriptBundle load(String id); + } + + record Entrypoint(Type type, String script) { + public enum Type { + WORLD + } + } + + record Script(String filename, String content) { + } + + String id(); + + List entrypoints(); + + Script loadScript(String name); + +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/package-info.java new file mode 100644 index 000000000..9042a5fad --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform.bundle; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/scripts/button-clicker/map.json b/scripts/button-clicker/map.json new file mode 100644 index 000000000..c23d4f246 --- /dev/null +++ b/scripts/button-clicker/map.json @@ -0,0 +1,9 @@ +{ + "id": "3080cf33-8ff9-4d3e-a469-a457a896ab3d", + "entrypoints": [ + { + "type": "world", + "script": "world.luau" + } + ] +} \ No newline at end of file diff --git a/scripts/button-clicker/world.luau b/scripts/button-clicker/world.luau new file mode 100644 index 000000000..bf6a97072 --- /dev/null +++ b/scripts/button-clicker/world.luau @@ -0,0 +1,3 @@ +function abc() + print("hello, world!") +end diff --git a/scripts/game-of-life/map.json b/scripts/game-of-life/map.json new file mode 100644 index 000000000..03774c049 --- /dev/null +++ b/scripts/game-of-life/map.json @@ -0,0 +1,9 @@ +{ + "id": "2d08b1c9-2193-4831-9318-75e905de8489", + "entrypoints": [ + { + "type": "world", + "script": "world.luau" + } + ] +} \ No newline at end of file diff --git a/scripts/game-of-life/world.luau b/scripts/game-of-life/world.luau new file mode 100644 index 000000000..efa457ba2 --- /dev/null +++ b/scripts/game-of-life/world.luau @@ -0,0 +1,92 @@ +local world = script.Parent + +function create_bit_board(width, height) + local bits_needed = width * height + local bytes_needed = math.ceil(bits_needed / 8) + return buffer.create(bytes_needed), width, height +end + +function get_cell(board, width, x, y) + local bit_index = y * width + x + return buffer.readbits(board, bit_index, 1) +end + +function set_cell(board, width, x, y, value) + local bit_index = y * width + x + buffer.writebits(board, bit_index, 1, value and 1 or 0) +end + +function count_neighbors_bitwise(board, width, height, x, y) + local count = 0 + + for dy = -1, 1 do + for dx = -1, 1 do + if dx ~= 0 or dy ~= 0 then -- Skip center cell + local nx, ny = x + dx, y + dy + if nx >= 0 and nx < width and ny >= 0 and ny < height then + count = count + buffer.readbits(board, ny * width + nx, 1) + end + end + end + end + return count +end + +local worldSpace = create_bit_board(64, 64) +local copySpace = create_bit_board(64, 64) +local stepTask = nil + +function init() + for x = 0, 63 do + for z = 0, 63 do + local active = world:GetBlock(vec(-x, 39, -z)) == Block.Stone + set_cell(worldSpace, 64, x, z, active) + end + end +end + +function step() + buffer.copy(copySpace, 0, worldSpace, 0, math.ceil(64 * 64 / 8)) + + for x = 0, 63 do + for z = 0, 63 do + local active = get_cell(copySpace, 64, x, z) == 1 + local neighbors = count_neighbors_bitwise(copySpace, 64, 64, x, z) + + local new_state = false + if active then + if neighbors == 2 or neighbors == 3 then + new_state = true -- Cell survives + else + new_state = false -- Cell dies + end + else + if neighbors == 3 then + new_state = true -- Cell becomes alive + end + end + + if new_state ~= active then + set_cell(worldSpace, 64, x, z, new_state) + world:SetBlock(vec(-x, 39, -z), new_state and Block.Stone or Block.Air) + end + end + end +end + +function toggleGame() + if stepTask then + task.cancel(stepTask) + stepTask = nil + else + stepTask = task.spawn(function() + init() + while true do + task.wait(2) + step() + end + end) + end +end + +toggleGame() From 0318721e98c049f48d4b065dfd57015624d0244a Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 20 Sep 2025 08:44:29 -0400 Subject: [PATCH 13/22] feat: button pressing --- idea/Luau-Formatter.ipynb | 41 ++----- .../runtime/freeform/FreeformMapWorld.java | 35 ++++-- .../runtime/freeform/FreeformState.java | 93 ++++++++++++++- .../runtime/freeform/ScriptState.java | 31 +++++ .../runtime/freeform/bundle/ScriptBundle.java | 2 +- .../runtime/freeform/lua/LuaEventSource.java | 79 +++++++++++++ .../freeform/lua/player/LuaPlayer.java | 108 ++++++++++++++++++ .../freeform/lua/player/package-info.java | 4 + .../runtime/freeform/script/LuaHelpers.java | 52 ++++++++- scripts/button-clicker/.luaurc | 3 + scripts/button-clicker/map.json | 4 + scripts/button-clicker/player.luau | 15 +++ scripts/button-clicker/world.luau | 4 +- 13 files changed, 424 insertions(+), 47 deletions(-) create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/ScriptState.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaEventSource.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/package-info.java create mode 100644 scripts/button-clicker/.luaurc create mode 100644 scripts/button-clicker/player.luau diff --git a/idea/Luau-Formatter.ipynb b/idea/Luau-Formatter.ipynb index a9907a0dd..4c7b2c75b 100644 --- a/idea/Luau-Formatter.ipynb +++ b/idea/Luau-Formatter.ipynb @@ -5,32 +5,19 @@ "metadata": { "collapsed": true, "ExecuteTime": { - "end_time": "2025-09-14T16:41:46.318731Z", - "start_time": "2025-09-14T16:41:45.419424Z" + "end_time": "2025-09-17T01:48:46.020867Z", + "start_time": "2025-09-17T01:48:45.960678Z" } }, "source": "%use intellij-platform", - "outputs": [ - { - "data": { - "text/plain": [ - "IntelliJ Platform integration is loaded" - ] - }, - "metadata": {}, - "output_type": "display_data", - "jetTransient": { - "display_id": null - } - } - ], - "execution_count": 1 + "outputs": [], + "execution_count": 3 }, { "metadata": { "ExecuteTime": { - "end_time": "2025-09-14T16:58:11.272099Z", - "start_time": "2025-09-14T16:58:11.025190Z" + "end_time": "2025-09-17T01:48:46.144155Z", + "start_time": "2025-09-17T01:48:46.021185Z" } }, "cell_type": "code", @@ -98,21 +85,7 @@ "registerExtension(FormattingService.EP_NAME, StyluaFormatter())" ], "outputs": [], - "execution_count": 7 - }, - { - "metadata": {}, - "cell_type": "code", - "outputs": [], - "execution_count": null, - "source": "" - }, - { - "metadata": {}, - "cell_type": "code", - "outputs": [], - "execution_count": null, - "source": "" + "execution_count": 4 } ], "metadata": { diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java index 709ce10fd..e340bdb3d 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java @@ -1,15 +1,17 @@ package net.hollowcube.mapmaker.runtime.freeform; +import net.hollowcube.common.util.ProtocolVersions; import net.hollowcube.luau.LuaState; import net.hollowcube.luau.compiler.LuauCompiler; -import net.hollowcube.mapmaker.map.AbstractMapWorld; -import net.hollowcube.mapmaker.map.MapData; -import net.hollowcube.mapmaker.map.MapServer; +import net.hollowcube.mapmaker.map.*; import net.hollowcube.mapmaker.misc.BossBars; +import net.hollowcube.mapmaker.player.PlayerData; import net.hollowcube.mapmaker.runtime.freeform.bundle.ScriptBundle; +import net.hollowcube.mapmaker.runtime.freeform.lua.LuaEventSource; import net.hollowcube.mapmaker.runtime.freeform.lua.LuaGlobals; import net.hollowcube.mapmaker.runtime.freeform.lua.LuaTask; import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; +import net.hollowcube.mapmaker.runtime.freeform.lua.player.LuaPlayer; import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaBlock; import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaWorld; import net.hollowcube.mapmaker.runtime.freeform.script.LuaScriptState; @@ -22,10 +24,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.UUID; public class FreeformMapWorld extends AbstractMapWorld { - private static final LuauCompiler LUAU_COMPILER = LuauCompiler.builder() + public static final LuauCompiler LUAU_COMPILER = LuauCompiler.builder() .userdataTypes() // todo .vectorType("vector") .vectorCtor("vec") @@ -46,6 +49,10 @@ public FreeformMapWorld(MapServer server, MapData map, ScriptBundle.Loader scrip this.globalState = createGlobalState(); } + public ScriptBundle scriptBundle() { + return this.scriptBundle; + } + public LuaState globalState() { return this.globalState; } @@ -100,12 +107,26 @@ public void close() { @Override protected FreeformState configurePlayer(Player player) { + final var playerData = PlayerData.fromPlayer(player); + SaveState saveState; + try { + saveState = server().mapService().getLatestSaveState(map().id(), + playerData.id(), SaveStateType.PLAYING, ScriptState.SERIALIZER); + } catch (MapService.NotFoundError ignored) { + // No save state yet, create one locally. + // We do an upsert to save, so it will be created in the map service at that point. + saveState = new SaveState(UUID.randomUUID().toString(), + map().id(), playerData.id(), SaveStateType.PLAYING, + ScriptState.SERIALIZER, new ScriptState(null)); + saveState.setProtocolVersion(ProtocolVersions.getProtocolVersion(player)); + } player.setRespawnPoint(map().settings().getSpawnPoint()); - return new FreeformState.Playing(); + return new FreeformState.Playing(saveState, new ArrayList<>()); } + @Override protected @Nullable List createBossBars() { return BossBars.createPlayingBossBar(server().playerService(), map()); @@ -126,12 +147,12 @@ private static LuaState createGlobalState() { // LuaColor.init(global); // LuaText.init(global); -// LuaEventSource.init(global); + LuaEventSource.init(global); LuaBlock.init(global); LuaWorld.init(global); // LuaParticle.init(global); // LuaEntity.init(global); -// LuaPlayer.init(global); + LuaPlayer.init(global); global.sandbox(); return global; diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java index 89394ad26..1cccf0bc6 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java @@ -1,16 +1,107 @@ package net.hollowcube.mapmaker.runtime.freeform; +import com.google.gson.JsonObject; +import net.hollowcube.common.util.FutureUtil; +import net.hollowcube.common.util.ProtocolVersions; +import net.hollowcube.mapmaker.ExceptionReporter; import net.hollowcube.mapmaker.map.PlayerState; +import net.hollowcube.mapmaker.map.SaveState; +import net.hollowcube.mapmaker.player.PlayerData; +import net.hollowcube.mapmaker.runtime.freeform.bundle.ScriptBundle; +import net.hollowcube.mapmaker.runtime.freeform.lua.player.LuaPlayer; +import net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers; +import net.hollowcube.mapmaker.runtime.freeform.script.LuaScriptState; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.Transcoder; import net.minestom.server.entity.Player; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; public sealed interface FreeformState extends PlayerState { + Logger log = LoggerFactory.getLogger(FreeformState.class); - record Playing() implements FreeformState { + record Playing(SaveState saveState, List activeScripts) implements FreeformState { @Override public void configurePlayer(FreeformMapWorld world, Player player, @Nullable FreeformState lastState) { FreeformState.super.configurePlayer(world, player, lastState); + for (var entrypoint : world.scriptBundle().entrypoints()) { + if (entrypoint.type() != ScriptBundle.Entrypoint.Type.PLAYER) + continue; + + try { + var script = world.scriptBundle().loadScript(entrypoint.script()); + log.info("Loading world script {}", script.filename()); + + var thread = LuaScriptState.create(world); + activeScripts.add(thread); + + JsonObject saveData = new JsonObject(); + var rawSaveData = saveState.state(ScriptState.class).saveData(); + if (rawSaveData != null) { + var parsed = rawSaveData.convertTo(Transcoder.JSON).orElse(null); + if (parsed != null) saveData = parsed.getAsJsonObject(); + } + + // We use the roblox pattern of having a global "script" which can be used to access the "owner" of the script. + // For now this is kinda dumb since its just the world/player, HOWEVER it gets around a very cursed optimization. + // Luau will eagerly evaluate all __index-es on globals when the script is loaded, meaning that if the player + // was a global, all occurrences of `player.Position` would be evaluated immediately instead of when they + // actually occur. Gross. + // todo whole thing is duped + thread.state().newTable(); + LuaPlayer.push(thread.state(), new LuaPlayer(thread.state(), player, saveData)); + thread.state().setField(-2, "Parent"); // Set the player as the parent + thread.state().setReadOnly(-1, true); // Make it read-only + thread.state().setGlobal("script"); + + thread.state().load(script.filename(), FreeformMapWorld.LUAU_COMPILER.compile(script.content())); + thread.state().pcall(0, 0); + } catch (Exception e) { + throw new RuntimeException("Failed to load world script " + entrypoint.script(), e); + } + } + } + + @Override + public void resetPlayer(FreeformMapWorld world, Player player, @Nullable FreeformState nextState) { + FreeformState.super.resetPlayer(world, player, nextState); + + if (!activeScripts.isEmpty()) { + // todo: dont access the savedata table in such a cursed way :skull: + var state = activeScripts.getFirst().state(); + state.getGlobal("script"); + state.getField(-1, "Parent"); + state.getField(-1, "SaveData"); + var json = LuaHelpers.readJsonElement(state, -1); + saveState.state(ScriptState.class).saveData( + Codec.RawValue.of(Transcoder.JSON, json) + ); + state.pop(4); + } + + FutureUtil.submitVirtual(() -> writeSaveState(world, player, saveState)); + + activeScripts.forEach(LuaScriptState::close); + activeScripts.clear(); + } + + private static void writeSaveState(FreeformMapWorld world, Player player, SaveState saveState) { + var update = saveState.createUpsertRequest(); + update.setProtocolVersion(ProtocolVersions.getProtocolVersion(player)); + + // Write the save state to the database + try { + var playerData = PlayerData.fromPlayer(player); + world.server().mapService().updateSaveState( + world.map().id(), playerData.id(), saveState.id(), update); + } catch (Exception e) { + var wrappedException = new RuntimeException("failed to save player save state", e); + ExceptionReporter.reportException(wrappedException, player); + } } } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/ScriptState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/ScriptState.java new file mode 100644 index 000000000..48e925abd --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/ScriptState.java @@ -0,0 +1,31 @@ +package net.hollowcube.mapmaker.runtime.freeform; + +import net.hollowcube.mapmaker.map.SaveStateType; +import net.hollowcube.mapmaker.map.util.datafix.HCDataTypes; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; +import org.jetbrains.annotations.Nullable; + +public class ScriptState { + + public static Codec CODEC = StructCodec.struct( + "saveData", Codec.RAW_VALUE, ScriptState::saveData, + ScriptState::new); + // Todo should not be a play state of course + public static final SaveStateType.Serializer SERIALIZER = SaveStateType.serializer("playState", CODEC, HCDataTypes.PLAY_STATE); + + private @Nullable Codec.RawValue saveData; + + public ScriptState(@Nullable Codec.RawValue saveData) { + this.saveData = saveData; + } + + public @Nullable Codec.RawValue saveData() { + return this.saveData; + } + + public void saveData(@Nullable Codec.RawValue saveData) { + this.saveData = saveData; + } + +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ScriptBundle.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ScriptBundle.java index dae3950eb..ed3e6bd9a 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ScriptBundle.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ScriptBundle.java @@ -12,7 +12,7 @@ interface Loader { record Entrypoint(Type type, String script) { public enum Type { - WORLD + WORLD, PLAYER } } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaEventSource.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaEventSource.java new file mode 100644 index 000000000..086d75de2 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaEventSource.java @@ -0,0 +1,79 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.LuaType; +import net.minestom.server.event.Event; +import net.minestom.server.event.EventNode; + +import java.util.function.ToIntBiFunction; + +import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchKey; +import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchMethod; + +public class LuaEventSource { + private static final String NAME = "EventSource"; + + public static void init(LuaState state) { + state.newMetaTable(NAME); + state.pushCFunction(LuaEventSource::luaIndex, "__index"); + state.setField(-2, "__index"); + state.pushCFunction(LuaEventSource::luaNameCall, "__namecall"); + state.setField(-2, "__namecall"); + state.pop(1); + } + + public static void push(LuaState state, LuaEventSource eventSource) { + state.newUserData(eventSource); + state.getMetaTable(NAME); + state.setMetaTable(-2); + } + + public static LuaEventSource checkArg(LuaState state, int index) { + return (LuaEventSource) state.checkUserDataArg(index, NAME); + } + + private final EventNode eventNode; + private final Class eventType; + private final ToIntBiFunction pushArgs; + + public LuaEventSource(EventNode eventNode, Class eventType, ToIntBiFunction pushArgs) { + this.eventNode = eventNode; + this.eventType = eventType; + this.pushArgs = pushArgs; + } + + // Methods + + private int listen(LuaState state) { + state.checkType(1, LuaType.FUNCTION); + int callbackRef = state.ref(1); + + eventNode.addListener(eventType, event -> { + state.getref(callbackRef); + int argCount = pushArgs.applyAsInt(state, event); + state.pcall(argCount, 0); + }); + + return 0; // todo return handle to cancel the listener + } + + // Metatable + + private static int luaIndex(LuaState state) { + final LuaEventSource eventSource = checkArg(state, 1); + final String key = state.checkStringArg(2); + return switch (key) { + default -> noSuchKey(state, NAME, key); + }; + } + + private static int luaNameCall(LuaState state) { + final LuaEventSource eventSource = checkArg(state, 1); + state.remove(1); // Remove the player userdata from the stack + final String methodName = state.nameCallAtom(); + return switch (methodName) { + case "Listen" -> eventSource.listen(state); + default -> noSuchMethod(state, NAME, methodName); + }; + } +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java new file mode 100644 index 000000000..9e7f453e8 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java @@ -0,0 +1,108 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.player; + +import com.google.gson.JsonObject; +import net.hollowcube.luau.LuaState; +import net.hollowcube.mapmaker.runtime.freeform.lua.LuaEventSource; +import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; +import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaBlock; +import net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockInteractEvent; + +import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchKey; +import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchMethod; + +public class LuaPlayer { + private static final String NAME = "Player"; + + public static void init(LuaState state) { + state.newMetaTable(NAME); + state.pushCFunction(LuaPlayer::luaIndex, "__index"); + state.setField(-2, "__index"); + state.pushCFunction(LuaPlayer::luaNewIndex, "__newindex"); + state.setField(-2, "__newindex"); + state.pushCFunction(LuaPlayer::luaNameCall, "__namecall"); + state.setField(-2, "__namecall"); + state.pop(1); + } + + public static void push(LuaState state, LuaPlayer entity) { + state.newUserData(entity); + state.getMetaTable(NAME); + state.setMetaTable(-2); + } + + public static LuaPlayer checkArg(LuaState state, int index) { + return (LuaPlayer) state.checkUserDataArg(index, NAME); + } + + private final Player player; + private final int saveDataRef; + + public LuaPlayer(LuaState state, Player player, JsonObject saveData) { + this.player = player; + + LuaHelpers.pushJsonElement(state, saveData); + this.saveDataRef = state.ref(-1); // todo dont leak this :) + state.pop(1); + } + + // Properties + + private int getUuid(LuaState state) { + state.pushString(player.getUuid().toString()); + return 1; + } + + private int getSaveData(LuaState state) { + state.getref(saveDataRef); + return 1; + } + + private int getOnBlockInteract(LuaState state) { + LuaEventSource.push(state, new LuaEventSource<>( + player.eventNode(), + PlayerBlockInteractEvent.class, + (eventState, event) -> { + LuaVectorTypeImpl.push(eventState, event.getBlockPosition()); + LuaBlock.push(eventState, event.getBlock()); + return 2; + } + )); + return 1; + } + + // Methods + + // Metamethods + + private static int luaIndex(LuaState state) { + final LuaPlayer self = checkArg(state, 1); + final String key = state.checkStringArg(2); + return switch (key) { + case "Uuid" -> self.getUuid(state); + case "SaveData" -> self.getSaveData(state); + case "OnBlockInteract" -> self.getOnBlockInteract(state); + default -> noSuchKey(state, NAME, key); + }; + } + + private static int luaNewIndex(LuaState state) { + final LuaPlayer self = checkArg(state, 1); + final String key = state.checkStringArg(2); + state.remove(1); // Remove the userdata from the stack + state.remove(1); // Remove the key from the stack + return switch (key) { + default -> noSuchKey(state, NAME, key); + }; + } + + private static int luaNameCall(LuaState state) { + final LuaPlayer self = checkArg(state, 1); + state.remove(1); // Remove the world userdata from the stack (so implementations can pretend they have no self) + final String methodName = state.nameCallAtom(); + return switch (methodName) { + default -> noSuchMethod(state, NAME, methodName); + }; + } +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/package-info.java new file mode 100644 index 000000000..a48761a4d --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform.lua.player; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java index c7b1409b3..c1b240a36 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java @@ -1,5 +1,6 @@ package net.hollowcube.mapmaker.runtime.freeform.script; +import com.google.gson.*; import net.hollowcube.luau.LuaState; import net.hollowcube.luau.LuaType; import net.kyori.adventure.key.InvalidKeyException; @@ -25,7 +26,7 @@ public static int noSuchMethod(LuaState state, String typeName, String methodNam /// The state should be left _exactly_ as it was before the call (value at -1). public static void tableForEach(LuaState state, int tableIndex, Consumer func) { state.pushNil(); - while (state.next(2)) { + while (state.next(tableIndex - 1)) { // Key is at index -2, value is at index -1 String key = state.toString(-2); func.accept(key); @@ -79,5 +80,54 @@ public static void pushFloat4(LuaState state, float[] floats) { } } + public static void pushJsonElement(LuaState state, JsonElement element) { + switch (element) { + case JsonObject object -> { + state.newTable(); + for (var entry : object.entrySet()) { + pushJsonElement(state, entry.getValue()); + state.setField(-2, entry.getKey()); + } + } + case JsonArray array -> { + state.newTable(); + for (int i = 0; i < array.size(); i++) { + pushJsonElement(state, array.get(i)); + state.rawSetI(-2, i + 1); + } + } + case JsonNull _ -> state.pushNil(); + case JsonPrimitive primitive -> { + if (primitive.isBoolean()) { + state.pushBoolean(primitive.getAsBoolean()); + } else if (primitive.isNumber()) { + state.pushNumber(primitive.getAsDouble()); + } else { + state.pushString(primitive.getAsString()); + } + } + default -> throw new IllegalArgumentException("Unknown JsonElement type: " + element.getClass()); + } + } + + public static JsonElement readJsonElement(LuaState state, int index) { + return switch (state.type(index)) { + case NIL -> JsonNull.INSTANCE; + case BOOLEAN -> new JsonPrimitive(state.toBoolean(-1)); + case NUMBER -> new JsonPrimitive(state.toNumber(-1)); + case STRING -> new JsonPrimitive(state.toString(-1)); + case TABLE -> { + // TODO: support arrays. + var obj = new JsonObject(); + tableForEach(state, index, key -> obj.add(key, readJsonElement(state, -1))); + yield obj; + } + // todo support vector type, some userdata types, and buffer type (probably) + case NONE, DEADKEY, UPVAL, PROTO, FUNCTION, LIGHTUSERDATA, USERDATA, VECTOR, THREAD, BUFFER -> { + throw new IllegalArgumentException("Cannot read JSON from type " + state.typeName(index)); + } + }; + } + } diff --git a/scripts/button-clicker/.luaurc b/scripts/button-clicker/.luaurc new file mode 100644 index 000000000..89bffcfa1 --- /dev/null +++ b/scripts/button-clicker/.luaurc @@ -0,0 +1,3 @@ +{ + "languageMode": "strict" +} \ No newline at end of file diff --git a/scripts/button-clicker/map.json b/scripts/button-clicker/map.json index c23d4f246..e4ee57e13 100644 --- a/scripts/button-clicker/map.json +++ b/scripts/button-clicker/map.json @@ -4,6 +4,10 @@ { "type": "world", "script": "world.luau" + }, + { + "type": "player", + "script": "player.luau" } ] } \ No newline at end of file diff --git a/scripts/button-clicker/player.luau b/scripts/button-clicker/player.luau new file mode 100644 index 000000000..5289ee94c --- /dev/null +++ b/scripts/button-clicker/player.luau @@ -0,0 +1,15 @@ +local player = script.Parent + +local BUTTON_POSITION = vec(0, 41, -5) + +function onButtonPress(blockPosition, block) + if blockPosition ~= BUTTON_POSITION then + return + end + + local buttonCount = player.SaveData.buttonCount or 0 + player.SaveData.buttonCount = buttonCount + 1 + print("Pressed", player.SaveData.buttonCount, "times") +end + +player.OnBlockInteract:Listen(onButtonPress) diff --git a/scripts/button-clicker/world.luau b/scripts/button-clicker/world.luau index bf6a97072..e94acca77 100644 --- a/scripts/button-clicker/world.luau +++ b/scripts/button-clicker/world.luau @@ -1,3 +1 @@ -function abc() - print("hello, world!") -end +print("hello, world!") From b4ef335521a5344ef718dae585fcf1ce1a4a4b64 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 20 Sep 2025 09:27:42 -0400 Subject: [PATCH 14/22] chore: graalgnn --- bin/map-isolate/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/map-isolate/build.gradle.kts b/bin/map-isolate/build.gradle.kts index bccb9dc3b..4f11baf8d 100644 --- a/bin/map-isolate/build.gradle.kts +++ b/bin/map-isolate/build.gradle.kts @@ -55,6 +55,7 @@ graalvmNative { listOf( "--enable-native-access=ALL-UNNAMED", "--enable-monitoring=jfr", "-H:+UnlockExperimentalVMOptions", "-H:+ForeignAPISupport", + "-H:+MLProfileInferenceUseGNNModel", "--features=net.hollowcube.nativeimage.HCNativeImageFeature", "--static-nolibc", "--no-fallback", "--emit build-report", From 0f6673209afdee19c424198c978924e96cb0195d Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 20 Sep 2025 12:26:40 -0400 Subject: [PATCH 15/22] chore: rebase & bump to java 25 --- .../net.hollowcube/luau/reflect-config.json | 36 ------------------- .../net.hollowcube/luau/resource-config.json | 10 ------ bin/map-isolate/build.gradle.kts | 6 +++- gradle/libs.versions.toml | 4 +-- 4 files changed, 7 insertions(+), 49 deletions(-) delete mode 100644 bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/reflect-config.json delete mode 100644 bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/resource-config.json diff --git a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/reflect-config.json b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/reflect-config.json deleted file mode 100644 index e4fb41f24..000000000 --- a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/reflect-config.json +++ /dev/null @@ -1,36 +0,0 @@ -[ - { - "name": "net.hollowcube.luau.internal.vm.lua_CFunction$Function", - "methods": [ - { - "name": "apply", - "parameterTypes": [ - "java.lang.foreign.MemorySegment" - ] - } - ] - }, - { - "name": "net.hollowcube.luau.internal.vm.lua_Callbacks$userthread$Function", - "methods": [ - { - "name": "apply", - "parameterTypes": [ - "java.lang.foreign.MemorySegment", - "java.lang.foreign.MemorySegment" - ] - } - ] - }, - { - "name": "net.hollowcube.luau.internal.vm.lua_newuserdatadtor$dtor$Function", - "methods": [ - { - "name": "apply", - "parameterTypes": [ - "java.lang.foreign.MemorySegment" - ] - } - ] - } -] \ No newline at end of file diff --git a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/resource-config.json b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/resource-config.json deleted file mode 100644 index d0aa2f3c3..000000000 --- a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/luau/resource-config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bundles": [], - "resources": { - "includes": [ - { - "pattern": "net/hollowcube/luau/(windows|linux|macos)/(x64|arm64)/lib(compiler|globalref|vm).(so|dll|dylib)" - } - ] - } -} \ No newline at end of file diff --git a/bin/map-isolate/build.gradle.kts b/bin/map-isolate/build.gradle.kts index 4f11baf8d..2ce3d138d 100644 --- a/bin/map-isolate/build.gradle.kts +++ b/bin/map-isolate/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("mapmaker.java-binary") id("mapmaker.packer-data") - id("org.graalvm.buildtools.native") version "0.10.6" + id("org.graalvm.buildtools.native") version "0.11.0" } dependencies { @@ -45,6 +45,7 @@ tasks.nativeCompile { application { mainClass = "net.hollowcube.mapmaker.isolate.IsolateMain" + applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") } graalvmNative { @@ -56,6 +57,9 @@ graalvmNative { "--enable-native-access=ALL-UNNAMED", "--enable-monitoring=jfr", "-H:+UnlockExperimentalVMOptions", "-H:+ForeignAPISupport", "-H:+MLProfileInferenceUseGNNModel", + + "-H:+UseCompressedReferences", "-XX:+CollectYoungGenerationSeparately", + "--features=net.hollowcube.nativeimage.HCNativeImageFeature", "--static-nolibc", "--no-fallback", "--emit build-report", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e85231e8..ebbccfe6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,8 +28,8 @@ blossom = "2.1.0" velocity = "3.4.0-SNAPSHOT" classgraph = "4.8.179" included = "-INCLUDED" -luau = "0.4.0" -luau-natives = "0.4.0" +luau = "0.5.1" +luau-natives = "0.5.1" [libraries] annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } From 532098c7de2a59d1212513d02f27989c3bd174d0 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 20 Sep 2025 12:30:49 -0400 Subject: [PATCH 16/22] fix: wrong classifier --- bin/map-isolate/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/map-isolate/build.gradle.kts b/bin/map-isolate/build.gradle.kts index 2ce3d138d..d6f860ac1 100644 --- a/bin/map-isolate/build.gradle.kts +++ b/bin/map-isolate/build.gradle.kts @@ -58,7 +58,7 @@ graalvmNative { "-H:+UnlockExperimentalVMOptions", "-H:+ForeignAPISupport", "-H:+MLProfileInferenceUseGNNModel", - "-H:+UseCompressedReferences", "-XX:+CollectYoungGenerationSeparately", + "-H:+UseCompressedReferences", "-H:+CollectYoungGenerationSeparately", "--features=net.hollowcube.nativeimage.HCNativeImageFeature", "--static-nolibc", "--no-fallback", From 9f506a739a5e00127f499e2cb61a0c333d38652f Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 20 Sep 2025 12:37:02 -0400 Subject: [PATCH 17/22] fix: nvm i guess its fake --- bin/map-isolate/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/map-isolate/build.gradle.kts b/bin/map-isolate/build.gradle.kts index d6f860ac1..074801675 100644 --- a/bin/map-isolate/build.gradle.kts +++ b/bin/map-isolate/build.gradle.kts @@ -58,7 +58,7 @@ graalvmNative { "-H:+UnlockExperimentalVMOptions", "-H:+ForeignAPISupport", "-H:+MLProfileInferenceUseGNNModel", - "-H:+UseCompressedReferences", "-H:+CollectYoungGenerationSeparately", + "-H:+UseCompressedReferences", "--features=net.hollowcube.nativeimage.HCNativeImageFeature", "--static-nolibc", "--no-fallback", From 3e5184a8fd071bf684f79ea2a0e585f0f813cf71 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 20 Sep 2025 17:07:34 -0400 Subject: [PATCH 18/22] feat: bundle scripts in resources and add loader for them --- .../mapmaker/resource-config.json | 3 + .../mapmaker/isolate/MapIsolateServer.java | 12 ++-- modules/map-runtime/build.gradle.kts | 41 +++++++++++- .../freeform/bundle/ResourcesLoader.java | 63 +++++++++++++++++++ 4 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ResourcesLoader.java diff --git a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/mapmaker/resource-config.json b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/mapmaker/resource-config.json index 8e599bfd4..65d5d22a7 100644 --- a/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/mapmaker/resource-config.json +++ b/bin/config/src/main/resources/META-INF/native-image/net.hollowcube/mapmaker/resource-config.json @@ -6,6 +6,9 @@ }, { "pattern": "\\Qdefault_config.json\\E" + }, + { + "pattern": "net\\.hollowcube\\.scripting/(.+)\\.zip" } ] } diff --git a/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java b/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java index b61c0b287..8d1ad931a 100644 --- a/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java +++ b/bin/map-isolate/src/main/java/net/hollowcube/mapmaker/isolate/MapIsolateServer.java @@ -10,6 +10,7 @@ import net.hollowcube.mapmaker.misc.ResourcePackManager; import net.hollowcube.mapmaker.runtime.freeform.FreeformMapWorld; import net.hollowcube.mapmaker.runtime.freeform.bundle.LocalFsLoader; +import net.hollowcube.mapmaker.runtime.freeform.bundle.ResourcesLoader; import net.hollowcube.mapmaker.runtime.freeform.bundle.ScriptBundle; import net.hollowcube.mapmaker.runtime.parkour.ParkourMapWorld; import net.hollowcube.mapmaker.session.Presence; @@ -25,8 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; import java.util.UUID; import static net.hollowcube.mapmaker.map.MapPlayer.simpleMapPlayer; @@ -49,15 +50,18 @@ public MapIsolateServer(ConfigLoaderV3 config) { if (IsolateMain.args.length < 1) throw new IllegalArgumentException("Map ID must be provided as the last argument"); this.mapId = UUID.fromString(IsolateMain.args[IsolateMain.args.length - 1]).toString(); - System.out.println("Map ID: " + this.mapId); - System.out.println("Args: " + Arrays.toString(IsolateMain.args)); + logger.info("Loading map {}", this.mapId); MinecraftServer.getGlobalEventHandler().addChild(EventNode.all("map-init") .addListener(AsyncPlayerConfigurationEvent.class, this::handleConfigPhase) .addListener(PlayerSpawnEvent.class, this::handleSpawn) .addListener(PlayerDisconnectEvent.class, this::handleDisconnect)); - this.scriptLoader = new LocalFsLoader(Path.of("../../scripts")); + var scriptsDirectory = Path.of("../../scripts"); + this.scriptLoader = Files.exists(scriptsDirectory) + ? new LocalFsLoader(scriptsDirectory) + : new ResourcesLoader(); + logger.info("Using script loader: {}", this.scriptLoader); } @Override diff --git a/modules/map-runtime/build.gradle.kts b/modules/map-runtime/build.gradle.kts index 3a88019e3..b5dc28604 100644 --- a/modules/map-runtime/build.gradle.kts +++ b/modules/map-runtime/build.gradle.kts @@ -1,3 +1,6 @@ +import com.google.gson.Gson +import com.google.gson.JsonObject + plugins { id("mapmaker.java-library") } @@ -14,9 +17,45 @@ dependencies { implementation(libs.bundles.adventure) implementation(libs.bundles.luau) - implementation("dev.hollowcube:luau:dev") testImplementation(project(":modules:compat")) testImplementation(project(":modules:test")) testImplementation(libs.bundles.otel) } + +// Collect all local scripts and add them to the jar +val gson = Gson() +val scriptsDir = rootProject.projectDir.resolve("scripts") +val outPath = layout.buildDirectory.dir("resources/main/net.hollowcube.scripting") + +val mapData: List> = scriptsDir.listFiles() + ?.filter { it.isDirectory } + ?.mapNotNull { dir -> + val mapJsonFile = dir.resolve("map.json") + if (!mapJsonFile.exists()) return@mapNotNull null + val jsonContent = mapJsonFile.readText() + val jsonObject = gson.fromJson(jsonContent, JsonObject::class.java) + val mapId = jsonObject.get("id").asString + return@mapNotNull mapId to dir + } ?: emptyList() + +val zipTasks = mapData.map { (mapId, dir) -> + tasks.register("zip${mapId.toPascalCase()}") { + archiveFileName.set("${mapId}.zip") + destinationDirectory.set(outPath) + from(dir) + + group = "scripting" + } +} + +tasks.named("processResources") { + dependsOn(zipTasks) +} + +fun String.toPascalCase(): String { + return this.split("-", "_") + .joinToString("") { word -> + word.replaceFirstChar { it.uppercase() } + } +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ResourcesLoader.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ResourcesLoader.java new file mode 100644 index 000000000..0afb64790 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/bundle/ResourcesLoader.java @@ -0,0 +1,63 @@ +package net.hollowcube.mapmaker.runtime.freeform.bundle; + +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ResourcesLoader implements ScriptBundle.Loader { + private static final Set KNOWN_SCRIPTED_IDS = Set.of( + "2d08b1c9-2193-4831-9318-75e905de8489", + "3080cf33-8ff9-4d3e-a469-a457a896ab3d" + ); + + @Override + public @Nullable ScriptBundle load(String id) { + if (!KNOWN_SCRIPTED_IDS.contains(id)) + return null; + + var vfs = new HashMap(); + var uri = "/net.hollowcube.scripting/%s.zip".formatted(id); + try (var is = ResourcesLoader.class.getResourceAsStream(uri)) { + if (is == null) return null; + + var zis = new ZipInputStream(is); + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) continue; + + var content = new String(zis.readAllBytes(), StandardCharsets.UTF_8); + vfs.put(entry.getName(), content); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + var mapJson = LocalFsLoader.GSON.fromJson(Objects.requireNonNull(vfs.get("map.json"), + "map.json not found in " + uri), LocalFsLoader.MapJson.class); + + return new ScriptBundle() { + @Override + public String id() { + return mapJson.id(); + } + + @Override + public List entrypoints() { + return mapJson.entrypoints(); + } + + @Override + public Script loadScript(String name) { + var file = Objects.requireNonNull(vfs.get(name), "script %s not found in %s".formatted(name, uri)); + return new Script(name, file); + } + }; + } +} From ed89e193c538c949874f6543353d301b44bc7496 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 20 Sep 2025 17:16:48 -0400 Subject: [PATCH 19/22] fix: make saveData optional --- .../net/hollowcube/mapmaker/runtime/freeform/ScriptState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/ScriptState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/ScriptState.java index 48e925abd..a87294158 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/ScriptState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/ScriptState.java @@ -9,7 +9,7 @@ public class ScriptState { public static Codec CODEC = StructCodec.struct( - "saveData", Codec.RAW_VALUE, ScriptState::saveData, + "saveData", Codec.RAW_VALUE.optional(), ScriptState::saveData, ScriptState::new); // Todo should not be a play state of course public static final SaveStateType.Serializer SERIALIZER = SaveStateType.serializer("playState", CODEC, HCDataTypes.PLAY_STATE); From 7c4a5846ebf6653a3d9a7ea40efe531b7efbcd7d Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 21 Sep 2025 23:25:52 -0400 Subject: [PATCH 20/22] feat: draft of lua boilerplate generator This time without attempting to properly map to java. Just to generate the boilerplate and serve `(LuaState) -> int`s to us. Additionally experiment with custom javadoc tags which we could use to make docs later --- gradle/libs.versions.toml | 2 + idea/Luau-Formatter.ipynb | 55 ++- modules/map-runtime/build.gradle.kts | 3 + .../runtime/freeform/lua/api-sketching.md | 359 ++++++++++++++++++ .../runtime/freeform/lua/base/LuaText.java | 93 +++++ .../freeform/lua/base/package-info.java | 4 + settings.gradle.kts | 2 + tools/lua-slopgen/api/build.gradle.kts | 3 + .../hollowcube/luau/annotation/LuaMeta.java | 14 + .../hollowcube/luau/annotation/LuaStatic.java | 12 + .../hollowcube/luau/annotation/LuaType.java | 16 + .../hollowcube/luau/annotation/MetaType.java | 37 ++ tools/lua-slopgen/build.gradle.kts | 10 + .../net/hollowcube/slopgen/LuaHandle.java | 12 + .../slopgen/LuaHandleCollector.java | 61 +++ .../slopgen/LuaSlopgenProcessor.java | 205 ++++++++++ 16 files changed, 884 insertions(+), 4 deletions(-) create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaText.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/package-info.java create mode 100644 tools/lua-slopgen/api/build.gradle.kts create mode 100644 tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaMeta.java create mode 100644 tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaStatic.java create mode 100644 tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaType.java create mode 100644 tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/MetaType.java create mode 100644 tools/lua-slopgen/build.gradle.kts create mode 100644 tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java create mode 100644 tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java create mode 100644 tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ebbccfe6b..0e8774f27 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ classgraph = "4.8.179" included = "-INCLUDED" luau = "0.5.1" luau-natives = "0.5.1" +javapoet = "0.7.0" [libraries] annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } @@ -52,6 +53,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" } +javapoet = { group = "com.palantir.javapoet", name = "javapoet", version.ref = "javapoet" } included-molang = { group = "dev.hollowcube", name = "molang", version.ref = "included" } included-schem = { group = "dev.hollowcube", name = "schem", version.ref = "included" } diff --git a/idea/Luau-Formatter.ipynb b/idea/Luau-Formatter.ipynb index 4c7b2c75b..d20ba5fdd 100644 --- a/idea/Luau-Formatter.ipynb +++ b/idea/Luau-Formatter.ipynb @@ -5,13 +5,60 @@ "metadata": { "collapsed": true, "ExecuteTime": { - "end_time": "2025-09-17T01:48:46.020867Z", - "start_time": "2025-09-17T01:48:45.960678Z" + "end_time": "2025-09-22T02:14:42.446744Z", + "start_time": "2025-09-22T02:14:42.237492Z" } }, - "source": "%use intellij-platform", + "source": [ + "%use intellij-platform\n", + "loadPlugins(\"com.intellij.java\")" + ], + "outputs": [], + "execution_count": 2 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-22T02:25:10.879859Z", + "start_time": "2025-09-22T02:25:10.732760Z" + } + }, + "cell_type": "code", + "source": [ + "import com.intellij.psi.PsiElement\n", + "import com.intellij.psi.PsiMethod\n", + "import com.intellij.psi.PsiReference\n", + "import com.intellij.psi.javadoc.CustomJavadocTagProvider\n", + "import com.intellij.psi.javadoc.JavadocTagInfo\n", + "import com.intellij.psi.javadoc.PsiDocTagValue\n", + "import org.jetbrains.annotations.Nls\n", + "\n", + "registerProjectExtension(JavadocTagInfo.EP_NAME.name, object : JavadocTagInfo {\n", + " override fun getName() = \"luaReturn\"\n", + "\n", + " override fun isInline() = false\n", + "\n", + " override fun isValidInContext(context: PsiElement?) = context is PsiMethod\n", + "\n", + " override fun checkTagValue(value: PsiDocTagValue?) = null\n", + "\n", + " override fun getReference(p0: PsiDocTagValue?) = null\n", + "})\n", + "\n", + "registerProjectExtension(JavadocTagInfo.EP_NAME.name, object : JavadocTagInfo {\n", + " override fun getName() = \"luaParam\"\n", + "\n", + " override fun isInline() = false\n", + "\n", + " override fun isValidInContext(context: PsiElement?) = context is PsiMethod\n", + "\n", + " override fun checkTagValue(value: PsiDocTagValue?) = null\n", + "\n", + " override fun getReference(p0: PsiDocTagValue?) = null\n", + "})" + ], "outputs": [], - "execution_count": 3 + "execution_count": 8 }, { "metadata": { diff --git a/modules/map-runtime/build.gradle.kts b/modules/map-runtime/build.gradle.kts index b5dc28604..9e0cf9048 100644 --- a/modules/map-runtime/build.gradle.kts +++ b/modules/map-runtime/build.gradle.kts @@ -11,6 +11,9 @@ dependencies { implementation(project(":modules:datafix")) implementation(project(":modules:compat")) + implementation(project(":tools:lua-slopgen:api")) + annotationProcessor(project(":tools:lua-slopgen")) + implementation(libs.minestom) implementation(libs.polar) implementation(libs.included.molang) diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md new file mode 100644 index 000000000..2b0f418ef --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md @@ -0,0 +1,359 @@ +## Imports + +`require('/path/to/script.luau')` for absolute from module root +`require('path/to/script.luau')` for relative from current script +`require('./path/to/script.luau')` alt for relative + +`require('@some/module')` for module imports + +`mapmaker`, `hollowcube`, `hc` are reserved. +eg `@mapmaker/gui` maybe for gui related things. + +(in theory we could have shared/external modules at some point, not any time soon) + +## Script + +## Globals + +### Runtime global + +Globally accessible in all scripts as `runtime`. Returns some info about the current runtime. + +#### API + +* `Version` - Mapmaker deployment version +* `Build` - Build number (first 6 of commit hash) +* `Size` - Runtime size name (eg `micro`) +* `Age` - Time (in seconds with fraction) since the runtime started. + * World age can say ticks since world started. +* `CPU` - Object for CPU inspection + * `TickTime` - Last tick time in seconds with fraction. (maybe should be µs no fraction) +* `Memory` - Object for memory inspection + * `Used/Max/Free` - Script memory space + * Probably should be allocated in an arena known to the jvm so we can track it better. + * `VMUsed/Max/Free` - JVM Memory (including script) + +### Map global + +Globally accessible in all scripts as `map`. + +> Should it be available in scripts not attached to the world/entities? + +#### API + +* `ID` - Map ID +* `Name` - Map name +* `Owner` - Map owner UUID +* `Size` - Map size name +* `Players` - Player manager +* `World` - Returns root world view (ie not a player view even for a player script) + +### Luau Libaries + +* Globals: Partially supported -> https://luau.org/library#global-functions + * Wont allow the following until good reason: `gcinfo`, `get/setfenv`, `newproxy`, `rawget`, `rawset` +* `math`: Fully supported -> https://luau.org/library#math-library +* `table`: Fully supported -> https://luau.org/library#table-library +* `string`: Fully supported -> https://luau.org/library#string-library +* `coroutine`: Not sure, probably partial -> https://luau.org/library#coroutine-library +* `bit32`: Fully supported -> https://luau.org/library#bit32-library +* `utf8`: Fully supported -> https://luau.org/library#utf8-library +* `os`: Fully supported -> https://luau.org/library#os-library +* `debug`: Fully supported -> https://luau.org/library#debug-library +* `buffer`: Fully supported -> https://luau.org/library#buffer-library +* `vector`: Fully supported -> https://luau.org/library#vector-library + +### Extra Global Libraries + +All of these are globally accessible. Might be worth making some of them imported. +Currently not sure what the distinction is between global libraries and imported ones. + +#### `task` + +```luau +task.spawn(function() +end) -- returns handle +task.wait(1) -- in ticks +task.cancel(handle) +``` + +#### `json` + +* `json.parse(string): unknown` +* `json.stringify(value, { indent?: integer }?): string` + +## Basic Types + +* luau primitives (incl. vector & buffer) +* `Quaternion` - quaternion +* `Color` - Any color (named, rgb, etc), may have alpha component (which is sometimes ignored) +* `Text` - Styled text component + * `AnyText` - type alias for anything that can convert to text, eg Text, string, possibly number/etc + * `Text.new` parses a minimessage string + * ? `Text.parseLegacy` to parse legacy color codes (eg `&c`). + * `Text.sanitize` to sanitize minimessage text (eg from player input) + * Operators + * `..` to join (styling inherited probably) + * `#` to get length + * `==` to compare + * `~=` to compare + * Instance methods + * some way to serialize to plain text +* `Direction` - `Direction.North`, `.South`, etc +* `Slot` - Special slot constants, eg `Slot.MainHand`, `.Saddle`, etc + +## Content Types + +### `Item` + +An item (stack) + +* A custom item predefined in module +* A vanilla item? +* A custom item created dynamically? +* What is an air item + * `nil`? a constant (eg `items.Air`)? `.IsAir` property? + * Below examples assume nil but not sure. +* Do we differentiate between item type (material) and itemstack (with count)? +* How do you create item instances (factoring in custom items)? + +Most likely we treat vanilla and custom items the same, +and wrap the components API to introduce our own in addition +to whichever vanilla components we want to expose. + +### `Block` + +A block with properties. Note that blocks should never have a nil value. Air is a block and should +be used in any such case to represent a missing block. + +* What to do with block entities? +* Custom blocks? + +Vanilla blocks acessible via `Block` global, eg `Block.Stone`. Properties +can be set via object constructor, eg `Block.StoneStairs { facing = Direction.West }`. + +#### API + +* `IsAir` - True for the air variants, false otherwise. +* `[property: string]: string` - Block property indexers + +### `Particle` + +A single particle & its settings. + +* Only vanilla particles for now. + +Accessible via `Particle` global, eg `Particle.Flame`. Can be configured +via object constructor, eg `Particle.Dust { color = Color.Red }`. + +Different from a ParticleSystem/Spawner we may introduce for fancier effects. + +### `Entity` + +An entity is any object added to the world, not just visible objects. + +For example, you could add a script to the world conditionally by attaching +it to an otherwise empty entity. It wouldn't render to the players at all. + +There will be some built-in entities in addition to player spawned ones. + +* Item -> ground item +* Text -> text display +* All projectiles? + +Entities have: + +* Properties? + * Used to control data on the entity (like some vanilla entity properties) +* AnimationController? + * Only present for entities with a model (possibly with animation_controller component) + +Entities can be described statically for modules: + +```json5 +{ + // other fields idk, anything that wouldnt be attached to instances of the + // entity like spawn conditions if we had them. + "components": { + // Script components attach the given script to the entity + // A new instance of the script will be created/removed with each instance of the entity + "mapmaker:script": { + "script": "path/to/script.luau" + }, + "mapmaker:model": { + // vanilla would show a vanilla entity at this position + // Not all vanilla entities may be used notably. + // This field is mutually exclusive with the rest below. + "vanilla": "minecraft:enderman" + } + } +} +``` + +You can spawn an entity with `world:SpawnEntity(position, type, initializer)`. + +For example: + +```luau +world:SpawnEntity(vec(1, 2, 3), "text", { + text = "Hello, world!" +}) +``` + +#### API + +* `Uuid` - uuid of the entity +* `Remove()` - Removes the entity from the world + +#### Built-in Entities + +* Item + * Set item stack + * Set pick up delay (per player?) + * Disable pick up entirely + +### Player + +#### API + +* `Uuid` - uuid of the player +* `Name` - username of the player +* `Position` - vector xyz position of the player +* `Yaw`, `Pitch` - rotation of the player +* `Velocity` - vector xyz velocity of the player + +Persistence + +* `SaveData` - Arbitrary scratch table which is persisted + * Saved as JSON, so may only contain tables/primitives. + * Never modified by the server itself + * Max size of bytes. +* `SaveDataSize(): integer` - Computes the size of the current save data, in bytes. + +Movement + +* `Teleport(position: vector, yaw: number?, pitch: number?, relativeFlags: 'xyzrw'?)` +* `TeleportWithVelocity(...)` +* `SetVelocity(velocity: vector)` - In blocks per second? Or blocks per tick? + +Communication + +* `SendMessage(message: AnyText)` +* `ShowTitle(title: AnyText, subtitle?: AnyText, { fadeIn?: number, stay?: number, fadeOut?: number }?)` +* `ShowActionBar(message: AnyText, duration?: number)` +* `Sidebar` - Sidebar object controls the sidebar + * `Enabled` - Get or set whether the sidebar is shown + * `Title` - Get or set the sidebar title + * `Clear()` - Remove all lines and reset title + * Not sure about below + * Should we support scores at all? + * Or just use fixed number format to show an arbitrary suffix and people can use it for score if they want? + * `AddLine(line: AnyText, index: integer?)` - Appends a line at the index (or end) + * `SetLine(line: AnyText, index: integer)` - Updates the line at the given index + * `RemoveLine(index: integer)` - Removes the line at the given index +* `PlaySound(sound: string, { volume?: number, pitch?: number, category?: string }?)` +* `PlaySoundAt(sound: string, position: vector, { volume?: number, pitch?: number, category?: string }?)` +* `PlaySoundFrom(sound: string, source: Entity | Player, { volume?: number, pitch?: number, category?: string }?)` +* `StopSound(sound: string?, category: string?)` +* 1`PlayerList` - Player list object controls the player list + * `Header` - Get or set the header + * `Footer` - Get or set the footer + * Possibly also functions to add fake players, choose who to show, etc +* 1`AddBossBar(...)` - later problem +* 1`RemoveBossBar(...)` - later problem + +1 Used for branding, so not sure if it should be exposed. Maybe its a paid feature to remove +all HC branding? Or for trusted people? Something else? + +Item/Inventory + +* `GetSlot(slot: Slot): Item | nil` - gets item in slot, returns nil if the player doesnt have that slot (eg saddle) +* `SetSlot(slot: Slot, item: Item | nil): boolean` - Sets the slot, returns whether the slot changed. +* `GetItem(index: integer): Item | nil` - gets item in player inventory at index. 1-9 is hotbar left to right, 10+ is + main inventory starting from top left moving right then down +* `SetItem(index: integer, item: Item | nil): boolean` - sets item in slot, returns whether the slot changed +* `AddItem(item: Item, { options }?): (retval)` - adds an item to the inventory + * Options: allowStacking (stack with same item), allowPartial (stack with same item) + * Return: Not sure :) + * Success is simplest but doesnt tell much and loses info if only part of the stack is added + * Slot would say where, but what if its added to multiple slots + * Possibly `(success bool, slot, remainder)` and accept losing info for multiple slots? Or make slot change to a + list if allowed to split multiple times + +#### Events + +* UseItem - Right click with item +* BlockInteract - Right click on block +* EntityInteract - Right click on entity +* PlayerInteract - Right click on other player (if entities and players are split, otherwise just fold into entity + interact) +* PickUpItem - Collect an item from the ground + +### `World` + +#### API + +* `Age` - World age, in ticks. + * See `runtime.Age` for runtime age in wall clock time. + +* *insert all the 'communication' functions from Player, but would send to all players* + +#### Events + +* Tick + +## Services/Managers/whatever + +### `PlayerManager` + +Accessible globally as `map.Players` + +#### API + +TODO: api to get entities by range/type/nearby/etc + +* `PlayerCount -> integer` +* `Players -> { Player }` +* `GetPlayer(uuid) -> Player?` +* `GetPlayerByName(name) -> Player?` +* ? `Kick(uuid | name | player, reason?: Text)` +* ? `Ban(uuid | name | player, reason?: Text)` + +#### Events + +* PlayerJoined +* PlayerLeft + +## Libraries + +### `@mapmaker/parkour`? + +Exposes access to parkour features, would allow programatically: + +* starting/stopping parkour + * Would add pk hotbar, start timer, etc. +* Resetting (soft and hard reset) +* Finishing +* Applying actions + +### `@mapmaker/terraform` + +Gives access to terraform operations like setting regions, loading/saving schematics, etc. + +### `@mapmaker/http`? + +HTTP request library? Probably a lot of issues here +that need to be thought through :| + +At a minimum, extremely rate limited. + +### `@mapmaker/store`? + +If we ever allowed people to sell things in maps for cubits. + +# Other Notes + +* Should allow people to buy custom map names + * `/play mycoolgame` + * Via `mycoolgame.hollowcu.be` + * Custom domain at extra cost maybe \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaText.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaText.java new file mode 100644 index 000000000..ccb57ac92 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaText.java @@ -0,0 +1,93 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.base; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaMeta; +import net.hollowcube.luau.annotation.LuaStatic; +import net.hollowcube.luau.annotation.LuaType; +import net.hollowcube.luau.annotation.MetaType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.text.minimessage.tag.standard.StandardTags; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + +//todo dont really need to inherit from the generated type. we just get TYPE_NAME from it. +// type name should also go away because we should be using tagged userdata. +@LuaType(implFor = Component.class, name = "Text") +public class LuaText implements LuaText$luau { + private static final PlainTextComponentSerializer PLAIN_TEXT_SERIALIZER = + PlainTextComponentSerializer.plainText(); + // Extremely limited mini message for now, likely will expand in the future. + // Not sure if we want open_url for example. Though probably do want some click events. + private static final MiniMessage MINI_MESSAGE = MiniMessage.builder() + .tags(TagResolver.builder() + .resolver(StandardTags.color()) + .resolver(StandardTags.decorations()) + .resolver(StandardTags.gradient()) + .resolver(StandardTags.rainbow()) + .resolver(StandardTags.hoverEvent()) + .resolver(StandardTags.pride()) + .build() + ) + .build(); + + public static void push(LuaState state, Component value) { + state.newUserData(value); + state.getMetaTable(TYPE_NAME); + state.setMetaTable(-2); + } + + public static Component checkArg(LuaState state, int index) { + return (Component) state.checkUserDataArg(index, TYPE_NAME); + } + + //region Static Methods + + /// Construct a new Text object from a minimessage string. + /// + /// ```luau + /// local redText = Text.new("This is red text") + /// player:SendMessage(redText) -- Player will see "This is red text" in red. + ///``` + /// + /// @return My return vaue + /// @luaParam text: string - The string text in minimessage format. User input text be escaped using [#sanitize]. + /// @luaReturn Text - The parsed text component. + @LuaStatic + public static int new_(LuaState state) { + var raw = state.checkStringArg(1); + push(state, MINI_MESSAGE.deserialize(raw)); + return 1; + } + + /// Sanitizes input text for any minimessage tags. + /// + /// ```luau + /// local raw = "This is not red text" + /// local safe = Text.sanitize(raw) + /// -- Safe contains the text "This is not red text", with no formatting. + ///``` + /// + /// @luaParam text: string - The raw text, possibly containing minimessage tags + /// @luaReturn string - The same string, but with any minimessage tags escaped. + @LuaStatic + public static int sanitize(LuaState state) { + var raw = state.checkStringArg(1); + state.pushString(MINI_MESSAGE.escapeTags(raw)); + return 1; + } + + //endregion + + //region Meta Methods + + @LuaMeta(MetaType.TOSTRING) + public static int luaToString(LuaState state) { + var component = checkArg(state, 1); + state.pushString(PLAIN_TEXT_SERIALIZER.serialize(component)); + return 1; + } + + //endregion +} + diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/package-info.java new file mode 100644 index 000000000..cea749d04 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform.lua.base; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8c15e8272..133f0f369 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,8 @@ include( ) include( + "tools:lua-slopgen:api", + "tools:lua-slopgen", "tools:native-image-helper", ) diff --git a/tools/lua-slopgen/api/build.gradle.kts b/tools/lua-slopgen/api/build.gradle.kts new file mode 100644 index 000000000..ccdf74af3 --- /dev/null +++ b/tools/lua-slopgen/api/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("mapmaker.java-library") +} diff --git a/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaMeta.java b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaMeta.java new file mode 100644 index 000000000..5f608f74e --- /dev/null +++ b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaMeta.java @@ -0,0 +1,14 @@ +package net.hollowcube.luau.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface LuaMeta { + + MetaType value(); + +} diff --git a/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaStatic.java b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaStatic.java new file mode 100644 index 000000000..77a10e2e0 --- /dev/null +++ b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaStatic.java @@ -0,0 +1,12 @@ +package net.hollowcube.luau.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Marks the annotated method as 'static', aka a member of the table with the same name as the type +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.SOURCE) +public @interface LuaStatic { +} diff --git a/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaType.java b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaType.java new file mode 100644 index 000000000..785413b4d --- /dev/null +++ b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaType.java @@ -0,0 +1,16 @@ +package net.hollowcube.luau.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface LuaType { + + Class implFor() default Object.class; + + String name() default ""; + +} diff --git a/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/MetaType.java b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/MetaType.java new file mode 100644 index 000000000..617395351 --- /dev/null +++ b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/MetaType.java @@ -0,0 +1,37 @@ +package net.hollowcube.luau.annotation; + +public enum MetaType { + ADD("__add"), + SUB("__sub"), + MUL("__mul"), + DIV("__div"), + UNM("__unm"), + MOD("__mod"), + POW("__pow"), + IDIV("__idiv"), + CONCAT("__concat"), + + EQ("__eq"), + LE("__le"), + LT("__lt"), + + LEN("__len"), + TOSTRING("__tostring"), + + ITER("__iter"), + CALL("__call"), + NAMECALL("__namecall"), + INDEX("__index"), + NEWINDEX("__newindex"), + ; + + private final String methodName; + + MetaType(String methodName) { + this.methodName = methodName; + } + + public String methodName() { + return this.methodName; + } +} diff --git a/tools/lua-slopgen/build.gradle.kts b/tools/lua-slopgen/build.gradle.kts new file mode 100644 index 000000000..9dd6aa645 --- /dev/null +++ b/tools/lua-slopgen/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("mapmaker.java-library") +} + +dependencies { + implementation(project(":tools:lua-slopgen:api")) + + implementation(libs.javapoet) + implementation(libs.luau.lib) +} diff --git a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java new file mode 100644 index 000000000..4d64c2193 --- /dev/null +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java @@ -0,0 +1,12 @@ +package net.hollowcube.slopgen; + +import com.palantir.javapoet.TypeName; +import net.hollowcube.luau.annotation.MetaType; +import org.jetbrains.annotations.Nullable; + +public record LuaHandle( + TypeName owningType, String methodName, + boolean isLuaStatic, boolean isStatic, + @Nullable MetaType metaType +) { +} diff --git a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java new file mode 100644 index 000000000..56f1be90b --- /dev/null +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java @@ -0,0 +1,61 @@ +package net.hollowcube.slopgen; + +import com.palantir.javapoet.TypeName; +import com.sun.source.util.DocTrees; +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaMeta; +import net.hollowcube.luau.annotation.LuaStatic; + +import javax.annotation.processing.Messager; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.util.SimpleElementVisitor14; +import java.util.List; + +public class LuaHandleCollector extends SimpleElementVisitor14> { + private final Messager messager; + private final DocTrees docTrees; + + public LuaHandleCollector(Messager messager, DocTrees docTrees) { + this.messager = messager; + this.docTrees = docTrees; + } + + @Override + public Void visitType(TypeElement e, List luaHandles) { + // Visit all enclosed elements (methods, fields, constructors, etc.) + for (Element enclosedElement : e.getEnclosedElements()) { + enclosedElement.accept(this, luaHandles); + } + return super.visitType(e, luaHandles); + } + + @Override + public Void visitExecutable(ExecutableElement e, List luaHandles) { + var isPublic = e.getModifiers().stream().anyMatch(m -> m == Modifier.PUBLIC); + if (!isPublic) return null; + + if (e.getParameters().size() != 1 || !e.getParameters().getFirst().asType().toString().equals(LuaState.class.getName())) + return null; + if (e.getReturnType().getKind() != TypeKind.INT) + return null; + + var luaMeta = e.getAnnotation(LuaMeta.class); + + messager.printWarning("docs: " + docTrees.getDocCommentTree(e), e); + + luaHandles.add(new LuaHandle( + TypeName.get(e.getEnclosingElement().asType()), + e.getSimpleName().toString(), + e.getAnnotation(LuaStatic.class) != null, + e.getModifiers().stream().anyMatch(m -> m == Modifier.STATIC), + luaMeta != null ? luaMeta.value() : null + )); + + return super.visitExecutable(e, luaHandles); + } + +} diff --git a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java new file mode 100644 index 000000000..5b02ff7d7 --- /dev/null +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java @@ -0,0 +1,205 @@ +package net.hollowcube.slopgen; + +import com.google.auto.service.AutoService; +import com.palantir.javapoet.*; +import com.sun.source.util.DocTrees; +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaType; +import net.hollowcube.luau.annotation.MetaType; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@AutoService(Processor.class) +public class LuaSlopgenProcessor extends AbstractProcessor { + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + var messager = processingEnv.getMessager(); + var elementUtils = processingEnv.getElementUtils(); + var filer = processingEnv.getFiler(); + var docTrees = DocTrees.instance(processingEnv); + + for (var annotatedElement : roundEnv.getElementsAnnotatedWith(LuaType.class)) { + if (!(annotatedElement instanceof TypeElement typeElement)) continue; + + var luaTypeMirror = typeElement.getAnnotationMirrors().stream() + .filter(mirror -> mirror.getAnnotationType().toString().equals(LuaType.class.getName())) + .findFirst().orElseThrow(); + var luaTypeMirrorValues = luaTypeMirror.getElementValues().entrySet().stream().collect(Collectors.toMap( + e -> e.getKey().getSimpleName().toString(), + Map.Entry::getValue)); + + var packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString(); + var glueTypeName = ClassName.get(packageName, typeElement.getSimpleName() + "$luau"); + var glueTypeBuilder = TypeSpec.interfaceBuilder(glueTypeName); + + var annotatedType = TypeName.get(typeElement.asType()); + var targetType = luaTypeMirrorValues.containsKey("implFor") + ? (TypeMirror) luaTypeMirrorValues.get("implFor").getValue() + : annotatedType; + var targetName = luaTypeMirrorValues.containsKey("name") + ? (String) luaTypeMirrorValues.get("name").getValue() + : typeElement.getSimpleName().toString().replace("Lua", ""); + + var handles = new ArrayList(); + new LuaHandleCollector(messager, docTrees).visit(typeElement, handles); + + // Add constant with metatable/type name + glueTypeBuilder.addField(FieldSpec.builder(String.class, "TYPE_NAME", + Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("$S", targetName) + .build()); + + boolean foundEqImpl = false, foundToStringImpl = false; + + { // Init Method + var initMethod = MethodSpec.methodBuilder("init$luau") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.VOID); + + initMethod.addStatement("state.newMetaTable(TYPE_NAME)"); + initMethod.addStatement("state.pushString(TYPE_NAME)"); + initMethod.addStatement("state.setField(-2, $S)", "__type"); + + // Insert references to meta methods + for (var handle : handles) { + if (handle.metaType() == null || handle.isLuaStatic()) + continue; + // Index, newindex, and namecall are always proxied through the impl class. Underlying implementations can still + // implement these functions, but they will be the "default" case if no other match is found. + if (handle.metaType() == MetaType.INDEX || handle.metaType() == MetaType.NEWINDEX || handle.metaType() == MetaType.NAMECALL) + continue; + + foundEqImpl |= handle.metaType() == MetaType.EQ; + foundToStringImpl |= handle.metaType() == MetaType.TOSTRING; + + initMethod.addStatement("state.pushCFunction($T::$L, $S)", handle.owningType(), + handle.methodName(), handle.metaType().methodName()); + initMethod.addStatement("state.setField(-2, $S)", handle.metaType().methodName()); + } + + // If we didn't find __eq or __tostring, add the default impl + if (!foundEqImpl) { + initMethod.addStatement("state.pushCFunction($T::luaEq, $S)", glueTypeName, "__eq"); + initMethod.addStatement("state.setField(-2, $S)", "__eq"); + } + if (!foundToStringImpl) { + initMethod.addStatement("state.pushCFunction($T::luaToString, $S)", glueTypeName, "__tostring"); + initMethod.addStatement("state.setField(-2, $S)", "__tostring"); + } + // Always add __index, __newindex, __namecall to the glue implementation + initMethod.addStatement("state.pushCFunction($T::luaIndex, $S)", glueTypeName, "__index"); + initMethod.addStatement("state.setField(-2, $S)", "__index"); + initMethod.addStatement("state.pushCFunction($T::luaNewIndex, $S)", glueTypeName, "__newindex"); + initMethod.addStatement("state.setField(-2, $S)", "__newindex"); + initMethod.addStatement("state.pushCFunction($T::luaNameCall, $S)", glueTypeName, "__namecall"); + initMethod.addStatement("state.setField(-2, $S)", "__namecall"); + + initMethod.addStatement("state.pop(1)"); // Pop the metatable + + initMethod.addCode("\n"); + + initMethod.addStatement("state.newTable()"); + initMethod.addStatement("state.pushValue(-1)"); + initMethod.addStatement("state.setMetaTable(-2)"); // Metatable to itself + + // Insert references to 'static' methods + for (var handle : handles) { + if (!handle.isLuaStatic()) + continue; + + var methodName = handle.metaType() != null ? handle.metaType().methodName() : handle.methodName(); + if (methodName.endsWith("_")) methodName = methodName.substring(0, methodName.length() - 1); + initMethod.addStatement("state.pushCFunction($T::$L, $S)", handle.owningType(), handle.methodName(), methodName); + initMethod.addStatement("state.setField(-2, $S)", methodName); + } + + initMethod.addStatement("state.setReadOnly(-1, true)"); + initMethod.addStatement("state.setGlobal(TYPE_NAME)"); + + glueTypeBuilder.addMethod(initMethod.build()); + } + + // If we didnt find eq or toString impls, add the defaults + if (!foundEqImpl) { + glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaEq") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT) + .addStatement("$T lhs = $T.checkArg(state, 1)", targetType, annotatedType) + .addStatement("$T rhs = $T.checkArg(state, 2)", targetType, annotatedType) + .addStatement("state.pushBoolean($T.equals(lhs, rhs))", Objects.class) + .addStatement("return 1") + .build()); + } + if (!foundToStringImpl) { + glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaToString") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT) + .addStatement("$T obj = $T.checkArg(state, 1)", targetType, annotatedType) + .addStatement("state.pushString($T.toString(obj))", Objects.class) + .addStatement("return 1") + .build()); + } + + // Always generate __index, __newindex, and __namecall. + glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaIndex") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT) + .addStatement("state.error($S)", "Not implemented") + .addStatement("return 0") + .build()); + glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaNewIndex") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT) + .addStatement("state.error($S)", "Not implemented") + .addStatement("return 0") + .build()); + glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaNameCall") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT) + .addStatement("state.error($S)", "Not implemented") + .addStatement("return 0") + .build()); + + try { + JavaFile.builder(packageName, glueTypeBuilder.build()) + .addFileComment("Generated by Lua Slopgen. DO NOT EDIT!") + .indent(" ") + .build() + .writeTo(filer); + } catch (IOException e) { + messager.printError("Failed to write generated file for " + annotatedElement.getSimpleName(), annotatedElement); + } + } + + return true; + } + + @Override + public Set getSupportedAnnotationTypes() { + return Set.of(LuaType.class.getName()); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.RELEASE_25; + } +} From 8a8e7a1bd0895f8eaec661d1b4997688e4282d73 Mon Sep 17 00:00:00 2001 From: mworzala Date: Mon, 22 Sep 2025 08:35:04 -0400 Subject: [PATCH 21/22] feat: more lua slop generation, draft of sidebar --- .../hollowcube/common/util/StringUtil.java | 13 ++ .../runtime/freeform/FreeformMapWorld.java | 22 +- .../runtime/freeform/lua/LuaTask.java | 1 + .../runtime/freeform/lua/api-sketching.md | 5 +- .../base/{LuaText.java => LuaTextImpl.java} | 34 ++- .../freeform/lua/player/LuaPlayer.java | 105 +++++----- .../freeform/lua/player/LuaSidebar.java | 62 ++++++ .../{LuaBlock.java => LuaBlockImpl.java} | 79 ++++--- .../runtime/freeform/lua/world/LuaWorld.java | 74 ++----- .../runtime/freeform/script/LuaHelpers.java | 10 + scripts/button-clicker/player.luau | 3 + .../luau/annotation/LuaProperty.java | 11 + .../net/hollowcube/slopgen/LuaHandle.java | 1 + .../slopgen/LuaHandleCollector.java | 2 + .../slopgen/LuaSlopgenProcessor.java | 195 +++++++++++++++--- 15 files changed, 438 insertions(+), 179 deletions(-) rename modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/{LuaText.java => LuaTextImpl.java} (74%) create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java rename modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/{LuaBlock.java => LuaBlockImpl.java} (52%) create mode 100644 tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaProperty.java diff --git a/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java b/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java index 635f0bbdf..309d6335f 100644 --- a/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java +++ b/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java @@ -16,4 +16,17 @@ public final class StringUtil { } return pascalCase.toString(); } + + public static @NotNull String pascalToSnake(@NotNull String pascalCase) { + StringBuilder snakeCase = new StringBuilder(); + for (int i = 0; i < pascalCase.length(); i++) { + char c = pascalCase.charAt(i); + if (Character.isUpperCase(c) && i > 0) { + snakeCase.append('_'); + } + snakeCase.append(Character.toLowerCase(c)); + } + return snakeCase.toString(); + } + } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java index e340bdb3d..bee02d51b 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java @@ -10,10 +10,13 @@ import net.hollowcube.mapmaker.runtime.freeform.lua.LuaEventSource; import net.hollowcube.mapmaker.runtime.freeform.lua.LuaGlobals; import net.hollowcube.mapmaker.runtime.freeform.lua.LuaTask; +import net.hollowcube.mapmaker.runtime.freeform.lua.base.LuaTextImpl$luau; import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; -import net.hollowcube.mapmaker.runtime.freeform.lua.player.LuaPlayer; -import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaBlock; +import net.hollowcube.mapmaker.runtime.freeform.lua.player.LuaPlayer$luau; +import net.hollowcube.mapmaker.runtime.freeform.lua.player.LuaSidebar$luau; +import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaBlockImpl$luau; import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaWorld; +import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaWorld$luau; import net.hollowcube.mapmaker.runtime.freeform.script.LuaScriptState; import net.kyori.adventure.bossbar.BossBar; import net.minestom.server.entity.Player; @@ -145,14 +148,21 @@ private static LuaState createGlobalState() { // Global APIs LuaVectorTypeImpl.init(global); // LuaColor.init(global); -// LuaText.init(global); + LuaTextImpl$luau.init$luau(global); LuaEventSource.init(global); - LuaBlock.init(global); - LuaWorld.init(global); + LuaBlockImpl$luau.init$luau(global); + LuaWorld$luau.init$luau(global); // LuaParticle.init(global); // LuaEntity.init(global); - LuaPlayer.init(global); + + // Player & friends + LuaPlayer$luau.init$luau(global); + LuaSidebar$luau.init$luau(global); + + // TODO for gen + // - use tagged user data (and a more generic way to add to state) + // - use service files to discover impls and load them. global.sandbox(); return global; diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java index 34356e1c7..8527a77ff 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.function.Supplier; +// @LuaLib("task") public class LuaTask { private static final String NAME = "task"; diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md index 2b0f418ef..0849e6f44 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md @@ -97,10 +97,11 @@ task.cancel(handle) * `#` to get length * `==` to compare * `~=` to compare + * `tostring` to serialize to plain text * Instance methods - * some way to serialize to plain text * `Direction` - `Direction.North`, `.South`, etc * `Slot` - Special slot constants, eg `Slot.MainHand`, `.Saddle`, etc + * Could probably be tagged light userdata constants ## Content Types @@ -239,6 +240,8 @@ Movement Communication * `SendMessage(message: AnyText)` +* `SendChatPrompt(message: AnyText, options: { [key: string]: AnyText }): string` -> sends the message and gives + clickable response options in the response. * `ShowTitle(title: AnyText, subtitle?: AnyText, { fadeIn?: number, stay?: number, fadeOut?: number }?)` * `ShowActionBar(message: AnyText, duration?: number)` * `Sidebar` - Sidebar object controls the sidebar diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaText.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaTextImpl.java similarity index 74% rename from modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaText.java rename to modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaTextImpl.java index ccb57ac92..3f681bb5f 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaText.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaTextImpl.java @@ -14,7 +14,7 @@ //todo dont really need to inherit from the generated type. we just get TYPE_NAME from it. // type name should also go away because we should be using tagged userdata. @LuaType(implFor = Component.class, name = "Text") -public class LuaText implements LuaText$luau { +public class LuaTextImpl implements LuaTextImpl$luau { private static final PlainTextComponentSerializer PLAIN_TEXT_SERIALIZER = PlainTextComponentSerializer.plainText(); // Extremely limited mini message for now, likely will expand in the future. @@ -41,6 +41,23 @@ public static Component checkArg(LuaState state, int index) { return (Component) state.checkUserDataArg(index, TYPE_NAME); } + public static Component checkAnyTextArg(LuaState state, int index) { + state.checkAny(index); // Make sure they provided an arg + return switch (state.type(index)) { + case STRING -> Component.text(state.toString(index)); + case NUMBER, BOOLEAN, VECTOR -> Component.text(state.toStringRepr(index)); + case TABLE -> { + state.argError(index, "Table to text is not yet supported"); + yield null; + } + case USERDATA -> checkArg(state, index); + default -> { + state.argError(index, "Expected a Text-able object"); + yield null; + } + }; + } + //region Static Methods /// Construct a new Text object from a minimessage string. @@ -81,6 +98,21 @@ public static int sanitize(LuaState state) { //region Meta Methods + @LuaMeta(MetaType.CONCAT) + public static int luaConcat(LuaState state) { + var lhs = checkArg(state, 1); + var rhs = checkAnyTextArg(state, 2); + push(state, Component.textOfChildren(lhs, rhs)); + return 1; + } + + @LuaMeta(MetaType.LEN) + public static int luaLen(LuaState state) { + var component = checkArg(state, 1); + state.pushInteger(PLAIN_TEXT_SERIALIZER.serialize(component).length()); + return 1; + } + @LuaMeta(MetaType.TOSTRING) public static int luaToString(LuaState state) { var component = checkArg(state, 1); diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java index 9e7f453e8..daa7d3c65 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java @@ -2,43 +2,35 @@ import com.google.gson.JsonObject; import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaProperty; +import net.hollowcube.luau.annotation.LuaType; import net.hollowcube.mapmaker.runtime.freeform.lua.LuaEventSource; +import net.hollowcube.mapmaker.runtime.freeform.lua.base.LuaTextImpl; import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; -import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaBlock; +import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaBlockImpl; import net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerBlockInteractEvent; +import org.jetbrains.annotations.Nullable; -import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchKey; -import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchMethod; - -public class LuaPlayer { - private static final String NAME = "Player"; - - public static void init(LuaState state) { - state.newMetaTable(NAME); - state.pushCFunction(LuaPlayer::luaIndex, "__index"); - state.setField(-2, "__index"); - state.pushCFunction(LuaPlayer::luaNewIndex, "__newindex"); - state.setField(-2, "__newindex"); - state.pushCFunction(LuaPlayer::luaNameCall, "__namecall"); - state.setField(-2, "__namecall"); - state.pop(1); - } +@LuaType +public class LuaPlayer implements LuaPlayer$luau { public static void push(LuaState state, LuaPlayer entity) { state.newUserData(entity); - state.getMetaTable(NAME); + state.getMetaTable(TYPE_NAME); state.setMetaTable(-2); } public static LuaPlayer checkArg(LuaState state, int index) { - return (LuaPlayer) state.checkUserDataArg(index, NAME); + return (LuaPlayer) state.checkUserDataArg(index, TYPE_NAME); } private final Player player; private final int saveDataRef; + private @Nullable LuaSidebar sidebar; // Lazy + public LuaPlayer(LuaState state, Player player, JsonObject saveData) { this.player = player; @@ -47,62 +39,61 @@ public LuaPlayer(LuaState state, Player player, JsonObject saveData) { state.pop(1); } - // Properties - - private int getUuid(LuaState state) { + @LuaProperty + public int getUuid(LuaState state) { state.pushString(player.getUuid().toString()); return 1; } - private int getSaveData(LuaState state) { + @LuaProperty + public int getName(LuaState state) { + state.pushString(player.getUsername()); + return 1; + } + + //region Communication + + @LuaProperty + public int getSidebar(LuaState state) { + if (sidebar == null) sidebar = new LuaSidebar(player); + LuaSidebar.push(state, sidebar); + return 1; + } + + public int sendMessage(LuaState state) { + var message = LuaTextImpl.checkAnyTextArg(state, 1); + player.sendMessage(message); + return 0; + } + + //endregion + + //region Persistence + + @LuaProperty + public int getSaveData(LuaState state) { state.getref(saveDataRef); return 1; } - private int getOnBlockInteract(LuaState state) { + //endregion Persistence + + //region Events + + @LuaProperty + public int getOnBlockInteract(LuaState state) { LuaEventSource.push(state, new LuaEventSource<>( player.eventNode(), PlayerBlockInteractEvent.class, (eventState, event) -> { LuaVectorTypeImpl.push(eventState, event.getBlockPosition()); - LuaBlock.push(eventState, event.getBlock()); + LuaBlockImpl.push(eventState, event.getBlock()); return 2; } )); return 1; } - // Methods + //endregion - // Metamethods - - private static int luaIndex(LuaState state) { - final LuaPlayer self = checkArg(state, 1); - final String key = state.checkStringArg(2); - return switch (key) { - case "Uuid" -> self.getUuid(state); - case "SaveData" -> self.getSaveData(state); - case "OnBlockInteract" -> self.getOnBlockInteract(state); - default -> noSuchKey(state, NAME, key); - }; - } - - private static int luaNewIndex(LuaState state) { - final LuaPlayer self = checkArg(state, 1); - final String key = state.checkStringArg(2); - state.remove(1); // Remove the userdata from the stack - state.remove(1); // Remove the key from the stack - return switch (key) { - default -> noSuchKey(state, NAME, key); - }; - } - - private static int luaNameCall(LuaState state) { - final LuaPlayer self = checkArg(state, 1); - state.remove(1); // Remove the world userdata from the stack (so implementations can pretend they have no self) - final String methodName = state.nameCallAtom(); - return switch (methodName) { - default -> noSuchMethod(state, NAME, methodName); - }; - } } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java new file mode 100644 index 000000000..a8b4e7404 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java @@ -0,0 +1,62 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.player; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaProperty; +import net.hollowcube.luau.annotation.LuaType; +import net.hollowcube.mapmaker.runtime.freeform.lua.base.LuaTextImpl; +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import net.minestom.server.scoreboard.Sidebar; + +@LuaType +public class LuaSidebar implements LuaSidebar$luau { + + public static void push(LuaState state, LuaSidebar value) { + state.newUserData(value); + state.getMetaTable(TYPE_NAME); + state.setMetaTable(-2); + } + + public static LuaSidebar checkArg(LuaState state, int index) { + return (LuaSidebar) state.checkUserDataArg(index, TYPE_NAME); + } + + private final Sidebar sidebar; + private final Player player; + + // Not stored on Sidebar and we want to be able to return it, so stored here. + private Component title = Component.empty(); + + public LuaSidebar(Player player) { + this.sidebar = new Sidebar(title); + this.player = player; + } + + @LuaProperty + public int getEnabled(LuaState state) { + state.pushBoolean(sidebar.isViewer(player)); + return 1; + } + + @LuaProperty + public int setEnabled(LuaState state) { + boolean newValue = state.checkBooleanArg(1); + if (newValue) sidebar.addViewer(player); + else sidebar.removeViewer(player); + return 1; + } + + @LuaProperty + public int getTitle(LuaState state) { + LuaTextImpl.push(state, title); + return 1; + } + + @LuaProperty + public int setTitle(LuaState state) { + title = LuaTextImpl.checkAnyTextArg(state, 1); + sidebar.setTitle(title); + return 1; + } + +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlock.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlockImpl.java similarity index 52% rename from modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlock.java rename to modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlockImpl.java index 7d15dffef..2ec1b1778 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlock.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlockImpl.java @@ -3,55 +3,56 @@ import net.hollowcube.common.util.BlockUtil; import net.hollowcube.common.util.StringUtil; import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaMeta; +import net.hollowcube.luau.annotation.LuaStatic; +import net.hollowcube.luau.annotation.LuaType; +import net.hollowcube.luau.annotation.MetaType; +import net.kyori.adventure.key.Key; import net.minestom.server.instance.block.Block; import java.util.HashMap; -public class LuaBlock { - public static final String NAME = "Block"; - - public static void init(LuaState state) { - // Create the metatable for Minestom Block - state.newMetaTable(NAME); - state.pushCFunction(LuaBlock::luaToString, "__tostring"); - state.setField(-2, "__tostring"); - state.pushCFunction(LuaBlock::luaCall, "__call"); - state.setField(-2, "__call"); - state.pushCFunction(LuaBlock::luaEq, "__eq"); - state.setField(-2, "__eq"); - state.pop(1); - - // Global table of all blocks - // todo this should probably just be an index metamethod, theres no need to prealloc this. - state.newTable(); - for (var block : Block.values()) { - var friendlyName = StringUtil.snakeToPascal( - block.key().value().replace("/", "_")); - - push(state, block); - state.setField(-2, friendlyName); - } - state.setReadOnly(-1, true); - state.setGlobal("Block"); - } +@LuaType(implFor = Block.class, name = "Block") +public class LuaBlockImpl implements LuaBlockImpl$luau { public static void push(LuaState state, Block block) { state.newUserData(block); - state.getMetaTable(NAME); + state.getMetaTable(TYPE_NAME); state.setMetaTable(-2); } public static Block checkArg(LuaState state, int index) { - return (Block) state.checkUserDataArg(index, NAME); + return (Block) state.checkUserDataArg(index, TYPE_NAME); } - private static int luaToString(LuaState state) { - var block = checkArg(state, 1); - state.pushString(BlockUtil.toString(block)); + //region Static Methods + + @LuaStatic + @LuaMeta(MetaType.INDEX) + public static int luaStaticIndex(LuaState state) { + var blockName = state.checkStringArg(1); + var blockId = StringUtil.pascalToSnake(blockName); + if (!Key.parseableValue(blockId)) { + state.argError(1, "Invalid block name"); + return 0; + } + + var block = Block.fromKey(blockName); + if (block == null) { + state.argError(1, "Invalid block name"); + return 0; + } + + push(state, block); return 1; } - private static int luaCall(LuaState state) { + //endregion + + //region Meta Methods + + @LuaMeta(MetaType.CALL) + public static int luaCall(LuaState state) { var block = checkArg(state, 1); var newProps = new HashMap(); state.pushNil(); @@ -74,11 +75,21 @@ private static int luaCall(LuaState state) { } } - private static int luaEq(LuaState state) { + @LuaMeta(MetaType.TOSTRING) + public static int luaToString(LuaState state) { + var block = checkArg(state, 1); + state.pushString(BlockUtil.toString(block)); + return 1; + } + + @LuaMeta(MetaType.EQ) + public static int luaEq(LuaState state) { var block1 = checkArg(state, 1); var block2 = checkArg(state, 2); state.pushBoolean(block1.stateId() == block2.stateId()); return 1; } + + //endregion } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java index 1323b6ac5..f9c697f5a 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java @@ -1,35 +1,22 @@ package net.hollowcube.mapmaker.runtime.freeform.lua.world; import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaProperty; +import net.hollowcube.luau.annotation.LuaType; import net.hollowcube.mapmaker.runtime.freeform.FreeformMapWorld; import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; -import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchKey; -import static net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers.noSuchMethod; - -public class LuaWorld { - private static final String NAME = "World"; - - public static void init(LuaState state) { - // Create the metatable for Entity - state.newMetaTable(NAME); - state.pushCFunction(LuaWorld::luaIndex, "__index"); - state.setField(-2, "__index"); - state.pushCFunction(LuaWorld::luaNewIndex, "__newindex"); - state.setField(-2, "__newindex"); - state.pushCFunction(LuaWorld::luaNameCall, "__namecall"); - state.setField(-2, "__namecall"); - state.pop(1); - } +@LuaType +public class LuaWorld implements LuaWorld$luau { public static void push(LuaState state, LuaWorld entity) { state.newUserData(entity); - state.getMetaTable(NAME); + state.getMetaTable(TYPE_NAME); state.setMetaTable(-2); } public static LuaWorld checkArg(LuaState state, int index) { - return (LuaWorld) state.checkUserDataArg(index, NAME); + return (LuaWorld) state.checkUserDataArg(index, TYPE_NAME); } private final FreeformMapWorld delegate; @@ -38,61 +25,34 @@ public LuaWorld(FreeformMapWorld world) { this.delegate = world; } - // Properties + //region Instance Properties - private int getUuid(LuaState state) { + @LuaProperty + public int getUuid(LuaState state) { state.pushString(delegate.map().id()); return 1; } - // Methods + //endregion + + //region Instance Methods - private int getBlock(LuaState state) { + public int getBlock(LuaState state) { var blockPosition = LuaVectorTypeImpl.checkArg(state, 1); var block = delegate.instance().getBlock(blockPosition); - LuaBlock.push(state, block); + LuaBlockImpl.push(state, block); return 1; } - private int setBlock(LuaState state) { + public int setBlock(LuaState state) { var blockPosition = LuaVectorTypeImpl.checkArg(state, 1); - var block = LuaBlock.checkArg(state, 2); + var block = LuaBlockImpl.checkArg(state, 2); delegate.instance().setBlock(blockPosition, block); return 0; } - // Metamethods - - private static int luaIndex(LuaState state) { - final LuaWorld world = checkArg(state, 1); - final String key = state.checkStringArg(2); - return switch (key) { - case "Uuid" -> world.getUuid(state); - default -> noSuchKey(state, NAME, key); - }; - } - - private static int luaNewIndex(LuaState state) { - final LuaWorld world = checkArg(state, 1); - final String key = state.checkStringArg(2); - state.remove(1); // Remove the userdata from the stack - state.remove(1); // Remove the key from the stack - return switch (key) { - default -> noSuchKey(state, NAME, key); - }; - } - - private static int luaNameCall(LuaState state) { - final LuaWorld world = checkArg(state, 1); - state.remove(1); // Remove the world userdata from the stack (so implementations can pretend they have no self) - final String methodName = state.nameCallAtom(); - return switch (methodName) { - case "GetBlock" -> world.getBlock(state); - case "SetBlock" -> world.setBlock(state); - default -> noSuchMethod(state, NAME, methodName); - }; - } + //endregion } \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java index c1b240a36..15c7e638b 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java @@ -20,6 +20,16 @@ public static int noSuchMethod(LuaState state, String typeName, String methodNam return 0; // Never reached, just to make java happy } + public static int fieldReadOnly(LuaState state, String typeName, String key) { + state.error(typeName + "." + key + " is read-only"); + return 0; + } + + public static int fieldWriteOnly(LuaState state, String typeName, String key) { + state.error(typeName + "." + key + " is write-only"); + return 0; + } + /// Iterates over a table (no checks to ensure its a table) and applies the given function for each key. /// During the callback, the value is always at index -1 (and the key at -2 if needed). /// diff --git a/scripts/button-clicker/player.luau b/scripts/button-clicker/player.luau index 5289ee94c..07ac70786 100644 --- a/scripts/button-clicker/player.luau +++ b/scripts/button-clicker/player.luau @@ -10,6 +10,9 @@ function onButtonPress(blockPosition, block) local buttonCount = player.SaveData.buttonCount or 0 player.SaveData.buttonCount = buttonCount + 1 print("Pressed", player.SaveData.buttonCount, "times") + + player.Sidebar.Enabled = true + player.Sidebar.Title = Text.new("Button Clicker") end player.OnBlockInteract:Listen(onButtonPress) diff --git a/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaProperty.java b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaProperty.java new file mode 100644 index 000000000..0d487e2e7 --- /dev/null +++ b/tools/lua-slopgen/api/src/main/java/net/hollowcube/luau/annotation/LuaProperty.java @@ -0,0 +1,11 @@ +package net.hollowcube.luau.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface LuaProperty { +} diff --git a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java index 4d64c2193..7ecf1968d 100644 --- a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java @@ -7,6 +7,7 @@ public record LuaHandle( TypeName owningType, String methodName, boolean isLuaStatic, boolean isStatic, + boolean isProperty, @Nullable MetaType metaType ) { } diff --git a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java index 56f1be90b..698115aac 100644 --- a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java @@ -4,6 +4,7 @@ import com.sun.source.util.DocTrees; import net.hollowcube.luau.LuaState; import net.hollowcube.luau.annotation.LuaMeta; +import net.hollowcube.luau.annotation.LuaProperty; import net.hollowcube.luau.annotation.LuaStatic; import javax.annotation.processing.Messager; @@ -52,6 +53,7 @@ public Void visitExecutable(ExecutableElement e, List luaHandles) { e.getSimpleName().toString(), e.getAnnotation(LuaStatic.class) != null, e.getModifiers().stream().anyMatch(m -> m == Modifier.STATIC), + e.getAnnotation(LuaProperty.class) != null, luaMeta != null ? luaMeta.value() : null )); diff --git a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java index 5b02ff7d7..ffd7f1141 100644 --- a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java @@ -43,7 +43,8 @@ public boolean process(Set annotations, RoundEnvironment var packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString(); var glueTypeName = ClassName.get(packageName, typeElement.getSimpleName() + "$luau"); - var glueTypeBuilder = TypeSpec.interfaceBuilder(glueTypeName); + var glueTypeBuilder = TypeSpec.interfaceBuilder(glueTypeName) + .addModifiers(Modifier.PUBLIC); var annotatedType = TypeName.get(typeElement.asType()); var targetType = luaTypeMirrorValues.containsKey("implFor") @@ -53,8 +54,15 @@ public boolean process(Set annotations, RoundEnvironment ? (String) luaTypeMirrorValues.get("name").getValue() : typeElement.getSimpleName().toString().replace("Lua", ""); + var luaHelpersType = ClassName.get("net.hollowcube.mapmaker.runtime.freeform.script", "LuaHelpers"); + var handles = new ArrayList(); new LuaHandleCollector(messager, docTrees).visit(typeElement, handles); + var getterSetterNames = handles.stream() + .filter(h -> h.metaType() == null && !h.isLuaStatic() && h.isProperty()) + .filter(h -> h.methodName().startsWith("get") || h.methodName().startsWith("set")) + .map(LuaHandle::methodName) + .toList(); // Add constant with metatable/type name glueTypeBuilder.addField(FieldSpec.builder(String.class, "TYPE_NAME", @@ -156,28 +164,169 @@ public boolean process(Set annotations, RoundEnvironment .build()); } - // Always generate __index, __newindex, and __namecall. - glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaIndex") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addParameter(LuaState.class, "state") - .returns(TypeName.INT) - .addStatement("state.error($S)", "Not implemented") - .addStatement("return 0") - .build()); - glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaNewIndex") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addParameter(LuaState.class, "state") - .returns(TypeName.INT) - .addStatement("state.error($S)", "Not implemented") - .addStatement("return 0") - .build()); - glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaNameCall") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addParameter(LuaState.class, "state") - .returns(TypeName.INT) - .addStatement("state.error($S)", "Not implemented") - .addStatement("return 0") - .build()); + { // Generate __index metamethod impl + var indexMethod = MethodSpec.methodBuilder("luaIndex") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT); + + indexMethod.addStatement("$T self = $T.checkArg(state, 1)", targetType, annotatedType); + indexMethod.addStatement("$T key = state.checkStringArg(2)", String.class); + indexMethod.beginControlFlow("return switch (key)"); + + for (var method : handles) { + if (method.metaType() != null || method.isLuaStatic() || !method.isProperty()) + continue; + if (method.methodName().startsWith("set")) { + // Check for read-only properties + var hasGetter = getterSetterNames.contains("get" + method.methodName().substring(3)); + if (!hasGetter) { + indexMethod.addStatement("case $S -> $T.fieldWriteOnly(state, TYPE_NAME, key)", + method.methodName().substring(3), luaHelpersType); + } + continue; + } + if (!method.methodName().startsWith("get")) + continue; + + indexMethod.addCode("case $S -> ", method.methodName().substring(3)); + if (method.isStatic()) { + indexMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + indexMethod.addStatement("self.$L(state)", method.methodName()); + } + } + + // If the class provides its own index metamethod, call that as the default case + boolean foundIndexProxy = false; + for (var method : handles) { + if (method.metaType() != MetaType.INDEX || method.isLuaStatic()) + continue; + + foundIndexProxy = true; + indexMethod.addCode("default -> "); + if (method.isStatic()) { + indexMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + indexMethod.addStatement("self.$L(state)", method.methodName()); + } + } + if (!foundIndexProxy) { + indexMethod.addStatement("default -> $T.noSuchKey(state, TYPE_NAME, key)", luaHelpersType); + } + + indexMethod.addCode("$<};"); + glueTypeBuilder.addMethod(indexMethod.build()); + } + { // Generate __newindex metamethod impl + var newIndexMethod = MethodSpec.methodBuilder("luaNewIndex") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT); + + newIndexMethod.addStatement("$T self = $T.checkArg(state, 1)", targetType, annotatedType); + newIndexMethod.addStatement("$T key = state.checkStringArg(2)", String.class); + newIndexMethod.beginControlFlow("return switch (key)"); + + for (var method : handles) { + if (method.metaType() != null || method.isLuaStatic() || !method.isProperty()) + continue; + if (method.methodName().startsWith("get")) { + // Check for read-only properties + var hasSetter = getterSetterNames.contains("set" + method.methodName().substring(3)); + if (!hasSetter) { + newIndexMethod.addStatement("case $S -> $T.fieldReadOnly(state, TYPE_NAME, key)", + method.methodName().substring(3), luaHelpersType); + } + continue; + } + if (!method.methodName().startsWith("set")) + continue; + + newIndexMethod.addCode("case $S -> ", method.methodName().substring(3)); + if (method.isStatic()) { + newIndexMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + // In the non-static case we remove the self and key args from the stack so the first arg + // is the key being set for setter methods. + newIndexMethod.addCode("{$>\n"); + newIndexMethod.addStatement("state.remove(1)"); + newIndexMethod.addStatement("state.remove(1)"); + newIndexMethod.addStatement("yield self.$L(state)", method.methodName()); + newIndexMethod.addCode("$<}\n"); + } + } + + // If the class provides its own index metamethod, call that as the default case + boolean foundNewIndexProxy = false; + for (var method : handles) { + if (method.metaType() != MetaType.NEWINDEX || method.isLuaStatic()) + continue; + + foundNewIndexProxy = true; + newIndexMethod.addCode("default -> "); + if (method.isStatic()) { + newIndexMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + newIndexMethod.addStatement("self.$L(state)", method.methodName()); + } + } + if (!foundNewIndexProxy) { + newIndexMethod.addStatement("default -> $T.noSuchKey(state, TYPE_NAME, key)", luaHelpersType); + } + + newIndexMethod.addCode("$<};"); + glueTypeBuilder.addMethod(newIndexMethod.build()); + } + { // Generate __namecall metamethod impl + var nameCallMethod = MethodSpec.methodBuilder("luaNameCall") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT); + + nameCallMethod.addStatement("$T self = $T.checkArg(state, 1)", targetType, annotatedType); + nameCallMethod.addStatement("$T methodName = state.nameCallAtom()", String.class); + nameCallMethod.beginControlFlow("return switch (methodName)"); + + // state.remove(1); // Remove the world userdata from the stack (so implementations can pretend they have no self) + for (var method : handles) { + if (method.metaType() != null || method.isLuaStatic() || method.isProperty()) + continue; + + nameCallMethod.addCode("case $S -> ", method.methodName()); + if (method.isStatic()) { + nameCallMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + // In the non-static case we remove the self arg from the stack so the first arg + // is the first parameter to the method. + nameCallMethod.addCode("{$>\n"); + nameCallMethod.addStatement("state.remove(1)"); + nameCallMethod.addStatement("yield self.$L(state)", method.methodName()); + nameCallMethod.addCode("$<}\n"); + } + } + + // If the class provides its own namecall metamethod, call that as the default case + boolean foundNameCallProxy = false; + for (var method : handles) { + if (method.metaType() != MetaType.NAMECALL || method.isLuaStatic()) + continue; + + foundNameCallProxy = true; + nameCallMethod.addCode("default -> "); + if (method.isStatic()) { + nameCallMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + nameCallMethod.addStatement("self.$L(state)", method.methodName()); + } + } + if (!foundNameCallProxy) { + nameCallMethod.addStatement("default -> $T.noSuchMethod(state, TYPE_NAME, methodName)", luaHelpersType); + } + + nameCallMethod.addCode("$<};"); + glueTypeBuilder.addMethod(nameCallMethod.build()); + } try { JavaFile.builder(packageName, glueTypeBuilder.build()) From e10c3c1e43dde3377a98b6351fe1791c46204e95 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 28 Sep 2025 22:05:18 -0400 Subject: [PATCH 22/22] feat: slopgen support for inheritance, partial text display --- .../runtime/freeform/FreeformMapWorld.java | 3 +- .../runtime/freeform/FreeformState.java | 3 + .../runtime/freeform/lua/api-sketching.md | 47 +++- .../freeform/lua/entity/LuaDisplayEntity.java | 92 ++++++++ .../freeform/lua/entity/LuaEntity.java | 86 +++++++ .../lua/entity/LuaTextDisplayEntity.java | 81 +++++++ .../freeform/lua/entity/package-info.java | 4 + .../freeform/lua/player/LuaSidebar.java | 25 ++ .../runtime/freeform/lua/world/LuaWorld.java | 27 +++ .../runtime/freeform/script/LuaHelpers.java | 4 +- scripts/button-clicker/player.luau | 13 +- .../slopgen/LuaSlopgenProcessor.java | 221 ++++++++++++------ 12 files changed, 527 insertions(+), 79 deletions(-) create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaDisplayEntity.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaEntity.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaTextDisplayEntity.java create mode 100644 modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/package-info.java diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java index bee02d51b..61cba1865 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java @@ -11,6 +11,7 @@ import net.hollowcube.mapmaker.runtime.freeform.lua.LuaGlobals; import net.hollowcube.mapmaker.runtime.freeform.lua.LuaTask; import net.hollowcube.mapmaker.runtime.freeform.lua.base.LuaTextImpl$luau; +import net.hollowcube.mapmaker.runtime.freeform.lua.entity.LuaEntity$luau; import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; import net.hollowcube.mapmaker.runtime.freeform.lua.player.LuaPlayer$luau; import net.hollowcube.mapmaker.runtime.freeform.lua.player.LuaSidebar$luau; @@ -154,7 +155,7 @@ private static LuaState createGlobalState() { LuaBlockImpl$luau.init$luau(global); LuaWorld$luau.init$luau(global); // LuaParticle.init(global); -// LuaEntity.init(global); + LuaEntity$luau.init$luau(global); // Player & friends LuaPlayer$luau.init$luau(global); diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java index 1cccf0bc6..6b331990a 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java @@ -9,6 +9,7 @@ import net.hollowcube.mapmaker.player.PlayerData; import net.hollowcube.mapmaker.runtime.freeform.bundle.ScriptBundle; import net.hollowcube.mapmaker.runtime.freeform.lua.player.LuaPlayer; +import net.hollowcube.mapmaker.runtime.freeform.lua.world.LuaWorld; import net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers; import net.hollowcube.mapmaker.runtime.freeform.script.LuaScriptState; import net.minestom.server.codec.Codec; @@ -55,6 +56,8 @@ public void configurePlayer(FreeformMapWorld world, Player player, @Nullable Fre thread.state().newTable(); LuaPlayer.push(thread.state(), new LuaPlayer(thread.state(), player, saveData)); thread.state().setField(-2, "Parent"); // Set the player as the parent + LuaWorld.push(thread.state(), new LuaWorld(world)); + thread.state().setField(-2, "World"); // todo want to expose world on game object instead of script object thread.state().setReadOnly(-1, true); // Make it read-only thread.state().setGlobal("script"); diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md index 0849e6f44..ccd70e989 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md @@ -196,14 +196,22 @@ You can spawn an entity with `world:SpawnEntity(position, type, initializer)`. For example: ```luau -world:SpawnEntity(vec(1, 2, 3), "text", { +local text = world:SpawnEntity(vec(1, 2, 3), "text", { text = "Hello, world!" }) + +task.spawn(function() + task.wait(100) + text:Remove() +end) ``` #### API * `Uuid` - uuid of the entity +* `Position` +* `Yaw` +* `Pitch` * `Remove()` - Removes the entity from the world #### Built-in Entities @@ -212,6 +220,36 @@ world:SpawnEntity(vec(1, 2, 3), "text", { * Set item stack * Set pick up delay (per player?) * Disable pick up entirely +* Text + * Init Properties + * Alignment + * Background + * DefaultBackground t/f + * LineWidth + * SeeThrough + * Shadow + * Text + * TextOpacity + * (all display) + * Billboard + * Block/Sky Light + * GlowColorOverride + * Width/Height + * InterpolationDuration + * ShadowRadius + * ShadowStrength + * Transformation + +``` + +entity.TextOpacity = 0 + +// later +entity.Interpolate(20 * 60 * 5, { + TextOpacity = 5 +}) + +``` ### Player @@ -248,12 +286,9 @@ Communication * `Enabled` - Get or set whether the sidebar is shown * `Title` - Get or set the sidebar title * `Clear()` - Remove all lines and reset title - * Not sure about below - * Should we support scores at all? - * Or just use fixed number format to show an arbitrary suffix and people can use it for score if they want? * `AddLine(line: AnyText, index: integer?)` - Appends a line at the index (or end) - * `SetLine(line: AnyText, index: integer)` - Updates the line at the given index - * `RemoveLine(index: integer)` - Removes the line at the given index + * `SetLine(index: integer, line: AnyText)` - Updates the line at the given index + * `RemoveLine(index: integer)` - Removes the line at the given index * `PlaySound(sound: string, { volume?: number, pitch?: number, category?: string }?)` * `PlaySoundAt(sound: string, position: vector, { volume?: number, pitch?: number, category?: string }?)` * `PlaySoundFrom(sound: string, source: Entity | Player, { volume?: number, pitch?: number, category?: string }?)` diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaDisplayEntity.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaDisplayEntity.java new file mode 100644 index 000000000..9087bf4da --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaDisplayEntity.java @@ -0,0 +1,92 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.entity; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaProperty; +import net.hollowcube.luau.annotation.LuaType; +import net.hollowcube.mapmaker.map.entity.impl.DisplayEntity; +import net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.metadata.display.AbstractDisplayMeta; + +import java.util.Locale; + +@LuaType +public class LuaDisplayEntity extends LuaEntity implements LuaDisplayEntity$luau { + + public LuaDisplayEntity(Entity delegate) { + super(delegate); + } + + @Override + protected DisplayEntity delegate() { + return (DisplayEntity) super.delegate(); + } + + @Override + public boolean readField(LuaState state, String key, int index) { + return switch (key) { + default -> readInterpField(state, key, index) + || super.readField(state, key, index); + }; + } + + public boolean readInterpField(LuaState state, String key, int index) { + // Note that these keys also need to be added to readField + return switch (key) { + default -> false; + }; + } + + //region Properties + + @LuaProperty + public int getBillboard(LuaState state) { + state.pushString(delegate().getEntityMeta().getBillboardRenderConstraints().name().toLowerCase(Locale.ROOT)); + return 1; + } + + @LuaProperty + public int setBillboard(LuaState state) { + var billboardString = state.checkStringArg(1); + try { + var billboard = AbstractDisplayMeta.BillboardConstraints.valueOf(billboardString.toUpperCase(Locale.ROOT)); + delegate().getEntityMeta().setBillboardRenderConstraints(billboard); + } catch (IllegalArgumentException e) { + state.argError(1, "Invalid billboard value, must be one of 'fixed', 'vertical', 'horizontal', or 'center'"); + } + return 0; + } + + // todo block/sky light, dnc for now + + //endregion + + //region Instance Methods + + public int interpolate(LuaState state) { + int duration = state.checkIntegerArg(1); + if (duration <= 0) state.argError(1, "must be a positive integer"); + + LuaHelpers.tableForEach(state, 2, (key) -> { + if (!readInterpField(state, key, -1)) { + state.error("Unknown property for interpolation: " + key); + } + }); + + return 0; + } + + //endregion + + /* + + * GlowColorOverride + * Width/Height + * InterpolationDuration + * ShadowRadius + * ShadowStrength + * Transformation + + */ + +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaEntity.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaEntity.java new file mode 100644 index 000000000..a3ddcabfd --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaEntity.java @@ -0,0 +1,86 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.entity; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaProperty; +import net.hollowcube.luau.annotation.LuaType; +import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; +import net.minestom.server.entity.Entity; + +@LuaType +public class LuaEntity implements LuaEntity$luau { + + public static void push(LuaState state, LuaEntity entity) { + state.newUserData(entity); + state.getMetaTable(TYPE_NAME); + state.setMetaTable(-2); + } + + public static E checkArg(LuaState state, int index, Class type) { + var entity = (LuaEntity) state.checkUserDataArg(index, TYPE_NAME); + if (!type.isAssignableFrom(entity.getClass())) { + state.argError(index, "Expected " + type.getSimpleName() + + ", got " + entity.getClass().getSimpleName()); + } + return type.cast(entity); + } + + public static LuaEntity checkArg(LuaState state, int index) { + return (LuaEntity) state.checkUserDataArg(index, TYPE_NAME); + } + + private final Entity delegate; + + public LuaEntity(Entity delegate) { + this.delegate = delegate; + } + + protected Entity delegate() { + return this.delegate; + } + + public boolean readField(LuaState state, String key, int index) { + return switch (key) { + default -> false; + }; + } + + //region Properties + + @LuaProperty + public int getUuid(LuaState state) { + state.pushString(delegate.getUuid().toString()); + return 1; + } + + @LuaProperty + public int getPosition(LuaState state) { + LuaVectorTypeImpl.push(state, delegate().getPosition()); + return 1; + } + + @LuaProperty + public int getYaw(LuaState state) { + state.pushNumber(delegate().getPosition().yaw()); + return 1; + } + + @LuaProperty + public int getPitch(LuaState state) { + state.pushNumber(delegate().getPosition().pitch()); + return 1; + } + + //endregion + + //region Instance Methods + + public int remove(LuaState state) { + if (delegate.isRemoved()) + return 0; + delegate.remove(); + return 0; + } + + //endregion + +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaTextDisplayEntity.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaTextDisplayEntity.java new file mode 100644 index 000000000..971fb9728 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/LuaTextDisplayEntity.java @@ -0,0 +1,81 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.entity; + +import net.hollowcube.luau.LuaState; +import net.hollowcube.luau.annotation.LuaProperty; +import net.hollowcube.luau.annotation.LuaType; +import net.hollowcube.mapmaker.map.entity.impl.DisplayEntity; +import net.hollowcube.mapmaker.runtime.freeform.lua.base.LuaTextImpl; +import net.minestom.server.entity.Entity; + +@LuaType +public class LuaTextDisplayEntity extends LuaDisplayEntity implements LuaTextDisplayEntity$luau { + + public LuaTextDisplayEntity(Entity delegate) { + super(delegate); + } + + @Override + protected DisplayEntity.Text delegate() { + return (DisplayEntity.Text) super.delegate(); + } + + @Override + public boolean readField(LuaState state, String key, int index) { + // Note: Fields supporting interpolation should be added to readInterpField ONLY, not this method also. + return switch (key) { + case "Text" -> { + var text = LuaTextImpl.checkAnyTextArg(state, -1); + delegate().getEntityMeta().setText(text); + yield true; + } + default -> super.readField(state, key, index); + }; + } + + @Override + public boolean readInterpField(LuaState state, String key, int index) { + return switch (key) { + default -> super.readInterpField(state, key, index); + }; + } + + //region Properties + + @LuaProperty + public int getText(LuaState state) { + LuaTextImpl.push(state, delegate().getEntityMeta().getText()); + return 1; + } + + @LuaProperty + public int setText(LuaState state) { + var text = LuaTextImpl.checkAnyTextArg(state, 1); + delegate().getEntityMeta().setText(text); + return 0; + } + + @LuaProperty + public int getTextOpacity(LuaState state) { + state.pushNumber((delegate().getEntityMeta().getTextOpacity() & 0xFF) / 255f); + return 1; + } + + @LuaProperty + public int setTextOpacity(LuaState state) { + float opacity = (float) state.checkNumberArg(1); + if (opacity < 0f || opacity > 1f) + state.argError(1, "Expected number between 0 and 1 (inclusive)"); + delegate().getEntityMeta().setTextOpacity((byte) Math.round(opacity * 255.0f)); + return 0; + } + + //endregion + +// * Alignment +// * Background +// * DefaultBackground t/f +// * LineWidth +// * SeeThrough +// * Shadow +// * TextOpacity +} diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/package-info.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/package-info.java new file mode 100644 index 000000000..3d52592ba --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/entity/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.hollowcube.mapmaker.runtime.freeform.lua.entity; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java index a8b4e7404..a82e0d4bd 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java @@ -43,6 +43,11 @@ public int setEnabled(LuaState state) { boolean newValue = state.checkBooleanArg(1); if (newValue) sidebar.addViewer(player); else sidebar.removeViewer(player); + + for (int i = 0; i < 15; i++) { + sidebar.createLine(new Sidebar.ScoreboardLine("myid" + i, Component.text("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"), i, Sidebar.NumberFormat.blank())); + + } return 1; } @@ -59,4 +64,24 @@ public int setTitle(LuaState state) { return 1; } + // AddLine(text: AnyText, index: integer?) -> () + public int addLine(LuaState state) { + return 0; + } + + // SetLine(text: AnyText, index: integer) -> () + public int setLine(LuaState state) { + return 0; + } + + // RemoveLine(index: integer) -> () + public int removeLine(LuaState state) { + return 0; + } + + // Clear() -> () + public int clear(LuaState state) { + return 0; + } + } diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java index f9c697f5a..f7742ecee 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java @@ -3,8 +3,14 @@ import net.hollowcube.luau.LuaState; import net.hollowcube.luau.annotation.LuaProperty; import net.hollowcube.luau.annotation.LuaType; +import net.hollowcube.mapmaker.map.entity.impl.DisplayEntity; import net.hollowcube.mapmaker.runtime.freeform.FreeformMapWorld; +import net.hollowcube.mapmaker.runtime.freeform.lua.entity.LuaEntity; +import net.hollowcube.mapmaker.runtime.freeform.lua.entity.LuaTextDisplayEntity; import net.hollowcube.mapmaker.runtime.freeform.lua.math.LuaVectorTypeImpl; +import net.hollowcube.mapmaker.runtime.freeform.script.LuaHelpers; + +import java.util.UUID; @LuaType public class LuaWorld implements LuaWorld$luau { @@ -53,6 +59,27 @@ public int setBlock(LuaState state) { return 0; } + public int spawnEntity(LuaState state) { + var position = LuaVectorTypeImpl.checkArg(state, 1); // position + var typeName = state.checkStringArg(2); // entity type + if (!typeName.equals("text")) { + state.error("Only text entity is supported"); + } + + var entity = new DisplayEntity.Text(UUID.randomUUID()); + entity.setInstance(delegate.instance(), position); + var luaEntity = new LuaTextDisplayEntity(entity); + + LuaHelpers.tableForEach(state, 3, (key) -> { + if (!luaEntity.readField(state, key, -1)) { + state.argError(3, "Unknown property: " + key); + } + }); + + LuaEntity.push(state, luaEntity); + return 1; + } + //endregion } \ No newline at end of file diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java index 15c7e638b..663968e61 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java @@ -36,7 +36,7 @@ public static int fieldWriteOnly(LuaState state, String typeName, String key) { /// The state should be left _exactly_ as it was before the call (value at -1). public static void tableForEach(LuaState state, int tableIndex, Consumer func) { state.pushNil(); - while (state.next(tableIndex - 1)) { + while (state.next(tableIndex)) { // Key is at index -2, value is at index -1 String key = state.toString(-2); func.accept(key); @@ -129,7 +129,7 @@ public static JsonElement readJsonElement(LuaState state, int index) { case TABLE -> { // TODO: support arrays. var obj = new JsonObject(); - tableForEach(state, index, key -> obj.add(key, readJsonElement(state, -1))); + tableForEach(state, index - 1, key -> obj.add(key, readJsonElement(state, -1))); yield obj; } // todo support vector type, some userdata types, and buffer type (probably) diff --git a/scripts/button-clicker/player.luau b/scripts/button-clicker/player.luau index 07ac70786..9b2c8f963 100644 --- a/scripts/button-clicker/player.luau +++ b/scripts/button-clicker/player.luau @@ -1,7 +1,11 @@ local player = script.Parent +local world = script.World local BUTTON_POSITION = vec(0, 41, -5) +player.Sidebar.Enabled = true +player.Sidebar.Title = Text.new("Button Clicker") + function onButtonPress(blockPosition, block) if blockPosition ~= BUTTON_POSITION then return @@ -11,8 +15,13 @@ function onButtonPress(blockPosition, block) player.SaveData.buttonCount = buttonCount + 1 print("Pressed", player.SaveData.buttonCount, "times") - player.Sidebar.Enabled = true - player.Sidebar.Title = Text.new("Button Clicker") + local entity = world:SpawnEntity(BUTTON_POSITION, "text", { + Text = Text.new("+" .. (buttonCount + 1)), + }) + task.spawn(function() + task.wait(10) + entity:Remove() + end) end player.OnBlockInteract:Listen(onButtonPress) diff --git a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java index ffd7f1141..125bb11c1 100644 --- a/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java @@ -6,6 +6,7 @@ import net.hollowcube.luau.LuaState; import net.hollowcube.luau.annotation.LuaType; import net.hollowcube.luau.annotation.MetaType; +import org.jetbrains.annotations.Nullable; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Processor; @@ -13,17 +14,54 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; import java.io.IOException; -import java.util.ArrayList; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; @AutoService(Processor.class) public class LuaSlopgenProcessor extends AbstractProcessor { + private static @Nullable TypeName getBaseType(Types types, TypeElement typeElement) { + TypeElement current = typeElement; + TypeElement topmost = current; + + while (current != null) { + TypeMirror superclass = current.getSuperclass(); + + // Check if we've reached Object or a type that has no superclass + if (superclass.getKind() == TypeKind.NONE) { + break; + } + + TypeElement superElement = (TypeElement) types.asElement(superclass); + + // If superclass is java.lang.Object, stop here + if (superElement.getQualifiedName().toString().equals("java.lang.Object")) { + break; + } + + topmost = superElement; + current = superElement; + } + + if (topmost == typeElement) return null; + return TypeName.get(topmost.asType()); + } + + private static void addCheck( + MethodSpec.Builder method, String name, int index, + TypeName targetType, TypeName annotatedType, + @Nullable TypeName superType, @Nullable TypeName baseType) { + if (superType != null) { + method.addStatement("$T $L = $T.checkArg(state, $L, $T.class)", targetType, name, baseType, index, annotatedType); + } else { + method.addStatement("$T $L = $T.checkArg(state, $L)", targetType, name, annotatedType, index); + } + } + @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { var messager = processingEnv.getMessager(); @@ -46,9 +84,19 @@ public boolean process(Set annotations, RoundEnvironment var glueTypeBuilder = TypeSpec.interfaceBuilder(glueTypeName) .addModifiers(Modifier.PUBLIC); + var superType = ClassName.get(typeElement.getSuperclass()); + if (superType.equals(TypeName.get(Object.class))) superType = null; + var baseType = getBaseType(processingEnv.getTypeUtils(), typeElement); + + TypeName glueSuperType = null; + if (superType != null) { + glueSuperType = ClassName.get(packageName, ((ClassName) superType).simpleName() + "$luau"); + glueTypeBuilder.addSuperinterface(glueSuperType); + } + var annotatedType = TypeName.get(typeElement.asType()); var targetType = luaTypeMirrorValues.containsKey("implFor") - ? (TypeMirror) luaTypeMirrorValues.get("implFor").getValue() + ? TypeName.get((TypeMirror) luaTypeMirrorValues.get("implFor").getValue()) : annotatedType; var targetName = luaTypeMirrorValues.containsKey("name") ? (String) luaTypeMirrorValues.get("name").getValue() @@ -56,6 +104,8 @@ public boolean process(Set annotations, RoundEnvironment var luaHelpersType = ClassName.get("net.hollowcube.mapmaker.runtime.freeform.script", "LuaHelpers"); + var needsMetaProxies = targetType.equals(annotatedType); + var handles = new ArrayList(); new LuaHandleCollector(messager, docTrees).visit(typeElement, handles); var getterSetterNames = handles.stream() @@ -65,14 +115,17 @@ public boolean process(Set annotations, RoundEnvironment .toList(); // Add constant with metatable/type name - glueTypeBuilder.addField(FieldSpec.builder(String.class, "TYPE_NAME", - Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .initializer("$S", targetName) - .build()); + if (superType == null) { + glueTypeBuilder.addField(FieldSpec.builder(String.class, "TYPE_NAME", + Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("$S", targetName) + .build()); + } boolean foundEqImpl = false, foundToStringImpl = false; - { // Init Method + // Init Method + if (superType == null) { var initMethod = MethodSpec.methodBuilder("init$luau") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addParameter(LuaState.class, "state") @@ -109,11 +162,14 @@ public boolean process(Set annotations, RoundEnvironment initMethod.addStatement("state.setField(-2, $S)", "__tostring"); } // Always add __index, __newindex, __namecall to the glue implementation - initMethod.addStatement("state.pushCFunction($T::luaIndex, $S)", glueTypeName, "__index"); + initMethod.addStatement("state.pushCFunction($T::$L, $S)", glueTypeName, + needsMetaProxies ? "luaIndex$proxy" : "luaIndex", "__index"); initMethod.addStatement("state.setField(-2, $S)", "__index"); - initMethod.addStatement("state.pushCFunction($T::luaNewIndex, $S)", glueTypeName, "__newindex"); + initMethod.addStatement("state.pushCFunction($T::$L, $S)", glueTypeName, + needsMetaProxies ? "luaNewIndex$proxy" : "luaNewIndex", "__newindex"); initMethod.addStatement("state.setField(-2, $S)", "__newindex"); - initMethod.addStatement("state.pushCFunction($T::luaNameCall, $S)", glueTypeName, "__namecall"); + initMethod.addStatement("state.pushCFunction($T::$L, $S)", glueTypeName, + needsMetaProxies ? "luaNameCall$proxy" : "luaNameCall", "__namecall"); initMethod.addStatement("state.setField(-2, $S)", "__namecall"); initMethod.addStatement("state.pop(1)"); // Pop the metatable @@ -139,38 +195,52 @@ public boolean process(Set annotations, RoundEnvironment initMethod.addStatement("state.setGlobal(TYPE_NAME)"); glueTypeBuilder.addMethod(initMethod.build()); + + // Insert the proxies for luaIndex, luaNewIndex, luaNameCall if not using a type impl + if (needsMetaProxies) { + for (var proxy : List.of("luaIndex", "luaNewIndex", "luaNameCall")) { + var method = MethodSpec.methodBuilder(proxy + "$proxy") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT); + addCheck(method, "self", 1, targetType, annotatedType, superType, baseType); + method.addStatement("return (($T) self).$L(state)", glueTypeName, proxy); + glueTypeBuilder.addMethod(method.build()); + } + } } // If we didnt find eq or toString impls, add the defaults - if (!foundEqImpl) { - glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaEq") + if (superType == null && !foundEqImpl) { + var method = MethodSpec.methodBuilder("luaEq") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addParameter(LuaState.class, "state") - .returns(TypeName.INT) - .addStatement("$T lhs = $T.checkArg(state, 1)", targetType, annotatedType) - .addStatement("$T rhs = $T.checkArg(state, 2)", targetType, annotatedType) - .addStatement("state.pushBoolean($T.equals(lhs, rhs))", Objects.class) - .addStatement("return 1") - .build()); + .returns(TypeName.INT); + addCheck(method, "lhs", 1, targetType, annotatedType, superType, baseType); + addCheck(method, "rhs", 2, targetType, annotatedType, superType, baseType); + method.addStatement("state.pushBoolean($T.equals(lhs, rhs))", Objects.class) + .addStatement("return 1"); + glueTypeBuilder.addMethod(method.build()); } - if (!foundToStringImpl) { - glueTypeBuilder.addMethod(MethodSpec.methodBuilder("luaToString") + if (superType == null && !foundToStringImpl) { + var method = MethodSpec.methodBuilder("luaToString") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addParameter(LuaState.class, "state") - .returns(TypeName.INT) - .addStatement("$T obj = $T.checkArg(state, 1)", targetType, annotatedType) - .addStatement("state.pushString($T.toString(obj))", Objects.class) - .addStatement("return 1") - .build()); + .returns(TypeName.INT); + addCheck(method, "obj", 1, targetType, annotatedType, superType, baseType); + method.addStatement("state.pushString($T.toString(obj))", Objects.class) + .addStatement("return 1"); + + glueTypeBuilder.addMethod(method.build()); } { // Generate __index metamethod impl var indexMethod = MethodSpec.methodBuilder("luaIndex") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addModifiers(Modifier.PUBLIC, needsMetaProxies ? Modifier.DEFAULT : Modifier.STATIC) .addParameter(LuaState.class, "state") .returns(TypeName.INT); - indexMethod.addStatement("$T self = $T.checkArg(state, 1)", targetType, annotatedType); + addCheck(indexMethod, "self", 1, targetType, annotatedType, superType, baseType); indexMethod.addStatement("$T key = state.checkStringArg(2)", String.class); indexMethod.beginControlFlow("return switch (key)"); @@ -198,20 +268,25 @@ public boolean process(Set annotations, RoundEnvironment } // If the class provides its own index metamethod, call that as the default case + // Only search for the proxy in the base class, otherwise we call the super as the default case. boolean foundIndexProxy = false; - for (var method : handles) { - if (method.metaType() != MetaType.INDEX || method.isLuaStatic()) - continue; - - foundIndexProxy = true; - indexMethod.addCode("default -> "); - if (method.isStatic()) { - indexMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); - } else { - indexMethod.addStatement("self.$L(state)", method.methodName()); + if (superType == null) { + for (var method : handles) { + if (method.metaType() != MetaType.INDEX || method.isLuaStatic()) + continue; + + foundIndexProxy = true; + indexMethod.addCode("default -> "); + if (method.isStatic()) { + indexMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + indexMethod.addStatement("self.$L(state)", method.methodName()); + } } } - if (!foundIndexProxy) { + if (superType != null) { + indexMethod.addStatement("default -> $T.super.luaIndex(state)", glueSuperType); + } else if (!foundIndexProxy) { indexMethod.addStatement("default -> $T.noSuchKey(state, TYPE_NAME, key)", luaHelpersType); } @@ -220,11 +295,11 @@ public boolean process(Set annotations, RoundEnvironment } { // Generate __newindex metamethod impl var newIndexMethod = MethodSpec.methodBuilder("luaNewIndex") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addModifiers(Modifier.PUBLIC, needsMetaProxies ? Modifier.DEFAULT : Modifier.STATIC) .addParameter(LuaState.class, "state") .returns(TypeName.INT); - newIndexMethod.addStatement("$T self = $T.checkArg(state, 1)", targetType, annotatedType); + addCheck(newIndexMethod, "self", 1, targetType, annotatedType, superType, baseType); newIndexMethod.addStatement("$T key = state.checkStringArg(2)", String.class); newIndexMethod.beginControlFlow("return switch (key)"); @@ -258,20 +333,25 @@ public boolean process(Set annotations, RoundEnvironment } // If the class provides its own index metamethod, call that as the default case + // Only search for the proxy in the base class, otherwise we call the super as the default case. boolean foundNewIndexProxy = false; - for (var method : handles) { - if (method.metaType() != MetaType.NEWINDEX || method.isLuaStatic()) - continue; - - foundNewIndexProxy = true; - newIndexMethod.addCode("default -> "); - if (method.isStatic()) { - newIndexMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); - } else { - newIndexMethod.addStatement("self.$L(state)", method.methodName()); + if (superType == null) { + for (var method : handles) { + if (method.metaType() != MetaType.NEWINDEX || method.isLuaStatic()) + continue; + + foundNewIndexProxy = true; + newIndexMethod.addCode("default -> "); + if (method.isStatic()) { + newIndexMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + newIndexMethod.addStatement("self.$L(state)", method.methodName()); + } } } - if (!foundNewIndexProxy) { + if (superType != null) { + newIndexMethod.addStatement("default -> $T.super.luaNewIndex(state)", glueSuperType); + } else if (!foundNewIndexProxy) { newIndexMethod.addStatement("default -> $T.noSuchKey(state, TYPE_NAME, key)", luaHelpersType); } @@ -280,11 +360,11 @@ public boolean process(Set annotations, RoundEnvironment } { // Generate __namecall metamethod impl var nameCallMethod = MethodSpec.methodBuilder("luaNameCall") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addModifiers(Modifier.PUBLIC, needsMetaProxies ? Modifier.DEFAULT : Modifier.STATIC) .addParameter(LuaState.class, "state") .returns(TypeName.INT); - nameCallMethod.addStatement("$T self = $T.checkArg(state, 1)", targetType, annotatedType); + addCheck(nameCallMethod, "self", 1, targetType, annotatedType, superType, baseType); nameCallMethod.addStatement("$T methodName = state.nameCallAtom()", String.class); nameCallMethod.beginControlFlow("return switch (methodName)"); @@ -293,7 +373,7 @@ public boolean process(Set annotations, RoundEnvironment if (method.metaType() != null || method.isLuaStatic() || method.isProperty()) continue; - nameCallMethod.addCode("case $S -> ", method.methodName()); + nameCallMethod.addCode("case $S -> ", method.methodName().substring(0, 1).toUpperCase(Locale.ROOT) + method.methodName().substring(1)); if (method.isStatic()) { nameCallMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); } else { @@ -307,20 +387,25 @@ public boolean process(Set annotations, RoundEnvironment } // If the class provides its own namecall metamethod, call that as the default case + // Only search for the proxy in the base class, otherwise we call the super as the default case. boolean foundNameCallProxy = false; - for (var method : handles) { - if (method.metaType() != MetaType.NAMECALL || method.isLuaStatic()) - continue; - - foundNameCallProxy = true; - nameCallMethod.addCode("default -> "); - if (method.isStatic()) { - nameCallMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); - } else { - nameCallMethod.addStatement("self.$L(state)", method.methodName()); + if (superType == null) { + for (var method : handles) { + if (method.metaType() != MetaType.NAMECALL || method.isLuaStatic()) + continue; + + foundNameCallProxy = true; + nameCallMethod.addCode("default -> "); + if (method.isStatic()) { + nameCallMethod.addStatement("$T.$L(state)", method.owningType(), method.methodName()); + } else { + nameCallMethod.addStatement("self.$L(state)", method.methodName()); + } } } - if (!foundNameCallProxy) { + if (superType != null) { + nameCallMethod.addStatement("default -> $T.super.luaNameCall(state)", glueSuperType); + } else if (!foundNameCallProxy) { nameCallMethod.addStatement("default -> $T.noSuchMethod(state, TYPE_NAME, methodName)", luaHelpersType); }