Skip to content

Make Guide Editor Completion Pluggable and Fix In-Game/Site Parity - #80

Merged
Dream-Master merged 100 commits into
masterfrom
guide-syntax-registry
Sep 14, 2026
Merged

Dream-Master merged 100 commits into
masterfrom
guide-syntax-registry

Conversation

@ABKQPO

@ABKQPO ABKQPO commented Sep 12, 2026

Copy link
Copy Markdown
Member

Summary

Turns guide editor completion from hardcoded tables into a pluggable syntax registry, while improving ingame/site parity and fixing completion, scene materialization, and font layout issues

Syntax and extensibility

  • Added immutable, cached GuideSyntaxModel with revision tracking
  • SyntaxValueKind is now a plugin extensible record identified by kindid()
  • Added 19 built in value sources
  • Added 8 extensibility surfaces with global and per guide registration
  • Guide declarations override global registrations
  • Added 28 guarded registration methods to GuideNhIntegrationRegistry
  • Third party hooks are isolated so failures do not break guides or completion

Recipe filters

  • Added handlerWhitelist / handlerBlacklist to recipe tags
  • Added global recipeHandlerBlacklist config
  • Defaults to hiding GTNEIMultiblockHandler and StructureCompatNEIHandler
  • Page level blacklists can add restrictions but cannot remove configured ones

Completion fixes

  • Completion now works with temporarily unparsable documents and incomplete tags
  • Fixed tag, attribute, value, quote, and replacement range detection
  • Fixed container completion, frontmatter parsing, recipe/item suggestions, popup position, and debounce behavior

Scene materialization

  • Tasks are now bound to their owning document
  • Yielding tasks no longer block the queue
  • Prevented results from being written to the wrong page after page changes

Font aware layout

  • Custom font metrics now correctly affect line height and prevent text overlap
  • Updated caret, selection, highlights, warnings, and home page layout
  • Added Angelica font support and live fontRenderer tracking after reloads

Ingame / site parity

  • Added site support for chart axis/style attributes, symbolic colours, task checkboxes, contributed renderers, and registry based recipes
  • Fixed chart axis step calculation, code fence casing, and guarded site renderer lookup

Checklist

  • I have tested this PR in DevEnv
  • I have tested this PR in Fullpack
  • This PR is in compliance with the GTNH AI Policy
  • This PR requires another PR in order to merge

Editor completion used to read a central tag-to-attribute table plus one
hardcoded provider per value source, so nothing outside this mod could add tag
syntax.

Guide syntax is now assembled from registrations:

- GuideSyntaxModel folds the tag names published by every registered
  TagCompiler and SceneElementTagCompiler, so a mod that adds a tag compiler
  gets completion for its tags without touching the editor.
- SyntaxContributor adds the facts a compiler does not express: container
  shape, child tags, attributes, markdown snippets, fence names, frontmatter
  keys and value kinds.
- SyntaxValueSource answers completion per SyntaxValueKind, so a mod can reuse
  a built-in kind or declare its own and register a source for it.
- BuiltinSyntaxContributor carries the syntax this mod ships and is registered
  as a default extension, so disableDefaultExtensions turns it off with the
  rest.

Value kinds and markdown snippet kinds are records with factory methods rather
than enums, so third parties are not forced to reuse a closed set.
The editor no longer dispatches completion through AutocompleteProviders and its
per-provider tag and attribute key sets.

- GuideEditorAutocompleteController owns the completion session: it resolves what
  the cursor sits in, asks the guide's syntax model for candidates and drives the
  popup, reporting what the host must do instead of editing text itself.
- GuideSyntaxCompletion turns a resolved context into candidates using only model
  data, so it knows no specific tag.
- Candidates describe snippets: replacement text, caret and selection offsets, an
  optional closing suffix and whether the value is quoted. Container tags complete
  as a paired tag with the caret inside, and markdown constructs put the caret
  where typing continues.
- An attribute name expands to an empty or boolean value according to its value
  kind, and typing the terminator accepts it. Results are ranked so the popup
  preselects the closest match.

