Edit on GitHub

hexdoc.patchouli.page

 1__all__ = [
 2    "BlastingPage",
 3    "CampfireCookingPage",
 4    "CraftingPage",
 5    "EmptyPage",
 6    "EntityPage",
 7    "ImagePage",
 8    "LinkPage",
 9    "Multiblock",
10    "MultiblockPage",
11    "Page",
12    "PageWithText",
13    "PageWithTitle",
14    "QuestPage",
15    "RelationsPage",
16    "SmeltingPage",
17    "SmithingPage",
18    "SmokingPage",
19    "SpotlightPage",
20    "StonecuttingPage",
21    "TextPage",
22]
23
24from .abstract_pages import Page, PageWithText, PageWithTitle
25from .pages import (
26    BlastingPage,
27    CampfireCookingPage,
28    CraftingPage,
29    EmptyPage,
30    EntityPage,
31    ImagePage,
32    LinkPage,
33    Multiblock,
34    MultiblockPage,
35    QuestPage,
36    RelationsPage,
37    SmeltingPage,
38    SmithingPage,
39    SmokingPage,
40    SpotlightPage,
41    StonecuttingPage,
42    TextPage,
43)
class BlastingPage(hexdoc.patchouli.page.PageWithTitle, typing.Generic[~_T_Recipe]):
30class BlastingPage(PageWithDoubleRecipe[BlastingRecipe], type="patchouli:blasting"):
31    pass

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

class CampfireCookingPage(hexdoc.patchouli.page.PageWithTitle, typing.Generic[~_T_Recipe]):
34class CampfireCookingPage(
35    PageWithDoubleRecipe[CampfireCookingRecipe], type="patchouli:campfire_cooking"
36):
37    pass

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

class CraftingPage(hexdoc.patchouli.page.PageWithTitle, typing.Generic[~_T_Recipe]):
40class CraftingPage(PageWithDoubleRecipe[CraftingRecipe], type="patchouli:crafting"):
41    pass

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

class EmptyPage(hexdoc.patchouli.page.Page):
44class EmptyPage(Page, type="patchouli:empty", template_type="patchouli:page"):
45    draw_filler: bool = True
draw_filler: bool
def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class EntityPage(hexdoc.patchouli.page.PageWithText):
48class EntityPage(PageWithText, type="patchouli:entity"):
49    entity: Entity
50    scale: float = 1
51    offset: float = 0
52    rotate: bool = True
53    default_rotation: float = -45
54    name: LocalizedStr | None = None

Base class for a Page with optional text.

If text is required, do not subclass this type.

scale: float
offset: float
rotate: bool
default_rotation: float
def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class ImagePage(hexdoc.patchouli.page.PageWithTitle):
57class ImagePage(PageWithTitle, type="patchouli:image"):
58    images: list[Texture]
59    border: bool = False
60
61    @property
62    def images_with_alt(self):
63        for image in self.images:
64            if self.title:
65                yield image, self.title
66            else:
67                yield image, str(image)

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

