diff --git a/packages/mecha/examples/basic_snapshot/beet.yml b/packages/mecha/examples/basic_snapshot/beet.yml new file mode 100644 index 000000000..3b6f19e4c --- /dev/null +++ b/packages/mecha/examples/basic_snapshot/beet.yml @@ -0,0 +1,15 @@ + + + +data_pack: + load: . + +minecraft: "26.1-snapshot-10" + +require: + - beet.contrib.snapshot + +pipeline: + - mecha + +output: build \ No newline at end of file diff --git a/packages/mecha/examples/basic_snapshot/data/name/function/test.mcfunction b/packages/mecha/examples/basic_snapshot/data/name/function/test.mcfunction new file mode 100644 index 000000000..41a40781c --- /dev/null +++ b/packages/mecha/examples/basic_snapshot/data/name/function/test.mcfunction @@ -0,0 +1,2 @@ + +swing @s mainhand \ No newline at end of file diff --git a/packages/mecha/examples/basic_wildcard/beet.yml b/packages/mecha/examples/basic_wildcard/beet.yml new file mode 100644 index 000000000..176a6d567 --- /dev/null +++ b/packages/mecha/examples/basic_wildcard/beet.yml @@ -0,0 +1,13 @@ + + + +data_pack: + load: . + +minecraft: "1.21.x" + + +pipeline: + - mecha + +output: build \ No newline at end of file diff --git a/packages/mecha/examples/basic_wildcard/data/name/function/test.mcfunction b/packages/mecha/examples/basic_wildcard/data/name/function/test.mcfunction new file mode 100644 index 000000000..4980205d6 --- /dev/null +++ b/packages/mecha/examples/basic_wildcard/data/name/function/test.mcfunction @@ -0,0 +1,2 @@ + +say BASIC \ No newline at end of file diff --git a/packages/mecha/src/mecha/config.py b/packages/mecha/src/mecha/config.py index 78f5db4ff..4d2583f10 100644 --- a/packages/mecha/src/mecha/config.py +++ b/packages/mecha/src/mecha/config.py @@ -11,6 +11,7 @@ from beet import ErrorMessage from beet.core.utils import FileSystemPath, JsonDict, VersionNumber, split_version from pydantic import BaseModel +from beet.resources.pack_format_registry import search_version class CommandTree(BaseModel): @@ -38,16 +39,12 @@ def load_from( for filename in args: sources.append(Path(filename).read_text()) - version = split_version(version) if version is not None else None - if version and not patch_only: - version_name = "_".join(map(str, version)) try: - sources.append( - files("mecha.resources") - .joinpath(f"{version_name}.json") - .read_text() + filename = ( + f"{'_'.join(map(str, split_version(search_version(version))))}.json" ) + sources.append(files("mecha.resources").joinpath(filename).read_text()) except FileNotFoundError as exc: raise ErrorMessage(f"Invalid minecraft version {version!r}.") from exc diff --git a/packages/mecha/src/mecha/parse.py b/packages/mecha/src/mecha/parse.py index 13292fea2..c14feedbe 100644 --- a/packages/mecha/src/mecha/parse.py +++ b/packages/mecha/src/mecha/parse.py @@ -93,6 +93,8 @@ from nbtlib import Byte, Double, Float, Int, Long, OutOfRange, Short, String from tokenstream import InvalidSyntax, SourceLocation, TokenStream, set_location +from beet.resources.pack_format_registry import search_version + from .ast import ( AstAdvancementPredicate, AstBlock, @@ -583,7 +585,11 @@ def get_default_parsers() -> Dict[str, Parser]: def get_parsers(version: VersionNumber = LATEST_MINECRAFT_VERSION) -> Dict[str, Parser]: """Return parsers for a specific version.""" - version = split_version(version) + resolved = search_version(version) + try: + version = split_version(resolved) + except ValueError: + version = split_version(LATEST_MINECRAFT_VERSION) parsers = get_default_parsers() diff --git a/packages/mecha/tests/snapshots/examples__build_basic_snapshot__0.pack.md b/packages/mecha/tests/snapshots/examples__build_basic_snapshot__0.pack.md new file mode 100644 index 000000000..c3caec3df --- /dev/null +++ b/packages/mecha/tests/snapshots/examples__build_basic_snapshot__0.pack.md @@ -0,0 +1,49 @@ +# Lectern snapshot + +## Data pack + +`@data_pack pack.mcmeta` + +```json +{ + "pack": { + "min_format": [ + 99, + 3 + ], + "max_format": [ + 99, + 3 + ], + "description": "" + } +} +``` + +### name + +`@function name:test` + +```mcfunction +swing @s mainhand +``` + +## Resource pack + +`@resource_pack pack.mcmeta` + +```json +{ + "pack": { + "min_format": [ + 82, + 0 + ], + "max_format": [ + 82, + 0 + ], + "description": "" + } +} +``` diff --git a/packages/mecha/tests/snapshots/examples__build_basic_wildcard__0.pack.md b/packages/mecha/tests/snapshots/examples__build_basic_wildcard__0.pack.md new file mode 100644 index 000000000..b261908ae --- /dev/null +++ b/packages/mecha/tests/snapshots/examples__build_basic_wildcard__0.pack.md @@ -0,0 +1,29 @@ +# Lectern snapshot + +## Data pack + +`@data_pack pack.mcmeta` + +```json +{ + "pack": { + "min_format": [ + 94, + 1 + ], + "max_format": [ + 94, + 1 + ], + "description": "" + } +} +``` + +### name + +`@function name:test` + +```mcfunction +say BASIC +``` diff --git a/src/beet/contrib/snapshot.py b/src/beet/contrib/snapshot.py new file mode 100644 index 000000000..67b0ed2d9 --- /dev/null +++ b/src/beet/contrib/snapshot.py @@ -0,0 +1,41 @@ +""" +Plugin to load snapshot pack formats and command trees from misode/mcmeta (or custom). +""" + +from beet import Context, DataPack, ResourcePack +from beet.resources.pack_format_registry import ( + PackFormatRegistry, + all_versions, + pack_format_registry, +) +from beet.toolchain.context import PluginOptions +from beet import configurable + + +class SnapshotOptions(PluginOptions): + """Plugin options for the snapshot plugin.""" + + url_versions: str = "https://raw.githubusercontent.com/misode/mcmeta/refs/tags/{version}-summary/version.json" + """URL template to download the version manifest from. The {version} placeholder will be replaced with the Minecraft version.""" + + url_command_tree: str = "https://raw.githubusercontent.com/misode/mcmeta/refs/tags/{version}-summary/commands/data.json" + """URL template to download the command tree from. The {version} placeholder will be replaced with the Minecraft version.""" + + +@configurable("snapshot", validator=SnapshotOptions) +def beet_default(ctx: Context, opts: SnapshotOptions): + cache = ctx.cache["snapshot"] + path = cache.download(opts.url_versions.format(version=ctx.minecraft_version)) + + all_versions.append(ctx.minecraft_version) + + pack_format = PackFormatRegistry.model_validate_json(path.open("r").read()) + pack_format_registry.append(pack_format) + for pack in ctx.packs: + pack.pack_format_registry.add_format(pack_format) + pack.assign_format(ctx.minecraft_version) + DataPack.pack_format_registry.add_format(pack_format) + ResourcePack.pack_format_registry.add_format(pack_format) + + path = cache.download(opts.url_command_tree.format(version=ctx.minecraft_version)) + ctx.meta.setdefault("mecha", {}).setdefault("commands", []).insert(0, path) diff --git a/src/beet/library/base.py b/src/beet/library/base.py index 12060e462..0f4a50aab 100644 --- a/src/beet/library/base.py +++ b/src/beet/library/base.py @@ -1240,22 +1240,27 @@ def copy(self, *, shallow: bool = False) -> Self: return pack_copy - def assign_format(self): + def assign_format(self, minecraft_version: str | None = None): if ( self.pack_format is None and self.min_format is None and self.max_format is None - ): - if isinstance(self.latest_pack_format, int): - if self.latest_pack_format < self.pack_format_switch_format: - self.pack_format = self.latest_pack_format + ) or minecraft_version: + format = ( + self.pack_format_registry.get(minecraft_version) + if minecraft_version + else self.latest_pack_format + ) + if isinstance(format, int): + if format < self.pack_format_switch_format: + self.pack_format = format self.min_format = None self.max_format = None else: self.pack_format = None - self.min_format = self.max_format = self.latest_pack_format + self.min_format = self.max_format = format else: - self.min_format = self.max_format = self.latest_pack_format + self.min_format = self.max_format = format def clear(self): self.extra.clear() diff --git a/src/beet/resources/pack_format_registry.py b/src/beet/resources/pack_format_registry.py index d816d24a4..7b13174c2 100644 --- a/src/beet/resources/pack_format_registry.py +++ b/src/beet/resources/pack_format_registry.py @@ -1,7 +1,7 @@ """ Pack format registry resource from https://raw.githubusercontent.com/misode/mcmeta/refs/heads/summary/versions/data.json -see file://./../../scripts/update_pack_format_registry.py +see file://./../../../scripts/update_pack_format_registry.py """ @@ -10,13 +10,15 @@ "pack_format_registry_path", "PackFormatRegistryContainer", "PackFormatRegistry", + "search_version", ] from importlib.resources import files import json from typing import Literal + from beet.toolchain.config import FormatSpecifier from pydantic import BaseModel -from beet.core.utils import normalize_string, VersionNumber +from beet.core.utils import VersionNumber from beet.core.container import Container @@ -40,21 +42,57 @@ class PackFormatRegistry(BaseModel): data = json.loads(pack_format_registry_path.read_text()) pack_format_registry: list[PackFormatRegistry] = [] +all_versions: list[str] = [] for item in data: - pack_format_registry.append(PackFormatRegistry.model_validate(item)) + pack_format = PackFormatRegistry.model_validate(item) + all_versions.append(pack_format.id) + pack_format_registry.append(pack_format) + + +def search_version(version: VersionNumber) -> str: + """ + This function search a version specifier and return a version + """ + if isinstance(version, tuple): + version = ".".join(str(x) for x in version) + if isinstance(version, str) and version in all_versions: + return version + if isinstance(version, str) and version.endswith(".x"): + major, minor, _ = version.split(".", 2) + max_patch = 0 + for search in all_versions: + search_splitted = search.split(".") + if search_splitted[0] != major or search_splitted[1] != minor: + continue + if len(search_splitted) == 3: + search_patch = int(search_splitted[2]) + if search_patch > max_patch: + max_patch = search_patch + + if max_patch == 0: + return f"{major}.{minor}" + else: + return f"{major}.{minor}.{max_patch}" + raise KeyError(version) class PackFormatRegistryContainer(Container[VersionNumber, FormatSpecifier]): """Container for pack format registry data.""" + pack_format_switch_format: int + pack_type: Literal["data_pack", "resource_pack"] + def __init__( self, pack_format_switch_format: int, pack_type: Literal["data_pack", "resource_pack"], ): + self.pack_format_switch_format = pack_format_switch_format + self.pack_type = pack_type super().__init__() + default_data: dict[VersionNumber, FormatSpecifier] if pack_type == "resource_pack": - data: dict[VersionNumber, FormatSpecifier] = { + default_data = { (1, 6): 1, (1, 7): 1, (1, 8): 1, @@ -63,65 +101,54 @@ def __init__( (1, 11): 3, (1, 12): 3, (1, 13): 4, - **{ - x.id: ( - x.resource_pack_version - if x.resource_pack_version < pack_format_switch_format - else (x.resource_pack_version, x.resource_pack_version_minor) - ) - for x in pack_format_registry - if x.type == "release" - }, } elif pack_type == "data_pack": - data: dict[VersionNumber, FormatSpecifier] = { + default_data = { (1, 13): 4, - **{ - x.id: ( - x.data_pack_version - if x.data_pack_version < pack_format_switch_format - else (x.data_pack_version, x.data_pack_version_minor) - ) - for x in pack_format_registry - if x.type == "release" - }, } else: raise ValueError( f'Illegal "{pack_type}", should be "data_pack" or "resource_pack"' ) - for key, value in data.items(): + for key, value in default_data.items(): self[key] = value - def normalize_key(self, key: VersionNumber) -> tuple[str | int, ...]: - """Normalize the key to a tuple of integers.""" - if isinstance(key, (int, float)): - key = str(key) + for version in pack_format_registry: + self.add_format(version) + + def add_format(self, version: PackFormatRegistry): + if self.pack_type == "data_pack": + self[version.id] = ( + version.data_pack_version + if version.data_pack_version < self.pack_format_switch_format + else (version.data_pack_version, version.data_pack_version_minor) + ) + elif self.pack_type == "resource_pack": + self[version.id] = ( + version.resource_pack_version + if version.resource_pack_version < self.pack_format_switch_format + else ( + version.resource_pack_version, + version.resource_pack_version_minor, + ) + ) + + def normalize_key(self, key: VersionNumber) -> str: + """Normalize the key to a string.""" if isinstance(key, str): - key = tuple(normalize_string(key).split("_")) - return tuple(int(value) if value != "x" else "x" for value in key) + return key + if isinstance(key, (int, float)): + return str(key) + if isinstance(key, tuple): + return ".".join(str(x) for x in key) + raise NotImplementedError() def missing(self, key: VersionNumber) -> FormatSpecifier: """ Implement the missing method to return a default value. """ - if not isinstance(key, tuple): - key = self.normalize_key(key) - if not isinstance(key[-1], str): - raise KeyError(key) - if key[-1] != "x": - raise KeyError(f'Version must end with "x", got {key}') - max_patch = 0 - for version in self.keys(): - normalized_version = self.normalize_key(version) - if key[0] != normalized_version[0] or key[1] != normalized_version[1]: - continue - if len(normalized_version) == 2: - # The maximum is 0 - continue - patch = normalized_version[2] - if isinstance(patch, int) and patch > max_patch: - max_patch = patch - if max_patch == 0: - return self[key[0], key[1]] - return self[key[0], key[1], max_patch] + version = search_version(key) + res = self.get(version) + if res: + return res + raise KeyError(key) diff --git a/tests/test_pack_registry_container_type.py b/tests/test_pack_registry_container_type.py index 6713ea5dd..5c7dff5cf 100644 --- a/tests/test_pack_registry_container_type.py +++ b/tests/test_pack_registry_container_type.py @@ -8,9 +8,10 @@ def test_registry_data(): - assert DataPack.pack_format_registry.normalize_key("1.21") == (1, 21) - assert DataPack.pack_format_registry.normalize_key("1.21.1") == (1, 21, 1) - assert DataPack.pack_format_registry.normalize_key("1.21.x") == (1, 21, "x") + assert DataPack.pack_format_registry.normalize_key(1.21) == "1.21" + assert DataPack.pack_format_registry.normalize_key("1.21.1") == "1.21.1" + assert DataPack.pack_format_registry.normalize_key("1.21.x") == "1.21.x" + assert DataPack.pack_format_registry.normalize_key(("1", "21", "x")) == "1.21.x" assert DataPack.pack_format_registry.missing((1, 20, "x")) == 41 assert DataPack.pack_format_registry.missing((1, 19, "x")) == 12