Registry snapshots are cached by key count and heading anchors by document
identity, so answering a query scans no registry.
The central tag-to-attribute table, the tag catalog, the markdown snippet catalog
and the per-provider dispatch keys are all replaced by the syntax registry, so they
go away together with the candidate types only they used.
The block statistics element declared ten attributes but only visible and
buttonEnabled were read, so mode, corner, dock, showNames, filterMode, filter,
maxWidth and maxHeight did nothing. The manual row element had no handler at all,
and an omitted count was dropped because the scene refuses a count of zero.

- Every documented attribute is now applied to the scene.
- Manual rows are compiled into manual entries, which select manual mode on their
  own; an explicit conflicting mode is reported instead of losing the rows.
- An omitted count shows the row once, an explicit zero keeps its meaning, and an
  unresolvable item is reported.
The input annotation renders the mouse-input icon a scene shows to hint that the
player should click somewhere. The site exporter already rendered it, but no scene
element compiler implemented it, so in game it was skipped as a tag without a
registered compiler.

The compiler anchors the icon at a position, resolves the button with the same
spellings the exporter accepts, and carries an optional modifier and item plus the
StructureLib visibility conditions.
Charts disagreed between the game and the exported site: the exporter read a
legend flag that does not exist in pages while the game hides its legend with
legend="none", and it read labelPosition as a boolean that only recognised one
value.

- Legend visibility comes from the in-game legend attribute, with the old flag
  kept as a fallback for pages written against it.
- labelPosition is parsed with the game's own parser, and the site renderer draws
  the value labels above, below, inside or not at all to match.
- The prefix-read axis options and grid switches were invisible to the editor
  because the chart parser reads them by concatenation, so nothing declared them.
The popup runs before the editor handles its own keys, so anything it claimed was
lost. Control chords (save, undo, redo, copy, paste, select all) are now handed
back to the host, and a slot the text has moved away from can no longer be
committed onto newer text.

- A control chord dismisses the popup and reports IGNORED, so GuideScreen still
  runs its own Ctrl+S/Z/Y handling and the text area keeps Ctrl+A/C/V/X. A
  modifier key on its own is not a chord, so holding Shift or Ctrl while typing
  keeps the popup open.
- Text that changes while the query is debounced dismisses the open popup instead
  of leaving a slot that describes the older text.
- Accepting a candidate re-checks that the recorded range still starts the typed
  text, so an out-of-band edit drops the commit rather than overwriting a range
  that now holds something else.
- An attribute value that is still open ends at the caret: `<ItemImage id="mine />`
  no longer replaces the ` /` of the self-closing tag.
- A frontmatter value committed on a line that already carries its list marker
  fills that entry instead of adding a second, empty one.
A scene only honours block statistics it is enabled for, and the flag was skipped
whenever a <BlockStats> child was present, so none of the documented attributes,
manual rows or the site export had any effect. The toggle is now enabled for every
scene before its elements are compiled, and a <BlockStats> element only overrides
what it declares.

- Manual rows select manual mode even when the element names another mode, because
  a row list that is not listed is the same as losing it; the override is reported.
- Only rows the scene accepts are counted, so an explicit count="0" hides its row
  without switching the scene to an empty manual list.
- The three diagnostics use warnAlways, so an author sees them without debug mode,
  and they name the element and attribute they are about.
- A malformed item id no longer aborts the whole scene: <BlockStat> and
  <InputAnnotation> report it and keep the rest of the scene compiling.
- showNames, visible and buttonEnabled are read with the shared boolean parser, so
  1/yes/on and a valueless attribute work like they do everywhere else.
- The editor declares count as an integer, which is how the scene reads it, and
  offers the two modifier spellings the renderer actually distinguishes.
- Fence completion offers the aliases the language registry maps, so sh, yml and kt
  complete next to the canonical language ids.
The model cache pinned every extension collection it had ever seen, so each guide
reload kept the replaced collection and a full model alive, and a contributor's own
markdown snippet kind could never be offered.