border: bool
images_with_alt
61    @property
62    def images_with_alt(self):
63        for image in self.images:
64            if self.title:
65                yield image, self.title
66            else:
67                yield image, str(image)
def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class LinkPage(hexdoc.patchouli.page.TextPage):
70class LinkPage(TextPage, type="patchouli:link"):
71    url: str
72    link_text: LocalizedStr
url: str
def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class Multiblock(hexdoc.model.base.HexdocModel):
 75class Multiblock(HexdocModel):
 76    """https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/multiblocks/"""
 77
 78    mapping: dict[str, ItemWithTexture | TagWithTexture]
 79    pattern: list[list[str]]
 80    symmetrical: bool = False
 81    offset: tuple[int, int, int] | None = None
 82
 83    def bill_of_materials(self):
 84        character_counts = defaultdict[str, int](int)
 85
 86        for layer in self.pattern:
 87            for row in layer:
 88                for character in row:
 89                    match character:
 90                        case str() if character in self.mapping:
 91                            character_counts[character] += 1
 92                        case " " | "0":  # air
 93                            pass
 94                        case _:
 95                            raise ValueError(
 96                                f"Character not found in multiblock mapping: `{character}`"
 97                            )
 98
 99        materials = [
100            (self.mapping[character], count)
101            for character, count in character_counts.items()
102        ]
103
104        # sort by descending count, break ties by ascending name
105        materials.sort(key=lambda v: v[0].name.value)
106        materials.sort(key=lambda v: v[1], reverse=True)
107
108        return materials
109
110    @field_validator("mapping", mode="after")
111    @classmethod
112    def _add_default_mapping(
113        cls,
114        mapping: dict[str, ItemWithTexture | TagWithTexture],
115        info: ValidationInfo,
116    ):
117        i18n = I18n.of(info)
118        return {
119            "_": ItemWithTexture(
120                id=ItemStack("hexdoc", "any"),
121                name=i18n.localize("hexdoc.any_block"),
122                texture=PNGTexture.load_id(
123                    ResourceLocation("hexdoc", "textures/gui/any_block.png"),
124                    context=info,
125                ),
126            ),
127        } | mapping
pattern: list[list[str]]
symmetrical: bool
offset: tuple[int, int, int] | None
def bill_of_materials(self):
 83    def bill_of_materials(self):
 84        character_counts = defaultdict[str, int](int)
 85
 86        for layer in self.pattern:
 87            for row in layer:
 88                for character in row:
 89                    match character:
 90                        case str() if character in self.mapping:
 91                            character_counts[character] += 1
 92                        case " " | "0":  # air
 93                            pass
 94                        case _:
 95                            raise ValueError(
 96                                f"Character not found in multiblock mapping: `{character}`"
 97                            )
 98
 99        materials = [
100            (self.mapping[character], count)
101            for character, count in character_counts.items()
102        ]
103
104        # sort by descending count, break ties by ascending name
105        materials.sort(key=lambda v: v[0].name.value)
106        materials.sort(key=lambda v: v[1], reverse=True)
107
108        return materials
class MultiblockPage(hexdoc.patchouli.page.PageWithText):
130class MultiblockPage(PageWithText, type="patchouli:multiblock"):
131    name: LocalizedStr
132    multiblock_id: ResourceLocation | None = None
133    multiblock: Multiblock | None = None
134    enable_visualize: bool = True
135
136    @model_validator(mode="after")
137    def _check_multiblock(self) -> Self:
138        if self.multiblock_id is None and self.multiblock is None:
139            raise ValueError(f"One of multiblock_id or multiblock must be set\n{self}")
140        return self

Base class for a Page with optional text.

If text is required, do not subclass this type.

multiblock_id: hexdoc.core.ResourceLocation | None
multiblock: Multiblock | None
enable_visualize: bool
def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class Page(hexdoc.model.tagged_union.TypeTaggedTemplate, hexdoc.patchouli.utils.AdvancementSpoilered):
19class Page(TypeTaggedTemplate, AdvancementSpoilered, type=None):
20    """Base class for Patchouli page types.
21
22    See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
23    """
24
25    advancement: ResourceLocation | None = None
26    flag: str | None = None
27    anchor: str | None = None
28
29    def __init_subclass__(
30        cls,
31        *,
32        type: str | InheritType | None = Inherit,
33        template_type: str | None = None,
34        **kwargs: Unpack[ConfigDict],
35    ) -> None:
36        super().__init_subclass__(type=type, template_type=template_type, **kwargs)
37
38    @classproperty
39    @classmethod
40    def type(cls) -> ResourceLocation | None:
41        assert cls._type is not NoValue
42        return cls._type
43
44    @model_validator(mode="wrap")
45    @classmethod
46    def _pre_root(cls, value: str | Any, handler: ModelWrapValidatorHandler[Self]):
47        match value:
48            case str(text):
49                # treat a plain string as a text page
50                value = {"type": "patchouli:text", "text": text}
51            case {"type": str(raw_type)} if ":" not in raw_type:
52                # default to the patchouli namespace if not specified
53                # see: https://github.com/VazkiiMods/Patchouli/blob/b87e91a5a08d/Xplat/src/main/java/vazkii/patchouli/client/book/ClientBookRegistry.java#L110
54                value["type"] = f"patchouli:{raw_type}"
55            case _:
56                pass
57        return handler(value)
58
59    @classproperty
60    @classmethod
61    def template(cls) -> str:
62        return cls.template_id.template_path("pages")
63
64    def book_link_key(self, entry_key: str):
65        """Key to look up this page in `BookContext.book_links`, or `None` if this page
66        has no anchor."""
67        if self.anchor is not None:
68            return f"{entry_key}#{self.anchor}"
69
70    def fragment(self, entry_fragment: str):
71        """URL fragment for this page in `BookContext.book_links`, or `None` if this
72        page has no anchor."""
73        if self.anchor is not None:
74            return f"{entry_fragment}@{self.anchor}"
75
76    def redirect_path(self, entry_path: str):
77        """Path to this page when generating redirect pages, or `None` if this page has
78        no anchor."""
79        if self.anchor is not None:
80            return f"{entry_path}/{self.anchor}"
81
82    def _get_advancement(self):
83        # implements AdvancementSpoilered
84        return self.advancement
advancement: hexdoc.core.ResourceLocation | None
flag: str | None
anchor: str | None
def type(unknown):

Equivalent of classmethod(property(...)).

Use @classproperty. Do not instantiate this class directly.

def template(unknown):

