package dev.cobblesurvival.progress import com.cobblemon.mod.common.entity.pokemon.PokemonEntity import dev.cobblesurvival.CobbleSurvival import net.neoforged.fml.ModList import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.lang.reflect.Proxy /** * Puts [Gates] in front of Cobblebase's skills, by reflection (Cobblebase is not a compile dependency): every skill * executor in Cobblebase's `ExecutorRegistry` is replaced by a proxy that runs it only when the pastured Pokémon passes * that skill's soft gate (level or held material); otherwise its owner is told what it needs. Cobblebase looks the executor up on every tick, so the proxies take effect at once. One * executor can serve several skills (all the finders), so the check is per skill, not per executor. */ object CobblebaseGate { private const val CORE = "notlown.cobblebase.core" val installed: Boolean get() = ModList.get().isLoaded("cobblebase") internal val executorType by lazy { Class.forName("$CORE.SkillExecutor") } internal val skillEntry by lazy { Class.forName("$CORE.SkillEntry") } private val executors by lazy { Class.forName("$CORE.ExecutorRegistry").let { it to it.getField("INSTANCE").get(null) } } private val skillRegistry by lazy { Class.forName("$CORE.SkillRegistry").let { it to it.getField("INSTANCE").get(null) } } internal val skillDef by lazy { Class.forName("$CORE.SkillDef") } private val skillId by lazy { skillDef.getMethod("getId") } /** Skill id → its definition (Cobblebase's SkillDef). */ @Suppress("UNCHECKED_CAST") fun skills(): Map { val (type, instance) = skillRegistry val all = type.getMethod("getAll").invoke(instance) as Map return all.values.associateBy { skillId.invoke(it) as String } } fun skillName(id: String): String? = skills()[id]?.let { skillDef.getMethod("getName").invoke(it) as String } fun executor(name: String): Any? { val (type, instance) = executors return type.getMethod("get", String::class.java).invoke(instance, name) } /** The executor names the skills use (several skills can share one). */ fun executorNames(): Set = skills().values.map { skillDef.getMethod("getExecutor").invoke(it) as String }.toSortedSet() fun install() { if (!installed) return val (type, instance) = executors val register = type.getMethod("register", String::class.java, executorType) var wrapped = 0 for (name in executorNames()) { val original = executor(name) ?: continue if (isGated(original)) continue register.invoke(instance, name, wrap(original)) wrapped++ } CobbleSurvival.LOGGER.info("Progression gates in front of {} Cobblebase skill executors", wrapped) } fun isGated(executor: Any): Boolean = Proxy.isProxyClass(executor.javaClass) && Proxy.getInvocationHandler(executor) is Gate /** [original] behind the gates: `tick(level, pos, pokemonEntity, skillDef, skillEntry)` only runs if allowed. */ fun wrap(original: Any): Any = Proxy.newProxyInstance(executorType.classLoader, arrayOf(executorType), Gate(original)) private class Gate(private val original: Any) : InvocationHandler { override fun invoke(proxy: Any, method: Method, args: Array?): Any? { if (method.name == "tick" && args != null && args.size == 5) { val entity = args[2] as PokemonEntity val skill = skillId.invoke(args[3]) as String if (!Gates.skillOpen(entity.pokemon, skill)) { Gates.noticeIdle(entity, skill) return null } } return try { method.invoke(original, *(args ?: emptyArray())) } catch (e: InvocationTargetException) { throw e.targetException } } } }