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 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/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..074801675 100644 --- a/bin/map-isolate/build.gradle.kts +++ b/bin/map-isolate/build.gradle.kts @@ -1,12 +1,7 @@ plugins { id("mapmaker.java-binary") id("mapmaker.packer-data") - id("org.graalvm.buildtools.native") version "0.10.6" -} - -repositories { - mavenLocal() - mavenCentral() + id("org.graalvm.buildtools.native") version "0.11.0" } dependencies { @@ -50,6 +45,7 @@ tasks.nativeCompile { application { mainClass = "net.hollowcube.mapmaker.isolate.IsolateMain" + applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") } graalvmNative { @@ -59,6 +55,11 @@ graalvmNative { buildArgs( listOf( "--enable-native-access=ALL-UNNAMED", "--enable-monitoring=jfr", + "-H:+UnlockExperimentalVMOptions", "-H:+ForeignAPISupport", + "-H:+MLProfileInferenceUseGNNModel", + + "-H:+UseCompressedReferences", + "--features=net.hollowcube.nativeimage.HCNativeImageFeature", "--static-nolibc", "--no-fallback", "--emit build-report", diff --git a/bin/map-isolate/deploy/Dockerfile-native b/bin/map-isolate/deploy/Dockerfile-native index 3b6315914..e8b66a2f0 100644 --- a/bin/map-isolate/deploy/Dockerfile-native +++ b/bin/map-isolate/deploy/Dockerfile-native @@ -1,4 +1,5 @@ -FROM gcr.io/distroless/base +#FROM gcr.io/distroless/cc +FROM debian:trixie-slim USER 65532:65532 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..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 @@ -4,9 +4,14 @@ 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.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; import net.kyori.adventure.text.Component; @@ -21,7 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.UUID; import static net.hollowcube.mapmaker.map.MapPlayer.simpleMapPlayer; @@ -29,12 +35,14 @@ 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 // 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); @@ -42,13 +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)); + + var scriptsDirectory = Path.of("../../scripts"); + this.scriptLoader = Files.exists(scriptsDirectory) + ? new LocalFsLoader(scriptsDirectory) + : new ResourcesLoader(); + logger.info("Using script loader: {}", this.scriptLoader); } @Override @@ -78,7 +91,7 @@ protected void prepareStart() { try { var map = mapService().getMap(Uuids.ZERO, this.mapId); - world = new ParkourMapWorld(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/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 52557d166..0e8774f27 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,9 @@ blossom = "2.1.0" velocity = "3.4.0-SNAPSHOT" 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" } @@ -50,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" } @@ -63,6 +67,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-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" } prometheus-httpserver = { group = "io.prometheus", name = "simpleclient_httpserver", version.ref = "prometheus" } @@ -86,6 +96,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-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/idea/Luau-Formatter.ipynb b/idea/Luau-Formatter.ipynb new file mode 100644 index 000000000..d20ba5fdd --- /dev/null +++ b/idea/Luau-Formatter.ipynb @@ -0,0 +1,159 @@ +{ + "cells": [ + { + "cell_type": "code", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-09-22T02:14:42.446744Z", + "start_time": "2025-09-22T02:14:42.237492Z" + } + }, + "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": 8 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-17T01:48:46.144155Z", + "start_time": "2025-09-17T01:48:46.021185Z" + } + }, + "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": 4 + } + ], + "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/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..309d6335f --- /dev/null +++ b/modules/common/src/main/java/net/hollowcube/common/util/StringUtil.java @@ -0,0 +1,32 @@ +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(); + } + + 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/build.gradle.kts b/modules/map-runtime/build.gradle.kts index 7da723033..9e0cf9048 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") } @@ -8,12 +11,54 @@ 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) implementation(libs.bundles.adventure) + implementation(libs.bundles.luau) + 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/FreeformMapWorld.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java new file mode 100644 index 000000000..61cba1865 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformMapWorld.java @@ -0,0 +1,171 @@ +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.*; +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.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; +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; +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; +import java.util.UUID; + +public class FreeformMapWorld extends AbstractMapWorld { + + public static final LuauCompiler LUAU_COMPILER = LuauCompiler.builder() + .userdataTypes() // todo + .vectorType("vector") + .vectorCtor("vec") + .build(); + private static final Logger log = LoggerFactory.getLogger(FreeformMapWorld.class); + + private final ScriptBundle scriptBundle; + + private final LuaState globalState; + private final List worldThreads = new ArrayList<>(); + + public FreeformMapWorld(MapServer server, MapData map, ScriptBundle.Loader scriptLoader) { + super(server, map, makeMapInstance(map, 'f'), FreeformState.class); + + var scriptBundle = scriptLoader.load(map.id()); + this.scriptBundle = Objects.requireNonNull(scriptBundle, "Failed to load script bundle for map " + map.id()); + + this.globalState = createGlobalState(); + } + + public ScriptBundle scriptBundle() { + return this.scriptBundle; + } + + public LuaState globalState() { + return this.globalState; + } + + //region World Lifecycle + + @Override + public void loadWorld() { + super.loadWorld(); + + 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); + } + } + } + + @Override + public void close() { + super.close(); + + this.worldThreads.forEach(LuaScriptState::close); + this.globalState.close(); + } + + //endregion + + //region Player Lifecycle + + @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(saveState, new ArrayList<>()); + } + + + @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); + LuaTextImpl$luau.init$luau(global); + + LuaEventSource.init(global); + LuaBlockImpl$luau.init$luau(global); + LuaWorld$luau.init$luau(global); +// LuaParticle.init(global); + LuaEntity$luau.init$luau(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/FreeformState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java new file mode 100644 index 000000000..6b331990a --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/FreeformState.java @@ -0,0 +1,112 @@ +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.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; +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(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 + 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"); + + 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..a87294158 --- /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.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); + + 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/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/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); + } + }; + } +} 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..ed3e6bd9a --- /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, PLAYER + } + } + + 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/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/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..8527a77ff --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/LuaTask.java @@ -0,0 +1,116 @@ +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.Task; +import net.minestom.server.timer.TaskSchedule; + +import java.util.Map; +import java.util.function.Supplier; + +// @LuaLib("task") +public class LuaTask { + private static final String NAME = "task"; + + public static void init(LuaState state) { + state.registerLib(NAME, Map.of( + "spawn", LuaTask::spawn, + "cancel", LuaTask::cancel, + "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); + task.selfRef = luaState.world().scheduler().submitTask(task); + + // TODO: This ref is a straight memory leak + state.ref(-1); // Store the thread in the registry + + // 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; + } + + 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 Task selfRef; + + 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/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..ccd70e989 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/api-sketching.md @@ -0,0 +1,397 @@ +## 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 + * `tostring` to serialize to plain text + * Instance methods +* `Direction` - `Direction.North`, `.South`, etc +* `Slot` - Special slot constants, eg `Slot.MainHand`, `.Saddle`, etc + * Could probably be tagged light userdata constants + +## 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 +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 + +* Item + * 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 + +#### 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)` +* `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 + * `Enabled` - Get or set whether the sidebar is shown + * `Title` - Get or set the sidebar title + * `Clear()` - Remove all lines and reset title + * `AddLine(line: AnyText, index: integer?)` - Appends a line at the index (or end) + * `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 }?)` +* `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/LuaTextImpl.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaTextImpl.java new file mode 100644 index 000000000..3f681bb5f --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/base/LuaTextImpl.java @@ -0,0 +1,125 @@ +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 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. + // 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); + } + + 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. + /// + /// ```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.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); + 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/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/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/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..daa7d3c65 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaPlayer.java @@ -0,0 +1,99 @@ +package net.hollowcube.mapmaker.runtime.freeform.lua.player; + +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.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; + +@LuaType +public class LuaPlayer implements LuaPlayer$luau { + + public static void push(LuaState state, LuaPlayer entity) { + state.newUserData(entity); + state.getMetaTable(TYPE_NAME); + state.setMetaTable(-2); + } + + public static LuaPlayer checkArg(LuaState state, int index) { + 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; + + LuaHelpers.pushJsonElement(state, saveData); + this.saveDataRef = state.ref(-1); // todo dont leak this :) + state.pop(1); + } + + @LuaProperty + public int getUuid(LuaState state) { + state.pushString(player.getUuid().toString()); + return 1; + } + + @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; + } + + //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()); + LuaBlockImpl.push(eventState, event.getBlock()); + return 2; + } + )); + return 1; + } + + //endregion + +} 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..a82e0d4bd --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/player/LuaSidebar.java @@ -0,0 +1,87 @@ +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); + + for (int i = 0; i < 15; i++) { + sidebar.createLine(new Sidebar.ScoreboardLine("myid" + i, Component.text("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"), i, Sidebar.NumberFormat.blank())); + + } + 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; + } + + // 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/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/lua/world/LuaBlockImpl.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlockImpl.java new file mode 100644 index 000000000..2ec1b1778 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaBlockImpl.java @@ -0,0 +1,95 @@ +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.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; + +@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(TYPE_NAME); + state.setMetaTable(-2); + } + + public static Block checkArg(LuaState state, int index) { + return (Block) state.checkUserDataArg(index, TYPE_NAME); + } + + //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; + } + + //endregion + + //region Meta Methods + + @LuaMeta(MetaType.CALL) + public 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; + } + } + + @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 new file mode 100644 index 000000000..f7742ecee --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/lua/world/LuaWorld.java @@ -0,0 +1,85 @@ +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.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 { + + public static void push(LuaState state, LuaWorld entity) { + state.newUserData(entity); + state.getMetaTable(TYPE_NAME); + state.setMetaTable(-2); + } + + public static LuaWorld checkArg(LuaState state, int index) { + return (LuaWorld) state.checkUserDataArg(index, TYPE_NAME); + } + + private final FreeformMapWorld delegate; + + public LuaWorld(FreeformMapWorld world) { + this.delegate = world; + } + + //region Instance Properties + + @LuaProperty + public int getUuid(LuaState state) { + state.pushString(delegate.map().id()); + return 1; + } + + //endregion + + //region Instance Methods + + public int getBlock(LuaState state) { + var blockPosition = LuaVectorTypeImpl.checkArg(state, 1); + + var block = delegate.instance().getBlock(blockPosition); + LuaBlockImpl.push(state, block); + return 1; + } + + public int setBlock(LuaState state) { + var blockPosition = LuaVectorTypeImpl.checkArg(state, 1); + var block = LuaBlockImpl.checkArg(state, 2); + + delegate.instance().setBlock(blockPosition, block); + 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/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..663968e61 --- /dev/null +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/freeform/script/LuaHelpers.java @@ -0,0 +1,143 @@ +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; +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 + } + + 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). + /// + /// 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)) { + // 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); + } + } + + 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 - 1, 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/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 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 new file mode 100644 index 000000000..e4ee57e13 --- /dev/null +++ b/scripts/button-clicker/map.json @@ -0,0 +1,13 @@ +{ + "id": "3080cf33-8ff9-4d3e-a469-a457a896ab3d", + "entrypoints": [ + { + "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..9b2c8f963 --- /dev/null +++ b/scripts/button-clicker/player.luau @@ -0,0 +1,27 @@ +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 + end + + local buttonCount = player.SaveData.buttonCount or 0 + player.SaveData.buttonCount = buttonCount + 1 + print("Pressed", player.SaveData.buttonCount, "times") + + 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/scripts/button-clicker/world.luau b/scripts/button-clicker/world.luau new file mode 100644 index 000000000..e94acca77 --- /dev/null +++ b/scripts/button-clicker/world.luau @@ -0,0 +1 @@ +print("hello, world!") 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() 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/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/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..7ecf1968d --- /dev/null +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandle.java @@ -0,0 +1,13 @@ +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, + 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 new file mode 100644 index 000000000..698115aac --- /dev/null +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaHandleCollector.java @@ -0,0 +1,63 @@ +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.LuaProperty; +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), + e.getAnnotation(LuaProperty.class) != null, + 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..125bb11c1 --- /dev/null +++ b/tools/lua-slopgen/src/main/java/net/hollowcube/slopgen/LuaSlopgenProcessor.java @@ -0,0 +1,439 @@ +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 org.jetbrains.annotations.Nullable; + +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.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import java.io.IOException; +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(); + 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) + .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") + ? TypeName.get((TypeMirror) luaTypeMirrorValues.get("implFor").getValue()) + : annotatedType; + var targetName = luaTypeMirrorValues.containsKey("name") + ? (String) luaTypeMirrorValues.get("name").getValue() + : typeElement.getSimpleName().toString().replace("Lua", ""); + + 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() + .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 + 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 + if (superType == null) { + 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::$L, $S)", glueTypeName, + needsMetaProxies ? "luaIndex$proxy" : "luaIndex", "__index"); + initMethod.addStatement("state.setField(-2, $S)", "__index"); + 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::$L, $S)", glueTypeName, + needsMetaProxies ? "luaNameCall$proxy" : "luaNameCall", "__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()); + + // 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 (superType == null && !foundEqImpl) { + var method = MethodSpec.methodBuilder("luaEq") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .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 (superType == null && !foundToStringImpl) { + var method = MethodSpec.methodBuilder("luaToString") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(LuaState.class, "state") + .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, needsMetaProxies ? Modifier.DEFAULT : Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT); + + addCheck(indexMethod, "self", 1, targetType, annotatedType, superType, baseType); + 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 + // Only search for the proxy in the base class, otherwise we call the super as the default case. + boolean foundIndexProxy = false; + 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 (superType != null) { + indexMethod.addStatement("default -> $T.super.luaIndex(state)", glueSuperType); + } else 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, needsMetaProxies ? Modifier.DEFAULT : Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT); + + addCheck(newIndexMethod, "self", 1, targetType, annotatedType, superType, baseType); + 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 + // Only search for the proxy in the base class, otherwise we call the super as the default case. + boolean foundNewIndexProxy = false; + 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 (superType != null) { + newIndexMethod.addStatement("default -> $T.super.luaNewIndex(state)", glueSuperType); + } else 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, needsMetaProxies ? Modifier.DEFAULT : Modifier.STATIC) + .addParameter(LuaState.class, "state") + .returns(TypeName.INT); + + addCheck(nameCallMethod, "self", 1, targetType, annotatedType, superType, baseType); + 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().substring(0, 1).toUpperCase(Locale.ROOT) + method.methodName().substring(1)); + 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 + // Only search for the proxy in the base class, otherwise we call the super as the default case. + boolean foundNameCallProxy = false; + 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 (superType != null) { + nameCallMethod.addStatement("default -> $T.super.luaNameCall(state)", glueSuperType); + } else if (!foundNameCallProxy) { + nameCallMethod.addStatement("default -> $T.noSuchMethod(state, TYPE_NAME, methodName)", luaHelpersType); + } + + nameCallMethod.addCode("$<};"); + glueTypeBuilder.addMethod(nameCallMethod.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; + } +} 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);