diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8517edc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.cmd text eol=crlf +*.bat text eol=crlf diff --git a/.github/workflows/mod.yml b/.github/workflows/mod.yml new file mode 100644 index 0000000..df531be --- /dev/null +++ b/.github/workflows/mod.yml @@ -0,0 +1,39 @@ +name: Mod + +on: + push: + paths: + - 'mod/**' + - 'minecraft_script/versions/**' + - '.github/workflows/mod.yml' + pull_request: + paths: + - 'mod/**' + - 'minecraft_script/versions/**' + - '.github/workflows/mod.yml' + +jobs: + build-matrix: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + profile: [1.21.2, 1.21.4, 1.21.5, 1.21.6, 1.21.7-8, 1.21.9-10, 1.21.11, 26.1] + loader: [fabric, forge, neoforge] + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4 + with: + distribution: temurin + java-version: ${{ !startsWith(matrix.profile, '1.') && '25' || '21' }} + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - name: Install Spyglass bundle dependencies + working-directory: mod/scripts + run: bun install --frozen-lockfile + - name: Apply version profile + run: python mod/scripts/apply_version.py ${{ matrix.profile }} --loader ${{ matrix.loader }} + - name: Build ${{ matrix.loader }} for ${{ matrix.profile }} + working-directory: mod + run: ./gradlew :${{ matrix.loader }}:build -Pmcs_profile=${{ matrix.profile }} -Penabled_platforms=${{ matrix.loader }} -x test diff --git a/.gitignore b/.gitignore index 40114b9..ce15fdc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,12 @@ build/ dist/ *.egg-info/ build_test/ +mod/.gradle/ +mod/**/.gradle/architectury-cache/ +mod/build/ +mod/out/ +mod/run/ +mod/scripts/node_modules/ highlighter/node_modules/ highlighter/dist/ highlighter/*.vsix diff --git a/minecraft_script/compiler/build_templates/builtins/1.21.2/log.mcfunction b/minecraft_script/compiler/build_templates/builtins/1.21.2/log.mcfunction index 14482e7..5426d29 100644 --- a/minecraft_script/compiler/build_templates/builtins/1.21.2/log.mcfunction +++ b/minecraft_script/compiler/build_templates/builtins/1.21.2/log.mcfunction @@ -1,7 +1,7 @@ -data modify storage minecraft:temp mcs_log set value "" -data modify storage minecraft:temp mcs_log append value from storage $(s0) $(n0) -data modify storage minecraft:temp mcs_log append value from storage $(s1) $(n1) -data modify storage minecraft:temp mcs_log append value from storage $(s2) $(n2) -data modify storage minecraft:temp mcs_log append value from storage $(s3) $(n3) -data modify storage minecraft:temp mcs_log append value from storage $(s4) $(n4) +$data modify storage minecraft:temp mcs_log set value "" +$data modify storage minecraft:temp mcs_log append value from storage $(s0) $(n0) +$data modify storage minecraft:temp mcs_log append value from storage $(s1) $(n1) +$data modify storage minecraft:temp mcs_log append value from storage $(s2) $(n2) +$data modify storage minecraft:temp mcs_log append value from storage $(s3) $(n3) +$data modify storage minecraft:temp mcs_log append value from storage $(s4) $(n4) $tellraw @a [{"nbt":"mcs_log","storage":"minecraft:temp","interpret":true}] diff --git a/minecraft_script/compiler/compile_interpreter.py b/minecraft_script/compiler/compile_interpreter.py index 9682473..f099e7e 100644 --- a/minecraft_script/compiler/compile_interpreter.py +++ b/minecraft_script/compiler/compile_interpreter.py @@ -493,7 +493,9 @@ def visit_FunctionCallNode(self, node, context: CompileContext) -> CompileResult self.schedule_function_generation(fnc) commands, return_value = fnc.call(self, arguments, context) if is_builtin: - self.used_builtin_functions.add(fnc.call.__name__) + builtin_name = fnc.call.__name__ + if builtin_name != "log" or self.version.orchestration.get("mcs_features", {}).get("log", {}).get("style") != "direct_tellraw": + self.used_builtin_functions.add(builtin_name) if commands is not None: commands = add_comment(tuple(commands), "Function call") self.add_commands(context.mcfunction_name, commands) diff --git a/minecraft_script/config_utils.py b/minecraft_script/config_utils.py index 07efec3..1c60752 100644 --- a/minecraft_script/config_utils.py +++ b/minecraft_script/config_utils.py @@ -1,10 +1,23 @@ import json +from contextlib import contextmanager from pathlib import Path from .common import COMMON_CONFIG, module_folder from .version_config import list_supported_versions, load_version_profile +@contextmanager +def temporary_config(**overrides): + saved = {key: COMMON_CONFIG[key] for key in overrides} + try: + for key, value in overrides.items(): + COMMON_CONFIG[key] = value + yield + finally: + for key, value in saved.items(): + COMMON_CONFIG[key] = value + + def _write_config() -> None: with open(f"{module_folder}/config.json", "wt", encoding="utf-8") as file: json.dump(COMMON_CONFIG, file, indent=4, ensure_ascii=False) diff --git a/minecraft_script/shell_commands.py b/minecraft_script/shell_commands.py index 6b667a7..d96b7c9 100644 --- a/minecraft_script/shell_commands.py +++ b/minecraft_script/shell_commands.py @@ -1,15 +1,66 @@ import json +import shutil import sys from . import debug_code from .compiler import build_datapack from .common import COMMON_CONFIG, version -from .config_utils import update_config, reset_config +from .config_utils import config_minecraft_version_check, temporary_config, update_config, reset_config from .lint import lint_code from .version_config import breaking_changes_between from pathlib import Path +def _parse_flag_args( + args: list, + *, + boolean_flags: set[str], + value_flags: set[str], +) -> tuple[dict[str, str | bool], list[str]]: + parsed: dict[str, str | bool] = {} + positional: list[str] = [] + supported_flags = boolean_flags | value_flags + index = 0 + while index < len(args): + arg = args[index] + if arg.startswith("--") and "=" in arg: + flag, value = arg.split("=", 1) + if flag in value_flags: + parsed[flag] = value + index += 1 + continue + if arg in boolean_flags: + parsed[arg] = True + index += 1 + continue + if arg in value_flags: + next_arg = args[index + 1] if index + 1 < len(args) else None + if next_arg is None or next_arg.startswith("--"): + print(f"Error: {arg} requires a value.") + exit(-1) + parsed[arg] = next_arg + index += 2 + continue + if arg.startswith("--"): + if arg not in supported_flags: + print(f"Error: Unknown flag {arg}.") + exit(-1) + index += 1 + continue + positional.append(arg) + index += 1 + return parsed, positional + + +def _validate_datapack_name(datapack_name: str) -> None: + if not datapack_name or datapack_name in {".", ".."}: + print("Error: Invalid datapack name.") + exit(-1) + if ".." in datapack_name or "/" in datapack_name or "\\" in datapack_name: + print("Error: Datapack name must not contain path separators or '..'.") + exit(-1) + + def handle_arguments(arguments: list): if not arguments: sh_default() @@ -31,10 +82,10 @@ def handle_arguments(arguments: list): - debug : debug the minecraft script file found at the given path. -- compile [] []: compile the associated -mcs file into a datapack. The resulting datapack folder will be named after -the mcs file, unless a datapack name is specified. The output path argument -specifies where the datapack should be generated (default to current path). +- compile [--mc-version ] [--force] [--verbose|--no-verbose] +[] []: compile the associated mcs file into a +datapack. --mc-version selects the Minecraft version profile without changing +config.json. --force deletes an existing output datapack folder before building. - lint [--json]: validate MCS syntax and imports for the given file. Use --stdin to read code from standard input and pass --source so @@ -79,29 +130,35 @@ def sh_debug(*args) -> None: def sh_compile(*args) -> None: - # Manage args & parameters: - arg_count = len(args) - if arg_count < 1: + flags, positional = _parse_flag_args( + list(args), + boolean_flags={"--force", "--verbose", "--no-verbose"}, + value_flags={"--mc-version"}, + ) + if len(positional) < 1: print("No path specified to compile.") exit() - source_path = Path(args[0]).resolve() + source_path = Path(positional[0]).resolve() datapack_name: str = ( "-".join(source_path.name.split(".")[:-1]).replace("_", " ").title() - if arg_count < 2 else - args[1] + if len(positional) < 2 else + positional[1] ) output_path = ( Path(COMMON_CONFIG["default_output_path"]).resolve() - if arg_count < 3 else - Path(args[2]).expanduser().resolve() + if len(positional) < 3 else + Path(positional[2]).expanduser().resolve() ) verbose = COMMON_CONFIG["verbose"] + if "--verbose" in flags: + verbose = True + if "--no-verbose" in flags: + verbose = False - # Check if given paths are valid: if not source_path.is_file(): - print(f"Error: Could not find file at {args[0] !r}") + print(f"Error: Could not find file at {positional[0] !r}") exit(-1) if output_path.exists() and not output_path.is_dir(): @@ -109,12 +166,37 @@ def sh_compile(*args) -> None: exit(-1) output_path.mkdir(parents=True, exist_ok=True) + resolved_output = output_path.resolve() + + _validate_datapack_name(datapack_name) + + datapack_folder = (resolved_output / datapack_name).resolve() + if not datapack_folder.is_relative_to(resolved_output): + print("Error: Datapack output path escapes output directory.") + exit(-1) + if "--force" in flags and datapack_folder.exists(): + if not datapack_folder.is_dir(): + print(f"Error: Output path is not a directory ({str(datapack_folder) !r})") + exit(-1) + shutil.rmtree(datapack_folder) + + overrides: dict[str, object] = {} + if "--mc-version" in flags: + mc_version = flags["--mc-version"] + if not isinstance(mc_version, str) or mc_version is True: + print("Error: --mc-version requires a value.") + exit(-1) + overrides["minecraft_version"] = config_minecraft_version_check(mc_version, "minecraft_version") + if "--verbose" in flags: + overrides["verbose"] = True + if "--no-verbose" in flags: + overrides["verbose"] = False - # Build datapack 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) + with temporary_config(**overrides): + build_datapack(code, datapack_name, str(output_path), verbose, source_path=source_path) def sh_config(*args) -> None: diff --git a/mod/README.md b/mod/README.md new file mode 100644 index 0000000..410ade4 --- /dev/null +++ b/mod/README.md @@ -0,0 +1,65 @@ +# MCS Packs Mod + +Architectury mod for Fabric, Forge, and NeoForge. Watches `.minecraft/mcs_packs//`, runs `mcs lint` → `mcs compile` → Spyglass validation, then hot-reloads into the active world. + +## Requirements + +- Java 21 +- MCS installed (`pip install -e .` from this repo). The mod auto-detects `mcs`, `python -m minecraft_script`, or common Windows Python installs; launchers with a limited PATH (Modrinth, Prism, etc.) get a bundled `mcs-compile.cmd` fallback. +- Node.js on PATH (Spyglass post-compile validation; optional — disable in `config/mcs-packs.json`) + +## Supported Minecraft versions + +All MCS profiles from `minecraft_script/versions/index.json`: + +| MCS profile | Minecraft versions | +| ----------- | ------------------ | +| `1.21.2` | 1.21.2 | +| `1.21.4` | 1.21.4 | +| `1.21.5` | 1.21.5 | +| `1.21.6` | 1.21.6 | +| `1.21.7-8` | 1.21.7, 1.21.8 | +| `1.21.9-10` | 1.21.9, 1.21.10 | +| `1.21.11` | 1.21.11 | +| `26.1` | 26.1 | + +Build one profile: + +```bash +cd mod +python scripts/apply_version.py 1.21.11 +./gradlew :fabric:build -Pmcs_profile=1.21.11 +``` + +Build every profile × loader (24 artifacts): + +```bash +cd mod +python scripts/build_all.py +``` + +Build a single target: + +```bash +python scripts/build_all.py --profile 1.21.4 --loader fabric +``` + +## Layout + +``` +.minecraft/ + config/mcs-packs.json + mcs_packs/ + starter/pack.mcs + my_pack/pack.mcs + _compiled/ +``` + +## Dev run (Fabric) + +```bash +python scripts/apply_version.py 1.21.11 +./gradlew :fabric:runClient -Pmcs_profile=1.21.11 +``` + +Loader dependency versions live in `versions/manifest.json`. Update those pins when bumping Minecraft support. diff --git a/mod/build.gradle b/mod/build.gradle new file mode 100644 index 0000000..b46fe8b --- /dev/null +++ b/mod/build.gradle @@ -0,0 +1,86 @@ +import groovy.json.JsonSlurper + +plugins { + id 'dev.architectury.loom' version "${architectury_loom_version}" apply false + id 'architectury-plugin' version "${architectury_plugin_version}" + id 'maven-publish' +} + +def manifest = new JsonSlurper().parse(file('versions/manifest.json')) +def mcsProfileFromCli = gradle.startParameter.projectProperties.containsKey('mcs_profile') +def activeProfileName = findProperty('mcs_profile') ?: '1.21.11' +def activeProfile = manifest.profiles[activeProfileName] +if (activeProfile == null) { + throw new GradleException("Unknown mcs_profile '${activeProfileName}'. See versions/manifest.json") +} + +ext { + mcsProfileName = activeProfileName + if (mcsProfileFromCli) { + minecraft_version = activeProfile.minecraft_version + mcsMinecraftProfile = activeProfile.mcs_profile + supportedGameVersions = (activeProfile.supported_game_versions as List).join(',') + fabric_loader_version = activeProfile.fabric_loader_version + fabric_api_version = activeProfile.fabric_api_version + forge_version = activeProfile.forge_version + neoforge_version = activeProfile.neoforge_version + architectury_api_version = activeProfile.architectury_api_version + } else { + minecraft_version = findProperty('minecraft_version') + mcsMinecraftProfile = findProperty('mcs_minecraft_profile') + supportedGameVersions = findProperty('supported_game_versions') + fabric_loader_version = findProperty('fabric_loader_version') + fabric_api_version = findProperty('fabric_api_version') + forge_version = findProperty('forge_version') + neoforge_version = findProperty('neoforge_version') + architectury_api_version = findProperty('architectury_api_version') + } + requiredJavaVersion = (minecraft_version ==~ /^(?:2[6-9]|[3-9]\d*)\..+/) ? 25 : 21 +} + +allprojects { + group = rootProject.maven_group + version = rootProject.mod_version +} + +subprojects { + apply plugin: 'dev.architectury.loom' + apply plugin: 'architectury-plugin' + apply plugin: 'maven-publish' + + repositories { + mavenCentral() + maven { url = 'https://maven.fabricmc.net/' } + maven { url = 'https://maven.architectury.dev/' } + maven { url = 'https://maven.minecraftforge.net/' } + maven { url = 'https://maven.neoforged.net/releases' } + } + + base { + archivesName = "${rootProject.archives_base_name}-${rootProject.mcsProfileName}-${project.name}" + } + + loom { + silentMojangMappingsLicense() + } + + dependencies { + minecraft "net.minecraft:minecraft:${rootProject.minecraft_version}" + mappings loom.officialMojangMappings() + } + + java { + withSourcesJar() + def javaVersion = JavaVersion.toVersion(rootProject.requiredJavaVersion) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + } + + tasks.withType(JavaCompile).configureEach { + it.options.release = rootProject.requiredJavaVersion + } +} + +architectury { + minecraft = rootProject.minecraft_version +} diff --git a/mod/common/.gradle/.architectury-transformer/debug.log b/mod/common/.gradle/.architectury-transformer/debug.log new file mode 100644 index 0000000..dfc3bda --- /dev/null +++ b/mod/common/.gradle/.architectury-transformer/debug.log @@ -0,0 +1,39 @@ +[Architectury Transformer DEBUG] +[Architectury Transformer DEBUG] ============================ +[Architectury Transformer DEBUG] Transforming from C:\Users\Carter\Code\Minecraft-Script\mod\common\build\devlibs\mcs-packs-1.21.2-common-0.1.0-dev.jar to C:\Users\Carter\Code\Minecraft-Script\mod\common\build\libs\mcs-packs-1.21.2-common-0.1.0-transformProductionFabric.jar +[Architectury Transformer DEBUG] ============================ +[Architectury Transformer DEBUG] +[Architectury Transformer DEBUG] Transforming 5 transformer(s) from C:\Users\Carter\Code\Minecraft-Script\mod\common\build\devlibs\mcs-packs-1.21.2-common-0.1.0-dev.jar to C:\Users\Carter\Code\Minecraft-Script\mod\common\build\libs\mcs-packs-1.21.2-common-0.1.0-transformProductionFabric.jar: +[Architectury Transformer DEBUG] - dev.architectury.transformer.transformers.RemapMixinVariables@2b184553 +[Architectury Transformer DEBUG] - dev.architectury.transformer.transformers.TransformExpectPlatform@26a5c8fc +[Architectury Transformer DEBUG] - dev.architectury.transformer.transformers.RemapInjectables@3e780c0a +[Architectury Transformer DEBUG] - AddRefmapName(enabled=() -> kotlin.Boolean) +[Architectury Transformer DEBUG] - dev.architectury.transformer.transformers.TransformPlatformOnly@3a17f3 +[Architectury Transformer DEBUG] Transforming from C:\Users\Carter\Code\Minecraft-Script\mod\common\build\devlibs\mcs-packs-1.21.2-common-0.1.0-dev.jar to C:\Users\Carter\Code\Minecraft-Script\mod\common\build\libs\mcs-packs-1.21.2-common-0.1.0-transformProductionFabric.jar with 5 transformer(s) on dev.architectury.transformer.handler.SimpleTransformerHandler +[Architectury Transformer DEBUG] Found class transformer +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/watch/PackWatcher with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/version/VersionMapper with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/version/VersionMapper$Index with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/util/SafePaths with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/util/PlayerFeedback with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/util/McsPaths with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/toolchain/SubprocessRunner with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/toolchain/ProcessResult with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/toolchain/McsToolchain with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/toolchain/McsToolchain$1 with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/toolchain/Diagnostic with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/toolchain/CompilerResolver with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/toolchain/CompilerResolver$CompilerCommand with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/registry/PackRegistry with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/registry/PackRegistry$PackOverrides with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/registry/PackDefinition with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/pipeline/PackPipeline with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/McsPacks with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/deploy/DatapackDeployer with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/deploy/DatapackDeployer$2 with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/deploy/DatapackDeployer$1 with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/deploy/CommandSources with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/config/ModConfigManager with maxs=true frames=true +[Architectury Transformer DEBUG] Writing dev/spyc0der77/mcspacks/config/ModConfig with maxs=true frames=true +[Architectury Transformer DEBUG] Closed File Systems for C:\Users\Carter\Code\Minecraft-Script\mod\common\build\libs\mcs-packs-1.21.2-common-0.1.0-transformProductionFabric.jar +[Architectury Transformer] Transformed jar with 5 transformer(s) in 225.5 ms diff --git a/mod/common/build.gradle b/mod/common/build.gradle new file mode 100644 index 0000000..9500651 --- /dev/null +++ b/mod/common/build.gradle @@ -0,0 +1,103 @@ +def usesPermissionSetApi = { + def version = rootProject.minecraft_version + if (version ==~ /^(?:2[6-9]|[3-9]\d*)\..+/) { + return true + } + def match = (version =~ /^1\.21\.(\d+)/) + if (match.matches()) { + return match[0][1].toInteger() >= 11 + } + return false +}() + +sourceSets { + main { + java { + srcDir usesPermissionSetApi ? 'src/modern/java' : 'src/legacy/java' + } + } +} + +architectury { + common rootProject.enabled_platforms.split(',').collect { it.trim() }.findAll { !it.isEmpty() } +} + +def generatedResourcesDir = layout.buildDirectory.dir('generated/resources/main') + +sourceSets.main.resources.srcDir(generatedResourcesDir) + +tasks.register('syncMcsVersions', Copy) { + from rootProject.file('../minecraft_script/versions/index.json') + into generatedResourcesDir + rename { 'mcs-versions.json' } +} + +tasks.register('syncStarterPack', Copy) { + from rootProject.file('../examples/starter_datapack.mcs') + into generatedResourcesDir.map { it.dir('starter') } + rename { 'pack.mcs' } +} + +processResources.dependsOn syncMcsVersions, syncStarterPack + +dependencies { + modImplementation "dev.architectury:architectury:${rootProject.architectury_api_version}" + implementation 'com.google.code.gson:gson:2.11.0' +} + +def spyglassSourceScript = file("${rootProject.projectDir}/scripts/mcs-spyglass-validate.mjs") +def spyglassBundledScript = layout.buildDirectory.file('generated/resources/main/scripts/mcs-spyglass-validate.js') + +tasks.register('bundleSpyglassScript') { + onlyIf { spyglassSourceScript.exists() } + inputs.file(spyglassSourceScript) + inputs.file("${rootProject.projectDir}/scripts/package.json") + inputs.file("${rootProject.projectDir}/scripts/bun.lock") + outputs.file(spyglassBundledScript.get().asFile) + + doLast { + def scriptsDir = file("${rootProject.projectDir}/scripts") + def bunAvailable = providers.exec { + workingDir scriptsDir + commandLine 'bun', '--version' + ignoreExitValue true + }.result.get().exitValue == 0 + + exec { + workingDir scriptsDir + if (bunAvailable) { + commandLine 'bun', 'run', 'bundle:spyglass' + } else { + commandLine 'npx', '--yes', 'esbuild@0.25.5', 'mcs-spyglass-validate.mjs', + '--bundle', '--platform=node', '--format=cjs', '--packages=bundle', + "--outfile=${spyglassBundledScript.get().asFile}", + '--banner:js=#!/usr/bin/env node' + } + } + } +} + +processResources { + dependsOn tasks.named('bundleSpyglassScript') + inputs.property 'mod_version', project.version + inputs.property 'minecraft_version', rootProject.minecraft_version + inputs.property 'mcs_profile', rootProject.mcsProfileName + inputs.property 'mcs_minecraft_profile', rootProject.mcsMinecraftProfile + inputs.property 'supported_game_versions', rootProject.supportedGameVersions + + filesMatching('fabric.mod.json') { + expand 'mod_version': project.version, + 'minecraft_version': rootProject.minecraft_version, + 'supported_game_versions': rootProject.supportedGameVersions + } + filesMatching('META-INF/mods.toml') { + expand 'mod_version': project.version, + 'minecraft_version': rootProject.minecraft_version, + 'supported_game_versions': rootProject.supportedGameVersions + } + filesMatching('META-INF/neoforge.mods.toml') { + expand 'mod_version': project.version, + 'minecraft_version': rootProject.minecraft_version, + 'supported_game_versions': rootProject.supportedGameVersions + } +} diff --git a/mod/common/src/legacy/java/dev/spyc0der77/mcspacks/deploy/CommandSources.java b/mod/common/src/legacy/java/dev/spyc0der77/mcspacks/deploy/CommandSources.java new file mode 100644 index 0000000..a6a9470 --- /dev/null +++ b/mod/common/src/legacy/java/dev/spyc0der77/mcspacks/deploy/CommandSources.java @@ -0,0 +1,14 @@ +package dev.spyc0der77.mcspacks.deploy; + +import net.minecraft.commands.CommandSourceStack; + +public final class CommandSources { + private static final int ALL_PERMISSIONS = 4; + + private CommandSources() { + } + + public static CommandSourceStack withAllPermissions(CommandSourceStack source) { + return source.withPermission(ALL_PERMISSIONS); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/McsPacks.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/McsPacks.java new file mode 100644 index 0000000..45d88d7 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/McsPacks.java @@ -0,0 +1,46 @@ +package dev.spyc0der77.mcspacks; + +import dev.architectury.event.events.common.LifecycleEvent; +import dev.spyc0der77.mcspacks.config.ModConfig; +import dev.spyc0der77.mcspacks.config.ModConfigManager; +import dev.spyc0der77.mcspacks.pipeline.PackPipeline; +import dev.spyc0der77.mcspacks.registry.PackRegistry; +import dev.spyc0der77.mcspacks.util.McsPaths; +import dev.spyc0der77.mcspacks.util.PlayerFeedback; +import dev.spyc0der77.mcspacks.version.VersionMapper; +import dev.spyc0der77.mcspacks.watch.PackWatcher; +import net.minecraft.server.MinecraftServer; + +public final class McsPacks { + private static PackPipeline pipeline; + private static PackWatcher watcher; + + private McsPacks() { + } + + public static void init() { + McsPaths.ensureLayout(); + ModConfig config = ModConfigManager.load(); + VersionMapper versionMapper = VersionMapper.load(); + PackRegistry registry = new PackRegistry(McsPaths.packsRoot(), McsPaths.compiledRoot()); + pipeline = new PackPipeline(config, versionMapper, registry, PlayerFeedback::broadcast); + watcher = new PackWatcher(McsPaths.packsRoot(), pipeline::handlePackChange, config.debounceMs); + + LifecycleEvent.SERVER_STARTING.register(McsPacks::onServerStarting); + LifecycleEvent.SERVER_STARTED.register(McsPacks::onServerStarted); + LifecycleEvent.SERVER_STOPPING.register(McsPacks::onServerStopping); + } + + private static void onServerStarting(MinecraftServer server) { + pipeline.prepareWorldPacks(server); + } + + private static void onServerStarted(MinecraftServer server) { + watcher.refreshBaselines(); + watcher.start(); + } + + private static void onServerStopping(MinecraftServer server) { + watcher.stop(); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/config/ModConfig.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/config/ModConfig.java new file mode 100644 index 0000000..b5e6f70 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/config/ModConfig.java @@ -0,0 +1,14 @@ +package dev.spyc0der77.mcspacks.config; + +public class ModConfig { + public String compiler = "auto"; + public String compilerPath = ""; + public int debounceMs = 500; + public boolean autoReload = true; + public boolean verboseCompile = false; + public String minecraftVersion = "auto"; + public boolean lintBeforeCompile = true; + public boolean spyglassValidateAfterCompile = true; + public boolean blockCompileOnLintErrors = true; + public boolean blockReloadOnSpyglassErrors = true; +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/config/ModConfigManager.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/config/ModConfigManager.java new file mode 100644 index 0000000..8d91dcc --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/config/ModConfigManager.java @@ -0,0 +1,53 @@ +package dev.spyc0der77.mcspacks.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import dev.spyc0der77.mcspacks.util.McsPaths; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +public final class ModConfigManager { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private ModConfigManager() { + } + + public static ModConfig load() { + try { + if (Files.notExists(McsPaths.configFile())) { + ModConfig defaults = new ModConfig(); + save(defaults); + return defaults; + } + String json = Files.readString(McsPaths.configFile()); + ModConfig config = GSON.fromJson(json, ModConfig.class); + return config == null ? new ModConfig() : config; + } catch (JsonParseException error) { + System.err.println("[MCS Packs] Invalid config JSON, using defaults: " + error.getMessage()); + return new ModConfig(); + } catch (IOException error) { + System.err.println("[MCS Packs] Failed to read config, using defaults: " + error.getMessage()); + return new ModConfig(); + } + } + + public static void save(ModConfig config) throws IOException { + Path configFile = McsPaths.configFile(); + Files.createDirectories(configFile.getParent()); + Path tempFile = Files.createTempFile(configFile.getParent(), "mcs-packs-", ".json.tmp"); + try { + Files.writeString(tempFile, GSON.toJson(config)); + try { + Files.move(tempFile, configFile, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException error) { + Files.move(tempFile, configFile, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(tempFile); + } + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/deploy/DatapackDeployer.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/deploy/DatapackDeployer.java new file mode 100644 index 0000000..7268cdb --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/deploy/DatapackDeployer.java @@ -0,0 +1,74 @@ +package dev.spyc0der77.mcspacks.deploy; + +import dev.spyc0der77.mcspacks.util.SafePaths; +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 net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +public final class DatapackDeployer { + private DatapackDeployer() { + } + + public static void syncPack(MinecraftServer server, Path compiledPack, String packFolderName) throws IOException { + Path worldDatapacks = server.getWorldPath(LevelResource.DATAPACK_DIR); + Files.createDirectories(worldDatapacks); + if (!Files.exists(compiledPack) || !Files.isDirectory(compiledPack) || !Files.isReadable(compiledPack)) { + throw new IOException("Compiled pack is missing or unreadable: " + compiledPack); + } + Path target = SafePaths.resolveChild(worldDatapacks, packFolderName); + if (Files.exists(target)) { + deleteRecursive(target); + } + copyRecursive(compiledPack, target); + } + + public static void reload(MinecraftServer server) { + server.submit(() -> server.getCommands().performPrefixedCommand( + CommandSources.withAllPermissions(server.createCommandSourceStack()), + "reload" + )); + } + + private static void copyRecursive(Path source, Path target) throws IOException { + Files.walkFileTree(source, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + Files.createDirectories(target.resolve(source.relativize(dir).toString())); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path destination = target.resolve(source.relativize(file).toString()); + Files.createDirectories(destination.getParent()); + Files.copy(file, destination, StandardCopyOption.REPLACE_EXISTING); + return FileVisitResult.CONTINUE; + } + }); + } + + private static void deleteRecursive(Path path) throws IOException { + if (!Files.exists(path)) { + return; + } + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/pipeline/PackPipeline.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/pipeline/PackPipeline.java new file mode 100644 index 0000000..392f103 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/pipeline/PackPipeline.java @@ -0,0 +1,182 @@ +package dev.spyc0der77.mcspacks.pipeline; + +import dev.architectury.platform.Platform; +import dev.spyc0der77.mcspacks.config.ModConfig; +import dev.spyc0der77.mcspacks.deploy.DatapackDeployer; +import dev.spyc0der77.mcspacks.registry.PackDefinition; +import dev.spyc0der77.mcspacks.registry.PackRegistry; +import dev.spyc0der77.mcspacks.toolchain.Diagnostic; +import dev.spyc0der77.mcspacks.toolchain.McsToolchain; +import dev.spyc0der77.mcspacks.util.PlayerFeedback; +import dev.spyc0der77.mcspacks.version.VersionMapper; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.BiConsumer; +import net.minecraft.server.MinecraftServer; + +public final class PackPipeline { + private final ModConfig config; + private final VersionMapper versionMapper; + private final PackRegistry registry; + private final BiConsumer messenger; + private final ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "mcs-packs-pipeline"); + thread.setDaemon(true); + return thread; + }); + private final Map generations = new ConcurrentHashMap<>(); + private volatile MinecraftServer activeServer; + + public PackPipeline( + ModConfig config, + VersionMapper versionMapper, + PackRegistry registry, + BiConsumer messenger + ) { + this.config = config; + this.versionMapper = versionMapper; + this.registry = registry; + this.messenger = messenger; + } + + public void bindServer(MinecraftServer server) { + this.activeServer = server; + } + + public void warmCompileAllPacks() { + executor.submit(() -> { + try { + for (PackDefinition pack : registry.discover()) { + runToolchain(pack, 0L); + } + } catch (IOException ignored) { + // Warm compile is best-effort before a world is opened. + } + }); + } + + public void prepareWorldPacks(MinecraftServer server) { + bindServer(server); + try { + for (PackDefinition pack : registry.discover()) { + try { + long generation = generations.merge(pack.id(), 1L, Long::sum); + processPack(server, pack, false, generation); + } catch (RuntimeException error) { + messenger.accept(server, "[MCS] Failed to prepare " + pack.id() + ": " + error.getMessage()); + } + } + } catch (IOException error) { + messenger.accept(server, "[MCS] Failed to prepare packs: " + error.getMessage()); + } + } + + public void handlePackChange(Path packFolder) { + MinecraftServer server = activeServer; + if (server == null) { + return; + } + PackDefinition pack; + try { + pack = registry.resolvePack(packFolder); + } catch (IOException error) { + messenger.accept(server, "[MCS] Failed to resolve " + packFolder.getFileName() + ": " + error.getMessage()); + return; + } + if (pack == null) { + return; + } + long generation = generations.merge(pack.id(), 1L, Long::sum); + executor.submit(() -> { + try { + processPack(server, pack, true, generation); + } catch (Exception error) { + messenger.accept(server, "[MCS] Failed to process " + packFolder.getFileName() + ": " + error.getMessage()); + } + }); + } + + private void processPack(MinecraftServer server, PackDefinition pack, boolean reloadAfterDeploy, long generation) { + messenger.accept(server, "[MCS] Processing " + pack.id() + "…"); + if (!runToolchain(pack, generation)) { + return; + } + if (!Files.exists(pack.compiledFolder())) { + messenger.accept(server, "[MCS] Compile finished but output folder is missing for " + pack.id()); + return; + } + + try { + if (config.autoReload) { + DatapackDeployer.syncPack(server, pack.compiledFolder(), pack.displayName()); + if (reloadAfterDeploy) { + DatapackDeployer.reload(server); + messenger.accept(server, "[MCS] Reloaded " + pack.id()); + } else { + messenger.accept(server, "[MCS] Prepared " + pack.id()); + } + } else { + messenger.accept(server, "[MCS] Built " + pack.id() + " (autoReload disabled)"); + } + } catch (Exception error) { + messenger.accept(server, "[MCS] Failed to deploy " + pack.id() + ": " + error.getMessage()); + } + } + + private boolean runToolchain(PackDefinition pack, long generation) { + try { + String mcsProfile = versionMapper.resolveMcsProfile(Platform.getMinecraftVersion(), config.minecraftVersion); + McsToolchain toolchain = new McsToolchain(config, mcsProfile); + + List lintDiagnostics = toolchain.lint(pack); + if (!lintDiagnostics.isEmpty()) { + MinecraftServer server = activeServer; + if (server != null) { + PlayerFeedback.diagnostics(server, "[MCS]", lintDiagnostics); + } + if (config.blockCompileOnLintErrors && lintDiagnostics.stream().anyMatch(Diagnostic::isError)) { + return false; + } + } + if (generation != 0L && generation != generations.get(pack.id())) { + return false; + } + + List compileDiagnostics = toolchain.compile(pack); + if (!compileDiagnostics.isEmpty()) { + MinecraftServer server = activeServer; + if (server != null) { + PlayerFeedback.diagnostics(server, "[MCS]", compileDiagnostics); + } + return false; + } + if (generation != 0L && generation != generations.get(pack.id())) { + return false; + } + + List spyglassDiagnostics = toolchain.validateWithSpyglass(pack, Platform.getMinecraftVersion()); + if (!spyglassDiagnostics.isEmpty()) { + MinecraftServer server = activeServer; + if (server != null) { + PlayerFeedback.diagnostics(server, "[Spyglass]", spyglassDiagnostics); + } + if (config.blockReloadOnSpyglassErrors && spyglassDiagnostics.stream().anyMatch(Diagnostic::isError)) { + return false; + } + } + return generation == 0L || generation == generations.get(pack.id()); + } catch (RuntimeException error) { + MinecraftServer server = activeServer; + if (server != null) { + messenger.accept(server, "[MCS] Toolchain failed for " + pack.id() + ": " + error.getMessage()); + } + return false; + } + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/registry/PackDefinition.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/registry/PackDefinition.java new file mode 100644 index 0000000..471d8c1 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/registry/PackDefinition.java @@ -0,0 +1,12 @@ +package dev.spyc0der77.mcspacks.registry; + +import java.nio.file.Path; + +public record PackDefinition( + String id, + Path folder, + Path entryFile, + Path compiledFolder, + String displayName +) { +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/registry/PackRegistry.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/registry/PackRegistry.java new file mode 100644 index 0000000..4b5e19c --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/registry/PackRegistry.java @@ -0,0 +1,101 @@ +package dev.spyc0der77.mcspacks.registry; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import dev.spyc0der77.mcspacks.util.McsPaths; +import dev.spyc0der77.mcspacks.util.SafePaths; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public final class PackRegistry { + private final Path packsRoot; + private final Path compiledRoot; + + public PackRegistry(Path packsRoot, Path compiledRoot) { + this.packsRoot = packsRoot; + this.compiledRoot = compiledRoot; + } + + public List discover() throws IOException { + List packs = new ArrayList<>(); + if (Files.notExists(packsRoot)) { + return packs; + } + try (var stream = Files.list(packsRoot)) { + for (Path folder : stream.toList()) { + if (!Files.isDirectory(folder)) { + continue; + } + String name = folder.getFileName().toString(); + if (name.startsWith("_") || name.startsWith(".")) { + continue; + } + PackDefinition pack = resolvePack(folder); + if (pack != null) { + packs.add(pack); + } + } + } + return packs; + } + + public PackDefinition resolvePack(Path folder) throws IOException { + PackOverrides overrides = readOverrides(folder); + Path entry = folder.resolve(overrides.entryFile == null ? McsPaths.DEFAULT_ENTRY : overrides.entryFile); + if (Files.notExists(entry)) { + return null; + } + String id = folder.getFileName().toString(); + String displayName = overrides.displayName == null + ? titleCase(id) + : overrides.displayName; + SafePaths.validateSafeName(displayName); + return new PackDefinition( + id, + folder, + entry, + SafePaths.resolveChild(compiledRoot, id), + displayName + ); + } + + private PackOverrides readOverrides(Path folder) throws IOException { + Path config = folder.resolve("pack.json"); + if (Files.notExists(config)) { + return new PackOverrides(); + } + try { + PackOverrides overrides = new Gson().fromJson(Files.readString(config), PackOverrides.class); + return overrides == null ? new PackOverrides() : overrides; + } catch (JsonParseException error) { + return new PackOverrides(); + } + } + + private static String titleCase(String value) { + String[] parts = value.replace('-', '_').split("_"); + StringBuilder builder = new StringBuilder(); + for (String part : parts) { + if (part.isBlank()) { + continue; + } + if (!builder.isEmpty()) { + builder.append(' '); + } + builder.append(part.substring(0, 1).toUpperCase(Locale.ROOT)); + if (part.length() > 1) { + builder.append(part.substring(1).toLowerCase(Locale.ROOT)); + } + } + return builder.isEmpty() ? value : builder.toString(); + } + + private static final class PackOverrides { + private String displayName; + private String entryFile; + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/CompilerResolver.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/CompilerResolver.java new file mode 100644 index 0000000..a79e106 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/CompilerResolver.java @@ -0,0 +1,200 @@ +package dev.spyc0der77.mcspacks.toolchain; + +import dev.spyc0der77.mcspacks.config.ModConfig; +import dev.spyc0der77.mcspacks.util.McsPaths; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public final class CompilerResolver { + private static final Object CACHE_LOCK = new Object(); + private static volatile String cachedKey; + private static volatile CompilerCommand cachedCommand; + + private CompilerResolver() { + } + + public record CompilerCommand(String executable, List baseArgs, boolean windowsBatch) { + List toProcessCommand(List args) { + List allArgs = new ArrayList<>(baseArgs); + allArgs.addAll(args); + if (windowsBatch && isWindows()) { + return List.of("cmd.exe", "/c", buildCmdInvocation(executable, allArgs)); + } + List command = new ArrayList<>(); + command.add(executable); + command.addAll(allArgs); + return command; + } + } + + public static CompilerCommand resolve(ModConfig config) { + String key = (config.compiler == null ? "auto" : config.compiler) + "|" + config.compilerPath; + synchronized (CACHE_LOCK) { + if (key.equals(cachedKey) && cachedCommand != null) { + return cachedCommand; + } + CompilerCommand resolved = resolveUncached(config); + cachedKey = key; + cachedCommand = resolved; + return resolved; + } + } + + private static CompilerCommand resolveUncached(ModConfig config) { + String mode = config.compiler == null ? "auto" : config.compiler.toLowerCase(Locale.ROOT); + if (config.compilerPath != null && !config.compilerPath.isBlank()) { + return fromExplicitPath(config.compilerPath, mode); + } + return switch (mode) { + case "python" -> resolvePython(); + case "mcs" -> new CompilerCommand("mcs", List.of(), false); + default -> resolveAuto(); + }; + } + + private static CompilerCommand fromExplicitPath(String compilerPath, String mode) { + if ("python".equals(mode) || looksLikePython(compilerPath)) { + return new CompilerCommand(compilerPath, List.of("-m", "minecraft_script"), isBatch(compilerPath)); + } + return new CompilerCommand(compilerPath, List.of(), isBatch(compilerPath)); + } + + private static CompilerCommand resolveAuto() { + CompilerCommand mcs = new CompilerCommand("mcs", List.of(), false); + if (canRun(mcs, "lint", "--help")) { + return mcs; + } + CompilerCommand python = resolvePython(); + if (canRun(python, "lint", "--help")) { + return python; + } + try { + Path wrapper = McsPaths.compilerWrapper(); + return new CompilerCommand(wrapper.toString(), List.of(), true); + } catch (IOException error) { + return new CompilerCommand("mcs", List.of(), false); + } + } + + private static CompilerCommand resolvePython() { + if (System.getenv("PYTHON") != null && Files.isExecutable(Path.of(System.getenv("PYTHON")))) { + CompilerCommand resolved = pythonCommand(System.getenv("PYTHON")); + if (canRun(resolved, "lint", "--help")) { + return resolved; + } + } + for (String version : List.of("313", "312", "311", "310")) { + for (Path candidate : knownPythonInstalls(version)) { + if (!Files.isExecutable(candidate)) { + continue; + } + CompilerCommand resolved = pythonCommand(candidate.toString()); + if (canRun(resolved, "lint", "--help")) { + return resolved; + } + } + } + for (String candidate : List.of("python", "python3", "py")) { + CompilerCommand resolved = "py".equals(candidate) + ? new CompilerCommand(candidate, List.of("-3", "-m", "minecraft_script"), false) + : pythonCommand(candidate); + if (canRun(resolved, "lint", "--help")) { + return resolved; + } + } + return pythonCommand("python"); + } + + private static List knownPythonInstalls(String version) { + List candidates = new ArrayList<>(); + if (isWindows()) { + String localAppData = System.getenv("LOCALAPPDATA"); + String programFiles = System.getenv("PROGRAMFILES"); + candidates.add(Path.of("C:\\Python" + version + "\\python.exe")); + if (localAppData != null) { + candidates.add(Path.of(localAppData, "Programs", "Python", "Python" + version, "python.exe")); + } + if (programFiles != null) { + candidates.add(Path.of(programFiles, "Python" + version, "python.exe")); + } + } else { + String dottedVersion = version.charAt(0) + "." + version.substring(1); + candidates.add(Path.of("/usr/local/bin/python" + dottedVersion)); + candidates.add(Path.of("/usr/bin/python" + dottedVersion)); + candidates.add(Path.of("/usr/local/bin/python3." + version.substring(1))); + candidates.add(Path.of("/usr/bin/python3." + version.substring(1))); + } + return candidates; + } + + private static CompilerCommand pythonCommand(String executable) { + return new CompilerCommand(executable, List.of("-m", "minecraft_script"), isBatch(executable)); + } + + private static boolean looksLikePython(String compilerPath) { + String lower = compilerPath.toLowerCase(Locale.ROOT); + return lower.endsWith("python.exe") || lower.endsWith("python") || lower.endsWith("python3"); + } + + private static boolean isBatch(String compilerPath) { + String lower = compilerPath.toLowerCase(Locale.ROOT); + return lower.endsWith(".cmd") || lower.endsWith(".bat"); + } + + private static boolean canRun(CompilerCommand compiler, String... args) { + try { + ProcessResult result = SubprocessRunner.run(compiler.toProcessCommand(List.of(args)), null, 15); + return result.success(); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + return false; + } catch (Exception ignored) { + return false; + } + } + + private static boolean isWindows() { + String os = System.getProperty("os.name", ""); + return os.toLowerCase(Locale.ROOT).contains("win"); + } + + private static String buildCmdInvocation(String executable, List args) { + StringBuilder command = new StringBuilder(); + command.append('"').append(executable).append('"'); + for (String arg : args) { + command.append(' ').append(quoteForCmd(arg)); + } + return command.toString(); + } + + private static String quoteForCmd(String arg) { + if (!needsCmdQuoting(arg)) { + return arg; + } + return "\"" + arg.replace("\"", "\\\"").replace("%", "%%") + "\""; + } + + private static boolean needsCmdQuoting(String arg) { + if (arg.isEmpty()) { + return true; + } + for (int index = 0; index < arg.length(); index++) { + char character = arg.charAt(index); + if (Character.isWhitespace(character) + || character == '"' + || character == '&' + || character == '|' + || character == '<' + || character == '>' + || character == '^' + || character == '%') { + return true; + } + } + return false; + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/Diagnostic.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/Diagnostic.java new file mode 100644 index 0000000..3ef1b21 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/Diagnostic.java @@ -0,0 +1,7 @@ +package dev.spyc0der77.mcspacks.toolchain; + +public record Diagnostic(String file, int line, int column, String message, String severity) { + public boolean isError() { + return "error".equalsIgnoreCase(severity); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/McsToolchain.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/McsToolchain.java new file mode 100644 index 0000000..ca94004 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/McsToolchain.java @@ -0,0 +1,138 @@ +package dev.spyc0der77.mcspacks.toolchain; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import dev.spyc0der77.mcspacks.config.ModConfig; +import dev.spyc0der77.mcspacks.registry.PackDefinition; +import dev.spyc0der77.mcspacks.util.McsPaths; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public final class McsToolchain { + private static final Gson GSON = new Gson(); + private static final Type DIAGNOSTIC_LIST = new TypeToken>() {}.getType(); + + private final ModConfig config; + private final String mcsProfile; + private final CompilerResolver.CompilerCommand compiler; + + public McsToolchain(ModConfig config, String mcsProfile) { + this.config = config; + this.mcsProfile = mcsProfile; + this.compiler = CompilerResolver.resolve(config); + } + + public List lint(PackDefinition pack) { + if (!config.lintBeforeCompile) { + return List.of(); + } + try { + ProcessResult result = SubprocessRunner.run( + compiler.toProcessCommand(List.of("lint", "--json", pack.entryFile().toString())), + pack.folder(), + 120 + ); + if (result.stdout().isBlank()) { + if (!result.success()) { + return List.of(new Diagnostic(relative(pack, pack.entryFile()), 1, 1, result.stderr().trim(), "error")); + } + return List.of(); + } + List diagnostics = GSON.fromJson(result.stdout(), DIAGNOSTIC_LIST); + return diagnostics == null ? List.of() : diagnostics; + } catch (Exception error) { + return List.of(new Diagnostic(relative(pack, pack.entryFile()), 1, 1, error.getMessage(), "error")); + } + } + + public List compile(PackDefinition pack) { + try { + if (Files.exists(pack.compiledFolder())) { + deleteRecursive(pack.compiledFolder()); + } + } catch (IOException error) { + return List.of(new Diagnostic(pack.id(), 1, 1, error.getMessage(), "error")); + } + List args = new ArrayList<>(); + args.add("compile"); + args.add("--mc-version"); + args.add(mcsProfile); + args.add("--force"); + if (!config.verboseCompile) { + args.add("--no-verbose"); + } + args.add(pack.entryFile().toString()); + args.add(pack.id()); + args.add(McsPaths.compiledRoot().toString()); + try { + ProcessResult result = SubprocessRunner.run(compiler.toProcessCommand(args), pack.folder(), 300); + if (!result.success()) { + String message = result.stderr().isBlank() ? result.stdout() : result.stderr(); + return List.of(new Diagnostic(relative(pack, pack.entryFile()), 1, 1, message.trim(), "error")); + } + return List.of(); + } catch (Exception error) { + return List.of(new Diagnostic(relative(pack, pack.entryFile()), 1, 1, error.getMessage(), "error")); + } + } + + public List validateWithSpyglass(PackDefinition pack, String gameVersion) { + if (!config.spyglassValidateAfterCompile || Files.notExists(pack.compiledFolder())) { + return List.of(); + } + try { + Path script = McsPaths.spyglassScript(); + ProcessResult result = SubprocessRunner.run( + List.of( + "node", + script.toString(), + "--json", + "--mc-version", + gameVersion, + pack.compiledFolder().toString() + ), + null, + 300 + ); + if (result.stdout().isBlank()) { + if (!result.success()) { + return List.of(new Diagnostic(pack.id(), 1, 1, result.stderr().trim(), "error")); + } + return List.of(); + } + List diagnostics = GSON.fromJson(result.stdout(), DIAGNOSTIC_LIST); + return diagnostics == null ? List.of() : diagnostics; + } catch (Exception error) { + return List.of(new Diagnostic(pack.id(), 1, 1, error.getMessage(), "warning")); + } + } + + private static String relative(PackDefinition pack, Path file) { + Path folder = pack.folder().toAbsolutePath().normalize(); + Path absolute = file.toAbsolutePath().normalize(); + if (!absolute.startsWith(folder)) { + return absolute.toString().replace('\\', '/'); + } + return folder.relativize(absolute).toString().replace('\\', '/'); + } + + private static void deleteRecursive(Path path) throws IOException { + if (Files.isSymbolicLink(path)) { + Files.delete(path); + return; + } + if (Files.isDirectory(path)) { + try (var stream = Files.list(path)) { + for (Path child : stream.toList()) { + deleteRecursive(child); + } + } + } + Files.deleteIfExists(path); + } + +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/ProcessResult.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/ProcessResult.java new file mode 100644 index 0000000..0f5849e --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/ProcessResult.java @@ -0,0 +1,7 @@ +package dev.spyc0der77.mcspacks.toolchain; + +public record ProcessResult(int exitCode, String stdout, String stderr) { + public boolean success() { + return exitCode == 0; + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/SubprocessRunner.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/SubprocessRunner.java new file mode 100644 index 0000000..5c54b35 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/toolchain/SubprocessRunner.java @@ -0,0 +1,52 @@ +package dev.spyc0der77.mcspacks.toolchain; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public final class SubprocessRunner { + private static final long REAP_TIMEOUT_SECONDS = 5; + + private SubprocessRunner() { + } + + public static ProcessResult run(List command, Path workingDirectory, long timeoutSeconds) throws IOException, InterruptedException { + ProcessBuilder builder = new ProcessBuilder(command); + if (workingDirectory != null) { + builder.directory(workingDirectory.toFile()); + } + builder.redirectErrorStream(false); + Process process = builder.start(); + CompletableFuture stdoutFuture = CompletableFuture.supplyAsync(() -> readStream(process.getInputStream())); + CompletableFuture stderrFuture = CompletableFuture.supplyAsync(() -> readStream(process.getErrorStream())); + try { + boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + reapProcess(process); + return new ProcessResult(-1, "", "Process timed out after " + timeoutSeconds + "s"); + } + return new ProcessResult(process.exitValue(), stdoutFuture.join(), stderrFuture.join()); + } catch (InterruptedException error) { + process.destroyForcibly(); + reapProcess(process); + Thread.currentThread().interrupt(); + return new ProcessResult(-1, "", "Process interrupted"); + } + } + + private static void reapProcess(Process process) throws InterruptedException { + process.waitFor(REAP_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + private static String readStream(java.io.InputStream stream) { + try { + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException ignored) { + return ""; + } + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/McsPaths.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/McsPaths.java new file mode 100644 index 0000000..93065bc --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/McsPaths.java @@ -0,0 +1,105 @@ +package dev.spyc0der77.mcspacks.util; + +import dev.architectury.platform.Platform; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; + +public final class McsPaths { + public static final String PACKS_DIR = "mcs_packs"; + public static final String COMPILED_DIR = "_compiled"; + public static final String DEFAULT_ENTRY = "pack.mcs"; + public static final String CONFIG_FILE = "mcs-packs.json"; + + private McsPaths() { + } + + public static Path gameRoot() { + return Platform.getGameFolder(); + } + + public static Path configFile() { + return Platform.getConfigFolder().resolve(CONFIG_FILE); + } + + public static Path packsRoot() { + return gameRoot().resolve(PACKS_DIR); + } + + public static Path compiledRoot() { + return packsRoot().resolve(COMPILED_DIR); + } + + public static Path compilerWrapper() throws IOException { + boolean windows = isWindows(); + String resourceName = windows ? "/scripts/mcs-compile.cmd" : "/scripts/mcs-compile.sh"; + String fileName = windows ? "mcs-compile.cmd" : "mcs-compile.sh"; + Path scriptsDir = gameRoot().resolve("mcs-packs-scripts"); + Files.createDirectories(scriptsDir); + Path wrapper = scriptsDir.resolve(fileName); + try (InputStream stream = McsPaths.class.getResourceAsStream(resourceName)) { + if (stream == null) { + throw new IOException("Bundled MCS compiler wrapper is missing from the mod JAR: " + resourceName); + } + Files.copy(stream, wrapper, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + if (!windows && !wrapper.toFile().setExecutable(true, false)) { + throw new IOException("Failed to mark MCS compiler wrapper as executable: " + wrapper); + } + return wrapper; + } + + public static Path spyglassScript() throws IOException { + Path scriptsDir = gameRoot().resolve("mcs-packs-scripts"); + Files.createDirectories(scriptsDir); + Path script = scriptsDir.resolve("mcs-spyglass-validate.js"); + InputStream bundled = McsPaths.class.getResourceAsStream("/scripts/mcs-spyglass-validate.mjs"); + if (bundled == null) { + bundled = McsPaths.class.getResourceAsStream("/scripts/mcs-spyglass-validate.js"); + } + if (bundled == null) { + throw new IOException("Bundled Spyglass validator script is missing from the mod JAR"); + } + try (InputStream stream = bundled) { + Files.copy(stream, script, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + return script; + } + + public static void ensureLayout() { + try { + Files.createDirectories(packsRoot()); + Files.createDirectories(compiledRoot()); + Path starter = packsRoot().resolve("starter"); + Files.createDirectories(starter); + Path starterPack = starter.resolve(DEFAULT_ENTRY); + if (Files.notExists(starterPack)) { + try (InputStream stream = McsPaths.class.getResourceAsStream("/starter/pack.mcs")) { + if (stream != null) { + Files.copy(stream, starterPack); + } + } + } + Path readme = packsRoot().resolve("README.txt"); + if (Files.notExists(readme)) { + Files.writeString(readme, """ + MCS Packs + ========= + Create one folder per datapack project under mcs_packs/. + Each folder needs a pack.mcs entry file. + Compiled output is written to mcs_packs/_compiled/. + Mod settings live in config/mcs-packs.json. + """); + } + } catch (IOException error) { + throw new IllegalStateException("Failed to initialize mcs_packs folders", error); + } + } + + private static boolean isWindows() { + String os = System.getProperty("os.name", ""); + return os.toLowerCase(Locale.ROOT).contains("win"); + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/PlayerFeedback.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/PlayerFeedback.java new file mode 100644 index 0000000..552c5fc --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/PlayerFeedback.java @@ -0,0 +1,25 @@ +package dev.spyc0der77.mcspacks.util; + +import dev.spyc0der77.mcspacks.toolchain.Diagnostic; +import java.util.List; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; + +public final class PlayerFeedback { + private PlayerFeedback() { + } + + public static void broadcast(MinecraftServer server, String message) { + server.submit(() -> { + Component component = Component.literal(message); + server.getPlayerList().broadcastSystemMessage(component, false); + }); + } + + public static void diagnostics(MinecraftServer server, String prefix, List diagnostics) { + for (Diagnostic diagnostic : diagnostics) { + String location = diagnostic.file() + ":" + diagnostic.line() + ":" + diagnostic.column(); + broadcast(server, prefix + " " + location + " — " + diagnostic.message()); + } + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/SafePaths.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/SafePaths.java new file mode 100644 index 0000000..8b511d8 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/util/SafePaths.java @@ -0,0 +1,28 @@ +package dev.spyc0der77.mcspacks.util; + +import java.io.IOException; +import java.nio.file.Path; + +public final class SafePaths { + private SafePaths() { + } + + public static void validateSafeName(String name) throws IOException { + if (name == null || name.isBlank()) { + throw new IOException("Name must not be blank"); + } + if (name.equals(".") || name.contains("/") || name.contains("\\") || name.contains("..")) { + throw new IOException("Unsafe path name: " + name); + } + } + + public static Path resolveChild(Path parent, String childName) throws IOException { + validateSafeName(childName); + Path normalizedParent = parent.toAbsolutePath().normalize(); + Path resolved = normalizedParent.resolve(childName).normalize(); + if (resolved.equals(normalizedParent) || !resolved.startsWith(normalizedParent)) { + throw new IOException("Path escapes parent directory: " + childName); + } + return resolved; + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/version/VersionMapper.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/version/VersionMapper.java new file mode 100644 index 0000000..2d84deb --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/version/VersionMapper.java @@ -0,0 +1,61 @@ +package dev.spyc0der77.mcspacks.version; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +public final class VersionMapper { + private final List supported; + private final Map profiles; + + private VersionMapper(List supported, Map profiles) { + this.supported = supported; + this.profiles = profiles; + } + + public static VersionMapper load() { + try (InputStream stream = VersionMapper.class.getResourceAsStream("/mcs-versions.json")) { + if (stream == null) { + throw new IllegalStateException("Missing mcs-versions.json resource"); + } + Index index = new Gson().fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), Index.class); + if (index == null || index.supported == null || index.supported.isEmpty()) { + throw new IllegalStateException("mcs-versions.json must define a non-empty supported version list"); + } + if (index.profiles == null || index.profiles.isEmpty()) { + throw new IllegalStateException("mcs-versions.json must define a non-empty profiles map"); + } + return new VersionMapper(index.supported, index.profiles); + } catch (Exception error) { + throw new IllegalStateException("Failed to load MCS version index", error); + } + } + + public String resolveMcsProfile(String gameVersion, String configuredVersion) { + String version = configuredVersion; + if (version == null || version.isBlank() || "auto".equalsIgnoreCase(version)) { + version = gameVersion; + } + if (profiles.containsKey(version)) { + return profiles.get(version); + } + if (supported.contains(version)) { + return version; + } + throw new IllegalStateException( + "Minecraft version " + version + " is not supported by MCS. Supported: " + String.join(", ", supported) + ); + } + + private static final class Index { + @SerializedName("supported") + private List supported; + + @SerializedName("profiles") + private Map profiles; + } +} diff --git a/mod/common/src/main/java/dev/spyc0der77/mcspacks/watch/PackWatcher.java b/mod/common/src/main/java/dev/spyc0der77/mcspacks/watch/PackWatcher.java new file mode 100644 index 0000000..ebb1482 --- /dev/null +++ b/mod/common/src/main/java/dev/spyc0der77/mcspacks/watch/PackWatcher.java @@ -0,0 +1,272 @@ +package dev.spyc0der77.mcspacks.watch; + +import dev.spyc0der77.mcspacks.util.McsPaths; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public final class PackWatcher { + private final Path packsRoot; + private final Consumer onPackChange; + private final long debounceMs; + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> { + Thread thread = new Thread(runnable, "mcs-packs-watcher-scheduler"); + thread.setDaemon(true); + return thread; + }); + private final Map> pending = new ConcurrentHashMap<>(); + private final Map lastModified = new ConcurrentHashMap<>(); + private volatile boolean running; + private Thread watchThread; + + public PackWatcher(Path packsRoot, Consumer onPackChange, long debounceMs) { + this.packsRoot = packsRoot; + this.onPackChange = onPackChange; + this.debounceMs = Math.max(100, debounceMs); + } + + public synchronized void start() { + if (running) { + return; + } + running = true; + seedModifiedTimes(); + watchThread = new Thread(this::runLoop, "mcs-packs-watcher"); + watchThread.setDaemon(true); + watchThread.start(); + } + + public void refreshBaselines() { + seedModifiedTimes(); + } + + public synchronized void stop() { + running = false; + if (watchThread != null) { + watchThread.interrupt(); + watchThread = null; + } + pending.values().forEach(future -> future.cancel(false)); + pending.clear(); + } + + private void runLoop() { + try (WatchService watchService = FileSystems.getDefault().newWatchService()) { + registerTree(watchService, packsRoot); + while (running) { + WatchKey key = watchService.poll(1, TimeUnit.SECONDS); + if (key == null) { + pollEntryFiles(); + continue; + } + Path watchedDirectory = (Path) key.watchable(); + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + continue; + } + Path context = (Path) event.context(); + if (context == null || shouldIgnore(context)) { + continue; + } + Path changedPath = watchedDirectory.resolve(context); + if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE && Files.isDirectory(changedPath)) { + registerTree(watchService, changedPath); + } + Path packFolder = resolvePackFolder(changedPath); + if (packFolder != null) { + touchEntryTimestamp(packFolder); + schedule(packFolder); + } + } + if (!key.reset()) { + registerDirectory(watchService, watchedDirectory); + } + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } catch (IOException error) { + throw new IllegalStateException("Failed to watch mcs_packs", error); + } + } + + private void schedule(Path packFolder) { + pending.compute(packFolder, (path, existing) -> { + if (existing != null) { + existing.cancel(false); + } + return scheduler.schedule(() -> { + pending.remove(path); + onPackChange.accept(path); + }, debounceMs, TimeUnit.MILLISECONDS); + }); + } + + private void seedModifiedTimes() { + try { + if (!Files.exists(packsRoot)) { + return; + } + try (var stream = Files.list(packsRoot)) { + for (Path packFolder : stream.toList()) { + if (!isPackFolder(packFolder)) { + continue; + } + Path entry = packFolder.resolve(McsPaths.DEFAULT_ENTRY); + if (Files.exists(entry)) { + lastModified.put(entry, Files.getLastModifiedTime(entry).toMillis()); + } + } + } + } catch (IOException ignored) { + // Best-effort baseline for polling. + } + } + + private void pollEntryFiles() { + try { + if (!Files.exists(packsRoot)) { + return; + } + try (var stream = Files.list(packsRoot)) { + for (Path packFolder : stream.toList()) { + if (!isPackFolder(packFolder)) { + continue; + } + Path entry = packFolder.resolve(McsPaths.DEFAULT_ENTRY); + if (!Files.exists(entry)) { + if (lastModified.remove(entry) != null) { + schedule(packFolder); + } + continue; + } + long modified = Files.getLastModifiedTime(entry).toMillis(); + Long previous = lastModified.put(entry, modified); + if (previous == null || modified > previous) { + schedule(packFolder); + } + } + } + } catch (IOException ignored) { + // Polling is a fallback when native watch events are missed. + } + } + + private void touchEntryTimestamp(Path packFolder) { + try { + Path entry = packFolder.resolve(McsPaths.DEFAULT_ENTRY); + if (Files.exists(entry)) { + lastModified.put(entry, Files.getLastModifiedTime(entry).toMillis()); + } + } catch (IOException ignored) { + // Ignore timestamp refresh failures. + } + } + + private Path resolvePackFolder(Path changedPath) { + if (!changedPath.startsWith(packsRoot)) { + return null; + } + + Path current; + if (Files.isDirectory(changedPath)) { + current = changedPath; + } else { + String fileName = changedPath.getFileName().toString().toLowerCase(Locale.ROOT); + if (!fileName.endsWith(".mcs")) { + return null; + } + current = changedPath.getParent(); + } + + while (current != null && current.startsWith(packsRoot) && !packsRoot.equals(current)) { + if (isPackFolder(current)) { + Path entry = current.resolve(McsPaths.DEFAULT_ENTRY); + if (Files.exists(entry)) { + return current; + } + if (lastModified.containsKey(entry)) { + return current; + } + return null; + } + current = current.getParent(); + } + return null; + } + + private boolean isPackFolder(Path packFolder) { + if (!Files.isDirectory(packFolder) || !packFolder.startsWith(packsRoot)) { + return false; + } + if (packsRoot.equals(packFolder)) { + return false; + } + Path relative = packsRoot.relativize(packFolder); + if (relative.getNameCount() != 1) { + return false; + } + String name = packFolder.getFileName().toString(); + return !name.startsWith("_") && !name.startsWith("."); + } + + private static boolean shouldIgnore(Path path) { + String name = path.getFileName().toString(); + return name.startsWith(".") || name.endsWith("~") || name.endsWith(".swp") || name.endsWith(".tmp"); + } + + private static void registerTree(WatchService watchService, Path root) throws IOException { + if (!Files.exists(root)) { + Files.createDirectories(root); + } + try (var stream = Files.walk(root)) { + stream.filter(Files::isDirectory).forEach(dir -> registerDirectory(watchService, dir)); + } + } + + private static void registerDirectory(WatchService watchService, Path directory) { + String name = directory.getFileName() == null ? "" : directory.getFileName().toString(); + if (name.equals("_compiled")) { + return; + } + try { + if (isWindows()) { + directory.register( + watchService, + new WatchEvent.Kind[]{ + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE + }, + com.sun.nio.file.SensitivityWatchEventModifier.HIGH + ); + } else { + directory.register( + watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE + ); + } + } catch (IOException ignored) { + // Some directories may not be watchable on all platforms. + } + } + + private static boolean isWindows() { + String os = System.getProperty("os.name", ""); + return os.toLowerCase(Locale.ROOT).contains("win"); + } +} diff --git a/mod/common/src/main/resources/scripts/mcs-compile.cmd b/mod/common/src/main/resources/scripts/mcs-compile.cmd new file mode 100644 index 0000000..98ff0e1 --- /dev/null +++ b/mod/common/src/main/resources/scripts/mcs-compile.cmd @@ -0,0 +1,69 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion +where mcs >nul 2>&1 && ( + call :run_mcs %* + exit /b !ERRORLEVEL! +) +if defined PYTHON if exist "%PYTHON%" ( + call :run_python "%PYTHON%" %* + exit /b !ERRORLEVEL! +) +for %%P in (313 312 311 310) do ( + if exist "C:\Python%%P\python.exe" ( + call :run_python "C:\Python%%P\python.exe" %* + exit /b !ERRORLEVEL! + ) + if exist "%LOCALAPPDATA%\Programs\Python\Python%%P\python.exe" ( + call :run_python "%LOCALAPPDATA%\Programs\Python\Python%%P\python.exe" %* + exit /b !ERRORLEVEL! + ) + if exist "%PROGRAMFILES%\Python%%P\python.exe" ( + call :run_python "%PROGRAMFILES%\Python%%P\python.exe" %* + exit /b !ERRORLEVEL! + ) +) +where python >nul 2>&1 && ( + call :run_python python %* + exit /b !ERRORLEVEL! +) +where py >nul 2>&1 && ( + call :run_py %* + exit /b !ERRORLEVEL! +) +echo MCS compiler not found. Install minecraft-script ^(pip install -e .^) or set compilerPath in config/mcs-packs.json. 1>&2 +exit /b 1 + +:run_mcs +set "ARGS=" +:quote_mcs +if "%~1"=="" goto exec_mcs +set "ARGS=%ARGS% "%~1"" +shift +goto quote_mcs +:exec_mcs +mcs %ARGS% +exit /b %ERRORLEVEL% + +:run_python +set "PY=%~1" +shift +set "ARGS=" +:quote_py +if "%~1"=="" goto exec_py +set "ARGS=%ARGS% "%~1"" +shift +goto quote_py +:exec_py +"%PY%" -m minecraft_script %ARGS% +exit /b %ERRORLEVEL% + +:run_py +set "ARGS=" +:quote_py_launcher +if "%~1"=="" goto exec_py_launcher +set "ARGS=%ARGS% "%~1"" +shift +goto quote_py_launcher +:exec_py_launcher +py -3 -m minecraft_script %ARGS% +exit /b %ERRORLEVEL% diff --git a/mod/common/src/main/resources/scripts/mcs-compile.sh b/mod/common/src/main/resources/scripts/mcs-compile.sh new file mode 100644 index 0000000..661799c --- /dev/null +++ b/mod/common/src/main/resources/scripts/mcs-compile.sh @@ -0,0 +1,41 @@ +#!/bin/sh +set -e + +if command -v mcs >/dev/null 2>&1; then + exec mcs "$@" +fi + +if [ -n "$PYTHON" ] && [ -x "$PYTHON" ]; then + exec "$PYTHON" -m minecraft_script "$@" +fi + +for version in 313 312 311 310; do + dotted_version="${version%??}.${version#?}" + for candidate in \ + "/c/Python${version}/python.exe" \ + "$LOCALAPPDATA/Programs/Python/Python${version}/python.exe" \ + "$PROGRAMFILES/Python${version}/python.exe" \ + "/usr/local/bin/python${dotted_version}" \ + "/usr/bin/python${dotted_version}" \ + "/usr/local/bin/python3.${version#?}" \ + "/usr/bin/python3.${version#?}"; do + if [ -x "$candidate" ]; then + exec "$candidate" -m minecraft_script "$@" + fi + done +done + +if command -v python3 >/dev/null 2>&1; then + exec python3 -m minecraft_script "$@" +fi + +if command -v python >/dev/null 2>&1; then + exec python -m minecraft_script "$@" +fi + +if command -v py >/dev/null 2>&1; then + exec py -3 -m minecraft_script "$@" +fi + +echo "MCS compiler not found. Install minecraft-script (pip install -e .) or set compilerPath in config/mcs-packs.json." >&2 +exit 1 diff --git a/mod/common/src/modern/java/dev/spyc0der77/mcspacks/deploy/CommandSources.java b/mod/common/src/modern/java/dev/spyc0der77/mcspacks/deploy/CommandSources.java new file mode 100644 index 0000000..00110d6 --- /dev/null +++ b/mod/common/src/modern/java/dev/spyc0der77/mcspacks/deploy/CommandSources.java @@ -0,0 +1,13 @@ +package dev.spyc0der77.mcspacks.deploy; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.permissions.PermissionSet; + +public final class CommandSources { + private CommandSources() { + } + + public static CommandSourceStack withAllPermissions(CommandSourceStack source) { + return source.withPermission(PermissionSet.ALL_PERMISSIONS); + } +} diff --git a/mod/dev/spyc0der77/mcspacks/toolchain/CompilerResolver.class b/mod/dev/spyc0der77/mcspacks/toolchain/CompilerResolver.class new file mode 100644 index 0000000..e12651e Binary files /dev/null and b/mod/dev/spyc0der77/mcspacks/toolchain/CompilerResolver.class differ diff --git a/mod/fabric/build.gradle b/mod/fabric/build.gradle new file mode 100644 index 0000000..1dd5d71 --- /dev/null +++ b/mod/fabric/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +architectury { + platformSetupLoomIde() + fabric() +} + +configurations { + common + shadowCommon + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentFabric.extendsFrom common +} + +dependencies { + modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}" + modImplementation "dev.architectury:architectury-fabric:${rootProject.architectury_api_version}" + + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowCommon(project(path: ':common', configuration: 'transformProductionFabric')) { transitive false } +} + +jar { + archiveClassifier = 'dev' +} + +shadowJar { + configurations = [project.configurations.shadowCommon] + archiveClassifier = 'dev-shadow' +} + +remapJar { + input.set shadowJar.archiveFile + dependsOn shadowJar + archiveClassifier = null +} + +processResources { + inputs.property 'mod_version', project.version + inputs.property 'minecraft_version', rootProject.minecraft_version + inputs.property 'supported_game_versions', rootProject.supportedGameVersions + inputs.property 'architectury_api_version', rootProject.architectury_api_version + inputs.property 'fabric_api_version', rootProject.fabric_api_version + inputs.property 'java_version', rootProject.requiredJavaVersion + filesMatching('fabric.mod.json') { + expand 'mod_version': project.version, + 'minecraft_version': rootProject.minecraft_version, + 'supported_game_versions': rootProject.supportedGameVersions, + 'architectury_api_version': rootProject.architectury_api_version, + 'fabric_api_version': rootProject.fabric_api_version, + 'java_version': rootProject.requiredJavaVersion + } +} diff --git a/mod/fabric/gradle.properties b/mod/fabric/gradle.properties new file mode 100644 index 0000000..e846a8f --- /dev/null +++ b/mod/fabric/gradle.properties @@ -0,0 +1 @@ +loom.platform=fabric diff --git a/mod/fabric/src/main/java/dev/spyc0der77/mcspacks/fabric/McsPacksFabric.java b/mod/fabric/src/main/java/dev/spyc0der77/mcspacks/fabric/McsPacksFabric.java new file mode 100644 index 0000000..64d6a7a --- /dev/null +++ b/mod/fabric/src/main/java/dev/spyc0der77/mcspacks/fabric/McsPacksFabric.java @@ -0,0 +1,11 @@ +package dev.spyc0der77.mcspacks.fabric; + +import dev.spyc0der77.mcspacks.McsPacks; +import net.fabricmc.api.ModInitializer; + +public final class McsPacksFabric implements ModInitializer { + @Override + public void onInitialize() { + McsPacks.init(); + } +} diff --git a/mod/fabric/src/main/resources/fabric.mod.json b/mod/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..fe55296 --- /dev/null +++ b/mod/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "mcs_packs", + "version": "${mod_version}", + "name": "MCS Packs", + "description": "Compile and hot-reload Minecraft Script datapacks from mcs_packs/", + "authors": ["SpyC0der77"], + "license": "MIT", + "environment": "*", + "entrypoints": { + "main": ["dev.spyc0der77.mcspacks.fabric.McsPacksFabric"] + }, + "depends": { + "fabricloader": ">=0.16.0", + "minecraft": "${minecraft_version}", + "java": ">=${java_version}", + "architectury": ">=${architectury_api_version}", + "fabric-api": ">=${fabric_api_version}" + } +} diff --git a/mod/forge/build.gradle b/mod/forge/build.gradle new file mode 100644 index 0000000..6b5e680 --- /dev/null +++ b/mod/forge/build.gradle @@ -0,0 +1,96 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +loom { + forge { + mixinConfig 'mcs_packs.mixins.json' + } +} + +architectury { + platformSetupLoomIde() + forge() +} + +configurations { + common + shadowCommon + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentForge.extendsFrom common +} + +dependencies { + forge "net.minecraftforge:forge:${rootProject.minecraft_version}-${rootProject.forge_version}" + modImplementation "dev.architectury:architectury-neoforge:${rootProject.architectury_api_version}" + + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowCommon(project(path: ':common', configuration: 'transformProductionForge')) { transitive false } +} + +jar { + archiveClassifier = 'dev' +} + +shadowJar { + configurations = [project.configurations.shadowCommon] + archiveClassifier = 'dev-shadow' +} + +remapJar { + input.set shadowJar.archiveFile + dependsOn shadowJar + archiveClassifier = null +} + +def stripPreReleaseSuffix = { String version -> + version.replaceFirst(/-(?:beta|alpha|rc\d*|snapshot|pre).*$/, '') +} + +def warnVersionFallback = { String name, String version, String reason, String safeUpper -> + logger.warn("Using fallback upper bound for ${name} '${version}': ${reason}. Using '${safeUpper}'") + safeUpper +} + +def nextMinecraftVersion = { + def version = stripPreReleaseSuffix(rootProject.minecraft_version) + def parts = version.tokenize('.') + if (parts.size() == 2 && parts[0].isInteger() && parts[1].isInteger() && parts[0].toInteger() >= 26) { + return "${parts[0]}.${parts[1].toInteger() + 1}" + } + if (parts.size() == 3 && parts.every { it.isInteger() }) { + return "${parts[0]}.${parts[1]}.${parts[2].toInteger() + 1}" + } + warnVersionFallback('minecraft_version', rootProject.minecraft_version, "unrecognized format after normalization '${version}'", '99.99.99') +}() + +def nextMajorVersion = { String version, String suffix = '', String name = 'version', String safeUpper = '99.0.0' -> + def normalized = stripPreReleaseSuffix(version) + def major = normalized.tokenize('.')[0] + if (major.isInteger()) { + return "${major.toInteger() + 1}${suffix}" + } + warnVersionFallback(name, version, "unrecognized major component '${major}'", safeUpper) +} + +def nextForgeVersion = nextMajorVersion(rootProject.forge_version, '.0.0', 'forge_version', '99.0.0') +def nextArchitecturyVersion = nextMajorVersion(rootProject.architectury_api_version, '.0.0', 'architectury_api_version', '99.0.0') + +processResources { + inputs.property 'mod_version', project.version + inputs.property 'minecraft_version', rootProject.minecraft_version + inputs.property 'supported_game_versions', rootProject.supportedGameVersions + inputs.property 'forge_version', rootProject.forge_version + inputs.property 'architectury_api_version', rootProject.architectury_api_version + filesMatching('META-INF/mods.toml') { + expand 'mod_version': project.version, + 'minecraft_version': rootProject.minecraft_version, + 'forge_version': rootProject.forge_version, + 'minecraft_version_upper': nextMinecraftVersion, + 'forge_version_upper': nextForgeVersion, + 'architectury_api_version': rootProject.architectury_api_version, + 'architectury_api_version_upper': nextArchitecturyVersion, + 'supported_game_versions': rootProject.supportedGameVersions + } +} diff --git a/mod/forge/gradle.properties b/mod/forge/gradle.properties new file mode 100644 index 0000000..8242585 --- /dev/null +++ b/mod/forge/gradle.properties @@ -0,0 +1 @@ +loom.platform=forge diff --git a/mod/forge/src/main/java/dev/spyc0der77/mcspacks/forge/McsPacksForge.java b/mod/forge/src/main/java/dev/spyc0der77/mcspacks/forge/McsPacksForge.java new file mode 100644 index 0000000..169c6eb --- /dev/null +++ b/mod/forge/src/main/java/dev/spyc0der77/mcspacks/forge/McsPacksForge.java @@ -0,0 +1,13 @@ +package dev.spyc0der77.mcspacks.forge; + +import dev.spyc0der77.mcspacks.McsPacks; +import net.minecraftforge.fml.common.Mod; + +@Mod(McsPacksForge.MOD_ID) +public final class McsPacksForge { + public static final String MOD_ID = "mcs_packs"; + + public McsPacksForge() { + McsPacks.init(); + } +} diff --git a/mod/forge/src/main/resources/META-INF/mods.toml b/mod/forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..85a6233 --- /dev/null +++ b/mod/forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,32 @@ +modLoader = "javafml" +loaderVersion = "[4,)" +license = "MIT" +issueTrackerURL = "https://github.com/SpyC0der77/Minecraft-Script/issues" + +[[mods]] +modId = "mcs_packs" +version = "${mod_version}" +displayName = "MCS Packs" +description = "Compile and hot-reload Minecraft Script datapacks from mcs_packs/" +authors = "SpyC0der77" + +[[dependencies.mcs_packs]] +modId = "forge" +mandatory = true +versionRange = "[${forge_version},${forge_version_upper})" +ordering = "NONE" +side = "BOTH" + +[[dependencies.mcs_packs]] +modId = "minecraft" +mandatory = true +versionRange = "[${minecraft_version},${minecraft_version_upper})" +ordering = "NONE" +side = "BOTH" + +[[dependencies.mcs_packs]] +modId = "architectury" +mandatory = true +versionRange = "[${architectury_api_version},${architectury_api_version_upper})" +ordering = "AFTER" +side = "BOTH" diff --git a/mod/forge/src/main/resources/mcs_packs.mixins.json b/mod/forge/src/main/resources/mcs_packs.mixins.json new file mode 100644 index 0000000..1edac23 --- /dev/null +++ b/mod/forge/src/main/resources/mcs_packs.mixins.json @@ -0,0 +1,9 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.spyc0der77.mcspacks.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "client": [], + "server": [] +} diff --git a/mod/gradle.properties b/mod/gradle.properties new file mode 100644 index 0000000..c072a9e --- /dev/null +++ b/mod/gradle.properties @@ -0,0 +1,25 @@ +org.gradle.jvmargs=-Xmx4G +org.gradle.parallel=true +org.gradle.daemon=false + +mcs_profile=1.21.2 +minecraft_version=1.21.2 +mcs_minecraft_profile=1.21.2 +supported_game_versions=1.21.2 + +architectury_plugin_version=3.4-SNAPSHOT +architectury_loom_version=1.13-SNAPSHOT +loom.ignoreDependencyLoomVersionValidation=true + +mod_version=0.1.0 +maven_group=dev.spyc0der77 +archives_base_name=mcs-packs +mod_id=mcs_packs + +fabric_loader_version=0.16.9 +fabric_api_version=0.106.0+1.21.2 +forge_version=52.0.28 +neoforge_version=21.2.0-beta +architectury_api_version=13.0.8 + +enabled_platforms=fabric,forge,neoforge 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..cae6a80 --- /dev/null +++ b/mod/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip +distributionSha256Sum=6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854 +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mod/gradlew b/mod/gradlew new file mode 100755 index 0000000..e016b08 --- /dev/null +++ b/mod/gradlew @@ -0,0 +1,251 @@ +#!/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\n' "$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/neoforge/build.gradle b/mod/neoforge/build.gradle new file mode 100644 index 0000000..4db757b --- /dev/null +++ b/mod/neoforge/build.gradle @@ -0,0 +1,96 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +loom { + neoForge { + version = rootProject.neoforge_version + } +} + +architectury { + platformSetupLoomIde() + neoForge() +} + +configurations { + common + shadowCommon + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentNeoForge.extendsFrom common +} + +dependencies { + neoForge "net.neoforged:neoforge:${rootProject.neoforge_version}" + modImplementation "dev.architectury:architectury-neoforge:${rootProject.architectury_api_version}" + + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowCommon(project(path: ':common', configuration: 'transformProductionNeoForge')) { transitive false } +} + +jar { + archiveClassifier = 'dev' +} + +shadowJar { + configurations = [project.configurations.shadowCommon] + archiveClassifier = 'dev-shadow' +} + +remapJar { + input.set shadowJar.archiveFile + dependsOn shadowJar + archiveClassifier = null +} + +def stripPreReleaseSuffix = { String version -> + version.replaceFirst(/-(?:beta|alpha|rc\d*|snapshot|pre).*$/, '') +} + +def warnVersionFallback = { String name, String version, String reason, String safeUpper -> + logger.warn("Using fallback upper bound for ${name} '${version}': ${reason}. Using '${safeUpper}'") + safeUpper +} + +def nextMinecraftVersion = { + def version = stripPreReleaseSuffix(rootProject.minecraft_version) + def parts = version.tokenize('.') + if (parts.size() == 2 && parts[0].isInteger() && parts[1].isInteger() && parts[0].toInteger() >= 26) { + return "${parts[0]}.${parts[1].toInteger() + 1}" + } + if (parts.size() == 3 && parts.every { it.isInteger() }) { + return "${parts[0]}.${parts[1]}.${parts[2].toInteger() + 1}" + } + warnVersionFallback('minecraft_version', rootProject.minecraft_version, "unrecognized format after normalization '${version}'", '99.99.99') +}() + +def nextMajorVersion = { String version, String suffix = '', String name = 'version', String safeUpper = '99.0.0' -> + def normalized = stripPreReleaseSuffix(version) + def major = normalized.tokenize('.')[0] + if (major.isInteger()) { + return "${major.toInteger() + 1}${suffix}" + } + warnVersionFallback(name, version, "unrecognized major component '${major}'", safeUpper) +} + +def nextNeoForgeVersion = nextMajorVersion(rootProject.neoforge_version, '.0.0-beta', 'neoforge_version', '99.0.0-beta') +def nextArchitecturyVersion = nextMajorVersion(rootProject.architectury_api_version, '.0.0', 'architectury_api_version', '99.0.0') + +processResources { + inputs.property 'mod_version', project.version + inputs.property 'minecraft_version', rootProject.minecraft_version + inputs.property 'supported_game_versions', rootProject.supportedGameVersions + inputs.property 'neoforge_version', rootProject.neoforge_version + inputs.property 'architectury_api_version', rootProject.architectury_api_version + filesMatching('META-INF/neoforge.mods.toml') { + expand 'mod_version': project.version, + 'minecraft_version': rootProject.minecraft_version, + 'neoforge_version': rootProject.neoforge_version, + 'minecraft_version_upper': nextMinecraftVersion, + 'neoforge_version_upper': nextNeoForgeVersion, + 'architectury_api_version': rootProject.architectury_api_version, + 'architectury_api_version_upper': nextArchitecturyVersion, + 'supported_game_versions': rootProject.supportedGameVersions + } +} diff --git a/mod/neoforge/gradle.properties b/mod/neoforge/gradle.properties new file mode 100644 index 0000000..7da18ea --- /dev/null +++ b/mod/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge diff --git a/mod/neoforge/src/main/java/dev/spyc0der77/mcspacks/neoforge/McsPacksNeoForge.java b/mod/neoforge/src/main/java/dev/spyc0der77/mcspacks/neoforge/McsPacksNeoForge.java new file mode 100644 index 0000000..9e2ab25 --- /dev/null +++ b/mod/neoforge/src/main/java/dev/spyc0der77/mcspacks/neoforge/McsPacksNeoForge.java @@ -0,0 +1,13 @@ +package dev.spyc0der77.mcspacks.neoforge; + +import dev.spyc0der77.mcspacks.McsPacks; +import net.neoforged.fml.common.Mod; + +@Mod(McsPacksNeoForge.MOD_ID) +public final class McsPacksNeoForge { + public static final String MOD_ID = "mcs_packs"; + + public McsPacksNeoForge() { + McsPacks.init(); + } +} diff --git a/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..8415203 --- /dev/null +++ b/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,32 @@ +modLoader = "javafml" +loaderVersion = "[1,)" +license = "MIT" +issueTrackerURL = "https://github.com/SpyC0der77/Minecraft-Script/issues" + +[[mods]] +modId = "mcs_packs" +version = "${mod_version}" +displayName = "MCS Packs" +description = "Compile and hot-reload Minecraft Script datapacks from mcs_packs/" +authors = "SpyC0der77" + +[[dependencies.mcs_packs]] +modId = "neoforge" +type = "required" +versionRange = "[${neoforge_version},${neoforge_version_upper})" +ordering = "NONE" +side = "BOTH" + +[[dependencies.mcs_packs]] +modId = "minecraft" +type = "required" +versionRange = "[${minecraft_version},${minecraft_version_upper})" +ordering = "NONE" +side = "BOTH" + +[[dependencies.mcs_packs]] +modId = "architectury" +type = "required" +versionRange = "[${architectury_api_version},${architectury_api_version_upper})" +ordering = "AFTER" +side = "BOTH" diff --git a/mod/scripts/apply_version.py b/mod/scripts/apply_version.py new file mode 100644 index 0000000..e1c7b02 --- /dev/null +++ b/mod/scripts/apply_version.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Write mod/gradle.properties for a selected MCS profile.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +MOD_ROOT = Path(__file__).resolve().parents[1] +MANIFEST = MOD_ROOT / "versions" / "manifest.json" +GRADLE_PROPERTIES = MOD_ROOT / "gradle.properties" +DEFAULT_ENABLED_PLATFORMS = "fabric,forge,neoforge" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("profile", help="MCS profile key from versions/manifest.json") + parser.add_argument( + "--loader", + choices=["fabric", "forge", "neoforge"], + help="Enable only this loader platform in gradle.properties (default: all loaders)", + ) + args = parser.parse_args() + + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + profile = manifest["profiles"].get(args.profile) + if profile is None: + known = ", ".join(manifest["profiles"].keys()) + raise SystemExit(f"Unknown profile {args.profile!r}. Known: {known}") + + lines = [ + "org.gradle.jvmargs=-Xmx4G", + "org.gradle.parallel=true", + "org.gradle.daemon=false", + "", + f"mcs_profile={args.profile}", + f"minecraft_version={profile['minecraft_version']}", + f"mcs_minecraft_profile={profile['mcs_profile']}", + f"supported_game_versions={','.join(profile['supported_game_versions'])}", + "", + "architectury_plugin_version=3.4-SNAPSHOT", + "architectury_loom_version=1.13-SNAPSHOT", + "loom.ignoreDependencyLoomVersionValidation=true", + "", + "mod_version=0.1.0", + "maven_group=dev.spyc0der77", + "archives_base_name=mcs-packs", + "mod_id=mcs_packs", + "", + f"fabric_loader_version={profile['fabric_loader_version']}", + f"fabric_api_version={profile['fabric_api_version']}", + f"forge_version={profile['forge_version']}", + f"neoforge_version={profile['neoforge_version']}", + f"architectury_api_version={profile['architectury_api_version']}", + "", + f"enabled_platforms={args.loader if args.loader else DEFAULT_ENABLED_PLATFORMS}", + ] + GRADLE_PROPERTIES.write_text("\n".join(lines) + "\n", encoding="utf-8") + print(f"Wrote {GRADLE_PROPERTIES} for profile {args.profile}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/mod/scripts/build_all.py b/mod/scripts/build_all.py new file mode 100644 index 0000000..ee2bef2 --- /dev/null +++ b/mod/scripts/build_all.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Build MCS Packs mod artifacts for every MCS profile and loader.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +MOD_ROOT = Path(__file__).resolve().parents[1] +MANIFEST = MOD_ROOT / "versions" / "manifest.json" +GRADLEW = MOD_ROOT / ("gradlew.bat" if sys.platform.startswith("win") else "gradlew") + + +def load_manifest() -> dict: + return json.loads(MANIFEST.read_text(encoding="utf-8")) + + +def run_build(profile: str, loader: str, skip_tests: bool) -> int: + apply = [ + sys.executable, + str(MOD_ROOT / "scripts" / "apply_version.py"), + profile, + "--loader", + loader, + ] + print(f"\n==> {' '.join(apply)}", flush=True) + apply_code = subprocess.run(apply, cwd=MOD_ROOT, check=False).returncode + if apply_code != 0: + return apply_code + + command = [ + str(GRADLEW), + f":{loader}:build", + f"-Pmcs_profile={profile}", + f"-Penabled_platforms={loader}", + ] + if skip_tests: + command.append("-x") + command.append("test") + print(f"\n==> {' '.join(command)}", flush=True) + return subprocess.run(command, cwd=MOD_ROOT, check=False).returncode + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--profile", help="Build only this MCS profile key (e.g. 1.21.11)") + parser.add_argument("--loader", choices=["fabric", "forge", "neoforge"], help="Build only this loader") + parser.add_argument("--skip-tests", action="store_true") + args = parser.parse_args() + + if not GRADLEW.is_file(): + print(f"Gradle wrapper not found at {GRADLEW}. Run setup first.", file=sys.stderr) + return 1 + + manifest = load_manifest() + profiles = [args.profile] if args.profile else list(manifest["profiles"].keys()) + loaders = [args.loader] if args.loader else manifest["loaders"] + + failures: list[str] = [] + for profile in profiles: + if profile not in manifest["profiles"]: + failures.append(f"unknown profile {profile!r}") + continue + for loader in loaders: + code = run_build(profile, loader, args.skip_tests) + if code != 0: + failures.append(f"{profile}:{loader} (exit {code})") + + if failures: + print("\nBuild failures:", file=sys.stderr) + for item in failures: + print(f" - {item}", file=sys.stderr) + return 1 + + print("\nAll requested builds completed successfully.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/mod/scripts/bun.lock b/mod/scripts/bun.lock new file mode 100644 index 0000000..4677c73 --- /dev/null +++ b/mod/scripts/bun.lock @@ -0,0 +1,260 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "mcs-mod-scripts", + "dependencies": { + "@spyglassmc/core": "^0.4.47", + "@spyglassmc/java-edition": "^0.3.60", + "@spyglassmc/mcdoc": "^0.3.51", + }, + "devDependencies": { + "esbuild": "^0.25.5", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@spyglassmc/core": ["@spyglassmc/core@0.4.47", "", { "dependencies": { "@spyglassmc/locales": "0.3.24", "base64-arraybuffer": "^1.0.2", "binary-search": "^1.3.6", "decompress": "^4.2.1", "follow-redirects": "^1.14.8", "picomatch": "^4.0.2", "rfdc": "^1.3.0", "vscode-languageserver-textdocument": "^1.0.4", "whatwg-url": "^14.0.0" } }, "sha512-xC75t4+yDZNiofCgRt/up8XuVb6qo03NoEziT32Y05+TbXae0fD5N/8KBZmcLfGnFZQaYB5pYPHkULxMKiV3sg=="], + + "@spyglassmc/java-edition": ["@spyglassmc/java-edition@0.3.60", "", { "dependencies": { "@spyglassmc/core": "0.4.47", "@spyglassmc/json": "0.3.51", "@spyglassmc/locales": "0.3.24", "@spyglassmc/mcdoc": "0.3.51", "@spyglassmc/mcfunction": "0.2.50", "@spyglassmc/nbt": "0.3.53" } }, "sha512-XUF9kddzOh2lSn5J+s1cTZfMUnY0bDtbk2dZEOnfuN2Jx/apGIiqgxGA/tv8zcs34vomSk/OPHW4rK7S4buvTA=="], + + "@spyglassmc/json": ["@spyglassmc/json@0.3.51", "", { "dependencies": { "@spyglassmc/core": "0.4.47", "@spyglassmc/locales": "0.3.24", "@spyglassmc/mcdoc": "0.3.51" } }, "sha512-iAvc5VnCcnwSRfF4Nr+aYzPdT2mcSNBjpNNP4O9pycIsOrlbBObOB77A55gi1Axm/e28HpKvge8kjWrn3NDCfg=="], + + "@spyglassmc/locales": ["@spyglassmc/locales@0.3.24", "", {}, "sha512-L3Y2p+zS++pyyDaGGGmBFBTPncjNFERbJ/QtLCBrNQqJyZYFLauEkOUvsWrOky6U7V7ObDF0lnzxoPiVQPUjSw=="], + + "@spyglassmc/mcdoc": ["@spyglassmc/mcdoc@0.3.51", "", { "dependencies": { "@spyglassmc/core": "0.4.47", "@spyglassmc/locales": "0.3.24" } }, "sha512-J4KXGbs2B7bnsMqbCdFkEj7ZYv+txSwuZsYhC5iwczIU6qyWs7K3nvAURml1j16K0EFJrMY/OVoj7i7IO5UxWA=="], + + "@spyglassmc/mcfunction": ["@spyglassmc/mcfunction@0.2.50", "", { "dependencies": { "@spyglassmc/core": "0.4.47", "@spyglassmc/locales": "0.3.24" } }, "sha512-C/wmEO08AGlLnOZG3ai7d2GnqEOJFav9aZVL4WDVE+/jJGH5tN1oErSl8LSgOz4x4uUzU+tTOnK0dVjeg4EiOw=="], + + "@spyglassmc/nbt": ["@spyglassmc/nbt@0.3.53", "", { "dependencies": { "@spyglassmc/core": "0.4.47", "@spyglassmc/locales": "0.3.24", "@spyglassmc/mcdoc": "0.3.51" } }, "sha512-7ybjL7vCCmkYcwQHKZkjjOkKDue/pgWCrzNPVzZ18ZlDQN7SYBOxI8N6MlbqW5bultG8OHFl49tQJ1TFUW4KKw=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "binary-search": ["binary-search@1.3.6", "", {}, "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA=="], + + "bl": ["bl@1.2.3", "", { "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="], + + "buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="], + + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "decompress": ["decompress@4.2.1", "", { "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", "decompress-targz": "^4.0.0", "decompress-unzip": "^4.0.1", "graceful-fs": "^4.1.10", "make-dir": "^1.0.0", "pify": "^2.3.0", "strip-dirs": "^2.0.0" } }, "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ=="], + + "decompress-tar": ["decompress-tar@4.1.1", "", { "dependencies": { "file-type": "^5.2.0", "is-stream": "^1.1.0", "tar-stream": "^1.5.2" } }, "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ=="], + + "decompress-tarbz2": ["decompress-tarbz2@4.1.1", "", { "dependencies": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", "is-stream": "^1.1.0", "seek-bzip": "^1.0.5", "unbzip2-stream": "^1.0.9" } }, "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A=="], + + "decompress-targz": ["decompress-targz@4.1.1", "", { "dependencies": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", "is-stream": "^1.1.0" } }, "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w=="], + + "decompress-unzip": ["decompress-unzip@4.0.1", "", { "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", "pify": "^2.3.0", "yauzl": "^2.4.2" } }, "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "file-type": ["file-type@5.2.0", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="], + + "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@2.3.1", "", { "dependencies": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" } }, "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-natural-number": ["is-natural-number@4.0.1", "", {}, "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ=="], + + "is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pinkie": ["pinkie@2.0.4", "", {}, "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg=="], + + "pinkie-promise": ["pinkie-promise@2.0.1", "", { "dependencies": { "pinkie": "^2.0.0" } }, "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "seek-bzip": ["seek-bzip@1.0.6", "", { "dependencies": { "commander": "^2.8.1" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "strip-dirs": ["strip-dirs@2.1.0", "", { "dependencies": { "is-natural-number": "^4.0.1" } }, "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g=="], + + "tar-stream": ["tar-stream@1.6.2", "", { "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", "end-of-stream": "^1.0.0", "fs-constants": "^1.0.0", "readable-stream": "^2.3.0", "to-buffer": "^1.1.1", "xtend": "^4.0.0" } }, "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "decompress-tarbz2/file-type": ["file-type@6.2.0", "", {}, "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg=="], + + "decompress-unzip/file-type": ["file-type@3.9.0", "", {}, "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA=="], + + "make-dir/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], + + "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + } +} diff --git a/mod/scripts/mcs-spyglass-validate.mjs b/mod/scripts/mcs-spyglass-validate.mjs new file mode 100644 index 0000000..a99a2a4 --- /dev/null +++ b/mod/scripts/mcs-spyglass-validate.mjs @@ -0,0 +1,208 @@ +import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, relative, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { + ErrorSeverity, + FileNode, + Logger, + Project, + VanillaConfig, +} from '@spyglassmc/core' +import { getNodeJsExternals } from '@spyglassmc/core/lib/nodejs.js' +import { initialize as initializeJavaEdition } from '@spyglassmc/java-edition' +import { initialize as initializeMcdoc } from '@spyglassmc/mcdoc' + +function parseArgs(argv) { + const flags = { + json: false, + mcVersion: null, + } + const positional = [] + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] + if (arg === '--json') { + flags.json = true + continue + } + if (arg === '--mc-version') { + const value = argv[index + 1] + if (!value || value.startsWith('-')) { + throw new Error('Missing value for --mc-version') + } + flags.mcVersion = value + index += 1 + continue + } + if (arg.startsWith('--mc-version=')) { + const value = arg.slice('--mc-version='.length) + if (!value) { + throw new Error('Missing value for --mc-version=') + } + flags.mcVersion = value + continue + } + positional.push(arg) + } + + if (positional.length < 1) { + throw new Error('Usage: node mcs-spyglass-validate.js [--json] [--mc-version ] ') + } + + return { flags, datapackDir: resolve(positional[0]) } +} + +function toRootUri(path) { + const normalized = resolve(path).replace(/\\/g, '/') + return pathToFileURL(`${normalized}/`).href +} + +function severityName(severity) { + switch (severity) { + case ErrorSeverity.Hint: + return 'hint' + case ErrorSeverity.Information: + return 'information' + case ErrorSeverity.Warning: + return 'warning' + case ErrorSeverity.Error: + return 'error' + default: + return 'error' + } +} + +function walkFiles(rootDir) { + const files = [] + const stack = [rootDir] + while (stack.length > 0) { + const current = stack.pop() + for (const entry of readdirSync(current)) { + const fullPath = join(current, entry) + const stats = statSync(fullPath) + if (stats.isDirectory()) { + stack.push(fullPath) + continue + } + files.push(fullPath) + } + } + return files +} + +function languageIdFor(path) { + if (path.endsWith('.mcfunction')) return 'mcfunction' + if (path.endsWith('.json')) return 'json' + if (path.endsWith('.nbt')) return 'nbt' + return null +} + +async function main() { + const { flags, datapackDir } = parseArgs(process.argv.slice(2)) + const packMetaPath = join(datapackDir, 'pack.mcmeta') + if (!statSync(datapackDir).isDirectory() || !statSync(packMetaPath).isFile()) { + throw new Error(`Expected a datapack directory containing pack.mcmeta at ${datapackDir}`) + } + + const cacheDir = mkdtempSync(join(tmpdir(), 'mcs-spyglass-')) + const cacheRoot = toRootUri(cacheDir) + mkdirSync(new URL(cacheRoot), { recursive: true }) + const projectRoot = toRootUri(datapackDir) + const gameVersion = flags.mcVersion + const diagnostics = [] + + try { + const project = new Project({ + cacheRoot, + defaultConfig: { + ...VanillaConfig, + env: { + ...VanillaConfig.env, + ...(gameVersion ? { gameVersion } : {}), + }, + }, + externals: getNodeJsExternals({ cacheRoot }), + initializers: [initializeMcdoc, initializeJavaEdition], + isDebugging: false, + logger: Logger.create('warn'), + projectRoots: [projectRoot], + }) + + project.on('documentErrored', ({ uri, errors }) => { + for (const error of errors) { + const filePath = decodeURIComponent(new URL(uri).pathname.replace(/^\/([A-Za-z]:)/, '$1')) + const relativePath = relative(datapackDir, filePath).replace(/\\/g, '/') + const start = error.posRange?.start + diagnostics.push({ + file: relativePath, + line: (start?.line ?? 0) + 1, + column: (start?.character ?? 0) + 1, + message: error.message, + severity: severityName(error.severity), + }) + } + }) + + await project.init() + await project.ready() + + for (const filePath of walkFiles(datapackDir)) { + const languageId = languageIdFor(filePath) + if (!languageId) continue + const uri = pathToFileURL(filePath).href + const content = readFileSync(filePath, 'utf8') + await project.onDidOpen(uri, languageId, 1, content) + const managed = await project.ensureClientManagedChecked(uri) + if (!managed) continue + for (const error of FileNode.getErrors(managed.node)) { + diagnostics.push({ + file: relative(datapackDir, filePath).replace(/\\/g, '/'), + line: error.range.start.line + 1, + column: error.range.start.character + 1, + message: error.message, + severity: severityName(error.severity), + }) + } + } + + await project.close() + } finally { + rmSync(cacheDir, { recursive: true, force: true }) + } + + const unique = new Map() + for (const diagnostic of diagnostics) { + const key = `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}:${diagnostic.severity || diagnostic.level}:${diagnostic.message}` + unique.set(key, diagnostic) + } + const result = [...unique.values()].sort((left, right) => { + if (left.file !== right.file) return left.file.localeCompare(right.file) + if (left.line !== right.line) return left.line - right.line + return left.column - right.column + }) + + if (flags.json) { + process.stdout.write(`${JSON.stringify(result)}\n`) + } else { + for (const diagnostic of result) { + process.stdout.write( + `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}: ${diagnostic.message}\n`, + ) + } + } + + const hasErrors = result.some((diagnostic) => diagnostic.severity === 'error') + process.exitCode = hasErrors ? 1 : 0 +} + +main().catch((error) => { + const message = error instanceof Error ? error.stack ?? error.message : String(error) + if (process.argv.includes('--json')) { + process.stdout.write(`${JSON.stringify([{ file: '', line: 0, column: 0, message, severity: 'error' }])}\n`) + } else { + process.stderr.write(`${message}\n`) + } + process.exitCode = 1 +}) diff --git a/mod/scripts/package.json b/mod/scripts/package.json new file mode 100644 index 0000000..8cbb58c --- /dev/null +++ b/mod/scripts/package.json @@ -0,0 +1,16 @@ +{ + "name": "mcs-mod-scripts", + "private": true, + "type": "module", + "scripts": { + "bundle:spyglass": "esbuild mcs-spyglass-validate.mjs --bundle --platform=node --format=cjs --packages=bundle --outfile=../common/build/generated/resources/main/scripts/mcs-spyglass-validate.js --banner:js=\"#!/usr/bin/env node\"" + }, + "dependencies": { + "@spyglassmc/core": "^0.4.47", + "@spyglassmc/java-edition": "^0.3.60", + "@spyglassmc/mcdoc": "^0.3.51" + }, + "devDependencies": { + "esbuild": "^0.25.5" + } +} diff --git a/mod/settings.gradle b/mod/settings.gradle new file mode 100644 index 0000000..5625720 --- /dev/null +++ b/mod/settings.gradle @@ -0,0 +1,37 @@ +pluginManagement { + repositories { + maven { url = 'https://maven.fabricmc.net/' } + maven { url = 'https://maven.architectury.dev/' } + maven { url = 'https://maven.minecraftforge.net/' } + gradlePluginPortal() + } +} + +rootProject.name = 'mcs-packs' + +def properties = new Properties() +def propertiesFile = file('gradle.properties') +if (propertiesFile.exists()) { + propertiesFile.withInputStream { properties.load(it) } +} + +def enabledPlatformsProperty = gradle.startParameter.projectProperties.get('enabled_platforms') +if (enabledPlatformsProperty == null) { + enabledPlatformsProperty = properties.getProperty('enabled_platforms', 'fabric,forge,neoforge') +} + +def enabledPlatforms = (enabledPlatformsProperty ?: 'fabric,forge,neoforge') + .split(',') + .collect { it.trim() } + .findAll { !it.isEmpty() } + +include 'common' +if (enabledPlatforms.contains('fabric')) { + include 'fabric' +} +if (enabledPlatforms.contains('forge')) { + include 'forge' +} +if (enabledPlatforms.contains('neoforge')) { + include 'neoforge' +} diff --git a/mod/versions/manifest.json b/mod/versions/manifest.json new file mode 100644 index 0000000..24f142f --- /dev/null +++ b/mod/versions/manifest.json @@ -0,0 +1,85 @@ +{ + "loaders": ["fabric", "forge", "neoforge"], + "profiles": { + "1.21.2": { + "minecraft_version": "1.21.2", + "mcs_profile": "1.21.2", + "supported_game_versions": ["1.21.2"], + "fabric_loader_version": "0.16.9", + "fabric_api_version": "0.106.0+1.21.2", + "forge_version": "52.0.28", + "neoforge_version": "21.2.0-beta", + "architectury_api_version": "13.0.8" + }, + "1.21.4": { + "minecraft_version": "1.21.4", + "mcs_profile": "1.21.4", + "supported_game_versions": ["1.21.4"], + "fabric_loader_version": "0.16.9", + "fabric_api_version": "0.112.2+1.21.4", + "forge_version": "54.0.10", + "neoforge_version": "21.4.33-beta", + "architectury_api_version": "13.0.8" + }, + "1.21.5": { + "minecraft_version": "1.21.5", + "mcs_profile": "1.21.5", + "supported_game_versions": ["1.21.5"], + "fabric_loader_version": "0.16.12", + "fabric_api_version": "0.119.9+1.21.5", + "forge_version": "55.0.4", + "neoforge_version": "21.5.28-beta", + "architectury_api_version": "14.0.3" + }, + "1.21.6": { + "minecraft_version": "1.21.6", + "mcs_profile": "1.21.6", + "supported_game_versions": ["1.21.6"], + "fabric_loader_version": "0.16.14", + "fabric_api_version": "0.128.1+1.21.6", + "forge_version": "56.0.0", + "neoforge_version": "21.6.4-beta", + "architectury_api_version": "14.0.3" + }, + "1.21.7-8": { + "minecraft_version": "1.21.8", + "mcs_profile": "1.21.7-8", + "supported_game_versions": ["1.21.7", "1.21.8"], + "fabric_loader_version": "0.17.2", + "fabric_api_version": "0.131.0+1.21.8", + "forge_version": "58.1.0", + "neoforge_version": "21.8.4-beta", + "architectury_api_version": "15.0.3" + }, + "1.21.9-10": { + "minecraft_version": "1.21.10", + "mcs_profile": "1.21.9-10", + "supported_game_versions": ["1.21.9", "1.21.10"], + "fabric_loader_version": "0.17.2", + "fabric_api_version": "0.136.0+1.21.10", + "forge_version": "60.1.0", + "neoforge_version": "21.10.4-beta", + "architectury_api_version": "16.0.3" + }, + "1.21.11": { + "minecraft_version": "1.21.11", + "mcs_profile": "1.21.11", + "supported_game_versions": ["1.21.11"], + "fabric_loader_version": "0.18.4", + "fabric_api_version": "0.140.2+1.21.11", + "forge_version": "61.1.0", + "neoforge_version": "21.11.42", + "architectury_api_version": "19.0.1" + }, + "26.1": { + "minecraft_version": "26.1", + "mcs_profile": "26.1", + "supported_game_versions": ["26.1"], + "fabric_loader_version": "0.19.3", + "fabric_api_version": "0.145.0+26.1", + "forge_version": "64.0.0", + "neoforge_version": "26.1.4-beta", + "architectury_api_version": "20.0.1" + } + } +} diff --git a/tests/test_compile_cli_flags.py b/tests/test_compile_cli_flags.py new file mode 100644 index 0000000..a5cf630 --- /dev/null +++ b/tests/test_compile_cli_flags.py @@ -0,0 +1,97 @@ +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +from minecraft_script.common import COMMON_CONFIG, module_folder + + +EXAMPLE = Path(module_folder).parent / "examples" / "starter_datapack.mcs" + + +@pytest.fixture +def build_output(tmp_path): + output = tmp_path / "out" + output.mkdir() + return output + + +def test_compile_force_rebuilds_existing_output(build_output): + first = subprocess.run( + [ + sys.executable, + "-m", + "minecraft_script", + "compile", + "--force", + "--mc-version", + "1.21.11", + "--no-verbose", + str(EXAMPLE), + "Starter Pack", + str(build_output), + ], + capture_output=True, + text=True, + check=False, + ) + assert first.returncode == 0, first.stderr + + marker = build_output / "Starter Pack" / "pack.mcmeta" + assert marker.is_file() + marker.write_text('{"pack":{"pack_format":0,"description":"stale"}}', encoding="utf-8") + + second = subprocess.run( + [ + sys.executable, + "-m", + "minecraft_script", + "compile", + "--force", + "--mc-version", + "1.21.11", + "--no-verbose", + str(EXAMPLE), + "Starter Pack", + str(build_output), + ], + capture_output=True, + text=True, + check=False, + ) + assert second.returncode == 0, second.stderr + assert "stale" not in marker.read_text(encoding="utf-8") + + +def test_compile_mc_version_does_not_mutate_config(build_output): + original_version = COMMON_CONFIG["minecraft_version"] + try: + result = subprocess.run( + [ + sys.executable, + "-m", + "minecraft_script", + "compile", + "--force", + "--mc-version", + "1.21.11", + "--no-verbose", + str(EXAMPLE), + "Starter Pack", + str(build_output), + ], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert COMMON_CONFIG["minecraft_version"] == original_version + with open(f"{module_folder}/config.json", encoding="utf-8") as config_file: + persisted_config = json.loads(config_file.read()) + assert persisted_config["minecraft_version"] == original_version + finally: + if build_output.exists(): + shutil.rmtree(build_output, ignore_errors=True) diff --git a/todo.md b/todo.md index 16b809c..0a698fd 100644 --- a/todo.md +++ b/todo.md @@ -1 +1,356 @@ -# MCS Todo list +# MCS Hot-Reload Mod — Implementation Todo + +A cross-loader Minecraft mod (Fabric, Forge, NeoForge) that adds an `mcs_packs` folder to the game directory, compiles `.mcs` files on save, and hot-reloads the resulting datapacks into the running world. + +**Target Minecraft versions** (from `minecraft_script/versions/index.json`): + +| MCS profile | Minecraft versions | +| ------------- | ------------------------- | +| `1.21.2` | 1.21.2 | +| `1.21.4` | 1.21.4 | +| `1.21.5` | 1.21.5 | +| `1.21.6` | 1.21.6 | +| `1.21.7-8` | 1.21.7, 1.21.8 | +| `1.21.9-10` | 1.21.9, 1.21.10 | +| `1.21.11` | 1.21.11 | +| `26.1` | 26.1 | + +--- + +## Core loop + +1. Watch `mcs_packs//` for `.mcs` file changes (create / modify / delete). +2. On update → **lint first**, then compile only if lint passes: + ```bash + mcs lint --json + ``` + Surface diagnostics in chat (`[MCS] my_pack/pack.mcs:12:3 — expected '}'`). On lint **errors**, skip compile and keep the last good datapack. +3. On lint OK → delete `mcs_packs/_compiled//` if it exists, then run MCS compile: + ```bash + mcs compile + ``` + Fallback if `mcs` is not on PATH: + ```bash + python -m minecraft_script compile + ``` +4. On compile success → run **Spyglass** on the compiled datapack folder: + ```bash + node mcs-spyglass-validate.mjs --json --mc-version + ``` + Spyglass validates generated `.mcfunction` files, tags, JSON, and commands (uses `pack.mcmeta` + game version). Surface diagnostics in chat (`[Spyglass] my_pack/data/.../main.mcfunction:3:1 — …`). +5. On Spyglass **errors** → do **not** hot-reload; keep the last good injected datapack. Compiled output stays in `_compiled/` for fixing. +6. On Spyglass OK → inject / refresh the datapack and hot-reload into the running world. +7. On compile failure → log the error, keep the last good datapack loaded. + +Pipeline: `mcs lint` → `mcs compile` → **Spyglass on output** → reload. + +--- + +## Phase 0 — Research & Decisions + +- [x] **Choose multi-loader scaffolding** — **Architectury** + - Use [Architectury](https://architectury.dev/) with a shared `common` module and thin Fabric / Forge / NeoForge entrypoints. + - Gradle layout: `mod/common`, `mod/fabric`, `mod/forge`, `mod/neoforge` (monorepo subfolder). + - Build tooling: Architectury Plugin + Architectury Loom; platform-specific hooks live in loader modules, shared logic in `common`. + +- [x] **Compiler integration** — subprocess `mcs compile` + - MCS is Python; the mod shells out to the existing CLI on every file update. + - Primary: `mcs compile ` (npm global install). + - Fallback: `python -m minecraft_script compile ` (pip / editable install). + - Set `minecraft_version` to match the running game before compile (env var or temp config — see Phase 3). + - Delete `_compiled//` before each compile (`Compiler.build()` fails if the output folder already exists). + +- [x] **Lint & validate pipeline** + - **Before compile:** `mcs lint --json` on changed `.mcs` files (syntax + imports). Errors block compile; show line/column in chat. + - **After compile:** Spyglass validates the full compiled datapack in `_compiled//` (mcfunctions, tags, JSON). Errors block hot-reload; show file/line in chat. + +- [x] **`mcs_packs` layout and output flow** + - Each pack is a **folder** under `mcs_packs/`. Mod config lives in the normal **`config/`** folder (not inside `mcs_packs/`). + ``` + .minecraft/ + config/ + mcs-packs.json # mod config (compiler path, debounce, auto-reload, etc.) + mcs_packs/ + my_pack/ # one folder per datapack project + pack.mcs # entry point (default name; override in pack.json) + helpers.mcs # optional additional sources / imports + pack.json # optional per-pack overrides (display name, entry file) + starter/ + pack.mcs + _compiled/ # mod-managed build output (gitignored by users) + my_pack/ # compiled datapack folder + starter/ + ``` + - On save: any `.mcs` change inside `mcs_packs/my_pack/` recompiles that pack → `_compiled/my_pack/`. + - Compile working directory: the pack folder (so relative imports between files in the same pack resolve). + - On reload: inject `_compiled/*` into the active world's datapack list (not copy into `saves//datapacks/` — avoids stale duplicates and `/reload` disable issues). + +- [ ] **Define hot-reload semantics** + - **Lint / compile failure:** keep previous good datapack loaded; surface error in chat + log. + - **Compile success + Spyglass errors:** compiled files remain on disk but are **not** injected/reloaded until Spyglass passes. + - **Compile success + Spyglass OK:** replace datapack source in the mod's `PackRepository` entry, then trigger a datapack reload. + - **Reload scope:** prefer targeted datapack reload over full `/reload` (preserves entities, reduces lag). Fall back to `/reload` if no stable internal API exists for a given loader version. + - **Dedicated server:** only reload when source files change on disk (no "save" event from an editor on the server machine unless files are synced). + +- [ ] **Map loader + MC version matrix** + - 8 MCS profiles × 3 loaders = up to 24 published artifacts per release. + - Use a version table in Gradle (similar to `minecraft_script/versions/index.json`) as the single source of truth for mod build targets. + - Confirm minimum loader versions per MC release (Fabric Loader, Forge, NeoForge). + +--- + +## Phase 1 — Project Scaffolding + +- [ ] Create `mod/` Gradle multi-project: + ``` + mod/ + build.gradle + settings.gradle + gradle.properties + common/ # loader-agnostic logic + fabric/ # Fabric entrypoint + platform hooks + forge/ # Forge entrypoint + platform hooks + neoforge/ # NeoForge entrypoint + platform hooks + ``` +- [ ] Configure Architectury Loom (or loader-specific Loom forks) for all three platforms. +- [ ] Add shared dependencies: SLF4J, Gson (config), optional Cloth Config for in-game settings UI. +- [ ] Wire CI job in `.github/workflows/` to build all matrix targets (can start with one MC version, expand later). +- [ ] Add `mod/README.md` with install instructions and `mcs_packs` usage (keep this todo as the engineering checklist). + +--- + +## Phase 2 — `mcs_packs` Folder Lifecycle + +- [ ] **Create folders on game launch** + - Resolve game directory via loader API (`FabricLoader.getGameDir()`, Forge `FMLPaths.GAMEDIR`, etc.). + - Create `mcs_packs/`, `mcs_packs/_compiled/`, and a starter pack folder `mcs_packs/starter/` with `pack.mcs` (from `examples/starter_datapack.mcs`) plus a short `mcs_packs/README.txt`. + - Create default mod config in `config/` if missing (use loader config dir: `FabricLoader.getConfigDir()`, Forge `FMLPaths.CONFIGDIR`). + +- [ ] **Load mod config from `config/mcs-packs.json`** (with defaults): + ```json + { + "compiler": "auto", + "compilerPath": "", + "debounceMs": 500, + "autoReload": true, + "verboseCompile": false, + "minecraftVersion": "auto", + "lintBeforeCompile": true, + "spyglassValidateAfterCompile": true, + "blockCompileOnLintErrors": true, + "blockReloadOnSpyglassErrors": true + } + ``` + - Lives in `.minecraft/config/` like every other mod — **not** inside `mcs_packs/`. + - `compiler: "auto"` — try `mcs` on PATH, then `python -m minecraft_script`. + - `minecraftVersion: "auto"` — derive from running game version, mapped through MCS profile aliases (`1.21.7` → `1.21.7-8` per `versions/index.json`). + +- [ ] **Per-pack overrides** (optional `mcs_packs//pack.json`): + - Custom datapack display name, namespace override, `entryFile` if not default `pack.mcs`. + +- [ ] **Discover packs** + - Scan `mcs_packs/*/` subdirectories on startup (exclude `_compiled`). + - Each subdirectory with a valid entry `.mcs` is one pack. + - Build a `PackRegistry` map: `packFolder → entryMcs → compiledPath → packId`. + +--- + +## Phase 3 — MCS Compiler Bridge + +- [ ] **Implement `McsCompiler` service (common module)** + - One method: `compile(Path mcsFile) → CompileResult`. + - Runs `mcs compile` (or fallback) as a subprocess; capture stdout/stderr. + - Working directory: the pack folder, e.g. `mcs_packs/my_pack/` (relative imports within a pack must resolve). + - Before compile: delete `_compiled//`. + - After compile: return success/failure + output path. + +- [ ] **Version mapping helper** + - Ship `mcs-versions.json` (from `minecraft_script/versions/index.json`) in the mod JAR. + - Map running game version → MCS profile (`1.21.8` → `1.21.7-8`). + - Pass to compiler via env `MCS_MINECRAFT_VERSION` or temp config file. + +- [ ] **Detect compiler on first run** + - Probe: user `compilerPath` → `mcs` on PATH → `python -m minecraft_script`. + - Error clearly if none found (link to MCS install docs). + +- [ ] **Surface compile diagnostics** + - Log subprocess output; show compile errors in chat + `logs/mcs-mod/latest.log`. + +--- + +## Phase 3b — Lint & Spyglass Validate + +- [ ] **Run `mcs lint --json` before each compile** (when `lintBeforeCompile` is true) + - Lint the changed file; for entry-point compiles, also lint the entry `.mcs` after any sibling file in the pack changes. + - Parse JSON diagnostics into `{ file, line, column, message, severity }`. + - Chat format: `[MCS] my_pack/helpers.mcs:14:1 — Could not resolve import 'foo.mcs'`. + - If `blockCompileOnLintErrors` and any error-severity diagnostic → skip compile, keep last good datapack. + +- [ ] **Run Spyglass on compiled output after each successful compile** (when `spyglassValidateAfterCompile` is true) + - Validate the datapack root: `mcs_packs/_compiled//` (contains `pack.mcmeta`, `data/`, generated mcfunctions). + - Build a headless CLI using `@spyglassmc/core` (same stack as `spyglass/` and `highlighter/`). Open the compiled folder as a Spyglass project; collect diagnostics on all files. + - Spawn via Node: `node mcs-spyglass-validate.mjs --json --mc-version `. + - Pass running game version (or read from `pack.mcmeta` + `mcs-versions.json` mapping). Optionally drop `spyglass.json` into `_compiled//` with `env.gameVersion` if pack format alone is ambiguous. + - Chat format: `[Spyglass] my_pack/data//function/....mcfunction:12:1 — Expected …`. + - If `blockReloadOnSpyglassErrors` and any error-severity diagnostic → skip inject + reload; keep last good injected pack. + - Warnings only → still reload (configurable later if needed). + - Spyglass / Node unavailable → one-time chat warning; optionally allow reload without validation via config fallback. + +- [ ] **Add `mcs-spyglass-validate.mjs` to repo** (e.g. under `mod/scripts/` or `npm/scripts/`) + - JSON stdout: `[{ "file", "line", "column", "message", "severity" }]`. + - Reuse Spyglass project init patterns from `highlighter/command-spyglass.mjs` / `spyglass` language server. + +--- + +## Phase 4 — File Watcher → Compile on Update + +- [ ] **Watch each `mcs_packs//` folder for `.mcs` changes** + - Java `WatchService` on a background thread; ignore `mcs_packs/_compiled/` and editor temp files. + - A change to any `.mcs` in a pack folder triggers a recompile of that pack's entry file. + +- [ ] **On any `.mcs` create/modify in a pack folder → lint → compile → Spyglass validate → reload** + - Debounce per pack (`debounceMs` from `config/mcs-packs.json`, default 500 ms). + - Cancel in-flight lint/compile/validate if the same pack changes again (generation counter). + +- [ ] **On world load → compile all pack folders once** + - Same code path as the watcher; ensures packs exist before first inject. + +- [ ] **On pack folder delete (or entry `.mcs` removed) → remove `_compiled//` and unregister datapack** + +--- + +## Phase 5 — Datapack Injection & Hot Reload + +- [ ] **Register compiled packs as dynamic datapack sources** + - **Fabric:** `ResourceManagerHelper.registerBuiltinResourcePack(...)` or custom `PackResources` via `PackSource` / server pack repository hooks (verify against target Fabric API for 1.21.2+). + - **Forge:** `AddPackFindersEvent` — add `PathPackResources` pointing at `_compiled//`. + - **NeoForge:** equivalent pack finder event (API renamed from Forge — confirm per version). + - Assign a stable pack ID (e.g. `mcs_mod:`) and `PackSource.BUILT_IN` or a custom source so `/datapack list` shows them. + +- [ ] **Implement `DatapackReloader` platform service** + - `registerPack(Path compiledDir, String packName)` + - `unregisterPack(String packName)` + - `reloadDatapacks(MinecraftServer server)` — trigger reload on server thread. + +- [ ] **Reload strategy (prefer least disruptive)** + 1. Try internal server reload API (`ReloadableServerResources`, `MinecraftServer.reloadResources`) with only datapack portion. + 2. If unavailable or unstable on a loader version, run `/reload` via server command source with OP bypass. + 3. Document side effects (function tags re-run, entities preserved vs. not) per approach. + +- [ ] **Hook world lifecycle** + - `SERVER_STARTED` → initial scan, compile, register, inject. + - `SERVER_STOPPING` → stop watcher, clear injected packs. + - Client disconnect / world unload — no reload needed. + +- [ ] **Avoid conflict with MCS `kill.mcfunction`** + - MCS kill template runs `datapack disable "file/{{datapackName}}"`. Injected packs are not `file/` packs — verify kill.mcfunction does not break hot-reload packs (may need MCS template tweak or mod-specific kill override in a future MCS release). + +--- + +## Phase 6 — User Experience + +- [ ] **In-game feedback** + - Chat messages: `[MCS] Compiling my_pack…`, `[MCS] Reloaded 2 datapacks (1.2s)`, `[MCS] Compile failed: …`. + - Optional action bar progress during compile + reload. + +- [ ] **Keybind or command** (optional) + - `/mcs reload` — force recompile all + reload. + - `/mcs status` — show compiler path, watched files, last compile times. + - `/mcs open` — open `mcs_packs/` folder in OS file manager. + +- [ ] **Config screen** (Cloth Config / YACL) + - Edits `config/mcs-packs.json` — compiler path, debounce, auto-reload toggle, verbose compile. + +- [ ] **Log file** + - `logs/mcs-mod/latest.log` with compile output for debugging. + +--- + +## Phase 7 — MCS Repo Integration + +- [ ] **Ship `mcs-versions.json` in mod JAR** + - Generated at build time from `minecraft_script/versions/index.json` + profile `minecraft_version` fields. + - Keeps mod in sync with MCS supported versions without manual duplication. + +- [ ] **Add MCS CLI flags for mod use** (nice-to-have, not blocking v1) + - `mcs compile --mc-version 1.21.11 --force ` — avoids mutating global `config.json` and handles output-dir cleanup in one step. + - v1 workaround: mod deletes output dir itself before calling plain `mcs compile`. + +- [ ] **Document workflow in main `readme.md`** + - Section: "Live development with the MCS mod" — install mod, create a folder in `mcs_packs/` with a `pack.mcs`, edit & save, see changes in world. + +- [ ] **Update `contributors.md`** + - Mod build prerequisites (JDK 21, Gradle), run-client tasks per loader. + +--- + +## Phase 8 — Version Matrix & Releases + +- [ ] **Gradle version manifest** — one row per releasable artifact: + | MC | Fabric Loader | Forge | NeoForge | MCS profile | + | ---- | ------------- | ----- | -------- | ----------- | + | 1.21.2 | … | … | … | 1.21.2 | + | … | … | … | … | … | + | 26.1 | … | … | … | 26.1 | + +- [ ] **Build all targets** in CI; publish to Modrinth + CurseForge (optional). +- [ ] **Tag naming:** `mod-v0.1.0+1.21.11-fabric`, etc. +- [ ] **Compatibility policy:** mod major version tracks MCS major; declare minimum MCS / Python / Node version in mod metadata. + +--- + +## Phase 9 — Testing + +- [ ] **Manual test matrix** (per loader × at least 2 MC versions): + - Fresh install creates `mcs_packs/`, `mcs_packs/starter/`, and `config/mcs-packs.json`. + - Save `.mcs` inside a pack folder triggers lint → compile → Spyglass validate → reload; `load` function runs. + - Lint, compile, or Spyglass errors keep previous injected pack active; chat shows diagnostic with file + line. + - Spyglass catches bad commands in generated `.mcfunction` files that MCS lint missed. + - Delete pack folder removes compiled output and unregisters datapack after reload. + - Multi-pack: two pack folders compile and load independently. + - Import between `.mcs` files within the same pack folder resolves correctly. + +- [ ] **Automated tests (where feasible)** + - Unit tests for version mapping (`1.21.10` → `1.21.9-10`). + - Unit tests for debounce / watch event coalescing. + - Integration test: invoke real `mcs compile` subprocess against `examples/starter_datapack.mcs` into `build_test/` (per `AGENTS.md`). + - GameTest / gametest framework hooks for in-world verification (stretch goal). + +- [ ] **Performance checks** + - Compile + reload time for small vs. large packs. + - No watcher thread leak on world unload. + +--- + +## Phase 10 — Known Risks & Open Questions + +- [ ] **Python / Node dependency** — v1 requires MCS installed separately (`npm install -g minecraft-script` or `pip install -e .`) **and Node on PATH** for post-compile Spyglass validation. Mod shows a clear error if `mcs` / `python -m minecraft_script` / Node is missing. +- [ ] **Dedicated server** — no automatic "save" unless using a sync tool; document that saves on the admin machine trigger reload. +- [ ] **`/reload` cost** — large packs may cause noticeable lag; targeted reload API needs per-version verification. +- [ ] **Pack format drift** — MCS `pack.mcmeta` templates must match the running MC version (handled by MCS version profiles if compiler gets correct `minecraft_version`). +- [ ] **Scoreboard / storage state** — reload re-runs `load.mcfunction`; user scripts must be idempotent or guard with scoreboard markers. +- [ ] **Multiplayer** — only operators / server-side compile should trigger reload; clients should not spawn compilers. +- [ ] **Security** — subprocess executes user-provided `.mcs` which compiles to mcfunctions; treat `mcs_packs/` as trusted local dev content only. + +--- + +## Suggested Implementation Order + +1. ~~Phase 0: Architectury scaffolding, subprocess `mcs compile`, `mcs_packs/` layout.~~ +2. ~~Phase 1 — Architectury Gradle project (all MCS profiles × Fabric/Forge/NeoForge).~~ +3. Phase 2 + 3 + 3b + 4 — `mcs_packs/` folder, file watcher, `mcs lint` → `mcs compile` → Spyglass validate on update. +4. Phase 5 — inject compiled output + hot-reload. +5. Phase 6 — chat/log feedback on compile success or failure. +6. Phase 8 — expand to all MCS versions + Forge + NeoForge. +7. Phase 7 + 9 — MCS CLI flags (optional), tests, release. + +--- + +## Success Criteria + +- [ ] Player installs mod, launches game, sees `mcs_packs/` and `config/mcs-packs.json` in `.minecraft/`. +- [ ] Saving `mcs_packs/starter/pack.mcs` triggers lint → compile → Spyglass validate within ~1–2 s. +- [ ] Spyglass errors in compiled output are shown in chat and **do not** hot-reload a broken pack. +- [ ] World reflects datapack changes without manual `/reload` or moving folders. +- [ ] Works on Fabric, Forge, and NeoForge for every MCS-supported Minecraft version listed above. +- [ ] Clear error when MCS is not installed; no silent failures.