commit 6b6c4a91b8d42e825f0c0bfa3cb9b17b08db76ed Author: TheBrokenRail Date: Sat Apr 25 09:33:17 2020 -0400 Initial Commit 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 0000000..5c2d1cf Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5baddfb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Feb 29 21:58:32 EST 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..8e25e6c --- /dev/null +++ b/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + 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 0000000..047b91f Binary files /dev/null and b/src/main/resources/assets/scriptcraft/icon.png differ 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;