Add WIP crafting table rendering
This commit is contained in:
parent
aa7d927e28
commit
0f6050fcca
35 changed files with 440 additions and 148 deletions
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
|
@ -16,9 +16,14 @@
|
|||
"name": "Python: Generate Docs",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "hexdoc.scripts.hexdoc",
|
||||
"module": "hexdoc.cli.main",
|
||||
"args": [
|
||||
"doc/properties.toml", "-o", "out", "--lang", "en_us",
|
||||
"render",
|
||||
"doc/properties.toml",
|
||||
"_site/src/docs",
|
||||
"--lang",
|
||||
"en_us",
|
||||
"--allow-missing",
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": false
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"watch": ["doc/src/hexdoc/_templates"],
|
||||
"ext": "jinja,html,css,js",
|
||||
"watch": ["doc/src/hexdoc/_templates", "doc/properties.toml"],
|
||||
"ignore": ["**/generated/**"],
|
||||
"ext": "jinja,html,css,js,toml,json,json5",
|
||||
"exec": "hexdoc serve doc/properties.toml --src _site/src/docs --dst _site/dst/docs --lang en_us --allow-missing --release"
|
||||
}
|
|
@ -26,6 +26,35 @@ regex = "{^_pattern_regex}"
|
|||
path = "{^_fabric.package}/FabricHexInitializer.kt"
|
||||
regex = "{^_pattern_regex}"
|
||||
|
||||
[minecraft_assets]
|
||||
# https://github.com/PrismarineJS/minecraft-assets/tree/83e2169afbbce40990d69fc53e5962e4a793d467/data/1.19.1
|
||||
ref = "83e2169afbbce40990d69fc53e5962e4a793d467"
|
||||
version = "1.19.1"
|
||||
|
||||
[textures]
|
||||
missing = [
|
||||
"farmersdelight:skillet",
|
||||
"hexcasting:*colorizer*",
|
||||
"hexcasting:amethyst_sconce",
|
||||
"hexcasting:edified_button",
|
||||
"hexcasting:edified_pressure_plate",
|
||||
"hexcasting:edified_slab",
|
||||
"hexcasting:edified_stairs",
|
||||
]
|
||||
|
||||
[textures.override]
|
||||
"hexcasting:akashic_connector" = "hexcasting:akashic_ligature"
|
||||
"hexcasting:directrix/empty" = "hexcasting:circle/directrix/empty/front_dim"
|
||||
"hexcasting:focus" = "hexcasting:focus_empty"
|
||||
"hexcasting:impetus/empty" = "hexcasting:circle/impetus/empty/front_dim"
|
||||
"hexcasting:quenched_allay_shard" = "hexcasting:quenched_shard_0"
|
||||
"hexcasting:scroll_large" = "hexcasting:scroll_pristine_large"
|
||||
"hexcasting:scroll_medium" = "hexcasting:scroll_pristine_medium"
|
||||
"hexcasting:scroll_small" = "hexcasting:scroll_pristine_small"
|
||||
"hexcasting:scroll" = "hexcasting:scroll_pristine_large"
|
||||
"hexcasting:slate_block" = "hexcasting:slate"
|
||||
"hexcasting:spellbook" = "hexcasting:spellbook_empty"
|
||||
"hexcasting:staff/quenched" = "hexcasting:staff/quenched_0"
|
||||
|
||||
[template]
|
||||
static_dir = "static"
|
||||
|
|
|
@ -38,23 +38,16 @@
|
|||
not_supported: "Your browser does not support visualizing patterns. Pattern code: {}",
|
||||
},
|
||||
|
||||
recipe: {
|
||||
show: "Click to show recipe: {}",
|
||||
hide: "Click to hide recipe: {}",
|
||||
},
|
||||
|
||||
pages: {
|
||||
patchouli: {
|
||||
crafting: {
|
||||
description: "Depicted in the book: The crafting recipe for the",
|
||||
separator: " and ",
|
||||
},
|
||||
},
|
||||
|
||||
hexcasting: {
|
||||
crafting_multi: {
|
||||
description: "Depicted in the book: Several crafting recipes, for the",
|
||||
separator: ", ",
|
||||
},
|
||||
|
||||
brainsweep: {
|
||||
description: "Depicted in the book: A mind-flaying recipe producing the",
|
||||
separator: "", // this value intentionally left blank
|
||||
separator: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 471 B |
|
@ -1,8 +1,8 @@
|
|||
{% import "common/macros.html.jinja" as macros %}
|
||||
{% import "macros/formatting.html.jinja" as formatting %}
|
||||
|
||||
<section id="{{ category.id.path }}">
|
||||
{% call macros.maybe_spoilered(category) %}
|
||||
{{- macros.section_header(category, "h2", "category-title") }}
|
||||
{% call formatting.maybe_spoilered(category) %}
|
||||
{{- formatting.section_header(category, "h2", "category-title") }}
|
||||
{{ category.description|hexdoc_block }}
|
||||
{% endcall %}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% import "common/macros.html.jinja" as macros -%}
|
||||
{% import "macros/formatting.html.jinja" as formatting -%}
|
||||
|
||||
{# put the link target outside of toc-container so jump to top still works in sidebar mode #}
|
||||
<div id="table-of-contents"></div>
|
||||
|
@ -9,18 +9,18 @@
|
|||
class="permalink toggle-link small"
|
||||
data-target="toc-category"
|
||||
title="{{ _('hexdoc.toc.toggle_all') }}"
|
||||
><i class="bi bi-list-nested"></i></a>{{ macros.permalink("table-of-contents", "toc-permalink") }}</span>
|
||||
><i class="bi bi-list-nested"></i></a>{{ formatting.permalink("table-of-contents", "toc-permalink") }}</span>
|
||||
</h2>
|
||||
|
||||
{% for category in book.categories.values() if category.entries.values() %}
|
||||
<details class="toc-category">
|
||||
{# category #}
|
||||
<summary>{{ macros.maybe_spoilered_link(category) }}</summary>
|
||||
<summary>{{ formatting.maybe_spoilered_link(category) }}</summary>
|
||||
|
||||
{# list of entries in the category #}
|
||||
<ul>
|
||||
{% for entry in category.entries.values() %}
|
||||
<li>{{ macros.maybe_spoilered_link(entry) }}</li>
|
||||
<li>{{ formatting.maybe_spoilered_link(entry) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% import "common/macros.html.jinja" as macros -%}
|
||||
{% import "macros/formatting.html.jinja" as formatting -%}
|
||||
|
||||
<div id="{{ entry.id.path }}">
|
||||
{% call macros.maybe_spoilered(entry) %}
|
||||
{{- macros.section_header(entry, "h3", "entry-title") }}
|
||||
{% call formatting.maybe_spoilered(entry) %}
|
||||
{{- formatting.section_header(entry, "h3", "entry-title") }}
|
||||
|
||||
{% for page in entry.pages +%}
|
||||
{% include page.template %}
|
||||
|
|
|
@ -38,15 +38,3 @@
|
|||
{{ caller() }}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{# shows the names of all the recipe results in a list of recipes #}
|
||||
{% macro recipe_block(recipes, result_attribute, description, separator) -%}
|
||||
<blockquote class="crafting-info">
|
||||
{{ description }} {{
|
||||
recipes
|
||||
|map(attribute="result." ~ result_attribute)
|
||||
|map("hexdoc_wrap", "code")
|
||||
|join(separator)
|
||||
}}.
|
||||
</blockquote>
|
||||
{%- endmacro %}
|
64
doc/src/hexdoc/_templates/macros/recipes.html.jinja
Normal file
64
doc/src/hexdoc/_templates/macros/recipes.html.jinja
Normal file
|
@ -0,0 +1,64 @@
|
|||
{# show the names of all the recipe results in a list of recipes #}
|
||||
{% macro generic(recipes, result_attribute, description, separator) -%}
|
||||
<blockquote class="crafting-info">
|
||||
{{ description }} {{
|
||||
recipes
|
||||
|map(attribute="result."~result_attribute)
|
||||
|map("hexdoc_wrap", "code")
|
||||
|join(separator)
|
||||
}}.
|
||||
</blockquote>
|
||||
{%- endmacro %}
|
||||
|
||||
{# 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>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro render_ingredient(ingredient) -%}
|
||||
{% if ingredient is sequence %}
|
||||
{# TODO: properly handle multi-option ingredients #}
|
||||
{{ render_ingredient(ingredient[0]) }}
|
||||
{% elif ingredient is none %}
|
||||
{# air #}
|
||||
<div></div>
|
||||
{% elif ingredient.type|string == "hexcasting:mod_conditional" %}
|
||||
{{ render_ingredient(ingredient.default) }}
|
||||
{% else %}
|
||||
{{ render_item(ingredient.item) }}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{# render a crafting table for each recipe #}
|
||||
{% macro crafting_table(recipes) -%}
|
||||
{% for recipe in recipes %}
|
||||
{% if loop.index0 > 0 %}<br />{% endif %}
|
||||
<details class="details-collapsible">
|
||||
<summary class="collapse-details">
|
||||
<span class="collapse-recipe-show">{{ _('hexdoc.recipe.show').format(recipe.result.item.name) }}</span>
|
||||
<span class="collapse-recipe-hide">{{ _('hexdoc.recipe.hide').format(recipe.result.item.name) }}</span>
|
||||
</summary>
|
||||
<div class="crafting-table">
|
||||
<img
|
||||
alt="Crafting table"
|
||||
src="{{ 'hexcasting:textures/gui/hexdoc/crafting_table.png'|hexdoc_texture }}"
|
||||
>
|
||||
|
||||
<div class="crafting-table-grid">
|
||||
{% for item in recipe.ingredients %}
|
||||
{{ render_ingredient(item) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="crafting-table-result">
|
||||
{{ render_item(recipe.result.item, recipe.result.count) }}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
|
@ -2,7 +2,7 @@ summary {
|
|||
display: list-item;
|
||||
}
|
||||
|
||||
details.spell-collapsible {
|
||||
.details-collapsible {
|
||||
display: inline-block;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
|
@ -10,31 +10,89 @@ details.spell-collapsible {
|
|||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
summary.collapse-spell {
|
||||
.collapse-details {
|
||||
font-weight: bold;
|
||||
margin: -0.5em -0.5em 0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
details.spell-collapsible[open] {
|
||||
.details-collapsible[open] {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
details[open] summary.collapse-spell {
|
||||
.details-collapsible[open] .collapse-details {
|
||||
border-bottom: 1px solid #aaa;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
details .collapse-spell::before {
|
||||
.details-collapsible .collapse-details.collapse-spell::before {
|
||||
content: "{{ _('hexdoc.pattern.show') }}";
|
||||
}
|
||||
|
||||
details[open] .collapse-spell::before {
|
||||
.details-collapsible[open] .collapse-details.collapse-spell::before {
|
||||
content: "{{ _('hexdoc.pattern.hide') }}";
|
||||
}
|
||||
|
||||
blockquote.crafting-info {
|
||||
font-size: inherit;
|
||||
/* if the collapsible is closed, hide the "hide recipe" message */
|
||||
.details-collapsible:not([open]) > .collapse-details > .collapse-recipe-hide {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* if the collapsible is open, hide the "show recipe" message */
|
||||
.details-collapsible[open] > .collapse-details > .collapse-recipe-show {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* we do a bit of crafting (mostly stolen from https://computercraft.info/wiki) */
|
||||
|
||||
.crafting-table {
|
||||
position: relative;
|
||||
width: 256px;
|
||||
height: 132px;
|
||||
|
||||
background-color: #c6c6c6;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.crafting-table > img { /* background */
|
||||
image-rendering: pixelated;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.crafting-table-grid {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
|
||||
display: grid;
|
||||
grid: repeat(3, 32px) / repeat(3, 32px);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.crafting-table-result {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 202px;
|
||||
}
|
||||
|
||||
.crafting-table-result .badge {
|
||||
position: absolute;
|
||||
background-color: #292929;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
:is(.crafting-table-grid, .crafting-table-result) :is(img, span) {
|
||||
image-rendering: pixelated;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a.permalink {
|
||||
|
|
|
@ -297,8 +297,7 @@ function hookSpoiler(elem) {
|
|||
}
|
||||
|
||||
// these are filled by Jinja
|
||||
const ROOT_URL = "{{ props.url }}";
|
||||
const PAGE_URL = "{{ page_url }}";
|
||||
const BOOK_URL = "{{ props.url }}";
|
||||
|
||||
const VERSION = "{{ version }}";
|
||||
const LANG = "{{ lang }}";
|
||||
|
@ -325,7 +324,7 @@ function versionDropdownItem(sitemap, version) {
|
|||
path = defaultPath;
|
||||
}
|
||||
|
||||
return dropdownItem(version, ROOT_URL + path);
|
||||
return dropdownItem(version, BOOK_URL + path);
|
||||
}
|
||||
|
||||
function versionDropdownItems(sitemap, versions) {
|
||||
|
@ -390,7 +389,7 @@ function addDropdowns(sitemap) {
|
|||
const langs = Object.keys(langPaths).sort();
|
||||
|
||||
document.getElementById("lang-dropdown").append(
|
||||
...langs.map((lang) => dropdownItem(lang, ROOT_URL + langPaths[lang])),
|
||||
...langs.map((lang) => dropdownItem(lang, BOOK_URL + langPaths[lang])),
|
||||
);
|
||||
|
||||
// return sitemap for chaining, i guess
|
||||
|
@ -399,12 +398,12 @@ function addDropdowns(sitemap) {
|
|||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// fetch the sitemap from the root and use it to generate the navbar
|
||||
fetch(`${ROOT_URL}/meta/sitemap.json`)
|
||||
fetch(`${BOOK_URL}/meta/sitemap.json`)
|
||||
.then(r => r.json())
|
||||
.then(addDropdowns)
|
||||
.catch(e => console.error(e))
|
||||
|
||||
document.querySelectorAll("details.spell-collapsible").forEach(hookLoad);
|
||||
document.querySelectorAll(".details-collapsible").forEach(hookLoad);
|
||||
document.querySelectorAll("a.toggle-link").forEach(hookToggle);
|
||||
document.querySelectorAll(".spoilered").forEach(hookSpoiler);
|
||||
|
||||
|
@ -417,3 +416,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
}
|
||||
requestAnimationFrame((t) => tick(t, t));
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "pages/patchouli/page.html.jinja" %}
|
||||
{% include "common/macros.html.jinja" with context %}
|
||||
{% import "macros/recipes.html.jinja" as recipe_macros with context %}
|
||||
|
||||
{% block body %}
|
||||
{{ macros.recipe_block(
|
||||
{{ recipe_macros.generic(
|
||||
[page.recipe],
|
||||
"name",
|
||||
_("hexdoc.pages.hexcasting.brainsweep.description"),
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
{% extends "pages/patchouli/text.html.jinja" %}
|
||||
{% include "common/macros.html.jinja" with context %}
|
||||
{% import "macros/recipes.html.jinja" as recipe_macros with context %}
|
||||
|
||||
{% block inner_body %}
|
||||
{{ macros.recipe_block(
|
||||
page.recipes,
|
||||
"item",
|
||||
_("hexdoc.pages.hexcasting.crafting_multi.description"),
|
||||
_("hexdoc.pages.hexcasting.crafting_multi.separator"),
|
||||
) }}
|
||||
{{ recipe_macros.crafting_table(page.recipes) }}
|
||||
{{ super() }}
|
||||
{% endblock inner_body %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "pages/patchouli/text.html.jinja" %}
|
||||
|
||||
{% block inner_body %}
|
||||
<details class="spell-collapsible">
|
||||
<summary class="collapse-spell"></summary>
|
||||
<details class="details-collapsible">
|
||||
<summary class="collapse-details collapse-spell"></summary>
|
||||
{% for pattern in page.patterns %}
|
||||
<canvas
|
||||
class="spell-viz"
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
{% extends "pages/patchouli/text.html.jinja" %}
|
||||
{% include "common/macros.html.jinja" %}
|
||||
{% import "macros/recipes.html.jinja" as recipe_macros with context %}
|
||||
|
||||
{% block inner_body %}
|
||||
{{ macros.recipe_block(
|
||||
page.recipes,
|
||||
"item",
|
||||
_("hexdoc.pages.patchouli.crafting.description"),
|
||||
_("hexdoc.pages.patchouli.crafting.separator"),
|
||||
) }}
|
||||
{{ recipe_macros.crafting_table(page.recipes) }}
|
||||
{{ super() }}
|
||||
{% endblock inner_body %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% block inner_body %}
|
||||
<p class="img-wrapper">
|
||||
{% for image in page.images %}
|
||||
<img src="{{ image|hexdoc_texture_url }}"></img>
|
||||
<img src="{{ image|hexdoc_texture }}"></img>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{{ super() }}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "pages/patchouli/page.html.jinja" %}
|
||||
{% import "common/macros.html.jinja" as macros %}
|
||||
{% import "macros/formatting.html.jinja" as macros %}
|
||||
|
||||
{% block body %}
|
||||
{% if page.title is not none %}
|
||||
|
|
|
@ -26,7 +26,7 @@ RequiredPathOption = Annotated[Path, typer.Option()]
|
|||
UpdateLatestOption = Annotated[bool, typer.Option(envvar="UPDATE_LATEST")]
|
||||
ReleaseOption = Annotated[bool, typer.Option(envvar="RELEASE")]
|
||||
|
||||
app = typer.Typer()
|
||||
app = typer.Typer(pretty_exceptions_enable=False)
|
||||
|
||||
|
||||
@app.command()
|
||||
|
@ -158,11 +158,10 @@ def serve(
|
|||
):
|
||||
book_path = dst.resolve().relative_to(Path.cwd())
|
||||
|
||||
base_url = f"http://localhost:{port}"
|
||||
book_url = f"{base_url}/{book_path.as_posix()}"
|
||||
book_url = f"/{book_path.as_posix()}"
|
||||
|
||||
os.environ |= {
|
||||
"DEBUG_GITHUBUSERCONTENT": base_url,
|
||||
"DEBUG_GITHUBUSERCONTENT": "",
|
||||
"GITHUB_PAGES_URL": book_url,
|
||||
}
|
||||
|
||||
|
@ -186,7 +185,9 @@ def serve(
|
|||
release=release,
|
||||
)
|
||||
|
||||
print(f"Serving web book at {book_url} (press ctrl+c to exit)\n")
|
||||
print(
|
||||
f"Serving web book at http://localhost:{port}{book_url} (press ctrl+c to exit)\n"
|
||||
)
|
||||
with HTTPServer(("", port), SimpleHTTPRequestHandler) as httpd:
|
||||
httpd.serve_forever()
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ from hexdoc.utils.jinja_extensions import (
|
|||
IncludeRawExtension,
|
||||
hexdoc_block,
|
||||
hexdoc_localize,
|
||||
hexdoc_texture_url,
|
||||
hexdoc_texture,
|
||||
hexdoc_wrap,
|
||||
)
|
||||
from hexdoc.utils.path import write_to_path
|
||||
|
@ -50,7 +50,7 @@ def create_jinja_env(props: Properties):
|
|||
"hexdoc_block": hexdoc_block,
|
||||
"hexdoc_wrap": hexdoc_wrap,
|
||||
"hexdoc_localize": hexdoc_localize,
|
||||
"hexdoc_texture_url": hexdoc_texture_url,
|
||||
"hexdoc_texture": hexdoc_texture,
|
||||
}
|
||||
|
||||
return env
|
||||
|
|
|
@ -4,8 +4,10 @@ __all__ = [
|
|||
"LocalizedStr",
|
||||
"Recipe",
|
||||
"Tag",
|
||||
"RenderedItemStack",
|
||||
]
|
||||
|
||||
from .assets import RenderedItemStack
|
||||
from .i18n import I18n, LocalizedItem, LocalizedStr
|
||||
from .recipe import Recipe
|
||||
from .tags import Tag
|
||||
|
|
82
doc/src/hexdoc/minecraft/assets.py
Normal file
82
doc/src/hexdoc/minecraft/assets.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
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
|
|
@ -20,7 +20,7 @@ from hexdoc.utils.resource_loader import LoaderContext
|
|||
|
||||
|
||||
@total_ordering
|
||||
class LocalizedStr(HexdocModel):
|
||||
class LocalizedStr(HexdocModel, frozen=True):
|
||||
"""Represents a string which has been localized."""
|
||||
|
||||
key: str
|
||||
|
@ -83,7 +83,7 @@ class LocalizedStr(HexdocModel):
|
|||
return self.value < other
|
||||
|
||||
|
||||
class LocalizedItem(LocalizedStr):
|
||||
class LocalizedItem(LocalizedStr, frozen=True):
|
||||
@classmethod
|
||||
def _localize(cls, i18n: I18n, key: str) -> Self:
|
||||
return i18n.localize_item(key)
|
||||
|
@ -253,6 +253,10 @@ class I18n(HexdocModel):
|
|||
def localize_key(self, key: str) -> LocalizedStr:
|
||||
return self.localize(f"key.{key}")
|
||||
|
||||
def localize_tag(self, tag: ResourceLocation):
|
||||
localized = self.localize(f"tag.{tag.namespace}.{tag.path}")
|
||||
return LocalizedStr(key=localized.key, value=f"Tag: {localized.value}")
|
||||
|
||||
|
||||
class I18nContext(LoaderContext):
|
||||
i18n: I18n
|
||||
|
|
|
@ -10,16 +10,12 @@ __all__ = [
|
|||
"ItemResult",
|
||||
]
|
||||
|
||||
from .abstract_recipes import Recipe
|
||||
from .abstract_recipes import CraftingRecipe, Recipe
|
||||
from .ingredients import (
|
||||
ItemIngredient,
|
||||
ItemIngredientOrList,
|
||||
ItemResult,
|
||||
MinecraftItemIdIngredient,
|
||||
MinecraftItemTagIngredient,
|
||||
)
|
||||
from .recipes import (
|
||||
CraftingRecipe,
|
||||
CraftingShapedRecipe,
|
||||
CraftingShapelessRecipe,
|
||||
ItemResult,
|
||||
)
|
||||
from .recipes import CraftingShapedRecipe, CraftingShapelessRecipe
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Self
|
||||
|
||||
from pydantic import ValidationInfo, model_validator
|
||||
from pydantic.functional_validators import ModelWrapValidatorHandler
|
||||
|
||||
from hexdoc.utils import ResourceLocation, TypeTaggedUnion
|
||||
from hexdoc.utils.deserialize import cast_or_raise
|
||||
from hexdoc.utils.resource_loader import LoaderContext
|
||||
|
||||
from .ingredients import ItemResult
|
||||
|
||||
|
||||
class Recipe(TypeTaggedUnion, type=None):
|
||||
id: ResourceLocation
|
||||
|
@ -14,23 +17,34 @@ class Recipe(TypeTaggedUnion, type=None):
|
|||
group: str | None = None
|
||||
category: str | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
def _pre_root(cls, values: Any, info: ValidationInfo):
|
||||
# use wrap validator so we load the file *before* resolving the tagged union
|
||||
@model_validator(mode="wrap")
|
||||
@classmethod
|
||||
def _pre_root(
|
||||
cls,
|
||||
value: Any,
|
||||
handler: ModelWrapValidatorHandler[Self],
|
||||
info: ValidationInfo,
|
||||
) -> Self:
|
||||
"""Loads the recipe from json if the actual value is a resource location str."""
|
||||
if not info.context:
|
||||
return values
|
||||
return handler(value)
|
||||
context = cast_or_raise(info.context, LoaderContext)
|
||||
|
||||
# if necessary, convert the id to a ResourceLocation
|
||||
match values:
|
||||
match value:
|
||||
case str():
|
||||
id = ResourceLocation.from_str(values)
|
||||
id = ResourceLocation.from_str(value)
|
||||
case ResourceLocation():
|
||||
id = values
|
||||
id = value
|
||||
case _:
|
||||
return values
|
||||
return handler(value)
|
||||
|
||||
# load the recipe
|
||||
_, data = context.loader.load_resource("data", "recipes", id)
|
||||
logging.getLogger(__name__).debug(f"Load {cls} from {id}")
|
||||
return data | {"id": id}
|
||||
return handler(data | {"id": id})
|
||||
|
||||
|
||||
class CraftingRecipe(Recipe, type=None):
|
||||
result: ItemResult
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
from hexdoc.utils import NoValue, ResourceLocation, TypeTaggedUnion
|
||||
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
|
||||
|
||||
|
||||
class ItemIngredient(TypeTaggedUnion, type=None):
|
||||
|
@ -9,8 +15,29 @@ ItemIngredientOrList = ItemIngredient | list[ItemIngredient]
|
|||
|
||||
|
||||
class MinecraftItemIdIngredient(ItemIngredient, type=NoValue):
|
||||
item: ResourceLocation
|
||||
item: RenderedItemStack
|
||||
|
||||
|
||||
class MinecraftItemTagIngredient(ItemIngredient, type=NoValue):
|
||||
tag: ResourceLocation
|
||||
item: RenderedItemStack | None = None
|
||||
|
||||
@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_tag(self.tag),
|
||||
texture=TAG_TEXTURE,
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class ItemResult(HexdocModel):
|
||||
item: RenderedItemStack
|
||||
count: int = 1
|
||||
|
|
|
@ -1,24 +1,34 @@
|
|||
from typing import Iterator
|
||||
|
||||
from pydantic import field_validator
|
||||
|
||||
from hexdoc.utils import HexdocModel
|
||||
from hexdoc.utils.compat import HexVersion
|
||||
|
||||
from ..i18n import LocalizedItem
|
||||
from .abstract_recipes import Recipe
|
||||
from .abstract_recipes import CraftingRecipe
|
||||
from .ingredients import ItemIngredientOrList
|
||||
|
||||
|
||||
class ItemResult(HexdocModel):
|
||||
item: LocalizedItem
|
||||
count: int | None = None
|
||||
class CraftingShapelessRecipe(CraftingRecipe, type="minecraft:crafting_shapeless"):
|
||||
ingredients: list[ItemIngredientOrList]
|
||||
|
||||
|
||||
class CraftingShapedRecipe(Recipe, type="minecraft:crafting_shaped"):
|
||||
class CraftingShapedRecipe(CraftingRecipe, type="minecraft:crafting_shaped"):
|
||||
key: dict[str, ItemIngredientOrList]
|
||||
pattern: list[str]
|
||||
result: ItemResult
|
||||
show_notification: bool | None = None
|
||||
|
||||
@property
|
||||
def ingredients(self) -> Iterator[ItemIngredientOrList | None]:
|
||||
for row in self.pattern:
|
||||
if len(row) > 3:
|
||||
raise ValueError(f"Expected len(row) <= 3, got {len(row)}: `{row}`")
|
||||
for item_key in row.ljust(3):
|
||||
match item_key:
|
||||
case " ":
|
||||
yield None
|
||||
case _:
|
||||
yield self.key[item_key]
|
||||
|
||||
@field_validator("show_notification")
|
||||
@classmethod
|
||||
def _check_show_notification(cls, value: bool | None):
|
||||
|
@ -28,11 +38,3 @@ class CraftingShapedRecipe(Recipe, type="minecraft:crafting_shaped"):
|
|||
case HexVersion.v0_10_x | HexVersion.v0_9_x:
|
||||
HexVersion.check(value is None, "show_notification")
|
||||
return value
|
||||
|
||||
|
||||
class CraftingShapelessRecipe(Recipe, type="minecraft:crafting_shapeless"):
|
||||
ingredients: list[ItemIngredientOrList]
|
||||
result: ItemResult
|
||||
|
||||
|
||||
CraftingRecipe = CraftingShapedRecipe | CraftingShapelessRecipe
|
||||
|
|
|
@ -3,13 +3,14 @@ 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 .text.formatting import FormattingContext
|
||||
|
||||
|
||||
class BookContext(FormattingContext, PluginManagerContext):
|
||||
class BookContext(FormattingContext, PluginManagerContext, MinecraftAssetsContext):
|
||||
spoilered_advancements: set[ResourceLocation] = Field(default_factory=set)
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
|
|
@ -5,7 +5,7 @@ _T = TypeVar("_T")
|
|||
_P = ParamSpec("_P")
|
||||
|
||||
|
||||
def must_yield(f: Callable[_P, Iterator[_T]]) -> Callable[_P, Iterator[_T]]:
|
||||
def must_yield_something(f: Callable[_P, Iterator[_T]]) -> Callable[_P, Iterator[_T]]:
|
||||
"""Raises StopIteration if the wrapped iterator doesn't yield anything."""
|
||||
|
||||
@functools.wraps(f)
|
||||
|
|
|
@ -5,6 +5,7 @@ from jinja2.ext import Extension
|
|||
from jinja2.parser import Parser
|
||||
from jinja2.runtime import Context
|
||||
from markupsafe import Markup
|
||||
from pydantic import ConfigDict, validate_call
|
||||
|
||||
from hexdoc.minecraft import I18n, LocalizedStr
|
||||
from hexdoc.patchouli import Book, FormatTree
|
||||
|
@ -12,7 +13,7 @@ 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
|
||||
from hexdoc.utils.resource_loader import HexdocMetadata, resolve_texture_from_metadata
|
||||
|
||||
from . import Properties
|
||||
from .deserialize import cast_or_raise
|
||||
|
@ -101,13 +102,11 @@ def hexdoc_localize(
|
|||
|
||||
|
||||
@pass_context
|
||||
def hexdoc_texture_url(context: Context, id: ResourceLocation) -> str:
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def hexdoc_texture(context: Context, id: ResourceLocation) -> str:
|
||||
try:
|
||||
metadata = cast_or_raise(
|
||||
context["mod_metadata"],
|
||||
dict[str, HexdocMetadata],
|
||||
)[id.namespace]
|
||||
return f"{metadata.asset_url}/{metadata.textures[id].as_posix()}"
|
||||
mod_metadata = cast_or_raise(context["mod_metadata"], dict[str, HexdocMetadata])
|
||||
return resolve_texture_from_metadata(mod_metadata, id)
|
||||
except Exception as e:
|
||||
e.add_note(f"id:\n {id}")
|
||||
raise
|
||||
|
|
|
@ -87,6 +87,16 @@ class TemplateProps(StripHiddenModel):
|
|||
return values
|
||||
|
||||
|
||||
class MinecraftAssetsProps(StripHiddenModel):
|
||||
ref: str
|
||||
version: str
|
||||
|
||||
|
||||
class TexturesProps(StripHiddenModel):
|
||||
missing: list[ResourceLocation]
|
||||
override: dict[ResourceLocation, ResourceLocation]
|
||||
|
||||
|
||||
class Properties(StripHiddenModel):
|
||||
env: EnvironmentVariableProps
|
||||
|
||||
|
@ -104,6 +114,11 @@ class Properties(StripHiddenModel):
|
|||
|
||||
entry_id_blacklist: set[ResourceLocation] = Field(default_factory=set)
|
||||
|
||||
minecraft_assets: MinecraftAssetsProps
|
||||
|
||||
# FIXME: remove this and get the data from the actual model files
|
||||
textures: TexturesProps
|
||||
|
||||
template: TemplateProps
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -90,6 +90,10 @@ class BaseResourceLocation:
|
|||
def _ser_model(self) -> str:
|
||||
return str(self)
|
||||
|
||||
@property
|
||||
def id(self) -> ResourceLocation:
|
||||
return ResourceLocation(self.namespace, self.path)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.namespace}:{self.path}"
|
||||
|
||||
|
@ -175,6 +179,9 @@ class ItemStack(BaseResourceLocation, regex=_make_regex(count=True, nbt=True)):
|
|||
count: int | None = None
|
||||
nbt: str | None = None
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any):
|
||||
super().__init_subclass__(regex=cls._from_str_regex, **kwargs)
|
||||
|
||||
def i18n_key(self, root: str = "item") -> str:
|
||||
# TODO: is this how i18n works????? (apparently, because it's working)
|
||||
return f"{root}.{self.namespace}.{self.path.replace('/', '.')}"
|
||||
|
|
|
@ -15,7 +15,7 @@ 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
|
||||
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
|
||||
|
||||
|
@ -47,12 +47,6 @@ class HexdocMetadata(HexdocModel):
|
|||
def path(cls, modid: str) -> Path:
|
||||
return Path(f"{modid}.hexdoc.json")
|
||||
|
||||
def export(self, loader: ModResourceLoader):
|
||||
loader.export(
|
||||
self.path(loader.props.modid),
|
||||
self.model_dump_json(by_alias=True, warnings=False),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(config=DEFAULT_CONFIG, kw_only=True)
|
||||
class ModResourceLoader:
|
||||
|
@ -114,21 +108,23 @@ 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
|
||||
loader.mod_metadata[props.modid] = metadata = HexdocMetadata(
|
||||
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),
|
||||
)
|
||||
metadata.export(loader)
|
||||
loader.mod_metadata[props.modid] = metadata
|
||||
loader.export(
|
||||
metadata.path(loader.props.modid),
|
||||
metadata.model_dump_json(by_alias=True, warnings=False),
|
||||
)
|
||||
|
||||
yield loader
|
||||
|
||||
def __post_init__(self):
|
||||
self.mod_metadata |= self.load_metadata(model_type=HexdocMetadata)
|
||||
|
||||
def _map_own_assets(self, folder: str, *, root: str | Path):
|
||||
return {
|
||||
id: path.resolve().relative_to(root)
|
||||
|
@ -170,7 +166,7 @@ class ModResourceLoader:
|
|||
return metadata
|
||||
|
||||
# TODO: maybe this should take lang as a variable?
|
||||
@must_yield
|
||||
@must_yield_something
|
||||
def load_book_assets(
|
||||
self,
|
||||
book_id: ResourceLocation,
|
||||
|
@ -494,3 +490,17 @@ 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:])
|
||||
|
|
|
@ -33,15 +33,16 @@ classifiers = [
|
|||
]
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"typing_extensions>=4.6.1,<5",
|
||||
"importlib_resources>=6.0.1,<7",
|
||||
"more_itertools>=10.1.0,<11",
|
||||
"pydantic>=2.3.0,<3,!=2.4.0",
|
||||
"pydantic_settings>=2.0.3,<3",
|
||||
"Jinja2>=3.1.2,<4",
|
||||
"pyjson5>=1.6.3,<2",
|
||||
"pluggy>=1.3.0,<2",
|
||||
"typer[all]>=0.9.0,<1",
|
||||
"typing_extensions~=4.6",
|
||||
"importlib_resources~=6.0",
|
||||
"more_itertools~=10.1",
|
||||
"pydantic~=2.3,!=2.4.0",
|
||||
"pydantic_settings~=2.0",
|
||||
"Jinja2~=3.1",
|
||||
"pyjson5~=1.6",
|
||||
"pluggy~=1.3",
|
||||
"typer[all]~=0.9.0",
|
||||
"requests~=2.31",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
|
|
Loading…
Reference in a new issue