Newer
Older
Twilight / src / main / kotlin / net / taehui / twilight / system / LevelSystem.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 org.apache.commons.io.FilenameUtils
import java.io.IOException
import java.nio.file.Files
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.stream.Stream

object LevelSystem : Logger {
    class LevelGroup {
        var avatarID = ""
        var levelItems = emptyArray<LevelItem>()

        class LevelItem : Comparable<LevelItem> {
            override fun compareTo(other: LevelItem): Int {
                val levelData = level.compareTo(other.level)
                if (levelData != 0) {
                    return levelData
                }
                var levelTextData = getValue(levelText).compareTo(getValue(other.levelText))
                if (levelTextData != 0) {
                    return levelTextData
                }
                levelTextData = levelText.compareTo(other.levelText)
                return if (levelTextData != 0) {
                    levelTextData
                } else title.compareTo(other.title)
            }

            var levelID = ""
            var noteID = ""
            var title = ""
            var comment = ""
            var levelText = ""
            var level = 0
            var stand: IntArray? = null
            var point: DoubleArray? = null
            var band: IntArray? = null
            var judgments: Array<IntArray>? = null
            var autoMode: IntArray? = null
            var noteSaltMode: IntArray? = null
            var audioMultiplier: DoubleArray? = null
            var faintNoteMode: IntArray? = null
            var judgmentMode: IntArray? = null
            var hitPointsMode: IntArray? = null
            var noteMobilityMode: IntArray? = null
            var longNoteMode: IntArray? = null
            var inputFavorMode: IntArray? = null
            var noteModifyMode: IntArray? = intArrayOf(0)
            var bpmMode: IntArray? = intArrayOf(0)
            var waveMode: IntArray? = intArrayOf(0)
            var setNoteMode: IntArray? = intArrayOf(0)
            var lowestJudgmentConditionMode: IntArray? = null
            var allowPause = true

            fun isSatisfy(qwilightWwwLevel: JSON.QwilightWwwLevel): Boolean {
                return judgments?.let {
                    it.indices.all { i ->
                        isSatisfy(it[i], qwilightWwwLevel.judgments[i])
                    }
                } != false &&
                        isSatisfy(stand, qwilightWwwLevel.stand) &&
                        isSatisfy(point, qwilightWwwLevel.point) &&
                        isSatisfy(band, qwilightWwwLevel.band) &&
                        isSatisfyMode(autoMode, qwilightWwwLevel.autoMode) &&
                        isSatisfyMode(noteSaltMode, qwilightWwwLevel.noteSaltMode) &&
                        isSatisfyMode(faintNoteMode, qwilightWwwLevel.faintNoteMode) &&
                        isSatisfyMode(judgmentMode, qwilightWwwLevel.judgmentMode) &&
                        isSatisfyMode(hitPointsMode, qwilightWwwLevel.hitPointsMode) &&
                        isSatisfyMode(noteMobilityMode, qwilightWwwLevel.noteMobilityMode) &&
                        isSatisfyMode(longNoteMode, qwilightWwwLevel.longNoteMode) &&
                        isSatisfyMode(inputFavorMode, qwilightWwwLevel.inputFavorMode) &&
                        isSatisfyMode(noteModifyMode, qwilightWwwLevel.noteModifyMode) &&
                        isSatisfyMode(bpmMode, qwilightWwwLevel.bpmMode) &&
                        isSatisfyMode(waveMode, qwilightWwwLevel.waveMode) &&
                        isSatisfyMode(setNoteMode, qwilightWwwLevel.setNoteMode) &&
                        isSatisfyMode(lowestJudgmentConditionMode, qwilightWwwLevel.lowestJudgmentConditionMode)
            }

            companion object {
                fun getValue(data: String): Int {
                    return try {
                        data.replace("\\D".toRegex(), "").toInt()
                    } catch (e: NumberFormatException) {
                        0
                    }
                }

                fun isSatisfy(data: IntArray?, target: Int): Boolean {
                    return if (data != null) {
                        val lowestData = data[0]
                        val highestData = data[1]
                        (lowestData == -1 || lowestData <= target) && (highestData == -1 || target <= highestData)
                    } else {
                        true
                    }
                }

                fun isSatisfy(data: DoubleArray?, target: Double): Boolean {
                    return if (data != null) {
                        val lowestData = data[0]
                        val highestData = data[1]
                        (lowestData == -1.0 || lowestData <= target) && (highestData == -1.0 || target <= highestData)
                    } else {
                        true
                    }
                }

                fun isSatisfyMode(data: IntArray?, target: Int): Boolean {
                    return data?.any {
                        it == target
                    } ?: true
                }
            }
        }
    }

