Edit on GitHub

hexdoc.minecraft.assets

 1__all__ = [
 2    "AnimatedTexture",
 3    "AnimationMeta",
 4    "HexdocAssetLoader",
 5    "ImageTexture",
 6    "ItemTexture",
 7    "ItemWithTexture",
 8    "ModelItem",
 9    "MultiItemTexture",
10    "NamedTexture",
11    "PNGTexture",
12    "SingleItemTexture",
13    "TagWithTexture",
14    "Texture",
15    "TextureContext",
16    "TextureLookup",
17    "TextureLookups",
18    "validate_texture",
19]
20
21from .animated import (
22    AnimatedTexture,
23    AnimationMeta,
24)
25from .items import (
26    ImageTexture,
27    ItemTexture,
28    MultiItemTexture,
29    SingleItemTexture,
30)
31from .load_assets import (
32    HexdocAssetLoader,
33    Texture,
34    validate_texture,
35)
36from .models import ModelItem
37from .textures import (
38    PNGTexture,
39    TextureContext,
40    TextureLookup,
41    TextureLookups,
42)
43from .with_texture import (
44    ItemWithTexture,
45    NamedTexture,
46    TagWithTexture,
47)
48
49HexdocPythonResourceLoader = None
50"""PLACEHOLDER - DO NOT USE
51
52This class has been removed from hexdoc, but this variable is required to fix an import
53error with old versions of `hexdoc_minecraft`.
54"""
class AnimatedTexture(hexdoc.minecraft.assets.textures.BaseTexture):
51class AnimatedTexture(BaseTexture):
52    url: PydanticURL | None
53    pixelated: bool
54    css_class: str
55    meta: AnimationMeta
56
57    @classmethod
58    def from_url(cls, *args: Any, **kwargs: Any) -> Self:
59        raise NotImplementedError("AnimatedTexture does not support from_url()")
60
61    @property
62    def time_seconds(self):
63        return self.time / 20
64
65    @cached_property
66    def time(self):
67        return sum(time for _, time in self._normalized_frames)
68
69    @property
70    def frames(self):
71        start = 0
72        for index, time in self._normalized_frames:
73            yield AnimatedTextureFrame(
74                index=index,
75                start=start,
76                time=time,
77                animation_time=self.time,
78            )
79            start += time
80
81    @property
82    def _normalized_frames(self):
83        """index, time"""
84        animation = self.meta.animation
85
86        for i, frame in enumerate(animation.frames):
87            match frame:
88                case int(index):
89                    time = None
90                case AnimationMetaFrame(index=index, time=time):
91                    pass
92
93            if index is None:
94                index = i
95            if time is None:
96                time = animation.frametime
97
98            yield index, time

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

url: Optional[Annotated[yarl.URL, GetPydanticSchema(get_pydantic_core_schema=<function <lambda> at 0x7f29b2e59760>, get_pydantic_json_schema=None)]]
pixelated: bool
css_class: str
@classmethod
def from_url(cls, *args: Any, **kwargs: Any) -> Self:
57    @classmethod
58    def from_url(cls, *args: Any, **kwargs: Any) -> Self:
59        raise NotImplementedError("AnimatedTexture does not support from_url()")
time_seconds
61    @property
62    def time_seconds(self):
63        return self.time / 20
time
65    @cached_property
66    def time(self):
67        return sum(time for _, time in self._normalized_frames)
frames
69    @property
70    def frames(self):
71        start = 0
72        for index, time in self._normalized_frames:
73            yield AnimatedTextureFrame(
74                index=index,
75                start=start,
76                time=time,
77                animation_time=self.time,
78            )
79            start += time
class AnimationMeta(hexdoc.model.base.HexdocModel):
26class AnimationMeta(HexdocModel):
27    animation: AnimationMetaTag

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

