From 6b6c4a91b8d42e825f0c0bfa3cb9b17b08db76ed Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Sat, 25 Apr 2020 09:33:17 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 35 ++ CHANGELOG.md | 4 + Dockerfile | 4 + Jenkinsfile | 22 + LICENSE | 21 + README.md | 5 + build.gradle | 218 ++++++++ cmake/linux_i686_toolchain.cmake | 7 + cmake/windows_i686_toolchain.cmake | 14 + cmake/windows_x86_64_toolchain.cmake | 14 + gradle.properties | 21 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 188 +++++++ gradlew.bat | 100 ++++ scripts/download-jni-headers.sh | 33 ++ scripts/download-quickjs.sh | 17 + scripts/setup.sh | 6 + settings.gradle | 12 + src/main/c/CMakeLists.txt | 31 ++ src/main/c/asprintf.h | 62 +++ ...hebrokenrail_scriptcraft_quickjs_QuickJS.c | 464 ++++++++++++++++++ ...hebrokenrail_scriptcraft_quickjs_QuickJS.h | 47 ++ src/main/c/console.c | 119 +++++ src/main/c/console.h | 5 + .../com/thebrokenrail/scriptcraft/Demo.java | 15 + .../scriptcraft/ScriptCraft.java | 35 ++ .../scriptcraft/api/CustomBlock.java | 28 ++ .../scriptcraft/api/CustomItem.java | 40 ++ .../bridge/BlockSettingsBridges.java | 35 ++ .../scriptcraft/bridge/BlockStateBridges.java | 12 + .../scriptcraft/bridge/Bridge.java | 5 + .../scriptcraft/bridge/Bridges.java | 41 ++ .../bridge/DamageSourceBridges.java | 23 + .../scriptcraft/bridge/EntityBridges.java | 49 ++ .../bridge/ItemSettingsBridges.java | 28 ++ .../scriptcraft/bridge/ItemStackBridges.java | 13 + .../bridge/LivingEntityBridges.java | 11 + .../scriptcraft/bridge/RegistryBridge.java | 26 + .../scriptcraft/bridge/WorldBridges.java | 27 + .../scriptcraft/quickjs/JSException.java | 7 + .../scriptcraft/quickjs/QuickJS.java | 147 ++++++ .../scriptcraft/quickjs/QuickJSManager.java | 189 +++++++ .../util/MinecraftAPIEntrypoint.java | 18 + .../scriptcraft/util/OSUtil.java | 56 +++ .../util/ScriptCraftEntrypoint.java | 11 + .../thebrokenrail/scriptcraft/util/Util.java | 30 ++ .../resources/assets/scriptcraft/icon.png | Bin 0 -> 453 bytes src/main/resources/fabric.mod.json | 32 ++ src/main/resources/scriptcraft/.prettierrc | 8 + .../resources/scriptcraft/minecraft/block.ts | 158 ++++++ .../resources/scriptcraft/minecraft/core.ts | 236 +++++++++ .../resources/scriptcraft/minecraft/entity.ts | 233 +++++++++ .../resources/scriptcraft/minecraft/index.ts | 6 + .../resources/scriptcraft/minecraft/item.ts | 221 +++++++++ .../scriptcraft/minecraft/registry.ts | 37 ++ .../resources/scriptcraft/minecraft/world.ts | 58 +++ src/main/resources/scriptcraft/test/index.ts | 41 ++ src/main/resources/scriptcraft/tsconfig.json | 20 + src/main/resources/scriptcraft/typedoc.json | 7 + .../scriptcraft/types/scriptcraft/index.d.ts | 28 ++ 61 files changed, 3386 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 Jenkinsfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 cmake/linux_i686_toolchain.cmake create mode 100644 cmake/windows_i686_toolchain.cmake create mode 100644 cmake/windows_x86_64_toolchain.cmake create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100755 scripts/download-jni-headers.sh create mode 100755 scripts/download-quickjs.sh create mode 100755 scripts/setup.sh create mode 100644 settings.gradle create mode 100644 src/main/c/CMakeLists.txt create mode 100644 src/main/c/asprintf.h create mode 100644 src/main/c/com_thebrokenrail_scriptcraft_quickjs_QuickJS.c create mode 100644 src/main/c/com_thebrokenrail_scriptcraft_quickjs_QuickJS.h create mode 100644 src/main/c/console.c create mode 100644 src/main/c/console.h create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/Demo.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/ScriptCraft.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/api/CustomBlock.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/api/CustomItem.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/BlockSettingsBridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/BlockStateBridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/Bridge.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/Bridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/DamageSourceBridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/EntityBridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/ItemSettingsBridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/ItemStackBridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/LivingEntityBridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/RegistryBridge.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/bridge/WorldBridges.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/quickjs/JSException.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/quickjs/QuickJS.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/quickjs/QuickJSManager.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/util/MinecraftAPIEntrypoint.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/util/OSUtil.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/util/ScriptCraftEntrypoint.java create mode 100644 src/main/java/com/thebrokenrail/scriptcraft/util/Util.java create mode 100644 src/main/resources/assets/scriptcraft/icon.png create mode 100644 src/main/resources/fabric.mod.json create mode 100644 src/main/resources/scriptcraft/.prettierrc create mode 100644 src/main/resources/scriptcraft/minecraft/block.ts create mode 100644 src/main/resources/scriptcraft/minecraft/core.ts create mode 100644 src/main/resources/scriptcraft/minecraft/entity.ts create mode 100644 src/main/resources/scriptcraft/minecraft/index.ts create mode 100644 src/main/resources/scriptcraft/minecraft/item.ts create mode 100644 src/main/resources/scriptcraft/minecraft/registry.ts create mode 100644 src/main/resources/scriptcraft/minecraft/world.ts create mode 100644 src/main/resources/scriptcraft/test/index.ts create mode 100644 src/main/resources/scriptcraft/tsconfig.json create mode 100644 src/main/resources/scriptcraft/typedoc.json create mode 100644 src/main/resources/scriptcraft/types/scriptcraft/index.d.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c42fe09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# fabric + +run/ + +remappedSrc/ + +src/main/c/build-* +src/main/c/quickjs +src/main/c/jni + +scripts/jdk.tar.gz +scripts/jdk +scripts/quickjs.tar.xz \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..141f1e4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +**1.0** +* Initial Release diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d4d4d3a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:8-jdk-alpine + +RUN apk add --no-cache npm +RUN npm install -g typescript typedoc \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..d0f5182 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,22 @@ +pipeline { + agent { + dockerfile true + } + stages { + stage('Setup') { + steps { + sh 'cd scripts; ./setup.sh' + } + } + stage('Build') { + steps { + sh './gradlew build' + } + post { + success { + archiveArtifacts artifacts: 'build/libs/*', fingerprint: true + } + } + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..886b254 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 TheBrokenRail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fffafed --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# ScriptCraft +JS API for Minecraft + +## Changelog +[View Changelog](CHANGELOG.md) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5fd4d02 --- /dev/null +++ b/build.gradle @@ -0,0 +1,218 @@ +plugins { + id 'fabric-loom' version '0.2.7-SNAPSHOT' + id 'com.matthewprenger.cursegradle' version '1.4.0' +} + +def cDir = new File(rootProject.projectDir.absolutePath, 'src/main/c') + +class JNIPlatform { + String name + String[] cmakeArgs + String libExtension + + JNIPlatform(String name, String[] cmakeArgs, String libExtension) { + this.name = name + this.cmakeArgs = cmakeArgs + this.libExtension = libExtension + } +} + +JNIPlatform[] jniPlatforms = [ + new JNIPlatform("linux-x86_64", [] as String[], ".so"), + new JNIPlatform("linux-i686", ["-DCMAKE_TOOLCHAIN_FILE=${rootDir.absolutePath}/cmake/linux_i686_toolchain.cmake"] as String[], ".so"), + new JNIPlatform("windows-x86_64", ["-DCMAKE_TOOLCHAIN_FILE=${rootDir.absolutePath}/cmake/windows_x86_64_toolchain.cmake"] as String[], ".dll"), + new JNIPlatform("windows-i686", ["-DCMAKE_TOOLCHAIN_FILE=${rootDir.absolutePath}/cmake/windows_i686_toolchain.cmake"] as String[], ".dll") +] + +for (JNIPlatform platform : jniPlatforms) { + def buildDir = new File(cDir, "build-${platform.name}") + if (!buildDir.exists()) { + buildDir.mkdir() + } + + tasks.create(name: "cmake-${platform.name}", type: Exec) { + workingDir buildDir + + executable 'cmake' + args platform.cmakeArgs + ['..'] + } + + tasks.create(name: "compileJNI-${platform.name}", type: Exec) { + workingDir buildDir + + executable 'make' + + dependsOn tasks.getByName("cmake-${platform.name}") + } + + processResources.dependsOn tasks.getByName("compileJNI-${platform.name}") + + tasks.create(name: "cleanJNI-${platform.name}", type: Delete) { + delete buildDir.absolutePath + } + + clean.dependsOn tasks.getByName("cleanJNI-${platform.name}") +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +archivesBaseName = project.archives_base_name +def mod_version = project.mod_version as Object +version = "${mod_version}+${project.minecraft_version}" +group = project.maven_group as Object + +minecraft { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.minecraft_version}+build.${project.yarn_build}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.fabric_loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" +} + +def typescriptOut = new File(buildDir, 'generated/typescript') + +sourceSets { + main { + resources { + srcDir typescriptOut + } + } +} + +def typescriptRoot = rootProject.typescript_root as String +def resources = new File(rootProject.rootDir, 'src/main/resources') +def typescriptRootFile = new File(resources, typescriptRoot) + +task compileTypescript { + inputs.dir typescriptRootFile + outputs.dir typescriptOut + + doFirst { + project.delete { + delete typescriptOut + } + typescriptOut.mkdirs() + + project.exec { + executable 'tsc' + + args '--outDir', new File(typescriptOut, typescriptRoot).absolutePath + args '--project', typescriptRootFile.absolutePath + } + } +} + +processResources.dependsOn compileTypescript + +def typedocOut = new File(buildDir, 'typedoc') + +task typedoc { + inputs.dir typescriptRootFile + outputs.dir typedocOut + + doFirst { + project.delete { + delete typedocOut + } + typedocOut.mkdirs() + + project.exec { + workingDir typescriptRootFile + executable 'typedoc' + + args '--out', typedocOut.absolutePath + } + } +} + +processResources { + inputs.property 'version', mod_version + inputs.property 'name', rootProject.name + + from(sourceSets.main.resources.srcDirs) { + include 'fabric.mod.json' + expand 'version': mod_version, 'name': rootProject.name + } + + from(sourceSets.main.resources.srcDirs) { + exclude 'fabric.mod.json' + } + + exclude typescriptRoot + '/**/*.ts' + exclude typescriptRoot + '/types' + + for (JNIPlatform platform : jniPlatforms) { + def buildDir = new File(cDir, "build-${platform.name}") + if (!buildDir.exists()) { + buildDir.mkdir() + } + + def file = new File(buildDir, 'libscriptcraft' + platform.libExtension) + inputs.files file + + from(file.absolutePath) { + into new File('natives', platform.name).path + } + } +} + +// ensure that the encoding is set to UTF-8, no matter what the system default is +// this fixes some edge cases with special characters not displaying correctly +// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task +// if it is present. +// If you remove this task, sources will not be generated. +task sourcesJar(type: Jar, dependsOn: classes) { + classifier 'sources' + from sourceSets.main.allSource +} + +task typedocJar(type: Jar) { + classifier 'typedoc' + from typedocOut +} + +typedocJar.dependsOn typedoc + +artifacts { + archives sourcesJar + archives typedocJar +} + +jar { + from 'LICENSE' +} + +if (project.hasProperty('curseforge.api_key')) { + curseforge { + apiKey = project.getProperty('curseforge.api_key') + project { + id = project.curseforge_id + changelog = 'A changelog can be found at https://gitea.thebrokenrail.com/TheBrokenRail/ScriptCraft/src/branch/master/CHANGELOG.md' + releaseType = 'release' + addGameVersion project.simple_minecraft_version + addGameVersion 'Fabric' + mainArtifact(remapJar) { + displayName = "ScriptCraft v${mod_version} for ${project.minecraft_version}" + } + afterEvaluate { + uploadTask.dependsOn('remapJar') + } + relations { + requiredDependency 'fabric-api' + } + } + options { + forgeGradleIntegration = false + } + } +} \ No newline at end of file diff --git a/cmake/linux_i686_toolchain.cmake b/cmake/linux_i686_toolchain.cmake new file mode 100644 index 0000000..ed2b173 --- /dev/null +++ b/cmake/linux_i686_toolchain.cmake @@ -0,0 +1,7 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR i686) + +set(CMAKE_C_FLAGS "-m32 -march=i686") +set(CMAKE_CXX_FLAGS "-m32 -march=i686") + +set(CMAKE_EXE_LINKER_FLAGS "-m32 -march=i686") \ No newline at end of file diff --git a/cmake/windows_i686_toolchain.cmake b/cmake/windows_i686_toolchain.cmake new file mode 100644 index 0000000..bec6e75 --- /dev/null +++ b/cmake/windows_i686_toolchain.cmake @@ -0,0 +1,14 @@ +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86) +set(TOOLCHAIN_PREFIX i686-w64-mingw32) + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++) +set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran) +set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) + +set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) \ No newline at end of file diff --git a/cmake/windows_x86_64_toolchain.cmake b/cmake/windows_x86_64_toolchain.cmake new file mode 100644 index 0000000..a374eea --- /dev/null +++ b/cmake/windows_x86_64_toolchain.cmake @@ -0,0 +1,14 @@ +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR amd64) +set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++) +set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran) +set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) + +set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..775419a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs = -Xmx1G + +typescript_root = scriptcraft + +# Fabric Properties + # check these on https://fabricmc.net/use + minecraft_version = 1.15.2 + curseforge_id = TBD + simple_minecraft_version = 1.15.2 + yarn_build = 15 + fabric_loader_version = 0.8.2+build.194 + +# Mod Properties + mod_version = 1.0.0 + maven_group = com.thebrokenrail + archives_base_name = scriptcraft + +# Dependencies + # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api + fabric_api_version = 0.5.12+build.296-1.15 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/scripts/download-jni-headers.sh b/scripts/download-jni-headers.sh new file mode 100755 index 0000000..68c17d9 --- /dev/null +++ b/scripts/download-jni-headers.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +set -e + +if [ -e jdk.tar.gz ]; then + rm -f jdk.tar.gz +fi +curl -o jdk.tar.gz https://hg.openjdk.java.net/jdk/jdk11/archive/tip.tar.gz + +if [ -d ../src/main/c/jni ]; then + rm -rf ../src/main/c/jni +fi +mkdir ../src/main/c/jni + +if [ -d jdk ]; then + rm -rf jdk +fi +mkdir jdk +tar -zxf jdk.tar.gz --strip-components=1 -C jdk + +rm -f jdk.tar.gz + +cp -r jdk/src/java.base/share/native/include/. ../src/main/c/jni + +copy_jni() { + mkdir "../src/main/c/jni/$1" + cp -r "jdk/src/java.base/$1/native/include/." "../src/main/c/jni/$1" +} + +copy_jni unix +copy_jni windows + +rm -rf jdk \ No newline at end of file diff --git a/scripts/download-quickjs.sh b/scripts/download-quickjs.sh new file mode 100755 index 0000000..7c39eff --- /dev/null +++ b/scripts/download-quickjs.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +QUICKJS_VERSION='2020-04-12' + +set -e + +if [ -e quickjs.tar.xz ]; then + rm -f quickjs.tar.xz +fi +curl -L -o quickjs.tar.xz https://bellard.org/quickjs/quickjs-${QUICKJS_VERSION}.tar.xz + +if [ -d ../src/main/c/quickjs ]; then + rm -rf ../src/main/c/quickjs +fi +mkdir ../src/main/c/quickjs +tar -Jxf quickjs.tar.xz --strip-components=1 -C ../src/main/c/quickjs +rm -f quickjs.tar.xz \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..2cb3844 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +./download-quickjs.sh +./download-jni-headers.sh \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..ef88f39 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + jcenter() + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} + +rootProject.name = 'ScriptCraft' diff --git a/src/main/c/CMakeLists.txt b/src/main/c/CMakeLists.txt new file mode 100644 index 0000000..8ba7257 --- /dev/null +++ b/src/main/c/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.10) + +project(scriptcraft C) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror -Wno-int-to-pointer-cast -Wno-pointer-to-int-cast") + +add_library( + scriptcraft + SHARED + quickjs/quickjs.c + quickjs/libregexp.c + quickjs/libunicode.c + quickjs/cutils.c + quickjs/libbf.c + console.c + com_thebrokenrail_scriptcraft_quickjs_QuickJS.c +) + +file(STRINGS "quickjs/VERSION" QUICKJS_VERSION) +target_compile_definitions(scriptcraft PUBLIC -D_GNU_SOURCE PUBLIC -DCONFIG_VERSION=\"${QUICKJS_VERSION}\" -DCONFIG_BIGNUM -DDUMP_LEAKS) + +target_include_directories(scriptcraft PUBLIC quickjs) + +include_directories(jni) +if(UNIX) + include_directories(jni/unix) +elseif(WIN32) + include_directories(jni/windows) +endif() + +target_link_libraries(scriptcraft m pthread) \ No newline at end of file diff --git a/src/main/c/asprintf.h b/src/main/c/asprintf.h new file mode 100644 index 0000000..74d0d6d --- /dev/null +++ b/src/main/c/asprintf.h @@ -0,0 +1,62 @@ +#ifndef ASPRINTF_H +#define ASPRINTF_H + +#if defined(__GNUC__) && ! defined(_GNU_SOURCE) +#define _GNU_SOURCE /* needed for (v)asprintf, affects '#include ' */ +#endif +#include /* needed for vsnprintf */ +#include /* needed for malloc, free */ +#include /* needed for va_* */ + +/* + * vscprintf: + * MSVC implements this as _vscprintf, thus we just 'symlink' it here + * GNU-C-compatible compilers do not implement this, thus we implement it here + */ +#ifdef _MSC_VER +#define vscprintf _vscprintf +#endif + +#ifdef __GNUC__ +inline static int vscprintf(const char *format, va_list ap) { + va_list ap_copy; + va_copy(ap_copy, ap); + int retval = vsnprintf(NULL, 0, format, ap_copy); + va_end(ap_copy); + return retval; +} +#endif + +/* + * asprintf, vasprintf: + * MSVC does not implement these, thus we implement them here + * GNU-C-compatible compilers implement these with the same names, thus we + * don't have to do anything + */ +#ifdef _MSC_VER +inline static int vasprintf(char **strp, const char *format, va_list ap) { + int len = vscprintf(format, ap); + if (len == -1) + return -1; + char *str = (char*)malloc((size_t) len + 1); + if (!str) + return -1; + int retval = vsnprintf(str, len + 1, format, ap); + if (retval == -1) { + free(str); + return -1; + } + *strp = str; + return retval; +} + +inline static int asprintf(char **strp, const char *format, ...) { + va_list ap; + va_start(ap, format); + int retval = vasprintf(strp, format, ap); + va_end(ap); + return retval; +} +#endif + +#endif // ASPRINTF_H \ No newline at end of file diff --git a/src/main/c/com_thebrokenrail_scriptcraft_quickjs_QuickJS.c b/src/main/c/com_thebrokenrail_scriptcraft_quickjs_QuickJS.c new file mode 100644 index 0000000..171977f --- /dev/null +++ b/src/main/c/com_thebrokenrail_scriptcraft_quickjs_QuickJS.c @@ -0,0 +1,464 @@ +#include +#include +#include +#include + +#include +#include + +#include "com_thebrokenrail_scriptcraft_quickjs_QuickJS.h" +#include "console.h" + +static JavaVM *jvm; + +static void *get_pointer(JNIEnv *env, jobject obj, char *name) { + jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/quickjs/QuickJS"); + jfieldID field = (*env)->GetFieldID(env, clazz, name, "J"); + return (void *) (long) (*env)->GetLongField(env, obj, field); +} + +static void set_pointer(JNIEnv *env, jobject obj, char *name, void *value) { + jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/quickjs/QuickJS"); + jfieldID field = (*env)->GetFieldID(env, clazz, name, "J"); + (*env)->SetLongField(env, obj, field, (jlong) (long) value); +} + +static char *js_module_normalize_name(JSContext *ctx, const char *base_name, const char *name, void *opaque) { + JNIEnv *env; + (*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL); + + jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/quickjs/QuickJS"); + jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "normalizeModule", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); + + jstring java_name = (*env)->NewStringUTF(env, name); + jstring java_base_name = (*env)->NewStringUTF(env, base_name); + jstring java_string = (jstring) (*env)->CallStaticObjectMethod(env, clazz, methodID, java_base_name, java_name); + (*env)->DeleteLocalRef(env, java_name); + (*env)->DeleteLocalRef(env, java_base_name); + + if (java_string) { + const char *native_string = (*env)->GetStringUTFChars(env, java_string, 0); + char *new_string = js_strdup(ctx, native_string); + (*env)->ReleaseStringUTFChars(env, java_string, native_string); + + (*env)->DeleteLocalRef(env, java_string); + + (*jvm)->DetachCurrentThread(jvm); + return new_string; + } else { + jthrowable err = (*env)->ExceptionOccurred(env); + if (err) { + jmethodID to_string = (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Object"), "toString", "()Ljava/lang/String;"); + jstring java_string = (jstring) (*env)->CallObjectMethod(env, err, to_string); + const char *str = (*env)->GetStringUTFChars(env, java_string, 0); + + JS_ThrowReferenceError(ctx, "could not normalize module name '%s': %s", name, str); + + (*env)->ExceptionClear(env); + } else { + JS_ThrowReferenceError(ctx, "could not normalize module name '%s'", name); + } + (*jvm)->DetachCurrentThread(jvm); + return NULL; + } +} + +static int js_module_set_import_meta(JSContext *ctx, JSValueConst func_val, JS_BOOL use_realpath, JS_BOOL is_main) { + JSModuleDef *m; + char buf[PATH_MAX + 16]; + JSValue meta_obj; + JSAtom module_name_atom; + const char *module_name; + + assert(JS_VALUE_GET_TAG(func_val) == JS_TAG_MODULE); + m = JS_VALUE_GET_PTR(func_val); + + module_name_atom = JS_GetModuleName(ctx, m); + module_name = JS_AtomToCString(ctx, module_name_atom); + JS_FreeAtom(ctx, module_name_atom); + if (!module_name) { + return -1; + } + if (!strchr(module_name, ':')) { + strcpy(buf, "file://"); + } + pstrcpy(buf, sizeof(buf), module_name); + JS_FreeCString(ctx, module_name); + + meta_obj = JS_GetImportMeta(ctx, m); + if (JS_IsException(meta_obj)) { + return -1; + } + JS_DefinePropertyValueStr(ctx, meta_obj, "url", JS_NewString(ctx, buf), JS_PROP_C_W_E); + JS_DefinePropertyValueStr(ctx, meta_obj, "main", JS_NewBool(ctx, is_main), JS_PROP_C_W_E); + JS_FreeValue(ctx, meta_obj); + return 0; +} + +static char *js_load_file(JSContext *ctx, const char *filename) { + JNIEnv *env; + (*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL); + + jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/quickjs/QuickJS"); + jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "loadModule", "(Ljava/lang/String;)Ljava/lang/String;"); + + jstring java_filename = (*env)->NewStringUTF(env, filename); + jstring java_string = (jstring) (*env)->CallStaticObjectMethod(env, clazz, methodID, java_filename); + (*env)->DeleteLocalRef(env, java_filename); + + if (java_string) { + const char *native_string = (*env)->GetStringUTFChars(env, java_string, 0); + char *new_string = js_strdup(ctx, native_string); + (*env)->ReleaseStringUTFChars(env, java_string, native_string); + + (*env)->DeleteLocalRef(env, java_string); + + (*jvm)->DetachCurrentThread(jvm); + return new_string; + } else { + jthrowable err = (*env)->ExceptionOccurred(env); + if (err) { + jmethodID to_string = (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Object"), "toString", "()Ljava/lang/String;"); + jstring java_string = (jstring) (*env)->CallObjectMethod(env, err, to_string); + const char *str = (*env)->GetStringUTFChars(env, java_string, 0); + + JS_ThrowReferenceError(ctx, "could not load module filename '%s': %s", filename, str); + + (*env)->ExceptionClear(env); + } else { + JS_ThrowReferenceError(ctx, "could not load module filename '%s'", filename); + } + (*jvm)->DetachCurrentThread(jvm); + return NULL; + } +} + +static JSModuleDef *js_module_loader(JSContext *ctx, const char *module_name, void *opaque) { + JSModuleDef *m; + + char *buf; + JSValue func_val; + + buf = js_load_file(ctx, module_name); + if (!buf) { + return NULL; + } + + int is_json = has_suffix(module_name, ".json"); + if (is_json) { + asprintf(&buf, "export default JSON.parse(`%s`);", buf); + } + + /* compile the module */ + func_val = JS_Eval(ctx, buf, strlen(buf), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + js_free(ctx, buf); + if (JS_IsException(func_val)) { + return NULL; + } + /* XXX: could propagate the exception */ + js_module_set_import_meta(ctx, func_val, 1, 0); + /* the module is already referenced, so we must free it */ + m = JS_VALUE_GET_PTR(func_val); + JS_FreeValue(ctx, func_val); + return m; +} + +static void throw_exception(JNIEnv *env, char *message) { + jclass exception_clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/quickjs/JSException"); + (*env)->ThrowNew(env, exception_clazz, message); +} + +static JSClassID JS_CLASS_JAVA_OBJECT_ID; + +typedef struct java_object_data { + jobject obj; +} java_object_data; + +static void java_object_finalizer(JSRuntime *rt, JSValue val) { + java_object_data *data = JS_GetOpaque(val, JS_CLASS_JAVA_OBJECT_ID); + if (data) { + JNIEnv *env; + (*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL); + + (*env)->DeleteGlobalRef(env, data->obj); + js_free_rt(rt, data); + + (*jvm)->DetachCurrentThread(jvm); + } +} + +static JSClassDef JS_CLASS_JAVA_OBJECT = { + "JavaObject", + .finalizer = java_object_finalizer +}; + +static JSValue java_object_to_js_object(JNIEnv *env, JSContext *ctx, jobject obj) { + JSValue out = JS_NULL; + + jclass boolean_clazz = (*env)->FindClass(env, "java/lang/Boolean"); + jclass double_clazz = (*env)->FindClass(env, "java/lang/Double"); + jclass string_clazz = (*env)->FindClass(env, "java/lang/String"); + jclass array_clazz = (*env)->FindClass(env, "[Ljava/lang/Object;"); + if ((*env)->IsInstanceOf(env, obj, string_clazz)) { + const char *native_string = (*env)->GetStringUTFChars(env, (jstring) obj, 0); + out = JS_NewString(ctx, native_string); + (*env)->ReleaseStringUTFChars(env, (jstring) obj, native_string); + } else if ((*env)->IsInstanceOf(env, obj, boolean_clazz)) { + jmethodID boolean_methodID = (*env)->GetMethodID(env, boolean_clazz, "booleanValue", "()Z"); + jboolean val = (*env)->CallBooleanMethod(env, obj, boolean_methodID); + out = JS_NewBool(ctx, val); + } else if ((*env)->IsInstanceOf(env, obj, double_clazz)) { + jmethodID double_methodID = (*env)->GetMethodID(env, double_clazz, "doubleValue", "()D"); + jdouble val = (*env)->CallDoubleMethod(env, obj, double_methodID); + out = JS_NewFloat64(ctx, val); + } else if ((*env)->IsInstanceOf(env, obj, array_clazz)) { + out = JS_NewArray(ctx); + + int length = (*env)->GetArrayLength(env, (jobjectArray) obj); + for (uint32_t i = 0; i < length; i++) { + JS_SetPropertyUint32(ctx, out, i, java_object_to_js_object(env, ctx, (*env)->GetObjectArrayElement(env, obj, i))); + } + } else { + java_object_data *data = js_mallocz(ctx, sizeof (java_object_data)); + data->obj = (*env)->NewGlobalRef(env, obj); + out = JS_NewObjectClass(ctx, JS_CLASS_JAVA_OBJECT_ID); + JS_SetOpaque(out, data); + } + + return out; +} + +static jobject js_object_to_java_object(JNIEnv *env, JSContext *ctx, JSValue input) { + jobject obj = NULL; + if (JS_IsBool(input)) { + int val = JS_ToBool(ctx, input); + + jclass boolean_clazz = (*env)->FindClass(env, "java/lang/Boolean"); + jmethodID methodID = (*env)->GetMethodID(env, boolean_clazz, "", "(Z)V"); + obj = (*env)->NewObject(env, boolean_clazz, methodID, val); + } else if (JS_IsNumber(input)) { + double val; + JS_ToFloat64(ctx, &val, input); + + jclass double_clazz = (*env)->FindClass(env, "java/lang/Double"); + jmethodID methodID = (*env)->GetMethodID(env, double_clazz, "", "(D)V"); + obj = (*env)->NewObject(env, double_clazz, methodID, val); + } else if (JS_IsString(input)) { + const char *val = JS_ToCString(ctx, input); + obj = (*env)->NewStringUTF(env, val); + JS_FreeCString(ctx, val); + } else if (JS_IsArray(ctx, input)) { + uint32_t length; + + JSValue js_length = JS_GetPropertyStr(ctx, input, "length"); + JS_ToUint32(ctx, &length, js_length); + JS_FreeValue(ctx, js_length); + + jclass obj_clazz = (*env)->FindClass(env, "java/lang/Object"); + jobjectArray arr = (*env)->NewObjectArray(env, length, obj_clazz, NULL); + + for (int i = 0; i < length; i++) { + JSValue element = JS_GetPropertyUint32(ctx, input, i); + jobject obj = js_object_to_java_object(env, ctx, element); + (*env)->SetObjectArrayElement(env, arr, i - 1, obj); + (*env)->DeleteLocalRef(env, obj); + JS_FreeValue(ctx, element); + } + + obj = arr; + } else { + java_object_data *data = JS_GetOpaque(input, JS_CLASS_JAVA_OBJECT_ID); + if (data) { + obj = (*env)->NewLocalRef(env, data->obj); + } + } + return obj; +} + +static JSValue js_bridge(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + JNIEnv *env; + (*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL); + + jclass obj_clazz = (*env)->FindClass(env, "java/lang/Object"); + jobjectArray arr = (*env)->NewObjectArray(env, argc - 1, obj_clazz, NULL); + + for (int i = 1; i < argc; i++) { + jobject obj = js_object_to_java_object(env, ctx, argv[i]); + (*env)->SetObjectArrayElement(env, arr, i - 1, obj); + (*env)->DeleteLocalRef(env, obj); + } + + jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/bridge/Bridges"); + jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "useBridge", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;"); + + const char *js_bridge_name = JS_ToCString(ctx, argv[0]); + jstring bridge_name = (*env)->NewStringUTF(env, js_bridge_name); + + jobject out = (*env)->CallStaticObjectMethod(env, clazz, methodID, bridge_name, arr); + + (*env)->DeleteLocalRef(env, bridge_name); + (*env)->DeleteLocalRef(env, arr); + + JSValue js_out; + if (out) { + js_out = java_object_to_js_object(env, ctx, out); + (*env)->DeleteLocalRef(env, out); + } else { + jthrowable err = (*env)->ExceptionOccurred(env); + if (err) { + jmethodID to_string = (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Object"), "toString", "()Ljava/lang/String;"); + jstring java_string = (jstring) (*env)->CallObjectMethod(env, err, to_string); + const char *str = (*env)->GetStringUTFChars(env, java_string, 0); + + js_out = JS_ThrowReferenceError(ctx, "unable to use bridge '%s': %s", js_bridge_name, str); + + (*env)->ExceptionClear(env); + } else { + js_out = JS_NULL; + } + } + + JS_FreeCString(ctx, js_bridge_name); + + (*jvm)->DetachCurrentThread(jvm); + + return js_out; +} + +JNIEXPORT jobject JNICALL Java_com_thebrokenrail_scriptcraft_quickjs_QuickJS_bridge(JNIEnv *env, jobject this_val, jstring bridge_name, jobjectArray arr) { + JSContext *ctx = (JSContext *) get_pointer(env, this_val, "ctx"); + + int length = (*env)->GetArrayLength(env, arr); + JSValue args[length]; + + for (int i = 0; i < length; i++) { + args[i] = java_object_to_js_object(env, ctx, (*env)->GetObjectArrayElement(env, arr, i)); + } + + JSValue global_obj = JS_GetGlobalObject(ctx); + JSValue scriptcraft_obj = JS_GetPropertyStr(ctx, global_obj, "__scriptcraft__"); + JSValue bridges = JS_GetPropertyStr(ctx, scriptcraft_obj, "bridges"); + + const char *native_string = (*env)->GetStringUTFChars(env, bridge_name, 0); + JSValue bridge = JS_GetPropertyStr(ctx, bridges, native_string); + (*env)->ReleaseStringUTFChars(env, bridge_name, native_string); + + JSValue out; + if (JS_IsFunction(ctx, bridge)) { + out = JS_Call(ctx, bridge, global_obj, length, args); + if (JS_IsException(out)) { + js_std_dump_error(ctx); + out = JS_NULL; + } + } else { + out = JS_NULL; + throw_exception(env, "Invalid Bridge"); + } + + for (int i = 0; i < length; i++) { + JS_FreeValue(ctx, args[i]); + } + + JS_FreeValue(ctx, bridge); + JS_FreeValue(ctx, bridges); + JS_FreeValue(ctx, scriptcraft_obj); + JS_FreeValue(ctx, global_obj); + + jobject java_out = js_object_to_java_object(env, ctx, out); + JS_FreeValue(ctx, out); + + return java_out; +} + +JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_quickjs_QuickJS_init(JNIEnv *env, jobject this_val) { + jint rc = (*env)->GetJavaVM(env, &jvm); + if (rc != JNI_OK) { + throw_exception(env, "qjs: unable to cache JavaVM"); + return; + } + + JSRuntime *rt = JS_NewRuntime(); + if (!rt) { + throw_exception(env, "qjs: cannot allocate JS runtime"); + return; + } + JSContext *ctx = JS_NewContext(rt); + if (!ctx) { + throw_exception(env, "qjs: cannot allocate JS context"); + return; + } + + JS_SetModuleLoaderFunc(rt, js_module_normalize_name, js_module_loader, NULL); + + js_console_init(ctx); + + JSValue global_obj = JS_GetGlobalObject(ctx); + + JSValue scriptcraft_obj = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, scriptcraft_obj, "bridges", JS_NewObject(ctx)); + JS_SetPropertyStr(ctx, scriptcraft_obj, "useBridge", JS_NewCFunction(ctx, js_bridge, "useBridge", 1)); + JS_SetPropertyStr(ctx, global_obj, "__scriptcraft__", scriptcraft_obj); + + JS_FreeValue(ctx, global_obj); + + JS_NewClassID(&JS_CLASS_JAVA_OBJECT_ID); + JS_NewClass(JS_GetRuntime(ctx), JS_CLASS_JAVA_OBJECT_ID, &JS_CLASS_JAVA_OBJECT); + JS_SetClassProto(ctx, JS_CLASS_JAVA_OBJECT_ID, JS_NewObject(ctx)); + + set_pointer(env, this_val, "rt", rt); + set_pointer(env, this_val, "ctx", ctx); +} + +JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_quickjs_QuickJS_free(JNIEnv *env, jobject this_val) { + JSContext *ctx = (JSContext *) get_pointer(env, this_val, "ctx"); + JSRuntime *rt = (JSRuntime *) get_pointer(env, this_val, "rt"); + + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + +static int eval_buf(JNIEnv *env, JSContext *ctx, const void *buf, int buf_len, const char *filename, int eval_flags) { + JSValue val; + int ret; + + if ((eval_flags & JS_EVAL_TYPE_MASK) == JS_EVAL_TYPE_MODULE) { + /* for the modules, we compile then run to be able to set + import.meta */ + val = JS_Eval(ctx, buf, buf_len, filename, eval_flags | JS_EVAL_FLAG_COMPILE_ONLY); + if (!JS_IsException(val)) { + js_module_set_import_meta(ctx, val, TRUE, TRUE); + val = JS_EvalFunction(ctx, val); + } + } else { + val = JS_Eval(ctx, buf, buf_len, filename, eval_flags); + } + if (JS_IsException(val)) { + js_std_dump_error(ctx); + ret = -1; + } else { + ret = 0; + } + JS_FreeValue(ctx, val); + return ret; +} + +JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_quickjs_QuickJS_run(JNIEnv *env, jobject this_val, jstring data) { + JSContext *ctx = (JSContext *) get_pointer(env, this_val, "ctx"); + + const char *native_string = (*env)->GetStringUTFChars(env, data, 0); + eval_buf(env, ctx, native_string, strlen(native_string), "", JS_EVAL_TYPE_MODULE); + (*env)->ReleaseStringUTFChars(env, data, native_string); +} + +void print_data(char *data, int err) { + JNIEnv *env; + (*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL); + + jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/quickjs/QuickJS"); + jmethodID print_methodID = (*env)->GetStaticMethodID(env, clazz, "print", "(Ljava/lang/String;Z)V"); + jstring str = (*env)->NewStringUTF(env, data); + (*env)->CallStaticVoidMethod(env, clazz, print_methodID, str, err); + (*env)->DeleteLocalRef(env, str); + + (*jvm)->DetachCurrentThread(jvm); +} \ No newline at end of file diff --git a/src/main/c/com_thebrokenrail_scriptcraft_quickjs_QuickJS.h b/src/main/c/com_thebrokenrail_scriptcraft_quickjs_QuickJS.h new file mode 100644 index 0000000..e049fc4 --- /dev/null +++ b/src/main/c/com_thebrokenrail_scriptcraft_quickjs_QuickJS.h @@ -0,0 +1,47 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_thebrokenrail_scriptcraft_quickjs_QuickJS */ + +#ifndef _Included_com_thebrokenrail_scriptcraft_quickjs_QuickJS +#define _Included_com_thebrokenrail_scriptcraft_quickjs_QuickJS +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_thebrokenrail_scriptcraft_quickjs_QuickJS + * Method: init + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_quickjs_QuickJS_init + (JNIEnv *, jobject); + +/* + * Class: com_thebrokenrail_scriptcraft_quickjs_QuickJS + * Method: free + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_quickjs_QuickJS_free + (JNIEnv *, jobject); + +/* + * Class: com_thebrokenrail_scriptcraft_quickjs_QuickJS + * Method: bridge + * Signature: (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object; + */ +JNIEXPORT jobject JNICALL Java_com_thebrokenrail_scriptcraft_quickjs_QuickJS_bridge + (JNIEnv *, jobject, jstring, jobjectArray); + +/* + * Class: com_thebrokenrail_scriptcraft_quickjs_QuickJS + * Method: run + * Signature: (Ljava/lang/String;Ljava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_quickjs_QuickJS_run + (JNIEnv *, jobject, jstring); + +void print_data(char *data, int err); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/main/c/console.c b/src/main/c/console.c new file mode 100644 index 0000000..38cc85c --- /dev/null +++ b/src/main/c/console.c @@ -0,0 +1,119 @@ +#include "asprintf.h" + +#include +#include +#include + +#include "com_thebrokenrail_scriptcraft_quickjs_QuickJS.h" + +static JSValue js_print_internal(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int err, int prepend_throw) { + int i; + char *out; + if (prepend_throw) { + out = "Throw: "; + } else { + out = ""; + } + const char *str; + + for (i = 0; i < argc; i++) { + if (i != 0) { + asprintf(&out, "%s ", out); + } + str = JS_ToCString(ctx, argv[i]); + if (!str) { + return JS_EXCEPTION; + } + asprintf(&out, "%s%s", out, str); + JS_FreeCString(ctx, str); + } + print_data(out, err); + free(out); + return JS_UNDEFINED; +} + +static JSValue js_print(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + return js_print_internal(ctx, this_val, argc, argv, 0, 0); +} + +static JSValue js_print_err(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + return js_print_internal(ctx, this_val, argc, argv, 1, 0); +} + +static JSValue js_assert(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + int i; + const char *str; + + if (argc < 1) { + return JS_EXCEPTION; + } + int bool_val = JS_ToBool(ctx, argv[0]); + if (bool_val == -1) { + return JS_EXCEPTION; + } + + if (!bool_val) { + char *out = "Assertion failed"; + for (i = 1; i < argc; i++) { + if (i != 1) { + asprintf(&out, "%s ", out); + } else { + asprintf(&out, "%s: ", out); + } + str = JS_ToCString(ctx, argv[i]); + if (!str) { + return JS_EXCEPTION; + } + asprintf(&out, "%s%s", out, str); + JS_FreeCString(ctx, str); + } + print_data(out, 1); + free(out); + } + return JS_UNDEFINED; +} + +static const JSCFunctionListEntry js_console_funcs[] = { + JS_CFUNC_DEF("log", 1, js_print), + JS_CFUNC_DEF("info", 1, js_print), + JS_CFUNC_DEF("warn", 1, js_print), + JS_CFUNC_DEF("error", 1, js_print_err), + JS_CFUNC_DEF("dir", 1, js_print), + JS_CFUNC_DEF("debug", 1, js_print), + JS_CFUNC_DEF("trace", 1, js_print), + JS_CFUNC_DEF("assert", 1, js_assert), + JS_PROP_STRING_DEF("[Symbol.toStringTag]", "Console", JS_PROP_CONFIGURABLE) +}; + +void js_console_init(JSContext *ctx) { + JSValue global_obj, console; + + /* XXX: should these global definitions be enumerable? */ + global_obj = JS_GetGlobalObject(ctx); + + console = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, console, js_console_funcs, sizeof js_console_funcs / sizeof (JSCFunctionListEntry)); + JS_SetPropertyStr(ctx, global_obj, "console", console); + + JS_FreeValue(ctx, global_obj); +} + +void js_std_dump_error(JSContext *ctx) { + JSValue exception_val, val; + const char *stack; + int is_error; + + exception_val = JS_GetException(ctx); + is_error = JS_IsError(ctx, exception_val); + js_print_internal(ctx, JS_NULL, 1, (JSValueConst *) &exception_val, 1, !is_error); + if (is_error) { + val = JS_GetPropertyStr(ctx, exception_val, "stack"); + if (JS_ToBool(ctx, val)) { + stack = JS_ToCString(ctx, val); + print_data((char *) stack, 1); + JS_FreeCString(ctx, stack); + } + JS_FreeValue(ctx, val); + } + JS_FreeValue(ctx, exception_val); +} \ No newline at end of file diff --git a/src/main/c/console.h b/src/main/c/console.h new file mode 100644 index 0000000..68d1791 --- /dev/null +++ b/src/main/c/console.h @@ -0,0 +1,5 @@ +#include + +void js_console_init(JSContext *ctx); + +void js_std_dump_error(JSContext *ctx); \ No newline at end of file diff --git a/src/main/java/com/thebrokenrail/scriptcraft/Demo.java b/src/main/java/com/thebrokenrail/scriptcraft/Demo.java new file mode 100644 index 0000000..192853e --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/Demo.java @@ -0,0 +1,15 @@ +package com.thebrokenrail.scriptcraft; + +import com.thebrokenrail.scriptcraft.util.ScriptCraftEntrypoint; + +public class Demo implements ScriptCraftEntrypoint { + @Override + public String getModID() { + return "test"; + } + + @Override + public String getModIndex() { + return "index.js"; + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/ScriptCraft.java b/src/main/java/com/thebrokenrail/scriptcraft/ScriptCraft.java new file mode 100644 index 0000000..808fde0 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/ScriptCraft.java @@ -0,0 +1,35 @@ +package com.thebrokenrail.scriptcraft; + +import com.thebrokenrail.scriptcraft.quickjs.QuickJS; +import com.thebrokenrail.scriptcraft.quickjs.QuickJSManager; +import com.thebrokenrail.scriptcraft.util.ScriptCraftEntrypoint; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.loader.api.FabricLoader; + +import java.util.List; +import java.util.regex.Pattern; + +public class ScriptCraft implements ModInitializer { + public static final String NAMESPACE = "scriptcraft"; + public static final Pattern MOD_ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{1,63}"); + + @Override + public void onInitialize() { + QuickJSManager.init(new QuickJSManager.Task() { + @Override + protected Object run(QuickJS quickjs) { + List mods = FabricLoader.getInstance().getEntrypoints(NAMESPACE, ScriptCraftEntrypoint.class); + for (ScriptCraftEntrypoint mod : mods) { + if (MOD_ID_PATTERN.matcher(mod.getModID()).matches()) { + if (mod.shouldAutoLoad()) { + quickjs.run("import '" + mod.getModID() + "';"); + } + } else { + throw new RuntimeException("Invalid Mod ID: " + mod.getModID()); + } + } + return null; + } + }); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/api/CustomBlock.java b/src/main/java/com/thebrokenrail/scriptcraft/api/CustomBlock.java new file mode 100644 index 0000000..08a9a27 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/api/CustomBlock.java @@ -0,0 +1,28 @@ +package com.thebrokenrail.scriptcraft.api; + +import com.thebrokenrail.scriptcraft.util.Util; +import com.thebrokenrail.scriptcraft.quickjs.QuickJSManager; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +@SuppressWarnings("deprecation") +public class CustomBlock extends Block { + private final Identifier id; + + public CustomBlock(Settings settings, Identifier id) { + super(settings); + this.id = id; + } + + @Override + public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { + return Util.getEnumValue(ActionResult.class, (String) QuickJSManager.bridge("CustomBlock.onUse", id.toString(), world, state, (double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), hit.getSide().name(), player, hand.name()), ActionResult.PASS); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/api/CustomItem.java b/src/main/java/com/thebrokenrail/scriptcraft/api/CustomItem.java new file mode 100644 index 0000000..0fd5004 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/api/CustomItem.java @@ -0,0 +1,40 @@ +package com.thebrokenrail.scriptcraft.api; + +import com.thebrokenrail.scriptcraft.util.Util; +import com.thebrokenrail.scriptcraft.quickjs.QuickJSManager; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.ItemUsageContext; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import net.minecraft.util.TypedActionResult; +import net.minecraft.world.World; + +public class CustomItem extends Item { + private final Identifier id; + + public CustomItem(Settings settings, Identifier id) { + super(settings); + this.id = id; + } + + @Override + public TypedActionResult use(World world, PlayerEntity user, Hand hand) { + ItemStack stack = user.getStackInHand(hand); + ActionResult result = Util.getEnumValue(ActionResult.class, (String) QuickJSManager.bridge("CustomItem.onUse", id.toString(), world, user, hand.name()), ActionResult.PASS); + return new TypedActionResult<>(result, stack); + } + + @Override + public ActionResult useOnBlock(ItemUsageContext context) { + return Util.getEnumValue(ActionResult.class, (String) QuickJSManager.bridge("CustomItem.onUseOnBlock", id.toString(), context.getWorld(), (double) context.getBlockPos().getX(), (double) context.getBlockPos().getY(), (double) context.getBlockPos().getZ(), context.getSide().name(), context.getPlayer(), context.getHand().name()), ActionResult.PASS); + } + + @Override + public boolean useOnEntity(ItemStack stack, PlayerEntity user, LivingEntity entity, Hand hand) { + return Util.toBoolean(QuickJSManager.bridge("CustomItem.onUseOnEntity", id.toString(), user, entity, hand.name()), false); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/BlockSettingsBridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/BlockSettingsBridges.java new file mode 100644 index 0000000..286a879 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/BlockSettingsBridges.java @@ -0,0 +1,35 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import net.fabricmc.fabric.api.block.FabricBlockSettings; +import net.minecraft.block.Material; +import net.minecraft.block.MaterialColor; + +import java.util.Locale; + +class BlockSettingsBridges { + static void register() { + Bridges.addBridge("BlockSettings.create", args -> { + try { + String materialID = ((String) args[0]).toUpperCase(Locale.ROOT); + Material material = (Material) Material.class.getField(materialID).get(null); + + MaterialColor materialColor; + if (args[1] != null) { + String materialColorID = ((String) args[1]).toUpperCase(Locale.ROOT); + materialColor = (MaterialColor) MaterialColor.class.getField(materialColorID).get(null); + } else { + materialColor = material.getColor(); + } + + FabricBlockSettings settings = FabricBlockSettings.of(material, materialColor); + + settings.hardness(((Double) args[2]).floatValue()); + settings.resistance(((Double) args[3]).floatValue()); + + return settings.build(); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/BlockStateBridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/BlockStateBridges.java new file mode 100644 index 0000000..24c2b36 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/BlockStateBridges.java @@ -0,0 +1,12 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import net.minecraft.block.BlockState; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +class BlockStateBridges { + static void register() { + Bridges.addBridge("BlockState.getDefaultState", args -> Registry.BLOCK.get(new Identifier((String) args[0])).getDefaultState()); + Bridges.addBridge("BlockState.getBlock", args -> Registry.BLOCK.getId(((BlockState) args[0]).getBlock()).toString()); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/Bridge.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/Bridge.java new file mode 100644 index 0000000..230b321 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/Bridge.java @@ -0,0 +1,5 @@ +package com.thebrokenrail.scriptcraft.bridge; + +public interface Bridge { + Object use(Object... args); +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/Bridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/Bridges.java new file mode 100644 index 0000000..94ccc39 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/Bridges.java @@ -0,0 +1,41 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import com.thebrokenrail.scriptcraft.quickjs.QuickJS; +import com.thebrokenrail.scriptcraft.quickjs.QuickJSManager; + +import java.util.HashMap; + +@SuppressWarnings("unused") +public class Bridges { + private static final HashMap bridges = new HashMap<>(); + + public static void addBridge(String name, Bridge bridge) { + bridges.put(name, bridge); + } + + public static Object useBridge(String name, Object... args) { + QuickJSManager.Task task = new QuickJSManager.Task() { + @Override + protected Object run(QuickJS quickjs) { + if (bridges.containsKey(name)) { + return bridges.get(name).use(args); + } else { + throw new RuntimeException("Invalid Bridge: '" + name + '\''); + } + } + }; + return QuickJSManager.sendTaskFromQuickJS(task); + } + + static { + BlockSettingsBridges.register(); + RegistryBridge.register(); + ItemStackBridges.register(); + LivingEntityBridges.register(); + BlockStateBridges.register(); + WorldBridges.register(); + ItemSettingsBridges.register(); + EntityBridges.register(); + DamageSourceBridges.register(); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/DamageSourceBridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/DamageSourceBridges.java new file mode 100644 index 0000000..65e9f17 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/DamageSourceBridges.java @@ -0,0 +1,23 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; + +import java.util.Locale; + +class DamageSourceBridges { + static void register() { + Bridges.addBridge("DamageSource.create", args -> { + try { + String id = ((String) args[0]).toUpperCase(Locale.ROOT); + return DamageSource.class.getField(id).get(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + + Bridges.addBridge("DamageSource.createFromPlayer", args -> DamageSource.player((PlayerEntity) args[1])); + Bridges.addBridge("DamageSource.createFromMob", args -> DamageSource.mob((LivingEntity) args[1])); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/EntityBridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/EntityBridges.java new file mode 100644 index 0000000..9dafba7 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/EntityBridges.java @@ -0,0 +1,49 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import com.thebrokenrail.scriptcraft.util.Util; +import net.minecraft.entity.Entity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.registry.Registry; + +import java.util.Objects; + +class EntityBridges { + static void register() { + Bridges.addBridge("Entity.getEntityWorld", args -> ((Entity) args[0]).getEntityWorld()); + Bridges.addBridge("Entity.getID", args -> Registry.ENTITY_TYPE.getId(((Entity) args[0]).getType()).toString()); + + Bridges.addBridge("Entity.getName", args -> ((Entity) args[0]).getName().asString()); + Bridges.addBridge("Entity.getDisplayName", args -> ((Entity) args[0]).getDisplayName().asString()); + Bridges.addBridge("Entity.getCustomName", args -> ((Entity) args[0]).hasCustomName() ? Objects.requireNonNull(((Entity) args[0]).getCustomName()).asString() : null); + + Bridges.addBridge("Entity.kill", args -> { + ((Entity) args[0]).kill(); + return null; + }); + Bridges.addBridge("Entity.remove", args -> { + ((Entity) args[0]).remove(); + return null; + }); + Bridges.addBridge("Entity.damage", args -> ((Entity) args[0]).damage((DamageSource) args[1], (float) args[2])); + + Bridges.addBridge("Entity.setFireTicks", args -> { + ((Entity) args[0]).setFireTicks((int) args[1]); + return null; + }); + Bridges.addBridge("Entity.getFireTicks", args -> ((Entity) args[0]).getFireTicks()); + + Bridges.addBridge("Entity.getPosition", args -> { + Vec3d pos = ((Entity) args[0]).getPos(); + Double[] out = new Double[3]; + out[0] = pos.getX(); + out[1] = pos.getY(); + out[2] = pos.getZ(); + return out; + }); + Bridges.addBridge("Entity.setPosition", args -> { + ((Entity) args[0]).updatePosition(Util.toDouble(args[1], 0), Util.toDouble(args[2], 0), Util.toDouble(args[3], 0)); + return null; + }); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/ItemSettingsBridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/ItemSettingsBridges.java new file mode 100644 index 0000000..ca2e141 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/ItemSettingsBridges.java @@ -0,0 +1,28 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.util.Rarity; + +class ItemSettingsBridges { + static void register() { + Bridges.addBridge("ItemSettings.create", args -> { + Item.Settings settings = new Item.Settings(); + + settings.maxCount(((Double) args[0]).intValue()); + settings.rarity(Rarity.valueOf((String) args[1])); + + String selectedGroup = (String) args[2]; + if (selectedGroup != null) { + for (ItemGroup group : ItemGroup.GROUPS) { + if (group.getName().equals(selectedGroup)) { + settings.group(group); + break; + } + } + } + + return settings; + }); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/ItemStackBridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/ItemStackBridges.java new file mode 100644 index 0000000..b6a422c --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/ItemStackBridges.java @@ -0,0 +1,13 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +class ItemStackBridges { + static void register() { + Bridges.addBridge("ItemStack.create", args -> new ItemStack(Registry.ITEM.get(new Identifier((String) args[0])), ((Double) args[1]).intValue())); + Bridges.addBridge("ItemStack.getItem", args -> Registry.ITEM.getId(((ItemStack) args[0]).getItem()).toString()); + Bridges.addBridge("ItemStack.getCount", args -> (double) ((ItemStack) args[0]).getCount()); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/LivingEntityBridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/LivingEntityBridges.java new file mode 100644 index 0000000..816c0a6 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/LivingEntityBridges.java @@ -0,0 +1,11 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.Hand; + +class LivingEntityBridges { + static void register() { + Bridges.addBridge("LivingEntity.getStackInHand", args -> ((LivingEntity) args[0]).getStackInHand((Hand) args[1])); + Bridges.addBridge("LivingEntity.getHealth", args -> ((LivingEntity) args[0]).getHealth()); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/RegistryBridge.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/RegistryBridge.java new file mode 100644 index 0000000..4ae6bc8 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/RegistryBridge.java @@ -0,0 +1,26 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import com.thebrokenrail.scriptcraft.api.CustomBlock; +import com.thebrokenrail.scriptcraft.api.CustomItem; +import net.minecraft.block.Block; +import net.minecraft.item.BlockItem; +import net.minecraft.item.Item; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +class RegistryBridge { + static void register() { + Bridges.addBridge("Registry.registerBlock", args -> { + Registry.register(Registry.BLOCK, new Identifier((String) args[0]), new CustomBlock((Block.Settings) args[1], new Identifier((String) args[0]))); + return null; + }); + Bridges.addBridge("Registry.registerItem", args -> { + Registry.register(Registry.ITEM, new Identifier((String) args[0]), new CustomItem((Item.Settings) args[1], new Identifier((String) args[0]))); + return null; + }); + Bridges.addBridge("Registry.registerBlockItem", args -> { + Registry.register(Registry.ITEM, new Identifier((String) args[0]), new BlockItem(Registry.BLOCK.get(new Identifier((String) args[2])), (Item.Settings) args[1])); + return null; + }); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/bridge/WorldBridges.java b/src/main/java/com/thebrokenrail/scriptcraft/bridge/WorldBridges.java new file mode 100644 index 0000000..116139c --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/bridge/WorldBridges.java @@ -0,0 +1,27 @@ +package com.thebrokenrail.scriptcraft.bridge; + +import com.thebrokenrail.scriptcraft.util.Util; +import net.minecraft.block.BlockState; +import net.minecraft.entity.Entity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.World; + +class WorldBridges { + static void register() { + Bridges.addBridge("World.getBlockState", args -> ((World) args[0]).getBlockState(new BlockPos((double) args[1], (double) args[2], (double) args[3]))); + Bridges.addBridge("World.setBlockState", args -> ((World) args[0]).setBlockState(new BlockPos((double) args[1], (double) args[2], (double) args[3]), (BlockState) args[4])); + + Bridges.addBridge("World.spawnEntity", args -> { + Entity entity = Registry.ENTITY_TYPE.get(new Identifier((String) args[4])).create((World) args[0]); + if (entity != null) { + entity.updatePosition(Util.toDouble(args[1], 0), Util.toDouble(args[2], 0), Util.toDouble(args[3], 0)); + ((World) args[0]).spawnEntity(entity); + return entity; + } else { + return null; + } + }); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/quickjs/JSException.java b/src/main/java/com/thebrokenrail/scriptcraft/quickjs/JSException.java new file mode 100644 index 0000000..aaf1597 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/quickjs/JSException.java @@ -0,0 +1,7 @@ +package com.thebrokenrail.scriptcraft.quickjs; + +public class JSException extends Exception { + public JSException(String message) { + super(message); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/quickjs/QuickJS.java b/src/main/java/com/thebrokenrail/scriptcraft/quickjs/QuickJS.java new file mode 100644 index 0000000..2490a5e --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/quickjs/QuickJS.java @@ -0,0 +1,147 @@ +package com.thebrokenrail.scriptcraft.quickjs; + +import com.thebrokenrail.scriptcraft.ScriptCraft; +import com.thebrokenrail.scriptcraft.util.OSUtil; +import com.thebrokenrail.scriptcraft.util.ScriptCraftEntrypoint; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; + +@SuppressWarnings("unused") +public class QuickJS { + public QuickJS() throws JSException { + init(); + } + + private long ctx; + private long rt; + + public native void init() throws JSException; + + public native void free(); + + public native Object bridge(String method, Object... args) throws JSException; + + public native void run(String data); + + static { + try { + File file = File.createTempFile("lib" + ScriptCraft.NAMESPACE, OSUtil.getLibExtension()); + file.deleteOnExit(); + System.out.println("Extracting ScriptCraft Native To: " + file.getAbsoluteFile().toPath()); + InputStream so = (QuickJS.class.getResourceAsStream(File.separator + "natives" + File.separator + OSUtil.getOS() + File.separator + "lib" + ScriptCraft.NAMESPACE + OSUtil.getLibExtension())); + if (so == null) { + throw new RuntimeException("ScriptCraft does not support your OS: " + OSUtil.getOS()); + } else { + Files.copy(so, file.getAbsoluteFile().toPath(), StandardCopyOption.REPLACE_EXISTING); + System.load(file.getAbsolutePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String loadModule(String name) { + String[] arr = name.split(File.separator, 2); + if (ScriptCraft.MOD_ID_PATTERN.matcher(arr[0]).matches()) { + List mods = FabricLoader.getInstance().getEntrypoints(ScriptCraft.NAMESPACE, ScriptCraftEntrypoint.class); + if (arr.length == 1) { + return null; + } else { + for (ScriptCraftEntrypoint mod : mods) { + if (mod.getModID().equals(arr[0])) { + //noinspection CatchMayIgnoreException + try { + InputStream stream = mod.getClass().getResourceAsStream(File.separator + ScriptCraft.NAMESPACE + File.separator + arr[0] + File.separator + arr[1]); + + if (stream != null) { + StringBuilder textBuilder = new StringBuilder(); + try (Reader reader = new BufferedReader(new InputStreamReader(stream))) { + int c; + while ((c = reader.read()) != -1) { + textBuilder.append((char) c); + } + } + stream.close(); + + return textBuilder.toString(); + } + } catch (IOException e) { + } + } + } + } + } + return null; + } + + private static final String[] SUPPORTED_EXTENSION = new String[]{"", ".js", ".json", ".mjs"}; + + public static String normalizeModule(String baseName, String name) { + String normalizedPath; + + if (name.startsWith(".")) { + Path parentPath = Paths.get(baseName.replaceAll("/", File.separator)).getParent(); + if (parentPath == null) { + parentPath = Paths.get(File.separator); + } + normalizedPath = Paths.get(parentPath.toString(), name.replaceAll("/", File.separator)).normalize().toString(); + if (normalizedPath.charAt(0) == File.separatorChar) { + normalizedPath = normalizedPath.substring(1); + } + } else { + normalizedPath = name; + } + + String result = null; + boolean success = false; + + String[] arr = normalizedPath.split(File.separator, 2); + if (ScriptCraft.MOD_ID_PATTERN.matcher(arr[0]).matches()) { + List mods = FabricLoader.getInstance().getEntrypoints(ScriptCraft.NAMESPACE, ScriptCraftEntrypoint.class); + if (arr.length == 1) { + for (ScriptCraftEntrypoint mod : mods) { + if (mod.getModID().equals(arr[0])) { + result = arr[0] + File.separator + mod.getModIndex(); + success = true; + break; + } + } + } else { + result = arr[0] + File.separator + arr[1]; + success = true; + } + } + + if (success) { + for (String extension : SUPPORTED_EXTENSION) { + if (loadModule(result + extension) != null) { + return result + extension; + } + } + } + + return null; + } + + public static void print(String data, boolean err) { + String[] lines = data.split("\n"); + for (String line : lines) { + if (err) { + System.err.println(line); + } else { + System.out.println(line); + } + } + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/quickjs/QuickJSManager.java b/src/main/java/com/thebrokenrail/scriptcraft/quickjs/QuickJSManager.java new file mode 100644 index 0000000..11e598b --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/quickjs/QuickJSManager.java @@ -0,0 +1,189 @@ +package com.thebrokenrail.scriptcraft.quickjs; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public class QuickJSManager { + private static QuickJS quickjs; + + public static abstract class Task { + private boolean lastTask = false; + private boolean done = false; + private boolean err; + private Object obj; + + protected abstract Object run(QuickJS quickjs) throws JSException; + } + + private static Thread thread; + + private static final AtomicReference quickJSInputTask = new AtomicReference<>(); + + private static final AtomicReference quickJSOutputTask = new AtomicReference<>(); + + public static Object sendTaskFromQuickJS(Task task) { + if (Thread.currentThread() != thread) { + throw new RuntimeException(); + } + task.done = false; + task.err = false; + synchronized (lock) { + quickJSOutputTask.set(task); + lock.notifyAll(); + while (!task.done) { + try { + lock.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + if (task.err) { + throw new RuntimeException("Java Exception While Executing Task"); + } else { + return task.obj; + } + } + + private static final Object startedLock = new Object(); + + private static class QuickJSThread extends Thread { + private QuickJSThread() { + super("QuickJS thread"); + } + + @Override + public void run() { + synchronized (lock) { + init(); + + while (true) { + try { + lock.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + Task task = quickJSInputTask.get(); + if (task != null && !task.done) { + try { + task.obj = task.run(quickjs); + } catch (Throwable e) { + e.printStackTrace(); + task.obj = null; + } + task.done = true; + if (task.lastTask) { + break; + } + } + lock.notifyAll(); + } + } + } + + private void init() { + try { + quickjs = new QuickJS(); + synchronized (startedLock) { + startedLock.notifyAll(); + } + } catch (JSException e) { + started.set(false); + synchronized (startedLock) { + startedLock.notifyAll(); + } + throw new RuntimeException(e); + } + } + } + + private static final AtomicBoolean started = new AtomicBoolean(false); + + public static void init(Task initTask) { + if (!started.get()) { + started.set(true); + + thread = new QuickJSThread(); + + synchronized (startedLock) { + thread.start(); + try { + startedLock.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + if (!started.get()) { + return; + } + + sendTaskToQuickJS(initTask); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + Task task = new Task() { + @Override + protected Object run(QuickJS quickjs) { + System.out.println("Freeing QuickJS"); + quickjs.free(); + return null; + } + }; + task.lastTask = true; + sendTaskToQuickJS(task); + })); + } + } + + private static final Object lock = new Object(); + + private static synchronized Object sendTaskToQuickJS(Task task) { + if (!started.get()) { + return null; + } + synchronized (lock) { + quickJSOutputTask.set(null); + quickJSInputTask.set(task); + lock.notifyAll(); + if (task.lastTask) { + return null; + } else { + while (true) { + try { + lock.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + Task outputTask = quickJSOutputTask.get(); + if (outputTask != null && !outputTask.done) { + try { + outputTask.obj = outputTask.run(null); + outputTask.err = false; + } catch (Throwable e) { + e.printStackTrace(); + outputTask.obj = null; + outputTask.err = true; + } + outputTask.done = true; + quickJSOutputTask.set(outputTask); + } + lock.notifyAll(); + if (task.done) { + break; + } + } + } + return task.obj; + } + } + + public static Object bridge(String method, Object... args) { + Task task = new Task() { + @Override + protected Object run(QuickJS quickjs) throws JSException { + return quickjs.bridge(method, args); + } + }; + return sendTaskToQuickJS(task); + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/util/MinecraftAPIEntrypoint.java b/src/main/java/com/thebrokenrail/scriptcraft/util/MinecraftAPIEntrypoint.java new file mode 100644 index 0000000..c8880be --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/util/MinecraftAPIEntrypoint.java @@ -0,0 +1,18 @@ +package com.thebrokenrail.scriptcraft.util; + +public class MinecraftAPIEntrypoint implements ScriptCraftEntrypoint { + @Override + public String getModID() { + return "minecraft"; + } + + @Override + public String getModIndex() { + return "index"; + } + + @Override + public boolean shouldAutoLoad() { + return false; + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/util/OSUtil.java b/src/main/java/com/thebrokenrail/scriptcraft/util/OSUtil.java new file mode 100644 index 0000000..6cc08b9 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/util/OSUtil.java @@ -0,0 +1,56 @@ +package com.thebrokenrail.scriptcraft.util; + +import java.util.Locale; + +public class OSUtil { + private static String getOSName() { + String raw = System.getProperty("os.name").toLowerCase(Locale.ROOT); + if (raw.contains("mac")) { + return "macosx"; + } else if (raw.contains("linux")) { + return "linux"; + } else if (raw.contains("windows")) { + return "windows"; + } else { + return "other"; + } + } + + public static String getOS() { + String raw = System.getProperty("os.arch"); + String arch; + switch (raw) { + case "x86": + case "i686": { + arch = "i686"; + break; + } + case "amd64": { + arch = "x86_64"; + break; + } + default: { + arch = "other"; + break; + } + } + return getOSName() + '-' + arch; + } + + public static String getLibExtension() { + switch (getOSName()) { + case "macos": { + return ".dylib"; + } + case "linux": { + return ".so"; + } + case "windows": { + return ".dll"; + } + default: { + return ""; + } + } + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/util/ScriptCraftEntrypoint.java b/src/main/java/com/thebrokenrail/scriptcraft/util/ScriptCraftEntrypoint.java new file mode 100644 index 0000000..5ede493 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/util/ScriptCraftEntrypoint.java @@ -0,0 +1,11 @@ +package com.thebrokenrail.scriptcraft.util; + +public interface ScriptCraftEntrypoint { + String getModID(); + + String getModIndex(); + + default boolean shouldAutoLoad() { + return true; + } +} diff --git a/src/main/java/com/thebrokenrail/scriptcraft/util/Util.java b/src/main/java/com/thebrokenrail/scriptcraft/util/Util.java new file mode 100644 index 0000000..2c08da4 --- /dev/null +++ b/src/main/java/com/thebrokenrail/scriptcraft/util/Util.java @@ -0,0 +1,30 @@ +package com.thebrokenrail.scriptcraft.util; + +import java.util.Locale; + +@SuppressWarnings("UnnecessaryUnboxing") +public class Util { + public static > T getEnumValue(Class clazz, String value, T defaultValue) { + try { + return Enum.valueOf(clazz, value.toUpperCase(Locale.ROOT)); + } catch (NullPointerException e) { + return defaultValue; + } + } + + public static double toDouble(Object value, double defaultValue) { + try { + return ((Double) value).doubleValue(); + } catch (NullPointerException e) { + return defaultValue; + } + } + + public static boolean toBoolean(Object value, boolean defaultValue) { + try { + return ((Boolean) value).booleanValue(); + } catch (NullPointerException e) { + return defaultValue; + } + } +} diff --git a/src/main/resources/assets/scriptcraft/icon.png b/src/main/resources/assets/scriptcraft/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..047b91f2347de5cf95f23284476fddbe21ba23fe GIT binary patch literal 453 zcmV;$0XqJPP)QAFYGys`80vegN0XDFh0OXKz&i8?Le#x7{1X)R+00000NkvXXu0mjf73i~T literal 0 HcmV?d00001 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..1ca9663 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "id": "scriptcraft", + "version": "${version}", + "name": "${name}", + "description": "JS API for Minecraft", + "authors": [ + "TheBrokenRail" + ], + "contact": { + "homepage": "https://thebrokenrail.com/", + "sources": "https://gitea.thebrokenrail.com/TheBrokenRail/ScriptCraft.git", + "issues": "https://gitea.thebrokenrail.com/TheBrokenRail/ScriptCraft/issues" + }, + "license": "MIT", + "icon": "assets/scriptcraft/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.thebrokenrail.scriptcraft.ScriptCraft" + ], + "scriptcraft": [ + "com.thebrokenrail.scriptcraft.util.MinecraftAPIEntrypoint", + "com.thebrokenrail.scriptcraft.Demo" + ] + }, + "depends": { + "fabricloader": ">=0.7.4", + "fabric": "*", + "minecraft": "1.15.x" + } +} diff --git a/src/main/resources/scriptcraft/.prettierrc b/src/main/resources/scriptcraft/.prettierrc new file mode 100644 index 0000000..0e98f6a --- /dev/null +++ b/src/main/resources/scriptcraft/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "none", + "tabWidth": 4, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "printWidth": 2400 +} \ No newline at end of file diff --git a/src/main/resources/scriptcraft/minecraft/block.ts b/src/main/resources/scriptcraft/minecraft/block.ts new file mode 100644 index 0000000..177ea07 --- /dev/null +++ b/src/main/resources/scriptcraft/minecraft/block.ts @@ -0,0 +1,158 @@ +import { World } from './world'; +import { PlayerEntity } from './entity'; +import { useBridge, addBridge, Identifier, Hand, Pos, ActionResult, Direction, SingleRegistry } from './core'; + +/** + * Settings for {@link CustomBlock} + */ +export class BlockSettings { + readonly #material: string; + readonly #materialColor: string; + + #resistance: number; + #hardness: number; + + /** + * Create Block Settings + * @param material Material + * @param materialColor Material Color + */ + constructor(material: string); + constructor(material: string, materialColor: string); + constructor(material: string, materialColor?: string) { + this.#material = material; + this.#materialColor = materialColor; + + this.#resistance = 0; + this.#hardness = 0; + } + + /** + * Set Block Strength + * @param hardness Hardness Value + * @param resistance Blast Resistance Value + */ + setStrength(hardness: number, resistance: number): BlockSettings { + this.#hardness = hardness; + this.#resistance = resistance; + return this; + } + + /** + * Set Hardness + * @param hardness Hardness Value + */ + setHardness(hardness: number): BlockSettings { + this.#hardness = hardness; + return this; + } + + /** + * Set Blast Resistance + * @param resistance Blast Resistance Value + */ + setResistance(resistance: number): BlockSettings { + this.#resistance = resistance; + return this; + } + + /** + * @internal + */ + createJavaObject(): JavaObject { + return useBridge('BlockSettings.create', this.#material, this.#materialColor, this.#hardness, this.#resistance) as JavaObject; + } +} + +/** + * Custom Block + */ +export abstract class CustomBlock { + /** + * @internal + */ + settings: BlockSettings; + + constructor(settings: BlockSettings) { + this.settings = settings; + } + + /** + * Called When The Block Is Used + * @param world World + * @param blockState Block State + * @param pos Block Position + * @param side Side Of The Block Used + * @param player Player + * @param hand Hand + * @returns Action Result + */ + abstract onUse(world: World, blockState: BlockState, pos: Pos, side: Direction, player: PlayerEntity, hand: Hand): ActionResult; +} + +/** + * Block State + */ +export class BlockState { + /** + * @internal + */ + javaObject: JavaObject; + + constructor(javaObject: JavaObject) { + this.javaObject = javaObject; + } + + /** + * Get Default Block State for Block + * @param block Block ID + * @returns Default Block State + */ + static getDefaultState(block: Identifier): BlockState { + const obj = useBridge('BlockState.getDefaultState', block.toString()) as JavaObject; + if (obj) { + return new BlockState(obj); + } else { + return null; + } + } + + /** + * Get Block ID + * @returns Block ID + */ + getBlock(): Identifier { + const obj = useBridge('BlockState.getBlock', this.javaObject) as string; + if (obj) { + return new Identifier(obj); + } else { + return null; + } + } +} + +/** + * @internal + */ +export class BlockRegistry implements SingleRegistry { + static INSTANCE = new BlockRegistry(); + + #blocks: Map; + + private constructor() { + this.#blocks = new Map(); + } + + register(id: Identifier, obj: CustomBlock) { + this.#blocks.set(id.toString(), obj); + useBridge('Registry.registerBlock', id.toString(), obj.settings.createJavaObject()); + } + + get(id: Identifier): CustomBlock { + return this.#blocks.get(id.toString()); + } +} + +addBridge('CustomBlock.onUse', (id: string, world: JavaObject, state: JavaObject, x: number, y: number, z: number, side: keyof typeof Direction, player: JavaObject, hand: keyof typeof Hand): string => { + return BlockRegistry.INSTANCE.get(new Identifier(id)).onUse(new World(world), new BlockState(state), new Pos(x, y, z), Direction[side], new PlayerEntity(player), Hand[hand]); +}); \ No newline at end of file diff --git a/src/main/resources/scriptcraft/minecraft/core.ts b/src/main/resources/scriptcraft/minecraft/core.ts new file mode 100644 index 0000000..ef56bd8 --- /dev/null +++ b/src/main/resources/scriptcraft/minecraft/core.ts @@ -0,0 +1,236 @@ +/** + * @internal + */ +export function addBridge(name: string, bridge: BridgeType) { + __scriptcraft__.bridges[name] = bridge; +} + +/** + * @internal + */ +export function useBridge(name: string, ...args: BridgeValueType[]): BridgeValueType { + return __scriptcraft__.useBridge(name, ...args); +} + +/** + * Action Result + */ +export enum ActionResult { + PASS = 'PASS', + SUCCESS = 'SUCCESS', + CONSUME = 'CONSUME' +} + +/** + * Hand + */ +export enum Hand { + MAIN_HAND = 'MAIN_HAND', + OFF_HAND = 'OFF_HAND' +} + +/** + * Direction + */ +export enum Direction { + DOWN = 'DOWN', + UP = 'UP', + NORTH = 'NORTH', + SOUTH = 'SOUTH', + WEST = 'WEST', + EAST = 'EAST' +} + +/** + * Utility Class for {@link Direction} + */ +export class DirectionUtil { + /** + * Get Direction's Offset + * @param direction Direction + */ + static getOffset(direction: Direction): Pos { + switch (direction) { + case Direction.UP: { + return new Pos(0, 1, 0); + } + case Direction.DOWN: { + return new Pos(0, -1, 0); + } + case Direction.NORTH: { + return new Pos(0, 0, -1); + } + case Direction.SOUTH: { + return new Pos(0, 0, 1); + } + case Direction.WEST: { + return new Pos(-1, 0, 0); + } + case Direction.EAST: { + return new Pos(1, 0, 0); + } + } + } +} + +/** + * Namespaced Identifier + * ":" + */ +export class Identifier { + readonly #namespace: string; + readonly #path: string; + + /** + * Create Identifier + * @param str Existing Identifier + * @param namespace Namespace + * @param path Path + */ + constructor(str: string); + constructor(namespace: string, path: string); + constructor(data: string, path?: string) { + if (path) { + this.#namespace = data; + this.#path = path; + } else { + const id = data.split(':'); + this.#namespace = id[1] ? id[0] : 'minecraft'; + this.#path = id[1] ? id[1] : id[0]; + } + } + + /** + * Get Namespace + */ + getNamespace(): string { + return this.#namespace; + } + + /** + * Get Path + */ + getPath(): string { + return this.#path; + } + + /** + * Convert To String + */ + toString(): string { + return this.#namespace + ':' + this.#path; + } +} + +/** + * Arbitrary Position + */ +export class Pos { + readonly #x: number; + readonly #y: number; + readonly #z: number; + + /** + * Create Position + * @param x X Coordinate + * @param y Y Coordinate + * @param z Z Coordinate + */ + constructor(x: number, y: number, z: number) { + this.#x = x; + this.#y = y; + this.#z = z; + } + + /** + * Get X Coordinate + * @returns X Coordinate + */ + getX(): number { + return this.#x; + } + + /** + * Get Y Coordinate + * @returns Y Coordinate + */ + getY(): number { + return this.#y; + } + + /** + * Get Z Coordinate + * @returns Z Coordinate + */ + getZ(): number { + return this.#z; + } + + /** + * Add Position + * @param pos Other Position + * @returns Position Sum + */ + add(pos: Pos): Pos { + return new Pos(this.getX() + pos.getX(), this.getY() + pos.getY(), this.getZ() + pos.getZ()); + } + + /** + * Subtract Position + * @param pos Other Position + * @returns Position Difference + */ + subtract(pos: Pos): Pos { + const newBlockPos = new Pos(-pos.getX(), -pos.getY(), -pos.getZ()); + return this.add(newBlockPos); + } + + /** + * Multiply Position + * @param pos Other Position + * @returns Position Product + */ + multiply(pos: Pos): Pos { + return new Pos(this.getX() * pos.getX(), this.getY() * pos.getY(), this.getZ() * pos.getZ()); + } + + /** + * Divide Position + * @param pos Position Quotient + */ + divide(pos: Pos): Pos { + return new Pos(this.getX() / pos.getX(), this.getY() / pos.getY(), this.getZ() / pos.getZ()); + } + + /** + * Offset Position with a {@link Direction} + * @param direction Offset Position + */ + offset(direction: Direction): Pos { + return this.add(DirectionUtil.getOffset(direction)); + } + + /** + * Round Position + * @returns Rounded Position + */ + round(): Pos { + return new Pos(Math.round(this.getX()), Math.round(this.getY()), Math.round(this.getZ())); + } + + /** + * Convert To String + */ + toString(): string { + return 'Pos{' + this.getX() + ', ' + this.getY() + ', ' + this.getZ() + '}'; + } +} + +/** + * @internal + */ +export interface SingleRegistry { + register(id: Identifier, obj: T): void; + + get(id: Identifier): T; +} \ No newline at end of file diff --git a/src/main/resources/scriptcraft/minecraft/entity.ts b/src/main/resources/scriptcraft/minecraft/entity.ts new file mode 100644 index 0000000..76f36fd --- /dev/null +++ b/src/main/resources/scriptcraft/minecraft/entity.ts @@ -0,0 +1,233 @@ +import { ItemStack } from './item'; +import { useBridge, Hand, Identifier, Pos } from './core'; +import { World } from './world'; + +/** + * Damage Source + */ +export class DamageSource { + /** + * @internal + */ + javaObject: JavaObject; + + /** + * @internal + */ + constructor(javaObject: JavaObject) { + this.javaObject = javaObject; + } + + /** + * @internal + */ + private static build(bridge: string, value: BridgeValueType): DamageSource { + const obj = useBridge(bridge, value) as JavaObject; + if (obj) { + return new DamageSource(obj); + } else { + return null; + } + } + + /** + * Create Damage Source from a name + * @param value Name + * @returns Damage Source + */ + static create(value: string): DamageSource { + return this.build('DamageSource.create', value); + } + + /** + * Create Damage Source from a player ({@link PlayerEntity}) + * @param value Player + * @returns Damage Source + */ + static createFromPlayer(value: PlayerEntity): DamageSource { + return this.build('DamageSource.createFromPlayer', value.javaObject); + } + + /** + * Create Damage Source from a mob ({@link LivingEntity}) + * @param value Mob + * @returns Damage Source + */ + static createFromMob(value: LivingEntity): DamageSource { + return this.build('DamageSource.createFromMob', value.javaObject); + } +} + +/** + * Entity + */ +export class Entity { + /** + * @internal + */ + javaObject: JavaObject; + + /** + * @internal + */ + constructor(object: JavaObject|Entity) { + if (object == null) { + return null; + } + if (object instanceof Entity) { + this.javaObject = object.javaObject; + } else { + this.javaObject = object; + } + } + + /** + * Get Entity World + * @returns Entity World + */ + getEntityWorld(): World { + const obj = useBridge('Entity.getEntityWorld', this.javaObject) as JavaObject; + if (obj) { + return new World(obj); + } else { + return null; + } + } + + /** + * Get Entity ID + * @returns Entity ID + */ + getID(): Identifier { + const obj = useBridge('Entity.getID', this.javaObject) as string; + if (obj) { + return new Identifier(obj); + } else { + return null; + } + } + + /** + * Get Entity Name + * @returns Entity Name + */ + getName(): string { + return useBridge('Entity.getName', this.javaObject) as string; + } + + /** + * Get Entity Display Name + * @returns Entity Display Name + */ + getDisplayName(): string { + return useBridge('Entity.getDisplayName', this.javaObject) as string; + } + + /** + * Get Entity Custom Name + * @returns Entity Custom Name + */ + getCustomName(): string { + return useBridge('Entity.getCustomName', this.javaObject) as string; + } + + /** + * Convert To String + */ + toString(): string { + return this.getName(); + } + + /** + * Kill Entity + */ + kill() { + useBridge('Entity.kill', this.javaObject); + } + + /** + * Remove Entity + */ + remove() { + useBridge('Entity.remove', this.javaObject); + } + + /** + * Set Entity Fire Ticks + * @param ticks Fire Ticks + */ + setFireTicks(ticks: number) { + useBridge('Entity.setFireTicks', this.javaObject, ticks); + } + + /** + * Get Entity Fire Ticks + * @returns Fire Ticks + */ + getFireTicks(): number { + return useBridge('Entity.getFireTicks', this.javaObject) as number; + } + + /** + * Damage Entity + * @param source Damage Source + * @param amount Damage Amount + */ + damage(source: DamageSource, amount: number) { + useBridge('Entity.damage', source.javaObject, amount); + } + + /** + * Get Entity Position + * @returns Entity Position + */ + getPosition(): Pos { + const pos: number[] = useBridge('Entity.getPosition', this.javaObject) as number[]; + if (pos && pos.length === 3) { + return new Pos(pos[0], pos[1], pos[2]); + } else { + return null; + } + } + + /** + * Set Entity Position + * @param pos Entity Position + */ + setPosition(pos: Pos) { + useBridge('Entity.setPosition', this.javaObject, pos.getX(), pos.getY(), pos.getZ()); + } +} + +/** + * Living Entity + */ +export class LivingEntity extends Entity { + /** + * Get Stack in {@link Hand} + * @param hand Hand + * @returns Item Stack + */ + getStackInHand(hand: Hand): ItemStack { + const obj = useBridge('LivingEntity.getStackInHand', this.javaObject, hand.toString()) as JavaObject; + if (obj) { + return new ItemStack(obj); + } else { + return null; + } + } + + /** + * Get Health + * @returns Entity Health + */ + getHealth(): number { + return useBridge('LivingEntity.getHealth', this.javaObject) as number; + } +} + +/** + * Player Entity + */ +export class PlayerEntity extends LivingEntity { +} \ No newline at end of file diff --git a/src/main/resources/scriptcraft/minecraft/index.ts b/src/main/resources/scriptcraft/minecraft/index.ts new file mode 100644 index 0000000..61248fe --- /dev/null +++ b/src/main/resources/scriptcraft/minecraft/index.ts @@ -0,0 +1,6 @@ +export { Identifier, ActionResult, Hand, Pos, Direction, DirectionUtil } from './core'; +export { CustomBlock, BlockSettings, BlockState } from './block'; +export { ItemStack, ItemSettings, CustomItem, BlockItem } from './item'; +export { World } from './world'; +export { LivingEntity, PlayerEntity } from './entity'; +export { Registry } from './registry'; \ No newline at end of file diff --git a/src/main/resources/scriptcraft/minecraft/item.ts b/src/main/resources/scriptcraft/minecraft/item.ts new file mode 100644 index 0000000..c4eb65a --- /dev/null +++ b/src/main/resources/scriptcraft/minecraft/item.ts @@ -0,0 +1,221 @@ +import { useBridge, Identifier, Hand, ActionResult, addBridge, Pos, Direction, SingleRegistry } from './core'; +import { World } from './world'; +import { PlayerEntity, LivingEntity } from './entity'; + +/** + * Item Stack + */ +export class ItemStack { + /** + * @internal + */ + javaObject: JavaObject; + + /** + * @internal + */ + constructor(obj: JavaObject) { + this.javaObject = obj; + } + + /** + * Create Item Stack + * @param item Item ID + * @param count Item Count + */ + static create(item: Identifier, count?: number): ItemStack { + const obj = useBridge('ItemStack.create', item.toString(), count ? count : 1) as JavaObject; + if (obj) { + return new ItemStack(obj); + } else { + return null; + } + } + + /** + * Get Item ID + * @returns Item ID + */ + getItem(): Identifier { + const obj = useBridge('ItemStack.getItem', this.javaObject) as string; + if (obj) { + return new Identifier(obj); + } else { + return null; + } + } + + /** + * Get Item Count + * @returns Item Count + */ + getCount(): number { + return useBridge('ItemStack.getCount', this.javaObject) as number; + } +} + +/** + * Item Rarity + */ +enum ItemRarity { + COMMON = 'COMMON', + UNCOMMON = 'UNCOMMON', + RARE = 'RARE', + EPIC = 'EPIC' +} + +/** + * Settings for {@link CustomItem} and {@link BlockItem} + */ +export class ItemSettings { + #maxCount: number; + #itemGroup: string; + #rarity: ItemRarity; + + /** + * Create Item Settings + */ + constructor() { + this.#maxCount = 64; + this.#rarity = ItemRarity.COMMON; + } + + /** + * Set Max Count + * @param maxCount Max Count + */ + setMaxCount(maxCount: number): ItemSettings { + this.#maxCount = maxCount; + return this; + } + + /** + * Set Item Group + * @param itemGroup Item Group + */ + setItemGroup(itemGroup: string | Identifier): ItemSettings { + this.#itemGroup = itemGroup.toString(); + return this; + } + + /** + * Set Item Rarity + * @param rarity Item Rarity + */ + setRarity(rarity: ItemRarity): ItemSettings { + this.#rarity = rarity; + return this; + } + + /** + * @internal + */ + createJavaObject(): JavaObject { + return useBridge('ItemSettings.create', this.#maxCount, this.#rarity, this.#itemGroup) as JavaObject; + } +} + +/** + * Custom Item + */ +export abstract class CustomItem { + /** + * @internal + */ + settings: ItemSettings; + + constructor(settings: ItemSettings) { + this.settings = settings; + } + + /** + * Called When The Item Is Used + * @param world World + * @param player Player + * @param hand Hand + * @returns Action Result + */ + abstract onUse(world: World, player: PlayerEntity, hand: Hand): ActionResult; + + /** + * Called When The Item Is Used On A Block + * @param world World + * @param pos Block Position + * @param side Side Of The Block Used + * @param player Player + * @param hand Hand + * @returns Action Result + */ + abstract onUseOnBlock(world: World, pos: Pos, side: Direction, player: PlayerEntity, hand: Hand): ActionResult; + + /** + * Called When The Item Is Used On An Entity + * @param player Player + * @param target Target Entity + * @param hand Hand + * @returns Action Result + */ + abstract onUseOnEntity(player: PlayerEntity, target: LivingEntity, hand: Hand): ActionResult; +} + +/** + * Block Item + */ +export class BlockItem { + /** + * @internal + */ + settings: ItemSettings; + /** + * @internal + */ + block: Identifier; + + /** + * Create Block Item + * @param block Block ID + * @param settings Item Settings + */ + constructor(block: Identifier, settings: ItemSettings) { + this.settings = settings; + this.block = block; + } +} + +/** + * @internal + */ +export class ItemRegistry implements SingleRegistry { + static INSTANCE = new ItemRegistry(); + + #items: Map; + + private constructor() { + this.#items = new Map(); + } + + register(id: Identifier, obj: CustomItem | BlockItem) { + if (obj instanceof CustomItem) { + this.#items.set(id.toString(), obj); + useBridge('Registry.registerItem', id.toString(), obj.settings.createJavaObject()); + } else { + useBridge('Registry.registerBlockItem', id.toString(), obj.settings.createJavaObject(), obj.block.toString()); + } + } + + get(id: Identifier): CustomItem { + return this.#items.get(id.toString()); + } +} + +addBridge('CustomItem.onUse', (id: string, world: JavaObject, player: JavaObject, hand: keyof typeof Hand): string => { + return ItemRegistry.INSTANCE.get(new Identifier(id)).onUse(new World(world), new PlayerEntity(player), Hand[hand]); +}); + +addBridge('CustomItem.onUseOnBlock', (id: string, world: JavaObject, x: number, y: number, z: number, side: keyof typeof Direction, player: JavaObject, hand: keyof typeof Hand): string => { + return ItemRegistry.INSTANCE.get(new Identifier(id)).onUseOnBlock(new World(world), new Pos(x, y, z), Direction[side], new PlayerEntity(player), Hand[hand]); +}); + +addBridge('CustomItem.onUseOnEntity', (id: string, player: JavaObject, target: JavaObject, hand: keyof typeof Hand): boolean => { + return ItemRegistry.INSTANCE.get(new Identifier(id)).onUseOnEntity(new PlayerEntity(player), new LivingEntity(target), Hand[hand]) === ActionResult.SUCCESS ? true : false; +}); diff --git a/src/main/resources/scriptcraft/minecraft/registry.ts b/src/main/resources/scriptcraft/minecraft/registry.ts new file mode 100644 index 0000000..30f822f --- /dev/null +++ b/src/main/resources/scriptcraft/minecraft/registry.ts @@ -0,0 +1,37 @@ +import { Identifier, SingleRegistry } from './core'; +import { BlockRegistry } from './block'; +import { ItemRegistry } from './item'; + +/** + * Registry + */ +export abstract class Registry { + /** + * Block Registry + */ + static BLOCK = BlockRegistry.INSTANCE; + /** + * Item Registry + */ + static ITEM = ItemRegistry.INSTANCE; + + /** + * Register Object + * @param registry Target Registry + * @param id ID + * @param obj Object + */ + static register(registry: SingleRegistry, id: Identifier, obj: T): T { + registry.register(id, obj); + return obj; + } + + /** + * Get Object From Registry + * @param registry Target Registry + * @param id ID + */ + static get(registry: SingleRegistry, id: Identifier): T { + return registry.get(id); + } +} \ No newline at end of file diff --git a/src/main/resources/scriptcraft/minecraft/world.ts b/src/main/resources/scriptcraft/minecraft/world.ts new file mode 100644 index 0000000..1187451 --- /dev/null +++ b/src/main/resources/scriptcraft/minecraft/world.ts @@ -0,0 +1,58 @@ +import { BlockState } from './block'; +import { Entity } from './entity'; +import { useBridge, Pos, Identifier } from './core'; + +/** + * World + */ +export class World { + /** + * @internal + */ + javaObject: JavaObject; + + /** + * @internal + */ + constructor(javaObject: JavaObject) { + this.javaObject = javaObject; + } + + /** + * Set Block State + * @param pos Position + * @param state Block State + * @returns True If Successful + */ + setBlockState(pos: Pos, state: BlockState): boolean { + return useBridge('World.setBlockState', this.javaObject, pos.getX(), pos.getY(), pos.getZ(), state.javaObject) as boolean; + } + + /** + * Get Block State + * @param pos Position + * @returns Block State + */ + getBlockState(pos: Pos): BlockState { + const obj = useBridge('World.getBlockState', this.javaObject, pos.getX(), pos.getY(), pos.getZ()) as JavaObject; + if (obj) { + return new BlockState(obj); + } else { + return null; + } + } + + /** + * Spawn Entity + * @param id Entity ID + * @param pos Position + */ + spawnEntity(id: Identifier, pos: Pos): Entity { + const obj = useBridge('World.spawnEntity', this.javaObject, pos.getX(), pos.getY(), pos.getZ(), id.toString()) as JavaObject; + if (obj) { + return new Entity(obj); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/resources/scriptcraft/test/index.ts b/src/main/resources/scriptcraft/test/index.ts new file mode 100644 index 0000000..2ecfecd --- /dev/null +++ b/src/main/resources/scriptcraft/test/index.ts @@ -0,0 +1,41 @@ +import { CustomBlock, BlockSettings, Identifier, Registry, BlockState, ActionResult, World, Pos, Hand, PlayerEntity, BlockItem, ItemSettings, CustomItem, Direction, LivingEntity } from 'minecraft'; + +console.log('hello'); + +class MyBlock extends CustomBlock { + constructor() { + super(new BlockSettings('STONE', 'IRON')); + } + + onUse(world: World, blockState: BlockState, blockPos: Pos, side: Direction, player: PlayerEntity, hand: Hand): ActionResult { + world.setBlockState(blockPos.offset(side), BlockState.getDefaultState(new Identifier('minecraft:stone'))); + return ActionResult.SUCCESS; + } +} + +Registry.register(Registry.BLOCK, new Identifier('test', 'my_block'), new MyBlock()); +Registry.register(Registry.ITEM, new Identifier('test', 'my_block'), new BlockItem(new Identifier('test', 'my_block'), new ItemSettings())); + +class MyItem extends CustomItem { + constructor() { + super(new ItemSettings().setItemGroup('building_blocks').setMaxCount(128)); + } + + onUse(world: World, player: PlayerEntity, hand: Hand): ActionResult { + console.log('Item Use: Normal'); + return ActionResult.SUCCESS; + } + + onUseOnBlock(world: World, blockPos: Pos, side: Direction, player: PlayerEntity, hand: Hand): ActionResult { + console.log('Item Use: Block ' + blockPos.toString()); + world.spawnEntity(new Identifier('minecraft:cow'), blockPos.offset(side).add(new Pos(0.5, 0, 0.5))).toString(); + return ActionResult.SUCCESS; + } + + onUseOnEntity(player: PlayerEntity, target: LivingEntity, hand: Hand): ActionResult { + console.log('Item Use: Entity ' + target.getPosition()); + return ActionResult.SUCCESS; + } +} + +Registry.register(Registry.ITEM, new Identifier('test', 'my_item'), new MyItem()); diff --git a/src/main/resources/scriptcraft/tsconfig.json b/src/main/resources/scriptcraft/tsconfig.json new file mode 100644 index 0000000..55f600f --- /dev/null +++ b/src/main/resources/scriptcraft/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "lib": ["es2020"], + "module": "es2020", + "target": "es2020", + "typeRoots": ["types"], + "rootDir": ".", + "baseUrl": ".", + "paths": { + "minecraft": ["minecraft/index"] + } + }, + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/src/main/resources/scriptcraft/typedoc.json b/src/main/resources/scriptcraft/typedoc.json new file mode 100644 index 0000000..2102109 --- /dev/null +++ b/src/main/resources/scriptcraft/typedoc.json @@ -0,0 +1,7 @@ +{ + "mode": "modules", + "readme": "none", + "excludeNotExported": true, + "excludePrivate": true, + "stripInternal": true +} \ No newline at end of file diff --git a/src/main/resources/scriptcraft/types/scriptcraft/index.d.ts b/src/main/resources/scriptcraft/types/scriptcraft/index.d.ts new file mode 100644 index 0000000..863f251 --- /dev/null +++ b/src/main/resources/scriptcraft/types/scriptcraft/index.d.ts @@ -0,0 +1,28 @@ +interface JavaObject { + discriminator: 'JavaObject'; +} + +type BridgeValueType = string | number | boolean | JavaObject | BridgeValueType[]; +type BridgeType = (...args: BridgeValueType[]) => BridgeValueType; + +interface ScriptCraft { + useBridge(name: string, ...args: BridgeValueType[]): BridgeValueType; + + bridges: { [key: string]: BridgeType }; +} + +declare const __scriptcraft__: ScriptCraft; + +interface Console { + log(...args: string[]): void; + info(...args: string[]): void; + warn(...args: string[]): void; + error(...args: string[]): void; + dir(...args: string[]): void; + debug(...args: string[]): void; + trace(...args: string[]): void; + + assert(condition: boolean, ...args: string[]): void; +} + +declare const console: Console;