Skip to content

Commit 3268792

Browse files
committed
wip: Add pydantic models to represent minecraft-data
1 parent db2cb11 commit 3268792

File tree

8 files changed

+396
-4
lines changed

8 files changed

+396
-4
lines changed

minebase/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import json
44
from enum import Enum
55
from pathlib import Path
6-
from typing import Any, cast
6+
from typing import Any, Literal, cast, overload
77

88
from minebase.types.common_data import CommonData
99
from minebase.types.data_paths import DataPaths
10+
from minebase.types.mcdata import BedrockMinecraftData, PcMinecraftData
1011

1112
DATA_SUBMODULE_PATH = Path(__file__).parent / "data"
1213
DATA_PATH = DATA_SUBMODULE_PATH / "data"
@@ -60,7 +61,15 @@ def supported_versions(edition: Edition = Edition.PC) -> list[str]:
6061
return list(edition_info.keys())
6162

6263

63-
def load_version(version: str, edition: Edition = Edition.PC) -> dict[str, Any]:
64+
@overload
65+
def load_version(version: str, edition: Literal[Edition.PC] = Edition.PC) -> PcMinecraftData: ...
66+
67+
68+
@overload
69+
def load_version(version: str, edition: Literal[Edition.BEDROCK]) -> BedrockMinecraftData: ...
70+
71+
72+
def load_version(version: str, edition: Edition = Edition.PC) -> PcMinecraftData | BedrockMinecraftData:
6473
"""Load minecraft-data for given `version` and `edition`."""
6574
_validate_data()
6675
version_data = _load_version_manifest(version, edition)
@@ -81,7 +90,10 @@ def load_version(version: str, edition: Edition = Edition.PC) -> dict[str, Any]:
8190
with file.open("rb") as fp:
8291
data[field] = json.load(fp)
8392

84-
return data
93+
if edition is Edition.PC:
94+
return PcMinecraftData.model_validate(data)
95+
96+
return BedrockMinecraftData.model_validate(data)
8597

8698

8799
def load_common_data(edition: Edition = Edition.PC) -> CommonData:

minebase/types/attributes.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from typing import final
4+
5+
from pydantic import model_validator
6+
7+
from minebase.types.base import MinecraftDataModel
8+
9+
10+
@final
11+
class MinecraftAttributeData(MinecraftDataModel):
12+
"""Minecraft-Data for an attribute.
13+
14+
Attributes:
15+
name: The name of this attribute
16+
resource: The Mojang name of an attribute (usually generic.[name] or minecraft:generic.[name]
17+
min: The minimum value of an attribute
18+
max: The maximum value of an attribute
19+
default: The default value of an attribute
20+
"""
21+
22+
name: str
23+
resource: str
24+
default: float
25+
min: float
26+
max: float
27+
28+
@model_validator(mode="after")
29+
def valid_default(self) -> MinecraftAttributeData:
30+
"""Enforce that the default value is within the expected min-max bounds."""
31+
if self.min <= self.default <= self.max:
32+
return self
33+
34+
raise ValueError("The default value is outside of the min-max bounds")