- The cache holds its keys weakly: a reload drops the entry for the collection it
  replaced, and a model is still rebuilt when the global registrations change.
- The model used when no guide is open now goes through the same cache, so it
  observes contributors registered after startup.
- A markdown snippet is matched by its placement rather than by kind identity, so a
  contributor that declares its own line-leading or inline kind gets completions.
- An attribute placeholder follows the value kind's quoting rule instead of always
  quoting, so a bare kind expands to `name=` and the value lands where the page
  expects it.
- The ore dictionary is copied into a snapshot that a query scans without
  allocating, and the snapshot is refreshed on an interval instead of per keystroke.
- The exported legend falls back to the in-game default position instead of none,
  and the docs no longer promise a count default the scene does not have.
The exported charts disagreed with the game: only the column chart drew value
labels, they were always white instead of using labelColor, and the legend was
always a row under the plot even when the page asked for another side.

- Every chart now draws labels where the game draws them: above, below, inside or
  beyond the end of a bar or point, and inside or past the rim of a pie slice with
  the game's "label percent" text.
- Labels use the chart's labelColor, which defaults to the in-game colour, instead
  of a hardcoded white for labels inside a bar.
- The legend is reserved on the side the legend attribute asks for, so a legend at
  the top, bottom, left or right no longer overlaps the plot, and an absent
  attribute keeps the in-game default of a legend at the top.
- The presentation every chart shares - size, frame, title, legend placement and
  label style - travels as one ChartStyle value, so the chart entry points stay
  readable and the individual-value overloads remain for compatibility.
Value sources that answer from a game registry or the open document lowercased
every candidate on every keystroke, and the ore dictionary alone is thousands of
names. They now share one snapshot type that precomputes the lowercase form and
refreshes on an interval, so a query scans strings instead of allocating.

- The ore dictionary, entity registry and command list answer from that snapshot.
- Page ids and asset paths build a snapshot per list, so a changed guide can never
  mix two page lists in one answer.
- Command values keep their leading slash out of the match, so typing the slash no
  longer hides every command.
The documented scene-relative overlay size was never applied: the method that
computes it had no caller, so every overlay stayed at the fixed minimum, and the
docs promised a percentage that no code used.

- A scene that does not size its own overlay now grows it with the scene, which is
  what the documentation described.
- The docs state the real rule in both languages: the fixed minimum, growing to 40%
  of a scene that is larger than that.
- A child of <BlockStats> that is not a <BlockStat> row is reported instead of being
  skipped without a word.
Whole numbers were printed with the axis formatter, which trims decimals
differently, so a fractional value read 0.3333 on the site and 0.33 in the game.

- Value labels and scatter point labels use the game's own rule: whole numbers
  plain, everything else with two decimals.
A snapshot was rebuilt only when a registry's key count moved, so an entry a mod
replaced with one of the same count stayed invisible for the whole session. The
count still drives the rebuild; a slow interval bounds the rest.
Everything the editor completed until now was syntax this mod parses. A contributor
with syntax of its own - a directive inside a block, a mini language in a code
fence, a field it compiles itself - had no way in, and a tag that only works with
attributes had to be typed out by hand.

- A SyntaxSlot declares where it owns text, which values the editor offers there and
  how an accepted value is written, so a mod can complete its own syntax without
  touching any of this mod's classes.
- Slots are asked before the editor's own resolvers and the first match owns the
  caret even when it offers no values, so this mod never guesses inside text another
  mod claims. A slot that throws is reported and skipped.
- A slot can also declare the range a double click inside it selects.
- sink.insertTemplates declares the text a tag completes as, and the editor writes it
  instead of the bare name. InsertTemplate.caretAfter finds the marker it is given, so
  a template says where typing continues without making the author count characters.
- The mod declares templates for its own tags that need more than a name: block
  statistics, the input annotation, recipes, a scene, and charts with their first
  series.
- The wiki documents slots and insert templates in English and Chinese.
Accepting a container tag or an insert template while a closing bracket was already
typed left a second bracket behind: `<Row>` completed as `<Row></Row>>`, and a chart
template added its whole form in front of the typed `>`.