animation: hexdoc.minecraft.assets.animated.AnimationMetaTag
@dataclass(kw_only=True)
class HexdocAssetLoader:
 60@dataclass(kw_only=True)
 61class HexdocAssetLoader:
 62    loader: ModResourceLoader
 63    site_url: PydanticURL
 64    asset_url: PydanticURL
 65    render_dir: Path
 66
 67    @cached_property
 68    def gaslighting_items(self):
 69        return Tag.GASLIGHTING_ITEMS.load(self.loader).value_ids_set
 70
 71    @property
 72    def texture_props(self):
 73        return self.loader.props.textures
 74
 75    def can_be_missing(self, id: ResourceLocation):
 76        if self.texture_props.missing == "*":
 77            return True
 78        return any(id.match(pattern) for pattern in self.texture_props.missing)
 79
 80    def get_override(
 81        self,
 82        id: ResourceLocation,
 83        image_textures: dict[ResourceLocation, ImageTexture],
 84    ) -> Texture | None:
 85        match self.texture_props.override.get(id):
 86            case PNGTextureOverride(url=url, pixelated=pixelated):
 87                return PNGTexture(url=url, pixelated=pixelated)
 88            case TextureTextureOverride(texture=texture):
 89                return image_textures[texture]
 90            case None:
 91                return None
 92
 93    def find_image_textures(
 94        self,
 95    ) -> Iterable[tuple[ResourceLocation, Path | ImageTexture]]:
 96        for resource_dir, texture_id, path in self.loader.find_resources(
 97            "assets",
 98            namespace="*",
 99            folder="textures",
100            glob="**/*.png",
101            internal_only=True,
102            allow_missing=True,
103        ):
104            if resource_dir:
105                self.loader.export_raw(
106                    path=path.relative_to(resource_dir.path),
107                    data=path.read_bytes(),
108                )
109            yield texture_id, path
110
111    def load_item_models(self) -> Iterable[tuple[ResourceLocation, ModelItem]]:
112        for _, item_id, data in self.loader.load_resources(
113            "assets",
114            namespace="*",
115            folder="models/item",
116            internal_only=True,
117            allow_missing=True,
118        ):
119            model = ModelItem.load_data("item" / item_id, data)
120            yield item_id, model
121
122    @cached_property
123    def renderer(self):
124        return BlockRenderer(
125            loader=self.loader,
126            output_dir=self.render_dir,
127        )
128
129    def fallback_texture(self, item_id: ResourceLocation) -> ItemTexture | None:
130        return None
131
132    def load_and_render_internal_textures(
133        self,
134        image_textures: dict[ResourceLocation, ImageTexture],
135    ) -> Iterator[tuple[ResourceLocation, Texture]]:
136        """For all item/block models in all internal resource dirs, yields the item id
137        (eg. `hexcasting:focus`) and some kind of texture that we can use in the book."""
138
139        # images
140        for texture_id, value in self.find_image_textures():
141            if not texture_id.path.startswith("textures"):
142                texture_id = "textures" / texture_id
143
144            match value:
145                case Path() as path:
146                    texture = load_texture(
147                        texture_id,
148                        path=path,
149                        repo_root=self.loader.props.repo_root,
150                        asset_url=self.asset_url,
151                        strict=self.texture_props.strict,
152                    )
153
154                case PNGTexture() | AnimatedTexture() as texture:
155                    pass
156
157            image_textures[texture_id] = texture
158            yield texture_id, texture
159
160        found_items_from_models = set[ResourceLocation]()
161        missing_items = set[ResourceLocation]()
162
163        missing_item_texture = SingleItemTexture.from_url(
164            MISSING_TEXTURE_URL, pixelated=True
165        )
166
167        # items
168        for item_id, model in self.load_item_models():
169            if result := self.get_override(item_id, image_textures):
170                yield item_id, result
171            elif result := load_and_render_item(
172                model,
173                self.loader,
174                self.renderer,
175                self.gaslighting_items,
176                image_textures,
177                self.site_url,
178            ):
179                found_items_from_models.add(item_id)
180                yield item_id, result
181            else:
182                missing_items.add(item_id)
183
184        for item_id in list(missing_items):
185            if result := self.fallback_texture(item_id):
186                logger.warning(f"Using fallback texture for item: {item_id}")
187            elif self.can_be_missing(item_id):
188                logger.warning(f"Using missing texture for item: {item_id}")
189                result = missing_item_texture
190            else:
191                continue
192            missing_items.remove(item_id)
193            yield item_id, result
194
195        # oopsies
196        if missing_items:
197            raise FileNotFoundError(
198                "Failed to find a texture for some items: "
199                + ", ".join(sorted(str(item) for item in missing_items))
200            )
HexdocAssetLoader( *, loader: hexdoc.core.ModResourceLoader, site_url: typing.Annotated[yarl.URL, GetPydanticSchema(get_pydantic_core_schema=<function <lambda>>, get_pydantic_json_schema=None)], asset_url: typing.Annotated[yarl.URL, GetPydanticSchema(get_pydantic_core_schema=<function <lambda>>, get_pydantic_json_schema=None)], render_dir: pathlib.Path)
site_url: typing.Annotated[yarl.URL, GetPydanticSchema(get_pydantic_core_schema=<function <lambda> at 0x7f29b2e59760>, get_pydantic_json_schema=None)]
asset_url: typing.Annotated[yarl.URL, GetPydanticSchema(get_pydantic_core_schema=<function <lambda> at 0x7f29b2e59760>, get_pydantic_json_schema=None)]
render_dir: pathlib.Path
gaslighting_items
67    @cached_property
68    def gaslighting_items(self):
69        return Tag.GASLIGHTING_ITEMS.load(self.loader).value_ids_set
texture_props
71    @property
72    def texture_props(self):
73        return self.loader.props.textures
def can_be_missing(self, id: hexdoc.core.ResourceLocation):
75    def can_be_missing(self, id: ResourceLocation):
76        if self.texture_props.missing == "*":
77            return True
78        return any(id.match(pattern) for pattern in self.texture_props.missing)
def get_override( self, id: hexdoc.core.ResourceLocation, image_textures: dict[hexdoc.core.ResourceLocation, PNGTexture | AnimatedTexture]) -> PNGTexture | AnimatedTexture | SingleItemTexture | MultiItemTexture | None:
80    def get_override(
81        self,
82        id: ResourceLocation,
83        image_textures: dict[ResourceLocation, ImageTexture],
84    ) -> Texture | None:
85        match self.texture_props.override.get(id):
86            case PNGTextureOverride(url=url, pixelated=pixelated):
87                return PNGTexture(url=url, pixelated=pixelated)
88            case TextureTextureOverride(texture=texture):
89                return image_textures[texture]
90            case None:
91                return None
def find_image_textures( self) -> Iterable[tuple[hexdoc.core.ResourceLocation, pathlib.Path | PNGTexture | AnimatedTexture]]:
 93    def find_image_textures(
 94        self,
 95    ) -> Iterable[tuple[ResourceLocation, Path | ImageTexture]]:
 96        for resource_dir, texture_id, path in self.loader.find_resources(
 97            "assets",
 98            namespace="*",
 99            folder="textures",
100            glob="**/*.png",
101            internal_only=True,
102            allow_missing=True,
103        ):
104            if resource_dir:
105                self.loader.export_raw(
106                    path=path.relative_to(resource_dir.path),
107                    data=path.read_bytes(),
108                )
109            yield texture_id, path
def load_item_models( self) -> Iterable[tuple[hexdoc.core.ResourceLocation, ModelItem]]:
111    def load_item_models(self) -> Iterable[tuple[ResourceLocation, ModelItem]]:
112        for _, item_id, data in self.loader.load_resources(
113            "assets",
114            namespace="*",
115            folder="models/item",
116            internal_only=True,
117            allow_missing=True,
118        ):
119            model = ModelItem.load_data("item" / item_id, data)
120            yield item_id, model
renderer
122    @cached_property
123    def renderer(self):
124        return BlockRenderer(
125            loader=self.loader,
126            output_dir=self.render_dir,
127        )
def fallback_texture( self, item_id: hexdoc.core.ResourceLocation) -> SingleItemTexture | MultiItemTexture | None:
129    def fallback_texture(self, item_id: ResourceLocation) -> ItemTexture | None:
130        return None
def load_and_render_internal_textures( self, image_textures: dict[hexdoc.core.ResourceLocation, PNGTexture | AnimatedTexture]) -> Iterator[tuple[hexdoc.core.ResourceLocation, PNGTexture | AnimatedTexture | SingleItemTexture | MultiItemTexture]]:
132    def load_and_render_internal_textures(
133        self,
134        image_textures: dict[ResourceLocation, ImageTexture],
135    ) -> Iterator[tuple[ResourceLocation, Texture]]:
136        """For all item/block models in all internal resource dirs, yields the item id
137        (eg. `hexcasting:focus`) and some kind of texture that we can use in the book."""
138
139        # images
140        for texture_id, value in self.find_image_textures():
141            if not texture_id.path.startswith("textures"):
142                texture_id = "textures" / texture_id
143
144            match value:
145                case Path() as path:
146                    texture = load_texture(
147                        texture_id,
148                        path=path,
149                        repo_root=self.loader.props.repo_root,
150                        asset_url=self.asset_url,
151                        strict=self.texture_props.strict,
152                    )
153
154                case PNGTexture() | AnimatedTexture() as texture:
155                    pass
156
157            image_textures[texture_id] = texture
158            yield texture_id, texture
159
160        found_items_from_models = set[ResourceLocation]()
161        missing_items = set[ResourceLocation]()
162
163        missing_item_texture = SingleItemTexture.from_url(
164            MISSING_TEXTURE_URL, pixelated=True
165        )
166
167        # items
168        for item_id, model in self.load_item_models():
169            if result := self.get_override(item_id, image_textures):
170                yield item_id, result
171            elif result := load_and_render_item(
172                model,
173                self.loader,
174                self.renderer,
175                self.gaslighting_items,
176                image_textures,
177                self.site_url,
178            ):
179                found_items_from_models.add(item_id)
180                yield item_id, result
181            else:
182                missing_items.add(item_id)
183
184        for item_id in list(missing_items):
185            if result := self.fallback_texture(item_id):
186                logger.warning(f"Using fallback texture for item: {item_id}")
187            elif self.can_be_missing(item_id):
188                logger.warning(f"Using missing texture for item: {item_id}")
189                result = missing_item_texture
190            else:
191                continue
192            missing_items.remove(item_id)
193            yield item_id, result
194
195        # oopsies
196        if missing_items:
197            raise FileNotFoundError(
198                "Failed to find a texture for some items: "
199                + ", ".join(sorted(str(item) for item in missing_items))
200            )