minebase/types/biomes.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
from typing import Literal, final
4+
5+
from pydantic import Field, model_validator
6+
7+
from minebase.types.base import MinecraftDataModel
8+
9+
10+
@final
11+
class BiomeClimateData(MinecraftDataModel):
12+
"""Minecraft-Data about the climate in a Biome.
13+
14+
This controls the ideal parameter ranges for Minecraft's multi-noise biome generation system.
15+
16+
Attributes:
17+
temperature: Controls hot/cold climate preference (-1.0 to 1.0)
18+
humidity: Controls dry/wet climate preference (-1.0 to 1.0)
19+
altitude: Controls low/high terrain preference (affects hills/valleys)
20+
weirdness: Controls terrain "strangeness" (also known as "ridges", -1.0 to 1.0)
21+
offset: Fine-tuning parameter for biome selection priority/weight
22+
"""
23+
24+
temperature: float = Field(ge=-1, le=1)
25+
humidity: float = Field(ge=-1, le=1)
26+
altitude: Literal[0] # not sure what the constraints here should be, minecraft-data only uses 0
27+
weirdness: float = Field(ge=-1, le=1)
28+
offset: float
29+
30+
31+
@final
32+
class BiomeData(MinecraftDataModel):
33+
"""Minecraft-Data about a Biome.
34+
35+
Attributes:
36+
id: The unique identifier for a biome
37+
name: The name of a biome
38+
category: Category to which this biome belongs to (e.g. "forest", "ocean", ...)
39+
temperature: The base temperature in a biome.
40+
precipitation: The type of precipitation (none, rain or snow) [before 1.19.4]
41+
has_precipitation: True if a biome has any precipitation (rain or snow) [1.19.4+]
42+
dimension: The dimension of a biome: overworld, nether or end (or the_end on bedrock)
43+
display_name: The display name of a biome
44+
color: The color in a biome
45+
rainfall: How much rain there is in a biome [before 1.19.4]
46+
depth: Depth corresponds approximately to the terrain height.
47+
climates: Climate data for the biome
48+
name_legacy: Legacy name of the biome used in older versions.
49+
parent: The name of the parent biome
50+
child: ID of a variant biome
51+
"""
52+
53+
id: int
54+
name: str
55+
category: str
56+
temperature: float = Field(ge=-1, le=2)
57+
precipitation: Literal["none", "rain", "snow"] | None = None
58+
# For some reason, this field actually uses snake_case, not camelCase
59+
has_precipitation: bool | None = Field(alias="has_precipitation", default=None)
60+
dimension: Literal["overworld", "nether", "end", "the_end"]
61+
display_name: str
62+
color: int
63+
rainfall: float | None = Field(ge=0, le=1, default=None)
64+
depth: float | None = Field(default=None)
65+
climates: list[BiomeClimateData] | None = Field(min_length=1, default=None)
66+
name_legacy: str | None = Field(alias="name_legacy", default=None) # also uses snake_case for some reason
67+
parent: str | None = None
68+
child: int | None = Field(ge=0, default=None)
69+
70+
@model_validator(mode="before")
71+
@classmethod
72+
def rename_has_percipitation(cls, data: dict[str, object]) -> dict[str, object]:
73+
"""Rename the typo field has_percipitation to has_precipitation.
74+
75+
This is a mistake in the minecraft-data dataset which is only present for a single
76+
minecraft version (bedrock 1.21.60), this function renames it back to standardize
77+
our data models.
78+
79+
This will get addressed with: https://github.com/PrismarineJS/minecraft-data/issues/1048
80+
after which this method can be removed.
81+
"""
82+
if "has_percipitation" not in data:
83+
return data
84+
85+
if "has_precipitation" in data:
86+
raise ValueError("Found biome with both has_percipitation and has_precipitation fields")
87+
88+
data["has_precipitation"] = data.pop("has_percipitation")
89+
return data

minebase/types/items.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from __future__ import annotations
2+
3+
from typing import final
4+
5+
from pydantic import Field, model_validator
6+
7+
from minebase.types.base import MinecraftDataModel
8+
9+
10+
@final
11+
class ItemVariationData(MinecraftDataModel):
12+
"""Minecraft-Data for an item variation.
13+
14+
Certain items have multiple variations that are distinguished through a metadata number.
15+
For example, coal (id 263) has a charcoal variation with metadata=1 (making it 263:1).
16+
These sub-items don't have their own entry in the items data, they're only tracked as
17+
variations of the parent item.
18+
19+
Attributes:
20+
metadata:
21+
The metadata number to distinguish this variation from the parent.
22+
23+
Each variation must have a metadata value.
24+
display_name:
25+
The item name as shown in the GUI.
26+
27+
Each variation must have it's own display name that differs from the parent item
28+
id:
29+
The unique identifier of the item.
30+
31+
Most variations don't have their own ID, and instead only contain metadata to
32+
distinguish them from the parent, however, some variations are given their own
33+
item ID, even though they're still only considered a variation of the parent.
34+
name:
35+
The minecraft name of an item (guaranteed to be unique).
36+
37+
Many variations aren't given a name and instead share the same item name with
38+
the parent item, and are distinguished only by metadata. However, some do have
39+
their own unique name, even though they're still only considered a variation
40+
of the parent.
41+
enchant_categories: Which enchant categories apply to this item variation
42+
stack_size: What is the stack size of this item variation
43+
44+
"""
45+
46+
metadata: int = Field(ge=0)
47+
display_name: str
48+
enchant_categories: list[str] | None = None
49+
stack_size: int | None = Field(ge=0, default=None)
50+
id: int | None = Field(ge=0, default=None)
51+
name: str | None = None
52+
53+
54+
@final
55+
class ItemsData(MinecraftDataModel):
56+
"""Minecraft-Data about an item.
57+
58+
Attributes:
59+
id: The unique identifier of the item
60+
name: The minecraft name of an item (guaranteed to be unique)
61+
display_name: The item name as shown in the GUI
62+
stack_size: The maximum amount that can be in a single stack for this item (usually 64)
63+
enchant_categories: Which enchant categories apply to this item
64+
repair_with: Items (item names) that this item can be combined with in an anvil for repair
65+
max_durability: The maximum amount of durability points for this item
66+
block_state_id: The unique identifier of the block that will be placed from this block item.
67+
variations: Variantions of this item (e.g. for coral, there's Tube Coral, Brain Coral, Bubble Coral, ...)
68+
metadata: Number used primarily to distinguish item variations (e.g. tall grass 150:1 vs fern 150:2)
69+
"""
70+
71+
id: int = Field(ge=0)
72+
name: str
73+
display_name: str
74+
stack_size: int = Field(ge=0)
75+
enchant_categories: list[str] | None = None
76+
repair_with: list[str] | None = None
77+
max_durability: int | None = Field(ge=0, default=None)
78+
variations: list[ItemVariationData] | None = None
79+
block_state_id: int | None = Field(ge=0, default=None)
80+
metadata: int | None = Field(ge=0, default=None)
81+
82+
@model_validator(mode="before")
83+
@classmethod
84+
def strip_durability(cls, data: dict[str, object]) -> dict[str, object]:
85+
"""Remove the redundant `durability` field, if present.
86+
87+
The minecraft-data dataset includes both `max_durability` and `durability`, however, these fields
88+
always match, since this is the data for new items only. This makes the durability field entirely
89+
redundant; strip it.
90+
91+
This will get addressed with: https://github.com/PrismarineJS/minecraft-data/pull/1052
92+
after which this method can be removed.
93+
"""
94+
if "durability" not in data:
95+
return data
96+
97+
if "maxDurability" not in data:
98+
raise ValueError("Found durability field without max_durability")
99+
100+
if data["durability"] != data["maxDurability"]:
101+
raise ValueError("The durability field doesn't match max_durability")
102+
103+
del data["durability"]
104+
return data
105+
106+
@model_validator(mode="before")
107+
@classmethod
108+
def rename_fixed_with(cls, data: dict[str, object]) -> dict[str, object]:
109+
"""Rename the `fixed_with` field to `repair_with`.
110+
111+
These fields mean the same thing, however, the minecraft-data dataset includes one
112+
single version (bedrock 1.17.10), where for some reason, the field name `fixed_with`
113+
is used instead of `repair_with`. For a simpler user-facing API, this renames that
114+
field back to `repair_with`.
115+
116+
This will get addressed with: https://github.com/PrismarineJS/minecraft-data/pull/1052
117+
after which this method can be removed.
118+
"""
119+
if "fixedWith" not in data:
120+
return data
121+
122+
if "repairWith" in data:
123+
raise ValueError("Found item with both fixed_with and repair_with field")
124+
125+
data["repairWith"] = data.pop("fixedWith")
126+
return data