- A candidate that brings its own complete form now consumes a following `>` or `/>`
  as part of its replacement, so the tag ends exactly once.
- A bare tag name keeps the bracket the author typed.
- A tag candidate builds its popup row once instead of reformatting a template on
  every rendered frame.
An insert template replaces the typed tag name, and the range a tag name occupies
starts after the '<' the author typed, so every template was written behind that
bracket: completing ColumnChart produced `<<ColumnChart ...>`.

- A candidate that writes a whole tag, opening bracket included, now consumes the
  bracket in front of the name as part of its replacement, so the written tag has
  exactly one opening bracket.
- A slot's writer is called through a guard: one that throws or answers nothing is
  reported in the log and the commit is dropped instead of escaping into the key
  handler.
- A slot match reports which slot answered, so its failures can name the contributor.
- The slot documentation states that the text a slot reports as typed has to be what
  the document really holds there, because the editor re-checks it before writing.
The site stylesheet gives every chart label 16 pixels, which overrides the font-size
attributes the SVG carries, so the legend rows, legend columns and label offsets that
were measured for a much smaller font overlapped each other.

- Chart text size is one constant the layout follows: label offsets, legend swatch,
  row height and the width a legend entry needs are all derived from it.
- A side legend is as wide as its longest name instead of a fixed width, and no legend
  draws a row past the space reserved for it.
- A pie label is placed by its text box rather than by its anchor, so a slice pointing
  sideways keeps its label inside the plot.
- Value labels and tooltips use the in-game value and percent formatters, including the
  guard that keeps a broken value from printing as NaN%.
- A label colour is resolved once per chart instead of once per drawn point.
- The chart entry points that still take a legend flag follow the in-game default of a
  legend at the top, like the pages do.
- The pie chart's insert template writes the attribute a slice actually reads, an
  ignored child of <BlockStats> is reported whatever form it takes, and the overlay size
  documentation states the rule the code implements.
The presentation attributes the chart SVG carries say 7 to 10 pixels while the site
stylesheet renders every chart label at 16, so the two disagreed and the attributes
were dead weight.

- The labels, legend names and axis titles of a chart are written at the size the
  layout constant names, so the SVG and the stylesheet agree whichever of them wins
  and a chart keeps its proportions if the stylesheet rule changes.
A candidate was ranked by the text it writes, and a tag template writes the whole
tag, so the template that fits best ranked below the plain tag name the author was
typing towards.

- A candidate says which text it is ranked by, and a tag candidate names the tag, so a
  template and the plain tag rank the same and the popup preselects the closest match.
The syntax registry runs contributor, compiler, value source and slot callbacks from the
client tick, and a failure there reaches the screen's key handler, where only a null
pointer is caught and anything else ends the game. Slots were guarded; the rest were
not.

- A contributor, tag compiler, scene element compiler, value source or slot that fails
  is reported with its namespace and skipped, so one broken plugin costs its own
  completions instead of the editor.
- A value source that answers nothing, or with null entries, contributes no values.
- Reporting a slot failure no longer calls the slot's own namespace unguarded, and the
  same guard is used for the report a failed writer produces.
- A syntax model is built outside the cache monitor, so plugin code cannot block or
  re-enter every other model lookup.
- Tag publishing is guarded on the page-compile and scene-compile paths as well, so a
  plugin that cannot list its tags does not take every page or scene down with it.

The guide editor also looks its model up on every tick now: looking one up is cached per
extension collection and registration revision, so a contributor or slot registered
while the editor is open is picked up instead of waiting for another guide.
Four inputs reachable from an ordinary page could stop the client or step outside the
guide's own files.

- A chart bound or data value that is not finite is refused when the element is read:
  an infinite axis bound made the tick loops run forever.
- A replacement region is bounded on every side, so a mistyped extent cannot make the
  scene build walk billions of positions.
- A guide path that climbs to a parent directory is refused before any asset lookup,
  and a directory-backed pack resolves a resource only inside its own root.
