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

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:
306    def localize_item(
307        self,
308        item: str | ResourceLocation | ItemStack,
309        silent: bool = False,
310    ) -> LocalizedItem:
311        """Localizes the given item resource name.
312
313        Raises ValueError if i18n is enabled but the key has no localization.
314        """
315        match item:
316            case str():
317                item = ItemStack.from_str(item)
318            case ResourceLocation(namespace=namespace, path=path):
319                item = ItemStack(namespace=namespace, path=path)
320            case _:
321                pass
322
323        localized = self.localize(
324            item.i18n_key(),
325            item.i18n_key("block"),
326            silent=silent,
327        )
328        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):
330    def localize_entity(self, entity: ResourceLocation, type: str | None = None):
331        if type:
332            entity = type / entity
333        return self.localize(entity.i18n_key("entity"))
def localize_key( self, key: str, silent: bool = False) -> LocalizedStr:
335    def localize_key(self, key: str, silent: bool = False) -> LocalizedStr:
336        if not key.startswith("key."):
337            key = "key." + key
338        return self.localize(key, silent=silent)
def localize_item_tag( self, tag: hexdoc.core.ResourceLocation, silent: bool = False):
340    def localize_item_tag(self, tag: ResourceLocation, silent: bool = False):
341        localized = self.localize(
342            f"tag.{tag.namespace}.{tag.path}",
343            f"tag.item.{tag.namespace}.{tag.path}",
344            f"tag.block.{tag.namespace}.{tag.path}",
345            default=self.fallback_tag_name(tag),
346            silent=silent,
347        )
348        return LocalizedStr(key=localized.key, value=f"Tag: {localized.value}")
def fallback_tag_name(self, tag: hexdoc.core.ResourceLocation):
350    def fallback_tag_name(self, tag: ResourceLocation):
351        """Generates a more-or-less reasonable fallback name for a tag.
352
353        For example:
354        * `forge:ores` -> Ores
355        * `c:saplings/almond` -> Almond Saplings
356        * `c:tea_ingredients/gloopy/weak` -> Tea Ingredients, Gloopy, Weak
357        """
358
359        if tag.path.count("/") == 1:
360            before, after = tag.path.split("/")
361            return f"{after} {before}".title()
362
363        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):
365    def localize_texture(self, texture_id: ResourceLocation, silent: bool = False):
366        path = texture_id.path.removeprefix("textures/").removesuffix(".png")
367        root, rest = path.split("/", 1)
368
369        # TODO: refactor / extensibilify
370        if root == "mob_effect":
371            root = "effect"
372
373        return self.localize(f"{root}.{texture_id.namespace}.{rest}", silent=silent)
def localize_lang(self, silent: bool = False):
375    def localize_lang(self, silent: bool = False):
376        name = self.localize("language.name", default="", silent=silent)
377        region = self.localize("language.region", silent=silent)
378        # don't allow language names to fall back to English (United States)
379        if name.value == "":
380            return self.lang
381        return f"{name} ({region})"
class LocalizedItem(hexdoc.minecraft.LocalizedStr):
96class LocalizedItem(LocalizedStr, frozen=True):
97    @classmethod
98    def _localize(cls, i18n: I18n, key: str) -> Self:
99        return cls.model_validate(i18n.localize_item(key))

Represents a string which has been localized.

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

Represents a string which has been localized.

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

Returns an instance of this class with value = key.

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

Returns an instance of this class with an empty key.

def map(self, fn: Callable[[str], str]) -> Self:
69    def map(self, fn: Callable[[str], str]) -> Self:
70        """Returns a copy of this object with `new.value = fn(old.value)`."""
71        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