Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
This commit is contained in:
194
src/main/kotlin/apis/Profiles.kt
Normal file
194
src/main/kotlin/apis/Profiles.kt
Normal file
@@ -0,0 +1,194 @@
|
||||
|
||||
|
||||
@file:UseSerializers(DashlessUUIDSerializer::class, InstantAsLongSerializer::class)
|
||||
|
||||
package moe.nea.firmament.apis
|
||||
|
||||
import io.github.moulberry.repo.constants.Leveling
|
||||
import io.github.moulberry.repo.data.Rarity
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import moe.nea.firmament.repo.RepoManager
|
||||
import moe.nea.firmament.util.LegacyFormattingCode
|
||||
import moe.nea.firmament.util.SkyblockId
|
||||
import moe.nea.firmament.util.assertNotNullOr
|
||||
import moe.nea.firmament.util.json.DashlessUUIDSerializer
|
||||
import moe.nea.firmament.util.json.InstantAsLongSerializer
|
||||
import net.minecraft.util.DyeColor
|
||||
import net.minecraft.util.Formatting
|
||||
import java.util.*
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
|
||||
@Serializable
|
||||
data class CollectionSkillData(
|
||||
val items: Map<CollectionType, CollectionInfo>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CollectionResponse(
|
||||
val success: Boolean,
|
||||
val collections: Map<String, CollectionSkillData>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CollectionInfo(
|
||||
val name: String,
|
||||
val maxTiers: Int,
|
||||
val tiers: List<CollectionTier>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CollectionTier(
|
||||
val tier: Int,
|
||||
val amountRequired: Long,
|
||||
val unlocks: List<String>,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class Profiles(
|
||||
val success: Boolean,
|
||||
val profiles: List<Profile>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Profile(
|
||||
@SerialName("profile_id")
|
||||
val profileId: UUID,
|
||||
@SerialName("cute_name")
|
||||
val cuteName: String,
|
||||
val selected: Boolean = false,
|
||||
val members: Map<UUID, Member>,
|
||||
)
|
||||
|
||||
enum class Skill(val accessor: KProperty1<Member, Double>, val color: DyeColor, val icon: SkyblockId) {
|
||||
FARMING(Member::experienceSkillFarming, DyeColor.YELLOW, SkyblockId("ROOKIE_HOE")),
|
||||
FORAGING(Member::experienceSkillForaging, DyeColor.BROWN, SkyblockId("TREECAPITATOR_AXE")),
|
||||
MINING(Member::experienceSkillMining, DyeColor.LIGHT_GRAY, SkyblockId("DIAMOND_PICKAXE")),
|
||||
ALCHEMY(Member::experienceSkillAlchemy, DyeColor.PURPLE, SkyblockId("BREWING_STAND")),
|
||||
TAMING(Member::experienceSkillTaming, DyeColor.GREEN, SkyblockId("SUPER_EGG")),
|
||||
FISHING(Member::experienceSkillFishing, DyeColor.BLUE, SkyblockId("FARMER_ROD")),
|
||||
RUNECRAFTING(Member::experienceSkillRunecrafting, DyeColor.PINK, SkyblockId("MUSIC_RUNE;1")),
|
||||
CARPENTRY(Member::experienceSkillCarpentry, DyeColor.ORANGE, SkyblockId("WORKBENCH")),
|
||||
COMBAT(Member::experienceSkillCombat, DyeColor.RED, SkyblockId("UNDEAD_SWORD")),
|
||||
SOCIAL(Member::experienceSkillSocial, DyeColor.WHITE, SkyblockId("EGG_HUNT")),
|
||||
ENCHANTING(Member::experienceSkillEnchanting, DyeColor.MAGENTA, SkyblockId("ENCHANTMENT_TABLE")),
|
||||
;
|
||||
|
||||
fun getMaximumLevel(leveling: Leveling) = assertNotNullOr(leveling.maximumLevels[name.lowercase()]) { 50 }
|
||||
|
||||
fun getLadder(leveling: Leveling): List<Int> {
|
||||
if (this == SOCIAL) return leveling.socialExperienceRequiredPerLevel
|
||||
if (this == RUNECRAFTING) return leveling.runecraftingExperienceRequiredPerLevel
|
||||
return leveling.skillExperienceRequiredPerLevel
|
||||
}
|
||||
}
|
||||
|
||||
enum class CollectionCategory(val skill: Skill?, val color: DyeColor, val icon: SkyblockId) {
|
||||
FARMING(Skill.FARMING, DyeColor.YELLOW, SkyblockId("ROOKIE_HOE")),
|
||||
FORAGING(Skill.FORAGING, DyeColor.BROWN, SkyblockId("TREECAPITATOR_AXE")),
|
||||
MINING(Skill.MINING, DyeColor.LIGHT_GRAY, SkyblockId("DIAMOND_PICKAXE")),
|
||||
FISHING(Skill.FISHING, DyeColor.BLUE, SkyblockId("FARMER_ROD")),
|
||||
COMBAT(Skill.COMBAT, DyeColor.RED, SkyblockId("UNDEAD_SWORD")),
|
||||
RIFT(null, DyeColor.PURPLE, SkyblockId("SKYBLOCK_MOTE")),
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class CollectionType(val string: String) {
|
||||
val skyblockId get() = SkyblockId(string.replace(":", "-").replace("MUSHROOM_COLLECTION", "HUGE_MUSHROOM_2"))
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Member(
|
||||
val pets: List<Pet> = listOf(),
|
||||
@SerialName("coop_invitation")
|
||||
val coopInvitation: CoopInvitation? = null,
|
||||
@SerialName("experience_skill_farming")
|
||||
val experienceSkillFarming: Double = 0.0,
|
||||
@SerialName("experience_skill_alchemy")
|
||||
val experienceSkillAlchemy: Double = 0.0,
|
||||
@SerialName("experience_skill_combat")
|
||||
val experienceSkillCombat: Double = 0.0,
|
||||
@SerialName("experience_skill_taming")
|
||||
val experienceSkillTaming: Double = 0.0,
|
||||
@SerialName("experience_skill_social2")
|
||||
val experienceSkillSocial: Double = 0.0,
|
||||
@SerialName("experience_skill_enchanting")
|
||||
val experienceSkillEnchanting: Double = 0.0,
|
||||
@SerialName("experience_skill_fishing")
|
||||
val experienceSkillFishing: Double = 0.0,
|
||||
@SerialName("experience_skill_foraging")
|
||||
val experienceSkillForaging: Double = 0.0,
|
||||
@SerialName("experience_skill_mining")
|
||||
val experienceSkillMining: Double = 0.0,
|
||||
@SerialName("experience_skill_runecrafting")
|
||||
val experienceSkillRunecrafting: Double = 0.0,
|
||||
@SerialName("experience_skill_carpentry")
|
||||
val experienceSkillCarpentry: Double = 0.0,
|
||||
val collection: Map<CollectionType, Long> = mapOf()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CoopInvitation(
|
||||
val timestamp: Instant,
|
||||
@SerialName("invited_by")
|
||||
val invitedBy: UUID? = null,
|
||||
val confirmed: Boolean,
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
@Serializable
|
||||
value class PetType(val name: String)
|
||||
|
||||
@Serializable
|
||||
data class Pet(
|
||||
val uuid: UUID? = null,
|
||||
val type: PetType,
|
||||
val exp: Double = 0.0,
|
||||
val active: Boolean = false,
|
||||
val tier: Rarity,
|
||||
val candyUsed: Int = 0,
|
||||
val heldItem: String? = null,
|
||||
val skin: String? = null,
|
||||
) {
|
||||
val itemId get() = SkyblockId("${type.name};${tier.ordinal}")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PlayerResponse(
|
||||
val success: Boolean,
|
||||
val player: PlayerData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PlayerData(
|
||||
val uuid: UUID,
|
||||
val firstLogin: Instant,
|
||||
val lastLogin: Instant? = null,
|
||||
@SerialName("playername")
|
||||
val playerName: String,
|
||||
val achievementsOneTime: List<String> = listOf(),
|
||||
@SerialName("newPackageRank")
|
||||
val packageRank: String? = null,
|
||||
val monthlyPackageRank: String? = null,
|
||||
val rankPlusColor: String = "GOLD"
|
||||
) {
|
||||
val rankPlusDyeColor = LegacyFormattingCode.values().find { it.name == rankPlusColor } ?: LegacyFormattingCode.GOLD
|
||||
val rankData get() = RepoManager.neuRepo.constants.misc.ranks[if (monthlyPackageRank == "NONE" || monthlyPackageRank == null) packageRank else monthlyPackageRank]
|
||||
fun getDisplayName(name: String = playerName) = rankData?.let {
|
||||
("§${it.color}[${it.tag}${rankPlusDyeColor.modern}" +
|
||||
"${it.plus ?: ""}§${it.color}] $name")
|
||||
} ?: "${Formatting.GRAY}$name"
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AshconNameLookup(
|
||||
val username: String,
|
||||
val uuid: UUID,
|
||||
)
|
||||
95
src/main/kotlin/apis/Routes.kt
Normal file
95
src/main/kotlin/apis/Routes.kt
Normal file
@@ -0,0 +1,95 @@
|
||||
|
||||
|
||||
package moe.nea.firmament.apis
|
||||
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.util.*
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.collections.MutableMap
|
||||
import kotlin.collections.listOf
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.set
|
||||
import moe.nea.firmament.Firmament
|
||||
import moe.nea.firmament.util.MinecraftDispatcher
|
||||
|
||||
object Routes {
|
||||
private val nameToUUID: MutableMap<String, Deferred<UUID?>> = CaseInsensitiveMap()
|
||||
private val profiles: MutableMap<UUID, Deferred<Profiles?>> = mutableMapOf()
|
||||
private val accounts: MutableMap<UUID, Deferred<PlayerData?>> = mutableMapOf()
|
||||
private val UUIDToName: MutableMap<UUID, Deferred<String?>> = mutableMapOf()
|
||||
|
||||
suspend fun getPlayerNameForUUID(uuid: UUID): String? {
|
||||
return withContext(MinecraftDispatcher) {
|
||||
UUIDToName.computeIfAbsent(uuid) {
|
||||
async(Firmament.coroutineScope.coroutineContext) {
|
||||
val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$uuid")
|
||||
if (!response.status.isSuccess()) return@async null
|
||||
val data = response.body<AshconNameLookup>()
|
||||
launch(MinecraftDispatcher) {
|
||||
nameToUUID[data.username] = async { data.uuid }
|
||||
}
|
||||
data.username
|
||||
}
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
|
||||
suspend fun getUUIDForPlayerName(name: String): UUID? {
|
||||
return withContext(MinecraftDispatcher) {
|
||||
nameToUUID.computeIfAbsent(name) {
|
||||
async(Firmament.coroutineScope.coroutineContext) {
|
||||
val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$name")
|
||||
if (!response.status.isSuccess()) return@async null
|
||||
val data = response.body<AshconNameLookup>()
|
||||
launch(MinecraftDispatcher) {
|
||||
UUIDToName[data.uuid] = async { data.username }
|
||||
}
|
||||
data.uuid
|
||||
}
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
|
||||
suspend fun getAccountData(uuid: UUID): PlayerData? {
|
||||
return withContext(MinecraftDispatcher) {
|
||||
accounts.computeIfAbsent(uuid) {
|
||||
async(Firmament.coroutineScope.coroutineContext) {
|
||||
val response = UrsaManager.request(listOf("v1", "hypixel","player", uuid.toString()))
|
||||
if (!response.status.isSuccess()) {
|
||||
launch(MinecraftDispatcher) {
|
||||
@Suppress("DeferredResultUnused")
|
||||
accounts.remove(uuid)
|
||||
}
|
||||
return@async null
|
||||
}
|
||||
response.body<PlayerResponse>().player
|
||||
}
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
|
||||
suspend fun getProfiles(uuid: UUID): Profiles? {
|
||||
return withContext(MinecraftDispatcher) {
|
||||
profiles.computeIfAbsent(uuid) {
|
||||
async(Firmament.coroutineScope.coroutineContext) {
|
||||
val response = UrsaManager.request(listOf("v1", "hypixel","profiles", uuid.toString()))
|
||||
if (!response.status.isSuccess()) {
|
||||
launch(MinecraftDispatcher) {
|
||||
@Suppress("DeferredResultUnused")
|
||||
profiles.remove(uuid)
|
||||
}
|
||||
return@async null
|
||||
}
|
||||
response.body<Profiles>()
|
||||
}
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
|
||||
}
|
||||
72
src/main/kotlin/apis/UrsaManager.kt
Normal file
72
src/main/kotlin/apis/UrsaManager.kt
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
|
||||
package moe.nea.firmament.apis
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withContext
|
||||
import moe.nea.firmament.Firmament
|
||||
import net.minecraft.client.MinecraftClient
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
object UrsaManager {
|
||||
private data class Token(
|
||||
val validUntil: Instant,
|
||||
val token: String,
|
||||
val obtainedFrom: String,
|
||||
) {
|
||||
fun isValid(host: String) = Instant.now().plusSeconds(60) < validUntil && obtainedFrom == host
|
||||
}
|
||||
|
||||
private var currentToken: Token? = null
|
||||
private val lock = Mutex()
|
||||
private fun getToken(host: String) = currentToken?.takeIf { it.isValid(host) }
|
||||
|
||||
suspend fun request(path: List<String>): HttpResponse {
|
||||
var didLock = false
|
||||
try {
|
||||
val host = "ursa.notenoughupdates.org"
|
||||
var token = getToken(host)
|
||||
if (token == null) {
|
||||
lock.lock()
|
||||
didLock = true
|
||||
token = getToken(host)
|
||||
}
|
||||
val response = Firmament.httpClient.get {
|
||||
url {
|
||||
this.host = host
|
||||
appendPathSegments(path, encodeSlash = true)
|
||||
}
|
||||
if (token == null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val mc = MinecraftClient.getInstance()
|
||||
val serverId = UUID.randomUUID().toString()
|
||||
mc.sessionService.joinServer(mc.session.uuidOrNull, mc.session.accessToken, serverId)
|
||||
header("x-ursa-username", mc.session.username)
|
||||
header("x-ursa-serverid", serverId)
|
||||
}
|
||||
} else {
|
||||
header("x-ursa-token", token.token)
|
||||
}
|
||||
}
|
||||
val savedToken = response.headers["x-ursa-token"]
|
||||
if (savedToken != null) {
|
||||
val validUntil = response.headers["x-ursa-expires"]?.toLongOrNull()?.let { Instant.ofEpochMilli(it) }
|
||||
?: (Instant.now() + Duration.ofMinutes(55))
|
||||
currentToken = Token(validUntil, savedToken, host)
|
||||
}
|
||||
if (response.status.value != 200) {
|
||||
Firmament.logger.error("Failed to contact ursa minor: ${response.bodyAsText()}")
|
||||
}
|
||||
return response
|
||||
} finally {
|
||||
if (didLock)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user