- A page is only written inside the export directory, so a resource pack entry name
  cannot place a file elsewhere.
- A heading depth that is not a number no longer drops its page out of the export.
- A scene element compiler that cannot list its tags is reported instead of stopping
  every scene.
Five gaps kept a mod from extending this one without patching it.

- The exported site renders contributed tags: register a guide site tag renderer and
  the first one that answers wins, with the built-in renderers as the fallback. The
  first-party chain stays where it is for now, which is the one asymmetry left here.
- Scene element compilers and symbolic colour resolvers can now be registered for every
  guide, the way tag compilers already could; a resolver that fails is reported and
  skipped.
- Value sources are routed by their kind id instead of by the whole kind, so a source
  that declares a bare value answers an attribute that declares the same kind quoted,
  which the documentation always claimed.
- The dead recipe type mapping extension point, which nothing read, is gone.
- Chart legends size themselves from their names and stop at the last row that fits, so
  a long name or a chart with many series cannot run over the plot, and a legend that
  cannot fit never throws the page out of the export.
Three places repeated work that the render and edit paths pay for directly.

- Wrapping a long line measured the whole remainder for every wrapped chunk, so a very
  long line copied O(n^2) characters per layout; the font is handed a bounded window
  instead, which keeps every wrap point.
- The editor text area re-measured the whole document on every edit even in the second
  pass of its scrollbar fix point, where the width it would use cannot change the
  layout of unwrapped text.
- Block statistics asked the registry for the provider list once per block, and a scene
  structure snapshot walked the filled blocks twice; both now read one snapshot or walk
  once.
The only plugin documentation was the editor page, so a mod author had no way to learn
which axes are open, which are deliberately closed, or which conventions the registry
relies on.

- The editor page now lists every extension point with the goal it serves, names the
  four axes that are still closed and what each one costs, and states the conventions:
  namespace your value kind ids, report the text the document really holds from a slot,
  and expect a failing plugin to be reported and skipped.
- It also says when a registration is read, because a compiler provider is consulted
  when a guide is built rather than when a mod loads.
- The SVG clip id counter is atomic: two exports can reach it from different threads.
- A guide asset scan that failed is retried on the next tick instead of being remembered
  as done, so a pack that was not readable yet does not stay unindexed for the session.
- The completion session disarms its pending query after the query is answered rather
  than before, so a failure cannot leave the popup shut until the next keystroke.
- The block statistics, item normalization and block display lookups run while a scene
  is drawn and now report and skip a provider of another mod that fails, instead of
  letting it break every frame of the scene it appears in.
- The exported scene resolver no longer hands out a scene that failed to export: the
  renderer counts the scenes it is given, so a missing one used to put every later
  BlockImage on the wrong scene.
The exported site read recipe slots from the NEI statics while the in-game renderer
reads the registered slot providers, so a mod that supplies its own slots rendered in
the book and exported an empty layout.

- The site recipe runtime now reads ingredient, result and other slots through the
  integration registry, which reaches NEI through the provider that is registered for
  it, so a mod's own provider is honoured on both sides.
A fence name could be declared for completion but nothing read its body, so a
contributor's fence name completed in the editor and was shown to the reader as a
plain code block.

- A CodeFenceRenderer declares the fence names it owns and answers with the block a
  body becomes. Contributed renderers are asked before the built-in fence handling,
  the first answer wins, and a renderer that fails is reported and skipped, so the
  fence falls back to a highlighted code block instead of taking the page down.
- The editor page now lists this axis as open and no longer counts it among the closed
  ones.
The page and asset writes of the JSON exporter joined a resource pack entry name onto
the output directory directly, so an entry that climbs to a parent directory wrote
wherever it pointed — the same hole the site writer already refuses.

- Both writes resolve their target inside the export directory and skip an id that
  would leave it, reporting the id instead.
A task was built with the document the host has mounted, but the host serves two trees at once and
the node's own tree is not always the mounted one: this branch queues the guide editor's preview
through dispatchToSubtree while the page is mounted, so every preview scene, tooltip and mermaid
task was tagged with the page document. Two consequences, the second made worse by the earlier
change that no longer stops the queue at the first yielding task:

- navigating the page dropped the preview's pending work, which is the amber placeholder this was
  meant to fix;
- the preview document never owned any task, so a rebuild never dropped its predecessor's work, and
  each rebuild queued another full set of scene builds, all of which now run.

A task also read the host's current page id when it finished rather than when it started, so one that
yielded across a page change wrote its node result into the next page's cache and allocated ids from
that page's counter. The page id is now captured when the script is dispatched, next to the document.
The book passed a contributed fence renderer the language exactly as the page wrote it, while the
export lowercased it first. A renderer declaring MyFence was therefore asked about that fence in the
book and never on the site, so its fence exported as a plain code block.

- Fence names are matched ignoring case, so a renderer declaring MyFence answers for myfence too.
- The export asks the renderer with the author's spelling, which is the value the book passes, so a
  renderer sees the same name on both sides. The built-in fences are still matched lowercased.
The frontmatter check looked for its closing delimiter only up to the caret and answered true at every
line before finding one. A page that opens with a thematic break and never closes it therefore reported
every caret as inside the frontmatter, and tag, attribute and fence completion were all answered from
the frontmatter path - the whole document was left without the completion this branch added. The check
now requires the closing delimiter, which is what the parser requires as well, and a page that never
closes the block is not frontmatter at all.

Checked per caret position on five documents: a closed block reports exactly the same range as before,
an unclosed one reports none of it, and a heading-led page is unchanged.
The renderer already cached its list of contributed tag renderers, with a comment explaining why, while
its sibling list of fence renderers was resolved from the registry on every call - twice per fence. A
page with two hundred fences copied the registry four hundred times and asked every contributing plugin
for its fence names just as often. The fence list is cached the same way, which is safe because the
renderer is built once per export.

The contributed-tag lookup also built its context, which carries the templates and the scene resolver,
before asking whether any plugin had contributed a renderer at all, and that runs for every MDX element
of every page. It returns early now.
The comment ran to nine lines for a three-line method; the point fits in three.
Trimming comment blocks to a single line by keeping the first sentence broke eight of them mid-sentence,
wherever the sentence did not end inside the text that was kept. Two ended with a dangling javadoc type
("{@code" followed by a period) and six stopped at a stray fragment such as "beats a." or "it compiles,.".
Each is written again as a complete sentence.

The comment on the asset path check was seven lines and is three, and no comment this branch added is now
over five lines.
The text-level path passed no container when it asked for tag completion, so a caret inside a container
in a document that cannot be parsed - which is exactly when that path answers - was offered every root
tag: Slice, Series, Tab and Plot inside a GameScene, and Column inside a Row. The parsed path reads the
container from the element the caret sits in; this path now reads it from the text, keeping a stack of
the tags that are open at the caret.

A tag whose opening form is unfinished is skipped, since it is the tag being named rather than a
container, so `text\n<Rec` still reports no container.

Checked on the inputs that reach this path: inside <Row><Col the container is Row, inside
<Row><Column><Item it is Column, inside <GameScene><ImportStructure ...><Rec it is ImportStructure, and
a caret in a bare half-typed tag reports none.
A recipe tag could only narrow its handler list. Two attributes are added to the six recipe tags: a
handlerBlacklist drops handlers from the results, and a handlerWhitelist keeps them.

- Both name a handler by its id, its overlay identifier or its class name, case-insensitively and by
  substring, so one entry can name a single handler or a whole package.
- The configured blacklist applies to every tag, and a page's own list is added to it, so a page can hide
  more than the configuration but not fewer.
- A handler a tag asks for by handlerId is kept even when a blacklist names it, and a whitelist entry does
  the same. That is what leaves an excluded handler reachable on purpose while it stays out of the results
  a tag refers to only by item.
- The blacklist is read at recipe-resolution time rather than cached, since a handler list is built per
  query and the configuration is a plain static by then.
