Skip to content

Commit 91c7f71

Browse files
committed
Implement proper flag support (close #8)
1 parent a9c698d commit 91c7f71

File tree

11 files changed

+241
-11
lines changed

11 files changed

+241
-11
lines changed

noxfile.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,20 @@ def dummy_setup(session: nox.Session):
299299
"icon": "minecraft:amethyst_shard",
300300
"description": "Foo bar baz qux$(br)$(li)quux$(br)$(li2)corge$(br)$(li3)grault$(br)$(li4)garply$(li)waldo$(br)fred plugh xyzzy thud",
301301
"sortnum": 0,
302+
"flag": "foo:bar",
303+
},
304+
"categories/disabled.json": {
305+
"name": "Disabled",
306+
"icon": "minecraft:bedrock",
307+
"description": "Disabled",
308+
"flag": "foo:baz",
302309
},
303310
"entries/bar.json": {
304311
"name": "Dummy Entry",
305312
"category": "dummy:foo",
306313
"icon": "minecraft:textures/mob_effect/nausea.png",
307314
"sortnum": 0,
315+
"flag": "foo:bar",
308316
"pages": [
309317
{
310318
"type": "patchouli:text",
@@ -471,6 +479,77 @@ def dummy_setup(session: nox.Session):
471479
},
472480
],
473481
},
482+
"entries/flags.json": {
483+
"name": "Flags",
484+
"category": "dummy:foo",
485+
"icon": "minecraft:paper",
486+
"pages": [
487+
{
488+
"type": "patchouli:text",
489+
"text": "enabled 1",
490+
"flag": "does not exist",
491+
},
492+
{
493+
"type": "patchouli:text",
494+
"text": "enabled 2",
495+
"flag": "foo:bar",
496+
},
497+
{
498+
"type": "patchouli:text",
499+
"text": "enabled 3",
500+
"flag": "advancements_disabled_foo",
501+
},
502+
{
503+
"type": "patchouli:text",
504+
"text": "enabled 4",
505+
"flag": "mod:aaaaaaaaa",
506+
},
507+
{
508+
"type": "patchouli:text",
509+
"text": "enabled 5",
510+
"flag": "mod:minecraft",
511+
},
512+
{
513+
"type": "patchouli:text",
514+
"text": "disabled 1",
515+
"flag": "foo:baz",
516+
},
517+
{
518+
"type": "patchouli:text",
519+
"text": "disabled 2",
520+
"flag": "mod:foo",
521+
},
522+
{
523+
"type": "patchouli:text",
524+
"text": "disabled 3",
525+
"flag": "advancements_disabled_bar",
526+
},
527+
{
528+
"type": "patchouli:text",
529+
"text": "disabled 4",
530+
"flag": "debug",
531+
},
532+
{
533+
"type": "patchouli:text",
534+
"text": "web only",
535+
"flag": "mod:hexdoc:web_only",
536+
},
537+
{
538+
"type": "patchouli:text",
539+
"text": "ingame only",
540+
"flag": "!mod:hexdoc:web_only",
541+
},
542+
],
543+
},
544+
"entries/disabled.json": {
545+
"name": "Disabled",
546+
"category": "dummy:foo",
547+
"icon": "minecraft:bedrock",
548+
"flag": "foo:baz",
549+
"pages": [
550+
"disabled",
551+
],
552+
},
474553
},
475554
},
476555
"data/dummy": {
@@ -553,6 +632,12 @@ def dummy_setup(session: nox.Session):
553632
]
554633
export_dir = "src/hexdoc_dummy/_export/generated"
555634
635+
[flags]
636+
"foo:bar" = true
637+
advancements_disabled_foo = true
638+
"foo:baz" = false
639+
"mod:foo" = false
640+
556641
[template]
557642
icon = "icon.png"
558643
include = [

src/hexdoc/cli/utils/load.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def init_context(
146146
loader
147147
).value_ids_set,
148148
all_metadata=all_metadata,
149+
flags=pm.load_flags(),
149150
),
150151
]:
151152
item.add_to_context(context)

src/hexdoc/core/properties.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ class Properties(BaseProperties):
241241

242242
textures: TexturesProps = Field(default_factory=TexturesProps)
243243

244+
flags: dict[str, bool] = Field(default_factory=dict)
245+
"""Local Patchouli flag overrides.
246+
247+
This has the final say over built-in defaults and flags exported by other mods.
248+
"""
249+
244250
template: TemplateProps | None = None
245251

