From 1d62e2d9d57e18913d0caca4292147185892a48e Mon Sep 17 00:00:00 2001 From: Estecka Date: Thu, 5 Sep 2024 18:14:34 +0200 Subject: [PATCH] V2.3.0 (#10) - Multiple modules are allowed per item. - Added the module `enchantment` for tools and armours. - `custom_data` may now search nested pathes. - `custom_name` will now convert all names into valid identifiers. --- Changelog.md | 8 ++ README.md | 26 +++-- gradle.properties | 2 +- .../variantscit/IItemModelProvider.java | 26 +++++ .../estecka/variantscit/ModuleDefinition.java | 12 +- .../fr/estecka/variantscit/ModuleLoader.java | 107 ++++++++++++++---- .../estecka/variantscit/ModuleRegistry.java | 4 +- .../estecka/variantscit/VariantManager.java | 7 +- .../estecka/variantscit/VariantsCitMod.java | 39 +++++-- .../variantscit/api/IVariantManager.java | 6 + .../variantscit/mixin/ItemRendererMixin.java | 4 +- .../variantscit/modules/CustomDataModule.java | 52 +++++++-- .../variantscit/modules/CustomNameModule.java | 56 ++++++--- .../modules/EnchantedToolModule.java | 51 +++++++++ 14 files changed, 326 insertions(+), 74 deletions(-) create mode 100644 src/main/java/fr/estecka/variantscit/IItemModelProvider.java create mode 100644 src/main/java/fr/estecka/variantscit/modules/EnchantedToolModule.java diff --git a/Changelog.md b/Changelog.md index bb21ea0..042ed1c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,3 +24,11 @@ - Special modules no longer need to register a constructor. They may be registered the same way as Simple modules. - Special modules are no longer required to implement their variant logic separately from the special logic. - The old ways of registering special and parameterized modules has been marked as deprecated. +## 2.3 +- It's now possible to apply multiple modules to a single item. +- It's now possible to apply a single module to multiple items. +- Added the module `enchantment` for tools and armours. +- `stored_enchantments` (plural) is being renamed to `stored_enchantment` (singular). +- `custom_data` may now look for variants inside nested pathes. The parameter `nbtKey` is being renamed to `nbtPath`. +- `custom_name` will now always convert all names into valid identifiers. The case sensitivity option was removed. +- `custom_name`'s special names are now case-sensitive. diff --git a/README.md b/README.md index a66d04d..410b6c0 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,37 @@ A CIT logic for MC 1.21, optimized around items with standardized variants. **This mod is not a plug-and-play replacement for Optifine/CIT-resewn;** it uses its own resource format. Changes to older packs are required for them to work. -On low-end PCs, using this mod instead of the optifine format may lead to improved performances in scenarios where a single item has very many different variants. -The mod is not as all-purpose as optifine, it still requires specialized code for most item type, but this code is modular and easy to expand upon. Other mods can also add custom modules for their own items. +The mod contains built-in logic for handling **Axolotl Buckets**, **Enchantments**, **Music Discs**, **Goat Horns**, and **Potions**. +There are also more generic modules that can identify a variant from the `custom_data` or `custom_name` component of an item. Other mods can easily create custom logic for their own items. -Built-in modules support **Axolotl Buckets**, **Enchanted Books**, **Music Discs**, **Goat Horns**, and **Potions**. -There are also more generic modules that can identify a variant from the `custom_data` or `custom_name` component of an item. -If Mojang ever makes these items more componentized, you can expect Banner Patterns, Trim Templates, and Pottery Sherds to become supported in the future. +If Mojang ever makes these items data-driven, you can expect Banner Patterns, Trim Templates, and Pottery Sherds to become supported in the future. +## Difference with Optifine/CIT-Resewn +The base concept for this mod was born out of a need for _optimization_, at a time when CIT-resewn was still an up-to-date option. +This comes at the cost of some flexibility; while being _multi_-purpose, it may not be as _all_-purpose as optifine. + +This mod excels in scenarios where a single item type has a large amount of variants, which can all be derived from a single piece of data. Instead of defining a condition for every single CIT, you define a single rule that governs all CITs in a collection (so-called modules). ## Resource Pack Format -The format revolves around item variants (reduced to namespaced identifiers) being automatically associated to [item models](https://minecraft.wiki/w/Model#Item_models) with matching names, stored in a directory of your choosing. +This is an overview, see the [wiki](/~https://github.com/Estecka/mc-Variants-CIT/wiki) for a complete guide. + +The format revolves around item **variants** (reduced to namespaced identifiers) being automatically associated to [item models](https://minecraft.wiki/w/Model#Item_models) with matching names, stored in a directory of your choosing. -Resource packs must start by providing a configuration file, that defines what item type is affected, how to figure out its variant, and where the models are located. +Resource packs must start by providing a module configuration, that defines what item is affected, how to figure out its variant, and where their models are located. For example, here's a module that would reproduce the behaviour of the previous version of the mod, Enchants-CIT : `/assets/minecraft/variant-cits/item/enchanted_book.json` ```json { - "type": "stored_enchantments", + "type": "stored_enchantment", + "items": ["enchanted_book"], "modelPrefix": "item/enchanted_book/", "special": { "multi": "enchants-cit:item/multi_enchanted_book" } } ``` -The targeted item type is automatically derived from the file name of the config. Here, the enchantment `minecraft:unbreaking` will be associated with the model stored at `/assets/minecraft/models/item/enchanted_book/unbreaking.json` -Some module types may define additional models to use in special cases, or take addional parameters. -See the [wiki](/~https://github.com/Estecka/mc-Variants-CIT/wiki) for a more complete guide. \ No newline at end of file +Some module types may define additional models to use in special cases, or take addional parameters. diff --git a/gradle.properties b/gradle.properties index 595990b..4c19d56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,6 +12,6 @@ loader_version=0.16.2 fabric_version=0.102.1+1.21.1 # Mod Properties -mod_version=2.2.0 +mod_version=2.3.0 maven_group=fr.estecka.variantscit archives_base_name=variants-cit diff --git a/src/main/java/fr/estecka/variantscit/IItemModelProvider.java b/src/main/java/fr/estecka/variantscit/IItemModelProvider.java new file mode 100644 index 0000000..4ca0f03 --- /dev/null +++ b/src/main/java/fr/estecka/variantscit/IItemModelProvider.java @@ -0,0 +1,26 @@ +package fr.estecka.variantscit; + +import java.util.List; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.item.ItemStack; + +public interface IItemModelProvider +{ + ModelIdentifier GetModelForItem(ItemStack stack); + + static public IItemModelProvider OfList(List providers){ + if (providers.isEmpty()) + return __->null; + else if (providers.size() == 1) + return providers.get(0); + + return (ItemStack stack)->{ + ModelIdentifier id; + for (var p : providers) + if (null != (id=p.GetModelForItem(stack))) + return id; + + return null; + }; + } +} diff --git a/src/main/java/fr/estecka/variantscit/ModuleDefinition.java b/src/main/java/fr/estecka/variantscit/ModuleDefinition.java index 95f5e37..1f9a5c5 100644 --- a/src/main/java/fr/estecka/variantscit/ModuleDefinition.java +++ b/src/main/java/fr/estecka/variantscit/ModuleDefinition.java @@ -1,6 +1,7 @@ package fr.estecka.variantscit; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.jetbrains.annotations.Nullable; @@ -13,11 +14,20 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; -public record ModuleDefinition(Identifier type, String modelPrefix, Optional fallbackModel, Map specialModels) +public record ModuleDefinition( + Identifier type, + Optional> targets, + int priority, + String modelPrefix, + Optional fallbackModel, + Map specialModels +) { static public final MapCodec CODEC = RecordCodecBuilder.mapCodec(builder->builder .group( Identifier.CODEC.fieldOf("type").forGetter(ModuleDefinition::type), + Identifier.CODEC.listOf().optionalFieldOf("items").forGetter(ModuleDefinition::targets), + Codec.INT.fieldOf("priority").orElse(0).forGetter(ModuleDefinition::priority), Codec.STRING.validate(ModuleDefinition::ValidatePath).fieldOf("modelPrefix").forGetter(ModuleDefinition::modelPrefix), Identifier.CODEC.optionalFieldOf("fallback").forGetter(ModuleDefinition::fallbackModel), Codec.unboundedMap(Codec.STRING, Identifier.CODEC).fieldOf("special").orElse(ImmutableMap.of()).forGetter(ModuleDefinition::specialModels) diff --git a/src/main/java/fr/estecka/variantscit/ModuleLoader.java b/src/main/java/fr/estecka/variantscit/ModuleLoader.java index 42bae92..adc34ca 100644 --- a/src/main/java/fr/estecka/variantscit/ModuleLoader.java +++ b/src/main/java/fr/estecka/variantscit/ModuleLoader.java @@ -1,67 +1,130 @@ package fr.estecka.variantscit; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -import org.jetbrains.annotations.Nullable; import com.google.gson.JsonObject; +import com.ibm.icu.impl.Pair; import com.mojang.serialization.DataResult; import com.mojang.serialization.JsonOps; import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin.DataLoader; import net.minecraft.item.Item; import net.minecraft.registry.Registries; +import net.minecraft.registry.entry.RegistryEntry; import net.minecraft.resource.Resource; import net.minecraft.resource.ResourceManager; import net.minecraft.util.Identifier; import net.minecraft.util.JsonHelper; -public class ModuleLoader -implements DataLoader> +public final class ModuleLoader +implements DataLoader { + + static public class Result { + public final Map,List> modulesPerItem = new HashMap<>(); + } + + /** + * Contains all the data pertaining to a module file. Most of this data is + * only relevant to the resource-loading phase, and will be discarded + * afterward. + */ + static public class MetaModule { + public final Identifier id; + public final VariantManager manager; + public final ModuleDefinition definition; + public final JsonObject parameters; + + private MetaModule(Identifier id, Resource resource) + throws IllegalStateException + { + this.id = id; + var dataResult = DefinitionFromResource(resource); + if (dataResult.isError()){ + throw new IllegalStateException(dataResult.error().get().message()); + } + var pair = dataResult.getOrThrow(); + this.definition = pair.first; + this.parameters = pair.second; + + try { + this.manager = ModuleRegistry.CreateManager(this.definition, this.parameters); + } + catch (IllegalStateException e){ + throw new IllegalStateException(e); + } + } + } + @Override - public CompletableFuture> load(ResourceManager resourceManager, Executor executor){ + public CompletableFuture load(ResourceManager resourceManager, Executor executor){ return CompletableFuture.supplyAsync(()->ReloadModules(resourceManager), executor); } - private Map ReloadModules(ResourceManager manager){ - Map result = new HashMap<>(); + static private ModuleLoader.Result ReloadModules(ResourceManager manager) + { + ModuleLoader.Result result = new ModuleLoader.Result(); for (Map.Entry entry : manager.findResources("variant-cits/item", id->id.getPath().endsWith(".json")).entrySet()) { Identifier resourceId = entry.getKey(); - Item item = ItemFromResourcePath(resourceId); - if (item == null) + MetaModule module; + try { + module = new MetaModule(entry.getKey(), entry.getValue()); + } + catch (IllegalStateException e){ + VariantsCitMod.LOGGER.error("Error in cit module {}: {}", resourceId, e); continue; - - DataResult dataResult = ManagerFromResource(entry.getValue()); - if (dataResult.isSuccess()){ - VariantManager module = dataResult.getOrThrow(); - module.ReloadVariants(manager); - result.put(item, module); } - else { - VariantsCitMod.LOGGER.error("Error in cit module {}: {}", resourceId, dataResult.error().get().message()); + + module.manager.ReloadVariants(manager); + + Set> targets = module.definition.targets() + .map(ModuleLoader::ItemsFromTarget) + .orElseGet(()->ItemsFromResourcePath(resourceId)) + ; + + for (var item : targets){ + result.modulesPerItem.computeIfAbsent(item, __->new ArrayList<>()).add(module); } } + // Sort highest priorities first. + for (List modules : result.modulesPerItem.values()){ + modules.sort((a,b) -> -Integer.compare(a.definition.priority(), b.definition.priority())); + } + return result; + } + + static private Set> ItemsFromTarget(List targets){ + Set> result = new HashSet<>(); + targets.stream() + .map(id->Registries.ITEM.getEntry(id).get()) + .filter(o->o!=null) + .forEach(result::add) + ; return result; } - static private @Nullable Item ItemFromResourcePath(Identifier resourceId){ + static private Set> ItemsFromResourcePath(Identifier resourceId){ String path = resourceId.getPath(); path = path.substring("variant-cits/item".length() + 1, path.length() - ".json".length()); Identifier itemId = Identifier.of(resourceId.getNamespace(), path); if (Registries.ITEM.containsId(itemId)) - return Registries.ITEM.get(itemId); + return Set.of(Registries.ITEM.getEntry(itemId).get()); else - return null; + return Set.of(); } - static private DataResult ManagerFromResource(Resource resource){ + static private DataResult> DefinitionFromResource(Resource resource){ JsonObject json; try { json = JsonHelper.deserialize(resource.getReader()); @@ -81,9 +144,9 @@ static private DataResult ManagerFromResource(Resource resource) if (parameters == null) parameters = new JsonObject(); - return DataResult.success(ModuleRegistry.CreateManager(definition, parameters)); + return DataResult.success(Pair.of(definition, parameters)); } - catch (IllegalArgumentException|IllegalStateException|ClassCastException e){ + catch (IllegalStateException|ClassCastException e){ return DataResult.error(e::toString); } } diff --git a/src/main/java/fr/estecka/variantscit/ModuleRegistry.java b/src/main/java/fr/estecka/variantscit/ModuleRegistry.java index b97a23a..4f365e8 100644 --- a/src/main/java/fr/estecka/variantscit/ModuleRegistry.java +++ b/src/main/java/fr/estecka/variantscit/ModuleRegistry.java @@ -69,13 +69,13 @@ static private void RegisterManager(Identifier type, ManagerFactory factory){ } static public VariantManager CreateManager(ModuleDefinition definition, JsonObject customData) - throws IllegalArgumentException, IllegalStateException + throws IllegalStateException { assert definition != null; Identifier type = definition.type(); if (!MODULE_TYPES.containsKey(type)) - throw new IllegalArgumentException("Unknown module type: " + type.toString()); + throw new IllegalStateException("Unknown module type: " + type.toString()); return MODULE_TYPES.get(type).build(definition, customData); } diff --git a/src/main/java/fr/estecka/variantscit/VariantManager.java b/src/main/java/fr/estecka/variantscit/VariantManager.java index 1fc1aa4..89140e1 100644 --- a/src/main/java/fr/estecka/variantscit/VariantManager.java +++ b/src/main/java/fr/estecka/variantscit/VariantManager.java @@ -14,7 +14,7 @@ import net.minecraft.util.Identifier; public final class VariantManager -implements IVariantManager +implements IVariantManager, IItemModelProvider { private final ICitModule module; private final String prefix; @@ -40,6 +40,11 @@ public VariantManager(ModuleDefinition definition, ICitModule module){ return GetVariantModel(module.GetItemVariant(stack)); } + @Override + public boolean HasVariantModel(Identifier variant){ + return this.variantModels.containsKey(variant); + } + @Override public @Nullable ModelIdentifier GetVariantModel(Identifier variant){ if (variant == null) diff --git a/src/main/java/fr/estecka/variantscit/VariantsCitMod.java b/src/main/java/fr/estecka/variantscit/VariantsCitMod.java index 0a994ef..2d5bcdf 100644 --- a/src/main/java/fr/estecka/variantscit/VariantsCitMod.java +++ b/src/main/java/fr/estecka/variantscit/VariantsCitMod.java @@ -7,25 +7,27 @@ import net.minecraft.client.util.ModelIdentifier; import net.minecraft.item.Item; import net.minecraft.item.Items; -import net.minecraft.registry.Registries; import net.minecraft.util.Identifier; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import fr.estecka.variantscit.ModuleLoader.MetaModule; import fr.estecka.variantscit.modules.*; public class VariantsCitMod -implements ClientModInitializer, PreparableModelLoadingPlugin> +implements ClientModInitializer, PreparableModelLoadingPlugin { static public final String MODID = "variants-cit"; public static final Logger LOGGER = LoggerFactory.getLogger(MODID); - static private Map MODULES = new HashMap<>(); + static private Map MODULES = new HashMap<>(); - static public @Nullable VariantManager GetModule(Item itemType){ + static public @Nullable IItemModelProvider GetModule(Item itemType){ return MODULES.get(itemType); } @@ -36,11 +38,16 @@ public void onInitializeClient(){ ModuleRegistry.Register(Identifier.ofVanilla("axolotl_variant"), new AxolotlBucketModule()); ModuleRegistry.Register(Identifier.ofVanilla("custom_data"), CustomDataModule.CODEC); ModuleRegistry.Register(Identifier.ofVanilla("custom_name"), CustomNameModule.CODEC); + ModuleRegistry.Register(Identifier.ofVanilla("enchantment"), new EnchantedToolModule()); ModuleRegistry.Register(Identifier.ofVanilla("instrument"), new GoatHornModule()); ModuleRegistry.Register(Identifier.ofVanilla("jukebox_playable"), new MusicDiscModule()); ModuleRegistry.Register(Identifier.ofVanilla("potion_effect"), new PotionEffectModule()); ModuleRegistry.Register(Identifier.ofVanilla("potion_type"), new PotionTypeModule()); - ModuleRegistry.Register(Identifier.ofVanilla("stored_enchantments"), new EnchantedBookModule()); + ModuleRegistry.Register(Identifier.ofVanilla("stored_enchantment"), new EnchantedBookModule()); + ModuleRegistry.Register(Identifier.ofVanilla("stored_enchantments"), _0 -> { + LOGGER.warn("Module name `stored_enchantments` (plural) is being deprecated. use `stored_enchantment` (singular) instead."); + return new EnchantedBookModule(); + }); ModelPredicateProviderRegistry.register(Items.ENCHANTED_BOOK, Identifier.ofVanilla("level"), new EnchantedBookLevelPredicate()); var potionPredicate = new PotionLevelPredicate(); @@ -50,15 +57,23 @@ public void onInitializeClient(){ } @Override - public void onInitializeModelLoader(Map modules, ModelLoadingPlugin.Context pluginContext){ - MODULES = modules; - for (var entry : MODULES.entrySet()){ - VariantManager module = entry.getValue(); - Identifier itemId = Registries.ITEM.getId(entry.getKey()); + public void onInitializeModelLoader(ModuleLoader.Result result, ModelLoadingPlugin.Context pluginContext){ + Set uniqueModules = new HashSet<>(); - module.GetAllModels().stream().map(ModelIdentifier::id).forEach(pluginContext::addModels); - LOGGER.info("Loaded {} CITs for item {}", module.GetVariantCount(), itemId); + MODULES = new HashMap<>(); + for (var entry : result.modulesPerItem.entrySet()){ + uniqueModules.addAll(entry.getValue()); + MODULES.put( + entry.getKey().value(), + IItemModelProvider.OfList( entry.getValue().stream().map(meta->meta.manager).toList() ) + ); } + + for (MetaModule module : uniqueModules){ + module.manager.GetAllModels().stream().map(ModelIdentifier::id).forEach(pluginContext::addModels); + LOGGER.info("Found {} variants for CIT module {}", module.manager.GetVariantCount(), module.id); + } + } } diff --git a/src/main/java/fr/estecka/variantscit/api/IVariantManager.java b/src/main/java/fr/estecka/variantscit/api/IVariantManager.java index 6c8c5f8..e194892 100644 --- a/src/main/java/fr/estecka/variantscit/api/IVariantManager.java +++ b/src/main/java/fr/estecka/variantscit/api/IVariantManager.java @@ -14,6 +14,12 @@ public interface IVariantManager @Deprecated public abstract @Nullable ModelIdentifier GetModelVariantForItem(ItemStack stack); + /** + * @return Whether this variant has it's own model, ignoring the fallback + * model. + */ + public abstract boolean HasVariantModel(Identifier variantId); + /** * @return The model that matches this variant, the fallback model if no * model was provided for this variant, or null if the variant is null. diff --git a/src/main/java/fr/estecka/variantscit/mixin/ItemRendererMixin.java b/src/main/java/fr/estecka/variantscit/mixin/ItemRendererMixin.java index 1ba188f..3bdff92 100644 --- a/src/main/java/fr/estecka/variantscit/mixin/ItemRendererMixin.java +++ b/src/main/java/fr/estecka/variantscit/mixin/ItemRendererMixin.java @@ -12,7 +12,7 @@ import net.minecraft.client.render.model.BakedModelManager; import net.minecraft.client.util.ModelIdentifier; import net.minecraft.item.ItemStack; -import fr.estecka.variantscit.VariantManager; +import fr.estecka.variantscit.IItemModelProvider; import fr.estecka.variantscit.VariantsCitMod; @Mixin(ItemRenderer.class) @@ -22,7 +22,7 @@ public class ItemRendererMixin @WrapOperation( method="getModel", at=@At( value="INVOKE", target="net/minecraft/client/render/item/ItemModels.getModel (Lnet/minecraft/item/ItemStack;)Lnet/minecraft/client/render/model/BakedModel;") ) private BakedModel GetVariantModel(ItemModels models, ItemStack stack, Operation original) { - final VariantManager module = VariantsCitMod.GetModule(stack.getItem()); + final IItemModelProvider module = VariantsCitMod.GetModule(stack.getItem()); ModelIdentifier modelId; if (module == null || (modelId=module.GetModelForItem(stack)) == null) diff --git a/src/main/java/fr/estecka/variantscit/modules/CustomDataModule.java b/src/main/java/fr/estecka/variantscit/modules/CustomDataModule.java index d9c4aea..a6afe5a 100644 --- a/src/main/java/fr/estecka/variantscit/modules/CustomDataModule.java +++ b/src/main/java/fr/estecka/variantscit/modules/CustomDataModule.java @@ -1,14 +1,18 @@ package fr.estecka.variantscit.modules; +import java.util.Optional; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import fr.estecka.variantscit.VariantsCitMod; import fr.estecka.variantscit.api.ISimpleCitModule; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.NbtComponent; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtElement; +import net.minecraft.nbt.NbtString; import net.minecraft.util.Identifier; public class CustomDataModule @@ -16,32 +20,66 @@ public class CustomDataModule { static public final MapCodec CODEC = RecordCodecBuilder.mapCodec(builder->builder .group( - Codec.STRING.fieldOf("nbtKey").forGetter(s->s.key), + Codec.STRING.optionalFieldOf("nbtKey").deprecated(0).validate(_0 -> { + VariantsCitMod.LOGGER.warn("The custom_data parameter `nbtKey` is being deprecated. Use `nbtPath` instead."); + return DataResult.success(_0); + }).forGetter(s->Optional.empty()), + Codec.STRING.optionalFieldOf("nbtPath").forGetter(s->Optional.empty()), Codec.BOOL.fieldOf("caseSensitive").orElse(true).forGetter(s->s.caseSensitive) ) .apply(builder, CustomDataModule::new) ); - private final String key; + /** + * TODO: implement proper getter for the codec. + */ + private final String[] path; private final boolean caseSensitive; - public CustomDataModule(String key, boolean caseSensitive){ - this.key = key; + private CustomDataModule(Optional key, Optional path, boolean caseSensitive) + throws IllegalStateException + { this.caseSensitive = caseSensitive; + if (path.isPresent()) + this.path = ParsePath(path.get()); + else if (key.isPresent()) + this.path = new String[]{ key.get() }; + else + throw new IllegalStateException("Nbt path not set"); } @Override public Identifier GetItemVariant(ItemStack stack){ NbtComponent component = stack.get(DataComponentTypes.CUSTOM_DATA); - NbtCompound nbt; + NbtElement nbt; + if (component==null || (nbt=component.getNbt())==null) + return null; + + for (int i=0; i CODEC = RecordCodecBuilder.mapCodec(builder->builder .group( - Codec.BOOL.fieldOf("caseSensitive").orElse(false).forGetter(p->p.caseSensitive), + Codec.BOOL.fieldOf("debug").orElse(false).forGetter(p->p.debug), Codec.unboundedMap(Codec.STRING, Identifier.CODEC).fieldOf("specialNames").orElse(Map.of()).forGetter(p->p.specialNames) ) .apply(builder, CustomNameModule::new) ); - private final boolean caseSensitive; - private final Map specialNames = new HashMap<>(); + /* + * Using a Text (i.e, the item's component) instead of a string key means + * that the lifetime of the each entry is roughly equivalent to the lifetime + * of the associated item stack. + * Keys are evaluated by identity, not by content. Item components are + * supposed to be immutable, so the value of text should change. + */ + private final WeakHashMap cachedVariants = new WeakHashMap<>(); - public CustomNameModule(Boolean caseSensitive, Map specialNames){ - this.caseSensitive = caseSensitive; - if (caseSensitive) - this.specialNames.putAll(specialNames); - else for (var e : specialNames.entrySet()) - this.specialNames.put(e.getKey().toLowerCase(), e.getValue()); + private final boolean debug; + private final Map specialNames; + + public CustomNameModule(boolean debug, Map specialNames){ + this.debug = debug; + this.specialNames = specialNames; } @Override @@ -39,11 +48,28 @@ public Identifier GetItemVariant(ItemStack stack){ if (component == null) return null; - String name = component.getString(); - if (!caseSensitive) - name = name.toLowerCase(); + if (!cachedVariants.containsKey(component)) + cachedVariants.put(component, GetVariantFromText(component)); + return cachedVariants.get(component); + + } + + public Identifier GetVariantFromText(Text text){ + String name = text.getString(); + if (specialNames.containsKey(name)) + return specialNames.get(name); + + name = this.Transform(name); + if (debug) + VariantsCitMod.LOGGER.info("[custom_name CIT] #{} \"{}\" -> `{}`", cachedVariants.size(), text.getString(), name); + return Identifier.tryParse(name); + } - Identifier variant = specialNames.get(name); - return (variant != null) ? variant : Identifier.tryParse(name); + public String Transform(String name){ + return Normalizer.normalize(name, Normalizer.Form.NFD) + .replace(' ', '_') + .toLowerCase() + .replaceAll("[^a-zA-Z0-9_.-]", "") + ; } } diff --git a/src/main/java/fr/estecka/variantscit/modules/EnchantedToolModule.java b/src/main/java/fr/estecka/variantscit/modules/EnchantedToolModule.java new file mode 100644 index 0000000..88a7ded --- /dev/null +++ b/src/main/java/fr/estecka/variantscit/modules/EnchantedToolModule.java @@ -0,0 +1,51 @@ +package fr.estecka.variantscit.modules; + +import java.util.Iterator; +import fr.estecka.variantscit.api.ICitModule; +import fr.estecka.variantscit.api.IVariantManager; +import it.unimi.dsi.fastutil.objects.Object2IntMap.Entry; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.ItemEnchantmentsComponent; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.entry.RegistryEntry; + +public class EnchantedToolModule +implements ICitModule +{ + @Override + public ModelIdentifier GetItemModel(ItemStack stack, IVariantManager models){ + ItemEnchantmentsComponent enchants = stack.get(DataComponentTypes.ENCHANTMENTS); + if (enchants == null || enchants.isEmpty()) + return null; + + Iterator>> iterator = enchants.getEnchantmentEntries().iterator(); + var bestFit = iterator.next(); + while (iterator.hasNext()){ + var contestant = iterator.next(); + if (Compare(contestant, bestFit, models) > 0) + bestFit = contestant; + } + + return models.GetVariantModel(bestFit.getKey().getKey().get().getValue()); + } + + public int Compare(Entry> a, Entry> b, IVariantManager models){ + int result = 0; + + result = Boolean.compare( + models.HasVariantModel(a.getKey().getKey().get().getValue()), + models.HasVariantModel(b.getKey().getKey().get().getValue()) + ); + if (result != 0) return result; + + result = a.getKey().value().exclusiveSet().size() - b.getKey().value().exclusiveSet().size(); + if (result != 0) return result; + + result = a.getIntValue() - b.getIntValue(); + if (result != 0) return result; + + return result; + } +}