diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/disable_unihex_font/UnihexProviderDefinitionMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/disable_unihex_font/UnihexProviderDefinitionMixin.java deleted file mode 100644 index 114577c1..00000000 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/disable_unihex_font/UnihexProviderDefinitionMixin.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.feature.disable_unihex_font; - -import com.mojang.blaze3d.font.GlyphProvider; -import com.mojang.datafixers.util.Either; -import net.minecraft.client.gui.font.CodepointMap; -import net.minecraft.client.gui.font.providers.GlyphProviderDefinition; -import net.minecraft.client.gui.font.providers.UnihexProvider; -import net.minecraft.server.packs.resources.ResourceManager; -import org.embeddedt.modernfix.ModernFix; -import org.embeddedt.modernfix.annotation.ClientOnlyMixin; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.io.IOException; -import java.lang.reflect.Constructor; - -@Mixin(UnihexProvider.Definition.class) -@ClientOnlyMixin -public class UnihexProviderDefinitionMixin { - @Inject(method = "unpack", at = @At("HEAD"), cancellable = true) - private void disableProvider(CallbackInfoReturnable> cir) { - cir.setReturnValue(Either.left(this::mfix$loadEmpty)); - } - - private GlyphProvider mfix$loadEmpty(ResourceManager resourceManager) throws IOException { - try { - ModernFix.LOGGER.warn("Unihex provider is disabled, a number of Unicode characters will likely not render"); - Constructor constructor = UnihexProvider.class.getDeclaredConstructor(CodepointMap.class); - constructor.setAccessible(true); - return constructor.newInstance(new CodepointMap<>(Object[]::new, Object[][]::new)); - } catch(ReflectiveOperationException e) { - throw new IOException("Failed to create empty loader", e); - } - } -} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/UnihexProviderByteContentsMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/UnihexProviderByteContentsMixin.java new file mode 100644 index 00000000..f358dad2 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/UnihexProviderByteContentsMixin.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.common.mixin.perf.compress_unihex_font; + +import com.llamalad7.mixinextras.sugar.Local; +import it.unimi.dsi.fastutil.bytes.ByteList; +import net.minecraft.client.gui.font.providers.UnihexProvider; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.render.font.CompactUnihexContents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(targets = {"net/minecraft/client/gui/font/providers/UnihexProvider$ByteContents"}) +@ClientOnlyMixin +public class UnihexProviderByteContentsMixin { + @Inject(method = "read", at = @At(value = "NEW", target = "([B)Lnet/minecraft/client/gui/font/providers/UnihexProvider$ByteContents;"), cancellable = true) + private static void useCompactIfPossible(int index, ByteList byteList, CallbackInfoReturnable cir, @Local(ordinal = 0) byte[] contents) { + if (contents.length == 16) { + cir.setReturnValue(new CompactUnihexContents.Bytes(contents)); + } + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/UnihexProviderShortContentsMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/UnihexProviderShortContentsMixin.java new file mode 100644 index 00000000..1f9abf47 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/UnihexProviderShortContentsMixin.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.common.mixin.perf.compress_unihex_font; + +import com.llamalad7.mixinextras.sugar.Local; +import it.unimi.dsi.fastutil.bytes.ByteList; +import net.minecraft.client.gui.font.providers.UnihexProvider; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.render.font.CompactUnihexContents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(targets = {"net/minecraft/client/gui/font/providers/UnihexProvider$ShortContents"}) +@ClientOnlyMixin +public class UnihexProviderShortContentsMixin { + @Inject(method = "read", at = @At(value = "NEW", target = "([S)Lnet/minecraft/client/gui/font/providers/UnihexProvider$ShortContents;"), cancellable = true) + private static void useCompactIfPossible(int index, ByteList byteList, CallbackInfoReturnable cir, @Local(ordinal = 0) short[] contents) { + if (contents.length == 16) { + cir.setReturnValue(new CompactUnihexContents.Shorts(contents)); + } + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_loop_spin_waiting/BlockableEventLoopMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_loop_spin_waiting/BlockableEventLoopMixin.java deleted file mode 100644 index 1f67f53a..00000000 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_loop_spin_waiting/BlockableEventLoopMixin.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.perf.fix_loop_spin_waiting; - -import net.minecraft.util.thread.BlockableEventLoop; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; - -// This should fix https://bugs.mojang.com/browse/MC-183518 -@Mixin(value = BlockableEventLoop.class, priority = 500) -public class BlockableEventLoopMixin { - private static final long MFIX$TICK_WAIT_TIME = TimeUnit.MILLISECONDS.toNanos(2); - - /** - * @author embeddedt - * @reason yielding the thread is pretty pointless if we're about to park anyway - */ - @Overwrite - public void waitForTasks() { - LockSupport.parkNanos("waiting for tasks", MFIX$TICK_WAIT_TIME); - } -} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_loop_spin_waiting/MinecraftServerMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_loop_spin_waiting/MinecraftServerMixin.java new file mode 100644 index 00000000..20030314 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_loop_spin_waiting/MinecraftServerMixin.java @@ -0,0 +1,48 @@ +package org.embeddedt.modernfix.common.mixin.perf.fix_loop_spin_waiting; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.Util; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.thread.BlockableEventLoop; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.concurrent.locks.LockSupport; +import java.util.function.BooleanSupplier; + +@Mixin(value = MinecraftServer.class, priority = 500) +public abstract class MinecraftServerMixin extends BlockableEventLoop { + @Shadow private long nextTickTimeNanos; + + protected MinecraftServerMixin(String name) { + super(name); + } + + @Unique + private boolean mfix$isWaitingForNextTick = false; + + @WrapOperation( + method = "waitUntilNextTick", + at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;managedBlock(Ljava/util/function/BooleanSupplier;)V") + ) + private void managedBlock(MinecraftServer instance, BooleanSupplier isDone, Operation original) { + try { + this.mfix$isWaitingForNextTick = true; + original.call(instance, isDone); + } finally { + this.mfix$isWaitingForNextTick = false; + } + } + + @Override + public void waitForTasks() { + if (this.mfix$isWaitingForNextTick) { + LockSupport.parkNanos("waiting for tasks", this.nextTickTimeNanos - Util.getNanos()); + } else { + super.waitForTasks(); + } + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/render/font/CompactUnihexContents.java b/common/src/main/java/org/embeddedt/modernfix/render/font/CompactUnihexContents.java new file mode 100644 index 00000000..2b2e199a --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/render/font/CompactUnihexContents.java @@ -0,0 +1,96 @@ +package org.embeddedt.modernfix.render.font; + +import net.minecraft.client.gui.font.providers.UnihexProvider; + +/** + * Implements more compact storage for LineData contents. + * + * Credit for the idea of using flattened fields rather than a backing array goes to @AnAwesomGuy. + */ +public class CompactUnihexContents { + private static long extract8Bytes(byte[] arr, int off) { + long l = 0; + for (int i = 0; i < 8; i++) { + l |= ((long)arr[off + i] << (i * 8)); + } + return l; + } + + private static byte extractByte(long compressed, int off) { + return (byte)((compressed >> (off * 8)) & 0xFF); + } + + private static long extract4Shorts(short[] arr, int off) { + long l = 0; + for (int i = 0; i < 4; i++) { + l |= ((long)arr[off + i] << (i * 16)); + } + return l; + } + + private static short extractShort(long compressed, int off) { + return (short)((compressed >> (off * 16)) & 0xFFFF); + } + + public static class Bytes implements UnihexProvider.LineData { + private final long b0; + private final long b8; + + public Bytes(byte[] contents) { + this.b0 = extract8Bytes(contents, 0); + this.b8 = extract8Bytes(contents, 8); + } + + @Override + public int line(int index) { + if (index < 0 || index >= 16) { + throw new ArrayIndexOutOfBoundsException(); + } + if (index < 8) { + return extractByte(b0, index) << 24; + } else { + return extractByte(b8, index - 8) << 24; + } + } + + @Override + public int bitWidth() { + return 8; + } + } + + public static class Shorts implements UnihexProvider.LineData { + private final long b0; + private final long b4; + private final long b8; + private final long b12; + + public Shorts(short[] contents) { + this.b0 = extract4Shorts(contents, 0); + this.b4 = extract4Shorts(contents, 4); + this.b8 = extract4Shorts(contents, 8); + this.b12 = extract4Shorts(contents, 12); + } + + @Override + public int line(int index) { + if (index < 0 || index >= 16) { + throw new ArrayIndexOutOfBoundsException(); + } + if (index < 4) { + return extractShort(b0, index) << 16; + } else if (index < 8) { + return extractShort(b4, index - 4) << 16; + } else if (index < 12) { + return extractShort(b8, index - 8) << 16; + } else { + return extractShort(b12, index - 12) << 16; + } + } + + @Override + public int bitWidth() { + return 16; + } + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/resources/PackResourcesCacheEngine.java b/common/src/main/java/org/embeddedt/modernfix/resources/PackResourcesCacheEngine.java index 479c8398..5919c0bf 100644 --- a/common/src/main/java/org/embeddedt/modernfix/resources/PackResourcesCacheEngine.java +++ b/common/src/main/java/org/embeddedt/modernfix/resources/PackResourcesCacheEngine.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Stream; @@ -24,6 +25,8 @@ import java.util.stream.Stream; */ public class PackResourcesCacheEngine { private static final Joiner SLASH_JOINER = Joiner.on('/'); + private static final ConcurrentHashMap PATH_COMPONENT_INTERNER = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap CACHED_SPLIT_PATHS = new ConcurrentHashMap<>(); static class Node { Map children; @@ -100,7 +103,6 @@ public class PackResourcesCacheEngine { // used for log message this.debugPath = basePathRetriever.apply(PackType.CLIENT_RESOURCES).toAbsolutePath(); this.root.children = new Object2ObjectOpenHashMap<>(); - ObjectOpenHashSet pathKeys = new ObjectOpenHashSet<>(); for(PackType type : PackType.values()) { var typeRoot = new Node(); this.root.children.put(type.getDirectory(), typeRoot); @@ -114,8 +116,12 @@ public class PackResourcesCacheEngine { .filter(PackResourcesCacheEngine::isValidCachedResourcePath) .forEach(path -> { var node = typeRoot; - for (Path component : path) { - String key = pathKeys.addOrGet(component.toString()); + int nameCount = path.getNameCount(); + for (int i = 0; i < nameCount; i++) { + String key = path.getName(i).toString(); + if (i < (nameCount - 1)) { + key = PATH_COMPONENT_INTERNER.computeIfAbsent(key, Function.identity()); + } if (node.children == null) { node.children = new Object2ObjectOpenHashMap<>(); } @@ -147,9 +153,17 @@ public class PackResourcesCacheEngine { public Set getNamespaces(PackType type) { awaitLoad(); - if(PackTypeHelper.isVanillaPackType(type)) - return this.root.getChild(type.getDirectory()).children.keySet(); - else + if(PackTypeHelper.isVanillaPackType(type)) { + var namespaceToNodeMap = this.root.getChild(type.getDirectory()).children; + var results = new ObjectOpenHashSet(); + for (var entry : namespaceToNodeMap.entrySet()) { + // Entries without children are files, not folders + if (!entry.getValue().children.isEmpty()) { + results.add(entry.getKey()); + } + } + return results; + } else return null; } @@ -205,4 +219,16 @@ public class PackResourcesCacheEngine { } node.collectResources(resourceNamespace, this.rootPathsByType.get(type).resolve(resourceNamespace), components, 0, maxDepth, output); } + + private static String[] decompose(String path) { + String[] components = path.split("/"); + for (int i = 0; i < components.length; i++) { + components[i] = PATH_COMPONENT_INTERNER.computeIfAbsent(components[i], Function.identity()); + } + return components; + } + + public static String[] decomposeCached(String path) { + return CACHED_SPLIT_PATHS.computeIfAbsent(path, PackResourcesCacheEngine::decompose); + } } diff --git a/common/src/main/resources/assets/modernfix/lang/en_us.json b/common/src/main/resources/assets/modernfix/lang/en_us.json index c7e317c4..918d98ed 100644 --- a/common/src/main/resources/assets/modernfix/lang/en_us.json +++ b/common/src/main/resources/assets/modernfix/lang/en_us.json @@ -117,7 +117,7 @@ "modernfix.option.mixin.perf.twilightforest.structure_spawn_fix": "Fixes lag caused by Twilight Forest worldgen checking structures very inefficiently", "modernfix.option.mixin.perf.fast_forge_dummies": "Speeds up Forge registry freezing during launch by using a faster code path", "modernfix.option.mixin.perf.tag_id_caching": "Speeds up uses of tag entries by caching the location object instead of recreating it every time", - "modernfix.option.mixin.feature.disable_unihex_font": "Remove the Unicode font, saves 10MB but causes special characters to no longer render", + "modernfix.option.mixin.perf.compress_unihex_font": "Stores the glyphs for the Unicode font more efficiently. Kudos to @AnAwesomGuy for the trick.", "modernfix.option.mixin.bugfix.world_leaks": "Reduces the memory usage of old client-side worlds that aren't needed after switching dimensions. These are normally garbage collected in vanilla, but mods sometimes retain references to them.", "modernfix.option.mixin.perf.compact_mojang_registries": "(Fabric) Experimental option that reduces the memory usage of registries by roughly 50%. Not useful in most modpacks unless they contain millions of blocks and items.", "modernfix.option.mixin.perf.dynamic_block_codecs": "Avoids storing a codec for every block(state) and instead generates and caches it on the fly when needed. Generally not worth enabling unless you have a million blocks/items.", diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/dynamic_resources/ForgeHooksClientMixin.java b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/dynamic_resources/ForgeHooksClientMixin.java index 198420e7..f66b34d0 100644 --- a/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/dynamic_resources/ForgeHooksClientMixin.java +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/dynamic_resources/ForgeHooksClientMixin.java @@ -1,18 +1,18 @@ -package org.embeddedt.modernfix.forge.mixin.perf.dynamic_resources; +package org.embeddedt.modernfix.neoforge.mixin.perf.dynamic_resources; import com.google.common.base.Stopwatch; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.client.resources.model.BakedModel; -import net.minecraft.resources.ResourceLocation; -import net.minecraftforge.client.ForgeHooksClient; -import net.minecraftforge.client.event.ModelEvent; -import net.minecraftforge.eventbus.api.Event; -import net.minecraftforge.fml.ModContainer; -import net.minecraftforge.fml.ModList; -import net.minecraftforge.fml.ModLoader; -import net.minecraftforge.fml.util.ObfuscationReflectionHelper; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.neoforged.bus.api.Event; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; +import net.neoforged.fml.ModLoader; +import net.neoforged.fml.util.ObfuscationReflectionHelper; +import net.neoforged.neoforge.client.ClientHooks; +import net.neoforged.neoforge.client.event.ModelEvent; import org.embeddedt.modernfix.ModernFix; -import org.embeddedt.modernfix.forge.dynresources.ModelBakeEventHelper; +import org.embeddedt.modernfix.neoforge.dynresources.ModelBakeEventHelper; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Redirect; @@ -23,24 +23,28 @@ import java.util.Comparator; import java.util.Map; import java.util.concurrent.TimeUnit; -@Mixin(ForgeHooksClient.class) +@Mixin(ClientHooks.class) public class ForgeHooksClientMixin { /** * Generate a more realistic keySet that contains every item and block model location, to help with mod compat. */ - @Redirect(method = "onModifyBakingResult", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/fml/ModLoader;postEvent(Lnet/minecraftforge/eventbus/api/Event;)V"), remap = false) - private static void postNamespacedKeySetEvent(ModLoader loader, Event event) { - if(!ModLoader.isLoadingStateValid()) + @Redirect(method = "onModifyBakingResult", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEvent(Lnet/neoforged/bus/api/Event;)V"), remap = false) + private static void postNamespacedKeySetEvent(Event event) { + if(ModLoader.hasErrors()) return; ModelEvent.ModifyBakingResult bakeEvent = ((ModelEvent.ModifyBakingResult)event); - ModelBakeEventHelper helper = new ModelBakeEventHelper(bakeEvent.getModels()); - Method acceptEv = ObfuscationReflectionHelper.findMethod(ModContainer.class, "acceptEvent", Event.class); Stopwatch globalTimer = Stopwatch.createStarted(); + Stopwatch selfTimer = Stopwatch.createStarted(); + ModelBakeEventHelper helper = new ModelBakeEventHelper(bakeEvent.getModels()); + selfTimer.stop(); + Method acceptEv = ObfuscationReflectionHelper.findMethod(ModContainer.class, "acceptEvent", Event.class); Map times = new Object2ObjectOpenHashMap<>(); + times.put("modernfix", selfTimer); ModList.get().forEachModContainer((id, mc) -> { - Map newRegistry = helper.wrapRegistry(id); - ModelEvent.ModifyBakingResult postedEvent = new ModelEvent.ModifyBakingResult(newRegistry, bakeEvent.getModelBakery()); - Stopwatch timer = times.computeIfAbsent(id, $ -> Stopwatch.createStarted()); + Map newRegistry = helper.wrapRegistry(id); + ModelEvent.ModifyBakingResult postedEvent = new ModelEvent.ModifyBakingResult(newRegistry, bakeEvent.getTextureGetter(), bakeEvent.getModelBakery()); + Stopwatch timer = times.computeIfAbsent(id, $ -> Stopwatch.createUnstarted()); + timer.start(); try { acceptEv.invoke(mc, postedEvent); } catch(ReflectiveOperationException e) { diff --git a/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/dynresources/ModelLocationBuilder.java b/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/dynresources/ModelLocationBuilder.java new file mode 100644 index 00000000..6dbf84d3 --- /dev/null +++ b/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/dynresources/ModelLocationBuilder.java @@ -0,0 +1,64 @@ +package org.embeddedt.modernfix.neoforge.dynresources; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.properties.Property; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +public class ModelLocationBuilder { + private final Map, PropertyData> propertyToOptionStrings = new Object2ObjectOpenHashMap<>(); + private final StringBuilder builder = new StringBuilder(); + + private record PropertyData(ImmutableList nameValuePairs, int maxPairLength) {} + + public void generateForBlock(Set destinationSet, Block block, ResourceLocation baseLocation) { + var props = block.getStateDefinition().getProperties(); + List> optionsList = new ArrayList<>(props.size()); + int requiredBuilderSize = Math.max(0, props.size() - 1); // commas + for (var prop : props) { + var data = propertyToOptionStrings.computeIfAbsent(prop, ModelLocationBuilder::computePropertyOptions); + optionsList.add(data.nameValuePairs); + requiredBuilderSize += data.maxPairLength; + } + var product = Lists.cartesianProduct(optionsList); + int count = product.size(); + int tupleEntryCount = optionsList.size(); + StringBuilder stringbuilder = this.builder; + stringbuilder.ensureCapacity(requiredBuilderSize); + for (int i = 0; i < count; i++) { + stringbuilder.setLength(0); + var result = product.get(i); + for (int j = 0; j < tupleEntryCount; j++) { + if (j != 0) { + stringbuilder.append(','); + } + stringbuilder.append(result.get(j)); + } + destinationSet.add(new ModelResourceLocation(baseLocation, stringbuilder.toString())); + } + } + + private static PropertyData computePropertyOptions(Property prop) { + ImmutableList.Builder valuesList = ImmutableList.builderWithExpectedSize(prop.getPossibleValues().size()); + int maxLength = 0; + for (var val : prop.getPossibleValues()) { + String pair = prop.getName() + "=" + getValueName(prop, val); + valuesList.add(pair.toLowerCase(Locale.ROOT)); + maxLength = Math.max(pair.length(), maxLength); + } + return new PropertyData(valuesList.build(), maxLength); + } + + private static > String getValueName(Property property, Comparable value) { + return property.getName((T)value); + } +}