246252
lang: defaultdict[

src/hexdoc/patchouli/book.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from collections import defaultdict
23
from typing import Any, Literal
34

@@ -19,6 +20,8 @@
1920
from .entry import Entry
2021
from .text import FormattingContext, FormatTree
2122

23+
logger = logging.getLogger(__name__)
24+
2225

2326
class Book(HexdocModel):
2427
"""Main Patchouli book class.
@@ -120,6 +123,9 @@ def _load_entries(
120123
use_resource_pack=self.use_resource_pack,
121124
):
122125
entry = Entry.load(resource_dir, id, data, cast_context(context))
126+
if not entry.is_flag_enabled:
127+
logger.info(f"Skipping entry {id} due to disabled flag {entry.flag}")
128+
continue
123129

124130
spoilered_categories[entry.category_id] = (
125131
entry.is_spoiler and spoilered_categories.get(entry.category_id, True)

src/hexdoc/patchouli/book_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class BookContext(ValidationContextModel):
1414
book_links: BookLinks = Field(default_factory=dict)
1515
spoilered_advancements: set[ResourceLocation]
1616
all_metadata: dict[str, HexdocMetadata]
17+
flags: dict[str, bool]
1718

1819
def get_link_base(self, resource_dir: PathResourceDir) -> URL:
1920
modid = resource_dir.modid

src/hexdoc/patchouli/category.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from typing import Any, Self
23

34
from pydantic import Field
@@ -13,9 +14,12 @@
1314

1415
from .entry import Entry
1516
from .text import FormatTree
17+
from .utils import Flagged
1618

19+
logger = logging.getLogger(__name__)
1720

18-
class Category(IDModel, Sortable):
21+
22+
class Category(IDModel, Sortable, Flagged):
1923
"""Category with pages and localizations.
2024
2125
See: https://vazkiimods.github.io/Patchouli/docs/reference/category-json
@@ -32,7 +36,6 @@ class Category(IDModel, Sortable):
3236
# optional
3337
parent_id: ResourceLocation | None = Field(default=None, alias="parent")
3438
_parent_cmp_key: tuple[int, ...] | None = None
35-
flag: str | None = None
3639
sortnum: int = 0
3740
secret: bool = False
3841

@@ -54,7 +57,16 @@ def load_all(
5457
"categories",
5558
use_resource_pack,
5659
):
57-
category = categories[id] = cls.load(resource_dir, id, data, context)
60+
# Patchouli checks flags before resolving category parents
61+
# https://github.com/VazkiiMods/Patchouli/blob/abd6d03a08c37bcf116730021fda9f477412b31f/Xplat/src/main/java/vazkii/patchouli/client/book/BookContentsBuilder.java#L151
62+
category = cls.load(resource_dir, id, data, context)
63+
if not category.is_flag_enabled:
64+
logger.info(
65+
f"Skipping category {id} due to disabled flag {category.flag}"
66+
)
67+
continue
68+
69+
categories[id] = category
5870
if category.parent_id:
5971
G.add_edge(category.parent_id, category.id)
6072

@@ -68,7 +80,13 @@ def load_all(
6880

6981
# late-init _parent_cmp_key
7082
for parent_id in G.topological_sort():
71-
parent = categories[parent_id]
83+
parent = categories.get(parent_id)
84+
if parent is None:
85+
children = ", ".join(str(v) for _, v in G.iter_out_edges(parent_id))
86+
raise ValueError(
87+
f"Parent category {parent_id} required by {children} does not exist"
88+
)
89+
7290
for _, child_id in G.iter_out_edges(parent_id):
7391
categories[child_id]._parent_cmp_key = parent._cmp_key
7492

src/hexdoc/patchouli/entry.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import logging
12
from typing import Iterable, Iterator
23

3-
from pydantic import Field
4+
from pydantic import Field, model_validator
45

56
from hexdoc.core import ItemStack, ResourceLocation
67
from hexdoc.minecraft import LocalizedStr
@@ -11,10 +12,12 @@
1112

1213
from .page import CraftingPage, Page, PageWithTitle
1314
from .text import FormatTree
14-
from .utils import AdvancementSpoilered
15+
from .utils import AdvancementSpoilered, Flagged
1516

17+
logger = logging.getLogger(__name__)
1618

17-
class Entry(IDModel, Sortable, AdvancementSpoilered):
19+
20+
class Entry(IDModel, Sortable, AdvancementSpoilered, Flagged):
1821
"""Entry json file, with pages and localizations.
1922
2023
See: https://vazkiimods.github.io/Patchouli/docs/reference/entry-json
@@ -28,7 +31,6 @@ class Entry(IDModel, Sortable, AdvancementSpoilered):
2831

2932
# optional (entry.json)
3033
advancement: ResourceLocation | None = None
31-
flag: str | None = None
3234
priority: bool = False
3335
secret: bool = False
3436
read_by_default: bool = False
@@ -118,6 +120,19 @@ def _get_advancement(self):
118120
# implements AdvancementSpoilered
119121
return self.advancement
120122

123+
@model_validator(mode="after")
124+
def _skip_disabled_pages(self):
125+
new_pages = list[Page]()
126+
for i, page in enumerate(self.pages):
127+
if not page.is_flag_enabled:
128+
logger.info(
129+
f"Skipping page {i} of entry {self.id} due to disabled flag {page.flag}"
130+
)
131+
continue
132+
new_pages.append(page)
133+
self.pages = new_pages
134+
return self
135+
121136

122137
class _CraftingPageAccumulator(PageWithTitle, template_type="patchouli:crafting"):
123138
recipes: list[CraftingRecipe] = Field(default_factory=list)

src/hexdoc/patchouli/page/abstract_pages.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,18 @@
1010
from hexdoc.utils import Inherit, InheritType, NoValue, classproperty
1111

1212
from ..text import FormatTree
13-
from ..utils import AdvancementSpoilered
13+
from ..utils import AdvancementSpoilered, Flagged
1414

1515
_T_Recipe = TypeVar("_T_Recipe", bound=Recipe)
1616

1717

18-
class Page(TypeTaggedTemplate, AdvancementSpoilered, type=None):
18+
class Page(TypeTaggedTemplate, AdvancementSpoilered, Flagged, type=None):
1919
"""Base class for Patchouli page types.
2020
2121
See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
2222
"""
2323

2424
advancement: ResourceLocation | None = None
25-
flag: str | None = None
2625
anchor: str | None = None
2726

2827
def __init_subclass__(

src/hexdoc/patchouli/utils.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import logging
12
from abc import ABC, abstractmethod
3+
from typing import Iterator
24

35
from pydantic import PrivateAttr, ValidationInfo, model_validator
46

@@ -7,6 +9,8 @@
79

810
from .book_context import BookContext
911

12+
logger = logging.getLogger(__name__)
13+
1014

1115
class AdvancementSpoilered(HexdocModel, ABC):
1216
_is_spoiler: bool = PrivateAttr(False)
@@ -30,3 +34,58 @@ def _check_is_spoiler(self, info: ValidationInfo):
3034
)
3135

3236
return self
37+
38+
39+
class Flagged(HexdocModel):
40+
"""Mixin model for categories, entries, and pages to implement flags."""
41+
42+
flag: str | None = None
43+
44+
_is_flag_enabled: bool = PrivateAttr(True)
45+
46+
@property
47+
def is_flag_enabled(self) -> bool:
48+
return self._is_flag_enabled
49+
50+
@model_validator(mode="after")
51+
def _evaluate_flag(self, info: ValidationInfo):
52+
ctx = BookContext.of(info)
53+
self._is_flag_enabled = (
54+
_evaluate_flag(self.flag, ctx.flags) if self.flag else True
55+
)
56+
return self
57+
58+
59+
# https://github.com/VazkiiMods/Patchouli/blob/abd6d03a08c37bcf116730021fda9f477412b31f/Xplat/src/main/java/vazkii/patchouli/common/base/PatchouliConfig.java#L42
60+
def _evaluate_flag(flag: str, flags: dict[str, bool]) -> bool:
61+
# this SHOULD never be called with an empty string
62+
match flag[0]:
63+
case "&":
64+
return all(_split_and_evaluate_flag(flag, flags))
65+
case "|":
66+
return any(_split_and_evaluate_flag(flag, flags))
67+
case "!":
68+
flag = flag[1:]
69+
target = False
70+
case _:
71+
target = True
72+
73+
flag = flag.strip().lower()
74+
75+
b = flags.get(flag)
76+
if b is None:
77+
if flag.startswith("advancements_disabled_"):
78+
b = False
79+
else:
80+
if not flag.startswith("mod:"):
81+
logger.warning(f"Unknown config flag defaulting to True: {flag}")
82+
b = True
83+
# speed up subsequent checks a bit and avoid logging unnecessary warnings
84+
flags[flag] = b
85+
86+
return b == target
87+
88+
89+
def _split_and_evaluate_flag(flag: str, flags: dict[str, bool]) -> Iterator[bool]:
90+
for inner in flag.replace("&", "").replace("|", "").split(","):
91+
yield _evaluate_flag(inner, flags)

0 commit comments

Comments
 (0)