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 import java.util.concurrent.atomic.AtomicInteger 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 noteID128Map = mutableMapOf<String, T>() val noteID256Map = mutableMapOf<String, T>() fun wipe() { noteID128Map.clear() noteID256Map.clear() } fun getValue(noteID128: String, noteID256: String, defaultValue: T): T { return noteID128Map.getOrDefault(noteID128, noteID256Map.getOrDefault(noteID256, defaultValue)) } } private val noteIDAbilityMap = mapOf<AbilityClassSystem.AbilityClassVariety, AbilityMap<Double>>( Pair(AbilityClassSystem.AbilityClassVariety.ETC, AbilityMap()), Pair(AbilityClassSystem.AbilityClassVariety.INPUT_MODE_6K, 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>>() private val abilityNameAbilityIDNoteIDCountMap = mutableMapOf<String, MutableMap<String, AtomicInteger>>() fun loadAbility() { logFuture { val jm = ObjectMapper() val defaultAbility = jm.readValue( TwilightComponent.ABILITY_ENTRY_PATH.resolve("Default.json").toFile().absoluteFile, 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_6K]?.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() abilityNameAbilityIDNoteIDCountMap.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().absoluteFile, JSON.BMSTable::class.java) val levels = abilityTable.level_order.map { "${abilityTable.symbol}$it" }.toTypedArray() jm.readValue( TwilightComponent.ABILITY_ENTRY_PATH.resolve("$abilityName.json") .toFile().absoluteFile, 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?.noteID128Map?.putIfAbsent(abilityTableData.md5, ability) } if (abilityTableData.sha256.isNotEmpty()) { noteIDAbilityMap?.noteID256Map?.putIfAbsent(abilityTableData.sha256, ability) } } if (abilityTableData.md5.isNotEmpty()) { noteIDAbilityIDsMap.noteID128Map.computeIfAbsent( abilityTableData.md5 ) { mutableListOf() }.add(abilityID) noteIDAbilityNamesMap.noteID128Map.computeIfAbsent( abilityTableData.md5 ) { mutableListOf() }.add(abilityName) } if (abilityTableData.sha256.isNotEmpty()) { noteIDAbilityIDsMap.noteID256Map.computeIfAbsent( abilityTableData.sha256 ) { mutableListOf() }.add(abilityID) noteIDAbilityNamesMap.noteID256Map.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) abilityNameAbilityIDNoteIDCountMap.computeIfAbsent(abilityName) { mutableMapOf() }.computeIfAbsent(abilityID) { AtomicInteger() }.incrementAndGet() } logInfo("Loaded Ability (${abilityTable.name})") } } } DB.learnAbility() logInfo("Learned Ability") } catch (e: IOException) { logFault(e) } } } 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 } fun getNoteIDCount(abilityName: String, abilityID: String): Int { return if (abilityID == "*") abilityNameAbilityIDNoteIDCountMap.getOrDefault( abilityName, emptyMap() ).values.sumOf { it.get() } else abilityNameAbilityIDNoteIDCountMap.getOrDefault(abilityName, emptyMap()) .getOrDefault(abilityID, AtomicInteger()).get() } }