Equivalent of classmethod(property(...)).

Use @classproperty. Do not instantiate this class directly.

def fragment(self, entry_fragment: str):
70    def fragment(self, entry_fragment: str):
71        """URL fragment for this page in `BookContext.book_links`, or `None` if this
72        page has no anchor."""
73        if self.anchor is not None:
74            return f"{entry_fragment}@{self.anchor}"

URL fragment for this page in BookContext.book_links, or None if this page has no anchor.

def redirect_path(self, entry_path: str):
76    def redirect_path(self, entry_path: str):
77        """Path to this page when generating redirect pages, or `None` if this page has
78        no anchor."""
79        if self.anchor is not None:
80            return f"{entry_path}/{self.anchor}"

Path to this page when generating redirect pages, or None if this page has no anchor.

def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class PageWithText(hexdoc.patchouli.page.Page):
87class PageWithText(Page, type=None):
88    """Base class for a `Page` with optional text.
89
90    If text is required, do not subclass this type.
91    """
92
93    text: FormatTree | None = None

Base class for a Page with optional text.

If text is required, do not subclass this type.

def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class PageWithTitle(hexdoc.patchouli.page.PageWithText):
 96class PageWithTitle(PageWithText, type=None):
 97    """Base class for a `Page` with optional title and text.
 98
 99    If title and/or text is required, do not subclass this type.
100    """
101
102    title: LocalizedStr | None = None

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class QuestPage(hexdoc.patchouli.page.PageWithText):
143class QuestPage(PageWithText, type="patchouli:quest"):
144    trigger: ResourceLocation | None = None
145    title: LocalizedStr = LocalizedStr.with_value("Objective")

Base class for a Page with optional text.

If text is required, do not subclass this type.

def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class RelationsPage(hexdoc.patchouli.page.PageWithText):
148class RelationsPage(PageWithText, type="patchouli:relations"):
149    entries: list[ResourceLocation]
150    title: LocalizedStr = LocalizedStr.with_value("Related Chapters")

Base class for a Page with optional text.

If text is required, do not subclass this type.

def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class SmeltingPage(hexdoc.patchouli.page.PageWithTitle, typing.Generic[~_T_Recipe]):
153class SmeltingPage(PageWithDoubleRecipe[SmeltingRecipe], type="patchouli:smelting"):
154    pass

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

class SmithingPage(hexdoc.patchouli.page.PageWithTitle, typing.Generic[~_T_Recipe]):
157class SmithingPage(PageWithDoubleRecipe[SmithingRecipe], type="patchouli:smithing"):
158    pass

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

class SmokingPage(hexdoc.patchouli.page.PageWithTitle, typing.Generic[~_T_Recipe]):
161class SmokingPage(PageWithDoubleRecipe[SmokingRecipe], type="patchouli:smoking"):
162    pass

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

class SpotlightPage(hexdoc.patchouli.page.PageWithText):
171class SpotlightPage(PageWithText, type="patchouli:spotlight"):
172    title_field: LocalizedStr | None = Field(default=None, alias="title")
173    item: ItemWithTexture
174    link_recipe: bool = False
175
176    @property
177    def title(self) -> LocalizedStr | None:
178        return self.title_field or self.item.name

Base class for a Page with optional text.

If text is required, do not subclass this type.

title_field: hexdoc.minecraft.LocalizedStr | None
title: hexdoc.minecraft.LocalizedStr | None
176    @property
177    def title(self) -> LocalizedStr | None:
178        return self.title_field or self.item.name
def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.

class StonecuttingPage(hexdoc.patchouli.page.PageWithTitle, typing.Generic[~_T_Recipe]):
165class StonecuttingPage(
166    PageWithDoubleRecipe[StonecuttingRecipe], type="patchouli:stonecutting"
167):
168    pass

Base class for a Page with optional title and text.

If title and/or text is required, do not subclass this type.

class TextPage(hexdoc.patchouli.page.Page):
25class TextPage(Page, type="patchouli:text"):
26    title: LocalizedStr | None = None
27    text: FormatTree
def model_post_init(self: pydantic.main.BaseModel, context: Any, /) -> None:
337def init_private_attributes(self: BaseModel, context: Any, /) -> None:
338    """This function is meant to behave like a BaseModel method to initialise private attributes.
339
340    It takes context as an argument since that's what pydantic-core passes when calling it.
341
342    Args:
343        self: The BaseModel instance.
344        context: The context.
345    """
346    if getattr(self, '__pydantic_private__', None) is None:
347        pydantic_private = {}
348        for name, private_attr in self.__private_attributes__.items():
349            default = private_attr.get_default()
350            if default is not PydanticUndefined:
351                pydantic_private[name] = default
352        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialise private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args: self: The BaseModel instance. context: The context.