minebase/types/mcdata.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from __future__ import annotations
2+
3+
from typing import final
4+
5+
from pydantic import Field
6+
7+
from minebase.types.attributes import MinecraftAttributeData
8+
from minebase.types.base import MinecraftDataModel
9+
from minebase.types.biomes import BiomeData
10+
from minebase.types.items import ItemsData
11+
from minebase.types.version import VersionData
12+
from minebase.types.windows import WindowData
13+
14+
15+
class BaseMinecraftData(MinecraftDataModel):
16+
biomes: list[BiomeData] | None = None
17+
items: list[ItemsData] | None = None
18+
version: VersionData
19+
attributes: list[MinecraftAttributeData] | None = None
20+
windows: list[WindowData] | None = None
21+
enchantments: list | None = None
22+
language: dict | None = None
23+
block_collision_shapes: dict | None = None
24+
instruments: list | None = None
25+
materials: dict | None = None
26+
entities: list | None = None
27+
effects: list | None = None
28+
recipes: dict | None = None
29+
entity_loot: list | None = None
30+
block_loot: list | None = None
31+
32+
33+
@final
34+
class PcMinecraftData(BaseMinecraftData):
35+
foods: list | None = None
36+
tints: dict | None = None
37+
map_icons: list | None = None
38+
sounds: list | None = None
39+
blocks: list
40+
protocol: dict
41+
particles: list | None = None
42+
protocol_comments: dict | None = None
43+
commands: dict | None = None
44+
login_packet: dict | None = None
45+
46+
47+
@final
48+
class BedrockMinecraftData(BaseMinecraftData):
49+
blocks: list | None = None
50+
protocol: dict | None = None
51+
steve: dict | None = None
52+
block_states: list | None = None
53+
blocks_b2j: dict | None = Field(alias="blocksB2J", default=None)
54+
blocks_j2b: dict | None = Field(alias="blocksJ2B", default=None)
55+
block_mappings: list | None = None

minebase/types/version.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from typing import Literal, final
5+
6+
from pydantic import Field
7+
8+
from minebase.types.base import MinecraftDataModel
9+
10+
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])")
11+
MAJOR_VERSION_RE = re.compile(r"[0-9]+\.[0-9]+[a-z]?")
12+
13+
14+
@final
15+
class VersionData(MinecraftDataModel):
16+
"""Minecraft-Data for a specific Minecraft version."""
17+
18+
version: int
19+
minecraft_version: str = Field(pattern=MC_VERSION_RE)
20+
major_version: str = Field(pattern=MAJOR_VERSION_RE)
21+
release_type: Literal["release", "snapshot"] | None = None

0 commit comments

Comments
 (0)