Edit on GitHub

hexdoc.minecraft

 1__all__ = [
 2    "I18n",
 3    "LocalizedItem",
 4    "LocalizedStr",
 5    "Tag",
 6    "TagValue",
 7    "assets",
 8    "recipe",
 9]
10
11from . import assets, recipe
12from .i18n import I18n, LocalizedItem, LocalizedStr
13from .tags import Tag, TagValue
class I18n(hexdoc.model.base.ValidationContextModel):
104class I18n(ValidationContextModel):
105    """Handles localization of strings."""
106
107    lookup: dict[str, LocalizedStr]
108    lang: str
109    default_i18n: I18n | None
110    enabled: bool
111    lang_props: LangProps
112
113    @classmethod
114    def list_all(cls, loader: ModResourceLoader):
115        # don't list languages which this particular mod doesn't support
116        # eg. if Hex has translations for ru_ru but an addon doesn't
117        return set(
118            id.path
119            for resource_dir, id, _ in cls._load_lang_resources(loader)
120            if not resource_dir.external
121        )
122
123    @classmethod
124    def load_all(cls, loader: ModResourceLoader, enabled: bool):
125        # lang -> (key -> value)
126        lookups = defaultdict[str, dict[str, LocalizedStr]](dict)
127        internal_langs = set[str]()
128
129        for resource_dir, lang_id, data in cls._load_lang_resources(loader):
130            lang = lang_id.path
131            if not langcodes.tag_is_valid(lang):
132                modid = resource_dir.modid or lang_id.namespace
133                raise ValueError(
134                    textwrap.dedent(f"""\
135                        Attempted to load invalid lang (provided by {modid}): {lang}
136                        Resource dir: {resource_dir.path}
137                    """).rstrip()
138                )
139
140            lookups[lang] |= cls.parse_lookup(data)
141            if not resource_dir.external:
142                internal_langs.add(lang)
143
144        default_lang = loader.props.default_lang
145        default_lookup = lookups[default_lang]
146        default_i18n = cls(
147            lookup=default_lookup,
148            lang=default_lang,
149            default_i18n=None,
150            enabled=enabled,
151            lang_props=loader.props.lang[default_lang],
152        )
153
154        return {default_lang: default_i18n} | {
155            lang: cls(
156                lookup=lookup,
157                lang=lang,
158                default_i18n=default_i18n,
159                enabled=enabled,
160                lang_props=loader.props.lang[lang],
161            )
162            for lang, lookup in lookups.items()
163            if lang in internal_langs and lang != default_lang
164        }
165
166    @classmethod
167    def load(
168        cls,
169        loader: ModResourceLoader,
170        enabled: bool,
171        lang: str,
172    ) -> Self:
173        if not langcodes.tag_is_valid(lang):
174            raise ValueError(f"Invalid lang: {lang}")
175
176        lookup = dict[str, LocalizedStr]()
177        is_internal = False
178
179        for resource_dir, _, data in cls._load_lang_resources(loader, lang):
180            lookup |= cls.parse_lookup(data)
181            if not resource_dir.external:
182                is_internal = True
183
184        if enabled and not is_internal:
185            raise FileNotFoundError(
186                f"Lang {lang} exists, but {loader.props.modid} does not support it"
187            )
188
189        default_lang = loader.props.default_lang
190        default_i18n = None
191        if lang != default_lang:
192            default_i18n = cls.load(loader, enabled, default_lang)
193
194        return cls(
195            lookup=lookup,
196            lang=lang,
197            default_i18n=default_i18n,
198            enabled=enabled,
199            lang_props=loader.props.lang[lang],
200        )
201
202    @classmethod
203    def parse_lookup(cls, raw_lookup: dict[str, str]) -> dict[str, LocalizedStr]:
204        return {
205            key: LocalizedStr(key=key, value=value.replace("%%", "%"))
206            for key, value in raw_lookup.items()
207        }
208
209    @classmethod
210    def _load_lang_resources(cls, loader: ModResourceLoader, lang: str = "*"):
211        return loader.load_resources_with_decoders(
212            "assets",
213            namespace="*",
214            folder="lang",
215            glob=[
216                f"{lang}.json",
217                f"{lang}.json5",
218                f"{lang}.flatten.json",
219                f"{lang}.flatten.json5",
220                f"{lang}.yml",
221                f"{lang}.yaml",
222            ],
223            decoders={
224                (".json", ".json5"): decode_and_flatten_json_dict,
225                (".yml", ".yaml"): decode_and_flatten_yaml_dict,
226            },
227            export=cls._export,
228        )
229
230    @classmethod
231    def _export(cls, new: dict[str, str], current: dict[str, str] | None):
232        return json.dumps((current or {}) | new)
233
234    @model_validator(mode="after")
235    def _warn_if_disabled(self):
236        if not self.enabled:
237            logger.info(
238                f"I18n is disabled for {self.lang}. Warnings about missing translations"
239                + " will only be logged in verbose mode."
240            )
241        elif self.lang_props.quiet:
242            logger.info(
243                f"Quiet mode is enabled for {self.lang}. Warnings about missing"
244                + " translations will only be logged in verbose mode."
245            )
246        return self
247
248    @property
249    def is_default(self):
250        return self.default_i18n is None
251
252    def localize(
253        self,
254        *keys: str,
255        default: str | None = None,
256        silent: bool = False,
257    ) -> LocalizedStr:
258        """Looks up the given string in the lang table if i18n is enabled. Otherwise,
259        returns the original key.
260
261        If multiple keys are provided, returns the value of the first key which exists.
262        That is, subsequent keys are treated as fallbacks for the first.
263
264        Raises ValueError if i18n is enabled and default is None but the key has no
265        corresponding localized value.
266        """
267
268        for key in keys:
269            if key in self.lookup:
270                return self.lookup[key]
271
272        if silent or not self.enabled or self.lang_props.quiet:
273            log_level = logging.DEBUG
274        else:
275            log_level = logging.WARNING
276
277        log_keys = set(keys) - self._logged_missing_keys
278        if log_keys:
279            self._logged_missing_keys.update(log_keys)
280            match list(log_keys):
281                case [key]:
282                    message = f"key {key}"
283                case _:
284                    message = "keys " + ", ".join(log_keys)
285            logger.log(log_level, f"No translation in {self.lang} for {message}")
286
287        if default is not None:
288            return LocalizedStr.skip_i18n(default)
289
290        if self.default_i18n:
291            return self.default_i18n.localize(*keys, default=default, silent=silent)
292
293        return LocalizedStr.skip_i18n(keys[0])
294
295    def localize_pattern(
296        self,
297        op_id: ResourceLocation,
298        silent: bool = False,
299    ) -> LocalizedStr:
300        """Localizes the given pattern id (internal name, eg. brainsweep).
301
302        Raises ValueError if i18n is enabled but the key has no localization.
303        """
304        key_group = ValueIfVersion(">=1.20", "action", "spell")()
305
306        # prefer the book-specific translation if it exists
307        return self.localize(
308            f"hexcasting.{key_group}.book.{op_id}",
309            f"hexcasting.{key_group}.{op_id}",
310            silent=silent,
311        )
312
313    def localize_item(
314        self,
315        item: str | ResourceLocation | ItemStack,
316        silent: bool = False,
317    ) -> LocalizedItem:
318        """Localizes the given item resource name.
319
320        Raises ValueError if i18n is enabled but the key has no localization.
321        """
322        match item:
323            case str():
324                item = ItemStack.from_str(item)
325            case ResourceLocation(namespace=namespace, path=path):
326                item = ItemStack(namespace=namespace, path=path)
327            case _:
328                pass
329
330        localized = self.localize(
331            item.i18n_key(),
332            item.i18n_key("block"),
333            silent=silent,
334        )
335        return LocalizedItem(key=localized.key, value=localized.value)
336
337    def localize_entity(self, entity: ResourceLocation, type: str | None = None):
338        if type:
339            entity = type / entity
340        return self.localize(entity.i18n_key("entity"))
341
342    def localize_key(self, key: str, silent: bool = False) -> LocalizedStr:
343        if not key.startswith("key."):
344            key = "key." + key
345        return self.localize(key, silent=silent)
346
347    def localize_item_tag(self, tag: ResourceLocation, silent: bool = False):
348        localized = self.localize(
349            f"tag.{tag.namespace}.{tag.path}",
350            f"tag.item.{tag.namespace}.{tag.path}",
351            f"tag.block.{tag.namespace}.{tag.path}",
352            default=self.fallback_tag_name(tag),
353            silent=silent,
354        )
355        return LocalizedStr(key=localized.key, value=f"Tag: {localized.value}")
356
357    def fallback_tag_name(self, tag: ResourceLocation):
358        """Generates a more-or-less reasonable fallback name for a tag.
359
360        For example:
361        * `forge:ores` -> Ores
362        * `c:saplings/almond` -> Almond Saplings
363        * `c:tea_ingredients/gloopy/weak` -> Tea Ingredients, Gloopy, Weak
364        """
365
366        if tag.path.count("/") == 1:
367            before, after = tag.path.split("/")
368            return f"{after} {before}".title()
369
370        return tag.path.replace("_", " ").replace("/", ", ").title()
371
372    def localize_texture(self, texture_id: ResourceLocation, silent: bool = False):
373        path = texture_id.path.removeprefix("textures/").removesuffix(".png")
374        root, rest = path.split("/", 1)
375
376        # TODO: refactor / extensibilify
377        if root == "mob_effect":
378            root = "effect"
379
380        return self.localize(f"{root}.{texture_id.namespace}.{rest}", silent=silent)
381
382    def localize_lang(self, silent: bool = False):
383        name = self.localize("language.name", default="", silent=silent)
384        region = self.localize("language.region", silent=silent)
385        # don't allow language names to fall back to English (United States)
386        if name.value == "":
387            return self.lang
388        return f"{name} ({region})"
389
390    @model_validator(mode="after")
391    def _init_logger_cache(self):
392        self._logged_missing_keys = set[str]()
393        return self

