Newer
Older
Twilight / src / main / kotlin / net / taehui / twilight / system / AbilitySystem.kt
package net.taehui.twilight.system

import com.fasterxml.jackson.databind.ObjectMapper
import net.taehui.twilight.JSON
import net.taehui.twilight.Logger
import net.taehui.twilight.TwilightComponent
import net.taehui.twilight.Utility
import org.apache.commons.io.FilenameUtils
import org.apache.hc.client5.http.classic.methods.HttpGet
import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler
import org.apache.hc.client5.http.impl.classic.HttpClients
import org.jsoup.Jsoup
import java.io.IOException
import java.nio.file.Files
import java.util.TreeSet

object AbilitySystem : Logger {
    class DefaultAbility {
        var abilityFiles = emptyArray<AbilityFile>()
        var abilityWww = emptyArray<String>()
        var abilityMap = emptyMap<String, Double>()

        class AbilityFile {
            var fileName = ""
            var abilityClassVariety = AbilityClassSystem.AbilityClassVariety.ETC
        }
    }

    class AbilityMap<T> {
        val hashMap128 = mutableMapOf<String, T>()
        val hashMap256 = mutableMapOf<String, T>()

        fun wipe() {
            hashMap128.clear()
            hashMap256.clear()
        }

        fun getValue(noteID128: String, noteID256: String, defaultValue: T): T {
            return hashMap128.getOrDefault(noteID128, hashMap256.getOrDefault(noteID256, defaultValue))
        }
    }

    private val noteIDAbilityMap = mapOf<AbilityClassSystem.AbilityClassVariety, AbilityMap<Double>>(
        Pair(AbilityClassSystem.AbilityClassVariety.ETC, AbilityMap()),
        Pair(AbilityClassSystem.AbilityClassVariety.INPUT_MODE_5K, AbilityMap()),
        Pair(AbilityClassSystem.AbilityClassVariety.INPUT_MODE_7K, AbilityMap()),
        Pair(AbilityClassSystem.AbilityClassVariety.INPUT_MODE_9K, AbilityMap())
    )
    private val noteIDAbilityIDsMap = AbilityMap<MutableCollection<String>>()
    private val noteIDAbilityNamesMap = AbilityMap<MutableCollection<String>>()
    private val abilityNameAbilityIDsMap = mutableMapOf<String, MutableCollection<String>>()
    var isLoading = false

