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)
Base class for a Page
with optional title and text.
If title and/or text is required, do not subclass this type.
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.
Base class for a Page
with optional title and text.
If title and/or text is required, do not subclass this type.
44class EmptyPage(Page, type="patchouli:empty", template_type="patchouli:page"): 45 draw_filler: bool = True
Base class for Patchouli page types.
See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
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.
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.
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.
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.
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.
Base class for Patchouli page types.
See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
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.
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
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
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.
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.
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
Base class for Patchouli page types.
See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
Equivalent of classmethod(property(...))
.
Use @classproperty
. Do not instantiate this class directly.
Equivalent of classmethod(property(...))
.
Use @classproperty
. Do not instantiate this class directly.
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}"
Key to look up this page in BookContext.book_links
, or None
if this page
has no anchor.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Base class for a Page
with optional title and text.
If title and/or text is required, do not subclass this type.
Base class for a Page
with optional title and text.
If title and/or text is required, do not subclass this type.
Base class for a Page
with optional title and text.
If title and/or text is required, do not subclass this type.
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.
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.
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.
25class TextPage(Page, type="patchouli:text"): 26 title: LocalizedStr | None = None 27 text: FormatTree
Base class for Patchouli page types.
See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
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.