Handles localization of strings.

lookup: dict[str, LocalizedStr]
lang: str
default_i18n: I18n | None
enabled: bool
@classmethod
def list_all(cls, loader: hexdoc.core.ModResourceLoader):
113    @classmethod
114    def list_all(cls, loader: ModResourceLoader):
115        # don't list languages which this particular mod doesn't support
116        # eg. if Hex has translations for ru_ru but an addon doesn't
117        return set(
118            id.path
119            for resource_dir, id, _ in cls._load_lang_resources(loader)
120            if not resource_dir.external
121        )
@classmethod
def load_all(cls, loader: hexdoc.core.ModResourceLoader, enabled: bool):
123    @classmethod
124    def load_all(cls, loader: ModResourceLoader, enabled: bool):
125        # lang -> (key -> value)
126        lookups = defaultdict[str, dict[str, LocalizedStr]](dict)
127        internal_langs = set[str]()
128
129        for resource_dir, lang_id, data in cls._load_lang_resources(loader):
130            lang = lang_id.path
131            if not langcodes.tag_is_valid(lang):
132                modid = resource_dir.modid or lang_id.namespace
133                raise ValueError(
134                    textwrap.dedent(f"""\
135                        Attempted to load invalid lang (provided by {modid}): {lang}
136                        Resource dir: {resource_dir.path}
137                    """).rstrip()
138                )
139
140            lookups[lang] |= cls.parse_lookup(data)
141            if not resource_dir.external:
142                internal_langs.add(lang)
143
144        default_lang = loader.props.default_lang
145        default_lookup = lookups[default_lang]
146        default_i18n = cls(
147            lookup=default_lookup,
148            lang=default_lang,
149            default_i18n=None,
150            enabled=enabled,
151            lang_props=loader.props.lang[default_lang],
152        )
153
154        return {default_lang: default_i18n} | {
155            lang: cls(
156                lookup=lookup,
157                lang=lang,
158                default_i18n=default_i18n,
159                enabled=enabled,
160                lang_props=loader.props.lang[lang],
161            )
162            for lang, lookup in lookups.items()
163            if lang in internal_langs and lang != default_lang
164        }
@classmethod
def load( cls, loader: hexdoc.core.ModResourceLoader, enabled: bool, lang: str) -> Self:
166    @classmethod
167    def load(
168        cls,
169        loader: ModResourceLoader,
170        enabled: bool,
171        lang: str,
172    ) -> Self:
173        if not langcodes.tag_is_valid(lang):
174            raise ValueError(f"Invalid lang: {lang}")
175
176        lookup = dict[str, LocalizedStr]()
177        is_internal = False
178
179        for resource_dir, _, data in cls._load_lang_resources(loader, lang):
180            lookup |= cls.parse_lookup(data)
181            if not resource_dir.external:
182                is_internal = True
183
184        if enabled and not is_internal:
185            raise FileNotFoundError(
186                f"Lang {lang} exists, but {loader.props.modid} does not support it"
187            )
188
189        default_lang = loader.props.default_lang
190        default_i18n = None
191        if lang != default_lang:
192            default_i18n = cls.load(loader, enabled, default_lang)
193
194        return cls(
195            lookup=lookup,
196            lang=lang,
197            default_i18n=default_i18n,
198            enabled=enabled,
199            lang_props=loader.props.lang[lang],
200        )
@classmethod
def parse_lookup( cls, raw_lookup: dict[str, str]) -> dict[str, LocalizedStr]:
202    @classmethod
203    def parse_lookup(cls, raw_lookup: dict[str, str]) -> dict[str, LocalizedStr]:
204        return {
205            key: LocalizedStr(key=key, value=value.replace("%%", "%"))
206            for key, value in raw_lookup.items()
207        }
is_default
248    @property
249    def is_default(self):
250        return self.default_i18n is None
def localize( self, *keys: str, default: str | None = None, silent: bool = False) -> LocalizedStr:
252    def localize(
253        self,
254        *keys: str,
255        default: str | None = None,
256        silent: bool = False,
257    ) -> LocalizedStr:
258        """Looks up the given string in the lang table if i18n is enabled. Otherwise,
259        returns the original key.
260
261        If multiple keys are provided, returns the value of the first key which exists.
262        That is, subsequent keys are treated as fallbacks for the first.
263
264        Raises ValueError if i18n is enabled and default is None but the key has no
265        corresponding localized value.
266        """
267
268        for key in keys:
269            if key in self.lookup:
270                return self.lookup[key]
271
272        if silent or not self.enabled or self.lang_props.quiet:
273            log_level = logging.DEBUG
274        else:
275            log_level = logging.WARNING
276
277        log_keys = set(keys) - self._logged_missing_keys
278        if log_keys:
279            self._logged_missing_keys.update(log_keys)
280            match list(log_keys):
281                case [key]:
282                    message = f"key {key}"
283                case _:
284                    message = "keys " + ", ".join(log_keys)
285            logger.log(log_level, f"No translation in {self.lang} for {message}")
286
287        if default is not None:
288            return LocalizedStr.skip_i18n(default)
289
290        if self.default_i18n:
291            return self.default_i18n.localize(*keys, default=default, silent=silent)
292
293        return LocalizedStr.skip_i18n(keys[0])

