mirror of
https://github.com/Auxilor/EcoEnchants.git
synced 2024-11-21 14:55:17 +01:00
Enchantments will now work when used by entities
This commit is contained in:
parent
fd064e31c8
commit
9a41250b35
@ -32,15 +32,15 @@ import com.willfp.ecoenchants.mechanics.ExtraItemSupport
|
||||
import com.willfp.ecoenchants.mechanics.GrindstoneSupport
|
||||
import com.willfp.ecoenchants.mechanics.LootSupport
|
||||
import com.willfp.ecoenchants.mechanics.VillagerSupport
|
||||
import com.willfp.ecoenchants.target.EnchantLookup.clearEnchantCache
|
||||
import com.willfp.ecoenchants.target.EnchantLookup.heldEnchantLevels
|
||||
import com.willfp.ecoenchants.target.EnchantFinder
|
||||
import com.willfp.ecoenchants.target.EnchantFinder.clearEnchantmentCache
|
||||
import com.willfp.libreforge.NamedValue
|
||||
import com.willfp.libreforge.loader.LibreforgePlugin
|
||||
import com.willfp.libreforge.loader.configs.ConfigCategory
|
||||
import com.willfp.libreforge.registerHolderPlaceholderProvider
|
||||
import com.willfp.libreforge.registerSpecificHolderProvider
|
||||
import com.willfp.libreforge.registerHolderProvider
|
||||
import com.willfp.libreforge.registerSpecificRefreshFunction
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.entity.LivingEntity
|
||||
import org.bukkit.event.Listener
|
||||
|
||||
internal lateinit var plugin: EcoEnchantsPlugin
|
||||
@ -75,12 +75,10 @@ class EcoEnchantsPlugin : LibreforgePlugin() {
|
||||
}
|
||||
|
||||
override fun handleEnable() {
|
||||
registerSpecificHolderProvider<Player> {
|
||||
it.heldEnchantLevels
|
||||
}
|
||||
registerHolderProvider(EnchantFinder.toHolderProvider())
|
||||
|
||||
registerSpecificRefreshFunction<Player> {
|
||||
it.clearEnchantCache()
|
||||
registerSpecificRefreshFunction<LivingEntity> {
|
||||
it.clearEnchantmentCache()
|
||||
}
|
||||
|
||||
registerHolderPlaceholderProvider<FoundEcoEnchantLevel> { it, _ ->
|
||||
|
@ -3,8 +3,8 @@ package com.willfp.ecoenchants.enchant.impl.hardcoded
|
||||
import com.willfp.eco.util.DurabilityUtils
|
||||
import com.willfp.ecoenchants.EcoEnchantsPlugin
|
||||
import com.willfp.ecoenchants.enchant.impl.HardcodedEcoEnchant
|
||||
import com.willfp.ecoenchants.target.EnchantLookup.getActiveEnchantLevelInSlot
|
||||
import com.willfp.ecoenchants.target.EnchantLookup.hasEnchantActive
|
||||
import com.willfp.ecoenchants.target.EnchantFinder.getItemsWithEnchantActive
|
||||
import com.willfp.ecoenchants.target.EnchantFinder.hasEnchantActive
|
||||
import com.willfp.libreforge.slot.impl.SlotTypeHands
|
||||
import org.bukkit.Bukkit
|
||||
|
||||
@ -29,18 +29,10 @@ class EnchantmentRepairing(
|
||||
if (player.hasEnchantActive(this)) {
|
||||
val repairPerLevel = config.getIntFromExpression("repair-per-level", player)
|
||||
|
||||
for ((slot, item) in player.inventory.withIndex()) {
|
||||
if (item == null) {
|
||||
continue
|
||||
}
|
||||
for ((item, level) in player.getItemsWithEnchantActive(this)) {
|
||||
val isHolding = item in SlotTypeHands.getItems(player)
|
||||
|
||||
if (notWhileHolding && slot in SlotTypeHands.getItemSlots(player)) {
|
||||
continue
|
||||
}
|
||||
|
||||
val level = player.getActiveEnchantLevelInSlot(this, slot)
|
||||
|
||||
if (level == 0) {
|
||||
if (notWhileHolding && isHolding) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import com.willfp.eco.core.EcoPlugin
|
||||
import com.willfp.ecoenchants.EcoEnchantsPlugin
|
||||
import com.willfp.ecoenchants.enchant.EcoEnchant
|
||||
import com.willfp.ecoenchants.enchant.impl.HardcodedEcoEnchant
|
||||
import com.willfp.ecoenchants.target.EnchantLookup.hasEnchantActive
|
||||
import com.willfp.ecoenchants.target.EnchantFinder.hasEnchantActive
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.block.BlockFace
|
||||
|
@ -11,8 +11,7 @@ import com.willfp.eco.core.items.Items
|
||||
import com.willfp.ecoenchants.EcoEnchantsPlugin
|
||||
import com.willfp.ecoenchants.enchant.EcoEnchant
|
||||
import com.willfp.ecoenchants.enchant.impl.HardcodedEcoEnchant
|
||||
import com.willfp.ecoenchants.target.EnchantLookup.getActiveEnchantLevelInSlot
|
||||
import com.willfp.ecoenchants.target.EnchantLookup.hasEnchantActive
|
||||
import com.willfp.ecoenchants.target.EnchantFinder.getItemsWithEnchantActive
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.EventHandler
|
||||
import org.bukkit.event.EventPriority
|
||||
@ -20,7 +19,6 @@ import org.bukkit.event.Listener
|
||||
import org.bukkit.event.entity.PlayerDeathEvent
|
||||
import org.bukkit.event.player.PlayerJoinEvent
|
||||
import org.bukkit.event.player.PlayerRespawnEvent
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.persistence.PersistentDataType
|
||||
|
||||
class EnchantmentSoulbound(
|
||||
@ -61,25 +59,7 @@ class EnchantmentSoulbound(
|
||||
}
|
||||
|
||||
val player = event.entity
|
||||
val items = mutableListOf<ItemStack>()
|
||||
|
||||
if (!player.hasEnchantActive(enchant)) {
|
||||
return
|
||||
}
|
||||
|
||||
for ((slot, item) in player.inventory.withIndex()) {
|
||||
if (item == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
val level = player.getActiveEnchantLevelInSlot(enchant, slot)
|
||||
|
||||
if (level == 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
items.add(item)
|
||||
}
|
||||
val items = player.getItemsWithEnchantActive(enchant).keys
|
||||
|
||||
if (items.isEmpty()) {
|
||||
return
|
||||
|
@ -0,0 +1,72 @@
|
||||
package com.willfp.ecoenchants.target
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.willfp.eco.core.fast.fast
|
||||
import com.willfp.ecoenchants.enchant.EcoEnchant
|
||||
import com.willfp.ecoenchants.enchant.EcoEnchantLevel
|
||||
import com.willfp.libreforge.ProvidedHolder
|
||||
import com.willfp.libreforge.slot.ItemHolderFinder
|
||||
import com.willfp.libreforge.slot.SlotType
|
||||
import com.willfp.libreforge.toDispatcher
|
||||
import org.bukkit.entity.LivingEntity
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object EnchantFinder : ItemHolderFinder<EcoEnchantLevel>() {
|
||||
private val provider = this.toHolderProvider()
|
||||
private val levelCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(1, TimeUnit.SECONDS)
|
||||
.build<UUID, List<ProvidedLevel>>()
|
||||
|
||||
override fun find(item: ItemStack): List<EcoEnchantLevel> {
|
||||
val enchantMap = item.fast().enchants
|
||||
val enchants = mutableListOf<EcoEnchantLevel>()
|
||||
|
||||
for ((enchant, level) in enchantMap) {
|
||||
if (enchant !is EcoEnchant) {
|
||||
continue
|
||||
}
|
||||
|
||||
enchants += enchant.getLevel(level)
|
||||
}
|
||||
|
||||
return enchants
|
||||
}
|
||||
|
||||
override fun isValidInSlot(holder: EcoEnchantLevel, slot: SlotType): Boolean {
|
||||
return slot in holder.enchant.targets.map { it.slot }
|
||||
}
|
||||
|
||||
internal fun LivingEntity.clearEnchantmentCache() = levelCache.invalidate(this.uniqueId)
|
||||
|
||||
private val LivingEntity.cachedLevels: List<ProvidedLevel>
|
||||
get() = levelCache.get(this.uniqueId) {
|
||||
provider.provide(this.toDispatcher())
|
||||
.mapNotNull {
|
||||
val level = it.holder as? EcoEnchantLevel ?: return@mapNotNull null
|
||||
val item = it.provider as? ItemStack ?: return@mapNotNull null
|
||||
|
||||
ProvidedLevel(level, item, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun LivingEntity.hasEnchantActive(enchant: EcoEnchant): Boolean {
|
||||
return this.cachedLevels
|
||||
.filter { it.level.enchant == enchant }
|
||||
.any { it.level.conditions.areMet(this.toDispatcher(), it.holder) }
|
||||
}
|
||||
|
||||
fun LivingEntity.getItemsWithEnchantActive(enchant: EcoEnchant): Map<ItemStack, Int> {
|
||||
return this.cachedLevels
|
||||
.filter { it.level.enchant == enchant }
|
||||
.filter { it.level.conditions.areMet(this.toDispatcher(), it.holder) }
|
||||
.associate { it.item to it.level.level }
|
||||
}
|
||||
|
||||
private data class ProvidedLevel(
|
||||
val level: EcoEnchantLevel,
|
||||
val item: ItemStack,
|
||||
val holder: ProvidedHolder
|
||||
)
|
||||
}
|
@ -1,345 +0,0 @@
|
||||
package com.willfp.ecoenchants.target
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.willfp.eco.core.fast.fast
|
||||
import com.willfp.eco.core.items.HashedItem
|
||||
import com.willfp.ecoenchants.enchant.EcoEnchant
|
||||
import com.willfp.ecoenchants.enchant.EcoEnchantLevel
|
||||
import com.willfp.ecoenchants.enchant.FoundEcoEnchantLevel
|
||||
import com.willfp.ecoenchants.plugin
|
||||
import com.willfp.libreforge.ItemProvidedHolder
|
||||
import com.willfp.libreforge.slot.SlotType
|
||||
import com.willfp.libreforge.slot.SlotTypes
|
||||
import com.willfp.libreforge.toDispatcher
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// The Int is the inventory slot ID
|
||||
typealias SlotProvider = (Player) -> Map<ItemInNumericSlot, ItemInSlot>
|
||||
|
||||
data class ItemInSlot internal constructor(
|
||||
val item: ItemStack, val slot: Collection<SlotType>
|
||||
) {
|
||||
constructor(
|
||||
item: ItemStack, slot: SlotType
|
||||
) : this(item, listOf(slot))
|
||||
}
|
||||
|
||||
data class ItemInNumericSlot internal constructor(
|
||||
val item: ItemStack, val slot: Int
|
||||
) {
|
||||
override fun hashCode(): Int {
|
||||
return HashedItem.of(item).hash * (slot + 1)
|
||||
}
|
||||
}
|
||||
|
||||
private data class HeldEnchant(
|
||||
val enchant: EcoEnchant, val level: Int
|
||||
)
|
||||
|
||||
object EnchantLookup {
|
||||
private val slotProviders = mutableSetOf<SlotProvider>()
|
||||
|
||||
private val itemCache =
|
||||
Caffeine.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS).build<Player, Map<ItemInNumericSlot, ItemInSlot>>()
|
||||
|
||||
private val enchantCache = Caffeine.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS)
|
||||
.build<Player, Map<ItemInNumericSlot, Collection<HeldEnchant>>>()
|
||||
|
||||
// Higher frequency cache as less intensive
|
||||
private val enchantLevelCache = Caffeine.newBuilder().expireAfterWrite(200, TimeUnit.MILLISECONDS)
|
||||
.build<Player, Map<ItemInNumericSlot, Map<EcoEnchant, Int>>>()
|
||||
|
||||
@JvmStatic
|
||||
fun registerProvider(provider: SlotProvider) {
|
||||
slotProviders.add {
|
||||
val found = mutableMapOf<ItemInNumericSlot, ItemInSlot>()
|
||||
for ((slot, inSlot) in provider(it)) {
|
||||
found[slot] = inSlot
|
||||
}
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
private fun provide(player: Player): Map<ItemInNumericSlot, ItemInSlot> {
|
||||
return itemCache.get(player) {
|
||||
val found = mutableMapOf<ItemInNumericSlot, ItemInSlot>()
|
||||
|
||||
for (provider in slotProviders) {
|
||||
val fromProvider = provider(player)
|
||||
for ((slot, item) in fromProvider) {
|
||||
// Basically a multimap
|
||||
val current = found[slot]
|
||||
|
||||
if (current == null) {
|
||||
found[slot] = item
|
||||
} else {
|
||||
found[slot] = ItemInSlot(item.item, listOf(current.slot, item.slot).flatten())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The inventory slot IDs mapped to HeldEnchants found in that slot.
|
||||
*/
|
||||
private val Player.slotHeldEnchants: Map<ItemInNumericSlot, Collection<HeldEnchant>>
|
||||
get() {
|
||||
return enchantCache.get(this) {
|
||||
val found = mutableMapOf<ItemInNumericSlot, MutableCollection<HeldEnchant>>()
|
||||
|
||||
for ((slotID, inSlot) in provide(this)) {
|
||||
val (item, slot) = inSlot
|
||||
|
||||
val enchants = item.fast().enchants
|
||||
|
||||
for ((enchant, level) in enchants) {
|
||||
if (enchant !is EcoEnchant) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (slot.none { it in enchant.slots }) {
|
||||
continue
|
||||
}
|
||||
|
||||
val held = HeldEnchant(enchant, level)
|
||||
|
||||
// Prevent repeating slot IDs found in multiple TargetSlots (e.g. HANDS and ANY)
|
||||
if (held in found.getOrDefault(slotID, mutableListOf())) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Basically a multimap
|
||||
found[slotID] =
|
||||
(found.getOrDefault(slotID, mutableListOf()) + HeldEnchant(enchant, level)).toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot IDs mapped to HeldEnchants found in that slot.
|
||||
*/
|
||||
private val Player.slotIDHeldEnchants: Map<Int, Collection<HeldEnchant>>
|
||||
get() {
|
||||
return this.slotHeldEnchants.mapKeys { it.key.slot }
|
||||
}
|
||||
|
||||
/**
|
||||
* All slots holding EcoEnchants mapped to their IDs, regardless of conditions.
|
||||
*/
|
||||
val Player.heldEnchantsInSlots: Map<ItemInNumericSlot, Map<EcoEnchant, Int>>
|
||||
get() {
|
||||
return enchantLevelCache.get(this) {
|
||||
val found = mutableMapOf<ItemInNumericSlot, MutableMap<EcoEnchant, Int>>()
|
||||
|
||||
for ((inSlot, enchants) in this.slotHeldEnchants) {
|
||||
val map = mutableMapOf<EcoEnchant, Int>()
|
||||
for ((enchant, level) in enchants) {
|
||||
map[enchant] = level
|
||||
}
|
||||
found[inSlot] = map
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All EcoEnchants mapped to their IDs, regardless of conditions.
|
||||
*/
|
||||
val Player.heldEnchants: Map<EcoEnchant, Int>
|
||||
get() {
|
||||
val found = mutableMapOf<EcoEnchant, Int>()
|
||||
for ((_, enchants) in this.heldEnchantsInSlots) {
|
||||
for ((enchant, level) in enchants) {
|
||||
found[enchant] = level
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* All slots mapped to EcoEnchants mapped to their IDs, respecting conditions.
|
||||
*/
|
||||
val Player.activeEnchantsInSlots: Map<ItemInNumericSlot, Map<EcoEnchant, Int>>
|
||||
get() {
|
||||
val found = mutableMapOf<ItemInNumericSlot, MutableMap<EcoEnchant, Int>>()
|
||||
for ((slot, enchants) in this.heldEnchantsInSlots) {
|
||||
val inSlot = mutableMapOf<EcoEnchant, Int>()
|
||||
for ((enchant, level) in enchants) {
|
||||
val enchantLevel = enchant.getLevel(level)
|
||||
val providedHolder = ItemProvidedHolder(enchantLevel, slot.item)
|
||||
if (enchantLevel.conditions.areMet(this.toDispatcher(), providedHolder)) {
|
||||
inSlot[enchant] = level
|
||||
}
|
||||
}
|
||||
found[slot] = inSlot
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* All EcoEnchants mapped to their IDs, respecting conditions.
|
||||
*/
|
||||
val Player.activeEnchants: Map<EcoEnchant, Int>
|
||||
get() {
|
||||
val found = mutableMapOf<EcoEnchant, Int>()
|
||||
for ((_, enchants) in this.activeEnchantsInSlots) {
|
||||
for ((enchant, level) in enchants) {
|
||||
found[enchant] = level
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enchantment level on a player, ignoring conditions.
|
||||
*
|
||||
* @return The level, or 0 if not found.
|
||||
*/
|
||||
fun Player.getEnchantLevel(enchant: EcoEnchant): Int {
|
||||
return this.heldEnchants[enchant] ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enchantment level on a player, respecting.
|
||||
*
|
||||
* @return The level, or 0 if not found.
|
||||
*/
|
||||
fun Player.getActiveEnchantLevel(enchant: EcoEnchant): Int {
|
||||
return this.activeEnchants[enchant] ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enchantment level on a player in a specific slot, ignoring conditions.
|
||||
*
|
||||
* @return The level, or 0 if not found.
|
||||
*/
|
||||
fun Player.getEnchantLevelInSlot(enchant: EcoEnchant, slot: Int): Int {
|
||||
val inSlot = this.slotIDHeldEnchants[slot] ?: return 0
|
||||
val heldEnchant = inSlot.firstOrNull { it.enchant == enchant } ?: return 0
|
||||
return heldEnchant.level
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enchantment level on a player in a specific slot, respecting conditions.
|
||||
*
|
||||
* @return The level, or 0 if not found.
|
||||
*/
|
||||
fun Player.getActiveEnchantLevelInSlot(enchant: EcoEnchant, slot: Int): Int {
|
||||
val level = getEnchantLevelInSlot(enchant, slot)
|
||||
|
||||
if (level == 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val enchantLevel = enchant.getLevel(level)
|
||||
val item = this.inventory.getItem(slot) ?: return 0
|
||||
val providedHolder = ItemProvidedHolder(enchantLevel, item)
|
||||
|
||||
if (!enchantLevel.conditions.areMet(this.toDispatcher(), providedHolder)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return level
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if a player has an enchantment, ignoring conditions.
|
||||
*
|
||||
* @return If the player has the enchantment.
|
||||
*/
|
||||
fun Player.hasEnchant(enchant: EcoEnchant): Boolean {
|
||||
return this.getEnchantLevel(enchant) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if a player has an enchantment, respecting conditions.
|
||||
*
|
||||
* @return If the player has the enchantment.
|
||||
*/
|
||||
fun Player.hasEnchantActive(enchant: EcoEnchant): Boolean {
|
||||
return this.getActiveEnchantLevel(enchant) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if a player has an enchantment in a slot, ignoring conditions.
|
||||
*
|
||||
* @return If the player has the enchantment.
|
||||
*/
|
||||
fun Player.hasEnchantInSlot(enchant: EcoEnchant, slot: Int): Boolean {
|
||||
return this.getEnchantLevelInSlot(enchant, slot) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if a player has an enchantment in a slot, respecting conditions.
|
||||
*
|
||||
* @return If the player has the enchantment.
|
||||
*/
|
||||
fun Player.hasEnchantActiveInSlot(enchant: EcoEnchant, slot: Int): Boolean {
|
||||
return this.getActiveEnchantLevelInSlot(enchant, slot) > 0
|
||||
}
|
||||
|
||||
val Player.heldEnchantLevels: List<ItemProvidedHolder>
|
||||
get() {
|
||||
val found = mutableListOf<ItemProvidedHolder>()
|
||||
|
||||
for ((slot, enchants) in this.heldEnchantsInSlots) {
|
||||
for ((enchant, level) in enchants) {
|
||||
found.add(ItemProvidedHolder(enchant.getLevel(level), slot.item))
|
||||
}
|
||||
}
|
||||
|
||||
// This is such a fucking disgusting way of implementing %active_level%,
|
||||
// and it's probably quite slow too.
|
||||
return if (plugin.configYml.getBool("extra-placeholders.active-level")) {
|
||||
found.map {
|
||||
val level = it.holder as EcoEnchantLevel
|
||||
|
||||
ItemProvidedHolder(
|
||||
FoundEcoEnchantLevel(level, this.getActiveEnchantLevel(level.enchant)), it.provider
|
||||
)
|
||||
}
|
||||
} else {
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear item and enchant cache.
|
||||
*/
|
||||
fun Player.clearEnchantCache() {
|
||||
itemCache.invalidate(player)
|
||||
enchantCache.invalidate(player)
|
||||
enchantLevelCache.invalidate(player)
|
||||
}
|
||||
|
||||
init {
|
||||
fun createProvider(slot: SlotType): SlotProvider {
|
||||
return { player: Player ->
|
||||
val found = mutableMapOf<ItemInNumericSlot, ItemInSlot>()
|
||||
|
||||
for (slotID in slot.getItemSlots(player)) {
|
||||
val item = player.inventory.getItem(slotID) ?: continue
|
||||
found[ItemInNumericSlot(item, slotID)] = ItemInSlot(item, slot)
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
for (slot in SlotTypes.values()) {
|
||||
registerProvider(createProvider(slot))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
#libreforge-updater
|
||||
#Wed Dec 27 14:18:03 CET 2023
|
||||
kotlin.code.style=official
|
||||
libreforge-version=4.51.1
|
||||
version=11.2.1
|
||||
libreforge-version=4.52.0
|
||||
version=12.0.0
|
||||
|
Loading…
Reference in New Issue
Block a user