aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornea <nea@nea.moe>2023-05-29 23:28:52 +0200
committernea <nea@nea.moe>2023-05-29 23:28:52 +0200
commitdd0afac3a8459035827dac36986a690c8a9541ea (patch)
tree2bc3ab50b69590a6811a8265f4c533163d07457c
parentc133505e3b1f8e5aeb62c1a0e99513ec4bfcf239 (diff)
downloadfirmament-dd0afac3a8459035827dac36986a690c8a9541ea.tar.gz
firmament-dd0afac3a8459035827dac36986a690c8a9541ea.tar.bz2
firmament-dd0afac3a8459035827dac36986a690c8a9541ea.zip
Add price checker
-rw-r--r--build.gradle.kts2
-rw-r--r--gradle/libs.versions.toml2
-rw-r--r--src/main/kotlin/moe/nea/firmament/Firmament.kt39
-rw-r--r--src/main/kotlin/moe/nea/firmament/commands/dsl.kt12
-rw-r--r--src/main/kotlin/moe/nea/firmament/commands/rome.kt26
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/HandledScreenKeyPressedEvent.kt8
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/TooltipEvent.kt15
-rw-r--r--src/main/kotlin/moe/nea/firmament/keybindings/IKeyBinding.kt27
-rw-r--r--src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/repo/ItemCostData.kt86
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt25
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/async/input.kt45
-rw-r--r--src/main/resources/assets/firmament/lang/en_us.json25
13 files changed, 285 insertions, 29 deletions
diff --git a/build.gradle.kts b/build.gradle.kts
index 0c92701..67cba5b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -100,6 +100,8 @@ dependencies {
transInclude(nonModImplentation(ktor("client-java"))!!)
transInclude(nonModImplentation(ktor("serialization-kotlinx-json"))!!)
transInclude(nonModImplentation(ktor("client-content-negotiation"))!!)
+ transInclude(nonModImplentation(ktor("client-encoding"))!!)
+ transInclude(nonModImplentation(ktor("client-logging"))!!)
// Dev environment preinstalled mods
modRuntimeOnly(libs.bundles.runtime.required)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index cda6e74..cb8975e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,7 +11,7 @@ modmenu = "6.2.1"
ktor = "2.3.0"
dbus_java = "4.2.1"
architectury = "8.1.79"
-neurepoparser = "0.0.1"
+neurepoparser = "1.1.0"
qolify = "1.2.2-1.19.4"
citresewn = "1.1.3+1.19.4"
ncr = "Fabric-1.19.4-v2.1.1"
diff --git a/src/main/kotlin/moe/nea/firmament/Firmament.kt b/src/main/kotlin/moe/nea/firmament/Firmament.kt
index 9786361..b3e2115 100644
--- a/src/main/kotlin/moe/nea/firmament/Firmament.kt
+++ b/src/main/kotlin/moe/nea/firmament/Firmament.kt
@@ -19,11 +19,14 @@
package moe.nea.firmament
import com.mojang.brigadier.CommandDispatcher
-import io.ktor.client.*
-import io.ktor.client.plugins.*
-import io.ktor.client.plugins.contentnegotiation.*
-import io.ktor.serialization.kotlinx.json.*
-import java.awt.Taskbar.Feature
+import io.ktor.client.HttpClient
+import io.ktor.client.plugins.UserAgent
+import io.ktor.client.plugins.cache.HttpCache
+import io.ktor.client.plugins.compression.ContentEncoding
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.serialization.kotlinx.json.json
import java.nio.file.Files
import java.nio.file.Path
import net.fabricmc.api.ClientModInitializer
@@ -35,14 +38,21 @@ import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.loader.api.Version
import net.fabricmc.loader.api.metadata.ModMetadata
import org.apache.logging.log4j.LogManager
+import org.apache.logging.log4j.Logger
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.plus
+import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlin.coroutines.EmptyCoroutineContext
import net.minecraft.command.CommandRegistryAccess
import moe.nea.firmament.commands.registerFirmamentCommand
import moe.nea.firmament.dbus.FirmamentDbusObject
import moe.nea.firmament.features.FeatureManager
+import moe.nea.firmament.repo.ItemCostData
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.data.IDataHolder
@@ -53,8 +63,10 @@ object Firmament : ModInitializer, ClientModInitializer {
val DEBUG = System.getProperty("firmament.debug") == "true"
val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) }
val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) }
- val logger = LogManager.getLogger("Firmament")
- val metadata: ModMetadata by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata }
+ val logger: Logger = LogManager.getLogger("Firmament")
+ private val metadata: ModMetadata by lazy {
+ FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata
+ }
val version: Version by lazy { metadata.version }
val json = Json {
@@ -68,9 +80,18 @@ object Firmament : ModInitializer, ClientModInitializer {
install(ContentNegotiation) {
json(json)
}
+ install(ContentEncoding) {
+ gzip()
+ deflate()
+ }
install(UserAgent) {
agent = "Firmament/$version"
}
+ if (DEBUG)
+ install(Logging) {
+ level = LogLevel.INFO
+ }
+ install(HttpCache)
}
}
@@ -98,7 +119,7 @@ object Firmament : ModInitializer, ClientModInitializer {
RepoManager.initialize()
SBData.init()
FeatureManager.autoload()
-
+ ItemCostData.spawnPriceLoop()
ClientCommandRegistrationCallback.EVENT.register(this::registerCommands)
ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping {
runBlocking {
diff --git a/src/main/kotlin/moe/nea/firmament/commands/dsl.kt b/src/main/kotlin/moe/nea/firmament/commands/dsl.kt
index e2b9e8a..155e700 100644
--- a/src/main/kotlin/moe/nea/firmament/commands/dsl.kt
+++ b/src/main/kotlin/moe/nea/firmament/commands/dsl.kt
@@ -23,6 +23,7 @@ import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.suggestion.SuggestionProvider
import java.lang.reflect.ParameterizedType
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import moe.nea.firmament.util.iterate
@@ -80,6 +81,17 @@ fun <T : ArgumentBuilder<DefaultSource, T>, AT : Any> T.thenArgument(
block: RequiredArgumentBuilder<DefaultSource, AT>.(TypeSafeArg<AT>) -> Unit
): T = then(argument(name, argument, block))
+fun <T : RequiredArgumentBuilder<DefaultSource, String>> T.suggestsList(provider: () -> Iterable<String>) {
+ suggests(SuggestionProvider<DefaultSource> { context, builder ->
+ provider()
+ .asSequence()
+ .filter { it.startsWith(builder.remaining, ignoreCase = true) }
+ .forEach {
+ builder.suggest(it)
+ }
+ builder.buildFuture()
+ })
+}
fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteral(
name: String,
diff --git a/src/main/kotlin/moe/nea/firmament/commands/rome.kt b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
index 55dce75..02b12d9 100644
--- a/src/main/kotlin/moe/nea/firmament/commands/rome.kt
+++ b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
@@ -19,12 +19,16 @@
package moe.nea.firmament.commands
import com.mojang.brigadier.CommandDispatcher
+import com.mojang.brigadier.arguments.StringArgumentType.getString
+import com.mojang.brigadier.arguments.StringArgumentType.string
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.minecraft.text.Text
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.gui.config.AllConfigsGui
+import moe.nea.firmament.repo.ItemCostData
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyblockId
fun firmamentCommand() = literal("firmament") {
@@ -47,6 +51,28 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
+ thenLiteral("price") {
+ thenArgument("item", string()) { item ->
+ suggestsList { RepoManager.neuRepo.items.items.keys }
+ thenExecute {
+ val itemName = SkyblockId(getString(context, "item"))
+ source.sendFeedback(Text.translatable("firmament.price", itemName.neuItem))
+ val bazaarData = ItemCostData.bazaarData[itemName]
+ if (bazaarData != null) {
+ source.sendFeedback(Text.translatable("firmament.price.bazaar"))
+ source.sendFeedback(Text.translatable("firmament.price.bazaar.productid", bazaarData.productId.bazaarId))
+ source.sendFeedback(Text.translatable("firmament.price.bazaar.buy.price", bazaarData.quickStatus.buyPrice))
+ source.sendFeedback(Text.translatable("firmament.price.bazaar.buy.order", bazaarData.quickStatus.buyOrders))
+ source.sendFeedback(Text.translatable("firmament.price.bazaar.sell.price", bazaarData.quickStatus.sellPrice))
+ source.sendFeedback(Text.translatable("firmament.price.bazaar.sell.order", bazaarData.quickStatus.sellOrders))
+ }
+ val lowestBin = ItemCostData.lowestBin[itemName]
+ if (lowestBin != null) {
+ source.sendFeedback(Text.translatable("firmament.price.lowestbin", lowestBin))
+ }
+ }
+ }
+ }
thenLiteral("dev") {
thenLiteral("config") {
thenExecute {
diff --git a/src/main/kotlin/moe/nea/firmament/events/HandledScreenKeyPressedEvent.kt b/src/main/kotlin/moe/nea/firmament/events/HandledScreenKeyPressedEvent.kt
index 8d19f7e..3c19aa7 100644
--- a/src/main/kotlin/moe/nea/firmament/events/HandledScreenKeyPressedEvent.kt
+++ b/src/main/kotlin/moe/nea/firmament/events/HandledScreenKeyPressedEvent.kt
@@ -19,10 +19,16 @@
package moe.nea.firmament.events
import net.minecraft.client.option.KeyBinding
+import moe.nea.firmament.keybindings.IKeyBinding
data class HandledScreenKeyPressedEvent(val keyCode: Int, val scanCode: Int, val modifiers: Int) : FirmamentEvent.Cancellable() {
companion object : FirmamentEventBus<HandledScreenKeyPressedEvent>()
+
fun matches(keyBinding: KeyBinding): Boolean {
- return keyBinding.matchesKey(keyCode, scanCode)
+ return matches(IKeyBinding.minecraft(keyBinding))
+ }
+
+ fun matches(keyBinding: IKeyBinding): Boolean {
+ return keyBinding.matches(keyCode, scanCode, modifiers)
}
}
diff --git a/src/main/kotlin/moe/nea/firmament/events/TooltipEvent.kt b/src/main/kotlin/moe/nea/firmament/events/TooltipEvent.kt
new file mode 100644
index 0000000..3341534
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/events/TooltipEvent.kt
@@ -0,0 +1,15 @@
+package moe.nea.firmament.events
+
+import net.minecraft.client.gui.tooltip.Tooltip
+import net.minecraft.client.item.TooltipContext
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.item.ItemStack
+
+data class TooltipEvent(
+ val itemStack: ItemStack,
+ val tooltip: Tooltip,
+ val tooltipContext: TooltipContext,
+ val player: PlayerEntity?
+) : FirmamentEvent() {
+ companion object : FirmamentEventBus<TooltipEvent>()
+}
diff --git a/src/main/kotlin/moe/nea/firmament/keybindings/IKeyBinding.kt b/src/main/kotlin/moe/nea/firmament/keybindings/IKeyBinding.kt
new file mode 100644
index 0000000..7cb2720
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/keybindings/IKeyBinding.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.keybindings
+
+import net.minecraft.client.option.KeyBinding
+
+interface IKeyBinding {
+ fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean
+
+ fun withModifiers(wantedModifiers: Int): IKeyBinding {
+ val old = this
+ return object : IKeyBinding {
+ override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ return old.matches(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers
+ }
+ }
+ }
+
+ companion object {
+ fun minecraft(keyBinding: KeyBinding) = object : IKeyBinding {
+ override fun matches(keyCode: Int, scanCode: Int, modifiers: Int) =
+ keyBinding.matchesKey(keyCode, scanCode)
+ }
+
+ fun ofKeyCode(wantedKeyCode: Int) = object : IKeyBinding {
+ override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt b/src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt
index 4420a24..a6d683d 100644
--- a/src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt
+++ b/src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt
@@ -106,7 +106,7 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
getEntry(SBItemStack(skyblockId, RepoManager.getNEUItem(skyblockId), count))
fun getEntry(ingredient: NEUIngredient): EntryStack<SBItemStack> =
- getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount)
+ getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount.toInt())
}
diff --git a/src/main/kotlin/moe/nea/firmament/repo/ItemCostData.kt b/src/main/kotlin/moe/nea/firmament/repo/ItemCostData.kt
new file mode 100644
index 0000000..a1084b9
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/repo/ItemCostData.kt
@@ -0,0 +1,86 @@
+package moe.nea.firmament.repo
+
+import io.ktor.client.call.body
+import io.ktor.client.request.get
+import org.apache.logging.log4j.LogManager
+import org.lwjgl.glfw.GLFW
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeoutOrNull
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlin.time.Duration.Companion.minutes
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.keybindings.IKeyBinding
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.async.waitForInput
+
+object ItemCostData {
+ private val logger = LogManager.getLogger("Firmament.ItemCostData")
+ private val moulberryBaseUrl = "https://moulberry.codes"
+ private val hypixelApiBaseUrl = "https://api.hypixel.net"
+ var lowestBin: Map<SkyblockId, Double> = mapOf()
+ private set
+ var bazaarData: Map<SkyblockId, BazaarData> = mapOf()
+ private set
+
+ @Serializable
+ data class BazaarData(
+ @SerialName("product_id")
+ val productId: SkyblockId.BazaarStock,
+ @SerialName("quick_status")
+ val quickStatus: BazaarStatus,
+ )
+
+ @Serializable
+ data class BazaarStatus(
+ val sellPrice: Double,
+ val sellVolume: Long,
+ val sellMovingWeek: Long,
+ val sellOrders: Long,
+ val buyPrice: Double,
+ val buyVolume: Long,
+ val buyMovingWeek: Long,
+ val buyOrders: Long
+ )
+
+ @Serializable
+ private data class BazaarResponse(
+ val success: Boolean,
+ val products: Map<SkyblockId.BazaarStock, BazaarData> = mapOf(),
+ )
+
+ fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[item]?.quickStatus?.buyPrice ?: lowestBin[item]
+
+ fun spawnPriceLoop() {
+ Firmament.coroutineScope.launch {
+ while (true) {
+ logger.info("Updating NEU prices")
+ updatePrices()
+ withTimeoutOrNull(10.minutes) { waitForInput(IKeyBinding.ofKeyCode(GLFW.GLFW_KEY_U)) }
+ }
+ }
+ }
+
+ private suspend fun updatePrices() {
+ awaitAll(
+ Firmament.coroutineScope.async { fetchBazaarPrices() },
+ Firmament.coroutineScope.async { fetchPricesFromMoulberry() },
+ )
+ }
+
+ private suspend fun fetchPricesFromMoulberry() {
+ lowestBin = Firmament.httpClient.get("$moulberryBaseUrl/lowestbin.json")
+ .body<Map<SkyblockId, Double>>()
+ }
+
+ private suspend fun fetchBazaarPrices() {
+ val response = Firmament.httpClient.get("$hypixelApiBaseUrl/skyblock/bazaar").body<BazaarResponse>()
+ if (!response.success) {
+ logger.warn("Retrieved unsuccessful bazaar data")
+ }
+ bazaarData = response.products.mapKeys { it.key.toRepoId() }
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt b/src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt
index 6033577..f0998a7 100644
--- a/src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt
@@ -27,11 +27,36 @@ import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.Identifier
+/**
+ * A skyblock item id, as used by the NEU repo.
+ * This is not exactly the format used by HyPixel, but is mostly the same.
+ * Usually this id splits an id used by HyPixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`,
+ * with those values extracted from other metadata.
+ */
@JvmInline
+@Serializable
value class SkyblockId(val neuItem: String) {
val identifier get() = Identifier("skyblockitem", neuItem.lowercase().replace(";", "__"))
+ /**
+ * A bazaar stock item id, as returned by the HyPixel bazaar api endpoint.
+ * These are not equivalent to the in-game ids, or the NEU repo ids, and in fact, do not refer to items, but instead
+ * to bazaar stocks. The main difference from [SkyblockId]s is concerning enchanted books. There are probably more,
+ * but for now this holds.
+ */
+ @JvmInline
+ @Serializable
+ value class BazaarStock(val bazaarId: String) {
+ fun toRepoId(): SkyblockId {
+ bazaarEnchantmentRegex.matchEntire(bazaarId)?.let {
+ return SkyblockId("${it.groupValues[1]};${it.groupValues[2]}")
+ }
+ return SkyblockId(bazaarId.replace(":", "-"))
+ }
+ }
+
companion object {
+ private val bazaarEnchantmentRegex = "ENCHANTMENT_(\\D*)_(\\d+)".toRegex()
val NULL: SkyblockId = SkyblockId("null")
}
}
diff --git a/src/main/kotlin/moe/nea/firmament/util/async/input.kt b/src/main/kotlin/moe/nea/firmament/util/async/input.kt
new file mode 100644
index 0000000..ac1cfb4
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/async/input.kt
@@ -0,0 +1,45 @@
+package moe.nea.firmament.util.async
+
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.keybindings.IKeyBinding
+
+private object InputHandler {
+ data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit)
+
+ private val activeContinuations = mutableListOf<KeyInputContinuation>()
+
+ fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit {
+ synchronized(InputHandler) {
+ activeContinuations.add(keyInputContinuation)
+ }
+ return {
+ synchronized(this) {
+ activeContinuations.remove(keyInputContinuation)
+ }
+ }
+ }
+
+ init {
+ HandledScreenKeyPressedEvent.subscribe { event ->
+ synchronized(InputHandler) {
+ val toRemove = activeContinuations.filter {
+ event.matches(it.keybind)
+ }
+ toRemove.forEach { it.onContinue() }
+ activeContinuations.removeAll(toRemove)
+ }
+ }
+ }
+}
+
+suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont ->
+ val unregister =
+ InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) })
+ cont.invokeOnCancellation {
+ unregister()
+ }
+}
+
+
diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json
index e372c7f..440d923 100644
--- a/src/main/resources/assets/firmament/lang/en_us.json
+++ b/src/main/resources/assets/firmament/lang/en_us.json
@@ -1,21 +1,12 @@
-/**
- * Firmament is a Hypixel Skyblock mod for modern Minecraft versions
- * Copyright (C) 2023 Linnea Gräf
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
{
+ "firmament.price": "Checking price for %s",
+ "firmament.price.bazaar": "Bazaar stats:",
+ "firmament.price.bazaar.productid": "Stock id: %s",
+ "firmament.price.bazaar.buy.price": "Buy Price: %.1f",
+ "firmament.price.bazaar.buy.order": "Buy orders: %d",
+ "firmament.price.bazaar.sell.price": "Sell Price: %.1f",
+ "firmament.price.bazaar.sell.order": "Sell orders: %d",
+ "firmament.price.lowestbin": "Lowest BIN: %.1f",
"firmament.repo.reload.network": "Trying to redownload the repository",
"firmament.repo.reload.disk": "Reloading repository from disk. This may lag a bit.",
"firmament.repo.cache": "Recaching items",