Looks up the given string in the lang table if i18n is enabled. Otherwise, returns the original key.

If multiple keys are provided, returns the value of the first key which exists. That is, subsequent keys are treated as fallbacks for the first.

Raises ValueError if i18n is enabled and default is None but the key has no corresponding localized value.

def localize_pattern( self, op_id: hexdoc.core.ResourceLocation, silent: bool = False) -> LocalizedStr:
295    def localize_pattern(
296        self,
297        op_id: ResourceLocation,
298        silent: bool = False,
299    ) -> LocalizedStr:
300        """Localizes the given pattern id (internal name, eg. brainsweep).
301
302        Raises ValueError if i18n is enabled but the key has no localization.
303        """
304        key_group = ValueIfVersion(">=1.20", "action", "spell")()
305
306        # prefer the book-specific translation if it exists
307        return self.localize(
308            f"hexcasting.{key_group}.book.{op_id}",
309            f"hexcasting.{key_group}.{op_id}",
310            silent=silent,
311        )

Localizes the given pattern id (internal name, eg. brainsweep).

Raises ValueError if i18n is enabled but the key has no localization.

def localize_item( self, item: str | hexdoc.core.ResourceLocation | hexdoc.core.ItemStack, silent: bool = False) -> LocalizedItem:
313    def localize_item(
314        self,
315        item: str | ResourceLocation | ItemStack,
316        silent: bool = False,
317    ) -> LocalizedItem:
318        """Localizes the given item resource name.
319
320        Raises ValueError if i18n is enabled but the key has no localization.
321        """
322        match item:
323            case str():
324                item = ItemStack.from_str(item)
325            case ResourceLocation(namespace=namespace, path=path):
326                item = ItemStack(namespace=namespace, path=path)
327            case _:
328                pass
329
330        localized = self.localize(
331            item.i18n_key(),
332            item.i18n_key("block"),
333            silent=silent,
334        )
335        return LocalizedItem(key=localized.key, value=localized.value)