For all item/block models in all internal resource dirs, yields the item id (eg. hexcasting:focus) and some kind of texture that we can use in the book.

ImageTexture = PNGTexture | AnimatedTexture
class ItemWithTexture(hexdoc.model.base.HexdocModel, typing.Generic[~_T_BaseResourceLocation, ~_T_Texture]):
67class ItemWithTexture(InlineItemModel, BaseWithTexture[ItemStack, ItemTexture]):
68    @classmethod
69    def load_id(cls, item: ItemStack, context: ContextSource):
70        """Implements InlineModel."""
71
72        i18n = I18n.of(context)
73        if (name := item.get_name()) is not None:
74            pass
75        elif item.path.startswith("texture"):
76            name = i18n.localize_texture(item.id)
77        else:
78            name = i18n.localize_item(item)
79
80        return {
81            "id": item,
82            "name": name,
83            "texture": item.id,  # TODO: fix InlineModel (ItemTexture), then remove .id
84        }

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

@classmethod
def load_id( cls, item: hexdoc.core.ItemStack, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context):
68    @classmethod
69    def load_id(cls, item: ItemStack, context: ContextSource):
70        """Implements InlineModel."""
71
72        i18n = I18n.of(context)
73        if (name := item.get_name()) is not None:
74            pass
75        elif item.path.startswith("texture"):
76            name = i18n.localize_texture(item.id)
77        else:
78            name = i18n.localize_item(item)
79
80        return {
81            "id": item,
82            "name": name,
83            "texture": item.id,  # TODO: fix InlineModel (ItemTexture), then remove .id
84        }

