Use weak caches for custom textures

This commit is contained in:
Linnea Gräf
2024-09-24 11:40:15 +02:00
parent 64099bd262
commit 420f2a61e1
18 changed files with 308 additions and 78 deletions

View File

@@ -23,6 +23,8 @@ import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.ScreenUtil import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.collections.InstanceList
import moe.nea.firmament.util.collections.WeakCache
fun firmamentCommand() = literal("firmament") { fun firmamentCommand() = literal("firmament") {
@@ -229,6 +231,18 @@ fun firmamentCommand() = literal("firmament") {
} }
} }
} }
thenLiteral("caches") {
thenExecute {
source.sendFeedback(Text.literal("Caches:"))
WeakCache.allInstances.getAll().forEach {
source.sendFeedback(Text.literal(" - ${it.name}: ${it.size}"))
}
source.sendFeedback(Text.translatable("Instance lists:"))
InstanceList.allInstances.getAll().forEach {
source.sendFeedback(Text.literal(" - ${it.name}: ${it.size}"))
}
}
}
thenLiteral("mixins") { thenLiteral("mixins") {
thenExecute { thenExecute {
source.sendFeedback(Text.translatable("firmament.mixins.start")) source.sendFeedback(Text.translatable("firmament.mixins.start"))

View File

@@ -1,24 +1,25 @@
package moe.nea.firmament.events package moe.nea.firmament.events
import java.util.* import java.util.Optional
import kotlin.jvm.optionals.getOrNull
import net.minecraft.client.render.model.BakedModel import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.render.model.BakedModelManager import net.minecraft.client.render.model.BakedModelManager
import net.minecraft.client.util.ModelIdentifier import net.minecraft.client.util.ModelIdentifier
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import moe.nea.firmament.util.collections.WeakCache
data class CustomItemModelEvent( data class CustomItemModelEvent(
val itemStack: ItemStack, val itemStack: ItemStack,
var overrideModel: ModelIdentifier? = null, var overrideModel: ModelIdentifier? = null,
) : FirmamentEvent() { ) : FirmamentEvent() {
companion object : FirmamentEventBus<CustomItemModelEvent>() { companion object : FirmamentEventBus<CustomItemModelEvent>() {
private val cache = IdentityHashMap<ItemStack?, Any>() val cache =
private val sentinelNull = Object() WeakCache.memoize<ItemStack, BakedModelManager, Optional<BakedModel>>("CustomItemModels") { stack, models ->
val modelId = getModelIdentifier(stack) ?: return@memoize Optional.empty()
fun clearCache() { val bakedModel = models.getModel(modelId)
cache.clear() if (bakedModel === models.missingModel) return@memoize Optional.empty()
} Optional.of(bakedModel)
}
@JvmStatic @JvmStatic
fun getModelIdentifier(itemStack: ItemStack?): ModelIdentifier? { fun getModelIdentifier(itemStack: ItemStack?): ModelIdentifier? {
@@ -29,15 +30,7 @@ data class CustomItemModelEvent(
@JvmStatic @JvmStatic
fun getModel(itemStack: ItemStack?, thing: BakedModelManager): BakedModel? { fun getModel(itemStack: ItemStack?, thing: BakedModelManager): BakedModel? {
if (itemStack == null) return null if (itemStack == null) return null
val cachedValue = cache.getOrPut(itemStack) { return cache.invoke(itemStack, thing).getOrNull()
val modelId = getModelIdentifier(itemStack) ?: return@getOrPut sentinelNull
val bakedModel = thing.getModel(modelId)
if (bakedModel === thing.missingModel) return@getOrPut sentinelNull
bakedModel
}
if (cachedValue === sentinelNull)
return null
return cachedValue as BakedModel
} }
} }
} }

View File

@@ -1,10 +1,35 @@
package moe.nea.firmament.events package moe.nea.firmament.events
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import net.minecraft.resource.ReloadableResourceManagerImpl import net.minecraft.resource.ReloadableResourceManagerImpl
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.ResourceReloader
import net.minecraft.util.profiler.Profiler
data class FinalizeResourceManagerEvent( data class FinalizeResourceManagerEvent(
val resourceManager: ReloadableResourceManagerImpl, val resourceManager: ReloadableResourceManagerImpl,
) : FirmamentEvent() { ) : FirmamentEvent() {
companion object : FirmamentEventBus<FinalizeResourceManagerEvent>() companion object : FirmamentEventBus<FinalizeResourceManagerEvent>()
inline fun registerOnApply(name: String, crossinline function: () -> Unit) {
resourceManager.registerReloader(object : ResourceReloader {
override fun reload(
synchronizer: ResourceReloader.Synchronizer,
manager: ResourceManager?,
prepareProfiler: Profiler?,
applyProfiler: Profiler?,
prepareExecutor: Executor?,
applyExecutor: Executor
): CompletableFuture<Void> {
return CompletableFuture.completedFuture(Unit)
.thenCompose(synchronizer::whenPrepared)
.thenAcceptAsync({ function() }, applyExecutor)
}
override fun getName(): String {
return name
}
})
}
} }

View File

@@ -3,8 +3,15 @@ package moe.nea.firmament.features.debug
import net.minecraft.text.Text import net.minecraft.text.Text
import moe.nea.firmament.util.MC import moe.nea.firmament.util.MC
import moe.nea.firmament.util.collections.InstanceList
class DebugLogger(val tag: String) { class DebugLogger(val tag: String) {
companion object {
val allInstances = InstanceList<DebugLogger>("DebugLogger")
}
init {
allInstances.add(this)
}
fun isEnabled() = DeveloperFeatures.isEnabled // TODO: allow filtering by tag fun isEnabled() = DeveloperFeatures.isEnabled // TODO: allow filtering by tag
fun log(text: () -> String) { fun log(text: () -> String) {
if (!isEnabled()) return if (!isEnabled()) return

View File

@@ -14,7 +14,7 @@ import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.events.subscription.SubscriptionOwner import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.mutableMapWithMaxSize import moe.nea.firmament.util.collections.mutableMapWithMaxSize
import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld
object NearbyBurrowsSolver : SubscriptionOwner { object NearbyBurrowsSolver : SubscriptionOwner {

View File

@@ -14,9 +14,8 @@ import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC import moe.nea.firmament.util.MC
import moe.nea.firmament.util.item.loreAccordingToNbt import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.lastNotNullOfOrNull import moe.nea.firmament.util.collections.lastNotNullOfOrNull
import moe.nea.firmament.util.memoize import moe.nea.firmament.util.collections.memoizeIdentity
import moe.nea.firmament.util.memoizeIdentity
import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.unformattedString
object ItemRarityCosmetics : FirmamentFeature { object ItemRarityCosmetics : FirmamentFeature {

View File

@@ -17,7 +17,7 @@ import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.memoize import moe.nea.firmament.util.collections.memoize
@Serializable @Serializable
data class InventoryButton( data class InventoryButton(

View File

@@ -1,4 +1,3 @@
package moe.nea.firmament.features.texturepack package moe.nea.firmament.features.texturepack
import com.google.gson.JsonArray import com.google.gson.JsonArray

View File

@@ -1,12 +1,13 @@
@file:UseSerializers(IdentifierSerializer::class) @file:UseSerializers(IdentifierSerializer::class)
package moe.nea.firmament.features.texturepack package moe.nea.firmament.features.texturepack
import java.util.Optional
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlinx.serialization.UseSerializers import kotlinx.serialization.UseSerializers
import kotlin.jvm.optionals.getOrNull
import net.minecraft.item.ArmorMaterial import net.minecraft.item.ArmorMaterial
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.resource.ResourceManager import net.minecraft.resource.ResourceManager
@@ -20,8 +21,7 @@ import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
import moe.nea.firmament.util.IdentifierSerializer import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.IdentityCharacteristics import moe.nea.firmament.util.collections.WeakCache
import moe.nea.firmament.util.computeNullableFunction
import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyBlockId
object CustomGlobalArmorOverrides : SubscriptionOwner { object CustomGlobalArmorOverrides : SubscriptionOwner {
@@ -59,21 +59,21 @@ object CustomGlobalArmorOverrides : SubscriptionOwner {
override val delegateFeature: FirmamentFeature override val delegateFeature: FirmamentFeature
get() = CustomSkyBlockTextures get() = CustomSkyBlockTextures
val overrideCache = mutableMapOf<IdentityCharacteristics<ItemStack>, Any>() val overrideCache = WeakCache.memoize<ItemStack, Optional<List<ArmorMaterial.Layer>>>("ArmorOverrides") { stack ->
val id = stack.skyBlockId ?: return@memoize Optional.empty()
val override = overrides[id.neuItem] ?: return@memoize Optional.empty()
for (suboverride in override.overrides) {
if (suboverride.predicate.test(stack)) {
return@memoize Optional.of(suboverride.bakedLayers)
}
}
return@memoize Optional.of(override.bakedLayers)
}
@JvmStatic @JvmStatic
fun overrideArmor(stack: ItemStack): List<ArmorMaterial.Layer>? { fun overrideArmor(stack: ItemStack): List<ArmorMaterial.Layer>? {
if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null
return overrideCache.computeNullableFunction(IdentityCharacteristics(stack)) { return overrideCache.invoke(stack).getOrNull()
val id = stack.skyBlockId ?: return@computeNullableFunction null
val override = overrides[id.neuItem] ?: return@computeNullableFunction null
for (suboverride in override.overrides) {
if (suboverride.predicate.test(stack)) {
return@computeNullableFunction suboverride.bakedLayers
}
}
return@computeNullableFunction override.bakedLayers
}
} }
var overrides: Map<String, ArmorOverride> = mapOf() var overrides: Map<String, ArmorOverride> = mapOf()

View File

@@ -1,9 +1,9 @@
@file:UseSerializers(IdentifierSerializer::class, CustomModelOverrideParser.FirmamentRootPredicateSerializer::class) @file:UseSerializers(IdentifierSerializer::class, CustomModelOverrideParser.FirmamentRootPredicateSerializer::class)
package moe.nea.firmament.features.texturepack package moe.nea.firmament.features.texturepack
import java.util.Optional
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
@@ -28,9 +28,9 @@ import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.subscription.SubscriptionOwner import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.IdentifierSerializer import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.IdentityCharacteristics
import moe.nea.firmament.util.MC import moe.nea.firmament.util.MC
import moe.nea.firmament.util.computeNullableFunction import moe.nea.firmament.util.collections.WeakCache
import moe.nea.firmament.util.intoOptional
import moe.nea.firmament.util.json.SingletonSerializableList import moe.nea.firmament.util.json.SingletonSerializableList
import moe.nea.firmament.util.runNull import moe.nea.firmament.util.runNull
@@ -140,7 +140,17 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText
.filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) } .filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) }
} }
val overrideCache = mutableMapOf<IdentityCharacteristics<ItemStack>, Any>() val overrideCache = WeakCache.memoize<ItemStack, ItemModels, Optional<BakedModel>>("CustomGlobalTextureModelOverrides") { stack, models ->
matchingOverrides
.firstNotNullOfOrNull {
it.overrides
.asSequence()
.filter { it.predicate.test(stack) }
.map { models.modelManager.getModel(ModelIdentifier(it.model, "inventory")) }
.firstOrNull()
}
.intoOptional()
}
@JvmStatic @JvmStatic
fun replaceGlobalModel( fun replaceGlobalModel(
@@ -148,19 +158,8 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText
stack: ItemStack, stack: ItemStack,
cir: CallbackInfoReturnable<BakedModel> cir: CallbackInfoReturnable<BakedModel>
) { ) {
val value = overrideCache.computeNullableFunction(IdentityCharacteristics(stack)) { overrideCache.invoke(stack, models)
for (guiClassOverride in matchingOverrides) { .ifPresent(cir::setReturnValue)
for (override in guiClassOverride.overrides) {
if (override.predicate.test(stack)) {
return@computeNullableFunction models.modelManager.getModel(
ModelIdentifier(override.model, "inventory"))
}
}
}
null
}
if (value != null)
cir.returnValue = value
} }

View File

@@ -2,7 +2,9 @@ package moe.nea.firmament.features.texturepack
import com.mojang.authlib.minecraft.MinecraftProfileTexture import com.mojang.authlib.minecraft.MinecraftProfileTexture
import com.mojang.authlib.properties.Property import com.mojang.authlib.properties.Property
import java.util.Optional
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import kotlin.jvm.optionals.getOrNull
import net.minecraft.block.SkullBlock import net.minecraft.block.SkullBlock
import net.minecraft.client.MinecraftClient import net.minecraft.client.MinecraftClient
import net.minecraft.client.render.RenderLayer import net.minecraft.client.render.RenderLayer
@@ -12,10 +14,11 @@ import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.BakeExtraModelsEvent import moe.nea.firmament.events.BakeExtraModelsEvent
import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.CustomItemModelEvent
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.IdentityCharacteristics import moe.nea.firmament.util.collections.WeakCache
import moe.nea.firmament.util.item.decodeProfileTextureProperty import moe.nea.firmament.util.item.decodeProfileTextureProperty
import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyBlockId
@@ -26,7 +29,8 @@ object CustomSkyBlockTextures : FirmamentFeature {
object TConfig : ManagedConfig(identifier) { object TConfig : ManagedConfig(identifier) {
val enabled by toggle("enabled") { true } val enabled by toggle("enabled") { true }
val skullsEnabled by toggle("skulls-enabled") { true } val skullsEnabled by toggle("skulls-enabled") { true }
val cacheDuration by integer("cache-duration", 0, 20) { 1 } val cacheForever by toggle("cache-forever") { true }
val cacheDuration by integer("cache-duration", 0, 100) { 1 }
val enableModelOverrides by toggle("model-overrides") { true } val enableModelOverrides by toggle("model-overrides") { true }
val enableArmorOverrides by toggle("armor-overrides") { true } val enableArmorOverrides by toggle("armor-overrides") { true }
val enableBlockOverrides by toggle("block-overrides") { true } val enableBlockOverrides by toggle("block-overrides") { true }
@@ -36,14 +40,31 @@ object CustomSkyBlockTextures : FirmamentFeature {
override val config: ManagedConfig override val config: ManagedConfig
get() = TConfig get() = TConfig
val allItemCaches by lazy {
listOf(
CustomItemModelEvent.cache.cache,
skullTextureCache.cache,
CustomGlobalTextures.overrideCache.cache,
CustomGlobalArmorOverrides.overrideCache.cache
)
}
fun clearAllCaches() {
allItemCaches.forEach(WeakCache<*, *, *>::clear)
}
@Subscribe @Subscribe
fun onTick(it: TickEvent) { fun onTick(it: TickEvent) {
if (TConfig.cacheForever) return
if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) { if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) {
// TODO: unify all of those caches somehow clearAllCaches()
CustomItemModelEvent.clearCache() }
skullTextureCache.clear() }
CustomGlobalTextures.overrideCache.clear()
CustomGlobalArmorOverrides.overrideCache.clear() @Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
event.registerOnApply("Clear firmament CIT caches") {
clearAllCaches()
} }
} }
@@ -74,8 +95,14 @@ object CustomSkyBlockTextures : FirmamentFeature {
it.overrideModel = ModelIdentifier.ofInventoryVariant(Identifier.of("firmskyblock", id.identifier.path)) it.overrideModel = ModelIdentifier.ofInventoryVariant(Identifier.of("firmskyblock", id.identifier.path))
} }
private val skullTextureCache = mutableMapOf<IdentityCharacteristics<ProfileComponent>, Any>() private val skullTextureCache =
private val sentinelPresentInvalid = Object() WeakCache.memoize<ProfileComponent, Optional<Identifier>>("SkullTextureCache") { component ->
val id = getSkullTexture(component) ?: return@memoize Optional.empty()
if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) {
return@memoize Optional.empty()
}
return@memoize Optional.of(id)
}
private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex() private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex()
@@ -100,16 +127,8 @@ object CustomSkyBlockTextures : FirmamentFeature {
if (type != SkullBlock.Type.PLAYER) return if (type != SkullBlock.Type.PLAYER) return
if (!TConfig.skullsEnabled) return if (!TConfig.skullsEnabled) return
if (component == null) return if (component == null) return
val ic = IdentityCharacteristics(component)
val n = skullTextureCache.getOrPut(ic) { val n = skullTextureCache.invoke(component).getOrNull() ?: return
val id = getSkullTexture(component) ?: return@getOrPut sentinelPresentInvalid cir.returnValue = RenderLayer.getEntityTranslucent(n)
if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) {
return@getOrPut sentinelPresentInvalid
}
return@getOrPut id
}
if (n === sentinelPresentInvalid) return
cir.returnValue = RenderLayer.getEntityTranslucent(n as Identifier)
} }
} }

View File

@@ -204,7 +204,7 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
} }
override fun asFormattedText(entry: EntryStack<SBItemStack>, value: SBItemStack): Text { override fun asFormattedText(entry: EntryStack<SBItemStack>, value: SBItemStack): Text {
return VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asItemStack()) return VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asImmutableItemStack())
} }
override fun hash(entry: EntryStack<SBItemStack>, value: SBItemStack, context: ComparisonContext): Long { override fun hash(entry: EntryStack<SBItemStack>, value: SBItemStack, context: ComparisonContext): Long {

View File

@@ -0,0 +1,5 @@
package moe.nea.firmament.util
import java.util.Optional
fun <T : Any> T?.intoOptional(): Optional<T> = Optional.ofNullable(this)

View File

@@ -0,0 +1,57 @@
package moe.nea.firmament.util.collections
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
class InstanceList<T : Any>(val name: String) {
val queue = object : ReferenceQueue<T>() {}
val set = mutableSetOf<Ref>()
val size: Int
get() {
clearOldReferences()
return set.size
}
fun clearOldReferences() {
while (true) {
val reference = queue.poll() ?: break
set.remove(reference)
}
}
fun getAll(): List<T> {
clearOldReferences()
return set.mapNotNull { it.get() }
}
fun add(t: T) {
set.add(Ref(t))
}
init {
if (init)
allInstances.add(this)
}
inner class Ref(referent: T) : WeakReference<T>(referent) {
val hashCode = System.identityHashCode(referent)
override fun equals(other: Any?): Boolean {
return other is InstanceList<*>.Ref && hashCode == other.hashCode && get() === other.get()
}
override fun hashCode(): Int {
return hashCode
}
}
companion object {
private var init = false
val allInstances = InstanceList<InstanceList<*>>("InstanceLists")
init {
init = true
allInstances.add(allInstances)
}
}
}

View File

@@ -1,5 +1,7 @@
package moe.nea.firmament.util package moe.nea.firmament.util.collections
import moe.nea.firmament.util.IdentityCharacteristics
fun <K, V> mutableMapWithMaxSize(maxSize: Int): MutableMap<K, V> = object : LinkedHashMap<K, V>() { fun <K, V> mutableMapWithMaxSize(maxSize: Int): MutableMap<K, V> = object : LinkedHashMap<K, V>() {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>): Boolean { override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>): Boolean {

View File

@@ -0,0 +1,110 @@
package moe.nea.firmament.util.collections
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
import moe.nea.firmament.features.debug.DebugLogger
/**
* Cache class that uses [WeakReferences][WeakReference] to only cache values while there is still a life reference to
* the key. Each key can have additional extra data that is used to look up values. That extra data is not required to
* be a life reference. The main Key is compared using strict reference equality. This map is not synchronized.
*/
class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) {
private val queue = object : ReferenceQueue<Key>() {}
private val map = mutableMapOf<Ref, Value>()
val size: Int
get() {
clearOldReferences()
return map.size
}
fun clearOldReferences() {
var successCount = 0
var totalCount = 0
while (true) {
val reference = queue.poll() ?: break
totalCount++
if (map.remove(reference) != null)
successCount++
}
if (totalCount > 0)
logger.log { "Cleared $successCount/$totalCount references from queue" }
}
fun get(key: Key, extraData: ExtraKey): Value? {
clearOldReferences()
return map[Ref(key, extraData)]
}
fun put(key: Key, extraData: ExtraKey, value: Value) {
clearOldReferences()
map[Ref(key, extraData)] = value
}
fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value {
clearOldReferences()
return map.getOrPut(Ref(key, extraData)) { value(key, extraData) }
}
fun clear() {
map.clear()
}
init {
allInstances.add(this)
}
companion object {
val allInstances = InstanceList<WeakCache<*, *, *>>("WeakCaches")
private val logger = DebugLogger("WeakCache")
fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value):
CacheFunction.NoExtraData<Key, Value> {
return CacheFunction.NoExtraData(WeakCache(name), function)
}
fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value):
CacheFunction.WithExtraData<Key, ExtraKey, Value> {
return CacheFunction.WithExtraData(WeakCache(name), function)
}
}
inner class Ref(
weakInstance: Key,
val extraData: ExtraKey,
) : WeakReference<Key>(weakInstance, queue) {
val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode()
override fun equals(other: Any?): Boolean {
if (other !is WeakCache<*, *, *>.Ref) return false
return other.hashCode == this.hashCode
&& other.get() === this.get()
&& other.extraData == this.extraData
}
override fun hashCode(): Int {
return hashCode
}
}
interface CacheFunction {
val cache: WeakCache<*, *, *>
data class NoExtraData<Key : Any, Value : Any>(
override val cache: WeakCache<Key, Unit, Value>,
val wrapped: (Key) -> Value,
) : CacheFunction, (Key) -> Value {
override fun invoke(p1: Key): Value {
return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) })
}
}
data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>(
override val cache: WeakCache<Key, ExtraKey, Value>,
val wrapped: (Key, ExtraKey) -> Value,
) : CacheFunction, (Key, ExtraKey) -> Value {
override fun invoke(p1: Key, p2: ExtraKey): Value {
return cache.getOrPut(p1, p2, wrapped)
}
}
}
}

View File

@@ -1,5 +1,5 @@
package moe.nea.firmament.util package moe.nea.firmament.util.collections
fun <T, R> List<T>.lastNotNullOfOrNull(func: (T) -> R?): R? { fun <T, R> List<T>.lastNotNullOfOrNull(func: (T) -> R?): R? {
for (i in indices.reversed()) { for (i in indices.reversed()) {

View File

@@ -173,6 +173,7 @@
"firmament.config.custom-skyblock-textures.block-overrides": "Enable Block re-modelling", "firmament.config.custom-skyblock-textures.block-overrides": "Enable Block re-modelling",
"firmament.config.custom-skyblock-textures.enabled": "Enable Custom Item Textures", "firmament.config.custom-skyblock-textures.enabled": "Enable Custom Item Textures",
"firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures", "firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures",
"firmament.config.custom-skyblock-textures.cache-forever": "Disable cache clearing",
"firmament.config.fixes": "Fixes", "firmament.config.fixes": "Fixes",
"firmament.config.fixes.player-skins": "Fix unsigned Player Skins", "firmament.config.fixes.player-skins": "Fix unsigned Player Skins",
"firmament.config.power-user.show-item-id": "Show SkyBlock Ids", "firmament.config.power-user.show-item-id": "Show SkyBlock Ids",