A custom font, which another mod can install, scales its glyphs vertically while leaving FONT_HEIGHT at
8. Every line height in this mod was derived from that constant, so with such a font the glyphs grew and
the lines did not: text overlapped and clipped. Nothing here read the vertical scale such a font reports,
which is what this changes.

- GuideFontCompat gains getLineHeight and getGlyphScaleY, reading the vertical scale the live renderer
  reports and answering FONT_HEIGHT when it reports none. The three places that lay text out use it: the
  layout font metrics, the render context, and the guide editor's text area.
- The guide navigation bar latched a title width on first use and never re-measured it, so a font change
  left every sidebar title measured against the old one. The measurement now records the height it was
  taken at and repeats when that changes.
- GTNHLib's text measurement casts the renderer to its own font interface, which a renderer only satisfies
  while that mod's font mixin is active. The cast is checked here now, so a renderer without it falls back
  to the vanilla measurement rather than failing.
Both languages, in the wiki page and in the example resource pack the mod ships. The new attributes are
described next to the handler filters they belong with, with one example each, and the two handlers that
are hidden by default are named so a page author can ask for one.
Both were in the font helper added in this session; the point of each fits in three lines.
…xport limits

The other five surfaces this branch added can be registered globally or on a single guide; the editor
action was the exception, so a guide could not carry its own toolbar entries.

- `GuideEditorActionContribution.Provider` declares actions for one guide. The guide's entries come before
  the global ones, which is the order the toolbar and the menu lay them out in, and a provider that cannot
  answer is reported and skipped like every other plugin entry point.
- Two limits are written down in the plugin guide, both found by reading the export path: a scene element
  of another mod reaches the site only when its element is a scene annotation, and the same was already
  true of annotations themselves.
The new attributes were documented with a table row and a single-entry example, which does not show what a
list is for. Both the wiki page and the example resource pack now lead with lists: one that drops the two
handler ids that fire for an item, one where a mod id covers a whole package, and a whitelist that names
both default-hidden multiblock handlers.

The pages also say what a whitelist does not do - it only rescues handlers a blacklist would drop, so
narrowing a result still takes handlerId, handlerName or handlerOrder - and what editing the configuration
changes compared with naming a handler on a tag.
…own height

A recipe box is as tall as the handler it came from, so a row of recipes from different handlers is as
tall as its tallest box and the shorter ones left all of that difference as empty space below them. The
row now centres its boxes, which is what the details block and the search results already do for a row of
unequal heights.

That alone would have broken a wrapped row. The horizontal layout aligned every child once, against the
whole box, so a box on the first line was centred against the combined height of every line and was pushed
out of its own row. Alignment is now applied per line, with the line's own top and height, and the single
pass that used to do it at the end handles the last line - which is the only line when nothing wrapped, so
an unwrapped layout behaves exactly as before.
Centring a short recipe box only moved the empty space from below it to above and below it. What the space
is for is the recipes that follow: a recipe row is now a set of balanced columns, so the next recipe drops
into whichever column has the most room left and the gap beside a short recipe is used up.

The column count follows the widest recipe in the row, between one and four, where it used to be fixed at
two. A row of small recipes - a crafting table and its variants - therefore fits four across, while a row
containing a wide multiblock preview falls back to two, or to a plain stack when even one column cannot hold
it. The component was unused before this, so no other tag changes behaviour.

This replaces the previous attempt, which centred the boxes and left the row as tall as its tallest box; the
alignment change it needed in the horizontal layout is withdrawn with it, leaving that file as it was.
… page

Columns were an equal share of the available width, so a row of narrow recipes had its columns spaced
further apart than the recipes themselves: a 110 wide recipe in a 150 wide column showed a 40 pixel gap
between neighbours, and the wider the page the larger that gap grew.

- A column is now as wide as the widest recipe in the group, so neighbours are only the gap apart.
- The two places that wrapped recipe boxes in a row both use this now. One of them was unreachable, which
  is why only one had been changed.
