diff --git a/changes/23.breaking.md b/changes/23.breaking.md new file mode 100644 index 0000000..d7f35fb --- /dev/null +++ b/changes/23.breaking.md @@ -0,0 +1 @@ +The `load_common_data` and `load_version` functions now returns custom pydantic objects instead of `dict`. This means support for `__getitem__` based accessing of the data (e.g. `load_common_data()["legacy"]["blocks"]`) will no longer work, in favor of attribute-based accessing (e.g. `load_common_data().legacy.blocks`). Additionally, to make sure we follow python's best practices, any camelCase attributes were converted into snake_case. diff --git a/minebase/__init__.py b/minebase/__init__.py index 9c7e26e..c1026e7 100644 --- a/minebase/__init__.py +++ b/minebase/__init__.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import json from enum import Enum from pathlib import Path -from typing import Any, cast +from typing import Any, Literal, cast, overload +from minebase.types._base import MinecraftValidationContext +from minebase.types.common_data import CommonData from minebase.types.data_paths import DataPaths +from minebase.types.mcdata import BedrockMinecraftData, PcMinecraftData DATA_SUBMODULE_PATH = Path(__file__).parent / "data" DATA_PATH = DATA_SUBMODULE_PATH / "data" @@ -35,13 +40,15 @@ def _load_data_paths() -> DataPaths: raise ValueError(f"minecraft-data submodule didn't contain data paths manifest (missing {file})") with file.open("rb") as fp: - return cast("DataPaths", json.load(fp)) + data = cast("DataPaths", json.load(fp)) + + return DataPaths.model_validate(data) -def _load_version_manifest(version: str, edition: Edition = Edition.PC) -> "dict[str, str]": +def _load_version_manifest(version: str, edition: Edition = Edition.PC) -> dict[str, str]: """Load the data paths manifest for given version (if it exists).""" manifest = _load_data_paths() - edition_info = manifest[edition.value] + edition_info = manifest.pc if edition is Edition.PC else manifest.bedrock try: return edition_info[version] except KeyError as exc: @@ -50,12 +57,44 @@ def _load_version_manifest(version: str, edition: Edition = Edition.PC) -> "dict def supported_versions(edition: Edition = Edition.PC) -> list[str]: """Get a list of all supported minecraft versions.""" + # We prefer versions from common data, as they're in a list, guaranteed to be + # ordered as they were released + data = load_common_data(edition) + versions = data.versions + + # This is just for a sanity check manifest = _load_data_paths() - edition_info = manifest[edition.value] - return list(edition_info.keys()) + edition_info = getattr(manifest, edition.value) + manifest_versions = set(edition_info.keys()) + + # These versions are present in the manifest, but aren't in the common data versions. + # I have no idea why, they're perfectly loadable. We can't just naively insert them + # as we want the versions list to be ordered. For now, as a hack, we remove these to + # pass the check below, trying to load these would work, but they won't be listed as + # supported from this function. + # https://github.com/PrismarineJS/minecraft-data/issues/1064 + manifest_versions.remove("1.16.5") + manifest_versions.remove("1.21") + manifest_versions.remove("1.21.6") + + if set(versions) != set(manifest_versions) or len(versions) != len(manifest_versions): + raise ValueError( + f"Data integrity error: common versions don't match manifest versions: " + f"{versions=} != {manifest_versions=}", + ) + + return versions -def load_version(version: str, edition: Edition = Edition.PC) -> dict[str, Any]: +@overload +def load_version(version: str, edition: Literal[Edition.PC] = Edition.PC) -> PcMinecraftData: ... + + +@overload +def load_version(version: str, edition: Literal[Edition.BEDROCK]) -> BedrockMinecraftData: ... + + +def load_version(version: str, edition: Edition = Edition.PC) -> PcMinecraftData | BedrockMinecraftData: """Load minecraft-data for given `version` and `edition`.""" _validate_data() version_data = _load_version_manifest(version, edition) @@ -76,10 +115,15 @@ def load_version(version: str, edition: Edition = Edition.PC) -> dict[str, Any]: with file.open("rb") as fp: data[field] = json.load(fp) - return data + validation_context = MinecraftValidationContext(version=version, edition=edition, versions=supported_versions()) + + if edition is Edition.PC: + return PcMinecraftData.model_validate(data, context=validation_context) + + return BedrockMinecraftData.model_validate(data, context=validation_context) -def load_common_data(edition: Edition = Edition.PC) -> dict[str, Any]: +def load_common_data(edition: Edition = Edition.PC) -> CommonData: """Load the common data from minecraft-data for given `edition`.""" _validate_data() common_dir = DATA_PATH / edition.value / "common" @@ -94,4 +138,4 @@ def load_common_data(edition: Edition = Edition.PC) -> dict[str, Any]: with file.open("rb") as fp: data[file.stem] = json.load(fp) - return data + return CommonData.model_validate(data) diff --git a/minebase/types/_base.py b/minebase/types/_base.py new file mode 100644 index 0000000..8753dc1 --- /dev/null +++ b/minebase/types/_base.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypedDict + +from pydantic import BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + +if TYPE_CHECKING: + from minebase import Edition + +__all__ = ["MinecraftDataModel", "_merge_base_config"] + + +class MinecraftValidationContext(TypedDict): + """Context information used during pydantic validation.""" + + edition: Edition + version: str + versions: list[str] + + +class MinecraftDataModel(BaseModel): + """Base type for a pydantic based class holding Minecraft-Data. + + This type is reserved for internal use, and it is not a guaranteed base class + for all minecraft-data models. It is a helper class that includes pre-configured + model config for automatic field conversion from camelCase to snakeCase and to + prevent unexpected extra attributes or class population without using the camelCase + aliases. + """ + + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=False, # only allow population by alias names + from_attributes=True, + extra="forbid", + ) + + +def _merge_base_config(conf: ConfigDict) -> ConfigDict: + """A function to override specific keys in the pydantic config of the `MinecraftDataModel`.""" + new = MinecraftDataModel.model_config.copy() + new.update(conf) + return new diff --git a/minebase/types/attributes.py b/minebase/types/attributes.py new file mode 100644 index 0000000..1a7a7bd --- /dev/null +++ b/minebase/types/attributes.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import final + +from pydantic import model_validator + +from minebase.types._base import MinecraftDataModel + + +@final +class MinecraftAttributeData(MinecraftDataModel): + """Minecraft-Data for an attribute. + + Attributes: + name: The name of this attribute + resource: The Mojang name of an attribute (usually generic.[name] or minecraft:generic.[name] + min: The minimum value of an attribute + max: The maximum value of an attribute + default: The default value of an attribute + """ + + name: str + resource: str + default: float + min: float + max: float + + @model_validator(mode="after") + def valid_default(self) -> MinecraftAttributeData: + """Enforce that the default value is within the expected min-max bounds.""" + if self.min <= self.default <= self.max: + return self + + raise ValueError("The default value is outside of the min-max bounds") diff --git a/minebase/types/biomes.py b/minebase/types/biomes.py new file mode 100644 index 0000000..a88cdf2 --- /dev/null +++ b/minebase/types/biomes.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Literal, final + +from pydantic import Field, model_validator + +from minebase.types._base import MinecraftDataModel + + +@final +class BiomeClimateData(MinecraftDataModel): + """Minecraft-Data about the climate in a Biome. + + This controls the ideal parameter ranges for Minecraft's multi-noise biome generation system. + + Attributes: + temperature: Controls hot/cold climate preference (-1.0 to 1.0) + humidity: Controls dry/wet climate preference (-1.0 to 1.0) + altitude: Controls low/high terrain preference (affects hills/valleys) + weirdness: Controls terrain "strangeness" (also known as "ridges", -1.0 to 1.0) + offset: Fine-tuning parameter for biome selection priority/weight + """ + + temperature: float = Field(ge=-1, le=1) + humidity: float = Field(ge=-1, le=1) + altitude: Literal[0] # not sure what the constraints here should be, minecraft-data only uses 0 + weirdness: float = Field(ge=-1, le=1) + offset: float + + +@final +class BiomeData(MinecraftDataModel): + """Minecraft-Data about a Biome. + + Attributes: + id: The unique identifier for a biome + name: The name of a biome + category: Category to which this biome belongs to (e.g. "forest", "ocean", ...) + temperature: The base temperature in a biome. + precipitation: The type of precipitation (none, rain or snow) [before 1.19.4] + has_precipitation: True if a biome has any precipitation (rain or snow) [1.19.4+] + dimension: The dimension of a biome: overworld, nether or end (or the_end on bedrock) + display_name: The display name of a biome + color: The color in a biome + rainfall: How much rain there is in a biome [before 1.19.4] + depth: Depth corresponds approximately to the terrain height. + climates: Climate data for the biome + name_legacy: Legacy name of the biome used in older versions. + parent: The name of the parent biome + child: ID of a variant biome + """ + + id: int + name: str + category: str + temperature: float = Field(ge=-1, le=2) + precipitation: Literal["none", "rain", "snow"] | None = None + # For some reason, this field actually uses snake_case, not camelCase + has_precipitation: bool | None = Field(alias="has_precipitation", default=None) + dimension: Literal["overworld", "nether", "end", "the_end"] + display_name: str + color: int + rainfall: float | None = Field(ge=0, le=1, default=None) + depth: float | None = Field(default=None) + climates: list[BiomeClimateData] | None = Field(min_length=1, default=None) + name_legacy: str | None = Field(alias="name_legacy", default=None) # also uses snake_case for some reason + parent: str | None = None + child: int | None = Field(ge=0, default=None) + + @model_validator(mode="before") + @classmethod + def rename_has_percipitation(cls, data: dict[str, object]) -> dict[str, object]: + """Rename the typo field has_percipitation to has_precipitation. + + This is a mistake in the minecraft-data dataset which is only present for a single + minecraft version (bedrock 1.21.60), this function renames it back to standardize + our data models. + + This will get addressed with: https://github.com/PrismarineJS/minecraft-data/issues/1048 + after which this method can be removed. + """ + if "has_percipitation" not in data: + return data + + if "has_precipitation" in data: + raise ValueError("Found biome with both has_percipitation and has_precipitation fields") + + data["has_precipitation"] = data.pop("has_percipitation") + return data diff --git a/minebase/types/block_collision_shapes.py b/minebase/types/block_collision_shapes.py new file mode 100644 index 0000000..81a4160 --- /dev/null +++ b/minebase/types/block_collision_shapes.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import final + +from pydantic import Field, field_validator, model_validator + +from minebase.types._base import MinecraftDataModel + + +@final +class CollisionBoxData(MinecraftDataModel): + """Minecraft-Data for a collision shape. + + Represents the (x0, y0, z0, x1, y1, z1) points of an axis aligned bounding box (AABB). + """ + + min_x: float = Field(ge=-0.25, le=1.5) + min_y: float = Field(ge=-0.25, le=1.5) + min_z: float = Field(ge=-0.25, le=1.5) + max_x: float = Field(ge=-0.25, le=1.5) + max_y: float = Field(ge=-0.25, le=1.5) + max_z: float = Field(ge=-0.25, le=1.5) + + @model_validator(mode="before") + @classmethod + def from_sequence(cls, value: object) -> dict[str, object]: + """Ensure the value is a 6 element tuple, then convert it into a dict for pydantic.""" + if not isinstance(value, Sequence): + raise TypeError(f"CollisionBoxData must be initialized from a sequence, got {type(value).__qualname__}") + + if len(value) != 6: + raise ValueError("CollisionBoxData must have exactly 6 elements") + + # Use camelCase here since MinecraftDataModel expects it + return { + "minX": value[0], + "minY": value[1], + "minZ": value[2], + "maxX": value[3], + "maxY": value[4], + "maxZ": value[5], + } + + @model_validator(mode="after") + def validate_min_less_than_max(self) -> CollisionBoxData: + """Validate that the min coordinate is always smaller (or equal) than the corresponding max coordinate.""" + try: + if self.min_x > self.max_x: + raise ValueError(f"min_x ({self.min_x}) must be less than max_x ({self.max_x})") # noqa: TRY301 + if self.min_y > self.max_y: + raise ValueError(f"min_y ({self.min_y}) must be less than max_y ({self.max_y})") # noqa: TRY301 + if self.min_z > self.max_z: + raise ValueError(f"min_z ({self.min_z}) must be less than max_z ({self.max_z})") # noqa: TRY301 + except ValueError: + # This is stupid, I'm aware, the above is essentially dead code. + # The reason for this is that some bedrock editions don't seem to meet this check. + # This seems like a problem with minecraft-data. See: + # https://github.com/PrismarineJS/minecraft-data/issues/1054 + return self + + return self + + @property + def as_aabb(self) -> tuple[float, float, float, float, float, float]: + """Get the data as (x0, y0, z0, x1, y1, z1), representing the points of an axis aligned bounding box (AABB).""" + return (self.min_x, self.min_y, self.min_z, self.max_x, self.max_y, self.max_z) + + +@final +class BlockCollisionShapeData(MinecraftDataModel): + """Minecraft-Data for a block collision model. + + blocks: + Mapping of block name -> collision shape ID(s). + + The value can either be a single number: collision ID shared by all block states of this block, + or a list of numbers: Shape IDs of each block state of this block. + + shapes: Collision shapes by ID, each shape being composed of a list of collision boxes. + """ + + blocks: dict[str, int | list[int]] + shapes: dict[int, list[CollisionBoxData]] + + @field_validator("blocks") + @classmethod + def validate_block_ids(cls, v: dict[str, int | list[int]]) -> dict[str, int | list[int]]: + """Ensure all specified shape IDs for the blocks are non-negative.""" + for key, val in v.items(): + if isinstance(val, int): + if val < 0: + raise ValueError(f"Got an unexpected collision ID value for block {key!r} (must be at least 0)") + continue + + for idx, collision_id in enumerate(val): + if collision_id < 0: + raise ValueError( + f"Got an unexpected collision ID value for block {key!r}. ID #{idx} must be at least 0", + ) + + return v + + @field_validator("shapes") + @classmethod + def validate_shape_ids(cls, v: dict[int, list[CollisionBoxData]]) -> dict[int, list[CollisionBoxData]]: + """Ensure all shape IDs are non-negative.""" + for key in v: + if key < 0: + raise ValueError(f"Collision IDs can't be negative (but found: {key})") + return v + + @model_validator(mode="after") + def validate_shape_references(self) -> BlockCollisionShapeData: + """Validate that all collision IDs specified for blocks have corresponding shape(s).""" + for block_name, shape_ids in self.blocks.items(): + if isinstance(shape_ids, int): + shape_ids = [shape_ids] # noqa: PLW2901 + + for shape_id in shape_ids: + if shape_id not in self.shapes: + raise ValueError( + f"Block {block_name!r} has a collision shape ID {shape_id}, without corresponding shape data", + ) + + return self + + def shape_for(self, block_name: str) -> list[CollisionBoxData] | list[list[CollisionBoxData]]: + """Get the collision shape for given block name. + + This is a convenience helper-function to skip having to look up the collision shape ID(s) + for the block and then having to look up the corresponding collision shape(s) based on that. + + Return: + Either a: + + - List of collision shape IDs (for all block states) + - List of lists of collision shape IDs (representing different collision shapes for different block states) + """ + shape_ids = self.blocks[block_name] + if isinstance(shape_ids, int): + return self.shapes[shape_ids] + + return [self.shapes[shape_id] for shape_id in shape_ids] diff --git a/minebase/types/block_loot.py b/minebase/types/block_loot.py new file mode 100644 index 0000000..7476d4c --- /dev/null +++ b/minebase/types/block_loot.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class BlockItemDrop(MinecraftDataModel): + """Minecraft-Data for an entity item drop information. + + Attributes: + item: The name of the item being dropped (guaranteed unique) + metadata: The metadata of the item being dropped (Bedrock Edition) + drop_chance: The percent chance of the item drop to occur + stack_size_range: The min/max number of items in this item drop stack + block_age: The required age of the block for the item drop to occur + silk_touch: If silk touch is required + no_silk_touch: If not having silk touch is required + """ + + item: str + metadata: int | None = Field(ge=0, le=127, default=None) + drop_chance: float + stack_size_range: tuple[int | None, int | None] + block_age: float | None = None + silk_touch: bool | None = None + no_silk_touch: bool | None = None + + +@final +class BlockLootData(MinecraftDataModel): + """Minecraft-Data for block loot information. + + Attributes: + block: The name of the block (guaranteed unique) + states: The states of the block (Bedrock Edition) + drops: The list of item drops + """ + + block: str + states: object | None = None + drops: list[BlockItemDrop] diff --git a/minebase/types/block_mappings.py b/minebase/types/block_mappings.py new file mode 100644 index 0000000..aa53539 --- /dev/null +++ b/minebase/types/block_mappings.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Any, final + +from minebase.types._base import MinecraftDataModel + + +@final +class BlockMappingEditionData(MinecraftDataModel): + """Minecraft-Data for a block mapping in a specific Minecraft edition (bedrock/java).""" + + name: str + # States can hold nested dicts, or various key-value pairs, where the value type + # differs wildly (for some keys, it's a bool, for others, it's an int, etc.) + # It's not feasible for us to create a model with all possible states, as there's + # just way too many, so this uses the permissive `Any` type for the dict values. + states: dict[str, Any] + + +@final +class BlockMappingData(MinecraftDataModel): + """Minecraft-Data showing how Bedrock edition blocks map to corresponding Java edition blocks. + + Attributes: + pc: Java edition block data + pe: Bedrock edition block data + """ + + pc: BlockMappingEditionData + pe: BlockMappingEditionData diff --git a/minebase/types/block_states.py b/minebase/types/block_states.py new file mode 100644 index 0000000..f3f3c9f --- /dev/null +++ b/minebase/types/block_states.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import Annotated, Literal, Union, final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class BedrockBlockStateStringData(MinecraftDataModel): + """Minecraft-Data for a Bedrock edition block state inner data for a string.""" + + type: Literal["string"] + value: str + + +@final +class BedrockBlockStateBytesData(MinecraftDataModel): + """Minecraft-Data for a Bedrock edition block state inner data for a byte (unsigned).""" + + type: Literal["byte"] + value: int = Field(ge=0, le=255) + + +@final +class BedrockBlockStateIntData(MinecraftDataModel): + """Minecraft-Data for a Bedrock edition block state inner data for an integer (signed, 4 byte number).""" + + type: Literal["int"] + value: int = Field(ge=-(2**31), le=2**31 - 1) + + +BedrockBlockStateInnerData = Annotated[ + Union[BedrockBlockStateIntData, BedrockBlockStateBytesData, BedrockBlockStateStringData], + Field(discriminator="type"), +] + + +@final +class BedrockBlockStateData(MinecraftDataModel): + """Minecraft-Data for block states (Bedrock only). + + In Java edition, block states are tracked under the blocks key, not as a separate + key. + + Note: + These data doesn't have a JSON schema to follow for the structure, so the + structure here is mostly just designed to match the underlying data. The + schema is expected to be added later on to minecraft-data; See: + """ + + name: str + states: dict[str, BedrockBlockStateInnerData] + version: int | None = None diff --git a/minebase/types/blocks.py b/minebase/types/blocks.py new file mode 100644 index 0000000..4a2d97a --- /dev/null +++ b/minebase/types/blocks.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +from typing import Any, Literal, cast, final + +from pydantic import Field, model_validator + +from minebase.types._base import MinecraftDataModel + + +@final +class BlockVariationData(MinecraftDataModel): + """Minecraft-Data for a block variation.""" + + metadata: int = Field(ge=0) + display_name: str + description: str | None = None + + +@final +class JavaBlockStateData(MinecraftDataModel): + """Minecraft-Data for a block state. + + Attributes: + name: The name of the property + type: The type of the property + values: The possible values of the property + num_values: The number of possible values + """ + + name: str + type: Literal["enum", "bool", "int", "direction"] + values: list[str] | None = None + num_values: int = Field(ge=1, alias="num_values") # for some reason, this is snake_cased + + +@final +class BlockDropItem(MinecraftDataModel): + """Minecraft-Data for a specific item drop from a block. + + Attributes: + id: The unique identifier of the dropped item. + metadata: Metadata information of the dropped item. + """ + + id: int = Field(ge=0) + metadata: int | None = Field(ge=0, default=None) + + @model_validator(mode="before") + @classmethod + def from_int(cls, v: object) -> object: + """Allow shorthand 'int' (meaning just id) for drop option by wrapping it.""" + if isinstance(v, int): + return {"id": v} + return v + + +@final +class BlockDropData(MinecraftDataModel): + """Minecraft-Data for a block drop. + + Attributes: + min_count: Minimum number or chance, default: 1 + max_count: Maximum number or chance, default: minCount + drop: Details about the dropped item. + """ + + min_count: float = Field(ge=0, default=1) + max_count: float = Field(ge=0) + drop: BlockDropItem + + @classmethod + def _default_max_count(cls, data: object) -> object: + """Populate max_count from min_count when it is missing. + + The `max_count` field should default to the value of the `min_count` field, if it's + not explicitly specified. + """ + if not isinstance(data, dict): + return data + + data = cast("dict[Any, Any]", data) # explicitly make the type unknown, to prevent pyright complaints + + if "maxCount" not in data: + data["maxCount"] = data.get("minCount", 1) + + return data + + @classmethod + def _from_int(cls, v: object) -> object: + """Allow shorthand 'int', meaning just an int drop (count=1) by wrapping it.""" + if isinstance(v, int): + return {"drop": v} + return v + + @model_validator(mode="before") + @classmethod + def pre_validator(cls, v: object) -> object: + """Run all before validation logic.""" + v = cls._from_int(v) + return cls._default_max_count(v) + + @model_validator(mode="after") + def check_bounds(self) -> BlockDropData: + """Ensure logical bounds between min_count and max_count.""" + if self.max_count < self.min_count: + raise ValueError("max_count must be greater than or equal to min_count") + return self + + +@final +class BlockData(MinecraftDataModel): + """Minecraft-Data for a specific block. + + Attributes: + id: The unique identifier for a block + name: The name of the block (guaranteed unique) + display_name: The name of the block as shown in the GUI + hardness: Hardness value of a block + stack_size: Stack size for a block + diggable: Can this block be digged? + bounding_box: The bounding box of a block + material: Material of a block + harvest_tools: + Using one of these tools is required to harvest a block. + + Without that, you get a 3.33x time penalty. + variations: The list of variations of this block + states: + The list of states of this block. + + This field is only present on Java edition, for Bedrock, block states + are tracked in a standalone key outside of block data. + transparent: Is this block transparent? + emit_light: Light level emitted by this block (0-15) + filter_light: Light filtered by this block (0-15) + min_state_id: Minimum state id + max_state_id: Maximum state id + default_state: Default state id + resistence: Blast resistance + """ + + id: int = Field(ge=0) + name: str + display_name: str + hardness: float | None = Field(ge=-1) + stack_size: int = Field(ge=0) + diggable: bool + bounding_box: Literal["block", "empty"] + material: str | None = None + harvest_tools: dict[int, bool] | None = None + variations: list[BlockVariationData] | None = None + states: list[JavaBlockStateData] | None = None + drops: list[BlockDropData] + transparent: bool + emit_light: int = Field(ge=0, le=15) + filter_light: int = Field(ge=0, le=15) + min_state_id: int | None = Field(ge=0, default=None) + max_state_id: int | None = Field(ge=0, default=None) + default_state: int | None = Field(ge=0, default=None) + resistance: float | None = Field(ge=-1, default=None) diff --git a/minebase/types/commands.py b/minebase/types/commands.py new file mode 100644 index 0000000..beb7eb0 --- /dev/null +++ b/minebase/types/commands.py @@ -0,0 +1,226 @@ +from __future__ import annotations + +from typing import Annotated, Literal, Union, final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class ParserModifier(MinecraftDataModel): + """Additional metadata and constraints for a command parser. + + A modifier adjusts how a parser interprets or restricts the accepted input. + Modifiers can enforce numeric ranges, specify special parsing modes, or point + to a game registry for valid values. + + Attributes: + amount: Indicates whether the parser consumes a single value or multiple values. + max: Upper bound for numeric parsers, if applicable. + min: Lower bound for numeric parsers, if applicable. + type: Special parsing mode (e.g. entities, players, word, phrase). + registry: + Namespace key of a Minecraft registry that provides the set of valid inputs + (e.g. "minecraft:entity_type", "minecraft:worldgen/biome"). + """ + + amount: Literal["multiple", "single"] | None = None + max: int | None = None + min: int | None = None + type: Literal["entities", "greedy", "phrase", "players", "word"] | None = None + registry: str | None = None + + +@final +class CommandRootParserInfo(MinecraftDataModel): + """Definition of a root-level command parser. + + Root parsers describe the set of available argument parsers + that can be referenced by command argument nodes. Each parser + may include example values and optional modifier constraints. + + Attributes: + parser: Fully qualified parser identifier (e.g. "brigadier:integer" / "minecraft:vec3" / "minecraft:entity"). + modifier: Optional modifier object with constraints and extra behavior. + examples: Example values accepted by the parser, used for hints or validation. + """ + + parser: str + modifier: ParserModifier | None + examples: list[str] + + +@final +class CommandInnerParserInfo(MinecraftDataModel): + """Definition of a parser attached to a command argument node. + + Inner parsers describe how a specific argument should be interpreted when + parsing a command. Unlike root parsers (which list all available parsers), + inner parsers are embedded within `CommandArgumentNode` instances and + specify the exact parser and optional modifier used for that argument. + + Examples of inner parsers include: + - `minecraft:entity` with modifiers that distinguish between single/multiple + entities or players. + - `minecraft:resource` or `minecraft:resource_key` tied to specific registries + such as `"minecraft:entity_type"` or `"minecraft:worldgen/structure"`. + - Simple parsers like `minecraft:time`, `minecraft:vec3`, or `minecraft:uuid` + without modifiers. + + Attributes: + parser: + Identifier of the parser used for this argument + (e.g. "minecraft:entity", "minecraft:time", "minecraft:vec3"). + modifier: + Optional modifier object that constrains or customizes parsing, + such as limiting numeric ranges, specifying single vs. multiple + entities, or binding to a registry of valid values. + """ + + parser: str + modifier: ParserModifier | None + + +@final +class CommandArgumentNode(MinecraftDataModel): + """Represents a typed argument in a Minecraft command tree (Java edition). + + Argument nodes define the structure and semantics of command parameters. Each + argument specifies how input should be parsed, whether it can directly execute + a command, and what further arguments or subcommands may follow. + + Examples include: + - Numeric arguments with constraints (e.g. `fadeIn: brigadier:integer(min=0)`). + - NBT or resource parsers (`value: minecraft:nbt_tag`, `loot_table: minecraft:resource_location`). + - Entity and player selectors with modifiers + (`player: minecraft:entity(type=players, amount=single)` or + `entities: minecraft:entity(type=entities, amount=multiple)`). + - Free-form string inputs (`name: brigadier:string(type=phrase)` or + `action: brigadier:string(type=greedy)`). + + Attributes: + type: Literal string identifying this as an `"argument"` node. + name: The argument's identifier as it appears in the command (e.g. `"player"`, `"distance"`, `"fadeIn"`). + executable: + Whether this node can directly terminate a valid command path and trigger + execution. If `False`, at least one child must be consumed. + redirects: + Optional list of alternative command paths this node may redirect to. + Useful when arguments delegate parsing or execution to another node + (e.g. `["execute"]`). + children: + Nested command nodes (both literal and argument) that may follow this + argument. Defines branching in the command tree. + parser: + The parser that defines how to interpret this argument's value, wrapped + in a `CommandInnerParserInfo`. This may include optional `ParserModifier` + metadata such as numeric bounds, registry lookups, or selection mode. + + """ + + type: Literal["argument"] + name: str + executable: bool + redirects: list[str] + children: list[CommandChildNode] + parser: CommandInnerParserInfo | None = None + + +@final +class CommandLiteralNode(MinecraftDataModel): + """Represents a fixed keyword in a Minecraft command tree (Java edition). + + Literal nodes define the constant words that form the structure of a command. + They are used for command names (e.g. `/time`, `/teleport`) and for branching + subcommands (e.g. `advancement from`, `advancement only `). + + Unlike argument nodes, literal nodes do not accept user input. Instead, they + specify exact keywords that must appear in the command. Each literal may be + executable on its own or act as a prefix that leads to additional child nodes. + + Examples include: + - Root-level commands: + - `time` + - `teleport` + - `whitelist` + - Nested subcommands: + - `advancement from ` + - `advancement only ` + + Attributes: + type: Literal string identifying this as a `"literal"` node. + name: The keyword as it appears in the command (e.g. `"time"`, `"from"`). + executable: + Whether this node can directly terminate a valid command path and + trigger execution (e.g. `/thunder`). + redirects: + Optional list of alternative command paths this node may redirect to. + Most literals have no redirects, but some delegate execution (e.g. + `"target" -> ["execute"]`). + children: + Nested command nodes (either literals or arguments) that may follow + this literal. Defines branching and subcommands beneath this keyword. + """ + + type: Literal["literal"] + name: str + executable: bool + redirects: list[str] + children: list[CommandChildNode] + + +CommandChildNode = Annotated[Union[CommandLiteralNode, CommandArgumentNode], Field(discriminator="type")] + + +@final +class CommandRootNode(MinecraftDataModel): + """Represents the root of the Minecraft command tree (Java edition). + + The root node serves as the single entry point for all commands. It does not + correspond to any literal keyword typed by the player, nor a dynamic argument + as a part of the command, it just acts as the invisible parent of every top-level + command literal (e.g. `time`, `teleport`, `advancement`). From this node, parsing + begins and branches into child `CommandLiteralNode` instances. + + Unlike literal or argument nodes, the root node: + - Always has `type = "root"`. + - Has a fixed `name = "root"`. + - Cannot be executable (`executable = False`). + - Cannot redirect to any other node (`redirects` is always empty). + + Attributes: + type: Literal string identifying this as the `"root"` node. + name: Always `"root"`, as there is only one root node in the tree. + executable: Always `False`, since the root itself cannot represent a runnable command. + redirects: Always an empty list, since the root cannot redirect to another node. + children: + List of all top-level command nodes of `CommandLiteralNode` instances corresponding + to each command keyword available in the game. + """ + + type: Literal["root"] + name: Literal["root"] + executable: Literal[False] + redirects: list[str] = Field(max_length=0) + children: list[CommandLiteralNode] = Field(min_length=1) + + +@final +class CommandsData(MinecraftDataModel): + """Minecraft-Data representing the full set of Minecraft commands (Java edition). + + This model encapsulates the complete command tree for the game, starting from the root node, + all top-level and nested command literals and arguments, as well as the definitions of all + root-level parsers used by arguments. + + Attributes: + root: The root node of the command tree. All command parsing begins here. + parsers: + List of root-level parsers (`CommandRootParserInfo`) that describe the + types of arguments used throughout the command tree, including examples + and optional constraints. + """ + + root: CommandRootNode + parsers: list[CommandRootParserInfo] diff --git a/minebase/types/common_data.py b/minebase/types/common_data.py new file mode 100644 index 0000000..7d6f53b --- /dev/null +++ b/minebase/types/common_data.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from typing import Literal, final + +from pydantic import model_validator + +from minebase.types._base import MinecraftDataModel + + +@final +class LegacyData(MinecraftDataModel): + """Minecraft-Data for Legacy mappings (id -> name). + + Attributes: + blocks: Mapping of block IDs to their names. + items: Mapping of item IDs to their names. + """ + + blocks: dict[str, str] + items: dict[str, str] + + +@final +class FeatureValueData(MinecraftDataModel): + """Minecraft-Data of a sub-feature and the versions in which it is supported. + + Certain features have multiple values / variants / sub-features, this represents such a sub-feature. + + Attributes: + value: The feature's value, which may be a string or integer. + versions: Inclusive version range in which this value applies, as a tuple (start_version, end_version). + version: Single version in which this value applies. + """ + + value: str | int + versions: tuple[str, str] | None = None + version: str | None = None + + @model_validator(mode="after") + def check_version_xor_versions(self) -> FeatureValueData: + """Validate that exactly one of `version` or `versions` is set.""" + if self.version is not None and self.versions is not None: + raise ValueError("Cannot specify both 'version' and 'versions'") + if self.version is None and self.versions is None: + raise ValueError("Must specify either 'version' or 'versions'") + return self + + +@final +class FeatureData(MinecraftDataModel): + """Minecraft-Data for a feature. + + Attributes: + name: The name of the feature. + description: Human-readable description of the feature. + versions: Inclusive version range in which this feature applies, as a tuple (start_version, end_version). + version: Single version in which this feature applies. + values: Possible sub-features / variants of this feature that can each apply for different versions. + """ + + name: str + description: str + versions: tuple[str, str] | None = None + version: str | None = None + values: list[FeatureValueData] | None = None + + @model_validator(mode="after") + def check_exclusive_fields(self) -> FeatureData: + """Validate that exactly one of `version`, `versions`, or `values` is set.""" + fields: dict[str, object] = {"version": self.version, "versions": self.versions, "values": self.values} + + # Count non-None fields + provided_fields = sum(1 for value in fields.values() if value is not None) + + if provided_fields == 0: + raise ValueError("Must provide exactly one of: 'version', 'versions', or 'values'") + if provided_fields > 1: + raise ValueError("Cannot provide more than one of: 'version', 'versions', or 'values'") + return self + + +@final +class ProtocolVersionData(MinecraftDataModel): + """Minecraft-Data about a protocol version. + + Attributes: + minecraft_version: The version of Minecraft that uses this protocol version. + version: The protocol version number. + data_version: Internal data version number. + uses_netty: + Whether this protocol version uses Netty networking. + + In version 1.7.2 of the PC (Java) edition, the protocol numbers were reset to 0 + as the protocol was rewritten to use Netty. + + This field is only present for PC versions. + major_version: The major Minecraft version identifier (e.g., '1.19'). + release_type: The release type, either 'snapshot' or 'release'. + """ + + minecraft_version: str + version: int + data_version: int | None = None + uses_netty: bool | None = None + major_version: str + release_type: Literal["snapshot", "release"] | None = None + + +@final +class CommonData(MinecraftDataModel): + """Minecraft-Data common across all Minecraft versions, or metadata information. + + Attributes: + legacy: Legacy block and item ID mappings. + versions: List of Minecraft version strings. + features: List of feature definitions. + protocol_versions: List of protocol version data entries. + """ + + legacy: LegacyData + versions: list[str] + features: list[FeatureData] + protocol_versions: list[ProtocolVersionData] diff --git a/minebase/types/data_paths.py b/minebase/types/data_paths.py index 1d7b09f..133d3db 100644 --- a/minebase/types/data_paths.py +++ b/minebase/types/data_paths.py @@ -1,8 +1,18 @@ -from typing import TypedDict +from __future__ import annotations +from typing import final -class DataPaths(TypedDict): - """Strucutre of the `dataPaths.json` manifest file.""" +from minebase.types._base import MinecraftDataModel + + +@final +class DataPaths(MinecraftDataModel): + """Strucutre of the `dataPaths.json` manifest file. + + Attributes: + pc: PC (Java) edition version to data paths mapping. + bedrock: Bedrock edition version to data paths mapping. + """ pc: dict[str, dict[str, str]] bedrock: dict[str, dict[str, str]] diff --git a/minebase/types/effects.py b/minebase/types/effects.py new file mode 100644 index 0000000..97a0aaf --- /dev/null +++ b/minebase/types/effects.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Literal, final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class EffectData(MinecraftDataModel): + """Minecraft-Data for an effect. + + Attributes: + id: The unique identifier for an effect + name: The name of an effect (guaranteed unique) + display_name: The name of an effect as shown in the GUI + type: Whether an effect is positive or negative + """ + + id: int = Field(ge=0) + name: str + display_name: str + type: Literal["good", "bad"] diff --git a/minebase/types/enchantments.py b/minebase/types/enchantments.py new file mode 100644 index 0000000..623e49c --- /dev/null +++ b/minebase/types/enchantments.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field, field_validator + +from minebase.types._base import MinecraftDataModel + + +@final +class EnchantmentCostCoefficients(MinecraftDataModel): + """Minecraft-Data for the cost equation's coefficients a * level + b.""" + + a: int + b: int + + +@final +class EnchantmentData(MinecraftDataModel): + """Minecraft-Data for an enchantment. + + Attributes: + id: The unique identifier for an enchantment + name: The name of an enchantment (guaranteed unique) + display_name: The name of an enchantment, as displayed in the GUI + max_level: Max cost equation's coefficients a * level + b. + min_level: Min cost equation's coefficients a * level + b. + category: The category of enchantable items + weight: Weight of the rarity of the enchantment + treasure_only: Can only be found in a treasure, not created + curse: Is a curse, not an enchantment + tradable: Can this enchantment be traded + discoverable: Can this enchantment be discovered + exclude: List on enchantments (names) not compatible + """ + + id: int = Field(ge=0) + name: str + display_name: str + max_level: int = Field(ge=1, le=5) + min_cost: EnchantmentCostCoefficients + max_cost: EnchantmentCostCoefficients + category: str + weight: int = Field(ge=1, le=10) + treasure_only: bool + curse: bool + tradeable: bool + discoverable: bool + exclude: list[str] | None = None + + @field_validator("exclude") + @classmethod + def ensure_unique_items(cls, v: list[str]) -> list[str]: + """Make sure that items in given list are unique.""" + if len(v) != len(set(v)): + raise ValueError("List items must be unique") + return v diff --git a/minebase/types/entities.py b/minebase/types/entities.py new file mode 100644 index 0000000..492267c --- /dev/null +++ b/minebase/types/entities.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from enum import Enum +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class EntityCategory(str, Enum): + """Minecraft-Data enumeration of the possible semantic entity categories.""" + + BLOCKS = "Blocks" + DROPS = "Drops" + GENERIC = "Generic" + HOSTILE_MOBS = "Hostile mobs" + IMMOBILE = "Immobile" + NPCS = "NPCs" + PASSIVE_MOBS = "Passive mobs" + PROJECTILES = "Projectiles" + UNKNOWN = "UNKNOWN" + VEHICLES = "Vehicles" + + +@final +class EntityType(str, Enum): + """Minecraft-Data enumeration of the possible entity types.""" + + BLANK = "" + UNKNOWN = "UNKNOWN" + AMBIENT = "ambient" + ANIMAL = "animal" + HOSTILE = "hostile" + LIVING = "living" + MOB = "mob" + OBJECT = "object" + OTHER = "other" + PASSIVE = "passive" + PLAYER = "player" + PROJECTILE = "projectile" + WATER_CREATURE = "water_creature" + + +@final +class EntityData(MinecraftDataModel): + """Minecraft-Data for an entity. + + Attributes: + id: The unique identifier for an entity + internal_id: The internal id of an entity; used in eggs metadata for example + name: The name of an entity (guaranteed unique) + display_name: The name of an entity as displayed in the GUI + type: The type of an entity + category: The semantic category of an entity + width: The width of the entity + height: The height of the entity + length: The length of the entity + offset: The offset of the entity + metadata_keys: The pc metadata keys of an entity (naming is via mc code, with data_ and id_ prefixes stripped) + """ + + id: int = Field(ge=0) + internal_id: int | None = Field(ge=0, default=None) + name: str + display_name: str + type: EntityType + category: EntityCategory | None = None + width: float | None + height: float | None + length: float | None = None + offset: float | None = None + metadata_keys: list[str] | None = None diff --git a/minebase/types/entity_loot.py b/minebase/types/entity_loot.py new file mode 100644 index 0000000..e87534b --- /dev/null +++ b/minebase/types/entity_loot.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class EntityItemDrop(MinecraftDataModel): + """Minecraft-Data for an entity item drop information. + + Attributes: + item: The name of the item being dropped (guaranteed unique) + metadata: The metadata of the item being dropped (Bedrock Edition) + drop_chance: The percent chance of the item drop to occur + stack_size_range: The min/max number of items in this item drop stack + player_kill: If a player kill is required + """ + + item: str + metadata: int | None = Field(ge=0, le=127, default=None) + drop_chance: float + stack_size_range: tuple[int, int] + player_kill: bool | None = None + + +@final +class EntityLootData(MinecraftDataModel): + """Minecraft-Data for entity loot information. + + Attributes: + entity: The name of the entity (guaranteed unique) + drops: The list of item drops + """ + + entity: str + drops: list[EntityItemDrop] diff --git a/minebase/types/foods.py b/minebase/types/foods.py new file mode 100644 index 0000000..b81ebc5 --- /dev/null +++ b/minebase/types/foods.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class FoodVariation(MinecraftDataModel): + """Minecraft-Data for a food item variation.""" + + metadata: int = Field(ge=0) + display_name: str + + +@final +class FoodData(MinecraftDataModel): + """Minecraft-Data for a food. + + Attributes: + id: The associated item ID for this food item + display_name: The name of the food item as shown in the GUI + name: The name of the food item (guaranteed unique) + stack_size: The stack size for this food item + food_points: The amount of food (hunger) points the food item replenishes + saturation: The amount of saturation points the food restores (food_points + saturation_ratio) + saturation_ratio: + The 'saturation modifier' in Minecraft code, used to determine how much saturation an item has + effective_quality: food_points + saturation + variations: All variations of this food item + """ + + id: int = Field(ge=0) + display_name: str + name: str + stack_size: int = Field(ge=1, le=64) + food_points: float = Field(ge=0) + saturation: float = Field(ge=0) + saturation_ratio: float = Field(ge=0) + effective_quality: float = Field(ge=0) + variations: list[FoodVariation] | None = None diff --git a/minebase/types/instruments.py b/minebase/types/instruments.py new file mode 100644 index 0000000..20f5914 --- /dev/null +++ b/minebase/types/instruments.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class InstrumentData(MinecraftDataModel): + """Minecraft-Data for an instrument. + + An instrument controls the behavior of a note block. + + Attributes: + id: The unique identifier for an instrument + name: The name of an instrument + sound: The sound ID played by this instrument + """ + + id: int = Field(ge=0) + name: str + sound: str | None = None diff --git a/minebase/types/items.py b/minebase/types/items.py new file mode 100644 index 0000000..c4cfd1c --- /dev/null +++ b/minebase/types/items.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field, model_validator + +from minebase.types._base import MinecraftDataModel + + +@final +class ItemVariationData(MinecraftDataModel): + """Minecraft-Data for an item variation. + + Certain items have multiple variations that are distinguished through a metadata number. + For example, coal (id 263) has a charcoal variation with metadata=1 (making it 263:1). + These sub-items don't have their own entry in the items data, they're only tracked as + variations of the parent item. + + Attributes: + metadata: + The metadata number to distinguish this variation from the parent. + + Each variation must have a metadata value. + display_name: + The item name as shown in the GUI. + + Each variation must have it's own display name that differs from the parent item + id: + The unique identifier of the item. + + Most variations don't have their own ID, and instead only contain metadata to + distinguish them from the parent, however, some variations are given their own + item ID, even though they're still only considered a variation of the parent. + name: + The minecraft name of an item (guaranteed to be unique). + + Many variations aren't given a name and instead share the same item name with + the parent item, and are distinguished only by metadata. However, some do have + their own unique name, even though they're still only considered a variation + of the parent. + enchant_categories: Which enchant categories apply to this item variation + stack_size: What is the stack size of this item variation + + """ + + metadata: int = Field(ge=0) + display_name: str + enchant_categories: list[str] | None = None + stack_size: int | None = Field(ge=0, le=64, default=None) + id: int | None = Field(ge=0, default=None) + name: str | None = None + + +@final +class ItemsData(MinecraftDataModel): + """Minecraft-Data about an item. + + Attributes: + id: The unique identifier of the item + name: The minecraft name of an item (guaranteed to be unique) + display_name: The item name as shown in the GUI + stack_size: The maximum amount that can be in a single stack for this item (usually 64) + enchant_categories: Which enchant categories apply to this item + repair_with: Items (item names) that this item can be combined with in an anvil for repair + max_durability: The maximum amount of durability points for this item + block_state_id: The unique identifier of the block that will be placed from this block item. + variations: Variantions of this item (e.g. for coral, there's Tube Coral, Brain Coral, Bubble Coral, ...) + metadata: Number used primarily to distinguish item variations (e.g. tall grass 150:1 vs fern 150:2) + """ + + id: int = Field(ge=0) + name: str + display_name: str + stack_size: int = Field(ge=0) + enchant_categories: list[str] | None = None + repair_with: list[str] | None = None + max_durability: int | None = Field(ge=0, default=None) + variations: list[ItemVariationData] | None = None + block_state_id: int | None = Field(ge=0, default=None) + metadata: int | None = Field(ge=0, default=None) + + @model_validator(mode="before") + @classmethod + def strip_durability(cls, data: dict[str, object]) -> dict[str, object]: + """Remove the redundant `durability` field, if present. + + The minecraft-data dataset includes both `max_durability` and `durability`, however, these fields + always match, since this is the data for new items only. This makes the durability field entirely + redundant; strip it. + """ + if "durability" not in data: + return data + + if "maxDurability" not in data: + raise ValueError("Found durability field without max_durability") + + if data["durability"] != data["maxDurability"]: + raise ValueError("The durability field doesn't match max_durability") + + del data["durability"] + return data + + @model_validator(mode="before") + @classmethod + def rename_fixed_with(cls, data: dict[str, object]) -> dict[str, object]: + """Rename the `fixed_with` field to `repair_with`. + + These fields mean the same thing, however, the minecraft-data dataset includes one + single version (bedrock 1.17.10), where for some reason, the field name `fixed_with` + is used instead of `repair_with`. For a simpler user-facing API, this renames that + field back to `repair_with`. + + This will get addressed with: https://github.com/PrismarineJS/minecraft-data/pull/1052 + after which this method can be removed. + """ + if "fixedWith" not in data: + return data + + if "repairWith" in data: + raise ValueError("Found item with both fixed_with and repair_with field") + + data["repairWith"] = data.pop("fixedWith") + return data diff --git a/minebase/types/map_icons.py b/minebase/types/map_icons.py new file mode 100644 index 0000000..5186a11 --- /dev/null +++ b/minebase/types/map_icons.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class MapIconData(MinecraftDataModel): + """Minecraft-Data for a map icon. + + This contains a list of icons that can show up on a map, e.g. a player indicator, + markers, X target for treasure maps, etc. + + Attributes: + id: The unique identifier for a map icon + name: The name of a map icon + appearance: Description of the map icon's appearance + visible_in_item_frame: Visibility in item frames + """ + + id: int = Field(ge=0) + name: str + appearance: str | None = None + visible_in_item_frame: bool diff --git a/minebase/types/mcdata.py b/minebase/types/mcdata.py new file mode 100644 index 0000000..fcaf210 --- /dev/null +++ b/minebase/types/mcdata.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel +from minebase.types.attributes import MinecraftAttributeData +from minebase.types.biomes import BiomeData +from minebase.types.block_collision_shapes import BlockCollisionShapeData +from minebase.types.block_loot import BlockLootData +from minebase.types.block_mappings import BlockMappingData +from minebase.types.block_states import BedrockBlockStateData +from minebase.types.blocks import BlockData +from minebase.types.commands import CommandsData +from minebase.types.effects import EffectData +from minebase.types.enchantments import EnchantmentData +from minebase.types.entities import EntityData +from minebase.types.entity_loot import EntityLootData +from minebase.types.foods import FoodData +from minebase.types.instruments import InstrumentData +from minebase.types.items import ItemsData +from minebase.types.map_icons import MapIconData +from minebase.types.particle_data import ParticleData +from minebase.types.recipes import BedrockRecipesData, JavaRecipesData +from minebase.types.sounds import SoundData +from minebase.types.steve import SteveData +from minebase.types.tints import TintData +from minebase.types.version import VersionData +from minebase.types.windows import WindowData + + +class BaseMinecraftData(MinecraftDataModel): + """Minecraft-Data for a specific game version. + + Attributes: + biomes: + items; + version: + attributes: + windows: + enchantments: + language: Language string translations into the en_US language. + block_collision_shapes: + instruments: + materials: + entities: + effects: + entity_loot: + block_loot: + """ + + biomes: list[BiomeData] | None = None + items: list[ItemsData] | None = None + version: VersionData + attributes: list[MinecraftAttributeData] | None = None + windows: list[WindowData] | None = None + enchantments: list[EnchantmentData] | None = None + language: dict[str, str] | None = None + block_collision_shapes: BlockCollisionShapeData | None = None + instruments: list[InstrumentData] | None = None + materials: dict[str, dict[int, float]] | None = None + entities: list[EntityData] | None = None + effects: list[EffectData] | None = None + entity_loot: list[EntityLootData] | None = None + block_loot: list[BlockLootData] | None = None + + +@final +class PcMinecraftData(BaseMinecraftData): + foods: list[FoodData] | None = None + tints: TintData | None = None + map_icons: list[MapIconData] | None = None + sounds: list[SoundData] | None = None + blocks: list[BlockData] + protocol: dict + particles: list[ParticleData] | None = None + protocol_comments: dict | None = None + commands: CommandsData | None = None + login_packet: dict | None = None + recipes: JavaRecipesData | None = None + + +@final +class BedrockMinecraftData(BaseMinecraftData): + blocks: list[BlockData] | None = None + protocol: dict | None = None + steve: SteveData | None = None + block_states: list[BedrockBlockStateData] | None = None + blocks_b2j: dict[str, str] | None = Field(alias="blocksB2J", default=None) + blocks_j2b: dict[str, str] | None = Field(alias="blocksJ2B", default=None) + block_mappings: list[BlockMappingData] | None = None + recipes: BedrockRecipesData | None = None diff --git a/minebase/types/particle_data.py b/minebase/types/particle_data.py new file mode 100644 index 0000000..0bf75fb --- /dev/null +++ b/minebase/types/particle_data.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class ParticleData(MinecraftDataModel): + """Minecraft-Data for a particle. + + Attributes: + id: The unique identifier for a particle + name: The name of a particle + """ + + id: int = Field(ge=0) + name: str diff --git a/minebase/types/recipes.py b/minebase/types/recipes.py new file mode 100644 index 0000000..d106320 --- /dev/null +++ b/minebase/types/recipes.py @@ -0,0 +1,322 @@ +from __future__ import annotations + +from typing import Annotated, Any, Literal, Union, final + +from pydantic import Field, RootModel, field_validator, model_validator +from typing_extensions import TypeAlias + +from minebase.types._base import MinecraftDataModel + +# region: Java + + +@final +class JavaRecipeResult(MinecraftDataModel): + """Represents the output item produced by a crafting recipe. + + Attributes: + id: The numerical item ID of the resulting item. + count: The quantity of the resulting item produced by the recipe. + metadata: Metadata value associated with the resulting item, or None if the item has no metadata. + """ + + id: int + count: int + metadata: int | None = None + + +@final +class JavaRecipeIngredientItem(MinecraftDataModel): + """Represents a single ingredient item in a crafting recipe. + + Attributes: + id: The numerical item ID of the ingredient. + metadata: Metadata value associated with the ingredient item, or None if the ingredient has no metadata. + + Notes: + Ingredient items can be specified either as: + - A simple integer representing the `id`. + - A structure containing both `id` and `metadata` values. + + When provided as an integer, it will be automatically converted into + the structured form with `metadata` set to None. + """ + + id: int + metadata: int | None + + @model_validator(mode="before") + @classmethod + def convert_pure_int_id(cls, obj: object) -> Any: + """Convert integer form into structured form. + + A recipe ingredient item can be specified as a simple integer (id), or as a structure of + {id: int, metadata: int}. This method converts the simple int into a structure that pydantic + can work with. + """ + if isinstance(obj, int): + return {"id": obj, "metadata": None} + + return obj + + +@final +class JavaShapelessRecipe(MinecraftDataModel): + """Minecraft-Data for a shapeless recipe. + + This represents a crafting recipe that does not require the items to be placed in any + specific order in the crafting grid. + + Attributes: + result: The item that will be created when this recipe is crafted. + ingredients: A list of ingredient items required for the recipe. Must contain at least one item. + """ + + result: JavaRecipeResult + ingredients: list[JavaRecipeIngredientItem] = Field(min_length=1) + + +@final +class JavaShapedRecipe(MinecraftDataModel): + """Minecraft-Data for a shaped recipe. + + This represents a crafting recipe that requires the items to be put in a specific order/shape. + + Attributes: + result: The item that will be created with this recipe + in_shape: + The 2D grid of ingredients as they are placed into the crafting table slots, + represented as a list of rows containing `JavaRecipeIngredientItem` instances or `None` (gaps). + Each row corresponds to one horizontal row of the crafting grid. + out_shape: + The 2D grid representing the remaining items in the crafting grid after the recipe + is completed. For example, items with containers (like milk buckets) may leave behind + their empty container (e.g., an empty bucket) in the same slot. Slots where nothing remains + are represented as `None`. + """ + + result: JavaRecipeResult + in_shape: list[list[JavaRecipeIngredientItem | None]] + out_shape: list[list[JavaRecipeIngredientItem | None]] | None = None + + @field_validator("in_shape", "out_shape") + @classmethod + def validate_shape( + cls, + v: list[list[JavaRecipeIngredientItem | None]] | None, + ) -> list[list[JavaRecipeIngredientItem | None]] | None: + """Validate that the shape follows the expected pattern. + + - There must be at least 1 row + - There must be at most 3 rows + - There must be at least 1 item in each row + - There must be at most 3 items in each row + - All rows must have the same length + """ + if v is None: + return v + + if len(v) < 1: + raise ValueError("A shape must have at least 1 row") + if len(v) > 3: + raise ValueError("A shape must have at most 3 rows") + + if len(v[0]) < 1: + raise ValueError("A shape must have at least 1 item in each row") + if len(v[0]) > 3: + raise ValueError("A shape must have at most 3 items in each row") + + row_len = len(v[0]) + if not all(len(row) == row_len for row in v): + raise ValueError("A shape must have the same row length for all rows") + + return v + + +@final +class JavaRecipesData(RootModel[dict[int, list["JavaShapedRecipe | JavaShapelessRecipe"]]]): ... + + +# endregion +# region: Bedrock + + +@final +class BedrockListEndNBTData(MinecraftDataModel): + """Represents an NBT list of type `end`. + + This is a special case in the NBT format indicating an empty list. + In Bedrock's JSON-like representation, this is modeled as a list with zero elements. + + This type can be treated as a simple indicator that the list is empty. It doesn't + contain any useful data. + """ + + type: Literal["end"] + value: list[object] = Field(max_length=0) # This is likely just a placeholder value to include something + + +@final +class BedrockCompoundListNBTData(MinecraftDataModel): + """Represents an NBT list data. + + Each element in `value` is a dictionary mapping NBT tag names to nested `BedrockNBT` values. + """ + + type: Literal["compound"] + value: list[dict[str, BedrockNBT]] + + +@final +class BedrockListNBTData(MinecraftDataModel): + """Represents a generic NBT list tag. + + This tag indicates that the inner compound tag is going to be a list of NBTs, + or an end indicator NBT (in case of an empty list) + """ + + type: Literal["list"] + value: BedrockCompoundListNBTData | BedrockListEndNBTData + + +@final +class BedrockByteNBTData(MinecraftDataModel): + """Represents an NBT (unsigned) byte tag.""" + + type: Literal["byte"] + value: int = Field(ge=0, le=255) + + +@final +class BedrockByteArrayNBTData(MinecraftDataModel): + """Represents an NBT byte array tag (contains a list of unsigned bytes).""" + + type: Literal["byteArray"] + value: list[Annotated[int, Field(ge=0, le=255)]] + + +@final +class BedrockIntNBTData(MinecraftDataModel): + """Represents an NBT integer (signed, 4 byte number) tag.""" + + type: Literal["int"] + value: int = Field(ge=-(2**31), le=2**31 - 1) + + +class BedrockCompoundNBTData(MinecraftDataModel): + """Represents a standard NBT compound tag. + + Each key is the tag name, and each value is another NBT tag. + """ + + type: Literal["compound"] + value: dict[str, BedrockNBT] + + +@final +class BedrockRootCompoundNBTData(BedrockCompoundNBTData): + """Represents the root NBT compound tag for a Bedrock file. + + The root compound NBT matches the simple compound NBT, but includes + a name string. However, this string always seems to be empty. + """ + + type: Literal["compound"] + name: Literal[""] + value: dict[str, BedrockNBT] + + +@final +class BedrockRootNBTData(MinecraftDataModel): + """Represents the top-level structure of a Bedrock Named Binary Tag (NBT).""" + + version: Literal[1] + nbt: BedrockRootCompoundNBTData + + +BedrockNBT: TypeAlias = Annotated[ + Union[ + BedrockCompoundNBTData, + BedrockByteNBTData, + BedrockListNBTData, + BedrockIntNBTData, + BedrockByteArrayNBTData, + ], + Field(discriminator="type"), +] + + +@final +class BedrockRecipeItem(MinecraftDataModel): + """Represents an item used as an ingredient or produced as output in a Bedrock recipe. + + Attributes: + name: The item name or identifier (e.g., "minecraft:planks"). + count: The number of this item required or produced. + metadata: Optional metadata value for the item, or None if not applicable. + nbt: Optional NBT data associated with the item. If None, the item has no NBT data. + """ + + name: str + count: int = Field(ge=1) + metadata: int | None = None + nbt: BedrockRootNBTData | None = None + + +@final +class BedrockRecipeData(MinecraftDataModel): + """Minecraft-Data for a crafting or processing recipe in Minecraft: Bedrock Edition. + + Attributes: + name: Internal recipe name identifier (guaranteed unique). + type: The type of crafting or processing station where the recipe applies. + ingredients: A list of `BedrockRecipeItem` objects representing the required ingredients. + input: + A 2D grid of integers referencing ingredient positions in `ingredients` (1-based index), + with 0 meaning a gap. Used only for shaped recipes. May be None for shapeless recipes + or non-crafting recipes. + output: A list of `BedrockRecipeItem` objects representing the produced items. + priority: + An optional priority value (currently only observed as 0). + + The exact meaning is unclear in Bedrock recipe data. + """ + + name: str + type: Literal[ + "multi", + "cartography_table", + "stonecutter", + "crafting_table", + "crafting_table_shapeless", + "shulker_box", + "furnace", + "blast_furnace", + "smoker", + "soul_campfire", + "campfire", + "smithing_table", + ] + ingredients: list[BedrockRecipeItem] = Field(min_length=1) + input: list[list[int]] | None = None + output: list[BedrockRecipeItem] = Field(min_length=1) + priority: Literal[0] | None = None + + @model_validator(mode="after") + def validate_input(self) -> BedrockRecipeData: + """Validate that the input shape only references the available ingredients.""" + if self.input is None: + return self + + max_ingredient_id = len(self.ingredients) + if not all(0 <= ingredient_id <= max_ingredient_id for shape_row in self.input for ingredient_id in shape_row): + raise ValueError("Recipe input shape references unknown ingredients") + + return self + + +@final +class BedrockRecipesData(RootModel[dict[int, BedrockRecipeData]]): ... + + +# endregion diff --git a/minebase/types/sounds.py b/minebase/types/sounds.py new file mode 100644 index 0000000..cdead0a --- /dev/null +++ b/minebase/types/sounds.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from typing import final + +from pydantic import field_validator + +from minebase.types._base import MinecraftDataModel + + +@final +class SoundData(MinecraftDataModel): + """Minecraft-Data for a single sound entry. + + Each entry maps a unique integer ID to a specific Minecraft sound, + identified by a namespaced string. This can include: + + - Block Sounds (e.g., 'block.stone.place') + - Entity Sounds (e.g., 'entity.snowman.shoot') + - Ambient Sounds (e.g., 'ambient.cave') + - Item sounds (e.g., 'item.bucket.fill_lava') + - Music tracks (e.g., 'music.credits') + - Music disks (e.g., 'music_disc.cat', or 'record.cat' in older versions) + - UI sounds (e.g., 'ui.button.click') + - Weather sounds (e.g., 'weather.rain') + - Events (e.g., 'event.raid.horn') + - Particles (e.g., 'particle.soul_escape') + - Enchantments (e.g. 'enchant.thorns.hit') + + Attributes: + id: Unique identifier for the sound entry. + name: Namespaced string representing the sound in Minecraft. + """ + + id: int + name: str + + @field_validator("name") + @classmethod + def name_namespace(cls, name: str) -> str: + """Validated that the sound `name` has one of the expected namespaces.""" + if name == "intentionally_empty": + return name + + try: + namespace, _ = name.split(".", maxsplit=1) + except ValueError as exc: + raise ValueError(f"Sound name {name} isn't namespaced") from exc + + namespaces = { + "block", + "entity", + "ambient", + "item", + "music", + "record", + "ui", + "weather", + "music_disc", + "event", + "particle", + "enchant", + } + + if namespace not in namespaces: + raise ValueError(f"Sound name {name} doesn't belong to any of the expected name-spaces: {namespaces}") + + return name diff --git a/minebase/types/steve.py b/minebase/types/steve.py new file mode 100644 index 0000000..578fd09 --- /dev/null +++ b/minebase/types/steve.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import re +from typing import Annotated, Literal, final +from uuid import UUID + +from pydantic import ConfigDict, Field +from pydantic.alias_generators import to_pascal +from pydantic.types import Base64Bytes, Base64Str, UuidVersion + +from minebase.types._base import MinecraftDataModel, _merge_base_config # pyright: ignore[reportPrivateUsage] + +# This allows any hex color with 1, 6 or 8 hex digits. This is a bit non-standard +# as in CSS, we can have 3, 4, 6 or 8. However, from observing the values present +# in minecraft-data, these seem to be the only values being used, with the 1 digit +# color being #0. For now, let's be strict and not allow 3/4 digit colors, we don't +# know what standard is being used here, let's be restrictive, we can allow those if +# we see a violation. +HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{1}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$") + + +@final +class AnimatedSkinImageData(MinecraftDataModel): + """Minecraft-Data for an animated skin image data (Bedrock only).""" + + # For some reason, these fields are in PascalCase... + model_config = _merge_base_config(ConfigDict(alias_generator=to_pascal)) + + type: Literal[1] + image_width: Literal[32] + image_height: Literal[64] + frames: Literal[2] + animation_expression: Literal[1] + image: Base64Bytes + + +@final +class SkinPieceTintColorsData(MinecraftDataModel): + """Minecraft-Data for a skin piece color (Bedrock only).""" + + # For some reason, these fields are in PascalCase... + model_config = _merge_base_config(ConfigDict(alias_generator=to_pascal)) + + colors: list[Annotated[str, Field(pattern=HEX_COLOR_RE)]] + piece_type: str + + +@final +class SkinPersonaPiecesData(MinecraftDataModel): + """Minecraft-Data for skin persona pieces (Bedrock only).""" + + # For some reason, these fields are in PascalCase... + model_config = _merge_base_config(ConfigDict(alias_generator=to_pascal)) + + is_default: Literal[True] + pack_id: Annotated[UUID, UuidVersion(4)] + piece_id: Annotated[UUID, UuidVersion(4)] + piece_type: str + product_id: Literal[""] + + +@final +class SteveData(MinecraftDataModel): + """Minecraft-Data for a steve (Bedrock only).""" + + # For some reason, these fields are in PascalCase... + model_config = _merge_base_config(ConfigDict(alias_generator=to_pascal)) + + animated_image_data: list[AnimatedSkinImageData] + arm_size: Literal["wide"] + cape_data: Literal[""] + cape_id: Literal[""] + cape_image_height: Literal[0] + cape_image_width: Literal[0] + cape_on_classic_skin: Literal[False] + persona_pieces: list[SkinPersonaPiecesData] + persona_skin: Literal[True] + piece_tint_colors: list[SkinPieceTintColorsData] + premium_skin: Literal[False] + skin_animation_data: Literal[""] + skin_color: str = Field(pattern=HEX_COLOR_RE) + skin_id: str + skin_image_height: Literal[256] + skin_image_width: Literal[256] + skin_resource_patch: Base64Str + skin_data: Base64Bytes + skin_geometry_data: Base64Str + skin_geometry_engine_version: str | None = None + skin_geometry_data_engine_version: str | None = None diff --git a/minebase/types/tints.py b/minebase/types/tints.py new file mode 100644 index 0000000..db36351 --- /dev/null +++ b/minebase/types/tints.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from typing import final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class TintDataStringKeyEntry(MinecraftDataModel): + """Represents a single tint mapping using string keys. + + Each entry associates one or more identifiers (e.g., biome names or block types) + with a specific color. + + Attributes: + keys: One or more string identifiers that this tint applies to. + color: The color value for the given keys, as an RGB integer. + """ + + keys: list[str] = Field(min_length=1) + color: int + + +@final +class TintDataIntegerKeyEntry(MinecraftDataModel): + """Represents a single tint mapping using integer keys. + + Each entry associates one or more integer identifiers with a specific color. + + Attributes: + keys: One or more integer identifiers that this tint applies to. + color: The color value for the given keys, typically as an RGB integer. + """ + + keys: list[int] = Field(min_length=1) + color: int + + +@final +class TintGroupIntegerKeys(MinecraftDataModel): + """A group of tints where each entry uses integer keys. + + Each entry in `data` specifies a set of integer keys as identifiers and the + corresponding color to use. The `default` field specifies a fallback color if + no keys match. + + Attributes: + data: List of integer-keyed tint entries. + default: Fallback color if no keys match. Optional. + """ + + data: list[TintDataIntegerKeyEntry] = Field(min_length=1) + default: int | None = None + + +@final +class TintGroupStringKeys(MinecraftDataModel): + """A group of tints where each entry uses string keys. + + Each entry in `data` specifies a set of string keys (e.g., biome names) and the + corresponding color to use. The `default` field specifies a fallback color if + no keys match. + + Attributes: + data: List of string-keyed tint entries. + default: Fallback color if no keys match. Optional. + """ + + # The `data` key should have min_length=1, however, currently, there is an entry + # in minecraft-data that does not conform to that (pc/1.21.4). Once this will get + # addressed, we should enforce min_lenght=1 here. + # https://github.com/PrismarineJS/minecraft-data/issues/1055 + data: list[TintDataStringKeyEntry] = Field(min_length=0) + default: int | None = None + + +@final +class TintData(MinecraftDataModel): + """Complete collection of Minecraft tints for different block or biome types. + + Each attribute represents a tint group for a specific category, mapping keys + to colors with an optional default. + + Attributes: + grass: Tint data for grass blocks. + foliage: Tint data for leaves and other foliage. + water: Tint data for water blocks and biomes. + redstone: Tint data for redstone dust depending on the signal strength level (as int keys). + constant: Tint data that remains constant across certain blocks. + """ + + grass: TintGroupStringKeys + foliage: TintGroupStringKeys + water: TintGroupStringKeys + redstone: TintGroupIntegerKeys + constant: TintGroupStringKeys diff --git a/minebase/types/version.py b/minebase/types/version.py new file mode 100644 index 0000000..6051d33 --- /dev/null +++ b/minebase/types/version.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import re +from typing import Literal, final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + +MC_VERSION_RE = re.compile(r"([0-9]+\.[0-9]+(\.[0-9]+)?[a-z]?(-pre[0-9]+)?)|([0-9]{2}w[0-9]{2}[a-z])") +MAJOR_VERSION_RE = re.compile(r"[0-9]+\.[0-9]+[a-z]?") + + +@final +class VersionData(MinecraftDataModel): + """Minecraft-Data for a specific Minecraft version.""" + + version: int + minecraft_version: str = Field(pattern=MC_VERSION_RE) + major_version: str = Field(pattern=MAJOR_VERSION_RE) + release_type: Literal["release", "snapshot"] | None = None diff --git a/minebase/types/windows.py b/minebase/types/windows.py new file mode 100644 index 0000000..c500718 --- /dev/null +++ b/minebase/types/windows.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from typing import Literal, final + +from pydantic import Field + +from minebase.types._base import MinecraftDataModel + + +@final +class WindowOpenedWithData(MinecraftDataModel): + """Minecraft-Data for open_with attribute of a window. + + Attributes: + id: The unique identifier of the block, item or the entity this window is opened with + type: The type of the object that this window is opened with (block, item or entity) + """ + + id: int + type: Literal["block", "item", "entity"] + + +@final +class WindowSlotsData(MinecraftDataModel): + """Minecraft- Data for a slot or slot range in a window. + + Attributes: + name: The name of the slot or slot range + index: The position of the slot or begin of the slot range + size: The size of the slot range + """ + + name: str + index: int = Field(ge=0) + size: int | None = Field(ge=0, default=None) + + +@final +class WindowData(MinecraftDataModel): + """Minecraft-Data for a window. + + Attributes: + id: The unique identifier for the window + name: The default displayed name of the window + slots: The slots displayed in the window + properties: Names of the properties of the window + opened_with: TODO + """ + + id: str + name: str + slots: list[WindowSlotsData] | None = Field(min_length=1, default=None) + properties: list[str] | None = Field(min_length=1, default=None) + opened_with: list[WindowOpenedWithData] | None = None diff --git a/pyproject.toml b/pyproject.toml index a5be029..137261a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,10 @@ classifiers = [ ] keywords = ["minecraft", "data"] requires-python = ">=3.9" -dependencies = [] +dependencies = [ + "eval-type-backport>=0.2.2", + "pydantic>=2.11.7", +] [project.urls] "Source code" = "https://github.com/py-mine/minebase" diff --git a/tests/conftest.py b/tests/conftest.py index 5ed7277..0ac7c5a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,13 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: params: list[tuple[Edition, str]] = [] for edition in Edition.__members__.values(): - versions = manifest[edition.value] + if edition is Edition.BEDROCK: + versions = manifest.bedrock + elif edition is Edition.PC: + versions = manifest.pc + else: + raise ValueError(f"Unhandled edition enum variant: {edition}") + if not versions: pytest.skip(f"No versions found for edition {edition}") params.extend((edition, version) for version in versions) diff --git a/tests/minebase/test_integration.py b/tests/minebase/test_integration.py index 087159a..a09acf7 100644 --- a/tests/minebase/test_integration.py +++ b/tests/minebase/test_integration.py @@ -13,6 +13,8 @@ load_common_data, load_version, ) +from minebase.types.common_data import CommonData +from minebase.types.mcdata import BaseMinecraftData def test_data_submodule_is_initialized() -> None: @@ -24,11 +26,11 @@ def test_data_submodule_is_initialized() -> None: def test_load_common_data_for_each_edition(edition: Edition) -> None: """Ensure common data exists and is loadable for each edition.""" data = load_common_data(edition) - assert isinstance(data, dict) + assert isinstance(data, CommonData) assert data, f"No common data found for edition {edition}" def test_all_versions_loadable(edition: Edition, version: str) -> None: # parametrized from conftest """Ensure that a specific version for an edition can be loaded.""" result = load_version(version, edition) - assert isinstance(result, dict) + assert isinstance(result, BaseMinecraftData) diff --git a/uv.lock b/uv.lock index 5c424eb..f0504a1 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 2 requires-python = ">=3.9" resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version < '3.10'", ] @@ -56,7 +57,8 @@ name = "click" version = "8.2.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, @@ -210,7 +212,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -367,6 +369,10 @@ wheels = [ [[package]] name = "minebase" source = { editable = "." } +dependencies = [ + { name = "eval-type-backport" }, + { name = "pydantic" }, +] [package.dev-dependencies] dev = [ @@ -396,6 +402,10 @@ test = [ ] [package.metadata] +requires-dist = [ + { name = "eval-type-backport", specifier = ">=0.2.2" }, + { name = "pydantic", specifier = ">=2.11.7" }, +] [package.metadata.requires-dev] dev = [