Localizes the given item resource name.

Raises ValueError if i18n is enabled but the key has no localization.

def localize_entity( self, entity: hexdoc.core.ResourceLocation, type: str | None = None):
337    def localize_entity(self, entity: ResourceLocation, type: str | None = None):
338        if type:
339            entity = type / entity
340        return self.localize(entity.i18n_key("entity"))
def localize_key( self, key: str, silent: bool = False) -> LocalizedStr:
342    def localize_key(self, key: str, silent: bool = False) -> LocalizedStr:
343        if not key.startswith("key."):
344            key = "key." + key
345        return self.localize(key, silent=silent)
def localize_item_tag( self, tag: hexdoc.core.ResourceLocation, silent: bool = False):
347    def localize_item_tag(self, tag: ResourceLocation, silent: bool = False):
348        localized = self.localize(
349            f"tag.{tag.namespace}.{tag.path}",
350            f"tag.item.{tag.namespace}.{tag.path}",
351            f"tag.block.{tag.namespace}.{tag.path}",
352            default=self.fallback_tag_name(tag),
353            silent=silent,
354        )
355        return LocalizedStr(key=localized.key, value=f"Tag: {localized.value}")
def fallback_tag_name(self, tag: hexdoc.core.ResourceLocation):
357    def fallback_tag_name(self, tag: ResourceLocation):
358        """Generates a more-or-less reasonable fallback name for a tag.
359
360        For example:
361        * `forge:ores` -> Ores
362        * `c:saplings/almond` -> Almond Saplings
363        * `c:tea_ingredients/gloopy/weak` -> Tea Ingredients, Gloopy, Weak
364        """
365
366        if tag.path.count("/") == 1:
367            before, after = tag.path.split("/")
368            return f"{after} {before}".title()
369
370        return tag.path.replace("_", " ").replace("/", ", ").title()

