Add animated textures, refactor a lot of things
This commit is contained in:
parent
8200465cdf
commit
dab57062b4
29 changed files with 519 additions and 232 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -18,5 +18,6 @@
|
|||
"files.associations": {
|
||||
"*.js.jinja": "javascript",
|
||||
"*.css.jinja": "css",
|
||||
"*.jcss.jinja": "jinja-css", // for files with a lot of jinja stuff, where the linting isn't useful
|
||||
},
|
||||
}
|
||||
|
|
|
@ -56,6 +56,42 @@ missing = [
|
|||
"hexcasting:spellbook" = "hexcasting:spellbook_empty"
|
||||
"hexcasting:staff/quenched" = "hexcasting:staff/quenched_0"
|
||||
|
||||
# ugh. TODO: properly get from model files
|
||||
"hexcasting:dye_colorizer_black" = "hexcasting:colorizer/dye_black"
|
||||
"hexcasting:dye_colorizer_blue" = "hexcasting:colorizer/dye_blue"
|
||||
"hexcasting:dye_colorizer_brown" = "hexcasting:colorizer/dye_brown"
|
||||
"hexcasting:dye_colorizer_cyan" = "hexcasting:colorizer/dye_cyan"
|
||||
"hexcasting:dye_colorizer_gray" = "hexcasting:colorizer/dye_gray"
|
||||
"hexcasting:dye_colorizer_green" = "hexcasting:colorizer/dye_green"
|
||||
"hexcasting:dye_colorizer_light_blue" = "hexcasting:colorizer/dye_light_blue"
|
||||
"hexcasting:dye_colorizer_light_gray" = "hexcasting:colorizer/dye_light_gray"
|
||||
"hexcasting:dye_colorizer_lime" = "hexcasting:colorizer/dye_lime"
|
||||
"hexcasting:dye_colorizer_magenta" = "hexcasting:colorizer/dye_magenta"
|
||||
"hexcasting:dye_colorizer_orange" = "hexcasting:colorizer/dye_orange"
|
||||
"hexcasting:dye_colorizer_pink" = "hexcasting:colorizer/dye_pink"
|
||||
"hexcasting:dye_colorizer_purple" = "hexcasting:colorizer/dye_purple"
|
||||
"hexcasting:dye_colorizer_red" = "hexcasting:colorizer/dye_red"
|
||||
"hexcasting:dye_colorizer_white" = "hexcasting:colorizer/dye_white"
|
||||
"hexcasting:dye_colorizer_yellow" = "hexcasting:colorizer/dye_yellow"
|
||||
"hexcasting:pride_colorizer_agender" = "hexcasting:colorizer/pride_agender"
|
||||
"hexcasting:pride_colorizer_aroace" = "hexcasting:colorizer/pride_aroace"
|
||||
"hexcasting:pride_colorizer_aromantic" = "hexcasting:colorizer/pride_aromantic"
|
||||
"hexcasting:pride_colorizer_asexual" = "hexcasting:colorizer/pride_asexual"
|
||||
"hexcasting:pride_colorizer_bisexual" = "hexcasting:colorizer/pride_bisexual"
|
||||
"hexcasting:pride_colorizer_demiboy" = "hexcasting:colorizer/pride_demiboy"
|
||||
"hexcasting:pride_colorizer_demigirl" = "hexcasting:colorizer/pride_demigirl"
|
||||
"hexcasting:pride_colorizer_gay" = "hexcasting:colorizer/pride_gay"
|
||||
"hexcasting:pride_colorizer_genderfluid" = "hexcasting:colorizer/pride_genderfluid"
|
||||
"hexcasting:pride_colorizer_genderqueer" = "hexcasting:colorizer/pride_genderqueer"
|
||||
"hexcasting:pride_colorizer_intersex" = "hexcasting:colorizer/pride_intersex"
|
||||
"hexcasting:pride_colorizer_lesbian" = "hexcasting:colorizer/pride_lesbian"
|
||||
"hexcasting:pride_colorizer_nonbinary" = "hexcasting:colorizer/pride_nonbinary"
|
||||
"hexcasting:pride_colorizer_pansexual" = "hexcasting:colorizer/pride_pansexual"
|
||||
"hexcasting:pride_colorizer_plural" = "hexcasting:colorizer/pride_plural"
|
||||
"hexcasting:pride_colorizer_transgender" = "hexcasting:colorizer/pride_transgender"
|
||||
"hexcasting:uuid_colorizer" = "hexcasting:colorizer/uuid"
|
||||
"hexcasting:default_colorizer" = "hexcasting:colorizer/uuid"
|
||||
|
||||
[template]
|
||||
static_dir = "static"
|
||||
packages = ["hexdoc"]
|
||||
|
|
13
doc/src/hexdoc/_templates/components/textures.jcss.jinja
Normal file
13
doc/src/hexdoc/_templates/components/textures.jcss.jinja
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% for animation in animations %}
|
||||
.{{ animation.class_name }} {
|
||||
animation: {{ animation.class_name }} {{ animation.time_seconds }}s linear infinite;
|
||||
background-size: 32px;
|
||||
background-image: url("{{ animation.url }}");
|
||||
}
|
||||
|
||||
@keyframes {{ animation.class_name }} {
|
||||
{% for frame in animation.frames %}
|
||||
{{ frame.start_percent }}%, {{ frame.end_percent }}% { background-position-y: {{ frame.index * -32 }}px }
|
||||
{% endfor %}
|
||||
}
|
||||
{% endfor %}
|
|
@ -12,12 +12,25 @@
|
|||
|
||||
{# display a single item, with a badge if the count is greater than 1 #}
|
||||
{% macro render_item(item, count=1, always_show_count=false) -%}
|
||||
<div data-toggle="tooltip" title="{{ item.name }}">
|
||||
<img loading="lazy" alt="Image of {{ item.name }}" src="{{ item.texture }}">
|
||||
{% if always_show_count or count > 1 %}
|
||||
<div class="badge">{{ count }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if item.texture.meta %}
|
||||
<div
|
||||
role="img"
|
||||
title="{{ item.name }}"
|
||||
aria-label="Animated image of {{ item.name }}"
|
||||
class="animated-texture {{ item.texture.class_name }}"
|
||||
></div>
|
||||
{% else %}
|
||||
<img
|
||||
title="{{ item.name }}"
|
||||
alt="Image of {{ item.name }}"
|
||||
src="{{ item.texture.url }}"
|
||||
loading="lazy"
|
||||
>
|
||||
{% endif %}
|
||||
|
||||
{% if always_show_count or count > 1 %}
|
||||
<div class="badge">{{ count }}</div>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro render_ingredient(ingredient) -%}
|
||||
|
|
|
@ -97,16 +97,12 @@ blockquote.crafting-info {
|
|||
right: -4px;
|
||||
}
|
||||
|
||||
:is(.crafting-table-grid, .crafting-table-result) :is(img, span) {
|
||||
:is(.crafting-table-grid, .crafting-table-result) :is(img, div) {
|
||||
image-rendering: pixelated;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a.permalink {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
@ -267,3 +263,8 @@ canvas.spell-viz {
|
|||
--dark-mode: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*{{"*"}}/
|
||||
{# ^ so this part doesn't show linter errors in vscode (scuffed, i know) #}
|
||||
{% include "components/textures.jcss.jinja" %}
|
||||
/**/
|
||||
|
|
|
@ -296,6 +296,19 @@ function hookSpoiler(elem) {
|
|||
}
|
||||
}
|
||||
|
||||
let startTime = null;
|
||||
|
||||
function hookSyncAnimations(elem) {
|
||||
elem.addEventListener("animationstart", (e) => {
|
||||
const anim = e.target.getAnimations()[0];
|
||||
if (startTime == null) {
|
||||
startTime = anim.startTime;
|
||||
} else {
|
||||
anim.startTime = startTime;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// these are filled by Jinja
|
||||
const BOOK_URL = "{{ props.url }}";
|
||||
|
||||
|
@ -406,6 +419,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
document.querySelectorAll(".details-collapsible").forEach(hookLoad);
|
||||
document.querySelectorAll("a.toggle-link").forEach(hookToggle);
|
||||
document.querySelectorAll(".spoilered").forEach(hookSpoiler);
|
||||
document.querySelectorAll(".animated-texture").forEach(hookSyncAnimations)
|
||||
|
||||
function tick(prevTime, time) {
|
||||
const dt = time - prevTime;
|
||||
|
|
|
@ -9,11 +9,13 @@ from typing import Annotated, Union
|
|||
import typer
|
||||
|
||||
from hexdoc.minecraft import I18n
|
||||
from hexdoc.minecraft.assets.textures import AnimatedTexture, Texture
|
||||
from hexdoc.utils import ModResourceLoader
|
||||
from hexdoc.utils.resource import ResourceLocation
|
||||
|
||||
from .load import load_book, load_books, load_common_data
|
||||
from .render import create_jinja_env, render_book
|
||||
from .sitemap import (
|
||||
from .utils.load import load_book, load_books, load_common_data
|
||||
from .utils.render import create_jinja_env, render_book
|
||||
from .utils.sitemap import (
|
||||
assert_version_exists,
|
||||
delete_root_book,
|
||||
delete_updated_books,
|
||||
|
@ -71,7 +73,16 @@ def render(
|
|||
|
||||
# load data
|
||||
props, pm, version = load_common_data(props_file, verbosity)
|
||||
books, mod_metadata = load_books(props, pm, lang, allow_missing)
|
||||
books, all_metadata = load_books(props, pm, lang, allow_missing)
|
||||
|
||||
textures = dict[ResourceLocation, Texture]()
|
||||
animations = list[AnimatedTexture]()
|
||||
|
||||
for metadata in all_metadata.values():
|
||||
textures |= metadata.textures
|
||||
for texture in metadata.textures.values():
|
||||
if isinstance(texture, AnimatedTexture):
|
||||
animations.append(texture)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"update_latest={update_latest}, release={release}")
|
||||
|
@ -102,7 +113,9 @@ def render(
|
|||
i18n=i18n,
|
||||
templates=templates,
|
||||
output_dir=output_dir,
|
||||
mod_metadata=mod_metadata,
|
||||
all_metadata=all_metadata,
|
||||
textures=textures,
|
||||
animations=animations,
|
||||
allow_missing=allow_missing,
|
||||
version=version_,
|
||||
is_root=is_root,
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from hexdoc.hexcasting.hex_book import load_hex_book
|
||||
from hexdoc.minecraft import I18n
|
||||
from hexdoc.minecraft.assets.textures import Texture
|
||||
from hexdoc.patchouli import Book
|
||||
from hexdoc.plugin import PluginManager
|
||||
from hexdoc.utils import ModResourceLoader, Properties
|
||||
from hexdoc.utils.metadata import HexdocMetadata
|
||||
|
||||
from .logging import setup_logging
|
||||
|
||||
|
@ -26,6 +29,33 @@ def load_version(props: Properties, pm: PluginManager):
|
|||
return version
|
||||
|
||||
|
||||
def load_all_metadata(props: Properties, pm: PluginManager, loader: ModResourceLoader):
|
||||
version = pm.mod_version(props.modid)
|
||||
root = Path(
|
||||
subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
).stdout.strip()
|
||||
)
|
||||
|
||||
# this mod's metadata
|
||||
metadata = HexdocMetadata(
|
||||
book_url=f"{props.url}/v/{version}",
|
||||
asset_url=props.env.githubusercontent,
|
||||
textures={
|
||||
texture.file_id: texture for texture in Texture.load_all(root, loader)
|
||||
},
|
||||
)
|
||||
|
||||
loader.export(
|
||||
metadata.path(props.modid),
|
||||
metadata.model_dump_json(by_alias=True, warnings=False),
|
||||
)
|
||||
|
||||
return loader.load_metadata(model_type=HexdocMetadata) | {props.modid: metadata}
|
||||
|
||||
|
||||
def load_book(
|
||||
props: Properties,
|
||||
pm: PluginManager,
|
||||
|
@ -37,10 +67,11 @@ def load_book(
|
|||
lang = props.default_lang
|
||||
|
||||
with ModResourceLoader.clean_and_load_all(props, pm) as loader:
|
||||
all_metadata = load_all_metadata(props, pm, loader)
|
||||
lang, i18n = _load_i18n(loader, lang, allow_missing)[0]
|
||||
|
||||
_, data = Book.load_book_json(loader, props.book)
|
||||
book = load_hex_book(data, pm, loader, i18n)
|
||||
book = load_hex_book(data, pm, loader, i18n, all_metadata)
|
||||
|
||||
return lang, book, i18n
|
||||
|
||||
|
@ -51,17 +82,20 @@ def load_books(
|
|||
lang: str | None,
|
||||
allow_missing: bool,
|
||||
):
|
||||
"""books, mod_metadata"""
|
||||
"""books, all_metadata"""
|
||||
|
||||
with ModResourceLoader.clean_and_load_all(props, pm) as loader:
|
||||
_, book_data = Book.load_book_json(loader, props.book)
|
||||
all_metadata = load_all_metadata(props, pm, loader)
|
||||
|
||||
_, book_data = Book.load_book_json(loader, props.book)
|
||||
books = dict[str, tuple[Book, I18n]]()
|
||||
|
||||
for lang, i18n in _load_i18n(loader, lang, allow_missing):
|
||||
books[lang] = (load_hex_book(book_data, pm, loader, i18n), i18n)
|
||||
book = load_hex_book(book_data, pm, loader, i18n, all_metadata)
|
||||
books[lang] = (book, i18n)
|
||||
loader.export_dir = None # only export the first (default) book
|
||||
|
||||
return books, loader.mod_metadata
|
||||
return books, all_metadata
|
||||
|
||||
|
||||
def _load_i18n(
|
|
@ -15,6 +15,7 @@ from jinja2 import (
|
|||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
from hexdoc.minecraft import I18n
|
||||
from hexdoc.minecraft.assets.textures import AnimatedTexture, Texture
|
||||
from hexdoc.patchouli import Book
|
||||
from hexdoc.utils import Properties
|
||||
from hexdoc.utils.jinja_extensions import (
|
||||
|
@ -24,8 +25,9 @@ from hexdoc.utils.jinja_extensions import (
|
|||
hexdoc_texture,
|
||||
hexdoc_wrap,
|
||||
)
|
||||
from hexdoc.utils.metadata import HexdocMetadata
|
||||
from hexdoc.utils.path import write_to_path
|
||||
from hexdoc.utils.resource_loader import HexdocMetadata
|
||||
from hexdoc.utils.resource import ResourceLocation
|
||||
|
||||
from .sitemap import MARKER_NAME, SitemapMarker
|
||||
|
||||
|
@ -64,7 +66,9 @@ def render_book(
|
|||
i18n: I18n,
|
||||
templates: dict[str, Template],
|
||||
output_dir: Path,
|
||||
mod_metadata: dict[str, HexdocMetadata],
|
||||
all_metadata: dict[str, HexdocMetadata],
|
||||
textures: dict[ResourceLocation, Texture],
|
||||
animations: list[AnimatedTexture],
|
||||
allow_missing: bool,
|
||||
version: str,
|
||||
is_root: bool,
|
||||
|
@ -89,10 +93,13 @@ def render_book(
|
|||
**props.template.args,
|
||||
"book": book,
|
||||
"props": props,
|
||||
"i18n": i18n,
|
||||
"page_url": page_url,
|
||||
"version": version,
|
||||
"lang": lang,
|
||||
"mod_metadata": mod_metadata,
|
||||
"all_metadata": all_metadata,
|
||||
"textures": textures,
|
||||
"animations": animations,
|
||||
"is_bleeding_edge": version == "latest",
|
||||
"_": lambda key: hexdoc_localize( # i18n helper
|
||||
key,
|
|
@ -10,6 +10,7 @@ from hexdoc.plugin import PluginManager
|
|||
from hexdoc.utils import HexdocModel, ModResourceLoader, ResourceLocation, init_context
|
||||
from hexdoc.utils.compat import HexVersion
|
||||
from hexdoc.utils.deserialize import cast_or_raise
|
||||
from hexdoc.utils.metadata import HexdocMetadata
|
||||
from hexdoc.utils.properties import PatternStubProps
|
||||
|
||||
from .pattern import Direction, PatternInfo
|
||||
|
@ -20,6 +21,7 @@ def load_hex_book(
|
|||
pm: PluginManager,
|
||||
loader: ModResourceLoader,
|
||||
i18n: I18n,
|
||||
all_metadata: dict[str, HexdocMetadata],
|
||||
):
|
||||
with init_context(data):
|
||||
context = HexContext(
|
||||
|
@ -28,6 +30,7 @@ def load_hex_book(
|
|||
i18n=i18n,
|
||||
# this SHOULD be set (as a ResourceLocation) by Book.get_book_json
|
||||
book_id=cast_or_raise(data["id"], ResourceLocation),
|
||||
all_metadata=all_metadata,
|
||||
)
|
||||
return Book.model_validate(data, context=context)
|
||||
|
||||
|
|
|
@ -4,10 +4,8 @@ __all__ = [
|
|||
"LocalizedStr",
|
||||
"Recipe",
|
||||
"Tag",
|
||||
"RenderedItemStack",
|
||||
]
|
||||
|
||||
from .assets import RenderedItemStack
|
||||
from .i18n import I18n, LocalizedItem, LocalizedStr
|
||||
from .recipe import Recipe
|
||||
from .tags import Tag
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
__all__ = [
|
||||
"MinecraftAssetsContext",
|
||||
"RenderedItemStack",
|
||||
"TextureContext",
|
||||
"TAG_TEXTURE",
|
||||
"MISSING_TEXTURE",
|
||||
]
|
||||
|
||||
from .assets import (
|
||||
MISSING_TEXTURE,
|
||||
TAG_TEXTURE,
|
||||
MinecraftAssetsContext,
|
||||
RenderedItemStack,
|
||||
)
|
||||
from .textures import MISSING_TEXTURE, TAG_TEXTURE, TextureContext
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
import logging
|
||||
from typing import Self
|
||||
|
||||
import requests
|
||||
from pydantic import Field, ValidationInfo, model_validator
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from hexdoc.minecraft.i18n import I18nContext, LocalizedStr
|
||||
from hexdoc.utils import ItemStack, ResourceLocation
|
||||
from hexdoc.utils.deserialize import cast_or_raise
|
||||
from hexdoc.utils.model import DEFAULT_CONFIG
|
||||
from hexdoc.utils.resource_loader import resolve_texture_from_metadata
|
||||
|
||||
# 16x16 hashtag icon for tags
|
||||
TAG_TEXTURE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAC4jAAAuIwF4pT92AAAANUlEQVQ4y2NgGJRAXV39v7q6+n9cfGTARKllFBvAiOxMUjTevHmTkSouGPhAHA0DWnmBrgAANLIZgSXEQxIAAAAASUVORK5CYII="
|
||||
|
||||
MISSING_TEXTURE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAJElEQVQoz2NkwAF+MPzAKs7EQCIY1UAMYMQV3hwMHKOhRD8NAPogBA/DVsDEAAAAAElFTkSuQmCC"
|
||||
|
||||
|
||||
class MinecraftAssetsContext(I18nContext):
|
||||
minecraft_textures: dict[ResourceLocation, str | None] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _fetch_minecraft_textures(self) -> Self:
|
||||
asset_props = self.props.minecraft_assets
|
||||
url = (
|
||||
"https://raw.githubusercontent.com/PrismarineJS/minecraft-assets"
|
||||
f"/{asset_props.ref}/data/{asset_props.version}/texture_content.json"
|
||||
)
|
||||
|
||||
logging.getLogger(__name__).info(f"Fetch textures from {url}")
|
||||
resp = requests.get(url)
|
||||
resp.raise_for_status()
|
||||
|
||||
textures_list: list[dict[{"name": str, "texture": str | None}]] = resp.json()
|
||||
|
||||
for item in textures_list:
|
||||
id = ResourceLocation("minecraft", item["name"])
|
||||
# items are first in the list (i think), so prioritize them
|
||||
if id not in self.minecraft_textures:
|
||||
self.minecraft_textures[id] = item["texture"]
|
||||
|
||||
return self
|
||||
|
||||
|
||||
@dataclass(config=DEFAULT_CONFIG, frozen=True, repr=False, kw_only=True)
|
||||
class RenderedItemStack(ItemStack):
|
||||
name: LocalizedStr | None = None
|
||||
texture: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _add_name_and_texture(self, info: ValidationInfo):
|
||||
"""Loads the recipe from json if the actual value is a resource location str."""
|
||||
if not info.context:
|
||||
return self
|
||||
context = cast_or_raise(info.context, MinecraftAssetsContext)
|
||||
|
||||
object.__setattr__(self, "name", context.i18n.localize_item(self))
|
||||
object.__setattr__(self, "texture", self._get_texture(context))
|
||||
|
||||
return self
|
||||
|
||||
def _get_texture(self, context: MinecraftAssetsContext):
|
||||
if self.namespace == "minecraft":
|
||||
return context.minecraft_textures[self.id]
|
||||
else:
|
||||
# check if there's an override to apply
|
||||
id = context.props.textures.override.get(self.id, self.id)
|
||||
|
||||
try:
|
||||
return resolve_texture_from_metadata(
|
||||
context.loader.mod_metadata,
|
||||
id.with_path(f"textures/item/{id.path}.png"),
|
||||
id.with_path(f"textures/block/{id.path}.png"),
|
||||
)
|
||||
|
||||
except KeyError as e:
|
||||
for missing_id in context.props.textures.missing:
|
||||
if id.match(missing_id):
|
||||
logging.getLogger(__name__).warn(f"No texture for {id}")
|
||||
return MISSING_TEXTURE
|
||||
raise KeyError(f"No texture for {id}") from e
|
213
doc/src/hexdoc/minecraft/assets/textures.py
Normal file
213
doc/src/hexdoc/minecraft/assets/textures.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import Literal, Self
|
||||
|
||||
from pydantic import Field, model_validator
|
||||
|
||||
from hexdoc.minecraft.i18n import I18nContext, LocalizedStr
|
||||
from hexdoc.utils import HexdocModel, ResourceLocation
|
||||
from hexdoc.utils.external import fetch_minecraft_textures
|
||||
from hexdoc.utils.properties import Properties
|
||||
from hexdoc.utils.resource_loader import ModResourceLoader
|
||||
from hexdoc.utils.resource_model import InlineModel
|
||||
|
||||
# 16x16 hashtag icon for tags
|
||||
TAG_TEXTURE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAC4jAAAuIwF4pT92AAAANUlEQVQ4y2NgGJRAXV39v7q6+n9cfGTARKllFBvAiOxMUjTevHmTkSouGPhAHA0DWnmBrgAANLIZgSXEQxIAAAAASUVORK5CYII="
|
||||
|
||||
# purple and black square
|
||||
MISSING_TEXTURE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAJElEQVQoz2NkwAF+MPzAKs7EQCIY1UAMYMQV3hwMHKOhRD8NAPogBA/DVsDEAAAAAElFTkSuQmCC"
|
||||
|
||||
|
||||
class Texture(InlineModel):
|
||||
file_id: ResourceLocation
|
||||
url: str | None
|
||||
meta: None = None
|
||||
|
||||
@classmethod
|
||||
def load_all(cls, root: Path, loader: ModResourceLoader):
|
||||
for _, id, path in loader.find_resources(
|
||||
"assets",
|
||||
namespace=loader.props.modid,
|
||||
folder="textures",
|
||||
glob=f"**/*.png",
|
||||
):
|
||||
texture = "/" + path.resolve().relative_to(root).as_posix()
|
||||
meta_path = path.with_suffix(".png.mcmeta")
|
||||
|
||||
if meta_path.is_file():
|
||||
yield AnimatedTexture(
|
||||
file_id=id,
|
||||
url=texture,
|
||||
meta=AnimationMeta.model_validate_json(meta_path.read_bytes()),
|
||||
)
|
||||
else:
|
||||
yield Texture(file_id=id, url=texture)
|
||||
|
||||
@classmethod
|
||||
def load_id(cls, id: ResourceLocation, context: TextureContext):
|
||||
"""Implements InlineModel."""
|
||||
return cls.find(id, props=context.props, textures=context.textures)
|
||||
|
||||
@classmethod
|
||||
def find(
|
||||
cls,
|
||||
*ids: ResourceLocation,
|
||||
props: Properties,
|
||||
textures: dict[ResourceLocation, Texture],
|
||||
):
|
||||
for id in ids:
|
||||
id = id.with_path(id.path.removeprefix("textures/"))
|
||||
if id in textures:
|
||||
return textures[id]
|
||||
|
||||
# fallback/error
|
||||
message = f"No texture for {', '.join(str(i) for i in ids)}"
|
||||
|
||||
for missing_id in props.textures.missing:
|
||||
for id in ids:
|
||||
if id.match(missing_id):
|
||||
logging.getLogger(__name__).warn(message)
|
||||
return Texture(file_id=id, url=MISSING_TEXTURE)
|
||||
|
||||
raise KeyError(message)
|
||||
|
||||
|
||||
class AnimatedTexture(Texture):
|
||||
meta: AnimationMeta
|
||||
|
||||
@property
|
||||
def class_name(self):
|
||||
return self.file_id.class_name
|
||||
|
||||
@property
|
||||
def time_seconds(self):
|
||||
return self.time / 20
|
||||
|
||||
@cached_property
|
||||
def time(self):
|
||||
return sum(time for _, time in self._normalized_frames)
|
||||
|
||||
@property
|
||||
def frames(self):
|
||||
start = 0
|
||||
for index, time in self._normalized_frames:
|
||||
yield AnimatedTextureFrame(
|
||||
index=index,
|
||||
start=start,
|
||||
time=time,
|
||||
animation_time=self.time,
|
||||
)
|
||||
start += time
|
||||
|
||||
@property
|
||||
def _normalized_frames(self):
|
||||
"""index, time"""
|
||||
animation = self.meta.animation
|
||||
|
||||
for i, frame in enumerate(animation.frames):
|
||||
match frame:
|
||||
case int(index):
|
||||
time = None
|
||||
case AnimationMetaFrame(index=index, time=time):
|
||||
pass
|
||||
|
||||
if index is None:
|
||||
index = i
|
||||
if time is None:
|
||||
time = animation.frametime
|
||||
|
||||
yield index, time
|
||||
|
||||
|
||||
class AnimatedTextureFrame(HexdocModel):
|
||||
index: int
|
||||
start: int
|
||||
time: int
|
||||
animation_time: int
|
||||
|
||||
@property
|
||||
def start_percent(self):
|
||||
return self._format_time(self.start)
|
||||
|
||||
@property
|
||||
def end_percent(self):
|
||||
return self._format_time(self.start + self.time, backoff=True)
|
||||
|
||||
def _format_time(self, time: int, *, backoff: bool = False) -> str:
|
||||
percent = 100 * time / self.animation_time
|
||||
if backoff and percent < 100:
|
||||
percent -= 0.0001
|
||||
return f"{percent:.4f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
class AnimationMeta(HexdocModel):
|
||||
animation: AnimationMetaTag
|
||||
|
||||
|
||||
class AnimationMetaTag(HexdocModel):
|
||||
interpolate: Literal[False] # TODO: handle interpolation
|
||||
width: None = None # TODO: handle non-square textures
|
||||
height: None = None
|
||||
frametime: int = 1
|
||||
frames: list[int | AnimationMetaFrame]
|
||||
|
||||
|
||||
class AnimationMetaFrame(HexdocModel):
|
||||
index: int | None = None
|
||||
time: int | None = None
|
||||
|
||||
|
||||
class TextureContext(I18nContext):
|
||||
textures: dict[ResourceLocation, Texture] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _add_minecraft_textures(self) -> Self:
|
||||
minecraft_textures = fetch_minecraft_textures(
|
||||
ref=self.props.minecraft_assets.ref,
|
||||
version=self.props.minecraft_assets.version,
|
||||
)
|
||||
|
||||
for item in minecraft_textures:
|
||||
id = ResourceLocation("minecraft", item["name"])
|
||||
# prefer items, since they're added first
|
||||
if id not in self.textures:
|
||||
self.textures[id] = Texture(file_id=id, url=item["texture"])
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class WithTexture(InlineModel):
|
||||
id: ResourceLocation
|
||||
name: LocalizedStr
|
||||
texture: Texture
|
||||
|
||||
|
||||
class ItemWithTexture(WithTexture):
|
||||
@classmethod
|
||||
def load_id(cls, id: ResourceLocation, context: TextureContext):
|
||||
"""Implements InlineModel."""
|
||||
texture_id = context.props.textures.override.get(id, id)
|
||||
return cls(
|
||||
id=id,
|
||||
name=context.i18n.localize_item(id),
|
||||
texture=Texture.find(
|
||||
texture_id,
|
||||
texture_id.with_path(f"item/{texture_id.path}.png"),
|
||||
texture_id.with_path(f"block/{texture_id.path}.png"),
|
||||
props=context.props,
|
||||
textures=context.textures,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TagWithTexture(WithTexture):
|
||||
@classmethod
|
||||
def load_id(cls, id: ResourceLocation, context: TextureContext):
|
||||
return cls(
|
||||
id=id,
|
||||
name=context.i18n.localize_item_tag(id),
|
||||
texture=Texture(file_id=id, url=TAG_TEXTURE),
|
||||
)
|
|
@ -219,17 +219,22 @@ class I18n(HexdocModel):
|
|||
f"hexcasting.{key_group}.{op_id}",
|
||||
)
|
||||
|
||||
def localize_item(self, item: ItemStack | str) -> LocalizedItem:
|
||||
def localize_item(self, item: str | ResourceLocation | ItemStack) -> LocalizedItem:
|
||||
"""Localizes the given item resource name.
|
||||
|
||||
Raises KeyError if i18n is enabled and skip_errors is False but the key has no localization.
|
||||
"""
|
||||
if isinstance(item, str):
|
||||
item = ItemStack.from_str(item)
|
||||
match item:
|
||||
case str():
|
||||
item = ItemStack.from_str(item)
|
||||
case ResourceLocation(namespace=namespace, path=path):
|
||||
item = ItemStack(namespace=namespace, path=path)
|
||||
case _:
|
||||
pass
|
||||
|
||||
localized = self.localize(
|
||||
item.i18n_key("block"),
|
||||
item.i18n_key(),
|
||||
item.i18n_key("block"),
|
||||
)
|
||||
return LocalizedItem(key=localized.key, value=localized.value)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from hexdoc.utils.resource_model import InlineIDModel
|
|||
from .ingredients import ItemResult
|
||||
|
||||
|
||||
class Recipe(InlineIDModel, TypeTaggedUnion, type=None):
|
||||
class Recipe(TypeTaggedUnion, InlineIDModel, type=None):
|
||||
group: str | None = None
|
||||
category: str | None = None
|
||||
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
from pydantic import ValidationInfo, model_validator
|
||||
|
||||
from hexdoc.minecraft import RenderedItemStack
|
||||
from hexdoc.minecraft.assets import TAG_TEXTURE
|
||||
from hexdoc.minecraft.i18n import I18nContext
|
||||
from hexdoc.utils import HexdocModel, NoValue, ResourceLocation, TypeTaggedUnion
|
||||
from hexdoc.utils.deserialize import cast_or_raise
|
||||
from hexdoc.minecraft.assets.textures import ItemWithTexture, TagWithTexture
|
||||
from hexdoc.utils import HexdocModel, NoValue, TypeTaggedUnion
|
||||
|
||||
|
||||
class ItemIngredient(TypeTaggedUnion, type=None):
|
||||
|
@ -15,29 +10,17 @@ ItemIngredientOrList = ItemIngredient | list[ItemIngredient]
|
|||
|
||||
|
||||
class MinecraftItemIdIngredient(ItemIngredient, type=NoValue):
|
||||
item: RenderedItemStack
|
||||
item: ItemWithTexture
|
||||
|
||||
|
||||
class MinecraftItemTagIngredient(ItemIngredient, type=NoValue):
|
||||
tag: ResourceLocation
|
||||
item: RenderedItemStack | None = None
|
||||
tag: TagWithTexture
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _add_item(self, info: ValidationInfo):
|
||||
if not info.context:
|
||||
return self
|
||||
context = cast_or_raise(info.context, I18nContext)
|
||||
|
||||
self.item = RenderedItemStack(
|
||||
namespace=self.tag.namespace,
|
||||
path=self.tag.path,
|
||||
name=context.i18n.localize_item_tag(self.tag),
|
||||
texture=TAG_TEXTURE,
|
||||
)
|
||||
|
||||
return self
|
||||
@property
|
||||
def item(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
class ItemResult(HexdocModel):
|
||||
item: RenderedItemStack
|
||||
item: ItemWithTexture
|
||||
count: int = 1
|
||||
|
|
|
@ -133,9 +133,7 @@ class Book(HexdocModel):
|
|||
# load categories
|
||||
self._categories = Category.load_all(context, self.id, self.use_resource_pack)
|
||||
for id, category in self._categories.items():
|
||||
self._link_bases[(id, None)] = context.loader.get_link_base(
|
||||
category.resource_dir
|
||||
)
|
||||
self._link_bases[(id, None)] = context.get_link_base(category.resource_dir)
|
||||
|
||||
if not self._categories:
|
||||
raise ValueError(
|
||||
|
@ -167,7 +165,7 @@ class Book(HexdocModel):
|
|||
):
|
||||
entry = Entry.load(resource_dir, id, data, context)
|
||||
|
||||
link_base = context.loader.get_link_base(resource_dir)
|
||||
link_base = context.get_link_base(resource_dir)
|
||||
self._link_bases[(id, None)] = link_base
|
||||
for page in entry.pages:
|
||||
if page.anchor is not None:
|
||||
|
|
|
@ -3,16 +3,27 @@ from typing import Self
|
|||
from pydantic import Field, model_validator
|
||||
|
||||
from hexdoc.minecraft import Tag
|
||||
from hexdoc.minecraft.assets import MinecraftAssetsContext
|
||||
from hexdoc.plugin.manager import PluginManagerContext
|
||||
from hexdoc.utils import ResourceLocation
|
||||
from hexdoc.utils.metadata import MetadataContext
|
||||
from hexdoc.utils.resource import PathResourceDir
|
||||
|
||||
from .text.formatting import FormattingContext
|
||||
|
||||
|
||||
class BookContext(FormattingContext, PluginManagerContext, MinecraftAssetsContext):
|
||||
class BookContext(
|
||||
FormattingContext,
|
||||
PluginManagerContext,
|
||||
MetadataContext,
|
||||
):
|
||||
spoilered_advancements: set[ResourceLocation] = Field(default_factory=set)
|
||||
|
||||
def get_link_base(self, resource_dir: PathResourceDir) -> str:
|
||||
modid = resource_dir.modid
|
||||
if modid is None or modid == self.props.modid:
|
||||
return ""
|
||||
return self.all_metadata[modid].book_url
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _post_root_load_tags(self) -> Self:
|
||||
self.spoilered_advancements |= Tag.load(
|
||||
|
|
|
@ -64,8 +64,12 @@ JSONDict = dict[str, "JSONValue"]
|
|||
JSONValue = JSONDict | list["JSONValue"] | str | int | float | bool | None
|
||||
|
||||
|
||||
def decode_json_dict(data: str) -> JSONDict:
|
||||
decoded = pyjson5.decode(data)
|
||||
def decode_json_dict(data: str | bytes) -> JSONDict:
|
||||
match data:
|
||||
case str():
|
||||
decoded = pyjson5.decode(data)
|
||||
case _:
|
||||
decoded = pyjson5.decode_utf8(data)
|
||||
assert isinstance_or_raise(decoded, dict)
|
||||
return decoded
|
||||
|
||||
|
|
26
doc/src/hexdoc/utils/external.py
Normal file
26
doc/src/hexdoc/utils/external.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
import logging
|
||||
from typing import TypedDict
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class MinecraftAssetsTextureContent(TypedDict):
|
||||
name: str
|
||||
texture: str | None
|
||||
|
||||
|
||||
def fetch_minecraft_textures(
|
||||
*,
|
||||
ref: str,
|
||||
version: str,
|
||||
) -> list[MinecraftAssetsTextureContent]:
|
||||
url = (
|
||||
"https://raw.githubusercontent.com/PrismarineJS/minecraft-assets"
|
||||
f"/{ref}/data/{version}/texture_content.json"
|
||||
)
|
||||
|
||||
logging.getLogger(__name__).info(f"Fetch textures from {url}")
|
||||
resp = requests.get(url)
|
||||
resp.raise_for_status()
|
||||
|
||||
return resp.json()
|
|
@ -15,3 +15,11 @@ def must_yield_something(f: Callable[_P, Iterator[_T]]) -> Callable[_P, Iterator
|
|||
yield from iterator
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def listify(f: Callable[_P, Iterator[_T]]) -> Callable[_P, list[_T]]:
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> list[_T]:
|
||||
return list(f(*args, **kwargs))
|
||||
|
||||
return wrapper
|
||||
|
|
|
@ -8,12 +8,12 @@ from markupsafe import Markup
|
|||
from pydantic import ConfigDict, validate_call
|
||||
|
||||
from hexdoc.minecraft import I18n, LocalizedStr
|
||||
from hexdoc.minecraft.assets.textures import Texture
|
||||
from hexdoc.patchouli import Book, FormatTree
|
||||
from hexdoc.patchouli.book import Book
|
||||
from hexdoc.patchouli.text import HTMLStream
|
||||
from hexdoc.patchouli.text.formatting import FormatTree
|
||||
from hexdoc.utils.resource import ResourceLocation
|
||||
from hexdoc.utils.resource_loader import HexdocMetadata, resolve_texture_from_metadata
|
||||
|
||||
from . import Properties
|
||||
from .deserialize import cast_or_raise
|
||||
|
@ -105,8 +105,12 @@ def hexdoc_localize(
|
|||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def hexdoc_texture(context: Context, id: ResourceLocation) -> str:
|
||||
try:
|
||||
mod_metadata = cast_or_raise(context["mod_metadata"], dict[str, HexdocMetadata])
|
||||
return resolve_texture_from_metadata(mod_metadata, id)
|
||||
props = cast_or_raise(context["props"], Properties)
|
||||
textures = cast_or_raise(context["textures"], dict[Any, Any])
|
||||
texture = Texture.find(id, props=props, textures=textures)
|
||||
|
||||
assert texture.url is not None
|
||||
return texture.url
|
||||
except Exception as e:
|
||||
e.add_note(f"id:\n {id}")
|
||||
raise
|
||||
|
|
35
doc/src/hexdoc/utils/metadata.py
Normal file
35
doc/src/hexdoc/utils/metadata.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from pathlib import Path
|
||||
from typing import Self
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from hexdoc.minecraft.assets.textures import Texture, TextureContext
|
||||
|
||||
from .model import HexdocModel
|
||||
from .properties import NoTrailingSlashHttpUrl
|
||||
from .resource import ResourceLocation
|
||||
|
||||
|
||||
class HexdocMetadata(HexdocModel):
|
||||
"""Automatically generated at `export_dir/modid.hexdoc.json`."""
|
||||
|
||||
book_url: NoTrailingSlashHttpUrl
|
||||
"""Github Pages base url."""
|
||||
asset_url: NoTrailingSlashHttpUrl
|
||||
"""raw.githubusercontent.com base url."""
|
||||
textures: dict[ResourceLocation, Texture]
|
||||
"""id -> path from repo root"""
|
||||
|
||||
@classmethod
|
||||
def path(cls, modid: str) -> Path:
|
||||
return Path(f"{modid}.hexdoc.json")
|
||||
|
||||
|
||||
class MetadataContext(TextureContext):
|
||||
all_metadata: dict[str, HexdocMetadata]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _add_metadata_textures(self) -> Self:
|
||||
for metadata in self.all_metadata.values():
|
||||
self.textures |= metadata.textures
|
||||
return self
|
|
@ -111,6 +111,11 @@ class ResourceLocation(BaseResourceLocation, regex=_make_regex()):
|
|||
def href(self) -> str:
|
||||
return f"#{self.path}"
|
||||
|
||||
@property
|
||||
def class_name(self):
|
||||
stripped_path = re.sub(r"[\*\/\.]", "-", self.path)
|
||||
return f"texture-{self.namespace}-{stripped_path}"
|
||||
|
||||
def with_namespace(self, namespace: str):
|
||||
"""Returns a copy of this ResourceLocation with the given namespace."""
|
||||
return ResourceLocation(namespace, self.path)
|
||||
|
|
|
@ -10,16 +10,15 @@ from pathlib import Path
|
|||
from textwrap import dedent
|
||||
from typing import Any, Callable, Literal, Self, TypeVar, overload
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from hexdoc.plugin.manager import PluginManager
|
||||
from hexdoc.utils.deserialize import JSONDict, decode_json_dict
|
||||
from hexdoc.utils.iterators import must_yield_something
|
||||
from hexdoc.utils.model import DEFAULT_CONFIG, HexdocModel, ValidationContext
|
||||
from hexdoc.utils.path import strip_suffixes, write_to_path
|
||||
|
||||
from .properties import NoTrailingSlashHttpUrl, Properties
|
||||
from .deserialize import JSONDict, decode_json_dict
|
||||
from .iterators import must_yield_something
|
||||
from .model import DEFAULT_CONFIG, HexdocModel, ValidationContext
|
||||
from .path import strip_suffixes, write_to_path
|
||||
from .properties import Properties
|
||||
from .resource import PathResourceDir, ResourceLocation, ResourceType
|
||||
|
||||
METADATA_SUFFIX = ".hexdoc.json"
|
||||
|
@ -30,32 +29,12 @@ _T_Model = TypeVar("_T_Model", bound=HexdocModel)
|
|||
ExportFn = Callable[[_T, _T | None], str]
|
||||
|
||||
|
||||
class HexdocMetadata(HexdocModel):
|
||||
"""Automatically generated at `export_dir/modid.hexdoc.json`."""
|
||||
|
||||
book_url: NoTrailingSlashHttpUrl
|
||||
"""Github Pages base url."""
|
||||
asset_url: NoTrailingSlashHttpUrl
|
||||
"""raw.githubusercontent.com base url."""
|
||||
|
||||
textures: dict[ResourceLocation, Path]
|
||||
"""id -> path from repo root"""
|
||||
sounds: dict[ResourceLocation, Path]
|
||||
"""id -> path from repo root"""
|
||||
|
||||
@classmethod
|
||||
def path(cls, modid: str) -> Path:
|
||||
return Path(f"{modid}.hexdoc.json")
|
||||
|
||||
|
||||
@dataclass(config=DEFAULT_CONFIG, kw_only=True)
|
||||
class ModResourceLoader:
|
||||
props: Properties
|
||||
export_dir: Path | None
|
||||
resource_dirs: list[PathResourceDir]
|
||||
|
||||
mod_metadata: dict[str, HexdocMetadata] = Field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def clean_and_load_all(
|
||||
cls,
|
||||
|
@ -90,13 +69,6 @@ class ModResourceLoader:
|
|||
export: bool = True,
|
||||
) -> Iterator[Self]:
|
||||
export_dir = props.export_dir if export else None
|
||||
version = pm.mod_version(props.modid)
|
||||
|
||||
repo_root = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
).stdout.strip()
|
||||
|
||||
with ExitStack() as stack:
|
||||
loader = cls(
|
||||
|
@ -108,20 +80,6 @@ class ModResourceLoader:
|
|||
for path_resource_dir in stack.enter_context(resource_dir.load(pm))
|
||||
],
|
||||
)
|
||||
loader.mod_metadata |= loader.load_metadata(model_type=HexdocMetadata)
|
||||
|
||||
# export this mod's metadata
|
||||
metadata = HexdocMetadata(
|
||||
book_url=f"{props.url}/v/{version}",
|
||||
asset_url=props.env.githubusercontent,
|
||||
textures=loader._map_own_assets("textures", root=repo_root),
|
||||
sounds=loader._map_own_assets("sounds", root=repo_root),
|
||||
)
|
||||
loader.mod_metadata[props.modid] = metadata
|
||||
loader.export(
|
||||
metadata.path(loader.props.modid),
|
||||
metadata.model_dump_json(by_alias=True, warnings=False),
|
||||
)
|
||||
|
||||
yield loader
|
||||
|
||||
|
@ -136,12 +94,6 @@ class ModResourceLoader:
|
|||
)
|
||||
}
|
||||
|
||||
def get_link_base(self, resource_dir: PathResourceDir) -> str:
|
||||
modid = resource_dir.modid
|
||||
if modid is None or modid == self.props.modid:
|
||||
return ""
|
||||
return self.mod_metadata[modid].book_url
|
||||
|
||||
def load_metadata(
|
||||
self,
|
||||
*,
|
||||
|
@ -490,17 +442,3 @@ class LoaderContext(ValidationContext):
|
|||
@property
|
||||
def props(self):
|
||||
return self.loader.props
|
||||
|
||||
|
||||
def resolve_texture_from_metadata(
|
||||
mod_metadata: dict[str, HexdocMetadata],
|
||||
id: ResourceLocation,
|
||||
*fallbacks: ResourceLocation,
|
||||
) -> str:
|
||||
try:
|
||||
metadata = mod_metadata[id.namespace]
|
||||
return f"{metadata.asset_url}/{metadata.textures[id].as_posix()}"
|
||||
except KeyError:
|
||||
if not fallbacks:
|
||||
raise
|
||||
return resolve_texture_from_metadata(mod_metadata, fallbacks[0], *fallbacks[1:])
|
||||
|
|
|
@ -34,14 +34,10 @@ class IDModel(HexdocModel):
|
|||
|
||||
|
||||
@dataclass_transform()
|
||||
class InlineIDModel(IDModel, ABC):
|
||||
class InlineModel(HexdocModel, ABC):
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def load_resource(
|
||||
cls,
|
||||
id: ResourceLocation,
|
||||
loader: ModResourceLoader,
|
||||
) -> tuple[PathResourceDir, JSONDict]:
|
||||
def load_id(cls, id: ResourceLocation, context: Any) -> Self:
|
||||
...
|
||||
|
||||
@model_validator(mode="wrap")
|
||||
|
@ -65,7 +61,23 @@ class InlineIDModel(IDModel, ABC):
|
|||
case _:
|
||||
return handler(value)
|
||||
|
||||
# load the recipe
|
||||
context = cast_or_raise(info.context, LoaderContext)
|
||||
# load the data
|
||||
context = cast_or_raise(info.context, ValidationContext)
|
||||
return cls.load_id(id, context)
|
||||
|
||||
|
||||
@dataclass_transform()
|
||||
class InlineIDModel(IDModel, InlineModel, ABC):
|
||||
@classmethod
|
||||
def load_id(cls, id: ResourceLocation, context: LoaderContext):
|
||||
resource_dir, data = cls.load_resource(id, context.loader)
|
||||
return cls.load(resource_dir, id, data, context)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def load_resource(
|
||||
cls,
|
||||
id: ResourceLocation,
|
||||
loader: ModResourceLoader,
|
||||
) -> tuple[PathResourceDir, JSONDict]:
|
||||
...
|
||||
|
|
Loading…
Reference in a new issue