Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -200,6 +200,25 @@ public class TestConfig_Test extends Config {
)
public static String[] singleItem = {"minecraft:ender_pearl"};

@EffectList(
title = "Effect List",
description = "Search registered status effects and add several of them to an ordered list.",
category = "Lists",
subcategory = "Effect List",
maxEntries = 6
)
public static String[] effectList = {"minecraft:speed", "minecraft:night_vision"};

@EffectList(
title = "Single Effect",
description = "The same picker limited to one selected effect.",
category = "Lists",
subcategory = "Effect List",
maxEntries = 1,
reorderable = false
)
public static String[] singleEffect = {"minecraft:strength"};

@TestTilePicker_Test(
title = "Tile Picker",
description = "Search a set of colors and pick up to three of them.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package org.polyfrost.oneconfig.internal.ui.components.effect

import org.jetbrains.skia.Image
import org.polyfrost.oneconfig.api.event.v1.EventManager
import org.polyfrost.oneconfig.api.event.v1.events.ResourceFinishedLoading
import org.slf4j.LoggerFactory

//? if > 1.8.9 {
import net.minecraft.client.Minecraft
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.Identifier
import net.minecraft.world.effect.MobEffectCategory
//?} else {
/*import kotlin.math.roundToInt
import net.minecraft.client.resource.language.I18n
import net.minecraft.entity.living.effect.StatusEffect
import net.ornithemc.osl.core.api.util.NamespacedIdentifiers
import net.ornithemc.osl.resource.loader.api.resource.manager.ResourceManager
import org.jetbrains.skia.Bitmap
*///?}

class MinecraftEffectCatalogService : EffectCatalogService {
private val lock = Any()

@Volatile
private var catalogCache: List<EffectDescriptor>? = null
private val iconCache = HashMap<String, Image?>()

//? if = 1.8.9
//private var atlasLoaded = false

init {
EventManager.register(ResourceFinishedLoading::class.java, Runnable(::clearCaches))
}

private fun clearCaches() {
synchronized(lock) {
catalogCache = null
iconCache.clear()
//? if = 1.8.9
//atlasLoaded = false
}
EffectCatalog.invalidate()
}

override fun effects(): List<EffectDescriptor> {
catalogCache?.let { return it }
return synchronized(lock) {
catalogCache ?: buildCatalog().also { catalogCache = it }
}
}

private fun buildCatalog(): List<EffectDescriptor> {
//? if > 1.8.9 {
val registry = BuiltInRegistries.MOB_EFFECT
return registry.mapNotNull { effect ->
val id = registry.getKey(effect) ?: return@mapNotNull null
val category = when (effect.category) {
MobEffectCategory.BENEFICIAL -> EffectCategory.BENEFICIAL
MobEffectCategory.NEUTRAL -> EffectCategory.NEUTRAL
MobEffectCategory.HARMFUL -> EffectCategory.HARMFUL
}
EffectDescriptor(id.toString(), effect.displayName.string, category)
}
//?} else {
/*return StatusEffect.getKeys().mapNotNull { key ->
val effect = StatusEffect.get(key.toString()) ?: return@mapNotNull null
val category = if (effect.isHarmful) EffectCategory.HARMFUL else EffectCategory.BENEFICIAL
EffectDescriptor(key.toString(), I18n.translate(effect.translationKey), category)
}
*///?}
}

override fun icon(id: String): Image? = synchronized(lock) {
//? if > 1.8.9 {
if (iconCache.containsKey(id)) return iconCache[id]
loadIcon(id).also { iconCache[id] = it }
//?} else {
/*if (!atlasLoaded) {
atlasLoaded = true
try {
loadAtlas()
} catch (e: Exception) {
LOG.warn("Failed to load the status effect icons", e)
}
}
iconCache[id]
*///?}
}

//? if > 1.8.9 {
private fun loadIcon(id: String): Image? {
val namespace = id.substringBefore(':', "minecraft")
val path = id.substringAfter(':')
val texture = Identifier.tryBuild(namespace, "textures/mob_effect/$path.png") ?: return null
return try {
Minecraft.getInstance().resourceManager.getResource(texture).orElse(null)
?.open()?.use { Image.makeFromEncoded(it.readAllBytes()) }
} catch (e: Exception) {
LOG.warn("Failed to load the icon for status effect {}", id, e)
null
}
}
//?} else {
/*private fun loadAtlas() {
val texture = NamespacedIdentifiers.from("minecraft", "textures/gui/container/inventory.png")
val atlas = ResourceManager.client().getResource(texture).orElse(null)
?.open()?.use { Image.makeFromEncoded(it.readBytes()) }
if (atlas == null) {
LOG.warn("Failed to read the status effect icon atlas {}", texture)
return
}
atlas.use {
val scale = atlas.width / 256f
val size = (ICON_SIZE * scale).roundToInt()
Bitmap().use { bitmap ->
bitmap.allocN32Pixels(size, size)
for (key in StatusEffect.getKeys()) {
val effect = StatusEffect.get(key.toString()) ?: continue
if (!effect.hasIcon()) continue
val x = ((effect.iconIndex % 8 * ICON_SIZE) * scale).roundToInt()
val y = ((198 + effect.iconIndex / 8 * ICON_SIZE) * scale).roundToInt()
bitmap.erase(0)
if (atlas.readPixels(bitmap, x, y)) iconCache[key.toString()] = Image.makeFromBitmap(bitmap)
}
}
}
}
*///?}

private companion object {
private val LOG = LoggerFactory.getLogger("OneConfig/Effect Catalog")

//? if = 1.8.9
//private const val ICON_SIZE = 18
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.polyfrost.oneconfig.internal.ui.components.effect.MinecraftEffectCatalogService
5 changes: 5 additions & 0 deletions minecraft/src/main/resources/assets/oneconfig/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@
"oneconfig.picker.search": "Search by name or ID",
"oneconfig.picker.empty": "No matches",
"oneconfig.picker.unavailable": "Unavailable",
"oneconfig.effectlist.add": "Choose effects",
"oneconfig.effectlist.title": "Select effects",
"oneconfig.effectlist.search": "Search by name or ID",
"oneconfig.effectlist.empty": "No matching effects",
"oneconfig.effectlist.unavailable": "Unavailable effect",
"oneconfig.itemlist.add": "Choose items",
"oneconfig.itemlist.title": "Select items",
"oneconfig.itemlist.search": "Search by name or ID",
Expand Down
20 changes: 20 additions & 0 deletions modules/config-impl/api/config-impl.api
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ public class org/polyfrost/oneconfig/api/config/v1/KtConfig : org/polyfrost/onec
protected final synthetic fun dropdown ([Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/KtConfig$Provider;
public static synthetic fun dropdown$default (Lorg/polyfrost/oneconfig/api/config/v1/KtConfig;Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun dropdown$default (Lorg/polyfrost/oneconfig/api/config/v1/KtConfig;[Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/polyfrost/oneconfig/api/config/v1/KtConfig$Provider;
protected final synthetic fun effectList (Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZLjava/lang/String;Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/KtConfig$Provider;
public static synthetic fun effectList$default (Lorg/polyfrost/oneconfig/api/config/v1/KtConfig;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZLjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/polyfrost/oneconfig/api/config/v1/KtConfig$Provider;
protected final synthetic fun file (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/KtConfig$Provider;
public static synthetic fun file$default (Lorg/polyfrost/oneconfig/api/config/v1/KtConfig;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/polyfrost/oneconfig/api/config/v1/KtConfig$Provider;
protected final synthetic fun fileList (Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/KtConfig$Provider;
Expand Down Expand Up @@ -326,6 +328,12 @@ public final class org/polyfrost/oneconfig/api/config/v1/Visualizer$DropdownVisu
public fun visualize (Lorg/polyfrost/oneconfig/api/config/v1/Property;Landroidx/compose/runtime/Composer;I)V
}

public final class org/polyfrost/oneconfig/api/config/v1/Visualizer$EffectListVisualizer : org/polyfrost/oneconfig/api/config/v1/Visualizer {
public static final field $stable I
public fun <init> ()V
public fun visualize (Lorg/polyfrost/oneconfig/api/config/v1/Property;Landroidx/compose/runtime/Composer;I)V
}

public final class org/polyfrost/oneconfig/api/config/v1/Visualizer$FileListVisualizer : org/polyfrost/oneconfig/api/config/v1/Visualizer {
public static final field $stable I
public fun <init> ()V
Expand Down Expand Up @@ -574,6 +582,17 @@ public abstract interface annotation class org/polyfrost/oneconfig/api/config/v1
public abstract fun titleTranslation ()Z
}

public abstract interface annotation class org/polyfrost/oneconfig/api/config/v1/annotations/EffectList : java/lang/annotation/Annotation {
public abstract fun addText ()Ljava/lang/String;
public abstract fun category ()Ljava/lang/String;
public abstract fun description ()Ljava/lang/String;
public abstract fun icon ()Ljava/lang/String;
public abstract fun maxEntries ()I
public abstract fun reorderable ()Z
public abstract fun subcategory ()Ljava/lang/String;
public abstract fun title ()Ljava/lang/String;
}

public abstract interface annotation class org/polyfrost/oneconfig/api/config/v1/annotations/File : java/lang/annotation/Annotation {
public abstract fun category ()Ljava/lang/String;
public abstract fun categoryKey ()Ljava/lang/String;
Expand Down Expand Up @@ -974,6 +993,7 @@ public final class org/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL {
public final fun draggableList ([Ljava/lang/String;[Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL$DraggableListProp;
public final fun dropdown (I[Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL$OptionsProp;
public final synthetic fun dropdown (I[Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL$Prop;
public final fun effectList ([Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL$ListProp;
public final fun file (Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL$FileProp;
public static synthetic fun file$default (Lorg/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL;Ljava/lang/String;ILjava/lang/Object;)Lorg/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL$FileProp;
public final fun fileList ([Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL$FileListProp;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* This file is part of OneConfig.
* OneConfig - Next Generation Config Library for Minecraft: Java Edition
* Copyright (C) 2021~2024 Polyfrost.
* <https://polyfrost.org> <https://github.com/Polyfrost/>
*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* OneConfig is licensed under the terms of version 3 of the GNU Lesser
* General Public License as published by the Free Software Foundation, AND
* under the Additional Terms Applicable to OneConfig, as published by Polyfrost,
* either version 1.0 of the Additional Terms, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License. If not, see <https://www.gnu.org/licenses/>. You should
* have also received a copy of the Additional Terms Applicable
* to OneConfig, as published by Polyfrost. If not, see
* <https://polyfrost.org/legal/oneconfig/additional-terms>
*/

package org.polyfrost.oneconfig.api.config.v1.annotations;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.polyfrost.oneconfig.api.config.v1.Visualizer;

/**
* Renders a searchable selector for status effects (potion effects)
* <p>
* The annotated field must be a {@code String[]} or {@code List<String>} containing
* namespaced effect registry IDs for example {@code minecraft:speed}
* <p>
* Selected entries are ordered and can be removed and may be reordered
* <p>
* Use {@link #maxEntries()} with a value of {@code 1} for a single-effect selector
*/
@Option(display = Visualizer.EffectListVisualizer.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface EffectList {
String title();

String description() default "";

String icon() default "";

String category() default "General";

String subcategory() default "General";

/** Maximum number of selected effects and {@code 0} means unlimited */
int maxEntries() default 0;

/** Allow the user to drag selected effects into a different order */
boolean reorderable() default true;

/** Label of the button that opens the effect selector */
@TranslatedDefault("oneconfig.effectlist.add")
String addText() default "oneconfig.effectlist.add";
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ protected void handleMetadata(Tree tree, @NotNull Property<?> property, @NotNull
case ColorList color -> {
if (!color.alpha()) property.addMetadata("noAlpha", Unit.INSTANCE);
}
case ItemList itemList -> validateItemListField(field, itemList);
case ItemList itemList -> validateIdListField(field, "@ItemList", itemList.maxEntries());
case EffectList effectList -> validateIdListField(field, "@EffectList", effectList.maxEntries());
default -> {}
}

Expand Down Expand Up @@ -276,20 +277,20 @@ private int rangeFieldSize(Field field, Property<?> property, String annotationN
));
}

private void validateItemListField(Field field, ItemList annotation) {
private void validateIdListField(Field field, String annotation, int maxEntries) {
Class<?> type = field.getType();
boolean stringArray = type.isArray() && type.getComponentType() == String.class;
boolean stringList = List.class.isAssignableFrom(type) && hasStringListElements(field.getGenericType());
if (!stringArray && !stringList) {
throw new IllegalArgumentException(String.format(
"@ItemList field '%s' must be a String[] or List<String>, but was %s.",
field.getName(), field.getGenericType().getTypeName()
"%s field '%s' must be a String[] or List<String>, but was %s.",
annotation, field.getName(), field.getGenericType().getTypeName()
));
}
if (annotation.maxEntries() < 0) {
if (maxEntries < 0) {
throw new IllegalArgumentException(String.format(
"@ItemList field '%s' has a negative maxEntries value (%s).",
field.getName(), annotation.maxEntries()
"%s field '%s' has a negative maxEntries value (%s).",
annotation, field.getName(), maxEntries
));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,29 @@ open class KtConfig(id: String, title: String, category: Category, icon: String?
addListMetadata(maxEntries, reorderable, addText, addTextKey)
}

/** Namespaced status effect IDs such as `minecraft:speed`, a [maxEntries] of 1 makes a single selector */
@JvmSynthetic
protected fun effectList(
name: String,
def: Array<String> = arrayOf(),
nameKey: String? = null,
description: String? = null,
descriptionKey: String? = null,
icon: String? = null,
category: String? = "General",
categoryKey: String? = null,
subcategory: String? = "General",
subcategoryKey: String? = null,
maxEntries: Int = 0,
reorderable: Boolean = true,
addText: String? = null,
addTextKey: String? = "oneconfig.effectlist.add",
) = Provider(def, name, description, category, subcategory, Array<String>::class.java, Visualizer.EffectListVisualizer()) {
addMetadata(name, nameKey, description, descriptionKey, icon, category, categoryKey, subcategory, subcategoryKey)
require(maxEntries >= 0) { "effectList '$name' has a negative maxEntries ($maxEntries)" }
addListMetadata(maxEntries, reorderable, addText, addTextKey)
}

private fun Node.addListMetadata(maxEntries: Int, reorderable: Boolean, addText: String?, addTextKey: String?) {
this.addMetadata("maxEntries", maxEntries)
this.addMetadata("reorderable", reorderable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ fun interface Visualizer {
@Composable override fun visualize(prop: Property<*>) { Visualizer[ItemListVisualizer::class.java]?.visualize(prop) }
}

/**
* A searchable status effect selector backed by a `String[]` or `List<String>` of
* namespaced registry IDs
*
* A `maxEntries` metadata value of one makes it a single selector
*/
class EffectListVisualizer : Visualizer {
@Composable override fun visualize(prop: Property<*>) { Visualizer[EffectListVisualizer::class.java]?.visualize(prop) }
}

class FileVisualizer : Visualizer {
@Composable override fun visualize(prop: Property<*>) { Visualizer[FileVisualizer::class.java]?.visualize(prop) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ class ConfigDSL(id: String? = null, title: String? = null, description: String?
/** Namespaced item IDs such as `minecraft:diamond`, a `maxEntries` of 1 makes a single selector */
fun itemList(vararg default: String): ListProp<Array<String>> = put(ListProp(arrayOf(*default), Array<String>::class.java), Visualizer.ItemListVisualizer())

/** Namespaced status effect IDs such as `minecraft:speed`, a `maxEntries` of 1 makes a single selector */
fun effectList(vararg default: String): ListProp<Array<String>> = put(ListProp(arrayOf(*default), Array<String>::class.java), Visualizer.EffectListVisualizer())

/** Moves the saved value of [this] from any of [names] when loading older configs, needs [Prop.id] set first */
fun Prop<*>.previousNames(vararg names: String) {
val id = requireNotNull(id) { "previousNames needs the property id to be set first" }
Expand Down
Loading
Loading