Implements InlineModel.

class ModelItem(hexdoc.model.base.HexdocModel):
 47class ModelItem(HexdocModel, extra="allow"):
 48    """https://minecraft.wiki/w/Tutorials/Models#Item_models
 49
 50    This is called BaseModelItem instead of BaseItemModel because SomethingModel is our
 51    naming convention for abstract Pydantic models.
 52    """
 53
 54    id: ResourceLocation
 55    """Not in the actual file."""
 56
 57    parent: ResourceLocation | None = None
 58    """Loads a different model with the given id."""
 59    display: dict[ItemDisplayPosition, ItemDisplay] | None = None
 60    gui_light: Literal["front", "side"] = "side"
 61    overrides: list[ModelOverride] | None = None
 62    # TODO: minecraft_render would need to support this
 63    elements: Any | None = None
 64    # TODO: support texture variables etc?
 65    textures: dict[str, ResourceLocation] | None = None
 66    """Texture ids. For example, `{"layer0": "item/red_bed"}` refers to the resource
 67    `assets/minecraft/textures/item/red_bed.png`.
 68
 69    Technically this is only allowed for `minecraft:item/generated`, but we're currently
 70    not loading Minecraft's item models, so there's lots of other parent ids that this
 71    field can show up for.
 72    """
 73
 74    @classmethod
 75    def load_resource(cls, id: ResourceLocation, loader: ModResourceLoader):
 76        _, data = loader.load_resource("assets", "models", id, export=False)
 77        return cls.load_data(id, data)
 78
 79    @classmethod
 80    def load_data(cls, id: ResourceLocation, data: JSONDict):
 81        return cls.model_validate(data | {"id": id})
 82
 83    @property
 84    def item_id(self):
 85        if "/" not in self.id.path:
 86            return self.id
 87        path_without_prefix = "/".join(self.id.path.split("/")[1:])
 88        return self.id.with_path(path_without_prefix)
 89
 90    @property
 91    def layer0(self):
 92        if self.textures:
 93            return self.textures.get("layer0")
 94
 95    def find_texture(
 96        self,
 97        loader: ModResourceLoader,
 98        gaslighting_items: Set[ResourceLocation],
 99        checked_overrides: defaultdict[ResourceLocation, set[int]] | None = None,
100    ) -> FoundTexture | None:
101        """May return a texture **or** a model. Texture ids will start with `textures/`."""
102        if checked_overrides is None:
103            checked_overrides = defaultdict(set)
104
105        # gaslighting
106        # as of 0.11.1-7, all gaslighting item models are implemented with overrides
107        if self.item_id in gaslighting_items:
108            if not self.overrides:
109                raise ValueError(
110                    f"Model {self.id} for item {self.item_id} marked as gaslighting but"
111                    " does not have overrides"
112                )
113
114            gaslighting_textures = list[FoundNormalTexture]()
115            for i, override in enumerate(self.overrides):
116                match self._find_override_texture(
117                    i, override, loader, gaslighting_items, checked_overrides
118                ):
119                    case "gaslighting", _:
120                        raise ValueError(
121                            f"Model {self.id} for item {self.item_id} marked as"
122                            f" gaslighting but override {i} resolves to another gaslighting texture"
123                        )
124                    case None:
125                        break
126                    case result:
127                        gaslighting_textures.append(result)
128            else:
129                return "gaslighting", gaslighting_textures
130
131        # if it exists, the layer0 texture is *probably* representative
132        # TODO: impl multi-layer textures for Sam
133        if self.layer0:
134            texture_id = "textures" / self.layer0 + ".png"
135            return "texture", texture_id
136
137        # first resolvable override, if any
138        for i, override in enumerate(self.overrides or []):
139            if result := self._find_override_texture(
140                i, override, loader, gaslighting_items, checked_overrides
141            ):
142                return result
143
144        if self.parent and self.parent.path.startswith("block/"):
145            # try the parent id
146            # we only do this for blocks in the same namespace because most other
147            # parents are generic "base class"-type models which won't actually
148            # represent the item
149            if self.parent.namespace == self.id.namespace:
150                return "block_model", self.parent
151
152            # FIXME: hack
153            # this entire selection process needs to be redone, but the idea here is to
154            # try rendering item models as blocks in certain cases (eg. edified button)
155            return "block_model", self.id
156
157        return None
158
159    def _find_override_texture(
160        self,
161        index: int,
162        override: ModelOverride,
163        loader: ModResourceLoader,
164        gaslighting_items: Set[ResourceLocation],
165        checked_overrides: defaultdict[ResourceLocation, set[int]],
166    ) -> FoundTexture | None:
167        if override.model.path.startswith("block/"):
168            return "block_model", override.model
169
170        if index in checked_overrides[self.id]:
171            logger.debug(f"Ignoring recursive override: {override.model}")
172            return None
173
174        checked_overrides[self.id].add(index)
175        return (
176            (ModelItem)
177            .load_resource(override.model, loader)
178            .find_texture(loader, gaslighting_items, checked_overrides)
179        )