    fun loadAbility() {
        isLoading = true
        logFuture {
            try {
                val jm = ObjectMapper()
                val defaultAbility = jm.readValue(
                    TwilightComponent.ABILITY_ENTRY_PATH.resolve("Default.json").toFile(),
                    DefaultAbility::class.java
                )
                if (Configure.mode.getAbility) {
                    defaultAbility.abilityWww.forEach { target ->
                        try {
                            val o = Jsoup.connect(target).get()
                            var dataValue = o.selectXpath("/html/head/meta[@name='bmstable']")
                            if (dataValue.isEmpty()) {
                                dataValue = o.selectXpath("/html/body/meta[@name='bmstable']")
                            }
                            if (dataValue.isEmpty()) {
                                dataValue = o.selectXpath("/html/head/body/meta[@name='bmstable']")
                            }
                            if (!dataValue.isEmpty()) {
                                val www = dataValue[0].attr("content")
                                HttpClients.createDefault().use {
                                    fun doModifyDataValue(target: String, dataValue: String): String {
                                        return if (!Utility.isValidWww(dataValue)) {
                                            if (target.substring(target.lastIndexOf('/'))
                                                    .contains(".") || target.endsWith("/")
                                            ) {
                                                "$target/../$dataValue"
                                            } else {
                                                "$target/$dataValue"
                                            }
                                        } else {
                                            dataValue
                                        }
                                    }

                                    val text = it.execute(
                                        HttpGet(doModifyDataValue(target, www)),
                                        BasicHttpClientResponseHandler()
                                    )
                                    val abilityTable = text.byteInputStream().use { src ->
                                        ObjectMapper().readValue(src, JSON.BMSTable::class.java)
                                    }
                                    Files.writeString(
                                        TwilightComponent.ABILITY_ENTRY_PATH.resolve("#" + abilityTable.name + ".json"),
                                        text
                                    )
                                    Files.writeString(
                                        TwilightComponent.ABILITY_ENTRY_PATH.resolve(abilityTable.name + ".json"),
                                        it.execute(
                                            HttpGet(doModifyDataValue(target, abilityTable.data_url)),
                                            BasicHttpClientResponseHandler()
                                        )
                                    )
                                    logInfo("Saved Ability (${abilityTable.name})")
                                }
                            }
                        } catch (e: IOException) {
                            logFault(e)
                        }
                    }
                }
                try {
                    val abilityMap = defaultAbility.abilityMap
                    noteIDAbilityMap[AbilityClassSystem.AbilityClassVariety.ETC]?.wipe()
                    noteIDAbilityMap[AbilityClassSystem.AbilityClassVariety.INPUT_MODE_5K]?.wipe()
                    noteIDAbilityMap[AbilityClassSystem.AbilityClassVariety.INPUT_MODE_7K]?.wipe()
                    noteIDAbilityMap[AbilityClassSystem.AbilityClassVariety.INPUT_MODE_9K]?.wipe()
                    noteIDAbilityIDsMap.wipe()
                    noteIDAbilityNamesMap.wipe()
                    abilityNameAbilityIDsMap.clear()
                    Files.list(TwilightComponent.ABILITY_ENTRY_PATH).use { abilityFilePaths ->
                        abilityFilePaths.filter { it.fileName.toString().startsWith("#") }.sorted(
                            Comparator.comparingInt { abilityFilePath ->
                                defaultAbility.abilityFiles.indexOfFirst {
                                    it.fileName == abilityFilePath.fileName.toString()
                                }
                            }
                        ).forEach { abilityFilePath ->
                            val abilityFileName = abilityFilePath.fileName.toString()
                            val abilityName = FilenameUtils.removeExtension(
                                abilityFileName.substring(abilityFileName.indexOf('#') + 1)
                            )
                            val abilityClassVariety = defaultAbility.abilityFiles.find {
                                it.fileName == abilityFileName
                            }?.abilityClassVariety
                            if (abilityClassVariety != null) {
                                val noteIDAbilityMap = noteIDAbilityMap[abilityClassVariety]
                                val abilityTable = jm.readValue(abilityFilePath.toFile(), JSON.BMSTable::class.java)
                                val levels =
                                    abilityTable.level_order.map { "${abilityTable.symbol}$it" }.toTypedArray();
                                jm.readValue(
                                    TwilightComponent.ABILITY_ENTRY_PATH.resolve("$abilityName.json")
                                        .toFile(),
                                    Array<JSON.BMSTableData>::class.java
                                ).forEach { abilityTableData ->
                                    val abilityID = "${abilityTable.symbol}${abilityTableData.level}"
                                    val ability = abilityMap[abilityID] ?: 0.0
                                    if (ability > 0.0) {
                                        if (abilityTableData.md5.isNotEmpty()) {
                                            noteIDAbilityMap?.hashMap128?.putIfAbsent(abilityTableData.md5, ability)
                                        }
                                        if (abilityTableData.sha256.isNotEmpty()) {
                                            noteIDAbilityMap?.hashMap256?.putIfAbsent(abilityTableData.sha256, ability)
                                        }
                                    }
                                    if (abilityTableData.md5.isNotEmpty()) {
                                        noteIDAbilityIDsMap.hashMap128.computeIfAbsent(
                                            abilityTableData.md5
                                        ) { mutableListOf() }.add(abilityID)
                                        noteIDAbilityNamesMap.hashMap128.computeIfAbsent(
                                            abilityTableData.md5
                                        ) { mutableListOf() }.add(abilityName)
                                    }
                                    if (abilityTableData.sha256.isNotEmpty()) {
                                        noteIDAbilityIDsMap.hashMap256.computeIfAbsent(
                                            abilityTableData.sha256
                                        ) { mutableListOf() }.add(abilityID)
                                        noteIDAbilityNamesMap.hashMap256.computeIfAbsent(
                                            abilityTableData.md5
                                        ) { mutableListOf() }.add(abilityName)
                                    }
                                    abilityNameAbilityIDsMap.computeIfAbsent(abilityName) {
                                        if (levels.isEmpty()) mutableSetOf() else TreeSet { o1, o2 ->
                                            levels.indexOf(o1).compareTo(levels.indexOf(o2))
                                        }
                                    }.add(abilityID)
                                }
                                logInfo("Loaded Ability (${abilityTable.name})")
                            }
                        }
                    }
                } catch (e: IOException) {
                    logFault(e)
                }
                try {
                    DB.learnAbility()
                    logInfo("Learned Ability")
                } catch (e: Throwable) {
                    logFault(e)
                }
            } finally {
                isLoading = false
            }
        }
    }

    fun getAbility(
        abilityClassVariety: AbilityClassSystem.AbilityClassVariety,
        noteID128: String,
        noteID256: String
    ): Double {
        return noteIDAbilityMap[abilityClassVariety]?.getValue(noteID128, noteID256, 0.0) ?: 0.0
    }

    fun getAbilityIDs(
        noteID128: String,
        noteID256: String
    ): Collection<String> {
        return noteIDAbilityIDsMap.getValue(noteID128, noteID256, mutableListOf())
    }

    fun getAbilityNames(
        noteID128: String,
        noteID256: String
    ): Collection<String> {
        return noteIDAbilityNamesMap.getValue(noteID128, noteID256, mutableListOf())
    }

    fun getAbilityIDs(
        abilityName: String
    ): Collection<String> {
        return abilityNameAbilityIDsMap.getOrDefault(abilityName, emptyList())
    }

    fun getAbilityNames(): Collection<String> {
        return abilityNameAbilityIDsMap.keys
    }
}