aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/moe')
-rw-r--r--src/main/kotlin/moe/nea/firmament/commands/rome.kt9
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/EntityDespawnEvent.kt16
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/EntityInteractionEvent.kt34
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt36
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/events/anniversity/AnniversaryFeatures.kt229
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/BarComponent.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/hud/MoulConfigHud.kt5
-rw-r--r--src/main/kotlin/moe/nea/firmament/repo/ItemNameLookup.kt103
-rw-r--r--src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt1
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt2
11 files changed, 436 insertions, 3 deletions
diff --git a/src/main/kotlin/moe/nea/firmament/commands/rome.kt b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
index 74391a6..dc46f1f 100644
--- a/src/main/kotlin/moe/nea/firmament/commands/rome.kt
+++ b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
@@ -9,7 +9,7 @@ package moe.nea.firmament.commands
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType.string
-import io.ktor.client.statement.*
+import io.ktor.client.statement.bodyAsText
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.minecraft.text.Text
import moe.nea.firmament.apis.UrsaManager
@@ -195,6 +195,13 @@ fun firmamentCommand() = literal("firmament") {
FairySouls.TConfig.showConfigEditor()
}
}
+ thenLiteral("simulate") {
+ thenArgument("message", RestArgumentType) { message ->
+ thenExecute {
+ MC.instance.messageHandler.onGameMessage(Text.literal(get(message)), false)
+ }
+ }
+ }
thenLiteral("sbdata") {
thenExecute {
source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.profile", SBData.profileId))
diff --git a/src/main/kotlin/moe/nea/firmament/events/EntityDespawnEvent.kt b/src/main/kotlin/moe/nea/firmament/events/EntityDespawnEvent.kt
new file mode 100644
index 0000000..c744729
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/events/EntityDespawnEvent.kt
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.events
+
+import net.minecraft.entity.Entity
+
+data class EntityDespawnEvent(
+ val entity: Entity?, val entityId: Int,
+ val reason: Entity.RemovalReason,
+) : FirmamentEvent() {
+ companion object: FirmamentEventBus<EntityDespawnEvent>()
+}
diff --git a/src/main/kotlin/moe/nea/firmament/events/EntityInteractionEvent.kt b/src/main/kotlin/moe/nea/firmament/events/EntityInteractionEvent.kt
new file mode 100644
index 0000000..fe16868
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/events/EntityInteractionEvent.kt
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.events
+
+import net.minecraft.entity.Entity
+import net.minecraft.util.Hand
+
+data class EntityInteractionEvent(
+ val kind: InteractionKind,
+ val entity: Entity,
+ val hand: Hand,
+) : FirmamentEvent() {
+ companion object : FirmamentEventBus<EntityInteractionEvent>()
+ enum class InteractionKind {
+ /**
+ * Is sent when left-clicking an entity
+ */
+ ATTACK,
+
+ /**
+ * Is a fallback when [INTERACT_AT_LOCATION] fails
+ */
+ INTERACT,
+
+ /**
+ * Is tried first on right click
+ */
+ INTERACT_AT_LOCATION,
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
index 4c1fde8..f2b2d25 100644
--- a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
@@ -22,6 +22,7 @@ import moe.nea.firmament.features.debug.DeveloperFeatures
import moe.nea.firmament.features.debug.MinorTrolling
import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.features.diana.DianaWaypoints
+import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures
import moe.nea.firmament.features.fixes.CompatibliltyFeatures
import moe.nea.firmament.features.fixes.Fixes
import moe.nea.firmament.features.inventory.CraftingOverlay
@@ -70,6 +71,7 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
loadFeature(ChatLinks)
loadFeature(InventoryButtons)
loadFeature(CompatibliltyFeatures)
+ loadFeature(AnniversaryFeatures)
loadFeature(QuickCommands)
loadFeature(SaveCursorPosition)
loadFeature(CustomSkyBlockTextures)
diff --git a/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt b/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt
index d530487..17e8253 100644
--- a/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt
@@ -10,12 +10,16 @@ package moe.nea.firmament.features.debug
import net.minecraft.block.SkullBlock
import net.minecraft.block.entity.SkullBlockEntity
import net.minecraft.component.DataComponentTypes
+import net.minecraft.entity.Entity
+import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.text.Text
import net.minecraft.util.hit.BlockHitResult
+import net.minecraft.util.hit.EntityHitResult
import net.minecraft.util.hit.HitResult
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.CustomItemModelEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.events.ItemTooltipEvent
@@ -41,6 +45,7 @@ object PowerUserTools : FirmamentFeature {
val copyTexturePackId by keyBindingWithDefaultUnbound("copy-texture-pack-id")
val copyNbtData by keyBindingWithDefaultUnbound("copy-nbt-data")
val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture")
+ val copyEntityData by keyBindingWithDefaultUnbound("entity-data")
}
override val config
@@ -65,6 +70,37 @@ object PowerUserTools : FirmamentFeature {
}
}
+ fun debugFormat(itemStack: ItemStack): Text {
+ return Text.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString())
+ }
+
+ @Subscribe
+ fun onEntityInfo(event: WorldKeyboardEvent) {
+ if (!event.matches(TConfig.copyEntityData)) return
+ val target = (MC.instance.crosshairTarget as? EntityHitResult)?.entity
+ if (target == null) {
+ MC.sendChat(Text.translatable("firmament.poweruser.entity.fail"))
+ return
+ }
+ showEntity(target)
+ }
+
+ fun showEntity(target: Entity) {
+ MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type))
+ MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name))
+ if (target is LivingEntity) {
+ MC.sendChat(Text.translatable("firmament.poweruser.entity.armor"))
+ for (armorItem in target.armorItems) {
+ MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem)))
+ }
+ }
+ MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.passengers", target.passengerList.size))
+ target.passengerList.forEach {
+ showEntity(target)
+ }
+ }
+
+
@Subscribe
fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) {
if (it.screen !is AccessorHandledScreen) return
diff --git a/src/main/kotlin/moe/nea/firmament/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/moe/nea/firmament/features/events/anniversity/AnniversaryFeatures.kt
new file mode 100644
index 0000000..5735e76
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/events/anniversity/AnniversaryFeatures.kt
@@ -0,0 +1,229 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.events.anniversity
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import moe.nea.jarvis.api.Point
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.entity.passive.PigEntity
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.EntityInteractionEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.gui.hud.MoulConfigHud
+import moe.nea.firmament.rei.SBItemEntryDefinition
+import moe.nea.firmament.repo.ItemNameLookup
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.parseShortNumber
+import moe.nea.firmament.util.useMatch
+
+object AnniversaryFeatures : FirmamentFeature {
+ override val identifier: String
+ get() = "anniversary"
+
+ object TConfig : ManagedConfig(identifier) {
+ val enableShinyPigTracker by toggle("shiny-pigs") {true}
+ val trackPigCooldown by position("pig-hud", 200, 300) { Point(0.1, 0.2) }
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+
+ data class ClickedPig(
+ val clickedAt: TimeMark,
+ val startLocation: BlockPos,
+ val pigEntity: PigEntity
+ ) {
+ @Bind("timeLeft")
+ fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration
+ }
+
+ val clickedPigs = ObservableList<ClickedPig>(mutableListOf())
+ var lastClickedPig: PigEntity? = null
+
+ val pigDuration = 90.seconds
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration }
+ }
+
+ val pattern = "SHINY! You extracted (?<reward>.*) from the piglet's orb!".toPattern()
+
+ @Subscribe
+ fun onChat(event: ProcessChatEvent) {
+ if(!TConfig.enableShinyPigTracker)return
+ if (event.unformattedString == "Oink! Bring the pig back to the Shiny Orb!") {
+ val pig = lastClickedPig ?: return
+ // TODO: store proper location based on the orb location, maybe
+ val startLocation = pig.blockPos ?: return
+ clickedPigs.add(ClickedPig(TimeMark.now(), startLocation, pig))
+ lastClickedPig = null
+ }
+ if (event.unformattedString == "SHINY! The orb is charged! Click on it for loot!") {
+ val player = MC.player ?: return
+ val lowest =
+ clickedPigs.minByOrNull { it.startLocation.getSquaredDistance(player.pos) } ?: return
+ clickedPigs.remove(lowest)
+ }
+ pattern.useMatch(event.unformattedString) {
+ val reward = group("reward")
+ val parsedReward = parseReward(reward)
+ addReward(parsedReward)
+ PigCooldown.rewards.atOnce {
+ PigCooldown.rewards.clear()
+ rewards.mapTo(PigCooldown.rewards) { PigCooldown.DisplayReward(it) }
+ }
+ }
+ }
+
+ fun addReward(reward: Reward) {
+ val it = rewards.listIterator()
+ while (it.hasNext()) {
+ val merged = reward.mergeWith(it.next()) ?: continue
+ it.set(merged)
+ return
+ }
+ rewards.add(reward)
+ }
+
+ val rewards = mutableListOf<Reward>()
+
+ fun <T> ObservableList<T>.atOnce(block: () -> Unit) {
+ val oldObserver = observer
+ observer = null
+ block()
+ observer = oldObserver
+ update()
+ }
+
+ sealed interface Reward {
+ fun mergeWith(other: Reward): Reward?
+ data class EXP(val amount: Double, val skill: String) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is EXP && other.skill == skill)
+ return EXP(amount + other.amount, skill)
+ return null
+ }
+ }
+
+ data class Coins(val amount: Double) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is Coins)
+ return Coins(other.amount + amount)
+ return null
+ }
+ }
+
+ data class Items(val amount: Int, val item: SkyblockId) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is Items && other.item == item)
+ return Items(amount + other.amount, item)
+ return null
+ }
+ }
+
+ data class Unknown(val text: String) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ return null
+ }
+ }
+ }
+
+ val expReward = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern()
+ val coinReward = "\\+(?<amount>$SHORT_NUMBER_FORMAT) coins".toPattern()
+ val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern()
+ fun parseReward(string: String): Reward {
+ expReward.useMatch<Unit>(string) {
+ val exp = parseShortNumber(group("exp"))
+ val kind = group("kind")
+ return Reward.EXP(exp, kind)
+ }
+ coinReward.useMatch<Unit>(string) {
+ val coins = parseShortNumber(group("amount"))
+ return Reward.Coins(coins)
+ }
+ itemReward.useMatch(string) {
+ val amount = group("amount")?.toIntOrNull() ?: 1
+ val name = group("name")
+ val item = ItemNameLookup.guessItemByName(name, false) ?: return@useMatch
+ return Reward.Items(amount, item)
+ }
+ return Reward.Unknown(string)
+ }
+
+ @Subscribe
+ fun onWorldClear(event: WorldReadyEvent) {
+ lastClickedPig = null
+ clickedPigs.clear()
+ }
+
+ @Subscribe
+ fun onEntityClick(event: EntityInteractionEvent) {
+ if (event.entity is PigEntity) {
+ lastClickedPig = event.entity
+ }
+ }
+
+ @Subscribe
+ fun init(event: WorldReadyEvent) {
+ PigCooldown.forceInit()
+ }
+
+ object PigCooldown : MoulConfigHud("anniversary_pig", TConfig.trackPigCooldown) {
+ override fun shouldRender(): Boolean {
+ return clickedPigs.isNotEmpty() && TConfig.enableShinyPigTracker
+ }
+
+ @Bind("pigs")
+ fun getPigs() = clickedPigs
+
+ class DisplayReward(val backedBy: Reward) {
+ @Bind
+ fun count(): String {
+ return when (backedBy) {
+ is Reward.Coins -> backedBy.amount
+ is Reward.EXP -> backedBy.amount
+ is Reward.Items -> backedBy.amount
+ is Reward.Unknown -> 0
+ }.toString()
+ }
+
+ val itemStack = if (backedBy is Reward.Items) {
+ SBItemEntryDefinition.getEntry(backedBy.item, backedBy.amount)
+ } else {
+ SBItemEntryDefinition.getEntry(SkyblockId.NULL)
+ }
+
+ @Bind
+ fun name(): String {
+ return when (backedBy) {
+ is Reward.Coins -> "Coins"
+ is Reward.EXP -> backedBy.skill
+ is Reward.Items -> itemStack.value.asItemStack().name.string
+ is Reward.Unknown -> backedBy.text
+ }
+ }
+
+ @Bind
+ fun isKnown() = backedBy !is Reward.Unknown
+ }
+
+ @get:Bind("rewards")
+ val rewards = ObservableList<DisplayReward>(mutableListOf())
+
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/BarComponent.kt b/src/main/kotlin/moe/nea/firmament/gui/BarComponent.kt
index a1eb23c..91b5735 100644
--- a/src/main/kotlin/moe/nea/firmament/gui/BarComponent.kt
+++ b/src/main/kotlin/moe/nea/firmament/gui/BarComponent.kt
@@ -58,7 +58,7 @@ class BarComponent(
ScreenDrawing.texturedRect(context, x, y, 4, 8, texture, emptyColor.color)
return
}
- val increasePerPixel = (sectionEnd - sectionStart / 4)
+ val increasePerPixel = (sectionEnd - sectionStart) / width
var valueAtPixel = sectionStart
for (i in (0 until width)) {
ScreenDrawing.texturedRect(
diff --git a/src/main/kotlin/moe/nea/firmament/gui/hud/MoulConfigHud.kt b/src/main/kotlin/moe/nea/firmament/gui/hud/MoulConfigHud.kt
index ec884bb..afc8740 100644
--- a/src/main/kotlin/moe/nea/firmament/gui/hud/MoulConfigHud.kt
+++ b/src/main/kotlin/moe/nea/firmament/gui/hud/MoulConfigHud.kt
@@ -30,11 +30,16 @@ abstract class MoulConfigHud(
private var fragment: GuiContext? = null
+ fun forceInit() {
+
+ }
+
open fun shouldRender(): Boolean {
return true
}
init {
+ require(name.matches("^[a-z_/]+$".toRegex()))
HudRenderEvent.subscribe {
if (!shouldRender()) return@subscribe
val renderContext = componentWrapper.createContext(it.context)
diff --git a/src/main/kotlin/moe/nea/firmament/repo/ItemNameLookup.kt b/src/main/kotlin/moe/nea/firmament/repo/ItemNameLookup.kt
new file mode 100644
index 0000000..4bdf616
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/repo/ItemNameLookup.kt
@@ -0,0 +1,103 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUItem
+import java.util.NavigableMap
+import java.util.TreeMap
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.removeColorCodes
+import moe.nea.firmament.util.skyblockId
+
+object ItemNameLookup : IReloadable {
+
+ fun getItemNameChunks(name: String): Set<String> {
+ return name.removeColorCodes().split(" ").filterTo(mutableSetOf()) { it.isNotBlank() }
+ }
+
+ var nameMap: NavigableMap<String, out Set<SkyblockId>> = TreeMap()
+
+ override fun reload(repository: NEURepository) {
+ val nameMap = TreeMap<String, MutableSet<SkyblockId>>()
+ repository.items.items.values.forEach { item ->
+ getAllNamesForItem(item).forEach { name ->
+ val chunks = getItemNameChunks(name)
+ chunks.forEach { chunk ->
+ val set = nameMap.getOrPut(chunk, ::mutableSetOf)
+ set.add(item.skyblockId)
+ }
+ }
+ }
+ this.nameMap = nameMap
+ }
+
+ fun getAllNamesForItem(item: NEUItem): Set<String> {
+ val names = mutableSetOf<String>()
+ names.add(item.displayName)
+ if (item.displayName.contains("Enchanted Book")) {
+ val enchantName = item.lore.firstOrNull()
+ if (enchantName != null) {
+ names.add(enchantName)
+ }
+ }
+ return names
+ }
+
+ fun findItemCandidatesByName(name: String): MutableSet<SkyblockId> {
+ val candidates = mutableSetOf<SkyblockId>()
+ for (chunk in getItemNameChunks(name)) {
+ val set = nameMap[chunk] ?: emptySet()
+ candidates.addAll(set)
+ }
+ return candidates
+ }
+
+
+ fun guessItemByName(
+ /**
+ * The display name of the item. Color codes will be ignored.
+ */
+ name: String,
+ /**
+ * Whether the [name] may contain other text, such as reforges, master stars and such.
+ */
+ mayBeMangled: Boolean
+ ): SkyblockId? {
+ val cleanName = name.removeColorCodes()
+ return findBestItemFromCandidates(
+ findItemCandidatesByName(cleanName),
+ cleanName,
+ true
+ )
+ }
+
+ fun findBestItemFromCandidates(
+ candidates: Iterable<SkyblockId>,
+ name: String, mayBeMangled: Boolean
+ ): SkyblockId? {
+ val expectedClean = name.removeColorCodes()
+ var bestMatch: SkyblockId? = null
+ var bestMatchLength = -1
+ for (candidate in candidates) {
+ val item = RepoManager.getNEUItem(candidate) ?: continue
+ for (name in getAllNamesForItem(item)) {
+ val actualClean = name.removeColorCodes()
+ val matches = if (mayBeMangled) expectedClean == actualClean
+ else expectedClean.contains(actualClean)
+ if (!matches) continue
+ if (actualClean.length > bestMatchLength) {
+ bestMatch = candidate
+ bestMatchLength = actualClean.length
+ }
+ }
+ }
+ return bestMatch
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt b/src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt
index a1e73b1..db17e6a 100644
--- a/src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt
+++ b/src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt
@@ -63,6 +63,7 @@ object RepoManager {
val neuRepo: NEURepository = NEURepository.of(RepoDownloadManager.repoSavedLocation).apply {
registerReloadListener(ItemCache)
registerReloadListener(ExpLadders)
+ registerReloadListener(ItemNameLookup)
registerReloadListener {
Firmament.coroutineScope.launch(MinecraftDispatcher) {
if (!trySendClientboundUpdateRecipesPacket()) {
diff --git a/src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt b/src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt
index 0b56c7c..4d3221f 100644
--- a/src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt
@@ -18,7 +18,7 @@ import org.w3c.dom.Element
import moe.nea.firmament.gui.BarComponent
object MoulConfigUtils {
- val firmUrl = "http://nea.moe/Firmament"
+ val firmUrl = "http://firmament.nea.moe/moulconfig"
val universe = XMLUniverse.getDefaultUniverse().also { uni ->
uni.registerMapper(java.awt.Color::class.java) {
if (it.startsWith("#")) {