Research Datagen
Research content is authored via datagen. The ResearchProvider orchestrator collects bundles from ResearchSubProviders and writes JSON files.
DataGenerators wiring
DataGenerators.gatherData requires a shared LanguageProviderCache and ResearchCache:
var langCache = new LanguageProviderCache("en_us");
var researchCache = new ResearchCache();
generator.addProvider(true, NeoBookProvider.of(event, langCache, researchCache,
new DemoBook(),
new DemoIndexBook(),
new DemoLeaflet()
));
generator.addProvider(true, NeoResearchProvider.of(event, langCache, researchCache, new DemoResearch(Modonomicon.MOD_ID)));
generator.addProvider(true, new EnUsProvider(generator.getPackOutput(), langCache));
NeoBookProvider.of() (and platform equivalents) take LanguageProviderCache and ResearchCache parameters. NeoResearchProvider, FabricResearchProvider, and ForgeResearchProvider are the platform-specific research providers.
Key classes
| Class | Purpose |
|---|---|
ResearchProvider | Top-level orchestrator, collects bundles from sub-providers |
SingleResearchSubProvider | Abstract base for authoring one research bundle |
ResearchDataBuilder | Low-level collector for facts, values, nodes, hooks |
ResearchNodeBuilder | Fluent builder for complex research nodes |
ResearchIngressHelper | Fluent helper for research ingress from external events |
ResearchCache | Shared cache for merging book-generated and authored research |
Declaring facts and values
var fact = this.fact("mymod/my_fact");
var value = this.value("mymod/my_counter");
Creating nodes
// Simple node requiring a fact
var node = this.node("mymod/my_node", fact);
// Node via fluent builder
this.nodeBuilder(this.node("mymod/my_node"))
.withFact(fact)
.withValue(new ResearchNodeSpec.ValueRequirement(value, 10))
.build();
Research ingress
// When an entry is viewed, grant a fact
this.ingress().onEntryViewedOnce(entryId).grantFact("mymod/my_hook", fact);
// When an item is crafted, increment a value
this.ingress().onItemCrafted(stackTemplate)
.incrementValue("mymod/my_hook", value, 1);
// When an advancement is earned, grant a fact
this.ingress().onAdvancementEarned(advancementId)
.declareFact("mymod/advancement_fact");
Generic Ingress
For custom trigger types, use the generic ingress entry point with typed targets:
this.ingress().on(myTriggerType, target)
.declareFact("mymod/my_hook");
The target type must match the trigger type's target type (e.g. Identifier for entry/advancement triggers, ItemStackTemplate for item triggers).
See Custom Research Hooks for details.
Entry markers
Entries can show an attention dot until a condition completes (see marker_condition in Entries).
For the common cases the backing hook + fact + node research is generated automatically during book compilation,
either on the entry model or through the ConditionHelper:
// In your CategoryProvider — model-based, research generated at compile time:
this.add(new MyEntry(this).generate()
.markerUntilEntryViewed(otherEntry))
.withParent(otherEntry);
this.add(new MyCraftingEntry(this).generate()
.markerUntilCrafted(Items.DIAMOND_SWORD));
this.add(new MyAcquiringEntry(this).generate()
.markerUntilAcquired(new ItemStackTemplate(Items.NETHER_STAR)));
// ... or helper-based, combined with an explicit marker condition:
entry.withMarkerCondition(this.condition().markerUntilAcquired(Items.NETHER_STAR));
markerUntilCrafted / markerUntilAcquired accept ItemStackTemplate, Item or ItemLike.
Matching follows the item trigger semantics: the template's item must match, and every component defined
on the template must be present on the crafted/acquired stack. There is no markerUntilAdvancement shorthand;
advancement-backed markers use the manual hook + fact + node pattern (see Research Conditions).
Generated research lands in the book's generated/<book_id> bundle alongside the hierarchy research:
- entry viewed: facts in
generated/<book_id>/facts/entry_viewed_once/<entry_path>, nodes ingenerated/<book_id>/nodes/entry_viewed_once/... - item crafted: facts in
generated/<book_id>/facts/item_crafted/<namespace>_<item_path>[_<components_hash>], nodes ingenerated/<book_id>/nodes/item_crafted/... - item acquired: same scheme under
.../facts/item_acquired/...and.../nodes/item_acquired/...
Node ids are deterministic from the target, so repeated requests for the same target are deduplicated into a single fact + node.
An explicit marker condition (withMarkerCondition / withMarker) combined with markerUntil* intents on the same entry
is merged with and.
Demo: MarkersCategory.java exercises the explicit, entry-viewed, crafted and acquired variants; IndexMarkersCategory.java covers markers in the index mode book.
Multi-stage nodes
var stage1Ref = this.stageRef("mymod/stage1");
var stage2Ref = this.stageRef("mymod/stage2");
this.nodeBuilder(this.node("mymod/my_node"))
.withFact(someFact)
.withStage(ResearchStageSpec.valuesOnly(stage1Ref, List.of(
new ResearchNodeSpec.ValueRequirement(someValue, 10)
)))
.withStage(ResearchStageSpec.valuesOnly(stage2Ref, List.of(
new ResearchNodeSpec.ValueRequirement(someValue, 25)
)))
.build();
Toast notifications
var toast = new ResearchToastDefinition(
"research_toast.mymod.node_unlocked", // title key
List.of(), // title args
"research_toast.modonomicon.research.node", // description key
someIcon // optional icon
);
this.nodeBuilder(this.node("mymod/my_node"))
.withToast(toast)
.build();
Config
ClientConfig.showResearchToasts(default:true) — controls toast notifications for research events