https://minecraft.wiki/w/Tutorials/Models#Item_models

This is called BaseModelItem instead of BaseItemModel because SomethingModel is our naming convention for abstract Pydantic models.

Not in the actual file.

Loads a different model with the given id.

display: dict[typing.Literal['thirdperson_righthand', 'thirdperson_lefthand', 'firstperson_righthand', 'firstperson_lefthand', 'gui', 'head', 'ground', 'fixed'], hexdoc.minecraft.assets.models.ItemDisplay] | None
gui_light: Literal['front', 'side']
overrides: list[hexdoc.minecraft.assets.models.ModelOverride] | None
elements: typing.Any | None
textures: dict[str, hexdoc.core.ResourceLocation] | None

Texture ids. For example, {"layer0": "item/red_bed"} refers to the resource assets/minecraft/textures/item/red_bed.png.

Technically this is only allowed for minecraft:item/generated, but we're currently not loading Minecraft's item models, so there's lots of other parent ids that this field can show up for.

@classmethod
def load_resource( cls, id: hexdoc.core.ResourceLocation, loader: hexdoc.core.ModResourceLoader):
74    @classmethod
75    def load_resource(cls, id: ResourceLocation, loader: ModResourceLoader):
76        _, data = loader.load_resource("assets", "models", id, export=False)
77        return cls.load_data(id, data)
@classmethod
def load_data( cls, id: hexdoc.core.ResourceLocation, data: dict[str, JsonValue]):
79    @classmethod
80    def load_data(cls, id: ResourceLocation, data: JSONDict):
81        return cls.model_validate(data | {"id": id})
item_id
83    @property
84    def item_id(self):
85        if "/" not in self.id.path:
86            return self.id
87        path_without_prefix = "/".join(self.id.path.split("/")[1:])
88        return self.id.with_path(path_without_prefix)
layer0
90    @property
91    def layer0(self):
92        if self.textures:
93            return self.textures.get("layer0")
def find_texture( self, loader: hexdoc.core.ModResourceLoader, gaslighting_items: Set[hexdoc.core.ResourceLocation], checked_overrides: collections.defaultdict[hexdoc.core.ResourceLocation, set[int]] | None = None) -> tuple[typing.Literal['texture', 'block_model'], hexdoc.core.ResourceLocation] | tuple[typing.Literal['gaslighting'], list[tuple[typing.Literal['texture', 'block_model'], hexdoc.core.ResourceLocation]]] | None:
 95    def find_texture(
 96        self,
 97        loader: ModResourceLoader,
 98        gaslighting_items: Set[ResourceLocation],
 99        checked_overrides: defaultdict[ResourceLocation, set[int]] | None = None,
100    ) -> FoundTexture | None:
101        """May return a texture **or** a model. Texture ids will start with `textures/`."""
102        if checked_overrides is None:
103            checked_overrides = defaultdict(set)
104
105        # gaslighting
106        # as of 0.11.1-7, all gaslighting item models are implemented with overrides
107        if self.item_id in gaslighting_items:
108            if not self.overrides:
109                raise ValueError(
110                    f"Model {self.id} for item {self.item_id} marked as gaslighting but"
111                    " does not have overrides"
112                )
113
114            gaslighting_textures = list[FoundNormalTexture]()
115            for i, override in enumerate(self.overrides):
116                match self._find_override_texture(
117                    i, override, loader, gaslighting_items, checked_overrides
118                ):
119                    case "gaslighting", _:
120                        raise ValueError(
121                            f"Model {self.id} for item {self.item_id} marked as"
122                            f" gaslighting but override {i} resolves to another gaslighting texture"
123                        )
124                    case None:
125                        break
126                    case result:
127                        gaslighting_textures.append(result)
128            else:
129                return "gaslighting", gaslighting_textures
130
131        # if it exists, the layer0 texture is *probably* representative
132        # TODO: impl multi-layer textures for Sam
133        if self.layer0:
134            texture_id = "textures" / self.layer0 + ".png"
135            return "texture", texture_id
136
137        # first resolvable override, if any
138        for i, override in enumerate(self.overrides or []):
139            if result := self._find_override_texture(
140                i, override, loader, gaslighting_items, checked_overrides
141            ):
142                return result
143
144        if self.parent and self.parent.path.startswith("block/"):
145            # try the parent id
146            # we only do this for blocks in the same namespace because most other
147            # parents are generic "base class"-type models which won't actually
148            # represent the item
149            if self.parent.namespace == self.id.namespace:
150                return "block_model", self.parent
151
152            # FIXME: hack
153            # this entire selection process needs to be redone, but the idea here is to
154            # try rendering item models as blocks in certain cases (eg. edified button)
155            return "block_model", self.id
156
157        return None

