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.
50class CampfireCookingPage( 51 PageWithDoubleRecipe[CampfireCookingRecipe], type="patchouli:campfire_cooking" 52): 53 pass
Base class for a Page
with optional title and text.
If title and/or text is required, do not subclass this type.
56class CraftingPage( 57 PageWithDoubleRecipeAccumulator[CraftingRecipe], type="patchouli:crafting" 58): 59 @classmethod 60 @override 61 def accumulator_type(cls): 62 return CraftingAccumulatorPage
Base class for a Page
with optional title and text.
If title and/or text is required, do not subclass this type.
71class EmptyPage(Page, type="patchouli:empty", template_type="patchouli:page"): 72 draw_filler: bool = True
Base class for Patchouli page types.
See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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 EntityPage(PageWithText, type="patchouli:entity"): 76 _entity_name: LocalizedStr = PrivateAttr() 77 _texture: PNGTexture = PrivateAttr() 78 79 entity: Entity 80 scale: float = 1 81 offset: float = 0 82 rotate: bool = True 83 default_rotation: float = -45 84 name_field: LocalizedStr | None = Field(default=None, serialization_alias="name") 85 86 @property 87 def entity_name(self): 88 return self._entity_name 89 90 @property 91 def name(self): 92 if self.name_field is None or not self.name_field.value: 93 return self._entity_name 94 return self.name_field 95 96 @property 97 def texture(self): 98 return self._texture 99 100 @model_validator(mode="after") 101 def _get_texture(self, info: ValidationInfo) -> Self: 102 # can't be on Entity's validator because it's frozen and 103 # causes circular references with the PNGTexture 104 assert info.context is not None 105 i18n = I18n.of(info) 106 self._entity_name = i18n.localize_entity(self.entity.id) 107 self._texture = PNGTexture.load_id( 108 id="textures/entities" / self.entity.id + ".png", context=info.context 109 ) 110 return self
Base class for a Page
with optional text.
If text is required, do not subclass this type.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
113class ImagePage(PageWithTitle, type="patchouli:image"): 114 images: list[Texture] 115 border: bool = False 116 117 @property 118 def images_with_alt(self): 119 for image in self.images: 120 if self.title: 121 yield image, self.title 122 else: 123 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.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
131class Multiblock(HexdocModel): 132 """https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/multiblocks/""" 133 134 mapping: dict[str, ItemWithTexture | TagWithTexture] 135 pattern: list[list[str]] 136 symmetrical: bool = False 137 offset: tuple[int, int, int] | None = None 138 139 def bill_of_materials(self): 140 character_counts = defaultdict[str, int](int) 141 142 for layer in self.pattern: 143 for row in layer: 144 for character in row: 145 match character: 146 case str() if character in self.mapping: 147 character_counts[character] += 1 148 case " " | "0": # air 149 pass 150 case _: 151 raise ValueError( 152 f"Character not found in multiblock mapping: `{character}`" 153 ) 154 155 materials = [ 156 (self.mapping[character], count) 157 for character, count in character_counts.items() 158 ] 159 160 # sort by descending count, break ties by ascending name 161 materials.sort(key=lambda v: v[0].name.value) 162 materials.sort(key=lambda v: v[1], reverse=True) 163 164 return materials 165 166 @field_validator("mapping", mode="after") 167 @classmethod 168 def _add_default_mapping( 169 cls, 170 mapping: dict[str, ItemWithTexture | TagWithTexture], 171 info: ValidationInfo, 172 ): 173 i18n = I18n.of(info) 174 return { 175 "_": ItemWithTexture( 176 id=ItemStack("hexdoc", "any"), 177 name=i18n.localize("hexdoc.any_block"), 178 texture=PNGTexture.load_id( 179 ResourceLocation("hexdoc", "textures/gui/any_block.png"), 180 context=info, 181 ), 182 ), 183 } | mapping
139 def bill_of_materials(self): 140 character_counts = defaultdict[str, int](int) 141 142 for layer in self.pattern: 143 for row in layer: 144 for character in row: 145 match character: 146 case str() if character in self.mapping: 147 character_counts[character] += 1 148 case " " | "0": # air 149 pass 150 case _: 151 raise ValueError( 152 f"Character not found in multiblock mapping: `{character}`" 153 ) 154 155 materials = [ 156 (self.mapping[character], count) 157 for character, count in character_counts.items() 158 ] 159 160 # sort by descending count, break ties by ascending name 161 materials.sort(key=lambda v: v[0].name.value) 162 materials.sort(key=lambda v: v[1], reverse=True) 163 164 return materials
186class MultiblockPage(PageWithText, type="patchouli:multiblock"): 187 name: LocalizedStr 188 multiblock_id: ResourceLocation | None = None 189 multiblock: Multiblock | None = None 190 enable_visualize: bool = True 191 192 @model_validator(mode="after") 193 def _check_multiblock(self) -> Self: 194 if self.multiblock_id is None and self.multiblock is None: 195 raise ValueError(f"One of multiblock_id or multiblock must be set\n{self}") 196 return self
Base class for a Page
with optional text.
If text is required, do not subclass this type.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
35class Page(TypeTaggedTemplate, AdvancementSpoilered, Flagged, type=None): 36 """Base class for Patchouli page types. 37 38 See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types 39 """ 40 41 advancement: ResourceLocation | None = None 42 anchor: str | None = None 43 44 def __init_subclass__( 45 cls, 46 *, 47 type: str | InheritType | None = Inherit, 48 template_type: str | None = None, 49 **kwargs: Unpack[ConfigDict], 50 ) -> None: 51 super().__init_subclass__(type=type, template_type=template_type, **kwargs) 52 53 @classproperty 54 @classmethod 55 def type(cls) -> ResourceLocation | None: 56 assert cls._type is not NoValue 57 return cls._type 58 59 @model_validator(mode="wrap") 60 @classmethod 61 def _pre_root(cls, value: str | Any, handler: ModelWrapValidatorHandler[Self]): 62 match value: 63 case str(text): 64 # treat a plain string as a text page 65 value = {"type": "patchouli:text", "text": text} 66 case {"type": str(raw_type)} if ":" not in raw_type: 67 # default to the patchouli namespace if not specified 68 # see: https://github.com/VazkiiMods/Patchouli/blob/b87e91a5a08d/Xplat/src/main/java/vazkii/patchouli/client/book/ClientBookRegistry.java#L110 69 value["type"] = f"patchouli:{raw_type}" 70 case _: 71 pass 72 return handler(value) 73 74 @classproperty 75 @classmethod 76 def template(cls) -> str: 77 return cls.template_id.template_path("pages") 78 79 def book_link_key(self, entry_key: str): 80 """Key to look up this page in `BookContext.book_links`, or `None` if this page 81 has no anchor.""" 82 if self.anchor is not None: 83 return f"{entry_key}#{self.anchor}" 84 85 def fragment(self, entry_fragment: str): 86 """URL fragment for this page in `BookContext.book_links`, or `None` if this 87 page has no anchor.""" 88 if self.anchor is not None: 89 return f"{entry_fragment}@{self.anchor}" 90 91 def redirect_path(self, entry_path: str): 92 """Path to this page when generating redirect pages, or `None` if this page has 93 no anchor.""" 94 if self.anchor is not None: 95 return f"{entry_path}/{self.anchor}" 96 97 def _get_advancement(self): 98 # implements AdvancementSpoilered 99 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.
79 def book_link_key(self, entry_key: str): 80 """Key to look up this page in `BookContext.book_links`, or `None` if this page 81 has no anchor.""" 82 if self.anchor is not None: 83 return f"{entry_key}#{self.anchor}"
Key to look up this page in BookContext.book_links
, or None
if this page
has no anchor.
85 def fragment(self, entry_fragment: str): 86 """URL fragment for this page in `BookContext.book_links`, or `None` if this 87 page has no anchor.""" 88 if self.anchor is not None: 89 return f"{entry_fragment}@{self.anchor}"
URL fragment for this page in BookContext.book_links
, or None
if this
page has no anchor.
91 def redirect_path(self, entry_path: str): 92 """Path to this page when generating redirect pages, or `None` if this page has 93 no anchor.""" 94 if self.anchor is not None: 95 return f"{entry_path}/{self.anchor}"
Path to this page when generating redirect pages, or None
if this page has
no anchor.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
102class PageWithText(Page, type=None): 103 """Base class for a `Page` with optional text. 104 105 If text is required, do not subclass this type. 106 """ 107 108 text: FormatTree | None = None
Base class for a Page
with optional text.
If text is required, do not subclass this type.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
111class PageWithTitle(PageWithText, type=None): 112 """Base class for a `Page` with optional title and text. 113 114 If title and/or text is required, do not subclass this type. 115 """ 116 117 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.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
199class QuestPage(PageWithText, type="patchouli:quest"): 200 trigger: ResourceLocation | None = None 201 title: LocalizedStr = LocalizedStr.with_value("Objective")
Base class for a Page
with optional text.
If text is required, do not subclass this type.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
204class RelationsPage(PageWithText, type="patchouli:relations"): 205 entries: list[ResourceLocation] 206 title: LocalizedStr = LocalizedStr.with_value("Related Chapters") 207 208 @pass_context 209 def get_entries(self, context: Context) -> list[ResourceLocation]: 210 for entry in self.entries: 211 if entry not in context["book"].all_entries: 212 raise ValueError(f"Broken entry reference in relations: {entry}") 213 return self.entries
Base class for a Page
with optional text.
If text is required, do not subclass this type.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
234class SpotlightPage(PageWithText, type="patchouli:spotlight"): 235 title_field: LocalizedStr | None = Field(default=None, alias="title") 236 item: ItemWithTexture 237 link_recipe: bool = False 238 239 @property 240 def title(self) -> LocalizedStr | None: 241 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.
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.
228class StonecuttingPage( 229 PageWithDoubleRecipe[StonecuttingRecipe], type="patchouli:stonecutting" 230): 231 pass
Base class for a Page
with optional title and text.
If title and/or text is required, do not subclass this type.
41class TextPage(Page, type="patchouli:text"): 42 title: LocalizedStr | None = None 43 text: FormatTree
Base class for Patchouli page types.
See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
358def init_private_attributes(self: BaseModel, context: Any, /) -> None: 359 """This function is meant to behave like a BaseModel method to initialise private attributes. 360 361 It takes context as an argument since that's what pydantic-core passes when calling it. 362 363 Args: 364 self: The BaseModel instance. 365 context: The context. 366 """ 367 if getattr(self, '__pydantic_private__', None) is None: 368 pydantic_private = {} 369 for name, private_attr in self.__private_attributes__.items(): 370 default = private_attr.get_default() 371 if default is not PydanticUndefined: 372 pydantic_private[name] = default 373 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.