diff --git a/.github/workflows/mod-ci.yml b/.github/workflows/mod-ci.yml new file mode 100644 index 0000000..3c1a69b --- /dev/null +++ b/.github/workflows/mod-ci.yml @@ -0,0 +1,196 @@ +name: Mod CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + defaults: + run: + working-directory: mod + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: "8.10.2" + + - name: Validate matrix and run common tests + run: | + chmod +x ./gradlew + ./gradlew validateModMatrix :common:test --stacktrace --info + + - name: Upload Gradle reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: mod-validate-gradle-reports + path: | + mod/**/build/reports/** + mod/**/build/test-results/** + + build: + runs-on: ubuntu-latest + needs: validate + strategy: + fail-fast: false + matrix: + include: + - minecraft-version: "1.21.2" + loader: fabric + - minecraft-version: "1.21.2" + loader: neoforge + - minecraft-version: "1.21.4" + loader: fabric + - minecraft-version: "1.21.4" + loader: forge + - minecraft-version: "1.21.4" + loader: neoforge + - minecraft-version: "1.21.5" + loader: fabric + - minecraft-version: "1.21.5" + loader: forge + - minecraft-version: "1.21.5" + loader: neoforge + - minecraft-version: "1.21.6" + loader: fabric + - minecraft-version: "1.21.6" + loader: forge + - minecraft-version: "1.21.6" + loader: neoforge + - minecraft-version: "1.21.7" + loader: fabric + - minecraft-version: "1.21.7" + loader: forge + - minecraft-version: "1.21.7" + loader: neoforge + - minecraft-version: "1.21.8" + loader: fabric + - minecraft-version: "1.21.8" + loader: forge + - minecraft-version: "1.21.8" + loader: neoforge + - minecraft-version: "1.21.9" + loader: fabric + - minecraft-version: "1.21.9" + loader: forge + - minecraft-version: "1.21.9" + loader: neoforge + - minecraft-version: "1.21.10" + loader: fabric + - minecraft-version: "1.21.10" + loader: forge + - minecraft-version: "1.21.10" + loader: neoforge + - minecraft-version: "1.21.11" + loader: fabric + - minecraft-version: "1.21.11" + loader: forge + - minecraft-version: "1.21.11" + loader: neoforge + + defaults: + run: + working-directory: mod + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: "8.10.2" + + - name: Build matrix project + id: build + env: + MINECRAFT_VERSION: ${{ matrix.minecraft-version }} + LOADER: ${{ matrix.loader }} + run: | + chmod +x ./gradlew + set -o pipefail + VERSION_KEY="${MINECRAFT_VERSION//./_}" + TASK=":versions:mc_${VERSION_KEY}:${LOADER}:build" + LOG_FILE="build-${MINECRAFT_VERSION}-${LOADER}.log" + echo "Building ${TASK}" + ./gradlew "${TASK}" --stacktrace --info 2>&1 | tee "${LOG_FILE}" + echo "log_file=${LOG_FILE}" >> "${GITHUB_OUTPUT}" + + - name: Write failure summary + if: failure() + env: + MINECRAFT_VERSION: ${{ matrix.minecraft-version }} + LOADER: ${{ matrix.loader }} + run: | + LOG_FILE="${{ steps.build.outputs.log_file }}" + VERSION_KEY="${MINECRAFT_VERSION//./_}" + { + echo "### Mod build failed: ${LOADER} / ${MINECRAFT_VERSION}" + echo "" + echo "**Gradle task:** \`:versions:mc_${VERSION_KEY}:${LOADER}:build\`" + echo "" + if [ -f "${LOG_FILE}" ]; then + echo "#### Error highlights" + echo '```text' + grep -E "FAILURE:|What went wrong:|Execution failed for task|error:|Caused by:|> Could not|BUILD FAILED" "${LOG_FILE}" | tail -n 40 || true + echo '```' + echo "" + echo "#### Last 120 log lines" + echo '```text' + tail -n 120 "${LOG_FILE}" + echo '```' + else + echo "No build log was captured." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Upload Gradle reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: mod-${{ matrix.minecraft-version }}-${{ matrix.loader }}-gradle-reports + path: | + mod/**/build/reports/** + mod/**/build/test-results/** + mod/**/build/tmp/** + mod/build-*.log + + summary: + runs-on: ubuntu-latest + needs: [validate, build] + if: always() + steps: + - name: Report overall mod CI status + run: | + { + echo "### Mod CI summary" + echo "" + echo "- validate: ${{ needs.validate.result }}" + echo "- build matrix: ${{ needs.build.result }}" + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${{ needs.validate.result }}" != "success" ] || [ "${{ needs.build.result }}" != "success" ]; then + echo "One or more mod CI jobs failed. Open the failed matrix job for the detailed Gradle log summary." + exit 1 + fi diff --git a/.gitignore b/.gitignore index 40114b9..8dc3aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ highlighter/node_modules/ highlighter/dist/ highlighter/*.vsix npm/node_modules/ -npm/*.tgz \ No newline at end of file +npm/*.tgz +mod/.gradle/ +mod/build/ +mod/**/build/ diff --git a/minecraft_script/shell_commands.py b/minecraft_script/shell_commands.py index 6b667a7..af20fdd 100644 --- a/minecraft_script/shell_commands.py +++ b/minecraft_script/shell_commands.py @@ -6,7 +6,7 @@ from .common import COMMON_CONFIG, version from .config_utils import update_config, reset_config from .lint import lint_code -from .version_config import breaking_changes_between +from .version_config import breaking_changes_between, load_version_profile from pathlib import Path @@ -80,6 +80,16 @@ def sh_debug(*args) -> None: def sh_compile(*args) -> None: # Manage args & parameters: + args = list(args) + minecraft_version = None + if "--minecraft-version" in args: + version_index = args.index("--minecraft-version") + if version_index + 1 >= len(args): + print("No version specified after --minecraft-version.") + exit(1) + minecraft_version = args[version_index + 1] + del args[version_index:version_index + 2] + arg_count = len(args) if arg_count < 1: print("No path specified to compile.") @@ -114,7 +124,19 @@ def sh_compile(*args) -> None: with open(source_path, 'rt', encoding='utf-8') as mcs_file: code = mcs_file.read() - build_datapack(code, datapack_name, str(output_path), verbose, source_path=source_path) + previous_version = COMMON_CONFIG["minecraft_version"] + if minecraft_version is not None: + try: + load_version_profile(minecraft_version) + except FileNotFoundError as error: + print(f"Error: {error}") + exit(1) + COMMON_CONFIG["minecraft_version"] = minecraft_version + + try: + build_datapack(code, datapack_name, str(output_path), verbose, source_path=source_path) + finally: + COMMON_CONFIG["minecraft_version"] = previous_version def sh_config(*args) -> None: diff --git a/mod/build.gradle b/mod/build.gradle new file mode 100644 index 0000000..874935e --- /dev/null +++ b/mod/build.gradle @@ -0,0 +1,121 @@ +plugins { + id "java" + id "dev.architectury.loom" version "${architectury_loom_version}" apply false + id "architectury-plugin" version "${architectury_plugin_version}" apply false +} + +ext.modSupportedMinecraftVersions = [ + "1.21.2", + "1.21.4", + "1.21.5", + "1.21.6", + "1.21.7", + "1.21.8", + "1.21.9", + "1.21.10", + "1.21.11", +] + +ext.modLoaders = ["fabric", "forge", "neoforge"] + +ext.fabricApiVersions = [ + "1.21.2": "0.106.1+1.21.2", + "1.21.4": "0.119.4+1.21.4", + "1.21.5": "0.128.2+1.21.5", + "1.21.6": "0.128.2+1.21.6", + "1.21.7": "0.129.0+1.21.7", + "1.21.8": "0.136.1+1.21.8", + "1.21.9": "0.134.1+1.21.9", + "1.21.10": "0.138.4+1.21.10", + "1.21.11": "0.141.4+1.21.11", +] + +ext.forgeVersions = [ + "1.21.2": null, + "1.21.4": "1.21.4-54.1.16", + "1.21.5": "1.21.5-55.1.10", + "1.21.6": "1.21.6-56.0.4", + "1.21.7": "1.21.7-57.0.3", + "1.21.8": "1.21.8-58.1.18", + "1.21.9": "1.21.9-59.0.5", + "1.21.10": "1.21.10-60.1.9", + "1.21.11": "1.21.11-61.1.8", +] + +ext.neoForgeVersions = [ + "1.21.2": "21.2.1-beta", + "1.21.4": "21.4.157", + "1.21.5": "21.5.97", + "1.21.6": "21.6.20-beta", + "1.21.7": "21.7.25-beta", + "1.21.8": "21.8.53", + "1.21.9": "21.9.16-beta", + "1.21.10": "21.10.64", + "1.21.11": "21.11.42", +] + +allprojects { + group = mod_group + version = mod_version +} + +subprojects { project -> + apply plugin: "java" + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } + + tasks.withType(JavaCompile).configureEach { + options.encoding = "UTF-8" + options.release = 21 + } + + repositories { + maven { url = "https://maven.fabricmc.net/" } + maven { url = "https://maven.minecraftforge.net/" } + maven { url = "https://maven.neoforged.net/releases/" } + maven { url = "https://maven.architectury.dev/" } + mavenCentral() + } +} + +project(":common") { + dependencies { + testImplementation platform("org.junit:junit-bom:5.10.3") + testImplementation "org.junit.jupiter:junit-jupiter" + } + + test { + useJUnitPlatform() + } +} + +tasks.register("validateModMatrix") { + inputs.file(rootProject.file("../minecraft_script/versions/index.json")) + doLast { + def index = new groovy.json.JsonSlurper().parse(rootProject.file("../minecraft_script/versions/index.json")) + def expectedVersions = index.supported.findAll { it != "26.1" } + if (expectedVersions != modSupportedMinecraftVersions) { + throw new GradleException("modSupportedMinecraftVersions must equal versions/index.json supported minus 26.1. expected=${expectedVersions}, actual=${modSupportedMinecraftVersions}") + } + + def missingProjects = [] + expectedVersions.each { mcVersion -> + modLoaders.each { loader -> + if (loader == "forge" && forgeVersions[mcVersion] == null) { + return + } + def projectPath = ":versions:mc_${mcVersion.replace(".", "_")}:${loader}" + if (findProject(projectPath) == null) { + missingProjects.add(projectPath) + } + } + } + if (!missingProjects.isEmpty()) { + throw new GradleException("Missing mod matrix projects: ${missingProjects.join(", ")}") + } + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCommandLine.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCommandLine.java new file mode 100644 index 0000000..ccf3a9d --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCommandLine.java @@ -0,0 +1,54 @@ +package dev.spyc0der.minecraftscript; + +import java.util.ArrayList; +import java.util.List; + +public final class McsCommandLine { + private McsCommandLine() { + } + + public static List split(String command) { + List parts = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + boolean escaping = false; + + for (int index = 0; index < command.length(); index++) { + char character = command.charAt(index); + if (escaping) { + current.append(character); + escaping = false; + continue; + } + if (character == '\\') { + escaping = true; + continue; + } + if (character == '\'' && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + continue; + } + if (character == '"' && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + continue; + } + if (Character.isWhitespace(character) && !inSingleQuote && !inDoubleQuote) { + if (!current.isEmpty()) { + parts.add(current.toString()); + current.setLength(0); + } + continue; + } + current.append(character); + } + + if (escaping) { + current.append('\\'); + } + if (!current.isEmpty()) { + parts.add(current.toString()); + } + return parts; + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompileResult.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompileResult.java new file mode 100644 index 0000000..e60388f --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompileResult.java @@ -0,0 +1,7 @@ +package dev.spyc0der.minecraftscript; + +public record McsCompileResult(int exitCode, String output) { + public boolean succeeded() { + return exitCode == 0; + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompiler.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompiler.java new file mode 100644 index 0000000..1349131 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompiler.java @@ -0,0 +1,9 @@ +package dev.spyc0der.minecraftscript; + +import java.io.IOException; +import java.nio.file.Path; + +public interface McsCompiler { + McsCompileResult compile(Path packFile, String datapackName, Path outputRoot, String minecraftVersion) + throws IOException, InterruptedException; +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompilerConfig.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompilerConfig.java new file mode 100644 index 0000000..1c1c043 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsCompilerConfig.java @@ -0,0 +1,43 @@ +package dev.spyc0der.minecraftscript; + +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +public record McsCompilerConfig(String pythonCommand) { + public static final String SYSTEM_PROPERTY = "minecraft_script.pythonCommand"; + public static final String ENVIRONMENT_VARIABLE = "MCS_PYTHON_COMMAND"; + public static final String CONFIG_FILE = "mcs-mod.properties"; + public static final String CONFIG_KEY = "pythonCommand"; + public static final String DEFAULT_COMMAND = "python -m minecraft_script"; + + public static McsCompilerConfig load(Path mcsRoot) { + String configured = System.getProperty(SYSTEM_PROPERTY); + if (configured == null || configured.isBlank()) { + configured = System.getenv(ENVIRONMENT_VARIABLE); + } + if ((configured == null || configured.isBlank()) && mcsRoot != null) { + configured = readConfigFile(mcsRoot.resolve(CONFIG_FILE)); + } + if (configured == null || configured.isBlank()) { + configured = DEFAULT_COMMAND; + } + return new McsCompilerConfig(configured.trim()); + } + + private static String readConfigFile(Path configFile) { + if (!Files.isRegularFile(configFile)) { + return null; + } + + Properties properties = new Properties(); + try (Reader reader = Files.newBufferedReader(configFile)) { + properties.load(reader); + } catch (IOException ignored) { + return null; + } + return properties.getProperty(CONFIG_KEY); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsModRuntime.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsModRuntime.java new file mode 100644 index 0000000..c6f4f0e --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsModRuntime.java @@ -0,0 +1,42 @@ +package dev.spyc0der.minecraftscript; + +import java.io.IOException; + +public final class McsModRuntime { + private static McsPackWatcher activeWatcher; + + private McsModRuntime() { + } + + public static synchronized void serverStarted(McsServerAccess serverAccess) { + serverStopping(); + + try { + McsWorldPackManager packManager = new McsWorldPackManager( + serverAccess, + new ProcessMcsCompiler(McsCompilerConfig.load(McsPaths.mcsPacksRoot(serverAccess.saveRoot()))) + ); + packManager.initializeWorldFolders(); + if (packManager.compileAll()) { + serverAccess.executeReload(); + } + activeWatcher = new McsPackWatcher(packManager, serverAccess); + activeWatcher.start(); + } catch (IOException error) { + String message = "Could not initialize mcs_packs: " + error.getMessage(); + System.out.println("[Minecraft Script] " + message); + serverAccess.notifyOperators(message); + } + } + + public static synchronized void serverStopping() { + if (activeWatcher == null) { + return; + } + try { + activeWatcher.close(); + } catch (IOException ignored) { + } + activeWatcher = null; + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsPackWatcher.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsPackWatcher.java new file mode 100644 index 0000000..27729ab --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsPackWatcher.java @@ -0,0 +1,252 @@ +package dev.spyc0der.minecraftscript; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.ClosedWatchServiceException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class McsPackWatcher implements Closeable { + private static final long PACK_SETTLE_MILLIS = 150; + private static final long PACK_MAX_WAIT_MILLIS = 400; + private static final long RELOAD_SETTLE_MILLIS = 75; + private static final long MTIME_POLL_INTERVAL_MILLIS = 200; + + private final McsWorldPackManager packManager; + private final McsServerAccess serverAccess; + private final WatchService watchService; + private final Map keys = new ConcurrentHashMap<>(); + private final Map> pendingCompiles = new ConcurrentHashMap<>(); + private final Map firstChangeAt = new ConcurrentHashMap<>(); + private final Map lastSeenMtime = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> { + Thread thread = new Thread(runnable, "mcs-pack-watcher-scheduler"); + thread.setDaemon(true); + return thread; + }); + private final ExecutorService compileExecutor = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "mcs-pack-watcher-compiler"); + thread.setDaemon(true); + return thread; + }); + private final AtomicBoolean reloadPending = new AtomicBoolean(false); + private final Thread watcherThread; + private volatile boolean running = true; + + public McsPackWatcher(McsWorldPackManager packManager, McsServerAccess serverAccess) throws IOException { + this.packManager = packManager; + this.serverAccess = serverAccess; + this.watchService = packManager.mcsRoot().getFileSystem().newWatchService(); + registerRecursively(packManager.mcsRoot()); + seedPackMtines(); + scheduler.scheduleAtFixedRate( + this::pollPackMtines, + MTIME_POLL_INTERVAL_MILLIS, + MTIME_POLL_INTERVAL_MILLIS, + TimeUnit.MILLISECONDS + ); + this.watcherThread = new Thread(this::watchLoop, "mcs-pack-watcher"); + this.watcherThread.setDaemon(true); + } + + public void start() { + watcherThread.start(); + } + + static long computeCompileDelayMillis(long firstChangeAtMillis, long nowMillis) { + long elapsed = nowMillis - firstChangeAtMillis; + if (elapsed >= PACK_MAX_WAIT_MILLIS) { + return 0; + } + return Math.min(PACK_SETTLE_MILLIS, PACK_MAX_WAIT_MILLIS - elapsed); + } + + private void watchLoop() { + while (running) { + WatchKey key; + try { + key = watchService.take(); + } catch (ClosedWatchServiceException ignored) { + return; + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + return; + } + + Path directory = keys.get(key); + if (directory == null) { + key.reset(); + continue; + } + + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + scheduleFullScan(); + continue; + } + + Path changed = directory.resolve((Path) event.context()); + if (shouldIgnoreChange(changed)) { + continue; + } + + if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE && Files.isDirectory(changed)) { + try { + registerRecursively(changed); + } catch (IOException error) { + System.out.println("[Minecraft Script] Could not watch " + changed + ": " + error.getMessage()); + } + } + + Optional packFolder = McsPaths.packFolderForChangedPath(packManager.mcsRoot(), changed); + packFolder.ifPresent(this::schedulePackRefresh); + } + + if (!key.reset()) { + keys.remove(key); + } + } + } + + private void scheduleFullScan() { + scheduler.schedule(() -> compileExecutor.execute(() -> { + if (packManager.compileAll()) { + scheduleReload(); + } + }), PACK_SETTLE_MILLIS, TimeUnit.MILLISECONDS); + } + + private void schedulePackRefresh(Path packFolder) { + Path normalized = packFolder.toAbsolutePath().normalize(); + long now = System.currentTimeMillis(); + if (firstChangeAt.putIfAbsent(normalized, now) == null) { + packManager.notifyPackChangePending(packFolder); + } + + ScheduledFuture existing = pendingCompiles.remove(normalized); + if (existing != null) { + existing.cancel(false); + } + + long delay = computeCompileDelayMillis(firstChangeAt.get(normalized), now); + ScheduledFuture future = scheduler.schedule(() -> compileExecutor.execute(() -> { + pendingCompiles.remove(normalized); + firstChangeAt.remove(normalized); + if (packManager.refreshPack(normalized)) { + scheduleReload(); + } + }), delay, TimeUnit.MILLISECONDS); + pendingCompiles.put(normalized, future); + } + + private void scheduleReload() { + if (!reloadPending.compareAndSet(false, true)) { + return; + } + scheduler.schedule(() -> { + reloadPending.set(false); + serverAccess.executeReload(); + }, RELOAD_SETTLE_MILLIS, TimeUnit.MILLISECONDS); + } + + private void seedPackMtines() { + try { + for (Path packFolder : packManager.discoverPackFolders()) { + rememberPackMtime(packFolder); + } + } catch (IOException error) { + System.out.println("[Minecraft Script] Could not seed pack mtimes: " + error.getMessage()); + } + } + + private void pollPackMtines() { + if (!running) { + return; + } + try { + List packFolders = packManager.discoverPackFolders(); + for (Path packFolder : packFolders) { + Path packFile = packFolder.resolve(McsPaths.PACK_FILE); + if (!Files.isRegularFile(packFile)) { + continue; + } + long mtime = Files.getLastModifiedTime(packFile).toMillis(); + Path normalized = packFolder.toAbsolutePath().normalize(); + Long previous = lastSeenMtime.get(normalized); + if (previous != null && mtime > previous) { + lastSeenMtime.put(normalized, mtime); + schedulePackRefresh(packFolder); + continue; + } + lastSeenMtime.put(normalized, mtime); + } + } catch (IOException error) { + System.out.println("[Minecraft Script] Could not poll pack mtimes: " + error.getMessage()); + } + } + + private void rememberPackMtime(Path packFolder) throws IOException { + Path packFile = packFolder.resolve(McsPaths.PACK_FILE); + if (!Files.isRegularFile(packFile)) { + return; + } + Path normalized = packFolder.toAbsolutePath().normalize(); + lastSeenMtime.put(normalized, Files.getLastModifiedTime(packFile).toMillis()); + } + + private static boolean shouldIgnoreChange(Path changed) { + String fileName = changed.getFileName().toString(); + return fileName.endsWith("~") + || fileName.endsWith(".swp") + || fileName.endsWith(".tmp") + || fileName.startsWith(".#"); + } + + private void registerRecursively(Path root) throws IOException { + if (!Files.isDirectory(root)) { + return; + } + Files.walkFileTree(root, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException { + WatchKey key = directory.register( + watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE + ); + keys.put(key, directory); + return FileVisitResult.CONTINUE; + } + }); + } + + @Override + public void close() throws IOException { + running = false; + for (ScheduledFuture future : new HashMap<>(pendingCompiles).values()) { + future.cancel(false); + } + scheduler.shutdownNow(); + compileExecutor.shutdownNow(); + watchService.close(); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsPaths.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsPaths.java new file mode 100644 index 0000000..cc1b5b6 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsPaths.java @@ -0,0 +1,47 @@ +package dev.spyc0der.minecraftscript; + +import java.nio.file.Path; +import java.util.Locale; +import java.util.Optional; + +public final class McsPaths { + public static final String SOURCE_ROOT = "mcs_packs"; + public static final String DATAPACK_ROOT = "datapacks"; + public static final String PACK_FILE = "pack.mcs"; + public static final String GENERATED_PREFIX = "mcs_"; + + private McsPaths() { + } + + public static Path mcsPacksRoot(Path saveRoot) { + return saveRoot.resolve(SOURCE_ROOT); + } + + public static Path datapacksRoot(Path saveRoot) { + return saveRoot.resolve(DATAPACK_ROOT); + } + + public static Optional packFolderForChangedPath(Path mcsRoot, Path changedPath) { + Path normalizedRoot = mcsRoot.toAbsolutePath().normalize(); + Path normalizedChanged = changedPath.toAbsolutePath().normalize(); + if (!normalizedChanged.startsWith(normalizedRoot)) { + return Optional.empty(); + } + + Path relative = normalizedRoot.relativize(normalizedChanged); + if (relative.getNameCount() == 0) { + return Optional.empty(); + } + return Optional.of(normalizedRoot.resolve(relative.getName(0))); + } + + public static String sanitizePackFolderName(String folderName) { + String sanitized = folderName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9._-]+", "_"); + sanitized = sanitized.replaceAll("_+", "_").replaceAll("^_+|_+$", ""); + return sanitized.isBlank() ? "pack" : sanitized; + } + + public static String generatedDatapackName(Path packFolder) { + return GENERATED_PREFIX + sanitizePackFolderName(packFolder.getFileName().toString()); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsServerAccess.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsServerAccess.java new file mode 100644 index 0000000..05f1eee --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsServerAccess.java @@ -0,0 +1,13 @@ +package dev.spyc0der.minecraftscript; + +import java.nio.file.Path; + +public interface McsServerAccess { + Path saveRoot(); + + String minecraftVersion(); + + void executeReload(); + + void notifyOperators(String message); +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsStarterPack.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsStarterPack.java new file mode 100644 index 0000000..636a7e9 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsStarterPack.java @@ -0,0 +1,15 @@ +package dev.spyc0der.minecraftscript; + +public final class McsStarterPack { + public static final String SOURCE = """ + function init() { + tellraw("@a", text().text("MCS Starter Pack Loaded")); + } + + function main() { + } + """; + + private McsStarterPack() { + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsWorldPackManager.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsWorldPackManager.java new file mode 100644 index 0000000..2e4900a --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/McsWorldPackManager.java @@ -0,0 +1,189 @@ +package dev.spyc0der.minecraftscript; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class McsWorldPackManager { + private final Path saveRoot; + private final Path mcsRoot; + private final Path datapacksRoot; + private final String minecraftVersion; + private final McsCompiler compiler; + private final McsServerAccess serverAccess; + + public McsWorldPackManager(McsServerAccess serverAccess, McsCompiler compiler) { + this.serverAccess = Objects.requireNonNull(serverAccess, "serverAccess"); + this.compiler = Objects.requireNonNull(compiler, "compiler"); + this.saveRoot = serverAccess.saveRoot(); + this.mcsRoot = McsPaths.mcsPacksRoot(saveRoot); + this.datapacksRoot = McsPaths.datapacksRoot(saveRoot); + this.minecraftVersion = serverAccess.minecraftVersion(); + } + + public Path mcsRoot() { + return mcsRoot; + } + + public void initializeWorldFolders() throws IOException { + Files.createDirectories(mcsRoot); + Files.createDirectories(datapacksRoot); + + Path starterPack = mcsRoot.resolve("starter").resolve(McsPaths.PACK_FILE); + if (!Files.exists(starterPack)) { + Files.createDirectories(starterPack.getParent()); + Files.writeString(starterPack, McsStarterPack.SOURCE); + } + } + + public List discoverPackFolders() throws IOException { + List packs = new ArrayList<>(); + if (!Files.isDirectory(mcsRoot)) { + return packs; + } + + try (var stream = Files.list(mcsRoot)) { + stream.filter(Files::isDirectory) + .filter(path -> Files.isRegularFile(path.resolve(McsPaths.PACK_FILE))) + .sorted() + .forEach(packs::add); + } + return packs; + } + + public boolean compileAll() { + boolean changed = false; + try { + for (Path packFolder : discoverPackFolders()) { + changed |= compilePack(packFolder); + } + } catch (IOException error) { + report("Could not scan mcs_packs: " + error.getMessage()); + } + return changed; + } + + public void notifyPackChangePending(Path packFolder) { + report("Detected changes in " + packFolder.getFileName() + ", recompiling..."); + } + + public boolean refreshPack(Path packFolder) { + Path packFile = packFolder.resolve(McsPaths.PACK_FILE); + if (Files.isRegularFile(packFile)) { + return compilePack(packFolder); + } + return deleteGeneratedPack(packFolder); + } + + public boolean compilePack(Path packFolder) { + String datapackName = McsPaths.generatedDatapackName(packFolder); + Path temporaryRoot = null; + try { + temporaryRoot = Files.createTempDirectory(datapacksRoot, "." + datapackName + "-compile-"); + McsCompileResult result = compiler.compile( + packFolder.resolve(McsPaths.PACK_FILE), + datapackName, + temporaryRoot, + minecraftVersion + ); + if (!result.succeeded()) { + report("Failed to compile " + packFolder.getFileName() + ":\n" + result.output()); + return false; + } + + Path compiledDatapack = temporaryRoot.resolve(datapackName); + if (!Files.isDirectory(compiledDatapack)) { + report("Compiler finished without creating " + compiledDatapack); + return false; + } + + replaceGeneratedDatapack(compiledDatapack, datapacksRoot.resolve(datapackName)); + report("Compiled MCS pack " + packFolder.getFileName() + " for Minecraft " + minecraftVersion); + return true; + } catch (IOException error) { + report("Could not compile " + packFolder.getFileName() + ": " + error.getMessage()); + return false; + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + report("Compile interrupted for " + packFolder.getFileName()); + return false; + } finally { + if (temporaryRoot != null) { + try { + deleteRecursively(temporaryRoot); + } catch (IOException ignored) { + } + } + } + } + + public boolean deleteGeneratedPack(Path packFolder) { + Path target = datapacksRoot.resolve(McsPaths.generatedDatapackName(packFolder)); + if (!Files.exists(target)) { + return false; + } + try { + deleteRecursively(target); + report("Removed generated datapack " + target.getFileName()); + return true; + } catch (IOException error) { + report("Could not remove generated datapack " + target.getFileName() + ": " + error.getMessage()); + return false; + } + } + + private void replaceGeneratedDatapack(Path compiledDatapack, Path target) throws IOException { + Path backup = target.resolveSibling(target.getFileName() + ".mcs_backup"); + deleteRecursively(backup); + + boolean movedOld = false; + if (Files.exists(target)) { + Files.move(target, backup, StandardCopyOption.REPLACE_EXISTING); + movedOld = true; + } + + try { + Files.move(compiledDatapack, target, StandardCopyOption.REPLACE_EXISTING); + deleteRecursively(backup); + } catch (IOException error) { + if (movedOld && Files.exists(backup) && !Files.exists(target)) { + Files.move(backup, target, StandardCopyOption.REPLACE_EXISTING); + } + throw error; + } + } + + private void report(String message) { + System.out.println("[Minecraft Script] " + message); + serverAccess.notifyOperators(message); + } + + public static void deleteRecursively(Path path) throws IOException { + if (path == null || !Files.exists(path)) { + return; + } + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.deleteIfExists(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path directory, IOException exc) throws IOException { + if (exc != null) { + throw exc; + } + Files.deleteIfExists(directory); + return FileVisitResult.CONTINUE; + } + }); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der/minecraftscript/ProcessMcsCompiler.java b/mod/common/src/main/java/dev/spyc0der/minecraftscript/ProcessMcsCompiler.java new file mode 100644 index 0000000..d2a970c --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der/minecraftscript/ProcessMcsCompiler.java @@ -0,0 +1,50 @@ +package dev.spyc0der.minecraftscript; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public final class ProcessMcsCompiler implements McsCompiler { + private static final long COMPILE_TIMEOUT_SECONDS = 60; + + private final McsCompilerConfig config; + + public ProcessMcsCompiler(McsCompilerConfig config) { + this.config = config; + } + + @Override + public McsCompileResult compile(Path packFile, String datapackName, Path outputRoot, String minecraftVersion) + throws IOException, InterruptedException { + List command = new ArrayList<>(McsCommandLine.split(config.pythonCommand())); + command.add("compile"); + command.add(packFile.toString()); + command.add(datapackName); + command.add(outputRoot.toString()); + command.add("--minecraft-version"); + command.add(minecraftVersion); + + Path logFile = Files.createTempFile(outputRoot, "mcs-compile-", ".log"); + try { + Process process = new ProcessBuilder(command) + .redirectErrorStream(true) + .redirectOutput(logFile.toFile()) + .start(); + + boolean exited = process.waitFor(COMPILE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + String output = Files.readString(logFile, StandardCharsets.UTF_8); + if (!exited) { + process.destroyForcibly(); + return new McsCompileResult(124, "Minecraft-Script compile timed out after " + + COMPILE_TIMEOUT_SECONDS + " seconds.\n" + output); + } + return new McsCompileResult(process.exitValue(), output); + } finally { + Files.deleteIfExists(logFile); + } + } +} diff --git a/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsCommandLineTest.java b/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsCommandLineTest.java new file mode 100644 index 0000000..1ece409 --- /dev/null +++ b/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsCommandLineTest.java @@ -0,0 +1,25 @@ +package dev.spyc0der.minecraftscript; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class McsCommandLineTest { + @Test + void keepsQuotedCommandPathTogether() { + assertEquals( + List.of("C:/Program Files/Python/python.exe", "-m", "minecraft_script"), + McsCommandLine.split("\"C:/Program Files/Python/python.exe\" -m minecraft_script") + ); + } + + @Test + void splitsDefaultPythonModuleCommand() { + assertEquals( + List.of("python", "-m", "minecraft_script"), + McsCommandLine.split("python -m minecraft_script") + ); + } +} diff --git a/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsPackWatcherTest.java b/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsPackWatcherTest.java new file mode 100644 index 0000000..d458857 --- /dev/null +++ b/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsPackWatcherTest.java @@ -0,0 +1,24 @@ +package dev.spyc0der.minecraftscript; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class McsPackWatcherTest { + @Test + void compileDelayWaitsForSettleAfterFirstChange() { + assertEquals(150, McsPackWatcher.computeCompileDelayMillis(1_000, 1_000)); + } + + @Test + void compileDelayShrinksAsMaxWaitApproaches() { + assertEquals(150, McsPackWatcher.computeCompileDelayMillis(1_000, 1_100)); + assertEquals(50, McsPackWatcher.computeCompileDelayMillis(1_000, 1_350)); + } + + @Test + void compileDelayFiresImmediatelyAfterMaxWait() { + assertEquals(0, McsPackWatcher.computeCompileDelayMillis(1_000, 1_400)); + assertEquals(0, McsPackWatcher.computeCompileDelayMillis(1_000, 2_000)); + } +} diff --git a/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsWorldPackManagerTest.java b/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsWorldPackManagerTest.java new file mode 100644 index 0000000..474773f --- /dev/null +++ b/mod/common/src/test/java/dev/spyc0der/minecraftscript/McsWorldPackManagerTest.java @@ -0,0 +1,124 @@ +package dev.spyc0der.minecraftscript; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class McsWorldPackManagerTest { + @TempDir + Path tempDir; + + @Test + void createsStarterPackAndDiscoversOnlyPackFolders() throws IOException { + McsWorldPackManager manager = newManager(new SuccessfulCompiler()); + + manager.initializeWorldFolders(); + Files.createDirectories(tempDir.resolve("mcs_packs/empty")); + Files.createDirectories(tempDir.resolve("mcs_packs/custom")); + Files.writeString(tempDir.resolve("mcs_packs/custom/pack.mcs"), "function main() {}"); + + List packNames = manager.discoverPackFolders().stream() + .map(path -> path.getFileName().toString()) + .toList(); + + assertEquals(List.of("custom", "starter"), packNames); + assertTrue(Files.readString(tempDir.resolve("mcs_packs/starter/pack.mcs")) + .contains("MCS Starter Pack Loaded")); + } + + @Test + void sanitizesGeneratedDatapackNames() { + assertEquals("mcs_my_pack_1", McsPaths.generatedDatapackName(Path.of("My Pack 1"))); + assertEquals("pack", McsPaths.sanitizePackFolderName("!!!")); + } + + @Test + void successfulCompileReplacesGeneratedDatapack() throws IOException { + McsWorldPackManager manager = newManager(new SuccessfulCompiler()); + manager.initializeWorldFolders(); + Path packFolder = tempDir.resolve("mcs_packs/My Pack"); + Files.createDirectories(packFolder); + Files.writeString(packFolder.resolve("pack.mcs"), "function main() {}"); + + assertTrue(manager.compilePack(packFolder)); + + Path generated = tempDir.resolve("datapacks/mcs_my_pack"); + assertTrue(Files.isDirectory(generated)); + assertEquals("compiled", Files.readString(generated.resolve("pack.mcmeta"))); + } + + @Test + void failedCompileKeepsExistingDatapack() throws IOException { + Path generated = tempDir.resolve("datapacks/mcs_starter"); + Files.createDirectories(generated); + Files.writeString(generated.resolve("pack.mcmeta"), "old"); + + McsWorldPackManager manager = newManager(new FailingCompiler()); + manager.initializeWorldFolders(); + + assertFalse(manager.compilePack(tempDir.resolve("mcs_packs/starter"))); + assertEquals("old", Files.readString(generated.resolve("pack.mcmeta"))); + } + + @Test + void deletedPackRemovesGeneratedDatapack() throws IOException { + McsWorldPackManager manager = newManager(new SuccessfulCompiler()); + manager.initializeWorldFolders(); + Path generated = tempDir.resolve("datapacks/mcs_starter"); + Files.createDirectories(generated); + Files.writeString(generated.resolve("pack.mcmeta"), "old"); + Files.delete(tempDir.resolve("mcs_packs/starter/pack.mcs")); + + assertTrue(manager.refreshPack(tempDir.resolve("mcs_packs/starter"))); + assertFalse(Files.exists(generated)); + } + + private McsWorldPackManager newManager(McsCompiler compiler) { + return new McsWorldPackManager(new TestServerAccess(tempDir), compiler); + } + + private record TestServerAccess(Path saveRoot) implements McsServerAccess { + @Override + public String minecraftVersion() { + return "1.21.2"; + } + + @Override + public void executeReload() { + } + + @Override + public void notifyOperators(String message) { + } + } + + private static final class SuccessfulCompiler implements McsCompiler { + final List compiled = new ArrayList<>(); + + @Override + public McsCompileResult compile(Path packFile, String datapackName, Path outputRoot, String minecraftVersion) + throws IOException { + compiled.add(packFile); + Path generated = outputRoot.resolve(datapackName); + Files.createDirectories(generated); + Files.writeString(generated.resolve("pack.mcmeta"), "compiled"); + return new McsCompileResult(0, "ok"); + } + } + + private static final class FailingCompiler implements McsCompiler { + @Override + public McsCompileResult compile(Path packFile, String datapackName, Path outputRoot, String minecraftVersion) { + return new McsCompileResult(1, "syntax error"); + } + } +} diff --git a/mod/gradle.properties b/mod/gradle.properties new file mode 100644 index 0000000..027340c --- /dev/null +++ b/mod/gradle.properties @@ -0,0 +1,14 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.parallel=true +org.gradle.caching=true + +mod_id=minecraft_script +mod_name=Minecraft Script +mod_version=0.1.0 +mod_group=dev.spyc0der.minecraftscript +mod_license=MIT +mod_description=Compiles Minecraft-Script packs from world mcs_packs folders into datapacks. + +architectury_loom_version=1.14-SNAPSHOT +architectury_plugin_version=3.4-SNAPSHOT +fabric_loader_version=0.19.3 diff --git a/mod/gradle/wrapper/gradle-wrapper.jar b/mod/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/mod/gradle/wrapper/gradle-wrapper.jar differ diff --git a/mod/gradle/wrapper/gradle-wrapper.properties b/mod/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..df97d72 --- /dev/null +++ b/mod/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mod/gradlew b/mod/gradlew new file mode 100644 index 0000000..d95bf61 --- /dev/null +++ b/mod/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod/gradlew.bat b/mod/gradlew.bat new file mode 100644 index 0000000..640d686 --- /dev/null +++ b/mod/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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=-Dfile.encoding=UTF-8 "-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% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod/loader.gradle b/mod/loader.gradle new file mode 100644 index 0000000..5b84928 --- /dev/null +++ b/mod/loader.gradle @@ -0,0 +1,118 @@ +apply plugin: "dev.architectury.loom" + +def minecraftVersion = project.ext.minecraftVersion +def loader = project.ext.loader +def platformJavaSuffix = "mc_legacy" +if (minecraftVersion == "1.21.11") { + platformJavaSuffix = "mc_1_21_11" +} else if (minecraftVersion == "1.21.9" || minecraftVersion == "1.21.10") { + platformJavaSuffix = "mc_1_21_9" +} +def forgeJavaSuffix = (minecraftVersion == "1.21.4" || minecraftVersion == "1.21.5") ? "forge_legacy" : "forge_modern" +def generatedBuildInfoDir = layout.buildDirectory.dir("generated/sources/mcsBuildInfo/java") + +sourceSets { + main { + java { + srcDir rootProject.file("common/src/main/java") + srcDir rootProject.file("platform/${loader}/src/main/java") + srcDir rootProject.file("platform/${loader}/src/${platformJavaSuffix}/java") + if (loader == "forge") { + srcDir rootProject.file("platform/forge/src/${forgeJavaSuffix}/java") + } + srcDir generatedBuildInfoDir + } + resources { + srcDir rootProject.file("platform/${loader}/src/main/resources") + } + } +} + +base { + archivesName = "${mod_id}-${loader}-${minecraftVersion}" +} + +loom { + silentMojangMappingsLicense() +} + +dependencies { + minecraft "com.mojang:minecraft:${minecraftVersion}" + mappings loom.officialMojangMappings() +} + +if (loader == "fabric") { + dependencies { + modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabricApiVersions[minecraftVersion]}" + } +} else if (loader == "forge") { + def forgeVersion = rootProject.forgeVersions[minecraftVersion] + if (forgeVersion == null) { + throw new GradleException("Forge does not publish a ${minecraftVersion} artifact. Remove this matrix project from settings.gradle.") + } + dependencies { + forge "net.minecraftforge:forge:${forgeVersion}" + } + tasks.named("remapJar") { + enabled = false + } + tasks.named("build") { + dependsOn tasks.named("jar") + } +} else if (loader == "neoforge") { + dependencies { + neoForge "net.neoforged:neoforge:${rootProject.neoForgeVersions[minecraftVersion]}" + } +} + +tasks.register("generateMcsBuildInfo") { + def outputDir = generatedBuildInfoDir + outputs.dir(outputDir) + doLast { + def packageDir = outputDir.get().asFile.toPath().resolve("dev/spyc0der/minecraftscript") + java.nio.file.Files.createDirectories(packageDir) + packageDir.resolve("McsBuildInfo.java").toFile().text = """package dev.spyc0der.minecraftscript; + +public final class McsBuildInfo { + public static final String MINECRAFT_VERSION = "${minecraftVersion}"; + public static final String LOADER = "${loader}"; + + private McsBuildInfo() { + } +} +""" + } +} + +tasks.withType(JavaCompile).configureEach { + dependsOn tasks.named("generateMcsBuildInfo") +} + +processResources { + inputs.property("mod_id", mod_id) + inputs.property("mod_name", mod_name) + inputs.property("mod_version", mod_version) + inputs.property("mod_description", mod_description) + inputs.property("minecraft_version", minecraftVersion) + inputs.property("loader", loader) + + filesMatching(["fabric.mod.json", "META-INF/mods.toml", "META-INF/neoforge.mods.toml"]) { + expand( + mod_id: mod_id, + mod_name: mod_name, + mod_version: mod_version, + mod_description: mod_description, + minecraft_version: minecraftVersion, + loader: loader, + ) + } +} + +tasks.register("printMatrixContext") { + doLast { + println("loader=${loader}") + println("minecraftVersion=${minecraftVersion}") + println("project=${project.path}") + } +} diff --git a/mod/matrix-project.gradle b/mod/matrix-project.gradle new file mode 100644 index 0000000..83c6335 --- /dev/null +++ b/mod/matrix-project.gradle @@ -0,0 +1,10 @@ +def requestedTasks = gradle.startParameter.taskNames +def isMatrixProjectRequested = requestedTasks.any { taskName -> + taskName == project.path || + taskName.startsWith("${project.path}:") || + taskName.contains(project.path) +} + +if (isMatrixProjectRequested) { + apply from: rootProject.file("loader.gradle") +} diff --git a/mod/platform/fabric/src/main/java/dev/spyc0der/minecraftscript/fabric/McsFabricMod.java b/mod/platform/fabric/src/main/java/dev/spyc0der/minecraftscript/fabric/McsFabricMod.java new file mode 100644 index 0000000..ad9487a --- /dev/null +++ b/mod/platform/fabric/src/main/java/dev/spyc0der/minecraftscript/fabric/McsFabricMod.java @@ -0,0 +1,14 @@ +package dev.spyc0der.minecraftscript.fabric; + +import dev.spyc0der.minecraftscript.McsModRuntime; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; + +public final class McsFabricMod implements ModInitializer { + @Override + public void onInitialize() { + ServerLifecycleEvents.SERVER_STARTED.register(server -> + McsModRuntime.serverStarted(new FabricServerAccess(server))); + ServerLifecycleEvents.SERVER_STOPPING.register(server -> McsModRuntime.serverStopping()); + } +} diff --git a/mod/platform/fabric/src/main/resources/fabric.mod.json b/mod/platform/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..6eebb76 --- /dev/null +++ b/mod/platform/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${mod_version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["SpyC0der77"], + "license": "MIT", + "environment": "*", + "entrypoints": { + "main": [ + "dev.spyc0der.minecraftscript.fabric.McsFabricMod" + ] + }, + "depends": { + "fabricloader": ">=0.16.0", + "fabric-api": "*", + "minecraft": "${minecraft_version}" + } +} diff --git a/mod/platform/fabric/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java b/mod/platform/fabric/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java new file mode 100644 index 0000000..92b217a --- /dev/null +++ b/mod/platform/fabric/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java @@ -0,0 +1,50 @@ +package dev.spyc0der.minecraftscript.fabric; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.permissions.LevelBasedPermissionSet; +import net.minecraft.server.players.NameAndId; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class FabricServerAccess implements McsServerAccess { + private final MinecraftServer server; + + FabricServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(LevelBasedPermissionSet.ADMIN) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(new NameAndId(player.getGameProfile()))) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/platform/fabric/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java b/mod/platform/fabric/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java new file mode 100644 index 0000000..67953d9 --- /dev/null +++ b/mod/platform/fabric/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java @@ -0,0 +1,49 @@ +package dev.spyc0der.minecraftscript.fabric; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.NameAndId; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class FabricServerAccess implements McsServerAccess { + private final MinecraftServer server; + + FabricServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(4) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(new NameAndId(player.getGameProfile()))) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/platform/fabric/src/mc_legacy/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java b/mod/platform/fabric/src/mc_legacy/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java new file mode 100644 index 0000000..b4c8db0 --- /dev/null +++ b/mod/platform/fabric/src/mc_legacy/java/dev/spyc0der/minecraftscript/fabric/FabricServerAccess.java @@ -0,0 +1,48 @@ +package dev.spyc0der.minecraftscript.fabric; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class FabricServerAccess implements McsServerAccess { + private final MinecraftServer server; + + FabricServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(4) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(player.getGameProfile())) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/platform/forge/src/forge_legacy/java/dev/spyc0der/minecraftscript/forge/McsForgeMod.java b/mod/platform/forge/src/forge_legacy/java/dev/spyc0der/minecraftscript/forge/McsForgeMod.java new file mode 100644 index 0000000..82f3da7 --- /dev/null +++ b/mod/platform/forge/src/forge_legacy/java/dev/spyc0der/minecraftscript/forge/McsForgeMod.java @@ -0,0 +1,25 @@ +package dev.spyc0der.minecraftscript.forge; + +import dev.spyc0der.minecraftscript.McsModRuntime; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod("minecraft_script") +public final class McsForgeMod { + public McsForgeMod() { + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void onServerStarted(ServerStartedEvent event) { + McsModRuntime.serverStarted(new ForgeServerAccess(event.getServer())); + } + + @SubscribeEvent + public void onServerStopping(ServerStoppingEvent event) { + McsModRuntime.serverStopping(); + } +} diff --git a/mod/platform/forge/src/forge_modern/java/dev/spyc0der/minecraftscript/forge/McsForgeMod.java b/mod/platform/forge/src/forge_modern/java/dev/spyc0der/minecraftscript/forge/McsForgeMod.java new file mode 100644 index 0000000..222b324 --- /dev/null +++ b/mod/platform/forge/src/forge_modern/java/dev/spyc0der/minecraftscript/forge/McsForgeMod.java @@ -0,0 +1,22 @@ +package dev.spyc0der.minecraftscript.forge; + +import dev.spyc0der.minecraftscript.McsModRuntime; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod("minecraft_script") +public final class McsForgeMod { + public McsForgeMod() { + ServerStartedEvent.BUS.addListener(this::onServerStarted); + ServerStoppingEvent.BUS.addListener(this::onServerStopping); + } + + private void onServerStarted(ServerStartedEvent event) { + McsModRuntime.serverStarted(new ForgeServerAccess(event.getServer())); + } + + private void onServerStopping(ServerStoppingEvent event) { + McsModRuntime.serverStopping(); + } +} diff --git a/mod/platform/forge/src/main/resources/META-INF/mods.toml b/mod/platform/forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..374bf64 --- /dev/null +++ b/mod/platform/forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,16 @@ +modLoader = "javafml" +loaderVersion = "[1,)" +license = "MIT" + +[[mods]] +modId = "${mod_id}" +version = "${mod_version}" +displayName = "${mod_name}" +description = '''${mod_description}''' + +[[dependencies.${mod_id}]] +modId = "minecraft" +type = "required" +versionRange = "[${minecraft_version}]" +ordering = "NONE" +side = "BOTH" diff --git a/mod/platform/forge/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java b/mod/platform/forge/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java new file mode 100644 index 0000000..8707027 --- /dev/null +++ b/mod/platform/forge/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java @@ -0,0 +1,50 @@ +package dev.spyc0der.minecraftscript.forge; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.permissions.LevelBasedPermissionSet; +import net.minecraft.server.players.NameAndId; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class ForgeServerAccess implements McsServerAccess { + private final MinecraftServer server; + + ForgeServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(LevelBasedPermissionSet.ADMIN) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(new NameAndId(player.getGameProfile()))) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/platform/forge/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java b/mod/platform/forge/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java new file mode 100644 index 0000000..da52bf4 --- /dev/null +++ b/mod/platform/forge/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java @@ -0,0 +1,49 @@ +package dev.spyc0der.minecraftscript.forge; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.NameAndId; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class ForgeServerAccess implements McsServerAccess { + private final MinecraftServer server; + + ForgeServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(4) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(new NameAndId(player.getGameProfile()))) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/platform/forge/src/mc_legacy/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java b/mod/platform/forge/src/mc_legacy/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java new file mode 100644 index 0000000..d08c0b7 --- /dev/null +++ b/mod/platform/forge/src/mc_legacy/java/dev/spyc0der/minecraftscript/forge/ForgeServerAccess.java @@ -0,0 +1,48 @@ +package dev.spyc0der.minecraftscript.forge; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class ForgeServerAccess implements McsServerAccess { + private final MinecraftServer server; + + ForgeServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(4) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(player.getGameProfile())) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/platform/neoforge/src/main/java/dev/spyc0der/minecraftscript/neoforge/McsNeoForgeMod.java b/mod/platform/neoforge/src/main/java/dev/spyc0der/minecraftscript/neoforge/McsNeoForgeMod.java new file mode 100644 index 0000000..4f4d856 --- /dev/null +++ b/mod/platform/neoforge/src/main/java/dev/spyc0der/minecraftscript/neoforge/McsNeoForgeMod.java @@ -0,0 +1,25 @@ +package dev.spyc0der.minecraftscript.neoforge; + +import dev.spyc0der.minecraftscript.McsModRuntime; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; + +@Mod("minecraft_script") +public final class McsNeoForgeMod { + public McsNeoForgeMod() { + NeoForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void onServerStarted(ServerStartedEvent event) { + McsModRuntime.serverStarted(new NeoForgeServerAccess(event.getServer())); + } + + @SubscribeEvent + public void onServerStopping(ServerStoppingEvent event) { + McsModRuntime.serverStopping(); + } +} diff --git a/mod/platform/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/mod/platform/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..374bf64 --- /dev/null +++ b/mod/platform/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,16 @@ +modLoader = "javafml" +loaderVersion = "[1,)" +license = "MIT" + +[[mods]] +modId = "${mod_id}" +version = "${mod_version}" +displayName = "${mod_name}" +description = '''${mod_description}''' + +[[dependencies.${mod_id}]] +modId = "minecraft" +type = "required" +versionRange = "[${minecraft_version}]" +ordering = "NONE" +side = "BOTH" diff --git a/mod/platform/neoforge/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java b/mod/platform/neoforge/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java new file mode 100644 index 0000000..a5696cc --- /dev/null +++ b/mod/platform/neoforge/src/mc_1_21_11/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java @@ -0,0 +1,50 @@ +package dev.spyc0der.minecraftscript.neoforge; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.permissions.LevelBasedPermissionSet; +import net.minecraft.server.players.NameAndId; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class NeoForgeServerAccess implements McsServerAccess { + private final MinecraftServer server; + + NeoForgeServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(LevelBasedPermissionSet.ADMIN) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(new NameAndId(player.getGameProfile()))) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/platform/neoforge/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java b/mod/platform/neoforge/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java new file mode 100644 index 0000000..6e61bef --- /dev/null +++ b/mod/platform/neoforge/src/mc_1_21_9/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java @@ -0,0 +1,49 @@ +package dev.spyc0der.minecraftscript.neoforge; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.NameAndId; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class NeoForgeServerAccess implements McsServerAccess { + private final MinecraftServer server; + + NeoForgeServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(4) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(new NameAndId(player.getGameProfile()))) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/platform/neoforge/src/mc_legacy/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java b/mod/platform/neoforge/src/mc_legacy/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java new file mode 100644 index 0000000..2690417 --- /dev/null +++ b/mod/platform/neoforge/src/mc_legacy/java/dev/spyc0der/minecraftscript/neoforge/NeoForgeServerAccess.java @@ -0,0 +1,48 @@ +package dev.spyc0der.minecraftscript.neoforge; + +import dev.spyc0der.minecraftscript.McsBuildInfo; +import dev.spyc0der.minecraftscript.McsServerAccess; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +import java.nio.file.Path; + +final class NeoForgeServerAccess implements McsServerAccess { + private final MinecraftServer server; + + NeoForgeServerAccess(MinecraftServer server) { + this.server = server; + } + + @Override + public Path saveRoot() { + return server.getWorldPath(LevelResource.ROOT); + } + + @Override + public String minecraftVersion() { + return McsBuildInfo.MINECRAFT_VERSION; + } + + @Override + public void executeReload() { + server.execute(() -> { + CommandSourceStack source = server.createCommandSourceStack() + .withPermission(4) + .withSuppressedOutput(); + server.getCommands().performPrefixedCommand(source, "reload"); + }); + } + + @Override + public void notifyOperators(String message) { + server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> { + if (server.getPlayerList().isOp(player.getGameProfile())) { + player.sendSystemMessage(Component.literal("[MCS] " + message).withStyle(ChatFormatting.YELLOW)); + } + })); + } +} diff --git a/mod/settings.gradle b/mod/settings.gradle new file mode 100644 index 0000000..d5477ec --- /dev/null +++ b/mod/settings.gradle @@ -0,0 +1,30 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = "https://maven.architectury.dev/" } + maven { url = "https://maven.fabricmc.net/" } + maven { url = "https://maven.minecraftforge.net/" } + maven { url = "https://maven.neoforged.net/releases/" } + mavenCentral() + } +} + +rootProject.name = "minecraft-script-mod" + +include(":common") + +def versionsIndex = new groovy.json.JsonSlurper().parse(file("../minecraft_script/versions/index.json")) +def versions = versionsIndex.supported.findAll { it != "26.1" } +def loaders = ["fabric", "forge", "neoforge"] +def unsupportedForgeVersions = ["1.21.2"] + +versions.each { mcVersion -> + loaders.each { loader -> + if (loader == "forge" && unsupportedForgeVersions.contains(mcVersion)) { + return + } + def versionPath = mcVersion.replace(".", "_") + include(":versions:mc_${versionPath}:${loader}") + project(":versions:mc_${versionPath}:${loader}").projectDir = file("versions/mc_${versionPath}/${loader}") + } +} diff --git a/mod/versions/mc_1_21_10/fabric/build.gradle b/mod/versions/mc_1_21_10/fabric/build.gradle new file mode 100644 index 0000000..239ad4e --- /dev/null +++ b/mod/versions/mc_1_21_10/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.10" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_10/forge/build.gradle b/mod/versions/mc_1_21_10/forge/build.gradle new file mode 100644 index 0000000..285d2bb --- /dev/null +++ b/mod/versions/mc_1_21_10/forge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.10" +ext.loader = "forge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_10/forge/gradle.properties b/mod/versions/mc_1_21_10/forge/gradle.properties new file mode 100644 index 0000000..871132d --- /dev/null +++ b/mod/versions/mc_1_21_10/forge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=forge + diff --git a/mod/versions/mc_1_21_10/neoforge/build.gradle b/mod/versions/mc_1_21_10/neoforge/build.gradle new file mode 100644 index 0000000..c4e9549 --- /dev/null +++ b/mod/versions/mc_1_21_10/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.10" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_10/neoforge/gradle.properties b/mod/versions/mc_1_21_10/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_10/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/mod/versions/mc_1_21_11/fabric/build.gradle b/mod/versions/mc_1_21_11/fabric/build.gradle new file mode 100644 index 0000000..eef3989 --- /dev/null +++ b/mod/versions/mc_1_21_11/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.11" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_11/forge/build.gradle b/mod/versions/mc_1_21_11/forge/build.gradle new file mode 100644 index 0000000..c80cc23 --- /dev/null +++ b/mod/versions/mc_1_21_11/forge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.11" +ext.loader = "forge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_11/forge/gradle.properties b/mod/versions/mc_1_21_11/forge/gradle.properties new file mode 100644 index 0000000..871132d --- /dev/null +++ b/mod/versions/mc_1_21_11/forge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=forge + diff --git a/mod/versions/mc_1_21_11/neoforge/build.gradle b/mod/versions/mc_1_21_11/neoforge/build.gradle new file mode 100644 index 0000000..33bc092 --- /dev/null +++ b/mod/versions/mc_1_21_11/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.11" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_11/neoforge/gradle.properties b/mod/versions/mc_1_21_11/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_11/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/mod/versions/mc_1_21_2/fabric/build.gradle b/mod/versions/mc_1_21_2/fabric/build.gradle new file mode 100644 index 0000000..e15b7de --- /dev/null +++ b/mod/versions/mc_1_21_2/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.2" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_2/neoforge/build.gradle b/mod/versions/mc_1_21_2/neoforge/build.gradle new file mode 100644 index 0000000..17cec4b --- /dev/null +++ b/mod/versions/mc_1_21_2/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.2" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_2/neoforge/gradle.properties b/mod/versions/mc_1_21_2/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_2/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/mod/versions/mc_1_21_4/fabric/build.gradle b/mod/versions/mc_1_21_4/fabric/build.gradle new file mode 100644 index 0000000..500cbf1 --- /dev/null +++ b/mod/versions/mc_1_21_4/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.4" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_4/forge/build.gradle b/mod/versions/mc_1_21_4/forge/build.gradle new file mode 100644 index 0000000..7978295 --- /dev/null +++ b/mod/versions/mc_1_21_4/forge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.4" +ext.loader = "forge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_4/forge/gradle.properties b/mod/versions/mc_1_21_4/forge/gradle.properties new file mode 100644 index 0000000..871132d --- /dev/null +++ b/mod/versions/mc_1_21_4/forge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=forge + diff --git a/mod/versions/mc_1_21_4/neoforge/build.gradle b/mod/versions/mc_1_21_4/neoforge/build.gradle new file mode 100644 index 0000000..7bda326 --- /dev/null +++ b/mod/versions/mc_1_21_4/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.4" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_4/neoforge/gradle.properties b/mod/versions/mc_1_21_4/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_4/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/mod/versions/mc_1_21_5/fabric/build.gradle b/mod/versions/mc_1_21_5/fabric/build.gradle new file mode 100644 index 0000000..7297769 --- /dev/null +++ b/mod/versions/mc_1_21_5/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.5" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_5/forge/build.gradle b/mod/versions/mc_1_21_5/forge/build.gradle new file mode 100644 index 0000000..82e8646 --- /dev/null +++ b/mod/versions/mc_1_21_5/forge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.5" +ext.loader = "forge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_5/forge/gradle.properties b/mod/versions/mc_1_21_5/forge/gradle.properties new file mode 100644 index 0000000..871132d --- /dev/null +++ b/mod/versions/mc_1_21_5/forge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=forge + diff --git a/mod/versions/mc_1_21_5/neoforge/build.gradle b/mod/versions/mc_1_21_5/neoforge/build.gradle new file mode 100644 index 0000000..164fe37 --- /dev/null +++ b/mod/versions/mc_1_21_5/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.5" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_5/neoforge/gradle.properties b/mod/versions/mc_1_21_5/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_5/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/mod/versions/mc_1_21_6/fabric/build.gradle b/mod/versions/mc_1_21_6/fabric/build.gradle new file mode 100644 index 0000000..e95a8d0 --- /dev/null +++ b/mod/versions/mc_1_21_6/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.6" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_6/forge/build.gradle b/mod/versions/mc_1_21_6/forge/build.gradle new file mode 100644 index 0000000..209737a --- /dev/null +++ b/mod/versions/mc_1_21_6/forge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.6" +ext.loader = "forge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_6/forge/gradle.properties b/mod/versions/mc_1_21_6/forge/gradle.properties new file mode 100644 index 0000000..871132d --- /dev/null +++ b/mod/versions/mc_1_21_6/forge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=forge + diff --git a/mod/versions/mc_1_21_6/neoforge/build.gradle b/mod/versions/mc_1_21_6/neoforge/build.gradle new file mode 100644 index 0000000..53f410f --- /dev/null +++ b/mod/versions/mc_1_21_6/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.6" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_6/neoforge/gradle.properties b/mod/versions/mc_1_21_6/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_6/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/mod/versions/mc_1_21_7/fabric/build.gradle b/mod/versions/mc_1_21_7/fabric/build.gradle new file mode 100644 index 0000000..3366efe --- /dev/null +++ b/mod/versions/mc_1_21_7/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.7" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_7/forge/build.gradle b/mod/versions/mc_1_21_7/forge/build.gradle new file mode 100644 index 0000000..245ec55 --- /dev/null +++ b/mod/versions/mc_1_21_7/forge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.7" +ext.loader = "forge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_7/forge/gradle.properties b/mod/versions/mc_1_21_7/forge/gradle.properties new file mode 100644 index 0000000..871132d --- /dev/null +++ b/mod/versions/mc_1_21_7/forge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=forge + diff --git a/mod/versions/mc_1_21_7/neoforge/build.gradle b/mod/versions/mc_1_21_7/neoforge/build.gradle new file mode 100644 index 0000000..6da9a2d --- /dev/null +++ b/mod/versions/mc_1_21_7/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.7" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_7/neoforge/gradle.properties b/mod/versions/mc_1_21_7/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_7/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/mod/versions/mc_1_21_8/fabric/build.gradle b/mod/versions/mc_1_21_8/fabric/build.gradle new file mode 100644 index 0000000..cc68b00 --- /dev/null +++ b/mod/versions/mc_1_21_8/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.8" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_8/forge/build.gradle b/mod/versions/mc_1_21_8/forge/build.gradle new file mode 100644 index 0000000..c826bc2 --- /dev/null +++ b/mod/versions/mc_1_21_8/forge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.8" +ext.loader = "forge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_8/forge/gradle.properties b/mod/versions/mc_1_21_8/forge/gradle.properties new file mode 100644 index 0000000..871132d --- /dev/null +++ b/mod/versions/mc_1_21_8/forge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=forge + diff --git a/mod/versions/mc_1_21_8/neoforge/build.gradle b/mod/versions/mc_1_21_8/neoforge/build.gradle new file mode 100644 index 0000000..83bb379 --- /dev/null +++ b/mod/versions/mc_1_21_8/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.8" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_8/neoforge/gradle.properties b/mod/versions/mc_1_21_8/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_8/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/mod/versions/mc_1_21_9/fabric/build.gradle b/mod/versions/mc_1_21_9/fabric/build.gradle new file mode 100644 index 0000000..cc820e7 --- /dev/null +++ b/mod/versions/mc_1_21_9/fabric/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.9" +ext.loader = "fabric" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_9/forge/build.gradle b/mod/versions/mc_1_21_9/forge/build.gradle new file mode 100644 index 0000000..e72e231 --- /dev/null +++ b/mod/versions/mc_1_21_9/forge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.9" +ext.loader = "forge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_9/forge/gradle.properties b/mod/versions/mc_1_21_9/forge/gradle.properties new file mode 100644 index 0000000..871132d --- /dev/null +++ b/mod/versions/mc_1_21_9/forge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=forge + diff --git a/mod/versions/mc_1_21_9/neoforge/build.gradle b/mod/versions/mc_1_21_9/neoforge/build.gradle new file mode 100644 index 0000000..c2d5912 --- /dev/null +++ b/mod/versions/mc_1_21_9/neoforge/build.gradle @@ -0,0 +1,3 @@ +ext.minecraftVersion = "1.21.9" +ext.loader = "neoforge" +apply from: rootProject.file("matrix-project.gradle") diff --git a/mod/versions/mc_1_21_9/neoforge/gradle.properties b/mod/versions/mc_1_21_9/neoforge/gradle.properties new file mode 100644 index 0000000..3ce8f56 --- /dev/null +++ b/mod/versions/mc_1_21_9/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge + diff --git a/tests/test_mod_compile_cli.py b/tests/test_mod_compile_cli.py new file mode 100644 index 0000000..a8a7eac --- /dev/null +++ b/tests/test_mod_compile_cli.py @@ -0,0 +1,73 @@ +import json +from pathlib import Path + +import pytest + +from minecraft_script.common import COMMON_CONFIG +from minecraft_script import shell_commands + + +STARTER_PACK_SOURCE = """ +function init() { + tellraw("@a", text().text("MCS Starter Pack Loaded")); +} + +function main() { +} +""" + + +def test_compile_minecraft_version_flag_does_not_persist_config(tmp_path, monkeypatch): + source = tmp_path / "pack.mcs" + output = tmp_path / "out" + source.write_text(STARTER_PACK_SOURCE, encoding="utf-8") + + monkeypatch.setitem(COMMON_CONFIG, "minecraft_version", "1.21.2") + + shell_commands.sh_compile( + str(source), + "Flag Version Pack", + str(output), + "--minecraft-version", + "1.21.11", + ) + + pack = json.loads((output / "Flag Version Pack" / "pack.mcmeta").read_text(encoding="utf-8"))["pack"] + assert pack["min_format"] == [94, 1] + assert COMMON_CONFIG["minecraft_version"] == "1.21.2" + + +@pytest.mark.parametrize( + "minecraft_version", + [ + "1.21.2", + "1.21.4", + "1.21.5", + "1.21.6", + "1.21.7", + "1.21.8", + "1.21.9", + "1.21.10", + "1.21.11", + ], +) +def test_mod_starter_pack_compiles_for_mod_supported_versions( + minecraft_version, + tmp_path, + monkeypatch, +): + source = tmp_path / "pack.mcs" + output = tmp_path / "out" + source.write_text(STARTER_PACK_SOURCE, encoding="utf-8") + + monkeypatch.setitem(COMMON_CONFIG, "minecraft_version", "1.21.2") + + shell_commands.sh_compile( + str(source), + f"Starter {minecraft_version}", + str(output), + "--minecraft-version", + minecraft_version, + ) + + assert (output / f"Starter {minecraft_version}" / "pack.mcmeta").is_file() diff --git a/uv.lock b/uv.lock index e182924..c6104e8 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "minecraft-script" -version = "0.3.5" +version = "0.3.7" source = { editable = "." } dependencies = [ { name = "pybars3" },