Generates a more-or-less reasonable fallback name for a tag.

For example:

  • forge:ores -> Ores
  • c:saplings/almond -> Almond Saplings
  • c:tea_ingredients/gloopy/weak -> Tea Ingredients, Gloopy, Weak
def localize_texture( self, texture_id: hexdoc.core.ResourceLocation, silent: bool = False):
372    def localize_texture(self, texture_id: ResourceLocation, silent: bool = False):
373        path = texture_id.path.removeprefix("textures/").removesuffix(".png")
374        root, rest = path.split("/", 1)
375
376        # TODO: refactor / extensibilify
377        if root == "mob_effect":
378            root = "effect"
379
380        return self.localize(f"{root}.{texture_id.namespace}.{rest}", silent=silent)
def localize_lang(self, silent: bool = False):
382    def localize_lang(self, silent: bool = False):
383        name = self.localize("language.name", default="", silent=silent)
384        region = self.localize("language.region", silent=silent)
385        # don't allow language names to fall back to English (United States)
386        if name.value == "":
387            return self.lang
388        return f"{name} ({region})"
class LocalizedItem(hexdoc.minecraft.LocalizedStr):
 98class LocalizedItem(LocalizedStr, frozen=True):
 99    @classmethod
100    def _localize(cls, i18n: I18n, key: str) -> Self:
101        return cls.model_validate(i18n.localize_item(key))