May return a texture or a model. Texture ids will start with textures/.

class MultiItemTexture(hexdoc.minecraft.assets.textures.BaseTexture):
37class MultiItemTexture(BaseTexture):
38    inner: list[ImageTexture]
39    gaslighting: bool
40
41    @classmethod
42    def from_url(cls, url: URL, pixelated: bool) -> Self:
43        return cls(
44            inner=[PNGTexture(url=url, pixelated=pixelated)],
45            gaslighting=False,
46        )
47
48    @classmethod
49    def load_id(cls, id: ResourceLocation | ItemStack, context: ContextSource):
50        return super().load_id(id.id, context)

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

inner: list[PNGTexture | AnimatedTexture]
gaslighting: bool
@classmethod
def from_url(cls, url: yarl.URL, pixelated: bool) -> Self:
41    @classmethod
42    def from_url(cls, url: URL, pixelated: bool) -> Self:
43        return cls(
44            inner=[PNGTexture(url=url, pixelated=pixelated)],
45            gaslighting=False,
46        )
@classmethod
def load_id( cls, id: hexdoc.core.ResourceLocation | hexdoc.core.ItemStack, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context):
48    @classmethod
49    def load_id(cls, id: ResourceLocation | ItemStack, context: ContextSource):
50        return super().load_id(id.id, context)
class NamedTexture(hexdoc.model.base.HexdocModel, typing.Generic[~_T_BaseResourceLocation, ~_T_Texture]):
104class NamedTexture(InlineModel, BaseWithTexture[ResourceLocation, ImageTexture]):
105    @classmethod
106    def load_id(cls, id: ResourceLocation, context: ContextSource):
107        i18n = I18n.of(context)
108        return {
109            "id": id,
110            "name": i18n.localize_texture(id, silent=True),
111            "texture": id,
112        }

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

@classmethod
def load_id( cls, id: hexdoc.core.ResourceLocation, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context):
105    @classmethod
106    def load_id(cls, id: ResourceLocation, context: ContextSource):
107        i18n = I18n.of(context)
108        return {
109            "id": id,
110            "name": i18n.localize_texture(id, silent=True),
111            "texture": id,
112        }
class PNGTexture(hexdoc.minecraft.assets.textures.BaseTexture):
81class PNGTexture(BaseTexture):
82    url: PydanticURL | None
83    pixelated: bool
84
85    @classmethod
86    def from_url(cls, url: URL, pixelated: bool) -> Self:
87        return cls(url=url, pixelated=pixelated)

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

url: Optional[Annotated[yarl.URL, GetPydanticSchema(get_pydantic_core_schema=<function <lambda> at 0x7f29b2e59760>, get_pydantic_json_schema=None)]]
pixelated: bool
@classmethod
def from_url(cls, url: yarl.URL, pixelated: bool) -> Self:
85    @classmethod
86    def from_url(cls, url: URL, pixelated: bool) -> Self:
87        return cls(url=url, pixelated=pixelated)
class SingleItemTexture(hexdoc.minecraft.assets.textures.BaseTexture):
19class SingleItemTexture(BaseTexture):
20    inner: ImageTexture
21
22    @classmethod
23    def from_url(cls, url: URL, pixelated: bool) -> Self:
24        return cls(
25            inner=PNGTexture(url=url, pixelated=pixelated),
26        )
27
28    @classmethod
29    def load_id(cls, id: ResourceLocation | ItemStack, context: ContextSource):
30        return super().load_id(id.id, context)
31
32    @property
33    def url(self):
34        return self.inner.url

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