    private val avatarIDLevelGroupMap = ConcurrentHashMap<String, MutableList<LevelGroup>>()
    private val levelNameLevelGroupMap = ConcurrentHashMap<String, LevelGroup>()
    private val avatarIDLevelNameMap = ConcurrentHashMap<String, MutableList<String>>()
    private val levelIDDrawingMap = ConcurrentHashMap<String, ByteArray>()
    val levelNoteIDs = CopyOnWriteArrayList<String>()
    var isLoading = false

    fun loadLevel() {
        isLoading = true
        try {
            avatarIDLevelGroupMap.clear()
            levelNameLevelGroupMap.clear()
            avatarIDLevelNameMap.clear()
            levelIDDrawingMap.clear()
            levelNoteIDs.clear()
            Files.list(TwilightComponent.LEVEL_ENTRY_PATH).use {
                it.forEach { levelFilePath ->
                    if (!Files.isDirectory(levelFilePath)) {
                        try {
                            val jm = ObjectMapper()
                            val levelGroup = jm.readValue(levelFilePath.toFile().absoluteFile, LevelGroup::class.java)
                            levelGroup.levelItems.map { data ->
                                data.noteID.split("/".toRegex()).toTypedArray()
                            }.forEach { noteIDs -> levelNoteIDs.addAll(listOf(*noteIDs)) }
                            Arrays.sort(levelGroup.levelItems)

                            val avatarID = levelGroup.avatarID
                            val levelName = FilenameUtils.removeExtension(levelFilePath.fileName.toString())
                            avatarIDLevelGroupMap.computeIfAbsent(avatarID) { mutableListOf() }.add(levelGroup)
                            levelNameLevelGroupMap[levelName] = levelGroup
                            avatarIDLevelNameMap.computeIfAbsent(avatarID) { mutableListOf() }.add(levelName)
                            levelGroup.levelItems.forEach { levelItem ->
                                val levelID = levelItem.levelID
                                try {
                                    levelIDDrawingMap[levelID] =
                                        Files.readAllBytes(
                                            TwilightComponent.LEVEL_ENTRY_PATH.resolve(levelName)
                                                .resolve("$levelID.png")
                                        )
                                } catch (_: IOException) {
                                }
                            }
                            logInfo("Loaded Level (${levelFilePath.fileName})")

                            jm.writerWithDefaultPrettyPrinter().writeValue(levelFilePath.toFile().absoluteFile, levelGroup)
                            logInfo("Saved Level (${levelFilePath.fileName})")
                        } catch (e: IOException) {
                            logFault(e)
                        }
                    }
                }
            }
        } catch (e: IOException) {
            logFault(e)
        } finally {
            isLoading = false
        }
    }

    val avatars: Array<Any>
        get() = avatarIDLevelGroupMap.keys.filter { it.isNotEmpty() }.map {
            object {
                val avatarID = it
                val avatarName = DB.getAvatarName(it)
            }
        }.sortedBy { it.avatarName }.toTypedArray()

    fun getLevelNames(avatarID: String): Array<String> {
        return avatarIDLevelNameMap[avatarID]?.sorted()?.toTypedArray() ?: emptyArray()
    }

    fun getLevelGroup(levelName: String): LevelGroup? {
        return levelNameLevelGroupMap[levelName]
    }

    fun getLevelIDDrawing(levelID: String): ByteArray? {
        return levelIDDrawingMap[levelID]
    }

    fun getSatisfiedLevelItems(qwilightWwwLevel: JSON.QwilightWwwLevel): Stream<LevelGroup.LevelItem> {
        return levelNameLevelGroupMap.values.stream().flatMap { Arrays.stream(it.levelItems) }
            .filter {
                it.noteID == qwilightWwwLevel.noteID && it.isSatisfy(
                    qwilightWwwLevel
                )
            }
    }

    fun getLevelItem(levelID: String): LevelGroup.LevelItem? {
        return levelNameLevelGroupMap.values.stream().flatMap { Arrays.stream(it.levelItems) }
            .filter { it.levelID == levelID }.toList().randomOrNull()
    }
}