JSON `clickEvent` research (FTB Quests 1.21.1 · vanilla 1.21.1 · Tenet)
Minecraft 1.21.1Fabric + NeoForgeTenet 0.2.0
Primary sources only: FTB-Quests 1.21.1/main GitHub source, local FTB jars
(ftb-quests-neoforge-2101.1.34.jar, ftb-library-neoforge-2101.1.35.jar) via
javap -c, Mojang-mapped 1.21.1 merged jar in tenet/.gradle/loom-cache via
javap -c, and Tenet/Armature sources below.
1. FTB Quests: raw JSON paragraphs keep real Components#
Quest.getDescription()parses each raw line withTextUtils.parseRawText(Quest.java→getDescription();TextUtils.java→parseRawText).parseRawText: trimmed{…}/[…]→ vanillaComponent.Serializer.fromJson(UNESCAPER.translate(s), provider); anything else (or JSON failure) → FTB legacyClientTextComponentUtils.parse(TextUtils.java→parseRawText). So a JSON paragraph keepscolor/underlined/clickEventas a realStyle; legacy codes never produce click events.- Render:
ViewQuestPanel.addDescriptionText(boolean, Component)shows the current{@pagebreak}page (pageIndices/getCurrentPage), oneQuestDescriptionField(FTBTextField) perComponentviasetText(component); image-contents go toImageComponentWidget(ViewQuestPanel.java→addDescriptionText,findImageComponent). FTB pages are description-line ranges (Quest.java→buildDescriptionIndex).
2. FTB click handling: change_page means quest id, not page#
- Press path:
QuestDescriptionField.mousePressed→getComponentStyleAt(…)→handleCustomClickEvent(style)first, else vanillaScreen.handleComponentClicked(style)(ViewQuestPanel.java→QuestDescriptionField.mousePressed). handleCustomClickEvent(ViewQuestPanel.java→ innerQuestDescriptionField):CHANGE_PAGE→new ImageClickAction(OPEN_QUEST, value).run().OPEN_URL+ value startsdocs:→SHOW_DOCS(value.substring(5)), elseOPEN_URI(value); exception →errorToPlayer("Can't open url for %s (%s)").- Anything else returns false → vanilla dispatch runs.
ImageClickAction.openQuest(ImageClickAction.java→openQuest): split value on/;parseHexId(fields[0]); unknown →Unknown quest object id, unparsable →Invalid quest object id; numericfields[1]+ viewed quest →viewQuestPanel.setCurrentPage(questId, parseInt−1), thenquestScreen.open(qo, false). Value = FTB code string%016Xuppercase hex (QuestObjectBase.getCodeString,.ftbq-src/QuestObjectBase.java:82-84), optionallyHEXID/1-based-page.- Editor proof:
MultilineTextEditorScreen.doLinkInsertioninserts{"text": "%s", "underlined": true, "clickEvent": {"action": "%s", "value": "%s"}}; quest-link dialog passeschange_page+getCodeString(quest.id), URL dialog passesopen_url(jar:lambda$openQuestLinkInsertScreen$10,lambda$openTextLinkInsertScreen$11). - Browser path:
ActionType.OPEN_URIconsumer =FTBQuestsClient.openUri(ImageClickAction.java→ActionType):URI.create→ ifchatLinksPromptshowConfirmLinkScreen(cb, url, trusted=false), elseUtil.getPlatform().openUridirectly; confirm restores prior screen (jarFTBQuestsClient→openUri,lambda$openUri$1). No FTB scheme allowlist — the vanilla prompt is the gate.
3. Vanilla 1.21.1 vocabulary and change_page page semantics#
- Actions (
ClickEvent$Action, merged jar):OPEN_URL,OPEN_FILE,RUN_COMMAND,SUGGEST_COMMAND,CHANGE_PAGE,COPY_TO_CLIPBOARD; serialisedopen_url/open_file/run_command/suggest_command/change_page/copy_to_clipboard;allowFromServertrue for all exceptOPEN_FILE. Screen.handleComponentClicked(merged jar →Screen): shift+insertion → chat insert;OPEN_URLgated onchatLinks→Util.parseAndValidateUntrustedUri→chatLinksPrompt?ConfirmLinkScreen(cb, value, trusted=false): directopenUri(failure logsCan't open url for {});OPEN_FILE→openFile;SUGGEST_COMMAND→insertText(filterText, true);RUN_COMMANDmust start/→sendUnsignedCommand(rest)else error log;COPY_TO_CLIPBOARD→keyboardHandler.setClipboard; unknown logsDon't know how to handle {}.Util.parseAndValidateUntrustedUri(merged jar →Util): scheme must be inALLOWED_UNTRUSTED_LINK_PROTOCOLS=http,httpsonly, elseURISyntaxException.BookViewScreen.handleComponentClickedoverride (merged jar →BookViewScreen):CHANGE_PAGE→forcePage(Integer.parseInt(value) − 1), parse failure → false; else super (closes book if super ranRUN_COMMAND).setPageclamps withMth.clamp(0, pageCount−1). Vanilla value = 1-based decimal page-number string; garbage is ignored, out-of-range clamps.- Crux: FTB reuses the
change_pageaction name with quest-id values; vanilla books use it with page-index values. Tenet must implement FTB's reading.
4. Tenet today: JSON keeps words, drops everything else#
FtbText.parse(tenet/.../client/FtbText.java:505): whole-string JSON →asComponent(:797) → singleJsonText(raw, fallback);flatten(:833) keeps onlytext/translate/extra words — dropscolor,underlined,clickEvent,hoverEvent.- Drop point:
appendRenderedJsonText arm (FtbText.java:211-212) —span(residual, spans, json.fallback(), WHITE, "", false, false, false): thelinkarg is hardcoded"", so no span is ever clickable and ink is white. Span(start, end, argb, link, bold, italic, underline)(:112);linkis a URL-or-empty;spanAt(:235).OpenUrl{open_url:}segments do populatelink(:204-205) — JSON never reaches that path.- Draw (
QuestBookScreen.java:26949-26976):link = piece.link() ?? look.link(), underline when linked,linkRects.add(new LinkRect(box, link))(:26976).LinkRect(box, url)(:589);linkRects(:603);pressedLink(:606). - Press
mouseClicked(:28048-28055, button 0 + QUEST panel + not editing); release (:29208-29216) →openLink(url); hover URL tooltipdrawLinkTarget(:22616-22623). openLink(:7827-7842): http/https only, else status message, thenUtil.getPlatform().openUri— matches vanilla's scheme rule, minus prompt.- Markdown
[label](url)also lands here viaRichText.Piece.link(armature/.../ui/kit/RichText.java:84, read at:26952). - Images already solve the quest-open half:
ClickAction7 types (tenet/.../quest/ClickAction.java:29-74);pressElement(:23821-23858)OPEN_QUEST→entryFor(:4474) →cacheEntryFor(:4479, id/alias, case-insensitive;ClientQuestCache.Entry.matches,ClientQuestCache.java:581-603) →selectedQuest+openOverlay;OPEN_URIreusesopenLink. - Pinned by test:
FtbTextTest.java:245-247({open_url:}span link),:259(nothing else opens),:267-269(component → fallback words).
5. Recommended approach#
- Parse, don't flatten, two actions. In
asComponent/flatten, extract per-node(action, value)alongside words: supportopen_url→ URL andchange_page→ quest ref; keep flatteningrun/suggest/copy/open_fileto plain words (no chat box, commands, or clipboard role in a quest book). - Carry it on the span. Widen
Spanwith aclick(e.g.Url(url)/Quest(ref)), populated from JSON nodes (and map JSONcolorincl.#hex→Span.argb, so JSON links look underlined+coloured like FTB).emitWords(:26850-26875) rebases spans already — no change needed there. - Dispatch beside
LinkRect. Widen the rect to carry the click (URL vs quest): URL → existingopenLinkhttp/https gate; quest →entryFor(ref)- the
pressElementOPEN_QUESTopen path (:23827-23838), same unknown-id status on miss. Keep press-and-release-on-same-rect + tooltip.
- the
- Map
change_pagevalues at conversion. FTB values are%016Xhex (HEXID[/1-based-page]); Tenet ids are lowercase strings remapped by the converter (docs/authoring/ftb-mapping.md:97). Rewrite values through that same id table; drop the/pagesuffix (Tenet prose scrolls — no pages). RuntimeentryForalready resolves id/alias case-insensitively, so it also accepts hand-written Tenet ids. - Document the gate: JSON clicks only work when the whole paragraph is
one component (
asComponentwhole-string rule,:505-510); other actions degrade to their visible words, matching today's fallback.