Represents a string which has been localized.

@total_ordering
class LocalizedStr(hexdoc.model.base.HexdocModel):
32@total_ordering
33class LocalizedStr(HexdocModel, frozen=True):
34    """Represents a string which has been localized."""
35
36    model_config = DEFAULT_CONFIG | json_schema_extra_config(type_str, inherited)
37
38    key: str
39    value: str
40
41    @classmethod
42    def skip_i18n(cls, key: str) -> Self:
43        """Returns an instance of this class with `value = key`."""
44        return cls(key=key, value=key)
45
46    @classmethod
47    def with_value(cls, value: str) -> Self:
48        """Returns an instance of this class with an empty key."""
49        return cls(key="", value=value)
50
51    @model_validator(mode="wrap")
52    @classmethod
53    def _check_localize(
54        cls,
55        value: str | Any,
56        handler: ModelWrapValidatorHandler[Self],
57        info: ValidationInfo,
58    ) -> Self:
59        # NOTE: if we need LocalizedStr to work as a dict key, add another check which
60        # returns cls.skip_i18n(value) if info.context is falsy
61        if not isinstance(value, str):
62            return handler(value)
63
64        i18n = I18n.of(info)
65        return cls._localize(i18n, value)
66
67    @classmethod
68    def _localize(cls, i18n: I18n, key: str) -> Self:
69        return cls.model_validate(i18n.localize(key))
70
71    def map(self, fn: Callable[[str], str]) -> Self:
72        """Returns a copy of this object with `new.value = fn(old.value)`."""
73        return self.model_copy(update={"value": fn(self.value)})
74
75    def __repr__(self) -> str:
76        return self.value
77
78    def __str__(self) -> str:
79        return self.value
80
81    def __eq__(self, other: Self | str | Any):
82        match other:
83            case LocalizedStr():
84                return self.value == other.value
85            case str():
86                return self.value == other
87            case _:
88                return super().__eq__(other)
89
90    def __lt__(self, other: Self | str):
91        match other:
92            case LocalizedStr():
93                return self.value < other.value
94            case str():
95                return self.value < other

Represents a string which has been localized.

key: str
value: str
@classmethod
def skip_i18n(cls, key: str) -> Self:
41    @classmethod
42    def skip_i18n(cls, key: str) -> Self:
43        """Returns an instance of this class with `value = key`."""
44        return cls(key=key, value=key)

Returns an instance of this class with value = key.

@classmethod
def with_value(cls, value: str) -> Self:
46    @classmethod
47    def with_value(cls, value: str) -> Self:
48        """Returns an instance of this class with an empty key."""
49        return cls(key="", value=value)

Returns an instance of this class with an empty key.

def map(self, fn: Callable[[str], str]) -> Self:
71    def map(self, fn: Callable[[str], str]) -> Self:
72        """Returns a copy of this object with `new.value = fn(old.value)`."""
73        return self.model_copy(update={"value": fn(self.value)})

Returns a copy of this object with new.value = fn(old.value).

class Tag(hexdoc.model.base.HexdocModel):
 41class Tag(HexdocModel):
 42    GASLIGHTING_ITEMS: ClassVar = TagLoader("hexdoc", "items", "gaslighting")
 43    """Item/block ids that gaslight you. This tag isn't real, it's all in your head.
 44
 45    File: `hexdoc/tags/items/gaslighting.json`
 46    """
 47    SPOILERED_ADVANCEMENTS: ClassVar = TagLoader("hexdoc", "advancements", "spoilered")
 48    """Advancements for entries that should be blurred in the web book.
 49
 50    File: `hexdoc/tags/advancements/spoilered.json`
 51    """
 52
 53    registry: str = Field(exclude=True)
 54    values: PydanticOrderedSet[TagValue]
 55    replace: bool = False
 56
 57    @classmethod
 58    def load(
 59        cls,
 60        registry: str,
 61        id: ResourceLocation,
 62        loader: ModResourceLoader,
 63    ) -> Self:
 64        values = PydanticOrderedSet[TagValue]()
 65        replace = False
 66
 67        for _, _, tag in loader.load_resources(
 68            "data",
 69            folder=f"tags/{registry}",
 70            id=id,
 71            decode=lambda raw_data: Tag._convert(
 72                registry=registry,
 73                raw_data=raw_data,
 74            ),
 75            export=cls._export,
 76        ):
 77            if tag.replace:
 78                values.clear()
 79            for value in tag._load_values(loader):
 80                values.add(value)
 81
 82        return cls(registry=registry, values=values, replace=replace)
 83
 84    @classmethod
 85    def _convert(cls, *, registry: str, raw_data: str) -> Self:
 86        data = decode_json_dict(raw_data)
 87        return cls.model_validate(data | {"registry": registry})
 88
 89    @property
 90    def value_ids(self) -> Iterator[ResourceLocation]:
 91        for value in self.values:
 92            match value:
 93                case ResourceLocation():
 94                    yield value
 95                case OptionalTagValue(id=id):
 96                    yield id
 97
 98    @property
 99    def value_ids_set(self):