@classmethod
def from_url(cls, url: yarl.URL, pixelated: bool) -> Self:
22    @classmethod
23    def from_url(cls, url: URL, pixelated: bool) -> Self:
24        return cls(
25            inner=PNGTexture(url=url, pixelated=pixelated),
26        )
@classmethod
def load_id( cls, id: hexdoc.core.ResourceLocation | hexdoc.core.ItemStack, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context):
28    @classmethod
29    def load_id(cls, id: ResourceLocation | ItemStack, context: ContextSource):
30        return super().load_id(id.id, context)
url
32    @property
33    def url(self):
34        return self.inner.url
class TagWithTexture(hexdoc.model.base.HexdocModel, typing.Generic[~_T_BaseResourceLocation, ~_T_Texture]):
 87class TagWithTexture(InlineModel, BaseWithTexture[ResourceLocation, Texture]):
 88    @classmethod
 89    def load_id(cls, id: ResourceLocation, context: ContextSource):
 90        i18n = I18n.of(context)
 91        return cls(
 92            id=id,
 93            name=i18n.localize_item_tag(id),
 94            texture=PNGTexture.from_url(TAG_TEXTURE_URL, pixelated=True),
 95        )
 96
 97    @field_validator("id", mode="after")
 98    @classmethod
 99    def _validate_id(cls, id: ResourceLocation):
100        assert id.is_tag, f"Expected tag id, got {id}"
101        return id

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

@classmethod
def load_id( cls, id: hexdoc.core.ResourceLocation, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context):
88    @classmethod
89    def load_id(cls, id: ResourceLocation, context: ContextSource):
90        i18n = I18n.of(context)
91        return cls(
92            id=id,
93            name=i18n.localize_item_tag(id),
94            texture=PNGTexture.from_url(TAG_TEXTURE_URL, pixelated=True),
95        )
class TextureContext(hexdoc.model.base.ValidationContextModel):
102class TextureContext(ValidationContextModel):
103    textures: TextureLookups[Any]
104    allowed_missing_textures: set[ResourceLocation] | Literal["*"]

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

textures: collections.defaultdict[str, typing.Annotated[dict[hexdoc.core.ResourceLocation, typing.Annotated[typing.Any, SerializeAsAny()]], FieldInfo(annotation=NoneType, required=False, default_factory=dict)]]
allowed_missing_textures: Union[set[hexdoc.core.ResourceLocation], Literal['*']]
TextureLookup = dict[hexdoc.core.ResourceLocation, typing.Annotated[~_T_BaseTexture, SerializeAsAny()]]
TextureLookups = collections.defaultdict[str, typing.Annotated[dict[hexdoc.core.ResourceLocation, typing.Annotated[~_T_BaseTexture, SerializeAsAny()]], FieldInfo(annotation=NoneType, required=False, default_factory=dict)]]
def validate_texture( value: Any, *, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context, model_type: type[~_T_Texture] | typing.Any = PNGTexture | AnimatedTexture | SingleItemTexture | MultiItemTexture) -> ~_T_Texture:
41def validate_texture(
42    value: Any,
43    *,
44    context: ContextSource,
45    model_type: type[_T_Texture] | Any = Texture,
46) -> _T_Texture:
47    ta = TypeAdapter(model_type)
48    return ta.validate_python(
49        value,
50        context=cast(dict[str, Any], context),  # lie
51    )