Skip to content

Commit db3ce21

Browse files
authored
feat: research system (#374)
1 parent 42371a3 commit db3ce21

305 files changed

Lines changed: 10043 additions & 2489 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

common/src/main/java/com/klikli_dev/modonomicon/api/ModonomiconConstants.java

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class ModonomiconConstants {
1313
public static class Data {
1414
public static final String MODONOMICON_DATA_PATH = ModonomiconAPI.ID + "/books";
1515
public static final String MULTIBLOCK_DATA_PATH = ModonomiconAPI.ID + "/multiblocks";
16+
public static final String RESEARCH_DATA_PATH = ModonomiconAPI.ID + "/research";
1617

1718
public static class Book {
1819
public static final String DEFAULT_FONT = Identifier.fromNamespaceAndPath(ModonomiconAPI.ID, "default").toString();
@@ -58,12 +59,12 @@ public static class Gui {
5859
public static final String BUTTON_VISUALIZE = PREFIX + "button.visualize";
5960
public static final String BUTTON_VISUALIZE_TOOLTIP = PREFIX + "button.visualize.tooltip";
6061

61-
public static final String BUTTON_READ_ALL = PREFIX + "button.read_all";
62-
public static final String BUTTON_READ_ALL_TOOLTIP_READ_UNLOCKED = PREFIX + "button.read_all.tooltip.read_unlocked";
63-
public static final String BUTTON_READ_ALL_TOOLTIP_READ_ALL = PREFIX + "button.read_all.tooltip.read_all";
64-
public static final String BUTTON_READ_ALL_TOOLTIP_NONE = PREFIX + "button.read_all.tooltip.none";
65-
public static final String BUTTON_READ_ALL_TOOLTIP_SHIFT_INSTRUCTIONS = PREFIX + "button.read_all.tooltip.shift";
66-
public static final String BUTTON_READ_ALL_TOOLTIP_SHIFT_WARNING = PREFIX + "button.read_all.tooltip.shift_warning";
62+
public static final String BUTTON_VIEWED_ONCE_RESEARCH = PREFIX + "button.viewed_once_research";
63+
public static final String BUTTON_VIEWED_ONCE_RESEARCH_TOOLTIP_VISIBLE = PREFIX + "button.viewed_once_research.tooltip.visible";
64+
public static final String BUTTON_VIEWED_ONCE_RESEARCH_TOOLTIP_ALL = PREFIX + "button.viewed_once_research.tooltip.all";
65+
public static final String BUTTON_VIEWED_ONCE_RESEARCH_TOOLTIP_NONE = PREFIX + "button.viewed_once_research.tooltip.none";
66+
public static final String BUTTON_VIEWED_ONCE_RESEARCH_TOOLTIP_SHIFT_INSTRUCTIONS = PREFIX + "button.viewed_once_research.tooltip.shift";
67+
public static final String BUTTON_VIEWED_ONCE_RESEARCH_TOOLTIP_SHIFT_WARNING = PREFIX + "button.viewed_once_research.tooltip.shift_warning";
6768

6869
public static final String HOVER_BOOK_LINK = PREFIX + "hover.book_link";
6970
public static final String HOVER_BOOK_LINK_ERROR = PREFIX + "hover.book_link.error";
@@ -126,13 +127,11 @@ public static class Subtitles {
126127
public static class Tooltips {
127128
public static final String PREFIX = "tooltip." + ModonomiconAPI.ID + ".";
128129
public static final String CONDITION_PREFIX = PREFIX + "condition.";
129-
public static final String CONDITION_ADVANCEMENT = CONDITION_PREFIX + "advancement";
130-
public static final String CONDITION_ADVANCEMENT_LOADING = CONDITION_ADVANCEMENT + ".loading";
131-
public static final String CONDITION_ADVANCEMENT_HIDDEN = CONDITION_ADVANCEMENT + ".hidden";
132130
public static final String CONDITION_MOD_LOADED = CONDITION_PREFIX + "mod_loaded";
133-
public static final String CONDITION_ENTRY_UNLOCKED = CONDITION_PREFIX + "entry_unlocked";
131+
public static final String CONDITION_RESEARCH_NODE_UNLOCKED = CONDITION_PREFIX + "research_node_unlocked";
132+
public static final String CONDITION_RESEARCH_STAGE_COMPLETED = CONDITION_PREFIX + "research_stage_completed";
134133
public static final String CONDITION_CATEGORY_HAS_VISIBLE_ENTRIES = CONDITION_PREFIX + "has_visible_entries";
135-
public static final String CONDITION_ENTRY_READ = CONDITION_PREFIX + "entry_read";
134+
public static final String CONDITION_ENTRY_UNLOCKED = CONDITION_PREFIX + "entry_unlocked";
136135
public static final String RECIPE_PREFIX = PREFIX + "recipe.";
137136
public static final String RECIPE_CRAFTING_SHAPELESS = RECIPE_PREFIX + "crafting_shapeless";
138137
public static final String ITEM_NO_BOOK_FOUND_FOR_STACK = PREFIX + "no_book_found_for_stack";
@@ -146,10 +145,20 @@ public static class Command {
146145

147146
public static final String ERROR_PREFIX = PREFIX + "error.";
148147
public static final String ERROR_UNKNOWN_BOOK = ERROR_PREFIX + "unknown_book";
148+
public static final String ERROR_UNKNOWN_FACT = ERROR_PREFIX + "unknown_fact";
149+
public static final String ERROR_UNKNOWN_NODE = ERROR_PREFIX + "unknown_node";
150+
public static final String ERROR_UNKNOWN_VALUE = ERROR_PREFIX + "unknown_value";
151+
public static final String ERROR_UNKNOWN_STAGE = ERROR_PREFIX + "unknown_stage";
149152
public static final String ERROR_LOAD_PROGRESS = ERROR_PREFIX + "load_progress";
150153
public static final String ERROR_LOAD_PROGRESS_CLIENT = ERROR_PREFIX + "load_progress_client";
151154
public static final String SUCCESS_PREFIX = PREFIX + "success.";
152155
public static final String SUCCESS_RESET_BOOK = SUCCESS_PREFIX + "reset_book";
156+
public static final String SUCCESS_RESET_RESEARCH = SUCCESS_PREFIX + "reset_research";
157+
public static final String SUCCESS_GRANT_FACT = SUCCESS_PREFIX + "grant_fact";
158+
public static final String SUCCESS_REVOKE_FACT = SUCCESS_PREFIX + "revoke_fact";
159+
public static final String SUCCESS_UNLOCK_NODE = SUCCESS_PREFIX + "unlock_node";
160+
public static final String SUCCESS_LOCK_NODE = SUCCESS_PREFIX + "lock_node";
161+
public static final String SUCCESS_SET_VALUE = SUCCESS_PREFIX + "set_value";
153162
public static final String SUCCESS_SAVE_PROGRESS = SUCCESS_PREFIX + "save_progress";
154163
public static final String SUCCESS_LOAD_PROGRESS = SUCCESS_PREFIX + "load_progress";
155164
public static final String RELOAD_SUCCESS = SUCCESS_PREFIX + "reload_requested";

common/src/main/java/com/klikli_dev/modonomicon/api/datagen/AddToBookSubProvider.java

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,15 @@ public abstract class AddToBookSubProvider extends ModonomiconProviderBase imple
2626
protected int currentSortIndex;
2727

2828
/**
29-
* @param targetBook The Identifier (modid and book name) of the book to add to.
30-
* @param defaultLang The LanguageProvider to fill with this book provider. IMPORTANT: the Language Provider needs to be added to the DataGenerator AFTER the BookProvider.
29+
* Creates a subprovider for adding content to an existing book.
30+
* <p>
31+
* Language access is provided via setup injection at generate time.
32+
*
33+
* @param targetBook the Identifier (modid and book name) of the book to add to
3134
*/
32-
public AddToBookSubProvider(Identifier targetBook, BiConsumer<String, String> defaultLang) {
33-
this(targetBook, defaultLang, Map.of());
34-
}
35-
36-
/**
37-
* @param targetBook The Identifier (modid and book name) of the book to add to.
38-
* @param defaultLang The LanguageProvider to fill with this book provider. IMPORTANT: the Language Provider needs to be added to the DataGenerator AFTER the BookProvider.
39-
*/
40-
public AddToBookSubProvider(Identifier targetBook, BiConsumer<String, String> defaultLang, Map<String, BiConsumer<String, String>> translations) {
41-
super(targetBook.getNamespace(), defaultLang, translations, new BookContextHelper(targetBook.getNamespace()), new ConditionHelper());
35+
public AddToBookSubProvider(Identifier targetBook) {
36+
super(targetBook.getNamespace(), null, Map.of(), new BookContextHelper(targetBook.getNamespace()), new ConditionHelper());
4237
this.book = null;
43-
4438
this.bookId = targetBook.getPath();
4539
this.currentSortIndex = 0;
4640
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 klikli-dev
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
package com.klikli_dev.modonomicon.api.datagen;
8+
9+
import com.klikli_dev.modonomicon.api.ModonomiconConstants.I18n.Tooltips;
10+
import com.klikli_dev.modonomicon.api.datagen.book.BookEntryModel;
11+
import com.klikli_dev.modonomicon.api.datagen.book.BookModel;
12+
import com.klikli_dev.modonomicon.api.datagen.book.condition.BookResearchNodeUnlockedConditionModel;
13+
import com.klikli_dev.modonomicon.api.datagen.research.ResearchBundle;
14+
import com.klikli_dev.modonomicon.api.datagen.research.ResearchDataBuilder;
15+
import com.klikli_dev.modonomicon.api.datagen.research.ResearchFactRef;
16+
import com.klikli_dev.modonomicon.api.datagen.research.ResearchIngressHelper;
17+
import com.klikli_dev.modonomicon.api.datagen.research.ResearchNodeRef;
18+
import net.minecraft.network.chat.Component;
19+
import net.minecraft.resources.Identifier;
20+
21+
import java.util.ArrayList;
22+
import java.util.HashMap;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Optional;
26+
27+
public final class BookHierarchyResearchCompiler {
28+
29+
/**
30+
* Compiles generated research for a book from both parent hierarchy and explicit store requests.
31+
*
32+
* @param book the book model
33+
* @param store the generated research store (may be null for legacy callers)
34+
*/
35+
public Optional<CompiledBookResearch> compile(BookModel book, GeneratedBookResearchStore store) {
36+
var research = new ResearchDataBuilder(book.getId().getNamespace());
37+
var ingress = new ResearchIngressHelper(research);
38+
var parentFacts = new HashMap<Identifier, ResearchFactRef>();
39+
var generatedEntries = new ArrayList<BookEntryModel>();
40+
var generatedNodes = new HashMap<Identifier, ResearchNodeRef>();
41+
var generatedFacts = new HashMap<Identifier, ResearchFactRef>();
42+
43+
// --- Process explicit entry-viewed-once requests from the store ---
44+
if (store != null) {
45+
for (var request : store.getEntryViewedOnceRequests()) {
46+
var requiredId = request.requiredEntryId();
47+
var fact = generatedFacts.computeIfAbsent(requiredId,
48+
id -> ingress.onEntryViewedOnce(id).declareFact(store.factPath(id)));
49+
var node = generatedNodes.computeIfAbsent(requiredId,
50+
id -> research.node(store.nodePath(id), fact));
51+
}
52+
}
53+
54+
// --- Process parent hierarchy (existing behavior) ---
55+
if (book.generateEntryHierarchyResearch()) {
56+
for (var category : book.getCategories()) {
57+
for (var entry : category.getEntries()) {
58+
if (entry.hasCondition() || entry.getParents().isEmpty()) continue;
59+
var requiredFacts = new ArrayList<ResearchFactRef>();
60+
for (var parent : entry.getParents()) {
61+
var parentId = parent.getEntryId();
62+
var fact = parentFacts.computeIfAbsent(parentId, id -> ingress.onEntryViewedOnce(id).declareFact(hierarchyFactPath(book, id)));
63+
requiredFacts.add(fact);
64+
}
65+
ResearchNodeRef node = research.node(hierarchyNodePath(book, entry), requiredFacts.toArray(ResearchFactRef[]::new));
66+
entry.withCondition(
67+
BookResearchNodeUnlockedConditionModel.create()
68+
.withNode(node.id())
69+
.withTooltip(Component.translatable(Tooltips.CONDITION_ENTRY_UNLOCKED,
70+
Component.translatable(entry.getName())))
71+
);
72+
generatedEntries.add(entry);
73+
}
74+
}
75+
}
76+
77+
if (generatedEntries.isEmpty() && (store == null || store.isEmpty())) return Optional.empty();
78+
return Optional.of(new CompiledBookResearch(
79+
Identifier.fromNamespaceAndPath(book.getId().getNamespace(), bundleId(book)),
80+
research
81+
));
82+
}
83+
84+
/**
85+
* Backward-compatible overload: compiles only parent hierarchy research.
86+
*/
87+
public Optional<CompiledBookResearch> compile(BookModel book) {
88+
return this.compile(book, null);
89+
}
90+
91+
private String bundleId(BookModel book) { return "generated/" + book.getId().getPath(); }
92+
private String hierarchyFactPath(BookModel book, Identifier entryId) { return bundleId(book) + "/facts/entry_viewed_once/" + entryId.getPath().replace('/', '_'); }
93+
private String hierarchyNodePath(BookModel book, BookEntryModel entry) { return bundleId(book) + "/nodes/entry_viewed_once/" + entry.getId().getPath().replace('/', '_'); }
94+
95+
public record CompiledBookResearch(Identifier bundleId, ResearchDataBuilder research) {}
96+
}

common/src/main/java/com/klikli_dev/modonomicon/api/datagen/BookProvider.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@
88
package com.klikli_dev.modonomicon.api.datagen;
99

1010
import com.klikli_dev.modonomicon.api.ModonomiconConstants;
11+
import com.klikli_dev.modonomicon.api.datagen.research.ResearchCache;
12+
import com.google.gson.JsonArray;
13+
import com.google.gson.JsonElement;
14+
import com.klikli_dev.modonomicon.research.data.ResearchFactDefinition;
15+
import com.klikli_dev.modonomicon.research.data.ResearchHookDefinition;
16+
import com.klikli_dev.modonomicon.research.data.ResearchNodeDefinition;
1117
import com.klikli_dev.modonomicon.api.datagen.book.BookCategoryModel;
1218
import com.klikli_dev.modonomicon.api.datagen.book.BookCommandModel;
13-
import com.klikli_dev.modonomicon.api.datagen.book.BookEntryModel;
19+
import com.klikli_dev.modonomicon.api.datagen.book.BookEntryModel;
1420
import com.klikli_dev.modonomicon.api.datagen.book.page.BookPageModel;
1521
import com.klikli_dev.modonomicon.api.datagen.book.BookModel;
22+
import com.mojang.serialization.Codec;
23+
import com.mojang.serialization.JsonOps;
1624
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
1725
import net.minecraft.core.HolderLookup;
1826
import net.minecraft.data.CachedOutput;
@@ -39,15 +47,25 @@ public class BookProvider implements DataProvider {
3947
//This is a bit of a relic, one provider is only supposed to generate one book.
4048
protected final Map<Identifier, BookModel> bookModels;
4149
protected final List<BookSubProvider> subProviders;
50+
protected final BookHierarchyResearchCompiler researchCompiler = new BookHierarchyResearchCompiler();
51+
private final LanguageProviderCache langCache;
52+
private final ResearchCache researchCache;
4253

4354

4455
public BookProvider(PackOutput packOutput, CompletableFuture<HolderLookup.Provider> registries, String modId,
4556
List<BookSubProvider> subProviders) {
57+
this(packOutput, registries, modId, subProviders, null, null);
58+
}
59+
60+
public BookProvider(PackOutput packOutput, CompletableFuture<HolderLookup.Provider> registries, String modId,
61+
List<BookSubProvider> subProviders, LanguageProviderCache langCache, ResearchCache researchCache) {
4662
this.packOutput = packOutput;
4763
this.registries = registries;
4864
this.modId = modId;
4965
this.subProviders = subProviders;
5066
this.bookModels = new Object2ObjectOpenHashMap<>();
67+
this.langCache = langCache;
68+
this.researchCache = researchCache;
5169
}
5270

5371
public String modId() {
@@ -119,9 +137,35 @@ protected Path getPagePath(Path dataFolder, BookEntryModel bookEntryModel, Strin
119137

120138
Path dataFolder = this.packOutput.getOutputFolder(PackOutput.Target.DATA_PACK);
121139

140+
if (this.langCache != null) {
141+
for (var subProvider : this.subProviders) {
142+
if (subProvider instanceof ModonomiconProviderBase base) {
143+
base.injectLang(this.langCache);
144+
}
145+
}
146+
}
147+
122148
this.subProviders.forEach(subProvider -> subProvider.generate(this.bookModels::put, registries));
123149

150+
// Build a map of book id → generated research store from subproviders
151+
var researchStores = new Object2ObjectOpenHashMap<Identifier, GeneratedBookResearchStore>();
152+
for (var subProvider : this.subProviders) {
153+
if (subProvider instanceof SingleBookSubProvider singleBook) {
154+
researchStores.put(Identifier.fromNamespaceAndPath(this.modId, singleBook.bookId()), singleBook.getGeneratedResearchStore());
155+
}
156+
}
157+
124158
for (var bookModel : this.bookModels.values()) {
159+
var store = researchStores.get(bookModel.getId());
160+
var compiledResearch = this.researchCompiler.compile(bookModel, store);
161+
if (compiledResearch.isPresent()) {
162+
if (this.researchCache != null) {
163+
this.researchCache.accept(compiledResearch.get().bundleId(), compiledResearch.get().research());
164+
} else {
165+
futures.add(this.writeResearchBundle(cache, dataFolder, compiledResearch.get()));
166+
}
167+
}
168+
125169
Path bookPath = this.getPath(dataFolder, bookModel);
126170

127171
if(!bookModel.dontGenerateJson()){ //a model from AddToBookSubProvider
@@ -167,6 +211,28 @@ protected Path getPagePath(Path dataFolder, BookEntryModel bookEntryModel, Strin
167211
});
168212
}
169213

214+
protected CompletableFuture<?> writeResearchBundle(CachedOutput cache, Path dataFolder, BookHierarchyResearchCompiler.CompiledBookResearch compiled) {
215+
var base = dataFolder.resolve(compiled.bundleId().getNamespace()).resolve(ModonomiconConstants.Data.RESEARCH_DATA_PATH).resolve(compiled.bundleId().getPath());
216+
var data = compiled.research();
217+
return CompletableFuture.allOf(
218+
this.save(cache, ResearchFactDefinition.CODEC, data.factDefinitions(), base.resolve("facts.json")),
219+
this.save(cache, ResearchNodeDefinition.CODEC, data.nodeDefinitions(), base.resolve("nodes.json")),
220+
this.save(cache, ResearchHookDefinition.CODEC, data.hookDefinitions(), base.resolve("hooks.json"))
221+
);
222+
}
223+
224+
protected <T> CompletableFuture<?> save(CachedOutput cache, Codec<T> codec, List<T> values, Path path) {
225+
return DataProvider.saveStable(cache, this.list(codec, values), path);
226+
}
227+
228+
protected <T> JsonElement list(Codec<T> codec, List<T> values) {
229+
JsonArray array = new JsonArray();
230+
for (T value : values) {
231+
array.add(codec.encodeStart(JsonOps.INSTANCE, value).getOrThrow());
232+
}
233+
return array;
234+
}
235+
170236
@Override
171237
public @NotNull String getName() {
172238
return "Books: " + this.modId();

0 commit comments

Comments
 (0)