100        return set(self.value_ids)
101
102    def __ror__(self, other: set[ResourceLocation]):
103        new = set(other)
104        new |= self.value_ids_set
105        return new
106
107    def __contains__(self, x: Any) -> bool:
108        if isinstance(x, BaseResourceLocation):
109            return x.id in self.value_ids_set
110        return NotImplemented
111
112    def _export(self: Tag, current: Tag | None):
113        if self.replace or current is None:
114            tag = self
115        else:
116            tag = self.model_copy(
117                update={"values": current.values | self.values},
118            )
119        return tag.model_dump_json(by_alias=True)
120
121    def _load_values(self, loader: ModResourceLoader) -> Iterator[TagValue]:
122        for value in self.values:
123            match value:
124                case (
125                    (ResourceLocation() as child_id)
126                    | OptionalTagValue(id=child_id)
127                ) if child_id.is_tag:
128                    try:
129                        child = Tag.load(self.registry, child_id, loader)
130                        yield from child._load_values(loader)
131                    except FileNotFoundError:
132                        yield value
133                case _:
134                    yield value

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.

GASLIGHTING_ITEMS: ClassVar = TagLoader(namespace='hexdoc', registry='items', path='gaslighting')

Item/block ids that gaslight you. This tag isn't real, it's all in your head.

File: hexdoc/tags/items/gaslighting.json

SPOILERED_ADVANCEMENTS: ClassVar = TagLoader(namespace='hexdoc', registry='advancements', path='spoilered')

Advancements for entries that should be blurred in the web book.

File: hexdoc/tags/advancements/spoilered.json

registry: str
values: hexdoc.utils.PydanticOrderedSet[hexdoc.core.ResourceLocation | hexdoc.minecraft.tags.OptionalTagValue]
replace: bool
@classmethod
def load( cls, registry: str, id: hexdoc.core.ResourceLocation, loader: hexdoc.core.ModResourceLoader) -> Self:
57    @classmethod
58    def load(
59        cls,
60        registry: str,
61        id: ResourceLocation,
62        loader: ModResourceLoader,
63    ) -> Self:
64        values = PydanticOrderedSet[TagValue]()
65        replace = False
66
67        for _, _, tag in loader.load_resources(
68            "data",
69            folder=f"tags/{registry}",
70            id=id,
71            decode=lambda raw_data: Tag._convert(
72                registry=registry,
73                raw_data=raw_data,
74            ),
75            export=cls._export,
76        ):
77            if tag.replace:
78                values.clear()
79            for value in tag._load_values(loader):
80                values.add(value)
81
82        return cls(registry=registry, values=values, replace=replace)
value_ids: Iterator[hexdoc.core.ResourceLocation]
89    @property
90    def value_ids(self) -> Iterator[ResourceLocation]:
91        for value in self.values:
92            match value:
93                case ResourceLocation():
94                    yield value
95                case OptionalTagValue(id=id):
96                    yield id
value_ids_set
 98    @property
 99    def value_ids_set(self):
100        return set(self.value_ids)
TagValue = hexdoc.core.ResourceLocation | hexdoc.minecraft.tags.OptionalTagValue