Skip to content

Commit b51f78d

Browse files
committed
Use nbtlib to parse ItemStack NBT tags and use them in spotlight pages (close #85)
1 parent 46be72e commit b51f78d

File tree

5 files changed

+120
-8
lines changed

5 files changed

+120
-8
lines changed

noxfile.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,11 @@ def dummy_setup(session: nox.Session):
316316
"item": "minecraft:stone",
317317
"anchor": "spotlight",
318318
},
319+
{
320+
"type": "patchouli:spotlight",
321+
"text": "spotlight with named item!",
322+
"item": """minecraft:stone{display:{Name:'{"text":"dirt?","color":"white"}'}}""",
323+
},
319324
],
320325
},
321326
"entries/patchistuff.json": {

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies = [
4444
"moderngl[headless]~=5.10",
4545
"moderngl-window~=2.4",
4646
"more_itertools~=10.1",
47+
"nbtlib==1.12.1",
4748
"networkx~=3.2",
4849
"ordered-set~=4.1",
4950
"packaging~=23.2",

src/hexdoc/core/resource.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44

55
from __future__ import annotations
66

7+
import json
78
import logging
89
import re
910
from fnmatch import fnmatch
1011
from pathlib import Path
1112
from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar
1213

14+
from nbtlib import (
15+
Compound,
16+
Path as NBTPath,
17+
parse_nbt, # pyright: ignore[reportUnknownVariableType]
18+
)
1319
from pydantic import (
1420
BeforeValidator,
1521
ConfigDict,
22+
JsonValue,
1623
TypeAdapter,
1724
field_validator,
1825
model_serializer,
@@ -86,6 +93,7 @@ def resloc_json_schema_extra(
8693
config=DEFAULT_CONFIG
8794
| ConfigDict(
8895
json_schema_extra=resloc_json_schema_extra,
96+
arbitrary_types_allowed=True,
8997
),
9098
)
9199
class BaseResourceLocation:
@@ -255,9 +263,40 @@ class ItemStack(BaseResourceLocation, regex=_make_regex(count=True, nbt=True)):
255263
count: int | None = None
256264
nbt: str | None = None
257265

266+
_data: Compound | None = None
267+
258268
def __init_subclass__(cls, **kwargs: Any):
259269
super().__init_subclass__(regex=cls._from_str_regex, **kwargs)
260270

271+
def __post_init__(self):
272+
object.__setattr__(self, "_data", _parse_nbt(self.nbt))
273+
274+
@property
275+
def data(self):
276+
return self._data
277+
278+
def get_name(self) -> str | None:
279+
if self.data is None:
280+
return None
281+
282+
component_json = self.data.get(NBTPath("display.Name")) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
283+
if not isinstance(component_json, str):
284+
return None
285+
286+
try:
287+
component: JsonValue = json.loads(component_json)
288+
except ValueError:
289+
return None
290+
291+
if not isinstance(component, dict):
292+
return None
293+
294+
name = component.get("text")
295+
if not isinstance(name, str):
296+
return None
297+
298+
return name
299+
261300
@override
262301
def i18n_key(self, root: str = "item") -> str:
263302
return super().i18n_key(root)
@@ -300,3 +339,18 @@ def _add_hashtag_to_tag(value: Any):
300339
AssumeTag = Annotated[_T, BeforeValidator(_add_hashtag_to_tag)]
301340
"""Validator that adds `#` to the start of strings, and sets `ResourceLocation.is_tag`
302341
to `True`."""
342+
343+
344+
def _parse_nbt(nbt: str | None) -> Compound | None:
345+
if nbt is None:
346+
return None
347+
348+
try:
349+
result = parse_nbt(nbt)
350+
except ValueError as e:
351+
raise ValueError(f"Failed to parse sNBT literal '{nbt}': {e}") from e
352+
353+
if not isinstance(result, Compound):
354+
raise ValueError(f"Expected Compound, got {type(result)}: {result}")
355+
356+
return result

src/hexdoc/minecraft/assets/with_texture.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ def load_id(cls, item: ItemStack, context: ContextSource):
6969
"""Implements InlineModel."""
7070

7171
i18n = I18n.of(context)
72-
if item.path.startswith("texture"):
72+
if (name := item.get_name()) is not None:
73+
pass
74+
elif item.path.startswith("texture"):
7375
name = i18n.localize_texture(item.id)
7476
else:
7577
name = i18n.localize_item(item)

test/core/test_resource.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,50 +23,100 @@ def test_resourcelocation(s: str, expected: ResourceLocation, str_prefix: str):
2323
assert str(actual) == str_prefix + s
2424

2525

26-
item_stacks: list[tuple[str, ItemStack, str]] = [
26+
item_stacks: list[tuple[str, ItemStack, str, str | None]] = [
2727
(
2828
"stone",
2929
ItemStack("minecraft", "stone", None, None),
3030
"minecraft:",
31+
None,
3132
),
3233
(
3334
"hexcasting:patchouli_book",
3435
ItemStack("hexcasting", "patchouli_book", None, None),
3536
"",
37+
None,
3638
),
3739
(
3840
"minecraft:stone#64",
3941
ItemStack("minecraft", "stone", 64, None),
4042
"",
43+
None,
4144
),
4245
(
43-
"minecraft:diamond_pickaxe{display:{Lore:['A really cool pickaxe']}",
46+
"minecraft:diamond_pickaxe{display:{Lore:['A really cool pickaxe']}}",
4447
ItemStack(
4548
"minecraft",
4649
"diamond_pickaxe",
4750
None,
48-
"{display:{Lore:['A really cool pickaxe']}",
51+
"{display:{Lore:['A really cool pickaxe']}}",
4952
),
5053
"",
54+
None,
5155
),
5256
(
53-
"minecraft:diamond_pickaxe#64{display:{Lore:['A really cool pickaxe']}",
57+
"minecraft:diamond_pickaxe#64{display:{Lore:['A really cool pickaxe']}}",
5458
ItemStack(
5559
"minecraft",
5660
"diamond_pickaxe",
5761
64,
58-
"{display:{Lore:['A really cool pickaxe']}",
62+
"{display:{Lore:['A really cool pickaxe']}}",
5963
),
6064
"",
65+
None,
66+
),
67+
(
68+
"""minecraft:diamond_pickaxe{display:{Name:'{"text": "foo"}'}}""",
69+
ItemStack(
70+
"minecraft",
71+
"diamond_pickaxe",
72+
None,
73+
"""{display:{Name:'{"text": "foo"}'}}""",
74+
),
75+
"",
76+
"foo",
77+
),
78+
(
79+
"""minecraft:diamond_pickaxe{display:{Name:'{"text": "foo}'}}""",
80+
ItemStack(
81+
"minecraft",
82+
"diamond_pickaxe",
83+
None,
84+
"""{display:{Name:'{"text": "foo}'}}""",
85+
),
86+
"",
87+
None,
88+
),
89+
(
90+
"""minecraft:diamond_pickaxe{displayy:{Name:'{"text": "foo"}'}}""",
91+
ItemStack(
92+
"minecraft",
93+
"diamond_pickaxe",
94+
None,
95+
"""{displayy:{Name:'{"text": "foo"}'}}""",
96+
),
97+
"",
98+
None,
99+
),
100+
(
101+
"""minecraft:diamond_pickaxe{display:{Namee:'{"text": "foo"}'}}""",
102+
ItemStack(
103+
"minecraft",
104+
"diamond_pickaxe",
105+
None,
106+
"""{display:{Namee:'{"text": "foo"}'}}""",
107+
),
108+
"",
109+
None,
61110
),
62111
]
63112

64113

65-
@pytest.mark.parametrize("s,expected,str_prefix", item_stacks)
66-
def test_itemstack(s: str, expected: ItemStack, str_prefix: str):
114+
@pytest.mark.parametrize("s,expected,str_prefix,name", item_stacks)
115+
def test_itemstack(s: str, expected: ItemStack, str_prefix: str, name: str | None):
67116
actual = ItemStack.from_str(s)
68117
assert actual == expected
69118
assert str(actual) == str_prefix + s
119+
assert actual.get_name() == name
70120

71121

72122
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)