The asset guard refused any path containing a parent step, which is every language candidate: a lookup
tries `_en_us/<path>` and `_zh_cn/<path>` first, and a page under a language folder reaches the guide's
shared assets as `_en_us/../assets/...`. Those resolve back inside the content root - the root keeps its
assets beside the language folders - so the guard rejected the paths that find those assets, and every
`<ImportStructure>` pointing at a shared structure reported a missing file.

The guard now asks whether the path stays inside the content root once its parent steps are applied, which
is what the join does, rather than whether it mentions a parent step at all. A path that still climbs when
resolved is refused as before, so the protection is unchanged.

Checked against the paths from the report: both language candidates and the plain path are accepted, while
`../assets/...`, `../../outside.md` and `sub/../../outside.md` remain refused.
The editor's syntax model is cached and rebuilt when the registry's revision changes. Only two of the four
registrations it is built from bumped that revision: a syntax contributor and a syntax slot did, while a
tag compiler provider and a scene element compiler provider did not, even though their compilers publish the
tag names the model offers.

A mod registering a provider after a guide's extension collection was built therefore kept completing from
a model that did not know its tags, until the guide was reloaded. The built-in integration registers during
pre-init, so this only reached a mod that registers later, which is exactly what the registry is for.

The revision is now bumped by all four, which is what the field is for.
…t say

The comments had grown back: 472 lines across the changed files, including 13 blocks over five lines, most
of them a second and third paragraph restating in prose what the first sentence and the code already said.
They are now 185 lines with no block over five lines.

- A javadoc keeps its summary sentence and drops the paragraph that followed it. The reasons a comment
  exists for - why a field is volatile, why a value is captured rather than read - are the first sentence, so
  they stay.
- A short javadoc on one member becomes the one-line form the rest of the codebase uses.
- A comment that only restates the name of what it documents is gone: `/** Whether a renderer answers for a
  fence name. */` above `answers(renderer, fenceName)` says nothing new.
- Measurements, rejected approaches and the reasoning that led to a decision stay in the commits that made
  them.
Completion was lost for most keystrokes, and typing a tag like `<I` showed nothing until one more character
was typed. Two callers closed the session immediately after the edit that tells it to query:

    runGuideEditorTextMutation(...)   // sets the marker, from inside
    autocompleteController.close();   // and this clears it again

close() does two things, and only one of them was wanted: it drops the popup, and it clears the marker an
edit sets so the next tick queries. Clearing a marker that was set microseconds earlier meant the caret was
never completed on the tick after the key, which is why the popup appeared only once typing paused or
another key followed.

- The key-forwarding path no longer closes: the session closed itself to get there.
- Accepting a candidate closes through the new closeForAppliedEdit, which drops the popup and leaves the
  marker alone, so typing on after a commit still completes.

Replayed both paths and the commit path against the controller's decision: with the stray close the query
never runs on the following tick, and without it, and on the commit path, it does.
EtFuturum and SimpleSkinBackport are compile-time dependencies now, so their helpers call the APIs directly behind @Optional.Method. NeiRecipeLookup no longer probes NeiDirectCalls by name; the per-call guards already cover a missing or incompatible NEI.
isAvailable/isNeiLoaded/isModelProvided/isQuestHoverAvailable and the RecipeAvailabilityProvider layer all answered Mods.*.isModLoaded() one indirection deep, and the bootstrap already gates every registration on the same check. Callers now read the enum directly.
FML's ModAPITransformer and SideTransformer delete annotated methods outright, so an @Optional.Method or @SideOnly entry point that callers reach unconditionally becomes a NoSuchMethodError rather than a stub. isDiagramGroupHandler, renderEmbedded, getEmbeddedTooltip and loadPreviewPlayer were in that shape; each is now an unannotated guard over a private annotated impl, the same layout CarpentersBlocksHelpers already used.
@Nana-Sakura

Copy link
Copy Markdown

I beg your not opening so big pr next time..

@Dream-Master
Dream-Master merged commit 515644a into master Sep 14, 2026
1 check passed
@Dream-Master
Dream-Master deleted the guide-syntax-registry branch September 14, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants