Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fe3b6c0
Fix: load modmenu menus once render thread becomes available
DeDiamondPro Aug 1, 2026
a9cfb92
Search providers for global search
DeDiamondPro Aug 2, 2026
9780f0e
Add description to modinfo
DeDiamondPro Aug 2, 2026
fb7e2bd
Run search asynchronously
DeDiamondPro Aug 2, 2026
616ee46
Centralized search system with search corpus
DeDiamondPro Aug 4, 2026
61139bf
Make compat ids stable
DeDiamondPro Aug 4, 2026
dff47d8
Merge remote-tracking branch 'upstream/v1' into feat/search-providers
DeDiamondPro Aug 4, 2026
44fb047
Scroll to top on search change
DeDiamondPro Aug 4, 2026
f83a34f
Merge remote-tracking branch 'upstream/v1' into feat/search-providers
DeDiamondPro Aug 5, 2026
507d46d
Add mod description to search corpus metadata
DeDiamondPro Aug 5, 2026
b7be562
Add HUDs to search corpus
DeDiamondPro Aug 6, 2026
3a459fe
Optimize search corpus
DeDiamondPro Aug 6, 2026
5b435a4
Add category to global search grouping
DeDiamondPro Aug 6, 2026
f9e2f61
Fix not always scrolling to top on search
DeDiamondPro Aug 6, 2026
cd33380
Fix: recover UI on composition failure
DeDiamondPro Aug 6, 2026
17654aa
Merge remote-tracking branch 'upstream/v1' into feat/search-providers
DeDiamondPro Aug 6, 2026
ae69d6a
Small fixes
DeDiamondPro Aug 6, 2026
5e5aeac
Merge remote-tracking branch 'upstream/v1' into feat/search-providers
DeDiamondPro Aug 6, 2026
03411b6
Add description and search tags to HUD
DeDiamondPro Aug 6, 2026
533af55
Add HUD mod card to search corpus
DeDiamondPro Aug 6, 2026
3ecb021
Fix small issues
DeDiamondPro Aug 7, 2026
2873f53
Merge remote-tracking branch 'upstream/v1' into feat/search-providers
DeDiamondPro Aug 7, 2026
9303dfc
Revert modcard concurrency change
DeDiamondPro Aug 7, 2026
c217ffa
Fix moulconfig compat ids not going trough unique id
DeDiamondPro Aug 7, 2026
aac9a24
fix scroll issues
Wyvest Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public Set<ModInfo> getMods() {
metadata.getContributors().stream()
.map(person -> person.getName())
.filter(name -> !name.isBlank())
.collect(Collectors.joining(", "))
.collect(Collectors.joining(", ")),
metadata.getDescription()
);
}).collect(Collectors.toSet());
//? }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.polyfrost.oneconfig.api.config.v1.ConfigManager;
import org.polyfrost.oneconfig.api.event.v1.EventManager;
import org.polyfrost.oneconfig.api.event.v1.events.InitializationEvent;
import org.polyfrost.oneconfig.api.event.v1.events.ResourceFinishedLoading;
import org.polyfrost.oneconfig.api.event.v1.events.ScreenOpenEvent;
import org.polyfrost.oneconfig.api.event.v1.events.WorldEvent;
import org.polyfrost.oneconfig.api.hud.v1.HudManager;
Expand All @@ -67,6 +68,7 @@
import org.polyfrost.oneconfig.internal.ui.keybind.KeybindProviderRegistry;
import org.polyfrost.oneconfig.internal.ui.keybind.MinecraftKeybindProvider;
import org.polyfrost.oneconfig.internal.ui.keybind.RightShiftConflicts;
import org.polyfrost.oneconfig.internal.ui.search.SearchCorpus;
import org.polyfrost.oneconfig.test.TestMod_Test;

/**
Expand Down Expand Up @@ -252,6 +254,8 @@ private static void registerEventHandlers() {
org.polyfrost.oneconfig.internal.ui.themes.ThemeRegistry.INSTANCE.loadFromConfig();
});
EventManager.register(WorldEvent.Load.class, e -> showFirstLaunchNotification());
// Initialize search corpus after loading is finished (and translation keys are available)
EventManager.register(ResourceFinishedLoading.class, e -> SearchCorpus.INSTANCE.init());
// //#if MC < 1.13
// // this is cringe but is better than the alternative of checking every frame in a mixin (that's how vanilla does it lol)
// AtomicBoolean active = new AtomicBoolean(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import org.polyfrost.oneconfig.api.config.v1.dsl.noCache
import org.polyfrost.oneconfig.api.config.v1.dsl.saveFunction
import org.polyfrost.oneconfig.api.config.v1.dsl.subcategory
import org.polyfrost.oneconfig.api.platform.v1.ModInfo
import org.polyfrost.oneconfig.internal.compat.CompatIds.componentKey
import org.polyfrost.oneconfig.internal.compat.CompatIds.idPart
import org.polyfrost.oneconfig.internal.compat.CompatIds.uniqueId
import java.lang.reflect.Field
import java.util.*
import java.util.function.Consumer
Expand Down Expand Up @@ -67,12 +70,15 @@ object ClothConfigCompat {
}

var added = false
val usedIds = HashSet<String>()
for (category in categoryMap.values) {
if (category == null) continue
val rawCategoryName = runCatching {
resolveComponent(category.javaClass.getMethod("getCategoryKey").invoke(category))
}.getOrNull()?.takeIf { it.isNotBlank() }
val categoryKey = runCatching {
category.javaClass.getMethod("getCategoryKey").invoke(category)
}.getOrNull()
val rawCategoryName = resolveComponent(categoryKey)?.takeIf { it.isNotBlank() }
val categoryName = cleanName(rawCategoryName, "General")
val categoryId = idPart(componentKey(categoryKey) ?: rawCategoryName, "general")

@Suppress("UNCHECKED_CAST")
val entries = runCatching {
Expand All @@ -82,30 +88,40 @@ object ClothConfigCompat {
for (entry in entries) {
if (entry == null) continue
runCatching {
if (parseEntry(entry, categoryName, categoryName, tree, tree)) added = true
if (parseEntry(entry, categoryName, categoryName, categoryId, tree, tree, usedIds)) added = true
}.onFailure { LOGGER.warn("Failed to parse Cloth entry", it) }
}
}

return if (added) tree else null
}

private fun parseEntry(entry: Any, categoryName: String, subcategoryName: String, dest: Tree, root: Tree): Boolean {
val name = runCatching {
resolveComponent(entry.javaClass.getMethod("getFieldName").invoke(entry))
}.getOrNull() ?: return false
private fun parseEntry(
entry: Any,
categoryName: String,
subcategoryName: String,
idPath: String,
dest: Tree,
root: Tree,
usedIds: MutableSet<String>,
): Boolean {
val nameComponent = runCatching { entry.javaClass.getMethod("getFieldName").invoke(entry) }.getOrNull()
val name = resolveComponent(nameComponent) ?: return false
val entryPath = "$idPath/${idPart(componentKey(nameComponent) ?: name, "entry")}"

val value = runCatching { invokeGetValue(entry) }.getOrNull()

if (isSubCategory(entry, value)) {
val children = value as? Collection<*> ?: return false
val rawSubName = runCatching {
resolveComponent(entry.javaClass.getMethod("getCategoryName").invoke(entry))
}.getOrNull()?.takeIf { it.isNotBlank() }
val subNameComponent = runCatching {
entry.javaClass.getMethod("getCategoryName").invoke(entry)
}.getOrNull()
val rawSubName = resolveComponent(subNameComponent)?.takeIf { it.isNotBlank() }
val subName = cleanName(rawSubName, name)
val subPath = componentKey(subNameComponent)?.let { "$idPath/${idPart(it, "entry")}" } ?: entryPath

val accordion = if (dest === root) {
Tree.tree(UUID.randomUUID().toString()).also {
Tree.tree(uniqueId(usedIds, subPath)).also {
it.title = subName
it.addMetadata("category", categoryName)
if (!isExpanded(entry)) it.addMetadata("collapsed", true)
Expand All @@ -118,7 +134,7 @@ object ClothConfigCompat {
for (child in children) {
if (child == null) continue
runCatching {
if (parseEntry(child, categoryName, subName, accordion, root)) added = true
if (parseEntry(child, categoryName, subName, subPath, accordion, root, usedIds)) added = true
}.onFailure { LOGGER.warn("Failed to parse Cloth sub-entry", it) }
}

Expand All @@ -129,7 +145,15 @@ object ClothConfigCompat {
val currentValue = value ?: return false

if (currentValue is List<*>) {
return parseListEntry(entry, currentValue, name, categoryName, subcategoryName, dest)
return parseListEntry(
entry,
currentValue,
name,
uniqueId(usedIds, entryPath),
categoryName,
subcategoryName,
dest,
)
}

val isColor = isColorEntry(entry)
Expand All @@ -156,7 +180,7 @@ object ClothConfigCompat {
@Suppress("UNCHECKED_CAST")
(saveCallbackField?.get(entry) as? Consumer<Any?>)?.accept(v)
},
id = UUID.randomUUID().toString(),
id = uniqueId(usedIds, entryPath),
name = name,
description = resolveTooltip(entry),
)
Expand Down Expand Up @@ -193,6 +217,7 @@ object ClothConfigCompat {
entry: Any,
currentValue: List<*>,
name: String,
id: String,
categoryName: String,
subcategoryName: String,
dest: Tree,
Expand All @@ -212,7 +237,7 @@ object ClothConfigCompat {
val values = v.mapTo(ArrayList()) { if (numeric) coerceNumber(it, element) else it?.toString() ?: "" }
(saveCallbackField?.get(entry) as? Consumer<Any?>)?.accept(values)
},
id = UUID.randomUUID().toString(),
id = id,
name = name,
description = resolveTooltip(entry),
type = java.util.List::class.java as Class<List<Any?>>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.polyfrost.oneconfig.internal.compat

/**
* Create stable ids for a mod
*/
internal object CompatIds {
private val notAllowedIdRegex = Regex("[^a-z0-9._-]+")

/**
* Take a string and turn it into something allowed in a node id
*/
fun idPart(raw: String?, fallback: String): String {
val cleaned = raw?.trim()?.lowercase()?.replace(notAllowedIdRegex, "_")?.trim('_')
return cleaned?.takeIf { it.isNotEmpty() } ?: fallback
}

/**
* Increment the id until it is unique, should be the same every launch because they are added in declaration order
*/
fun uniqueId(used: MutableSet<String>, base: String): String {
if (used.add(base)) return base
var i = 2
while (!used.add("${base}_$i")) i++
return "${base}_$i"
}

/**
* Try to get the translation key of a component, preferred over the displayed text so ids do not
* change with the active language
*/
fun componentKey(value: Any?): String? {
if (value == null || value is String) return null
val contents = runCatching { value.javaClass.getMethod("getContents").invoke(value) }.getOrNull()
if (contents != null) {
runCatching { contents.javaClass.getMethod("getKey").invoke(contents) as? String }
.getOrNull()?.takeIf { it.isNotBlank() }?.let { return it }
}
return runCatching { value.javaClass.getMethod("getKey").invoke(value) as? String }
.getOrNull()?.takeIf { it.isNotBlank() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ import org.polyfrost.oneconfig.api.config.v1.dsl.visualizer
import org.polyfrost.oneconfig.api.config.v1.internal.ConfigVisualizer
import org.polyfrost.oneconfig.api.platform.v1.ModInfo
import org.polyfrost.oneconfig.api.platform.v1.Platform
import java.util.UUID
import org.polyfrost.oneconfig.internal.compat.CompatIds.componentKey
import org.polyfrost.oneconfig.internal.compat.CompatIds.idPart
import org.polyfrost.oneconfig.internal.compat.CompatIds.uniqueId
import java.util.function.Function
import java.util.function.Supplier

Expand Down Expand Up @@ -68,40 +70,74 @@ object DandelionCompat {
tree.addMetadata("icon_path", it)
}

categories.forEach { parseCategory(it, tree) }
val usedIds = HashSet<String>()
categories.forEach { parseCategory(it, tree, usedIds) }

return tree
}

fun parseCategory(category: ConfigCategory, root: Tree) {
category.rootGroup()?.let { parseGroup(it, category, true, root) }
category.groups().forEach { parseGroup(it, category, false, root) }
fun parseCategory(category: ConfigCategory, root: Tree, usedIds: MutableSet<String>) {
val categoryPath = idPart(componentKey(category.name()) ?: category.name().string, "general")
category.rootGroup()?.let { parseGroup(it, category, true, root, categoryPath, usedIds) }
category.groups().forEach { parseGroup(it, category, false, root, categoryPath, usedIds) }
}

fun parseGroup(group: OptionGroup, category: ConfigCategory, isRootGroup: Boolean, root: Tree) {
fun parseGroup(
group: OptionGroup,
category: ConfigCategory,
isRootGroup: Boolean,
root: Tree,
categoryPath: String,
usedIds: MutableSet<String>,
) {
if (group is ListOption<*>) {
@Suppress("UNCHECKED_CAST")
parseOption(group as ListOption<Any>, root, category.name().string, ConfigVisualizer.DEFAULT_SUBCATEGORY)
parseOption(
group as ListOption<Any>,
root,
category.name().string,
ConfigVisualizer.DEFAULT_SUBCATEGORY,
categoryPath,
usedIds,
)
return
}

val groupPath = if (isRootGroup) {
categoryPath
} else {
"$categoryPath/${idPart(componentKey(group.name()) ?: group.name().string, "group")}"
}

group.options().forEach {
parseOption(
it,
root,
category.name().string,
if (isRootGroup) ConfigVisualizer.DEFAULT_SUBCATEGORY else group.name().string
if (isRootGroup) ConfigVisualizer.DEFAULT_SUBCATEGORY else group.name().string,
groupPath,
usedIds,
)
}
}

fun <T : Any> parseOption(option: Option<T>, root: Tree, category: String, subcategory: String) = runCatching {
fun <T : Any> parseOption(
option: Option<T>,
root: Tree,
category: String,
subcategory: String,
groupPath: String,
usedIds: MutableSet<String>,
) = runCatching {

val controller = runCatching { option.controller() }.getOrNull()
// Dandelion options usually carry their own id; fall back to the option's place in the config.
val optionId = option.id()?.toString()
?: uniqueId(usedIds, "$groupPath/${idPart(componentKey(option.name()) ?: option.name().string, "option")}")

when (option) {
is ButtonOption -> {
val property = Properties.dummy(id = option.id()?.toString() ?: UUID.randomUUID().toString())
val property = Properties.dummy(id = optionId)
property.title = option.name()
property.description = option.description()
property.visualizer = Visualizer.ButtonVisualizer::class.java
Expand All @@ -115,7 +151,7 @@ object DandelionCompat {
}

is LabelOption -> {
val property = Properties.dummy(id = option.id()?.toString() ?: UUID.randomUUID().toString())
val property = Properties.dummy(id = optionId)
property.title = option.name()
property.description = option.description()
property.category = category
Expand All @@ -125,17 +161,17 @@ object DandelionCompat {
}

is ListOption<*> -> {
val property = listProperty(option, category, subcategory)
val property = listProperty(option, optionId, category, subcategory)
if (property == null) {
LOGGER.warn("Unsupported list: ${option.name()} - ${option.entryType().simpleName}")
root.put(unsupportedOptionProperty(option, option.entryController(), category, subcategory))
root.put(unsupportedOptionProperty(option, optionId, option.entryController(), category, subcategory))
} else {
root.put(property)
}
}

else if controller == null -> {
val property = Properties.dummy(id = option.id()?.toString() ?: UUID.randomUUID().toString())
val property = Properties.dummy(id = optionId)
property.title = option.name()
property.description =
Component.literal("Failed to create compat entry for option! ").append(option.name())
Expand Down Expand Up @@ -163,7 +199,7 @@ object DandelionCompat {
val property = Properties.functional(
getter = { getter() },
setter = { value -> setter(value) },
id = UUID.randomUUID().toString(),
id = optionId,
name = option.name(),
description = option.description(),
)
Expand Down Expand Up @@ -196,7 +232,7 @@ object DandelionCompat {
is StringController -> property.visualizer = Visualizer.TextVisualizer::class.java
else -> {
LOGGER.warn("Unsupported: ${option.name()} - ${controller.javaClass.simpleName}")
root.put(unsupportedOptionProperty(option, controller, category, subcategory))
root.put(unsupportedOptionProperty(option, optionId, controller, category, subcategory))
return@runCatching
}
}
Expand All @@ -206,7 +242,7 @@ object DandelionCompat {
}

@Suppress("UNCHECKED_CAST")
private fun listProperty(listOption: ListOption<*>, category: String, subcategory: String): Property<*>? {
private fun listProperty(listOption: ListOption<*>, id: String, category: String, subcategory: String): Property<*>? {
val option = listOption as ListOption<Any>
val entryType = option.entryType()
val entryController = runCatching { option.entryController() }.getOrNull()
Expand Down Expand Up @@ -243,7 +279,7 @@ object DandelionCompat {
option.listeners().forEach { it.onUpdate(option, OptionListener.UpdateType.VALUE_CHANGE) }
option.flags().forEach { it.accept(Minecraft.getInstance()) }
},
id = option.id()?.toString() ?: UUID.randomUUID().toString(),
id = id,
name = option.name(),
description = option.description(),
type = java.util.List::class.java as Class<List<Any?>>,
Expand Down Expand Up @@ -277,11 +313,12 @@ object DandelionCompat {

private fun <T : Any> unsupportedOptionProperty(
option: Option<T>,
id: String,
controller: Any,
category: String,
subcategory: String
): Property<Void> {
val property = Properties.dummy(id = option.id()?.toString() ?: UUID.randomUUID().toString())
val property = Properties.dummy(id = id)
property.title = option.name()
property.description =
Component.literal("Option currently not supported by OneConfig")
Expand Down
Loading
Loading