Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.cmd text eol=crlf
*.bat text eol=crlf
39 changes: 39 additions & 0 deletions .github/workflows/mod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Mod

on:
push:
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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}]
4 changes: 3 additions & 1 deletion minecraft_script/compiler/compile_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions minecraft_script/config_utils.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
116 changes: 99 additions & 17 deletions minecraft_script/shell_commands.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -31,10 +82,10 @@ def handle_arguments(arguments: list):

- debug <path>: debug the minecraft script file found at the given path.

- compile <path> [<datapack name>] [<output path>]: 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 <version>] [--force] [--verbose|--no-verbose] <path>
[<datapack name>] [<output path>]: 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 <path> [--json]: validate MCS syntax and imports for the given file.
Use --stdin to read code from standard input and pass --source <path> so
Expand Down Expand Up @@ -79,42 +130,73 @@ 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():
print(f"Error: Output path is not a directory ({str(output_path) !r})")
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)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

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:
Expand Down
65 changes: 65 additions & 0 deletions mod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# MCS Packs Mod

Architectury mod for Fabric, Forge, and NeoForge. Watches `.minecraft/mcs_packs/<pack>/`, 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.
Loading
Loading