From 8c1a705b9ee722d84ceff74adcfd1828cd22ae38 Mon Sep 17 00:00:00 2001 From: Sollace Date: Thu, 15 Dec 2022 00:35:30 +0000 Subject: [PATCH] More changes to texture suppliers --- .../client/pony/PonyManager.java | 1 + .../entity/npc/AbstractNpcRenderer.java | 10 +-- .../entity/npc/VillagerPonyRenderer.java | 10 ++- .../entity/npc/ZomponyVillagerRenderer.java | 12 ++- .../npc/textures/CustomPonyTextures.java | 86 ------------------ .../npc/textures/PlayerTextureSupplier.java | 59 +++++++++++++ .../entity/npc/textures/PonyTextures.java | 87 ------------------- .../textures/ProfessionTextureSupplier.java | 68 +++++++++++++++ .../textures/SillyPonyTextureSupplier.java | 36 ++++++++ .../npc/textures/SillyPonyTextures.java | 27 ------ .../entity/npc/textures/TextureSupplier.java | 34 +++++++- .../com/minelittlepony/util/FunctionUtil.java | 12 +++ 12 files changed, 227 insertions(+), 215 deletions(-) delete mode 100644 src/main/java/com/minelittlepony/client/render/entity/npc/textures/CustomPonyTextures.java create mode 100644 src/main/java/com/minelittlepony/client/render/entity/npc/textures/PlayerTextureSupplier.java delete mode 100644 src/main/java/com/minelittlepony/client/render/entity/npc/textures/PonyTextures.java create mode 100644 src/main/java/com/minelittlepony/client/render/entity/npc/textures/ProfessionTextureSupplier.java create mode 100644 src/main/java/com/minelittlepony/client/render/entity/npc/textures/SillyPonyTextureSupplier.java delete mode 100644 src/main/java/com/minelittlepony/client/render/entity/npc/textures/SillyPonyTextures.java create mode 100644 src/main/java/com/minelittlepony/util/FunctionUtil.java diff --git a/src/main/java/com/minelittlepony/client/pony/PonyManager.java b/src/main/java/com/minelittlepony/client/pony/PonyManager.java index dec53be7..e9d86b30 100644 --- a/src/main/java/com/minelittlepony/client/pony/PonyManager.java +++ b/src/main/java/com/minelittlepony/client/pony/PonyManager.java @@ -32,6 +32,7 @@ import java.util.concurrent.TimeUnit; public class PonyManager implements IPonyManager, SimpleSynchronousResourceReloadListener { private static final Identifier ID = new Identifier("minelittlepony", "background_ponies"); public static final Identifier BACKGROUND_PONIES = new Identifier("minelittlepony", "textures/entity/pony"); + public static final Identifier BACKGROUND_ZOMPONIES = new Identifier("minelittlepony", "textures/entity/zompony"); private final PonyConfig config; diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/AbstractNpcRenderer.java b/src/main/java/com/minelittlepony/client/render/entity/npc/AbstractNpcRenderer.java index b454f5b5..4d54c29d 100644 --- a/src/main/java/com/minelittlepony/client/render/entity/npc/AbstractNpcRenderer.java +++ b/src/main/java/com/minelittlepony/client/render/entity/npc/AbstractNpcRenderer.java @@ -28,10 +28,10 @@ abstract class AbstractNpcRenderer private final NpcClothingFeature, AbstractNpcRenderer> clothing; - public AbstractNpcRenderer(EntityRendererFactory.Context context, String type, TextureSupplier formatter) { + public AbstractNpcRenderer(EntityRendererFactory.Context context, String type, TextureSupplier textureSupplier, TextureSupplier formatter) { super(context, ModelType.getPlayerModel(Race.EARTH).getKey(false)); entityType = type; - baseTextures = new SillyPonyTextures<>(new CustomPonyTextures<>(new PonyTextures<>(formatter)), formatter); + baseTextures = new SillyPonyTextureSupplier<>(textureSupplier, formatter); clothing = new NpcClothingFeature<>(this, entityType); addFeature(clothing); } @@ -39,7 +39,7 @@ abstract class AbstractNpcRenderer @Override public boolean shouldRender(ClientPonyModel model, T entity, Wearable wearable, IGear gear) { - boolean special = PonyTextures.isBestPony(entity); + boolean special = SillyPonyTextureSupplier.isBestPony(entity); if (wearable == Wearable.SADDLE_BAGS_BOTH) { VillagerProfession profession = entity.getVillagerData().getProfession(); @@ -52,7 +52,7 @@ abstract class AbstractNpcRenderer } if (wearable == Wearable.MUFFIN) { - return PonyTextures.isCrownPony(entity); + return SillyPonyTextureSupplier.isCrownPony(entity); } return super.shouldRender(model, entity, wearable, gear); @@ -85,6 +85,6 @@ abstract class AbstractNpcRenderer @Override public Identifier getTexture(T villager) { - return baseTextures.supplyTexture(villager); + return baseTextures.apply(villager); } } diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/VillagerPonyRenderer.java b/src/main/java/com/minelittlepony/client/render/entity/npc/VillagerPonyRenderer.java index 3857e0af..ffbc1d21 100644 --- a/src/main/java/com/minelittlepony/client/render/entity/npc/VillagerPonyRenderer.java +++ b/src/main/java/com/minelittlepony/client/render/entity/npc/VillagerPonyRenderer.java @@ -6,8 +6,8 @@ import net.minecraft.entity.passive.VillagerEntity; import net.minecraft.util.math.MathHelper; import com.minelittlepony.client.model.ClientPonyModel; -import com.minelittlepony.client.render.entity.npc.textures.PonyTextures; -import com.minelittlepony.client.render.entity.npc.textures.TextureSupplier; +import com.minelittlepony.client.pony.PonyManager; +import com.minelittlepony.client.render.entity.npc.textures.*; public class VillagerPonyRenderer extends AbstractNpcRenderer { @@ -15,13 +15,15 @@ public class VillagerPonyRenderer extends AbstractNpcRenderer { private static final TextureSupplier FORMATTER = TextureSupplier.formatted("minelittlepony", "textures/entity/villager/%s.png"); public VillagerPonyRenderer(EntityRendererFactory.Context context) { - super(context, TYPE, FORMATTER); + super(context, TYPE, + TextureSupplier.ofPool(PonyManager.BACKGROUND_PONIES, + PlayerTextureSupplier.create(ProfessionTextureSupplier.create(FORMATTER))), FORMATTER); } @Override protected void initializeModel(ClientPonyModel model) { model.onSetModelAngles((m, move, swing, ticks, entity) -> { - m.getAttributes().visualHeight += PonyTextures.isCrownPony(entity) ? 0.3F : -0.1F; + m.getAttributes().visualHeight += SillyPonyTextureSupplier.isCrownPony(entity) ? 0.3F : -0.1F; boolean isHeadRolling = entity instanceof MerchantEntity && ((MerchantEntity)entity).getHeadRollingTimeLeft() > 0; diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/ZomponyVillagerRenderer.java b/src/main/java/com/minelittlepony/client/render/entity/npc/ZomponyVillagerRenderer.java index 7a5d6988..e8885ee0 100644 --- a/src/main/java/com/minelittlepony/client/render/entity/npc/ZomponyVillagerRenderer.java +++ b/src/main/java/com/minelittlepony/client/render/entity/npc/ZomponyVillagerRenderer.java @@ -7,8 +7,8 @@ import net.minecraft.entity.mob.ZombieVillagerEntity; import com.minelittlepony.client.model.ClientPonyModel; import com.minelittlepony.client.model.IMobModel; -import com.minelittlepony.client.render.entity.npc.textures.PonyTextures; -import com.minelittlepony.client.render.entity.npc.textures.TextureSupplier; +import com.minelittlepony.client.pony.PonyManager; +import com.minelittlepony.client.render.entity.npc.textures.*; public class ZomponyVillagerRenderer extends AbstractNpcRenderer { @@ -16,13 +16,17 @@ public class ZomponyVillagerRenderer extends AbstractNpcRenderer FORMATTER = TextureSupplier.formatted("minelittlepony", "textures/entity/zombie_villager/zombie_%s.png"); public ZomponyVillagerRenderer(EntityRendererFactory.Context context) { - super(context, TYPE, FORMATTER); + super(context, TYPE, + TextureSupplier.ofPool(PonyManager.BACKGROUND_ZOMPONIES, + TextureSupplier.ofPool(PonyManager.BACKGROUND_PONIES, + PlayerTextureSupplier.create(ProfessionTextureSupplier.create(FORMATTER)))), + FORMATTER); } @Override protected void initializeModel(ClientPonyModel model) { model.onSetModelAngles((m, move, swing, ticks, entity) -> { - m.getAttributes().visualHeight += PonyTextures.isCrownPony(entity) ? 0.3F : -0.1F; + m.getAttributes().visualHeight += SillyPonyTextureSupplier.isCrownPony(entity) ? 0.3F : -0.1F; if (m.rightArmPose == ArmPose.EMPTY) { IMobModel.rotateUndeadArms(m, move, ticks); diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/CustomPonyTextures.java b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/CustomPonyTextures.java deleted file mode 100644 index f096c928..00000000 --- a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/CustomPonyTextures.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.minelittlepony.client.render.entity.npc.textures; - -import net.minecraft.block.entity.SkullBlockEntity; -import net.minecraft.entity.LivingEntity; -import net.minecraft.util.Identifier; -import org.jetbrains.annotations.Nullable; - -import com.minelittlepony.api.config.PonyConfig; -import com.minelittlepony.api.config.PonyLevel; -import com.minelittlepony.api.pony.IPony; -import com.minelittlepony.client.MineLittlePony; -import com.minelittlepony.client.SkinsProxy; -import com.minelittlepony.client.pony.PonyManager; -import com.mojang.authlib.GameProfile; - -import java.util.*; - -public class CustomPonyTextures implements TextureSupplier { - - private final TextureSupplier fallback; - private final Map customNameCache = new HashMap<>(); - - public CustomPonyTextures(TextureSupplier fallback) { - this.fallback = fallback; - } - - @Override - public Identifier supplyTexture(T entity) { - Identifier override = getCustomTexture(entity); - if (override != null) { - return override; - } - return fallback.supplyTexture(entity); - } - - @Nullable - private Identifier getCustomTexture(T entity) { - if (!entity.hasCustomName()) { - return null; - } - - String key = entity.getCustomName().getString() + "_" + entity.getUuidAsString(); - - if (!customNameCache.containsKey(key)) { - customNameCache.put(key, new Entry(entity)); - } - return customNameCache.get(key).getTexture(); - } - - class Entry { - private final UUID uuid; - private final Identifier texture; - - @Nullable - private GameProfile profile; - - Entry(T entity) { - uuid = entity.getUuid(); - texture = MineLittlePony.getInstance().getVariatedTextures() - .get(PonyManager.BACKGROUND_PONIES) - .getByName(entity.getCustomName().getString(), uuid) - .orElse(null); - - if (texture == null) { - SkullBlockEntity.loadProperties(new GameProfile(null, entity.getCustomName().getString()), resolved -> { - profile = resolved; - }); - } - } - - public Identifier getTexture() { - if (profile != null) { - Identifier skin = SkinsProxy.instance.getSkinTexture(profile); - if (skin != null) { - if (IPony.getManager().getPony(skin).race().isHuman()) { - if (PonyConfig.getInstance().ponyLevel.get() == PonyLevel.PONIES) { - return IPony.getManager().getBackgroundPony(uuid).texture(); - } - } - return skin; - } - } - return texture; - } - } -} diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/PlayerTextureSupplier.java b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/PlayerTextureSupplier.java new file mode 100644 index 00000000..8f062d3e --- /dev/null +++ b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/PlayerTextureSupplier.java @@ -0,0 +1,59 @@ +package com.minelittlepony.client.render.entity.npc.textures; + +import net.minecraft.block.entity.SkullBlockEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +import com.minelittlepony.api.config.PonyConfig; +import com.minelittlepony.api.config.PonyLevel; +import com.minelittlepony.api.pony.IPony; +import com.minelittlepony.client.SkinsProxy; +import com.minelittlepony.util.FunctionUtil; +import com.mojang.authlib.GameProfile; + +import java.util.*; +import java.util.function.Function; + +public class PlayerTextureSupplier { + public static TextureSupplier create(TextureSupplier fallback) { + Function customNameCache = FunctionUtil.memoize(Entry::new, entity -> entity.getCustomName().getString() + "_" + entity.getUuidAsString()); + return entity -> { + Identifier override = entity.hasCustomName() ? customNameCache.apply(entity).getTexture() : null; + if (override != null) { + return override; + } + return fallback.apply(entity); + }; + } + + static final class Entry { + private final UUID uuid; + + @Nullable + private GameProfile profile; + + Entry(LivingEntity entity) { + uuid = entity.getUuid(); + SkullBlockEntity.loadProperties(new GameProfile(null, entity.getCustomName().getString()), resolved -> { + profile = resolved; + }); + } + + @Nullable + public Identifier getTexture() { + if (profile != null) { + Identifier skin = SkinsProxy.instance.getSkinTexture(profile); + if (skin != null) { + if (IPony.getManager().getPony(skin).race().isHuman()) { + if (PonyConfig.getInstance().ponyLevel.get() == PonyLevel.PONIES) { + return IPony.getManager().getBackgroundPony(uuid).texture(); + } + } + return skin; + } + } + return null; + } + } +} diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/PonyTextures.java b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/PonyTextures.java deleted file mode 100644 index adbf858c..00000000 --- a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/PonyTextures.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.minelittlepony.client.render.entity.npc.textures; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.entity.LivingEntity; -import net.minecraft.resource.ResourceManager; -import net.minecraft.util.Identifier; -import net.minecraft.village.VillagerData; -import net.minecraft.village.VillagerDataContainer; -import net.minecraft.village.VillagerProfession; -import net.minecraft.village.VillagerType; - -import com.minelittlepony.util.ResourceUtil; - -import java.util.*; - -/** - * Cached pool of villager textures. - */ -public class PonyTextures implements TextureSupplier { - - private final TextureSupplier formatter; - - private final Identifier fallback; - - private final Map cache = new HashMap<>(); - - private final ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager(); - - /** - * Creates a new profession cache - * - * @param formatter Formatter used when creating new textures - * @param keyMapper Mapper to convert integer ids into a string value for format insertion - * @param fallback The default if any generated textures fail to load. This is stored in place of failing textures. - */ - public PonyTextures(TextureSupplier formatter) { - this.formatter = formatter; - this.fallback = formatter.supplyTexture("villager_pony"); - } - - @Override - public Identifier supplyTexture(T entity) { - VillagerData t = entity.getVillagerData(); - - return getTexture(t.getType(), t.getProfession()); - } - - private Identifier getTexture(final VillagerType type, final VillagerProfession profession) { - - - String key = ResourceUtil.format("pony/%s/%s", type, profession); - - if (cache.containsKey(key)) { - return cache.get(key); // People often complain that villagers cause lag, - // so let's do better than Mojang and rather NOT go - // through all the lambda generations if we can avoid it. - } - - Identifier result = verifyTexture(formatter.supplyTexture(key)).orElseGet(() -> { - if (type == VillagerType.PLAINS) { - // if texture loading fails, use the fallback. - return fallback; - } - - return getTexture(VillagerType.PLAINS, profession); - }); - - cache.put(key, result); - return result; - } - - protected Optional verifyTexture(Identifier texture) { - return resourceManager.getResource(texture).map(i -> texture); - } - - public static boolean isBestPony(LivingEntity entity) { - if (!entity.hasCustomName()) { - return false; - } - String name = entity.getCustomName().getString(); - return "Derpy".equals(name) || (entity.isBaby() && "Dinky".equals(name)); - } - - public static boolean isCrownPony(LivingEntity entity) { - return isBestPony(entity) && entity.getUuid().getLeastSignificantBits() % 20 == 0; - } -} diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/ProfessionTextureSupplier.java b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/ProfessionTextureSupplier.java new file mode 100644 index 00000000..41a09bb3 --- /dev/null +++ b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/ProfessionTextureSupplier.java @@ -0,0 +1,68 @@ +package com.minelittlepony.client.render.entity.npc.textures; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.Identifier; +import net.minecraft.village.*; + +import com.minelittlepony.util.ResourceUtil; + +import java.util.*; + +public class ProfessionTextureSupplier implements TextureSupplier { + + public static TextureSupplier create(TextureSupplier formatter) { + return TextureSupplier.memoize(new ProfessionTextureSupplier<>(formatter), ProfessionTextureSupplier::getKey); + } + + private final TextureSupplier formatter; + + private final Identifier fallback; + + public ProfessionTextureSupplier(TextureSupplier formatter) { + this.formatter = formatter; + this.fallback = formatter.apply("villager_pony"); + } + + @Override + public Identifier apply(T container) { + return apply(container.getVillagerData()); + } + + public Identifier apply(VillagerData t) { + return getTexture(t.getType(), t.getProfession()); + } + + public static String getKey(VillagerDataContainer container) { + VillagerData t = container.getVillagerData(); + return ResourceUtil.format("pony/%s/%s", t.getType(), t.getProfession()); + } + + private Identifier getTexture(final VillagerType type, final VillagerProfession profession) { + String key = ResourceUtil.format("pony/%s/%s", type, profession); + return verifyTexture(formatter.apply(key)).orElseGet(() -> { + if (type == VillagerType.PLAINS) { + // if texture loading fails, use the fallback. + return fallback; + } + + return getTexture(VillagerType.PLAINS, profession); + }); + } + + protected Optional verifyTexture(Identifier texture) { + return MinecraftClient.getInstance().getResourceManager().getResource(texture).map(i -> texture); + } + + public static boolean isBestPony(LivingEntity entity) { + if (!entity.hasCustomName()) { + return false; + } + String name = entity.getCustomName().getString(); + return "Derpy".equals(name) || (entity.isBaby() && "Dinky".equals(name)); + } + + public static boolean isCrownPony(LivingEntity entity) { + return isBestPony(entity) && entity.getUuid().getLeastSignificantBits() % 20 == 0; + } +} diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/SillyPonyTextureSupplier.java b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/SillyPonyTextureSupplier.java new file mode 100644 index 00000000..1750fed8 --- /dev/null +++ b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/SillyPonyTextureSupplier.java @@ -0,0 +1,36 @@ +package com.minelittlepony.client.render.entity.npc.textures; + +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.Identifier; +import net.minecraft.village.VillagerDataContainer; + +public class SillyPonyTextureSupplier implements TextureSupplier { + + private final TextureSupplier fallback; + + private final Identifier egg; + private final Identifier egg2; + + public SillyPonyTextureSupplier(TextureSupplier fallback, TextureSupplier formatter) { + this.fallback = fallback; + this.egg = formatter.apply("silly_pony"); + this.egg2 = formatter.apply("tiny_silly_pony"); + } + + @Override + public Identifier apply(T entity) { + return isBestPony(entity) ? (entity.isBaby() ? egg2 : egg) : fallback.apply(entity); + } + + public static boolean isBestPony(LivingEntity entity) { + if (!entity.hasCustomName()) { + return false; + } + String name = entity.getCustomName().getString(); + return "Derpy".equals(name) || (entity.isBaby() && "Dinky".equals(name)); + } + + public static boolean isCrownPony(LivingEntity entity) { + return isBestPony(entity) && entity.getUuid().getLeastSignificantBits() % 20 == 0; + } +} diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/SillyPonyTextures.java b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/SillyPonyTextures.java deleted file mode 100644 index f348a04e..00000000 --- a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/SillyPonyTextures.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.minelittlepony.client.render.entity.npc.textures; - -import net.minecraft.entity.LivingEntity; -import net.minecraft.util.Identifier; -import net.minecraft.village.VillagerDataContainer; - -public class SillyPonyTextures implements TextureSupplier { - - private final TextureSupplier fallback; - - private final Identifier egg; - private final Identifier egg2; - - public SillyPonyTextures(TextureSupplier fallback, TextureSupplier formatter) { - this.fallback = fallback; - this.egg = formatter.supplyTexture("silly_pony"); - this.egg2 = formatter.supplyTexture("tiny_silly_pony"); - } - - @Override - public Identifier supplyTexture(T entity) { - if (PonyTextures.isBestPony(entity)) { - return entity.isBaby() ? egg2 : egg; - } - return fallback.supplyTexture(entity); - } -} diff --git a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/TextureSupplier.java b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/TextureSupplier.java index cc4b4548..f13c9db1 100644 --- a/src/main/java/com/minelittlepony/client/render/entity/npc/textures/TextureSupplier.java +++ b/src/main/java/com/minelittlepony/client/render/entity/npc/textures/TextureSupplier.java @@ -1,18 +1,48 @@ package com.minelittlepony.client.render.entity.npc.textures; +import net.minecraft.entity.LivingEntity; import net.minecraft.util.Identifier; +import com.minelittlepony.client.MineLittlePony; +import com.minelittlepony.util.FunctionUtil; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + /** * A texture pool for generating multiple associated textures. */ @FunctionalInterface -public interface TextureSupplier { +public interface TextureSupplier extends Function { /** * Supplies a new texture. May be generated for returned from a pool indexed by the given key. */ - Identifier supplyTexture(T key); + @Override + Identifier apply(T key); static TextureSupplier formatted(String domain, String path) { return key -> new Identifier(domain, String.format(path, key)); } + + static TextureSupplier ofPool(Identifier poolId, TextureSupplier fallback) { + final Function cache = FunctionUtil.memoize(entity -> { + return MineLittlePony.getInstance().getVariatedTextures() + .get(poolId) + .getByName(entity.getCustomName().getString(), entity.getUuid()) + .orElse(null); + }, entity -> entity.getCustomName().getString() + "_" + entity.getUuidAsString()); + return entity -> { + Identifier override = entity.hasCustomName() ? cache.apply(entity) : null; + if (override != null) { + return override; + } + return fallback.apply(entity); + }; + } + + static TextureSupplier memoize(Function func, Function keyFunc) { + final Map cache = new ConcurrentHashMap<>(); + return a -> cache.computeIfAbsent(keyFunc.apply(a), k -> func.apply(a)); + } } diff --git a/src/main/java/com/minelittlepony/util/FunctionUtil.java b/src/main/java/com/minelittlepony/util/FunctionUtil.java new file mode 100644 index 00000000..c933ffc4 --- /dev/null +++ b/src/main/java/com/minelittlepony/util/FunctionUtil.java @@ -0,0 +1,12 @@ +package com.minelittlepony.util; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +public interface FunctionUtil { + static Function memoize(Function func, Function keyFunc) { + final Map cache = new ConcurrentHashMap<>(); + return a -> cache.computeIfAbsent(keyFunc.apply(a), k -> func.apply(a)); + } +}