diff --git a/common/src/main/java/rearth/oracle/OracleClient.java b/common/src/main/java/rearth/oracle/OracleClient.java index fc8cc74..305288e 100644 --- a/common/src/main/java/rearth/oracle/OracleClient.java +++ b/common/src/main/java/rearth/oracle/OracleClient.java @@ -13,12 +13,14 @@ import org.lwjgl.glfw.GLFW; import rearth.oracle.ui.OracleScreen; import rearth.oracle.ui.SearchScreen; +import rearth.oracle.util.BookMetadata; import rearth.oracle.util.MarkdownParser; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Set; public final class OracleClient { @@ -26,7 +28,7 @@ public final class OracleClient { public static final KeyBinding ORACLE_WIKI = new KeyBinding("key.oracle_index.open", GLFW.GLFW_KEY_H, "key.categories.oracle"); public static final KeyBinding ORACLE_SEARCH = new KeyBinding("key.oracle_index.search", -1, "key.categories.oracle"); - public static final Set LOADED_BOOKS = new HashSet<>(); + public static final HashMap LOADED_BOOKS = new HashMap<>(); public static final HashMap ITEM_LINKS = new HashMap<>(); public static ItemStack tooltipStack; @@ -84,7 +86,7 @@ public static void init() { */ public static void openScreen(@Nullable String bookId, @Nullable Identifier entryId, @Nullable Screen parent) { if (bookId != null) - OracleScreen.activeBook = bookId; + OracleScreen.activeBook = LOADED_BOOKS.get(bookId); if (entryId != null) OracleScreen.activeEntry = entryId; @@ -96,19 +98,27 @@ private static void findAllResourceEntries() { var resources = resourceManager.findResources("books", path -> path.getPath().endsWith(".mdx")); LOADED_BOOKS.clear(); + var supportedLanguages = new HashSet(); for (var resourceId : resources.keySet()) { + var purePath = resourceId.getPath().replaceFirst("books/", ""); var segments = purePath.split("/"); var modId = segments[0]; // e.g. "oritech" var entryPath = purePath.replaceFirst(modId + "/", ""); // e.g. "tools/wrench.mdx" var entryFileName = segments[segments.length - 1]; // e.g. "wrench.mdx" var entryDirectory = entryPath.replace(entryFileName, ""); // e.g. "tools" or "processing/reactor" - - if (entryDirectory.startsWith(".translated")) continue; // skip / don't support translations for now + + + if (entryPath.startsWith(".translated")) { + var segments2 = entryDirectory.split("/"); + if (segments2.length > 1) { + supportedLanguages.add(segments2[1]); + } // e.g. ".translated/zh_cn/tools/wrench.mdx" will return "zh_cn" + } - try { - var fileContent = new String(resources.get(resourceId).getInputStream().readAllBytes(), StandardCharsets.UTF_8); + try { + var fileContent = new String(resources.get(resourceId).getInputStream().readAllBytes(), StandardCharsets.UTF_8); var fileComponents = MarkdownParser.parseFrontmatter(fileContent); if (fileComponents.containsKey("related_items")) { var baseString = fileComponents.get("related_items").replace("[", "").replace("]", ""); @@ -120,12 +130,12 @@ private static void findAllResourceEntries() { } } - } catch (IOException e) { + } catch (IOException e) { Oracle.LOGGER.error("Unable to load book with id: " + resourceId); - throw new RuntimeException(e); - } - - LOADED_BOOKS.add(modId); + throw new RuntimeException(e); + } + + LOADED_BOOKS.put(modId, new BookMetadata(modId, supportedLanguages)); } } diff --git a/common/src/main/java/rearth/oracle/command/OracleCommand.java b/common/src/main/java/rearth/oracle/command/OracleCommand.java new file mode 100644 index 0000000..8483cb9 --- /dev/null +++ b/common/src/main/java/rearth/oracle/command/OracleCommand.java @@ -0,0 +1,4 @@ +package rearth.oracle.command; + +public class OracleCommand { +} diff --git a/common/src/main/java/rearth/oracle/ui/OracleScreen.java b/common/src/main/java/rearth/oracle/ui/OracleScreen.java index 5eb3a1f..3d66036 100644 --- a/common/src/main/java/rearth/oracle/ui/OracleScreen.java +++ b/common/src/main/java/rearth/oracle/ui/OracleScreen.java @@ -4,6 +4,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import io.wispforest.owo.ui.base.BaseOwoScreen; +import io.wispforest.owo.ui.base.BaseParentComponent; import io.wispforest.owo.ui.component.Components; import io.wispforest.owo.ui.component.ItemComponent; import io.wispforest.owo.ui.component.LabelComponent; @@ -25,7 +26,9 @@ import rearth.oracle.OracleClient; import rearth.oracle.ui.components.ColoredCollapsibleContainer; import rearth.oracle.ui.components.ScalableLabelComponent; +import rearth.oracle.util.BookMetadata; import rearth.oracle.util.MarkdownParser; +import rearth.oracle.util.Util; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -40,13 +43,14 @@ public class OracleScreen extends BaseOwoScreen { private FlowLayout rootComponent; private FlowLayout leftPanel; private ScrollContainer outerContentContainer; + private BaseParentComponent langSelector; private final Screen parent; private boolean needsLayout = false; public static Identifier activeEntry; - public static String activeBook; + public static BookMetadata activeBook; private static final int wideContentWidth = 50; // in % @@ -94,7 +98,7 @@ protected void build(FlowLayout rootComponent) { outerContentContainer = Containers.verticalScroll(Sizing.fill(wideContentWidth), Sizing.fill(), contentContainer); outerContentContainer.allowOverflow(true); rootComponent.child(outerContentContainer); - + buildModNavigation(leftPanel); var outerNavigationBarContainer = Containers.verticalScroll(Sizing.content(3), Sizing.fill(80), navigationBar); @@ -155,6 +159,9 @@ private void updateLayout() { leftPanel.margins(Insets.of(0, 0, 10, 5)); outerContentContainer.horizontalSizing(Sizing.fixed(this.width - leftPanelSize - 20)); } + + langSelector.positioning(Positioning.absolute(this.width - 120, 10)) + .margins(Insets.top(10).withRight(5)); } @Override @@ -172,7 +179,7 @@ public void render(DrawContext context, int mouseX, int mouseY, float delta) { } } - private void loadContentContainer(Identifier filePath, String bookId) throws IOException { + private void loadContentContainer(Identifier filePath, BookMetadata bookMetadata) throws IOException { contentContainer.clearChildren(); activeEntry = filePath; @@ -181,31 +188,35 @@ private void loadContentContainer(Identifier filePath, String bookId) throws IOE var resourceCandidate = resourceManager.getResource(filePath); if (resourceCandidate.isEmpty()) { - System.out.println("No content file found for " + filePath); + Oracle.LOGGER.warn("No content file found for {}", filePath); return; } var fileContent = new String(resourceCandidate.get().getInputStream().readAllBytes(), StandardCharsets.UTF_8); - var parsedTexts = MarkdownParser.parseMarkdownToOwoComponents(fileContent, bookId, link -> { + var parsedTexts = MarkdownParser.parseMarkdownToOwoComponents(fileContent, bookMetadata.getBookId(), link -> { if (link.startsWith("http")) return false; var pathSegments = filePath.getPath().split("/"); - var newPath = ""; + StringBuilder newPathBuilder = new StringBuilder(); // build path based on relative information var parentIteration = link.startsWith("../") ? 1 : 0; for (int i = 0; i < pathSegments.length - 1 - parentIteration; i++) { - newPath += pathSegments[i] + "/"; + newPathBuilder.append(pathSegments[i]).append("/"); } - newPath = newPath.split("#")[0]; // anchors are not supported, so we just remove them - newPath += link.replace("../", "") + ".mdx"; // add file ending - + newPathBuilder = new StringBuilder(newPathBuilder.toString().split("#")[0]); // anchors are not supported, so we just remove them + newPathBuilder.append(link.replace("../", "")).append(".mdx"); // add file ending + + var newPath = newPathBuilder.toString(); + + Oracle.LOGGER.info("Loading content file: " + newPath); + var newId = Identifier.of(Oracle.MOD_ID, newPath); try { - loadContentContainer(newId, bookId); + loadContentContainer(newId, bookMetadata); } catch (IOException e) { return false; } @@ -241,9 +252,9 @@ private void loadContentContainer(Identifier filePath, String bookId) throws IOE } private void buildModNavigation(FlowLayout buttonContainer) { - - // collect all book ids - var bookIds = OracleClient.LOADED_BOOKS.stream() + + // collect all book metadata + var bookMetadataList = OracleClient.LOADED_BOOKS.values().stream() .sorted() .toList(); @@ -251,7 +262,7 @@ private void buildModNavigation(FlowLayout buttonContainer) { modSelectorDropdown.zIndex(5); if (activeBook == null) - activeBook = bookIds.getFirst(); + activeBook = bookMetadataList.getFirst(); if (activeEntry != null) { try { @@ -265,6 +276,8 @@ private void buildModNavigation(FlowLayout buttonContainer) { if (this.height < 350) { topMargins = 5; } + + buildLangSelector(); var bookTitleLabel = new ScalableLabelComponent(Text.translatable(Oracle.MOD_ID + ".title." + activeBook).formatted(Formatting.DARK_GRAY).append(" >").formatted(Formatting.DARK_GRAY), text -> false); bookTitleLabel.scale = 1.5f; @@ -275,12 +288,8 @@ private void buildModNavigation(FlowLayout buttonContainer) { bookTitleWrapper.child(bookTitleLabel); buttonContainer.child(bookTitleWrapper.zIndex(5)); - bookTitleWrapper.mouseEnter().subscribe(() -> { - bookTitleWrapper.surface(MarkdownParser.ORACLE_PANEL_HOVER); - }); - bookTitleWrapper.mouseLeave().subscribe(() -> { - bookTitleWrapper.surface(MarkdownParser.ORACLE_PANEL); - }); + bookTitleWrapper.mouseEnter().subscribe(() -> bookTitleWrapper.surface(MarkdownParser.ORACLE_PANEL_HOVER)); + bookTitleWrapper.mouseLeave().subscribe(() -> bookTitleWrapper.surface(MarkdownParser.ORACLE_PANEL)); bookTitleWrapper.mouseDown().subscribe((a, b, c) -> { if (modSelectorDropdown.hasParent()) { modSelectorDropdown.remove(); @@ -290,33 +299,95 @@ private void buildModNavigation(FlowLayout buttonContainer) { return true; }); - for (var bookId : bookIds) { + for (var bookMetadata : bookMetadataList) { + var bookId = bookMetadata.getBookId(); modSelectorDropdown.button(Text.translatable(Oracle.MOD_ID + ".title." + bookId), elem -> { + activeBook = bookMetadata; activeEntry = null; modSelectorDropdown.remove(); - buildModNavigationBar(bookId); + rootComponent.removeChild(langSelector); + buildLangSelector(); + buildModNavigationBar(bookMetadata); bookTitleLabel.text(Text.translatable(Oracle.MOD_ID + ".title." + bookId).formatted(Formatting.DARK_GRAY).append(" >").formatted(Formatting.DARK_GRAY)); - activeBook = bookId; }); } buildModNavigationBar(activeBook); } + + private void buildLangSelector() { + var langLabel = Components.label(Text.translatable("oracle_index.label.lang")) + .color(Color.ofFormatting(Formatting.WHITE)) + .margins(Insets.right(5)); + + var currentLangText = Components.label( + Util.getLanguageText(activeBook.getCurrentLanguage()).copy().formatted(Formatting.DARK_GRAY) + ); + + var currentLangTextWrapper = Containers.horizontalFlow(Sizing.content(), Sizing.content()); + currentLangTextWrapper.surface(MarkdownParser.ORACLE_PANEL); + currentLangTextWrapper.mouseEnter().subscribe(() -> currentLangTextWrapper.surface(MarkdownParser.ORACLE_PANEL_HOVER)); + currentLangTextWrapper.mouseLeave().subscribe(() -> currentLangTextWrapper.surface(MarkdownParser.ORACLE_PANEL)); + currentLangTextWrapper.child(currentLangText.margins(Insets.of(3, 5, 3, 5))); + + var langLabelWrapper = Containers.horizontalFlow(Sizing.content(), Sizing.content()); + langLabelWrapper.child(langLabel); + langLabelWrapper.child(currentLangTextWrapper); + + var langDropdown = Components.dropdown(Sizing.content()); + + // add supported languages + for (String lang : activeBook.getSupportedLanguages()) { + langDropdown.button(Util.getLanguageText(lang).copy().formatted(Formatting.WHITE), dropdownComponent -> { + activeBook.setCurrentLanguage(lang); + currentLangText.text(Util.getLanguageText(lang).copy().formatted(Formatting.DARK_GRAY)); + langDropdown.remove(); + + // reload entries and content + if (activeEntry != null) { + try { + Oracle.LOGGER.info("Reloading content for " + activeEntry.getPath()); + activeEntry = Identifier.of(Oracle.MOD_ID, activeBook.convertPathToCurrentLanguage(activeEntry.getPath())); + buildModNavigationBar(activeBook); + loadContentContainer(activeEntry, activeBook); + } catch (IOException e) { + Oracle.LOGGER.error("Failed to reload content: " + e.getMessage()); + } + } + }).zIndex(10); + } + + // show dropdown + langLabelWrapper.mouseDown().subscribe((mouseX, mouseY, button) -> { + if (langDropdown.hasParent()) { + langDropdown.remove(); + return true; + } + + langDropdown.positioning(Positioning.absolute(langLabelWrapper.x(), langLabelWrapper.y() + langLabelWrapper.height())); + rootComponent.child(langDropdown); + return true; + }); + + langSelector = langLabelWrapper + .positioning(Positioning.absolute(this.width - 120, 10)) + .margins(Insets.top(10).withRight(5)); + rootComponent.child(langSelector); + } - private void buildModNavigationBar(String bookId) { + private void buildModNavigationBar(BookMetadata bookMetadata) { navigationBar.clearChildren(); - buildNavigationEntriesForModPath(bookId, "", navigationBar); + buildNavigationEntriesForModPath(bookMetadata, "", navigationBar); } - private void buildNavigationEntriesForModPath(String bookId, String path, FlowLayout container) { - + private void buildNavigationEntriesForModPath(BookMetadata bookMetadata, String path, FlowLayout container) { var resourceManager = MinecraftClient.getInstance().getResourceManager(); - var metaPath = Identifier.of(Oracle.MOD_ID, "books/" + bookId + path + "/_meta.json"); + var metaPath = Identifier.of(Oracle.MOD_ID, bookMetadata.getEntryPath(path) + "/_meta.json"); var resourceCandidate = resourceManager.getResource(metaPath); if (resourceCandidate.isEmpty()) { - System.out.println("No _meta.json found for " + bookId + " at " + metaPath); + System.out.println("No _meta.json found for " + bookMetadata.getBookId() + " at " + metaPath); return; } @@ -327,8 +398,9 @@ private void buildNavigationEntriesForModPath(String bookId, String path, FlowLa if (activeEntry == null) { var firstEntry = entries.stream().filter(elem -> !elem.directory).findFirst(); if (firstEntry.isPresent()) { - var firstEntryPath = Identifier.of(Oracle.MOD_ID, "books/" + bookId + path + "/" + firstEntry.get().id()); - loadContentContainer(firstEntryPath, bookId); + // Oracle.LOGGER.info(bookMetadata.getEntryPath(path) + firstEntry.get().id()); + var firstEntryPath = Identifier.of(Oracle.MOD_ID, bookMetadata.getEntryPath(path) + "/" + firstEntry.get().id()); + loadContentContainer(firstEntryPath, bookMetadata); activeEntry = firstEntryPath; } } @@ -341,7 +413,7 @@ private void buildNavigationEntriesForModPath(String bookId, String path, FlowLa Sizing.content(1), Sizing.content(1), Text.translatable(entry.name()).formatted(Formatting.WHITE), false); - buildNavigationEntriesForModPath(bookId, path + "/" + entry.id(), directoryContainer); + buildNavigationEntriesForModPath(bookMetadata, path + "/" + entry.id(), directoryContainer); directoryContainer.margins(Insets.of(0, 0, 0, 0)); container.child(directoryContainer); @@ -360,23 +432,19 @@ private void buildNavigationEntriesForModPath(String bookId, String path, FlowLa levelContainers.add(directoryContainer); } else { - final var labelPath = Identifier.of(Oracle.MOD_ID, "books/" + bookId + path + "/" + entry.id()); + final var labelPath = Identifier.of(Oracle.MOD_ID, bookMetadata.getEntryPath(path) + "/" + entry.id()); final var labelText = Text.translatable(entry.name).formatted(Formatting.WHITE); final var label = Components.label(labelText.formatted(Formatting.UNDERLINE)); - label.mouseEnter().subscribe(() -> { - label.text(labelText.copy().formatted(Formatting.GRAY)); - }); - label.mouseLeave().subscribe(() -> { - label.text(labelText.copy()); - }); + label.mouseEnter().subscribe(() -> label.text(labelText.copy().formatted(Formatting.GRAY))); + label.mouseLeave().subscribe(() -> label.text(labelText.copy())); label.mouseDown().subscribe((a, b, c) -> { try { - loadContentContainer(labelPath, bookId); + loadContentContainer(labelPath, bookMetadata); return true; } catch (IOException e) { - Oracle.LOGGER.error(e.getMessage()); + Oracle.LOGGER.error(e.getMessage(), e); return false; } }); diff --git a/common/src/main/java/rearth/oracle/util/BookMetadata.java b/common/src/main/java/rearth/oracle/util/BookMetadata.java new file mode 100644 index 0000000..df6e7d1 --- /dev/null +++ b/common/src/main/java/rearth/oracle/util/BookMetadata.java @@ -0,0 +1,75 @@ +package rearth.oracle.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Set; + +public class BookMetadata implements Comparable { + public static final String DEFAULT_LANGUAGE = "en_us"; + + private final String bookId; + private final Set supportedLanguages = new HashSet<>(); + private String currentLanguage = DEFAULT_LANGUAGE; + + public BookMetadata(String bookId, Set supportedLanguages) { + this.bookId = bookId; + supportedLanguages.add(DEFAULT_LANGUAGE); + this.supportedLanguages.addAll(supportedLanguages); + } + + public String getBookId() { + return bookId; + } + + public Set getSupportedLanguages() { + return supportedLanguages; + } + + public boolean isSupportedLanguage(String language) { + return supportedLanguages.contains(language); + } + + public void updateSupportedLanguages(Set supportedLanguages) { + if (this.supportedLanguages.equals(supportedLanguages)) return; + this.supportedLanguages.clear(); + this.supportedLanguages.addAll(supportedLanguages); + } + + public String getCurrentLanguage() { + return currentLanguage; + } + + public void setCurrentLanguage(String currentLanguage) { + if(currentLanguage == null || !supportedLanguages.contains(currentLanguage)) return; + this.currentLanguage = currentLanguage; + } + + public boolean isCurrentLanguageDefault() { + return currentLanguage.equals(DEFAULT_LANGUAGE); + } + + public boolean isCurrentLanguageSupported() { + return supportedLanguages.contains(currentLanguage); + } + + public String getEntryPath(String path) { + if (path.startsWith("/")) path = path.substring(1); + if (isCurrentLanguageDefault()) return "books/" + bookId + "/" + path; + else return "books/" + bookId + "/.translated/" + currentLanguage + "/" + path; + } + + public String convertPathToCurrentLanguage(String path) { + return getEntryPath(path.replaceFirst("^books/[^/]+/(?:.translated/[^/]+/)?", "")); + } + + @Override + public int compareTo(@NotNull BookMetadata bookMetadata) { + return bookId.compareTo(bookMetadata.bookId); + } + + @Override + public String toString() { + return bookId; + } +} diff --git a/common/src/main/java/rearth/oracle/util/Util.java b/common/src/main/java/rearth/oracle/util/Util.java new file mode 100644 index 0000000..d51677f --- /dev/null +++ b/common/src/main/java/rearth/oracle/util/Util.java @@ -0,0 +1,17 @@ +package rearth.oracle.util; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public class Util { + public static Text getLanguageText(String code) { + var languageManager = MinecraftClient.getInstance().getLanguageManager(); + var languageDefinition = languageManager.getLanguage(code); + Text langText = Text.literal(code).formatted(Formatting.BOLD, Formatting.UNDERLINE); // fallback + if (languageDefinition != null) { + langText = languageDefinition.getDisplayText().copy().formatted(Formatting.BOLD); + } + return langText; + } +} diff --git a/common/src/main/resources/assets/oracle_index/lang/en_us.json b/common/src/main/resources/assets/oracle_index/lang/en_us.json index 766d7f2..8d70928 100644 --- a/common/src/main/resources/assets/oracle_index/lang/en_us.json +++ b/common/src/main/resources/assets/oracle_index/lang/en_us.json @@ -6,5 +6,6 @@ "key.categories.oracle": "Oracle Index", "oracle_index.searchbar.placeholder" : "Find or Calculate", "oracle_index.searchbar.tooltip" : "Smart search that supports full sentences and math expressions.", - "tooltip.oracle_index.open_search": "Open Search. You can always open this by pressing [CTRL + %s] or [%s]." + "tooltip.oracle_index.open_search": "Open Search. You can always open this by pressing [CTRL + %s] or [%s].", + "oracle_index.label.lang": "Lang:" } \ No newline at end of file diff --git a/docs/.translated/zh_cn/_meta.json b/docs/.translated/zh_cn/_meta.json new file mode 100644 index 0000000..04f650f --- /dev/null +++ b/docs/.translated/zh_cn/_meta.json @@ -0,0 +1,14 @@ +{ + "user_guide.mdx": { + "name": "\uD83D\uDCD7 User Guide", + "icon": null + }, + "search.mdx": { + "name": "\uD83D\uDD0D User Guide", + "icon": null + }, + "developer": { + "name": "⌨ Mod Dev Guide", + "icon": null + } +} \ No newline at end of file diff --git a/docs/.translated/zh_cn/search.mdx b/docs/.translated/zh_cn/search.mdx new file mode 100644 index 0000000..fb26087 --- /dev/null +++ b/docs/.translated/zh_cn/search.mdx @@ -0,0 +1,18 @@ +--- +title: 搜索 +--- + +Oracle Index 具备智能搜索功能。要打开搜索界面,可以按下 [CTRL + ORACLE_HOTKEY],或者使用默认未绑定的搜索快捷键。在查看维基条目时,界面上也会显示一个小的搜索图标。 + + +搜索功能还支持数学计算和表达式! + + +首次打开搜索界面时,可能需要几秒钟来对可用页面进行索引,之后才能进行搜索。这是一个功能强大的语义搜索,由机器学习驱动。这意味着你不需要用精确的关键词进行搜索,可以直接输入完整的句子,即使词语不完全匹配,系统也会展示相关的内容。任何相关主题都会被显示出来。 + +## Technical Details +Under the hood, Oracle Index generates embeddings for all pages, which are preprocessed into chunks. A small sentence transformer model (all-MiniLm-L6-V2-q) then generates embeddings +for each chunk, and for the search query. The embedding vectors are stored in memory. The search query embedding vector is then compared to the embeddings of the wiki chunks. +The closest N results are then displayed to the user. + +The library used for this is Langchain4j, which uses the DJL Framework. The embedding model is about 15mb in size. \ No newline at end of file diff --git a/docs/.translated/zh_cn/user_guide.mdx b/docs/.translated/zh_cn/user_guide.mdx new file mode 100644 index 0000000..8fe242b --- /dev/null +++ b/docs/.translated/zh_cn/user_guide.mdx @@ -0,0 +1,18 @@ +--- +title: User Guide +--- + +Oracle Index is an ingame clientside documentation mod, which is intended as an ingame view of content from [moddedmc.wiki](https://moddedmc.wiki/). + +Mod developers can choose to include their wikis in their mods, and then use the Oracle Index mod to display them ingame. Press the wiki keybinding (default [H]) to open the oracle index screen. +You don't need a special book item or anything. When exiting the screen, it'll remember where you left off. + +You can only view the documentation of a mod if the mod is installed and the mod developer chose to include their documentation from moddedmc.wiki as Oracle Index wiki as well. + +## Mod Switching +On the left side of the screen, you'll see the navigation bar for the currently selected mod. To switch to another mod, simply click on the mods title bar above the navigation bar. +A dropdown will open where you can select which mod to browse. + +## Item Links +Mod developers can add wiki links to ingame items. If a wiki page is available for an item, you'll see a small extra in the items tooltip. +Hold the [ALT] button to open the corresponding wiki entry when seeing the items tooltip. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 940fd74..db692a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,3 +18,9 @@ fabric_loader_version = 0.16.10 fabric_api_version = 0.115.0+1.21.1 neoforge_version = 21.1.84 yarn_mappings_patch_neoforge_version = 1.21+build.4 + +loom_libraries_base=https://bmclapi2.bangbang93.com/maven/ +loom_resources_base=https://bmclapi2.bangbang93.com/assets/ +loom_version_manifests=https://bmclapi2.bangbang93.com/mc/game/version_manifest.json +loom_experimental_versions=https://maven.fabricmc.net/net/minecraft/experimental_versions.json +loom_fabric_repository=https://repository.hanbings.io/proxy/