Refactor source layout

Introduce compat source sets and move all kotlin sources to the main directory

[no changelog]
This commit is contained in:
Linnea Gräf
2024-08-28 19:04:24 +02:00
parent a690630816
commit d2f240ff0c
251 changed files with 295 additions and 38 deletions

View File

@@ -0,0 +1,120 @@
package moe.nea.firmament.features
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.generated.AllSubscriptions
import moe.nea.firmament.events.FeaturesInitializedEvent
import moe.nea.firmament.events.FirmamentEvent
import moe.nea.firmament.events.subscription.Subscription
import moe.nea.firmament.features.chat.AutoCompletions
import moe.nea.firmament.features.chat.ChatLinks
import moe.nea.firmament.features.chat.QuickCommands
import moe.nea.firmament.features.debug.DebugView
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.events.carnival.CarnivalFeatures
import moe.nea.firmament.features.fixes.CompatibliltyFeatures
import moe.nea.firmament.features.fixes.Fixes
import moe.nea.firmament.features.inventory.CraftingOverlay
import moe.nea.firmament.features.inventory.ItemRarityCosmetics
import moe.nea.firmament.features.inventory.PriceData
import moe.nea.firmament.features.inventory.SaveCursorPosition
import moe.nea.firmament.features.inventory.SlotLocking
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay
import moe.nea.firmament.features.mining.PickaxeAbility
import moe.nea.firmament.features.mining.PristineProfitTracker
import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.features.world.Waypoints
import moe.nea.firmament.util.data.DataHolder
object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) {
@Serializable
data class Config(
val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf()
)
private val features = mutableMapOf<String, FirmamentFeature>()
val allFeatures: Collection<FirmamentFeature> get() = features.values
private var hasAutoloaded = false
init {
autoload()
}
fun autoload() {
synchronized(this) {
if (hasAutoloaded) return
loadFeature(MinorTrolling)
loadFeature(FairySouls)
loadFeature(AutoCompletions)
// TODO: loadFeature(FishingWarning)
loadFeature(SlotLocking)
loadFeature(StorageOverlay)
loadFeature(PristineProfitTracker)
loadFeature(CraftingOverlay)
loadFeature(PowerUserTools)
loadFeature(Waypoints)
loadFeature(ChatLinks)
loadFeature(InventoryButtons)
loadFeature(CompatibliltyFeatures)
loadFeature(AnniversaryFeatures)
loadFeature(QuickCommands)
loadFeature(SaveCursorPosition)
loadFeature(CustomSkyBlockTextures)
loadFeature(PriceData)
loadFeature(Fixes)
loadFeature(DianaWaypoints)
loadFeature(ItemRarityCosmetics)
loadFeature(PickaxeAbility)
loadFeature(CarnivalFeatures)
if (Firmament.DEBUG) {
loadFeature(DeveloperFeatures)
loadFeature(DebugView)
}
allFeatures.forEach { it.config }
FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList()))
hasAutoloaded = true
}
}
fun subscribeEvents() {
AllSubscriptions.provideSubscriptions {
subscribeSingleEvent(it)
}
}
private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
it.eventBus.subscribe(false, it.invoke)
}
fun loadFeature(feature: FirmamentFeature) {
synchronized(features) {
if (feature.identifier in features) {
Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature")
return
}
features[feature.identifier] = feature
feature.onLoad()
}
}
fun isEnabled(identifier: String): Boolean? =
data.enabledFeatures[identifier]
fun setEnabled(identifier: String, value: Boolean) {
data.enabledFeatures[identifier] = value
markDirty()
}
}

View File

@@ -0,0 +1,23 @@
package moe.nea.firmament.features
import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.gui.config.ManagedConfig
// TODO: remove this entire feature system and revamp config
interface FirmamentFeature : SubscriptionOwner {
val identifier: String
val defaultEnabled: Boolean
get() = true
var isEnabled: Boolean
get() = FeatureManager.isEnabled(identifier) ?: defaultEnabled
set(value) {
FeatureManager.setEnabled(identifier, value)
}
override val delegateFeature: FirmamentFeature
get() = this
val config: ManagedConfig? get() = null
fun onLoad() {}
}

View File

@@ -0,0 +1,57 @@
package moe.nea.firmament.features.chat
import com.mojang.brigadier.arguments.StringArgumentType.string
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.suggestsList
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.MaskCommands
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
object AutoCompletions : FirmamentFeature {
object TConfig : ManagedConfig(identifier) {
val provideWarpTabCompletion by toggle("warp-complete") { true }
val replaceWarpIsByWarpIsland by toggle("warp-is") { true }
}
override val config: ManagedConfig?
get() = TConfig
override val identifier: String
get() = "auto-completions"
@Subscribe
fun onMaskCommands(event: MaskCommands) {
if (TConfig.provideWarpTabCompletion) {
event.mask("warp")
}
}
@Subscribe
fun onCommandEvent(event: CommandEvent) {
if (!TConfig.provideWarpTabCompletion) return
event.deleteCommand("warp")
event.register("warp") {
thenArgument("to", string()) { toArg ->
suggestsList {
RepoManager.neuRepo.constants?.islands?.warps?.flatMap { listOf(it.warp) + it.aliases } ?: listOf()
}
thenExecute {
val warpName = get(toArg)
if (warpName == "is" && TConfig.replaceWarpIsByWarpIsland) {
MC.sendServerCommand("warp island")
} else {
MC.sendServerCommand("warp $warpName")
}
}
}
}
}
}

View File

@@ -0,0 +1,161 @@
package moe.nea.firmament.features.chat
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.jvm.javaio.toInputStream
import java.net.URL
import java.util.Collections
import moe.nea.jarvis.api.Point
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlin.math.min
import net.minecraft.client.gui.screen.ChatScreen
import net.minecraft.client.texture.NativeImage
import net.minecraft.client.texture.NativeImageBackedTexture
import net.minecraft.text.ClickEvent
import net.minecraft.text.HoverEvent
import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.util.Formatting
import net.minecraft.util.Identifier
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ModifyChatEvent
import moe.nea.firmament.events.ScreenRenderPostEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.transformEachRecursively
import moe.nea.firmament.util.unformattedString
object ChatLinks : FirmamentFeature {
override val identifier: String
get() = "chat-links"
object TConfig : ManagedConfig(identifier) {
val enableLinks by toggle("links-enabled") { true }
val imageEnabled by toggle("image-enabled") { true }
val allowAllHosts by toggle("allow-all-hosts") { false }
val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" }
val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() }
val position by position("position", 16 * 20, 9 * 20) { Point(0.0, 0.0) }
}
private fun isHostAllowed(host: String) =
TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) }
private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
override val config get() = TConfig
val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex()
data class Image(
val texture: Identifier,
val width: Int,
val height: Int,
)
val imageCache: MutableMap<String, Deferred<Image?>> =
Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>())
private fun tryCacheUrl(url: String) {
if (!isUrlAllowed(url)) {
return
}
if (url in imageCache) {
return
}
imageCache[url] = Firmament.coroutineScope.async {
try {
val response = Firmament.httpClient.get(URL(url))
if (response.status.value == 200) {
val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob)
val image = NativeImage.read(inputStream)
val texture = MC.textureManager.registerDynamicTexture(
"dynamic_image_preview",
NativeImageBackedTexture(image)
)
Image(texture, image.width, image.height)
} else
null
} catch (exc: Exception) {
exc.printStackTrace()
null
}
}
}
val imageExtensions = listOf("jpg", "png", "gif", "jpeg")
fun isImageUrl(url: String): Boolean {
return (url.substringAfterLast('.').lowercase() in imageExtensions)
}
@Subscribe
@OptIn(ExperimentalCoroutinesApi::class)
fun onRender(it: ScreenRenderPostEvent) {
if (!TConfig.imageEnabled) return
if (it.screen !is ChatScreen) return
val hoveredComponent =
MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return
val hoverEvent = hoveredComponent.hoverEvent ?: return
val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return
val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return
if (!isImageUrl(url)) return
val imageFuture = imageCache[url] ?: return
if (!imageFuture.isCompleted) return
val image = imageFuture.getCompleted() ?: return
it.drawContext.matrices.push()
val pos = TConfig.position
pos.applyTransformations(it.drawContext.matrices)
val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width))
it.drawContext.matrices.scale(scale, scale, 1F)
it.drawContext.drawTexture(
image.texture,
0,
0,
1F,
1F,
image.width,
image.height,
image.width,
image.height,
)
it.drawContext.matrices.pop()
}
@Subscribe
fun onModifyChat(it: ModifyChatEvent) {
if (!TConfig.enableLinks) return
it.replaceWith = it.replaceWith.transformEachRecursively { child ->
val text = child.string
if ("://" !in text) return@transformEachRecursively child
val s = Text.empty().setStyle(child.style)
var index = 0
while (index < text.length) {
val nextMatch = urlRegex.find(text, index)
if (nextMatch == null) {
s.append(Text.literal(text.substring(index, text.length)))
break
}
val range = nextMatch.groups[0]!!.range
val url = nextMatch.groupValues[0]
s.append(Text.literal(text.substring(index, range.first)))
s.append(
Text.literal(url).setStyle(
Style.EMPTY.withUnderline(true).withColor(
Formatting.AQUA
).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url)))
.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url))
)
)
if (isImageUrl(url))
tryCacheUrl(url)
index = range.last + 1
}
s
}
}
}

View File

@@ -0,0 +1,100 @@
package moe.nea.firmament.features.chat
import com.mojang.brigadier.context.CommandContext
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.DefaultSource
import moe.nea.firmament.commands.RestArgumentType
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
object QuickCommands : FirmamentFeature {
override val identifier: String
get() = "quick-commands"
fun removePartialPrefix(text: String, prefix: String): String? {
var lf: String? = null
for (i in 1..prefix.length) {
if (text.startsWith(prefix.substring(0, i))) {
lf = text.substring(i)
}
}
return lf
}
val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
@Subscribe
fun onCommands(it: CommandEvent) {
it.register("join") {
thenArgument("what", RestArgumentType) { what ->
thenExecute {
val what = this[what]
if (!SBData.isOnSkyblock) {
MC.sendCommand("join $what")
return@thenExecute
}
val joinName = getNameForFloor(what.replace(" ", "").lowercase())
if (joinName == null) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
} else {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
joinName))
MC.sendCommand("joininstance $joinName")
}
}
}
thenExecute {
source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
}
}
}
fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
if (kuudraLevel != null) {
val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
it.startsWith(
kuudraLevel,
true
)
}
if (l !in kuudraLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
kuudraLevel))
return null
}
return "KUUDRA_${kuudraLevelNames[l]}"
}
val masterLevel = removePartialPrefix(w, "master")
val normalLevel =
removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
val dungeonLevel = masterLevel ?: normalLevel
if (dungeonLevel != null) {
val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
it.startsWith(
dungeonLevel,
true
)
}
if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
return "CATACOMBS_ENTRANCE"
}
if (l !in dungeonLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
kuudraLevel))
return null
}
return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
}
return null
}
}

View File

@@ -0,0 +1,13 @@
package moe.nea.firmament.features.debug
import net.minecraft.text.Text
import moe.nea.firmament.util.MC
class DebugLogger(val tag: String) {
fun isEnabled() = DeveloperFeatures.isEnabled // TODO: allow filtering by tag
fun log(text: () -> String) {
if (!isEnabled()) return
MC.sendChat(Text.literal(text()))
}
}

View File

@@ -0,0 +1,38 @@
package moe.nea.firmament.features.debug
import moe.nea.firmament.Firmament
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.TimeMark
object DebugView : FirmamentFeature {
private data class StoredVariable<T>(
val obj: T,
val timer: TimeMark,
)
private val storedVariables: MutableMap<String, StoredVariable<*>> = sortedMapOf()
override val identifier: String
get() = "debug-view"
override val defaultEnabled: Boolean
get() = Firmament.DEBUG
fun <T : Any?> showVariable(label: String, obj: T) {
synchronized(this) {
storedVariables[label] = StoredVariable(obj, TimeMark.now())
}
}
fun recalculateDebugWidget() {
}
override fun onLoad() {
TickEvent.subscribe {
synchronized(this) {
recalculateDebugWidget()
}
}
}
}

View File

@@ -0,0 +1,55 @@
package moe.nea.firmament.features.debug
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
import kotlin.io.path.absolute
import kotlin.io.path.exists
import net.minecraft.client.MinecraftClient
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.errorBoundary
import moe.nea.firmament.util.iterate
object DeveloperFeatures : FirmamentFeature {
override val identifier: String
get() = "developer"
override val config: TConfig
get() = TConfig
override val defaultEnabled: Boolean
get() = Firmament.DEBUG
val gradleDir =
Path.of(".").absolute()
.iterate { it.parent }
.find { it.resolve("settings.gradle.kts").exists() }
object TConfig : ManagedConfig("developer") {
val autoRebuildResources by toggle("auto-rebuild") { false }
}
@JvmStatic
fun hookOnBeforeResourceReload(client: MinecraftClient): CompletableFuture<Void> {
val reloadFuture = if (TConfig.autoRebuildResources && isEnabled && gradleDir != null) {
val builder = ProcessBuilder("./gradlew", ":processResources")
builder.directory(gradleDir.toFile())
builder.inheritIO()
val process = builder.start()
MC.player?.sendMessage(Text.translatable("firmament.dev.resourcerebuild.start"))
val startTime = TimeMark.now()
process.toHandle().onExit().thenApply {
MC.player?.sendMessage(Text.stringifiedTranslatable("firmament.dev.resourcerebuild.done", startTime.passedTime()))
Unit
}
} else {
CompletableFuture.completedFuture(Unit)
}
return reloadFuture.thenCompose { client.reloadResources() }
}
}

View File

@@ -0,0 +1,27 @@
package moe.nea.firmament.features.debug
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ModifyChatEvent
import moe.nea.firmament.features.FirmamentFeature
// In memorian Dulkir
object MinorTrolling : FirmamentFeature {
override val identifier: String
get() = "minor-trolling"
val trollers = listOf("nea89o", "lrg89")
val t = "From(?: \\[[^\\]]+])? ([^:]+): (.*)".toRegex()
@Subscribe
fun onTroll(it: ModifyChatEvent) {
val m = t.matchEntire(it.unformattedString) ?: return
val (_, name, text) = m.groupValues
if (name !in trollers) return
if (!text.startsWith("c:")) return
it.replaceWith = Text.literal(text.substring(2).replace("&", "§"))
}
}

View File

@@ -0,0 +1,193 @@
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
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.focusedItemStack
import moe.nea.firmament.util.skyBlockId
object PowerUserTools : FirmamentFeature {
override val identifier: String
get() = "power-user"
object TConfig : ManagedConfig(identifier) {
val showItemIds by toggle("show-item-id") { false }
val copyItemId by keyBindingWithDefaultUnbound("copy-item-id")
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
get() = TConfig
var lastCopiedStack: Pair<ItemStack, Text>? = null
set(value) {
field = value
if (value != null)
lastCopiedStackViewTime = true
}
var lastCopiedStackViewTime = false
override fun onLoad() {
TickEvent.subscribe {
if (!lastCopiedStackViewTime)
lastCopiedStack = null
lastCopiedStackViewTime = false
}
ScreenChangeEvent.subscribe {
lastCopiedStack = null
}
}
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))
MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos))
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(it)
}
}
@Subscribe
fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) {
if (it.screen !is AccessorHandledScreen) return
val item = it.screen.focusedItemStack ?: return
if (it.matches(TConfig.copyItemId)) {
val sbId = item.skyBlockId
if (sbId == null) {
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skyblockid.fail"))
return
}
ClipboardUtils.setTextContent(sbId.neuItem)
lastCopiedStack =
Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem))
} else if (it.matches(TConfig.copyTexturePackId)) {
val model = CustomItemModelEvent.getModelIdentifier(item)
if (model == null) {
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail"))
return
}
ClipboardUtils.setTextContent(model.toString())
lastCopiedStack =
Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString()))
} else if (it.matches(TConfig.copyNbtData)) {
// TODO: copy full nbt
val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toString() ?: "<empty>"
ClipboardUtils.setTextContent(nbt)
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt"))
} else if (it.matches(TConfig.copySkullTexture)) {
if (item.item != Items.PLAYER_HEAD) {
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-skull"))
return
}
val profile = item.get(DataComponentTypes.PROFILE)
if (profile == null) {
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile"))
return
}
val skullTexture = CustomSkyBlockTextures.getSkullTexture(profile)
if (skullTexture == null) {
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-texture"))
return
}
ClipboardUtils.setTextContent(skullTexture.toString())
lastCopiedStack =
Pair(
item,
Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString())
)
println("Copied skull id: $skullTexture")
}
}
@Subscribe
fun onCopyWorldInfo(it: WorldKeyboardEvent) {
if (it.matches(TConfig.copySkullTexture)) {
val p = MC.camera ?: return
val blockHit = p.raycast(20.0, 0.0f, false) ?: return
if (blockHit.type != HitResult.Type.BLOCK || blockHit !is BlockHitResult) {
MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail"))
return
}
val blockAt = p.world.getBlockState(blockHit.blockPos)?.block
val entity = p.world.getBlockEntity(blockHit.blockPos)
if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.owner == null) {
MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail"))
return
}
val id = CustomSkyBlockTextures.getSkullTexture(entity.owner!!)
if (id == null) {
MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail"))
} else {
ClipboardUtils.setTextContent(id.toString())
MC.sendChat(Text.stringifiedTranslatable("firmament.tooltip.copied.skull", id.toString()))
}
}
}
@Subscribe
fun addItemId(it: ItemTooltipEvent) {
if (TConfig.showItemIds) {
val id = it.stack.skyBlockId ?: return
it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem))
}
val (item, text) = lastCopiedStack ?: return
if (!ItemStack.areEqual(item, it.stack)) {
lastCopiedStack = null
return
}
lastCopiedStackViewTime = true
it.lines.add(text)
}
}

View File

@@ -0,0 +1,131 @@
package moe.nea.firmament.features.diana
import kotlin.time.Duration.Companion.seconds
import net.minecraft.particle.ParticleTypes
import net.minecraft.sound.SoundEvents
import net.minecraft.util.math.Vec3d
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ParticleSpawnEvent
import moe.nea.firmament.events.SoundReceiveEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.WarpUtil
import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.skyBlockId
object AncestralSpadeSolver : SubscriptionOwner {
var lastDing = TimeMark.farPast()
private set
private val pitches = mutableListOf<Float>()
val particlePositions = mutableListOf<Vec3d>()
var nextGuess: Vec3d? = null
private set
val ancestralSpadeId = SkyblockId("ANCESTRAL_SPADE")
private var lastTeleportAttempt = TimeMark.farPast()
fun isEnabled() =
DianaWaypoints.TConfig.ancestralSpadeSolver
&& SBData.skyblockLocation == SkyBlockIsland.HUB
&& MC.player?.inventory?.containsAny { it.skyBlockId == ancestralSpadeId } == true // TODO: add a reactive property here
@Subscribe
fun onKeyBind(event: WorldKeyboardEvent) {
if (!isEnabled()) return
if (!event.matches(DianaWaypoints.TConfig.ancestralSpadeTeleport)) return
if (lastTeleportAttempt.passedTime() < 3.seconds) return
WarpUtil.teleportToNearestWarp(SkyBlockIsland.HUB, nextGuess ?: return)
lastTeleportAttempt = TimeMark.now()
}
@Subscribe
fun onParticleSpawn(event: ParticleSpawnEvent) {
if (!isEnabled()) return
if (event.particleEffect != ParticleTypes.DRIPPING_LAVA) return
if (event.offset.x != 0.0F || event.offset.y != 0F || event.offset.z != 0F)
return
particlePositions.add(event.position)
if (particlePositions.size > 20) {
particlePositions.removeFirst()
}
}
@Subscribe
fun onPlaySound(event: SoundReceiveEvent) {
if (!isEnabled()) return
if (!SoundEvents.BLOCK_NOTE_BLOCK_HARP.matchesId(event.sound.value().id)) return
if (lastDing.passedTime() > 1.seconds) {
particlePositions.clear()
pitches.clear()
}
lastDing = TimeMark.now()
pitches.add(event.pitch)
if (pitches.size > 20) {
pitches.removeFirst()
}
if (particlePositions.size < 3) {
return
}
val averagePitchDelta =
if (pitches.isEmpty()) return
else pitches
.zipWithNext { a, b -> b - a }
.average()
val soundDistanceEstimate = (Math.E / averagePitchDelta) - particlePositions.first().distanceTo(event.position)
if (soundDistanceEstimate > 1000) {
return
}
val lastParticleDirection = particlePositions
.takeLast(3)
.let { (a, _, b) -> b.subtract(a) }
.normalize()
nextGuess = event.position.add(lastParticleDirection.multiply(soundDistanceEstimate))
}
@Subscribe
fun onWorldRender(event: WorldRenderLastEvent) {
if (!isEnabled()) return
RenderInWorldContext.renderInWorld(event) {
nextGuess?.let {
color(1f, 1f, 0f, 0.5f)
tinyBlock(it, 1f)
color(1f, 1f, 0f, 1f)
tracer(it, lineWidth = 3f)
}
if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) {
color(0f, 1f, 0f, 0.7f)
line(particlePositions)
}
}
}
@Subscribe
fun onSwapWorld(event: WorldReadyEvent) {
nextGuess = null
particlePositions.clear()
pitches.clear()
lastDing = TimeMark.farPast()
}
override val delegateFeature: FirmamentFeature
get() = DianaWaypoints
}

View File

@@ -0,0 +1,35 @@
package moe.nea.firmament.features.diana
import moe.nea.firmament.events.AttackBlockEvent
import moe.nea.firmament.events.ParticleSpawnEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.SoundReceiveEvent
import moe.nea.firmament.events.UseBlockEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
object DianaWaypoints : FirmamentFeature {
override val identifier get() = "diana"
override val config get() = TConfig
object TConfig : ManagedConfig(identifier) {
val ancestralSpadeSolver by toggle("ancestral-spade") { true }
val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport")
val nearbyWaypoints by toggle("nearby-waypoints") { true }
}
override fun onLoad() {
UseBlockEvent.subscribe {
NearbyBurrowsSolver.onBlockClick(it.hitResult.blockPos)
}
AttackBlockEvent.subscribe {
NearbyBurrowsSolver.onBlockClick(it.blockPos)
}
}
}

View File

@@ -0,0 +1,144 @@
package moe.nea.firmament.features.diana
import kotlin.time.Duration.Companion.seconds
import net.minecraft.particle.ParticleTypes
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.MathHelper
import net.minecraft.util.math.Position
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ParticleSpawnEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.mutableMapWithMaxSize
import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld
object NearbyBurrowsSolver : SubscriptionOwner {
private val recentlyDugBurrows: MutableMap<BlockPos, TimeMark> = mutableMapWithMaxSize(20)
private val recentEnchantParticles: MutableMap<BlockPos, TimeMark> = mutableMapWithMaxSize(500)
private var lastBlockClick: BlockPos? = null
enum class BurrowType {
START, MOB, TREASURE
}
val burrows = mutableMapOf<BlockPos, BurrowType>()
@Subscribe
fun onChatEvent(event: ProcessChatEvent) {
val lastClickedBurrow = lastBlockClick ?: return
if (event.unformattedString.startsWith("You dug out a Griffin Burrow!") ||
event.unformattedString.startsWith(" ☠ You were killed by") ||
event.unformattedString.startsWith("You finished the Griffin burrow chain!")
) {
markAsDug(lastClickedBurrow)
burrows.remove(lastClickedBurrow)
}
}
fun wasRecentlyDug(blockPos: BlockPos): Boolean {
val lastDigTime = recentlyDugBurrows[blockPos] ?: TimeMark.farPast()
return lastDigTime.passedTime() < 10.seconds
}
fun markAsDug(blockPos: BlockPos) {
recentlyDugBurrows[blockPos] = TimeMark.now()
}
fun wasRecentlyEnchanted(blockPos: BlockPos): Boolean {
val lastEnchantTime = recentEnchantParticles[blockPos] ?: TimeMark.farPast()
return lastEnchantTime.passedTime() < 4.seconds
}
fun markAsEnchanted(blockPos: BlockPos) {
recentEnchantParticles[blockPos] = TimeMark.now()
}
@Subscribe
fun onParticles(event: ParticleSpawnEvent) {
if (!DianaWaypoints.TConfig.nearbyWaypoints) return
val position: BlockPos = event.position.toBlockPos().down()
if (wasRecentlyDug(position)) return
val isEven50Spread = (event.offset.x == 0.5f && event.offset.z == 0.5f)
if (event.particleEffect.type == ParticleTypes.ENCHANT) {
if (event.count == 5 && event.speed == 0.05F && event.offset.y == 0.4F && isEven50Spread) {
markAsEnchanted(position)
}
return
}
if (!wasRecentlyEnchanted(position)) return
if (event.particleEffect.type == ParticleTypes.ENCHANTED_HIT
&& event.count == 4
&& event.speed == 0.01F
&& event.offset.y == 0.1f
&& isEven50Spread
) {
burrows[position] = BurrowType.START
}
if (event.particleEffect.type == ParticleTypes.CRIT
&& event.count == 3
&& event.speed == 0.01F
&& event.offset.y == 0.1F
&& isEven50Spread
) {
burrows[position] = BurrowType.MOB
}
if (event.particleEffect.type == ParticleTypes.DRIPPING_LAVA
&& event.count == 2
&& event.speed == 0.01F
&& event.offset.y == 0.1F
&& event.offset.x == 0.35F && event.offset.z == 0.35f
) {
burrows[position] = BurrowType.TREASURE
}
}
@Subscribe
fun onRender(event: WorldRenderLastEvent) {
if (!DianaWaypoints.TConfig.nearbyWaypoints) return
renderInWorld(event) {
for ((location, burrow) in burrows) {
when (burrow) {
BurrowType.START -> color(.2f, .8f, .2f, 0.4f)
BurrowType.MOB -> color(0.3f, 0.4f, 0.9f, 0.4f)
BurrowType.TREASURE -> color(1f, 0.7f, 0.2f, 0.4f)
}
block(location)
}
}
}
@Subscribe
fun onSwapWorld(worldReadyEvent: WorldReadyEvent) {
burrows.clear()
recentEnchantParticles.clear()
recentlyDugBurrows.clear()
lastBlockClick = null
}
fun onBlockClick(blockPos: BlockPos) {
if (!DianaWaypoints.TConfig.nearbyWaypoints) return
burrows.remove(blockPos)
lastBlockClick = blockPos
}
override val delegateFeature: FirmamentFeature
get() = DianaWaypoints
}
fun Position.toBlockPos(): BlockPos {
return BlockPos(MathHelper.floor(x), MathHelper.floor(y), MathHelper.floor(z))
}

View File

@@ -0,0 +1,224 @@
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())
}
}

View File

@@ -0,0 +1,17 @@
package moe.nea.firmament.features.events.carnival
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
object CarnivalFeatures : FirmamentFeature {
object TConfig : ManagedConfig(identifier) {
val enableBombSolver by toggle("bombs-solver") { true }
val displayTutorials by toggle("tutorials") { true }
}
override val config: ManagedConfig?
get() = TConfig
override val identifier: String
get() = "carnival"
}

View File

@@ -0,0 +1,276 @@
package moe.nea.firmament.features.events.carnival
import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
import io.github.notenoughupdates.moulconfig.xml.Bind
import java.util.UUID
import net.minecraft.block.Blocks
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.text.ClickEvent
import net.minecraft.text.Text
import net.minecraft.util.math.BlockPos
import net.minecraft.world.WorldAccess
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.AttackBlockEvent
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.EntityUpdateEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.debug.DebugLogger
import moe.nea.firmament.util.LegacyFormattingCode
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.item.createSkullItem
import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.setSkyBlockFirmamentUiId
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.useMatch
object MinesweeperHelper {
val sandBoxLow = BlockPos(-112, 72, -11)
val sandBoxHigh = BlockPos(-106, 72, -5)
val boardSize = Pair(sandBoxHigh.x - sandBoxLow.x, sandBoxHigh.z - sandBoxLow.z)
val gameStartMessage = "[NPC] Carnival Pirateman: Good luck, matey!"
val gameEndMessage = "Fruit Digging"
val bombPattern = "MINES! There (are|is) (?<bombCount>[0-8]) bombs? hidden nearby\\.".toPattern()
val startGameQuestion = "[NPC] Carnival Pirateman: Would ye like to do some Fruit Digging?"
enum class Piece(
@get:Bind("fruitName")
val fruitName: String,
val points: Int,
val specialAbility: String,
val totalPerBoard: Int,
val textureHash: String,
val fruitColor: LegacyFormattingCode,
) {
COCONUT("Coconut",
200,
"Prevents a bomb from exploding next turn",
3,
"10ceb1455b471d016a9f06d25f6e468df9fcf223e2c1e4795b16e84fcca264ee",
LegacyFormattingCode.DARK_PURPLE),
APPLE("Apple",
100,
"Gains 100 points for each apple dug up",
8,
"17ea278d6225c447c5943d652798d0bbbd1418434ce8c54c54fdac79994ddd6c",
LegacyFormattingCode.GREEN),
WATERMELON("Watermelon",
100,
"Blows up an adjacent fruit for half the points",
4,
"efe4ef83baf105e8dee6cf03dfe7407f1911b3b9952c891ae34139560f2931d6",
LegacyFormattingCode.DARK_BLUE),
DURIAN("Durian",
800,
"Halves the points earned in the next turn",
2,
"ac268d36c2c6047ffeec00124096376b56dbb4d756a55329363a1b27fcd659cd",
LegacyFormattingCode.DARK_PURPLE),
MANGO("Mango",
300,
"Just an ordinary fruit",
10,
"f363a62126a35537f8189343a22660de75e810c6ac004a7d3da65f1c040a839",
LegacyFormattingCode.GREEN),
DRAGON_FRUIT("Dragonfruit",
1200,
"Halves the points earned in the next turn",
1,
"3cc761bcb0579763d9b8ab6b7b96fa77eb6d9605a804d838fec39e7b25f95591",
LegacyFormattingCode.LIGHT_PURPLE),
POMEGRANATE("Pomegranate",
200,
"Grants an extra 50% more points in the next turn",
4,
"40824d18079042d5769f264f44394b95b9b99ce689688cc10c9eec3f882ccc08",
LegacyFormattingCode.DARK_BLUE),
CHERRY("Cherry",
200,
"The second cherry grants 300 bonus points",
2,
"c92b099a62cd2fbf8ada09dec145c75d7fda4dc57b968bea3a8fa11e37aa48b2",
LegacyFormattingCode.DARK_PURPLE),
BOMB("Bomb",
-1,
"Destroys nearby fruit",
15,
"a76a2811d1e176a07b6d0a657b910f134896ce30850f6e80c7c83732d85381ea",
LegacyFormattingCode.DARK_RED),
RUM("Rum",
-1,
"Stops your dowsing ability for one turn",
5,
"407b275d28b927b1bf7f6dd9f45fbdad2af8571c54c8f027d1bff6956fbf3c16",
LegacyFormattingCode.YELLOW),
;
val textureUrl = "http://textures.minecraft.net/texture/$textureHash"
val itemStack = createSkullItem(UUID.randomUUID(), textureUrl)
.setSkyBlockFirmamentUiId("MINESWEEPER_$name")
@Bind
fun getIcon() = ModernItemStack.of(itemStack)
@Bind
fun pieceLabel() = fruitColor.formattingCode + fruitName
@Bind
fun boardLabel() = "§a$totalPerBoard§7/§rboard"
@Bind("description")
fun getDescription() = buildString {
append(specialAbility)
if (points >= 0) {
append(" Default points: $points.")
}
}
}
object TutorialScreen {
@get:Bind("pieces")
val pieces = ObservableList(Piece.entries.toList().reversed())
@get:Bind("modes")
val modes = ObservableList(DowsingMode.entries.toList())
}
enum class DowsingMode(
val itemType: Item,
@get:Bind("feature")
val feature: String,
@get:Bind("description")
val description: String,
) {
MINES(Items.IRON_SHOVEL, "Bomb detection", "Tells you how many bombs are near the block"),
ANCHOR(Items.DIAMOND_SHOVEL, "Lowest fruit", "Shows you which block nearby contains the lowest scoring fruit"),
TREASURE(Items.GOLDEN_SHOVEL, "Highest fruit", "Tells you which kind of fruit is the highest scoring nearby"),
;
@Bind("itemType")
fun getItemStack() = ModernItemStack.of(ItemStack(itemType))
companion object {
val id = SkyblockId("CARNIVAL_SHOVEL")
fun fromItem(itemStack: ItemStack): DowsingMode? {
if (itemStack.skyBlockId != id) return null
return DowsingMode.entries.find { it.itemType == itemStack.item }
}
}
}
data class BoardPosition(
val x: Int,
val y: Int
) {
fun toBlockPos() = BlockPos(sandBoxLow.x + x, sandBoxLow.y, sandBoxLow.z + y)
fun getBlock(world: WorldAccess) = world.getBlockState(toBlockPos()).block
fun isUnopened(world: WorldAccess) = getBlock(world) == Blocks.SAND
fun isOpened(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE
fun isScorched(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE_STAIRS
companion object {
fun fromBlockPos(blockPos: BlockPos): BoardPosition? {
if (blockPos.y != sandBoxLow.y) return null
val x = blockPos.x - sandBoxLow.x
val y = blockPos.z - sandBoxLow.z
if (x < 0 || x >= boardSize.first) return null
if (y < 0 || y >= boardSize.second) return null
return BoardPosition(x, y)
}
}
}
data class GameState(
val nearbyBombs: MutableMap<BoardPosition, Int> = mutableMapOf(),
val knownBombPositions: MutableSet<BoardPosition> = mutableSetOf(),
var lastClickedPosition: BoardPosition? = null,
var lastDowsingMode: DowsingMode? = null,
)
var gameState: GameState? = null
val log = DebugLogger("minesweeper")
@Subscribe
fun onCommand(event: CommandEvent.SubCommand) {
event.subcommand("minesweepertutorial") {
thenExecute {
ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("carnival/minesweeper_tutorial",
TutorialScreen,
null))
}
}
}
@Subscribe
fun onWorldChange(event: WorldReadyEvent) {
gameState = null
}
@Subscribe
fun onChat(event: ProcessChatEvent) {
if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) {
MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled {
it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial"))
})
}
if (!CarnivalFeatures.TConfig.enableBombSolver) {
gameState = null // TODO: replace this which a watchable property
return
}
if (event.unformattedString == gameStartMessage) {
gameState = GameState()
log.log { "Game started" }
}
if (event.unformattedString.trim() == gameEndMessage) {
gameState = null // TODO: add a loot tracker maybe? probably not, i dont think people care
log.log { "Finished game" }
}
val gs = gameState ?: return
bombPattern.useMatch(event.unformattedString) {
val bombCount = group("bombCount").toInt()
log.log { "Marking ${gs.lastClickedPosition} as having $bombCount nearby" }
val pos = gs.lastClickedPosition ?: return
gs.nearbyBombs[pos] = bombCount
}
}
@Subscribe
fun onMobChange(event: EntityUpdateEvent) {
val gs = gameState ?: return
if (event !is EntityUpdateEvent.TrackedDataUpdate) return
// TODO: listen to state
}
@Subscribe
fun onBlockClick(event: AttackBlockEvent) {
val gs = gameState ?: return
val boardPosition = BoardPosition.fromBlockPos(event.blockPos)
log.log { "Breaking block at ${event.blockPos} ($boardPosition)" }
gs.lastClickedPosition = boardPosition
gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack)
}
@Subscribe
fun onRender(event: WorldRenderLastEvent) {
val gs = gameState ?: return
RenderInWorldContext.renderInWorld(event) {
for ((pos, bombCount) in gs.nearbyBombs) {
this.text(pos.toBlockPos().up().toCenterPos(), Text.literal("§a$bombCount \uD83D\uDCA3"))
}
}
}
}

View File

@@ -0,0 +1,51 @@
package moe.nea.firmament.features.fixes
import net.fabricmc.loader.api.FabricLoader
import net.superkat.explosiveenhancement.api.ExplosiveApi
import net.minecraft.particle.ParticleTypes
import net.minecraft.util.math.Vec3d
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ParticleSpawnEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
object CompatibliltyFeatures : FirmamentFeature {
override val identifier: String
get() = "compatibility"
object TConfig : ManagedConfig(identifier) {
val enhancedExplosions by toggle("explosion-enabled") { false }
val explosionSize by integer("explosion-power", 10, 50) { 1 }
}
override val config: ManagedConfig?
get() = TConfig
interface ExplosiveApiWrapper {
fun spawnParticle(vec3d: Vec3d, power: Float)
}
class ExplosiveApiWrapperImpl : ExplosiveApiWrapper {
override fun spawnParticle(vec3d: Vec3d, power: Float) {
ExplosiveApi.spawnParticles(MC.world, vec3d.x, vec3d.y, vec3d.z, TConfig.explosionSize / 10F)
}
}
val explosiveApiWrapper = if (FabricLoader.getInstance().isModLoaded("explosiveenhancement")) {
ExplosiveApiWrapperImpl()
} else null
@Subscribe
fun onExplosion(it: ParticleSpawnEvent) {
if (TConfig.enhancedExplosions &&
it.particleEffect.type == ParticleTypes.EXPLOSION_EMITTER &&
explosiveApiWrapper != null
) {
it.cancel()
explosiveApiWrapper.spawnParticle(it.position, 2F)
}
}
}

View File

@@ -0,0 +1,71 @@
package moe.nea.firmament.features.fixes
import moe.nea.jarvis.api.Point
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import net.minecraft.client.MinecraftClient
import net.minecraft.client.option.KeyBinding
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
import net.minecraft.util.Arm
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.errorBoundary
object Fixes : FirmamentFeature {
override val identifier: String
get() = "fixes"
object TConfig : ManagedConfig(identifier) {
val fixUnsignedPlayerSkins by toggle("player-skins") { true }
var autoSprint by toggle("auto-sprint") { false }
val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
val peekChat by keyBindingWithDefaultUnbound("peek-chat")
}
override val config: ManagedConfig
get() = TConfig
fun handleIsPressed(
keyBinding: KeyBinding,
cir: CallbackInfoReturnable<Boolean>
) {
if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
cir.returnValue = true
}
@Subscribe
fun onRenderHud(it: HudRenderEvent) {
if (!TConfig.autoSprintKeyBinding.isBound) return
it.context.matrices.push()
TConfig.autoSprintHud.applyTransformations(it.context.matrices)
it.context.drawText(
MC.font, Text.translatable(
if (TConfig.autoSprint)
"firmament.fixes.auto-sprint.on"
else if (MC.player?.isSprinting == true)
"firmament.fixes.auto-sprint.sprinting"
else
"firmament.fixes.auto-sprint.not-sprinting"
), 0, 0, -1, false
)
it.context.matrices.pop()
}
@Subscribe
fun onWorldKeyboard(it: WorldKeyboardEvent) {
if (it.matches(TConfig.autoSprintKeyBinding)) {
TConfig.autoSprint = !TConfig.autoSprint
}
}
fun shouldPeekChat(): Boolean {
return TConfig.peekChat.isPressed(atLeast = true)
}
}

View File

@@ -0,0 +1,66 @@
package moe.nea.firmament.features.inventory
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.item.ItemStack
import net.minecraft.util.Formatting
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.rei.FirmamentReiPlugin.Companion.asItemEntry
import moe.nea.firmament.rei.SBItemEntryDefinition
import moe.nea.firmament.rei.recipes.SBCraftingRecipe
import moe.nea.firmament.util.MC
object CraftingOverlay : FirmamentFeature {
private var screen: GenericContainerScreen? = null
private var recipe: SBCraftingRecipe? = null
private val craftingOverlayIndices = listOf(
10, 11, 12,
19, 20, 21,
28, 29, 30,
)
fun setOverlay(screen: GenericContainerScreen, recipe: SBCraftingRecipe) {
this.screen = screen
this.recipe = recipe
}
override val identifier: String
get() = "crafting-overlay"
@Subscribe
fun onSlotRender(event: SlotRenderEvents.After) {
val slot = event.slot
val recipe = this.recipe ?: return
if (slot.inventory != screen?.screenHandler?.inventory) return
val recipeIndex = craftingOverlayIndices.indexOf(slot.index)
if (recipeIndex < 0) return
val expectedItem = recipe.neuRecipe.inputs[recipeIndex]
val actualStack = slot.stack ?: ItemStack.EMPTY!!
val actualEntry = SBItemEntryDefinition.getEntry(actualStack).value
if ((actualEntry.skyblockId.neuItem != expectedItem.itemId || actualEntry.getStackSize() < expectedItem.amount) && expectedItem.amount.toInt() != 0) {
event.context.fill(
event.slot.x,
event.slot.y,
event.slot.x + 16,
event.slot.y + 16,
0x80FF0000.toInt()
)
}
if (!slot.hasStack()) {
val itemStack = SBItemEntryDefinition.getEntry(expectedItem).asItemEntry().value
event.context.drawItem(itemStack, event.slot.x, event.slot.y)
event.context.drawItemInSlot(
MC.font,
itemStack,
event.slot.x,
event.slot.y,
"${Formatting.RED}${expectedItem.amount.toInt()}"
)
}
}
}

View File

@@ -0,0 +1,85 @@
package moe.nea.firmament.features.inventory
import java.awt.Color
import net.minecraft.client.gui.DrawContext
import net.minecraft.item.ItemStack
import net.minecraft.util.Formatting
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HotbarItemRenderEvent
import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.lastNotNullOfOrNull
import moe.nea.firmament.util.memoize
import moe.nea.firmament.util.memoizeIdentity
import moe.nea.firmament.util.unformattedString
object ItemRarityCosmetics : FirmamentFeature {
override val identifier: String
get() = "item-rarity-cosmetics"
object TConfig : ManagedConfig(identifier) {
val showItemRarityBackground by toggle("background") { false }
val showItemRarityInHotbar by toggle("background-hotbar") { false }
}
override val config: ManagedConfig
get() = TConfig
private val rarityToColor = mapOf(
"UNCOMMON" to Formatting.GREEN,
"COMMON" to Formatting.WHITE,
"RARE" to Formatting.DARK_BLUE,
"EPIC" to Formatting.DARK_PURPLE,
"LEGENDARY" to Formatting.GOLD,
"LEGENJERRY" to Formatting.GOLD,
"MYTHIC" to Formatting.LIGHT_PURPLE,
"DIVINE" to Formatting.BLUE,
"SPECIAL" to Formatting.DARK_RED,
"SUPREME" to Formatting.DARK_RED,
).mapValues {
val c = Color(it.value.colorValue!!)
Triple(c.red / 255F, c.green / 255F, c.blue / 255F)
}
private fun getSkyblockRarity0(itemStack: ItemStack): Triple<Float, Float, Float>? {
return itemStack.loreAccordingToNbt.lastNotNullOfOrNull {
val entry = it.unformattedString
rarityToColor.entries.find { (k, v) -> k in entry }?.value
}
}
val getSkyblockRarity = ::getSkyblockRarity0.memoizeIdentity(100)
fun drawItemStackRarity(drawContext: DrawContext, x: Int, y: Int, item: ItemStack) {
val (r, g, b) = getSkyblockRarity(item) ?: return
drawContext.drawSprite(
x, y,
0,
16, 16,
MC.guiAtlasManager.getSprite(Identifier.of("firmament:item_rarity_background")),
r, g, b, 1F
)
}
@Subscribe
fun onRenderSlot(it: SlotRenderEvents.Before) {
if (!TConfig.showItemRarityBackground) return
val stack = it.slot.stack ?: return
drawItemStackRarity(it.context, it.slot.x, it.slot.y, stack)
}
@Subscribe
fun onRenderHotbarItem(it: HotbarItemRenderEvent) {
if (!TConfig.showItemRarityInHotbar) return
val stack = it.item
drawItemStackRarity(it.context, it.x, it.y, stack)
}
}

View File

@@ -0,0 +1,51 @@
package moe.nea.firmament.features.inventory
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ItemTooltipEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.repo.HypixelStaticData
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.skyBlockId
object PriceData : FirmamentFeature {
override val identifier: String
get() = "price-data"
object TConfig : ManagedConfig(identifier) {
val tooltipEnabled by toggle("enable-always") { true }
val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind")
}
override val config get() = TConfig
@Subscribe
fun onItemTooltip(it: ItemTooltipEvent) {
if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) {
return
}
val sbId = it.stack.skyBlockId
val bazaarData = HypixelStaticData.bazaarData[sbId]
val lowestBin = HypixelStaticData.lowestBin[sbId]
if (bazaarData != null) {
it.lines.add(Text.literal(""))
it.lines.add(
Text.stringifiedTranslatable("firmament.tooltip.bazaar.sell-order",
FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1))
)
it.lines.add(
Text.stringifiedTranslatable("firmament.tooltip.bazaar.buy-order",
FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1))
)
} else if (lowestBin != null) {
it.lines.add(Text.literal(""))
it.lines.add(
Text.stringifiedTranslatable("firmament.tooltip.ah.lowestbin",
FirmFormatters.formatCommas(lowestBin, 1))
)
}
}
}

View File

@@ -0,0 +1,66 @@
package moe.nea.firmament.features.inventory
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.milliseconds
import net.minecraft.client.util.InputUtil
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.assertNotNullOr
object SaveCursorPosition : FirmamentFeature {
override val identifier: String
get() = "save-cursor-position"
object TConfig : ManagedConfig(identifier) {
val enable by toggle("enable") { true }
val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds }
}
override val config: TConfig
get() = TConfig
var savedPositionedP1: Pair<Double, Double>? = null
var savedPosition: SavedPosition? = null
data class SavedPosition(
val middle: Pair<Double, Double>,
val cursor: Pair<Double, Double>,
val savedAt: TimeMark = TimeMark.now()
)
@JvmStatic
fun saveCursorOriginal(positionedX: Double, positionedY: Double) {
savedPositionedP1 = Pair(positionedX, positionedY)
}
@JvmStatic
fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? {
if (!TConfig.enable) return null
val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance }
savedPosition = null
if (lastPosition != null &&
(lastPosition.middle.first - middleX).absoluteValue < 1 &&
(lastPosition.middle.second - middleY).absoluteValue < 1
) {
InputUtil.setCursorParameters(
MC.window.handle,
InputUtil.GLFW_CURSOR_NORMAL,
lastPosition.cursor.first,
lastPosition.cursor.second
)
return lastPosition.cursor
}
return null
}
@JvmStatic
fun saveCursorMiddle(middleX: Double, middleY: Double) {
if (!TConfig.enable) return
val cursorPos = assertNotNullOr(savedPositionedP1) { return }
savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos)
}
}

View File

@@ -0,0 +1,203 @@
@file:UseSerializers(DashlessUUIDSerializer::class)
package moe.nea.firmament.features.inventory
import com.mojang.blaze3d.systems.RenderSystem
import java.util.UUID
import org.lwjgl.glfw.GLFW
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.GenericContainerScreenHandler
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.events.IsSlotProtectedEvent
import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.keybindings.SavedKeyBinding
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.CommonSoundEffects
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.item.displayNameAccordingToNbt
import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.json.DashlessUUIDSerializer
import moe.nea.firmament.util.skyblockUUID
import moe.nea.firmament.util.unformattedString
object SlotLocking : FirmamentFeature {
override val identifier: String
get() = "slot-locking"
@Serializable
data class Data(
val lockedSlots: MutableSet<Int> = mutableSetOf(),
val lockedSlotsRift: MutableSet<Int> = mutableSetOf(),
val lockedUUIDs: MutableSet<UUID> = mutableSetOf(),
)
object TConfig : ManagedConfig(identifier) {
val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L }
val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") {
SavedKeyBinding(GLFW.GLFW_KEY_L, shift = true)
}
}
override val config: TConfig
get() = TConfig
object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "locked-slots", ::Data)
val lockedUUIDs get() = DConfig.data?.lockedUUIDs
val lockedSlots
get() = when (SBData.skyblockLocation) {
SkyBlockIsland.RIFT -> DConfig.data?.lockedSlotsRift
null -> null
else -> DConfig.data?.lockedSlots
}
fun isSalvageScreen(screen: HandledScreen<*>?): Boolean {
if (screen == null) return false
return screen.title.unformattedString.contains("Salvage Item")
}
fun isTradeScreen(screen: HandledScreen<*>?): Boolean {
if (screen == null) return false
val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false
if (handler.inventory.size() < 9) return false
val middlePane = handler.inventory.getStack(handler.inventory.size() - 5)
if (middlePane == null) return false
return middlePane.displayNameAccordingToNbt?.unformattedString == "⇦ Your stuff"
}
fun isNpcShop(screen: HandledScreen<*>?): Boolean {
if (screen == null) return false
val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false
if (handler.inventory.size() < 9) return false
val sellItem = handler.inventory.getStack(handler.inventory.size() - 5)
if (sellItem == null) return false
if (sellItem.displayNameAccordingToNbt?.unformattedString == "Sell Item") return true
val lore = sellItem.loreAccordingToNbt
return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!"
}
@Subscribe
fun onSalvageProtect(event: IsSlotProtectedEvent) {
if (event.slot == null) return
if (!event.slot.hasStack()) return
if (event.slot.stack.displayNameAccordingToNbt?.unformattedString != "Salvage Items") return
val inv = event.slot.inventory
var anyBlocked = false
for (i in 0 until event.slot.index) {
val stack = inv.getStack(i)
if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack))
anyBlocked = true
}
if (anyBlocked) {
event.protectSilent()
}
}
@Subscribe
fun onProtectUuidItems(event: IsSlotProtectedEvent) {
val doesNotDeleteItem = event.actionType == SlotActionType.SWAP
|| event.actionType == SlotActionType.PICKUP
|| event.actionType == SlotActionType.QUICK_MOVE
|| event.actionType == SlotActionType.QUICK_CRAFT
|| event.actionType == SlotActionType.CLONE
|| event.actionType == SlotActionType.PICKUP_ALL
val isSellOrTradeScreen =
isNpcShop(MC.handledScreen) || isTradeScreen(MC.handledScreen) || isSalvageScreen(MC.handledScreen)
if ((!isSellOrTradeScreen || event.slot?.inventory !is PlayerInventory)
&& doesNotDeleteItem
) return
val stack = event.itemStack ?: return
val uuid = stack.skyblockUUID ?: return
if (uuid in (lockedUUIDs ?: return)) {
event.protect()
}
}
@Subscribe
fun onProtectSlot(it: IsSlotProtectedEvent) {
if (it.slot != null && it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())) {
it.protect()
}
}
@Subscribe
fun onLockUUID(it: HandledScreenKeyPressedEvent) {
if (!it.matches(TConfig.lockUUID)) return
val inventory = MC.handledScreen ?: return
inventory as AccessorHandledScreen
val slot = inventory.focusedSlot_Firmament ?: return
val stack = slot.stack ?: return
val uuid = stack.skyblockUUID ?: return
val lockedUUIDs = lockedUUIDs ?: return
if (uuid in lockedUUIDs) {
lockedUUIDs.remove(uuid)
} else {
lockedUUIDs.add(uuid)
}
DConfig.markDirty()
CommonSoundEffects.playSuccess()
it.cancel()
}
@Subscribe
fun onLockSlot(it: HandledScreenKeyPressedEvent) {
if (!it.matches(TConfig.lockSlot)) return
val inventory = MC.handledScreen ?: return
inventory as AccessorHandledScreen
val slot = inventory.focusedSlot_Firmament ?: return
val lockedSlots = lockedSlots ?: return
if (slot.inventory is PlayerInventory) {
if (slot.index in lockedSlots) {
lockedSlots.remove(slot.index)
} else {
lockedSlots.add(slot.index)
}
DConfig.markDirty()
CommonSoundEffects.playSuccess()
}
}
@Subscribe
fun onRenderSlotOverlay(it: SlotRenderEvents.After) {
val isSlotLocked = it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())
val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf())
if (isSlotLocked || isUUIDLocked) {
RenderSystem.disableDepthTest()
it.context.drawSprite(
it.slot.x, it.slot.y, 0,
16, 16,
MC.guiAtlasManager.getSprite(
when {
isSlotLocked ->
(Identifier.of("firmament:slot_locked"))
isUUIDLocked ->
(Identifier.of("firmament:uuid_locked"))
else ->
error("unreachable")
}
)
)
RenderSystem.enableDepthTest()
}
}
}

View File

@@ -0,0 +1,85 @@
package moe.nea.firmament.features.inventory.buttons
import com.mojang.brigadier.StringReader
import me.shedaniel.math.Dimension
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import kotlinx.serialization.Serializable
import net.minecraft.client.gui.DrawContext
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.command.argument.ItemStackArgumentType
import net.minecraft.item.ItemStack
import net.minecraft.resource.featuretoggle.FeatureFlags
import net.minecraft.util.Identifier
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.memoize
@Serializable
data class InventoryButton(
var x: Int,
var y: Int,
var anchorRight: Boolean,
var anchorBottom: Boolean,
var icon: String? = "",
var command: String? = "",
) {
companion object {
val itemStackParser by lazy {
ItemStackArgumentType.itemStack(CommandRegistryAccess.of(MC.defaultRegistries,
FeatureFlags.VANILLA_FEATURES))
}
val dimensions = Dimension(18, 18)
val getItemForName = ::getItemForName0.memoize(1024)
fun getItemForName0(icon: String): ItemStack {
val repoItem = RepoManager.getNEUItem(SkyblockId(icon))
var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon))
if (repoItem == null) {
val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give"))
icon.split(" ", limit = 3).getOrNull(2) ?: icon
else icon
val componentItem =
runCatching {
itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false)
}.getOrNull()
if (componentItem != null)
itemStack = componentItem
}
return itemStack
}
}
fun render(context: DrawContext) {
context.drawSprite(
0,
0,
0,
dimensions.width,
dimensions.height,
MC.guiAtlasManager.getSprite(Identifier.of("firmament:inventory_button_background"))
)
context.drawItem(getItem(), 1, 1)
}
fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank()
fun getPosition(guiRect: Rectangle): Point {
return Point(
(if (anchorRight) guiRect.maxX else guiRect.minX) + x,
(if (anchorBottom) guiRect.maxY else guiRect.minY) + y,
)
}
fun getBounds(guiRect: Rectangle): Rectangle {
return Rectangle(getPosition(guiRect), dimensions)
}
fun getItem(): ItemStack {
return getItemForName(icon ?: "")
}
}

View File

@@ -0,0 +1,184 @@
package moe.nea.firmament.features.inventory.buttons
import io.github.notenoughupdates.moulconfig.common.IItemStack
import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
import io.github.notenoughupdates.moulconfig.xml.Bind
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import org.lwjgl.glfw.GLFW
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.ButtonWidget
import net.minecraft.client.util.InputUtil
import net.minecraft.text.Text
import net.minecraft.util.math.MathHelper
import net.minecraft.util.math.Vec2f
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.FragmentGuiScreen
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
class InventoryButtonEditor(
val lastGuiRect: Rectangle,
) : FragmentGuiScreen() {
inner class Editor(val originalButton: InventoryButton) {
@field:Bind
var command: String = originalButton.command ?: ""
@field:Bind
var icon: String = originalButton.icon ?: ""
@Bind
fun getItemIcon(): IItemStack {
save()
return ModernItemStack.of(InventoryButton.getItemForName(icon))
}
@Bind
fun delete() {
buttons.removeIf { it === originalButton }
popup = null
}
fun save() {
originalButton.icon = icon
originalButton.command = command
}
}
var buttons: MutableList<InventoryButton> =
InventoryButtons.DConfig.data.buttons.map { it.copy() }.toMutableList()
override fun close() {
InventoryButtons.DConfig.data.buttons = buttons
InventoryButtons.DConfig.markDirty()
super.close()
}
override fun init() {
super.init()
addDrawableChild(
ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) {
val t = ClipboardUtils.getTextContents()
val newButtons = InventoryButtonTemplates.loadTemplate(t)
if (newButtons != null)
buttons = newButtons.toMutableList()
}
.position(lastGuiRect.minX + 10, lastGuiRect.minY + 35)
.width(lastGuiRect.width - 20)
.build()
)
addDrawableChild(
ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.save-preset")) {
ClipboardUtils.setTextContent(InventoryButtonTemplates.saveTemplate(buttons))
}
.position(lastGuiRect.minX + 10, lastGuiRect.minY + 60)
.width(lastGuiRect.width - 20)
.build()
)
}
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.matrices.push()
context.matrices.translate(0f, 0f, -10f)
context.fill(lastGuiRect.minX, lastGuiRect.minY, lastGuiRect.maxX, lastGuiRect.maxY, -1)
context.setShaderColor(1f, 1f, 1f, 1f)
context.matrices.pop()
for (button in buttons) {
val buttonPosition = button.getBounds(lastGuiRect)
context.matrices.push()
context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat(), 0F)
button.render(context)
context.matrices.pop()
}
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (super.keyPressed(keyCode, scanCode, modifiers)) return true
if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
close()
return true
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (super.mouseReleased(mouseX, mouseY, button)) return true
val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) }
if (clickedButton != null && !justPerformedAClickAction) {
createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY))
return true
}
justPerformedAClickAction = false
lastDraggedButton = null
return false
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true
if (initialDragMousePosition.distanceSquared(Vec2f(mouseX.toFloat(), mouseY.toFloat())) >= 4 * 4) {
initialDragMousePosition = Vec2f(-10F, -10F)
lastDraggedButton?.let { dragging ->
justPerformedAClickAction = true
val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mouseX.toInt(), mouseY.toInt())
?: return true
dragging.x = offsetX
dragging.y = offsetY
dragging.anchorRight = anchorRight
dragging.anchorBottom = anchorBottom
}
}
return false
}
var lastDraggedButton: InventoryButton? = null
var justPerformedAClickAction = false
var initialDragMousePosition = Vec2f(-10F, -10F)
data class AnchoredCoords(
val anchorRight: Boolean,
val anchorBottom: Boolean,
val offsetX: Int,
val offsetY: Int,
)
fun getCoordsForMouse(mx: Int, my: Int): AnchoredCoords? {
if (lastGuiRect.contains(mx, my) || lastGuiRect.contains(
Point(
mx + InventoryButton.dimensions.width,
my + InventoryButton.dimensions.height,
)
)
) return null
val anchorRight = mx > lastGuiRect.maxX
val anchorBottom = my > lastGuiRect.maxY
var offsetX = mx - if (anchorRight) lastGuiRect.maxX else lastGuiRect.minX
var offsetY = my - if (anchorBottom) lastGuiRect.maxY else lastGuiRect.minY
if (InputUtil.isKeyPressed(MC.window.handle, InputUtil.GLFW_KEY_LEFT_SHIFT)) {
offsetX = MathHelper.floor(offsetX / 20F) * 20
offsetY = MathHelper.floor(offsetY / 20F) * 20
}
return AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (super.mouseClicked(mouseX, mouseY, button)) return true
val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) }
if (clickedButton != null) {
lastDraggedButton = clickedButton
initialDragMousePosition = Vec2f(mouseX.toFloat(), mouseY.toFloat())
return true
}
val mx = mouseX.toInt()
val my = mouseY.toInt()
val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mx, my) ?: return true
buttons.add(InventoryButton(offsetX, offsetY, anchorRight, anchorBottom, null, null))
justPerformedAClickAction = true
return true
}
}

View File

@@ -0,0 +1,35 @@
package moe.nea.firmament.features.inventory.buttons
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TemplateUtil
object InventoryButtonTemplates {
val legacyPrefix = "NEUBUTTONS/"
val modernPrefix = "MAYBEONEDAYIWILLHAVEMYOWNFORMAT"
fun loadTemplate(t: String): List<InventoryButton>? {
val buttons = TemplateUtil.maybeDecodeTemplate<List<String>>(legacyPrefix, t) ?: return null
return buttons.mapNotNull {
try {
Firmament.json.decodeFromString<InventoryButton>(it).also {
if (it.icon?.startsWith("extra:") == true || it.command?.any { it.isLowerCase() } == true) {
MC.sendChat(Text.translatable("firmament.inventory-buttons.import-failed"))
}
}
} catch (e: Exception) {
null
}
}
}
fun saveTemplate(buttons: List<InventoryButton>): String {
return TemplateUtil.encodeTemplate(legacyPrefix, buttons.map { Firmament.json.encodeToString(it) })
}
}

View File

@@ -0,0 +1,88 @@
package moe.nea.firmament.features.inventory.buttons
import me.shedaniel.math.Rectangle
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenClickEvent
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenPushREIEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.data.DataHolder
import moe.nea.firmament.util.getRectangle
object InventoryButtons : FirmamentFeature {
override val identifier: String
get() = "inventory-buttons"
object TConfig : ManagedConfig(identifier) {
val _openEditor by button("open-editor") {
openEditor()
}
}
object DConfig : DataHolder<Data>(serializer(), identifier, ::Data)
@Serializable
data class Data(
var buttons: MutableList<InventoryButton> = mutableListOf()
)
override val config: ManagedConfig
get() = TConfig
fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() }
@Subscribe
fun onRectangles(it: HandledScreenPushREIEvent) {
val bounds = it.screen.getRectangle()
for (button in getValidButtons()) {
val buttonBounds = button.getBounds(bounds)
it.block(buttonBounds)
}
}
@Subscribe
fun onClickScreen(it: HandledScreenClickEvent) {
val bounds = it.screen.getRectangle()
for (button in getValidButtons()) {
val buttonBounds = button.getBounds(bounds)
if (buttonBounds.contains(it.mouseX, it.mouseY)) {
MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */)
break
}
}
}
@Subscribe
fun onRenderForeground(it: HandledScreenForegroundEvent) {
val bounds = it.screen.getRectangle()
for (button in getValidButtons()) {
val buttonBounds = button.getBounds(bounds)
it.context.matrices.push()
it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F)
button.render(it.context)
it.context.matrices.pop()
}
lastRectangle = bounds
}
var lastRectangle: Rectangle? = null
fun openEditor() {
ScreenUtil.setScreenLater(
InventoryButtonEditor(
lastRectangle ?: Rectangle(
MC.window.scaledWidth / 2 - 100,
MC.window.scaledHeight / 2 - 100,
200, 200,
)
)
)
}
}

View File

@@ -0,0 +1,53 @@
package moe.nea.firmament.features.inventory.storageoverlay
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.screen.GenericContainerScreenHandler
import moe.nea.firmament.util.ifMatches
import moe.nea.firmament.util.unformattedString
/**
* A handle representing the state of the "server side" screens.
*/
sealed interface StorageBackingHandle {
sealed interface HasBackingScreen {
val handler: GenericContainerScreenHandler
}
/**
* The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage`
*/
data class Overview(override val handler: GenericContainerScreenHandler) : StorageBackingHandle, HasBackingScreen
/**
* An individual storage page is open. This may be a backpack or an enderchest page. This page is accessible via
* the [Overview] or via `/ec <index + 1>` for enderchest pages.
*/
data class Page(override val handler: GenericContainerScreenHandler, val storagePageSlot: StoragePageSlot) :
StorageBackingHandle, HasBackingScreen
companion object {
private val enderChestName = "^Ender Chest \\(([1-9])/[1-9]\\)$".toRegex()
private val backPackName = "^.+Backpack \\(Slot #([0-9]+)\\)$".toRegex()
/**
* Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not
* representable as a [StorageBackingHandle], meaning another screen is open, for example the enderchest icon
* selection screen.
*/
fun fromScreen(screen: Screen?): StorageBackingHandle? {
if (screen == null) return null
if (screen !is GenericContainerScreen) return null
val title = screen.title.unformattedString
if (title == "Storage") return Overview(screen.screenHandler)
return title.ifMatches(enderChestName) {
Page(screen.screenHandler, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt()))
} ?: title.ifMatches(backPackName) {
Page(screen.screenHandler, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt()))
}
}
}
}

View File

@@ -0,0 +1,21 @@
@file:UseSerializers(SortedMapSerializer::class)
package moe.nea.firmament.features.inventory.storageoverlay
import java.util.SortedMap
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import moe.nea.firmament.util.SortedMapSerializer
@Serializable
data class StorageData(
val storageInventories: SortedMap<StoragePageSlot, StorageInventory> = sortedMapOf()
) {
@Serializable
data class StorageInventory(
var title: String,
val slot: StoragePageSlot,
var inventory: VirtualInventory?,
)
}

View File

@@ -0,0 +1,154 @@
package moe.nea.firmament.features.inventory.storageoverlay
import java.util.SortedMap
import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.Items
import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotClickEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.customgui.customGui
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
object StorageOverlay : FirmamentFeature {
object Data : ProfileSpecificDataHolder<StorageData>(serializer(), "storage-data", ::StorageData)
override val identifier: String
get() = "storage-overlay"
object TConfig : ManagedConfig(identifier) {
val alwaysReplace by toggle("always-replace") { true }
val columns by integer("rows", 1, 10) { 3 }
val scrollSpeed by integer("scroll-speed", 1, 50) { 10 }
val inverseScroll by toggle("inverse-scroll") { false }
val padding by integer("padding", 1, 20) { 5 }
val margin by integer("margin", 1, 60) { 20 }
}
fun adjustScrollSpeed(amount: Double): Double {
return amount * TConfig.scrollSpeed * (if (TConfig.inverseScroll) 1 else -1)
}
override val config: TConfig
get() = TConfig
var lastStorageOverlay: StorageOverviewScreen? = null
var skipNextStorageOverlayBackflip = false
var currentHandler: StorageBackingHandle? = null
@Subscribe
fun onTick(event: TickEvent) {
rememberContent(currentHandler ?: return)
}
@Subscribe
fun onClick(event: SlotClickEvent) {
if (lastStorageOverlay != null && event.slot.inventory !is PlayerInventory && event.slot.index < 9
&& event.stack.item != Items.BLACK_STAINED_GLASS_PANE
) {
skipNextStorageOverlayBackflip = true
}
}
@Subscribe
fun onScreenChange(it: ScreenChangeEvent) {
if (it.old == null && it.new == null) return
val storageOverlayScreen = it.old as? StorageOverlayScreen
?: ((it.old as? HandledScreen<*>)?.customGui as? StorageOverlayCustom)?.overview
var storageOverviewScreen = it.old as? StorageOverviewScreen
val screen = it.new as? GenericContainerScreen
val oldHandler = currentHandler
currentHandler = StorageBackingHandle.fromScreen(screen)
rememberContent(currentHandler)
if (storageOverviewScreen != null && oldHandler is StorageBackingHandle.HasBackingScreen) {
val player = MC.player
assert(player != null)
player?.networkHandler?.sendPacket(CloseHandledScreenC2SPacket(oldHandler.handler.syncId))
if (player?.currentScreenHandler === oldHandler.handler) {
player.currentScreenHandler = player.playerScreenHandler
}
}
storageOverviewScreen = storageOverviewScreen ?: lastStorageOverlay
if (it.new == null && storageOverlayScreen != null && !storageOverlayScreen.isExiting) {
it.overrideScreen = storageOverlayScreen
return
}
if (storageOverviewScreen != null
&& !storageOverviewScreen.isClosing
&& (currentHandler is StorageBackingHandle.Overview || currentHandler == null)
) {
if (skipNextStorageOverlayBackflip) {
skipNextStorageOverlayBackflip = false
} else {
it.overrideScreen = storageOverviewScreen
lastStorageOverlay = null
}
return
}
screen ?: return
screen.customGui = StorageOverlayCustom(
currentHandler ?: return,
screen,
storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return))
}
fun rememberContent(handler: StorageBackingHandle?) {
handler ?: return
// TODO: Make all of these functions work on deltas / updates instead of the entire contents
val data = Data.data?.storageInventories ?: return
when (handler) {
is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data)
is StorageBackingHandle.Page -> rememberPage(handler, data)
}
Data.markDirty()
}
private fun rememberStorageOverview(
handler: StorageBackingHandle.Overview,
data: SortedMap<StoragePageSlot, StorageData.StorageInventory>
) {
for ((index, stack) in handler.handler.stacks.withIndex()) {
// Ignore unloaded item stacks
if (stack.isEmpty) continue
val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue
val isEmpty = stack.item in StorageOverviewScreen.emptyStorageSlotItems
if (slot in data) {
if (isEmpty)
data.remove(slot)
continue
}
if (!isEmpty) {
data[slot] = StorageData.StorageInventory(slot.defaultName(), slot, null)
}
}
}
private fun rememberPage(
handler: StorageBackingHandle.Page,
data: SortedMap<StoragePageSlot, StorageData.StorageInventory>
) {
// TODO: FIXME: FIXME NOW: Definitely don't copy all of this every tick into persistence
val newStacks =
VirtualInventory(handler.handler.stacks.take(handler.handler.rows * 9).drop(9).map { it.copy() })
data.compute(handler.storagePageSlot) { slot, existingInventory ->
(existingInventory ?: StorageData.StorageInventory(
slot.defaultName(),
slot,
null
)).also {
it.inventory = newStacks
}
}
}
}

View File

@@ -0,0 +1,98 @@
package moe.nea.firmament.features.inventory.storageoverlay
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.slot.Slot
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.customgui.CustomGui
class StorageOverlayCustom(
val handler: StorageBackingHandle,
val screen: GenericContainerScreen,
val overview: StorageOverlayScreen,
) : CustomGui() {
override fun onVoluntaryExit(): Boolean {
overview.isExiting = true
return super.onVoluntaryExit()
}
override fun getBounds(): List<Rectangle> {
return overview.getBounds()
}
override fun afterSlotRender(context: DrawContext, slot: Slot) {
if (slot.inventory !is PlayerInventory)
context.disableScissor()
}
override fun beforeSlotRender(context: DrawContext, slot: Slot) {
if (slot.inventory !is PlayerInventory)
overview.createScissors(context)
}
override fun onInit() {
overview.init(MinecraftClient.getInstance(), screen.width, screen.height)
overview.init()
screen as AccessorHandledScreen
screen.x_Firmament = overview.measurements.x
screen.y_Firmament = overview.measurements.y
screen.backgroundWidth_Firmament = overview.measurements.totalWidth
screen.backgroundHeight_Firmament = overview.measurements.totalHeight
}
override fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean {
if (!super.isPointOverSlot(slot, xOffset, yOffset, pointX, pointY))
return false
if (slot.inventory !is PlayerInventory) {
if (!overview.getScrollPanelInner().contains(pointX, pointY))
return false
}
return true
}
override fun shouldDrawForeground(): Boolean {
return false
}
override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
return overview.mouseClicked(mouseX, mouseY, button, (handler as? StorageBackingHandle.Page)?.storagePageSlot)
}
override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) {
overview.drawBackgrounds(drawContext)
overview.drawPages(drawContext,
mouseX,
mouseY,
delta,
(handler as? StorageBackingHandle.Page)?.storagePageSlot,
screen.screenHandler.slots.take(screen.screenHandler.rows * 9).drop(9),
Point((screen as AccessorHandledScreen).x_Firmament, screen.y_Firmament))
overview.drawScrollBar(drawContext)
}
override fun moveSlot(slot: Slot) {
val index = slot.index
if (index in 0..<36) {
val (x, y) = overview.getPlayerInventorySlotPosition(index)
slot.x = x - (screen as AccessorHandledScreen).x_Firmament
slot.y = y - screen.y_Firmament
} else {
slot.x = -100000
slot.y = -100000
}
}
override fun mouseScrolled(
mouseX: Double,
mouseY: Double,
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
}

View File

@@ -0,0 +1,296 @@
package moe.nea.firmament.features.inventory.storageoverlay
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.screen.slot.Slot
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.assertTrueOr
class StorageOverlayScreen : Screen(Text.literal("")) {
companion object {
val PLAYER_WIDTH = 184
val PLAYER_HEIGHT = 91
val PLAYER_Y_INSET = 3
val SLOT_SIZE = 18
val PADDING = 10
val PAGE_WIDTH = SLOT_SIZE * 9
val HOTBAR_X = 12
val HOTBAR_Y = 67
val MAIN_INVENTORY_Y = 9
val SCROLL_BAR_WIDTH = 8
val SCROLL_BAR_HEIGHT = 16
}
var isExiting: Boolean = false
var scroll: Float = 0F
var pageWidthCount = StorageOverlay.TConfig.columns
inner class Measurements {
val innerScrollPanelWidth = PAGE_WIDTH * pageWidthCount + (pageWidthCount - 1) * PADDING
val overviewWidth = innerScrollPanelWidth + 3 * PADDING + SCROLL_BAR_WIDTH
val x = width / 2 - overviewWidth / 2
val overviewHeight = minOf(3 * 18 * 6, height - PLAYER_HEIGHT - minOf(80, height / 10))
val innerScrollPanelHeight = overviewHeight - PADDING * 2
val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2
val playerX = width / 2 - PLAYER_WIDTH / 2
val playerY = y + overviewHeight - PLAYER_Y_INSET
val totalWidth = overviewWidth
val totalHeight = overviewHeight - PLAYER_Y_INSET + PLAYER_HEIGHT
}
var measurements = Measurements()
var lastRenderedInnerHeight = 0
public override fun init() {
super.init()
pageWidthCount = StorageOverlay.TConfig.columns
.coerceAtMost((width - PADDING) / (PAGE_WIDTH + PADDING))
.coerceAtLeast(1)
measurements = Measurements()
}
override fun mouseScrolled(
mouseX: Double,
mouseY: Double,
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
scroll = (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toFloat()
.coerceAtMost(getMaxScroll())
.coerceAtLeast(0F)
return true
}
fun getMaxScroll() = lastRenderedInnerHeight.toFloat() - getScrollPanelInner().height
val playerInventorySprite = Identifier.of("firmament:storageoverlay/player_inventory")
val upperBackgroundSprite = Identifier.of("firmament:storageoverlay/upper_background")
val slotRowSprite = Identifier.of("firmament:storageoverlay/storage_row")
val scrollbarBackground = Identifier.of("firmament:storageoverlay/scroll_bar_background")
val scrollbarKnob = Identifier.of("firmament:storageoverlay/scroll_bar_knob")
override fun close() {
isExiting = true
super.close()
}
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
drawBackgrounds(context)
drawPages(context, mouseX, mouseY, delta, null, null, Point())
drawScrollBar(context)
drawPlayerInventory(context, mouseX, mouseY, delta)
}
fun getScrollbarPercentage(): Float {
return scroll / getMaxScroll()
}
fun drawScrollBar(context: DrawContext) {
val sbRect = getScrollBarRect()
context.drawGuiTexture(
scrollbarBackground,
sbRect.minX, sbRect.minY,
sbRect.width, sbRect.height,
)
context.drawGuiTexture(
scrollbarKnob,
sbRect.minX, sbRect.minY + (getScrollbarPercentage() * (sbRect.height - SCROLL_BAR_HEIGHT)).toInt(),
SCROLL_BAR_WIDTH, SCROLL_BAR_HEIGHT
)
}
fun drawBackgrounds(context: DrawContext) {
context.drawGuiTexture(upperBackgroundSprite,
measurements.x,
measurements.y,
0,
measurements.overviewWidth,
measurements.overviewHeight)
context.drawGuiTexture(playerInventorySprite,
measurements.playerX,
measurements.playerY,
0,
PLAYER_WIDTH,
PLAYER_HEIGHT)
}
fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> {
if (int < 9) {
return Pair(measurements.playerX + int * SLOT_SIZE + HOTBAR_X, HOTBAR_Y + measurements.playerY)
}
return Pair(
measurements.playerX + (int % 9) * SLOT_SIZE + HOTBAR_X,
measurements.playerY + (int / 9 - 1) * SLOT_SIZE + MAIN_INVENTORY_Y
)
}
fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
val items = MC.player?.inventory?.main ?: return
items.withIndex().forEach { (index, item) ->
val (x, y) = getPlayerInventorySlotPosition(index)
context.drawItem(item, x, y, 0)
context.drawItemInSlot(textRenderer, item, x, y)
}
}
fun getScrollBarRect(): Rectangle {
return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
measurements.y + PADDING,
SCROLL_BAR_WIDTH,
measurements.innerScrollPanelHeight)
}
fun getScrollPanelInner(): Rectangle {
return Rectangle(measurements.x + PADDING,
measurements.y + PADDING,
measurements.innerScrollPanelWidth,
measurements.innerScrollPanelHeight)
}
fun createScissors(context: DrawContext) {
val rect = getScrollPanelInner()
context.enableScissor(
rect.minX, rect.minY,
rect.maxX, rect.maxY
)
}
fun drawPages(
context: DrawContext, mouseX: Int, mouseY: Int, delta: Float,
excluding: StoragePageSlot?,
slots: List<Slot>?,
slotOffset: Point
) {
createScissors(context)
val data = StorageOverlay.Data.data ?: StorageData()
layoutedForEach(data) { rect, page, inventory ->
drawPage(context,
rect.x,
rect.y,
page, inventory,
if (excluding == page) slots else null,
slotOffset
)
}
context.disableScissor()
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
return mouseClicked(mouseX, mouseY, button, null)
}
fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean {
if (getScrollPanelInner().contains(mouseX, mouseY)) {
val data = StorageOverlay.Data.data ?: StorageData()
layoutedForEach(data) { rect, page, _ ->
if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) {
page.navigateTo()
return true
}
}
return false
}
val sbRect = getScrollBarRect()
if (sbRect.contains(mouseX, mouseY)) {
// TODO: support dragging of the mouse and such
val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight()
scroll = (getMaxScroll() * percentage).toFloat()
mouseScrolled(0.0, 0.0, 0.0, 0.0)
return true
}
return false
}
private inline fun layoutedForEach(
data: StorageData,
func: (
rectangle: Rectangle,
page: StoragePageSlot, inventory: StorageData.StorageInventory,
) -> Unit
) {
var yOffset = -scroll.toInt()
var xOffset = 0
var maxHeight = 0
for ((page, inventory) in data.storageInventories.entries) {
val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight }
?: 18
maxHeight = maxOf(maxHeight, currentHeight)
val rect = Rectangle(
measurements.x + PADDING + (PAGE_WIDTH + PADDING) * xOffset,
yOffset + measurements.y + PADDING,
PAGE_WIDTH,
currentHeight
)
func(rect, page, inventory)
xOffset++
if (xOffset >= pageWidthCount) {
yOffset += maxHeight
xOffset = 0
maxHeight = 0
}
}
lastRenderedInnerHeight = maxHeight + yOffset + scroll.toInt()
}
fun drawPage(
context: DrawContext,
x: Int,
y: Int,
page: StoragePageSlot,
inventory: StorageData.StorageInventory,
slots: List<Slot>?,
slotOffset: Point,
): Int {
val inv = inventory.inventory
if (inv == null) {
context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18)
context.drawText(textRenderer,
Text.literal("TODO: open this page"),
x + 4,
y + 4,
-1,
true)
return 18
}
assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 }
val name = page.defaultName()
context.drawText(textRenderer, Text.literal(name), x + 4, y + 2,
if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true)
context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE)
inv.stacks.forEachIndexed { index, stack ->
val slotX = (index % 9) * SLOT_SIZE + x + 1
val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1
if (slots == null) {
context.drawItem(stack, slotX, slotY)
context.drawItemInSlot(textRenderer, stack, slotX, slotY)
} else {
val slot = slots[index]
slot.x = slotX - slotOffset.x
slot.y = slotY - slotOffset.y
}
}
return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight
}
fun getBounds(): List<Rectangle> {
return listOf(
Rectangle(measurements.x,
measurements.y,
measurements.overviewWidth,
measurements.overviewHeight),
Rectangle(measurements.playerX,
measurements.playerY,
PLAYER_WIDTH,
PLAYER_HEIGHT))
}
}

View File

@@ -0,0 +1,123 @@
package moe.nea.firmament.features.inventory.storageoverlay
import org.lwjgl.glfw.GLFW
import kotlin.math.max
import net.minecraft.block.Blocks
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.item.Item
import net.minecraft.item.Items
import net.minecraft.text.Text
import net.minecraft.util.DyeColor
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.toShedaniel
class StorageOverviewScreen() : Screen(Text.empty()) {
companion object {
val emptyStorageSlotItems = listOf<Item>(
Blocks.RED_STAINED_GLASS_PANE.asItem(),
Blocks.BROWN_STAINED_GLASS_PANE.asItem(),
Items.GRAY_DYE
)
val pageWidth get() = 19 * 9
}
val content = StorageOverlay.Data.data ?: StorageData()
var isClosing = false
var scroll = 0
var lastRenderedHeight = 0
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.fill(0, 0, width, height, 0x90000000.toInt())
layoutedForEach { (key, value), offsetX, offsetY ->
context.matrices.push()
context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F)
renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY)
context.matrices.pop()
}
}
inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) {
var offsetY = 0
var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll
var totalHeight = -currentMaxHeight
content.storageInventories.onEachIndexed { index, (key, value) ->
val pageX = (index % StorageOverlay.config.columns)
if (pageX == 0) {
currentMaxHeight += StorageOverlay.config.padding
offsetY += currentMaxHeight
totalHeight += currentMaxHeight
currentMaxHeight = 0
}
val xPosition =
width / 2 - (StorageOverlay.config.columns * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding)
onEach(Pair(key, value), xPosition, offsetY)
val height = getStorePageHeight(value)
currentMaxHeight = max(currentMaxHeight, height)
}
lastRenderedHeight = totalHeight + currentMaxHeight
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
layoutedForEach { (k, p), x, y ->
val rx = mouseX - x
val ry = mouseY - y
if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) {
close()
StorageOverlay.lastStorageOverlay = this
k.navigateTo()
return true
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
fun getStorePageHeight(page: StorageData.StorageInventory): Int {
return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60
}
override fun mouseScrolled(
mouseX: Double,
mouseY: Double,
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
scroll =
(scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt()
.coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0)
return true
}
private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
context.drawText(MC.font, page.title, 2, 2, -1, true)
val inventory = page.inventory
if (inventory == null) {
// TODO: Missing texture
context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color)
context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1)
return
}
for ((index, stack) in inventory.stacks.withIndex()) {
val x = (index % 9) * 19
val y = (index / 9) * 19 + MC.font.fontHeight + 2
if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) {
context.fill(x, y, x + 18, y + 18, 0x80808080.toInt())
} else {
context.fill(x, y, x + 18, y + 18, 0x40808080.toInt())
}
context.drawItem(stack, x + 1, y + 1)
context.drawItemInSlot(MC.font, stack, x + 1, y + 1)
}
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (keyCode == GLFW.GLFW_KEY_ESCAPE)
isClosing = true
return super.keyPressed(keyCode, scanCode, modifiers)
}
}

View File

@@ -0,0 +1,66 @@
package moe.nea.firmament.features.inventory.storageoverlay
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import moe.nea.firmament.util.MC
@Serializable(with = StoragePageSlot.Serializer::class)
data class StoragePageSlot(val index: Int) : Comparable<StoragePageSlot> {
object Serializer : KSerializer<StoragePageSlot> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StoragePageSlot", PrimitiveKind.INT)
override fun deserialize(decoder: Decoder): StoragePageSlot {
return StoragePageSlot(decoder.decodeInt())
}
override fun serialize(encoder: Encoder, value: StoragePageSlot) {
encoder.encodeInt(value.index)
}
}
init {
assert(index in 0 until (3 * 9))
}
val isEnderChest get() = index < 9
val isBackPack get() = !isEnderChest
val slotIndexInOverviewPage get() = if (isEnderChest) index + 9 else index + 18
fun defaultName(): String = if (isEnderChest) "Ender Chest #${index + 1}" else "Backpack #${index - 9 + 1}"
fun navigateTo() {
if (isBackPack) {
MC.sendCommand("backpack ${index - 9 + 1}")
} else {
MC.sendCommand("enderchest ${index + 1}")
}
}
companion object {
fun fromOverviewSlotIndex(slot: Int): StoragePageSlot? {
if (slot in 9 until 18) return StoragePageSlot(slot - 9)
if (slot in 27 until 45) return StoragePageSlot(slot - 27 + 9)
return null
}
fun ofEnderChestPage(slot: Int): StoragePageSlot {
assert(slot in 1..9)
return StoragePageSlot(slot - 1)
}
fun ofBackPackPage(slot: Int): StoragePageSlot {
assert(slot in 1..18)
return StoragePageSlot(slot - 1 + 9)
}
}
override fun compareTo(other: StoragePageSlot): Int {
return this.index - other.index
}
}

View File

@@ -0,0 +1,65 @@
package moe.nea.firmament.features.inventory.storageoverlay
import io.ktor.util.decodeBase64Bytes
import io.ktor.util.encodeBase64
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtIo
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtSizeTracker
@Serializable(with = VirtualInventory.Serializer::class)
data class VirtualInventory(
val stacks: List<ItemStack>
) {
val rows = stacks.size / 9
init {
assert(stacks.size % 9 == 0)
assert(stacks.size / 9 in 1..5)
}
object Serializer : KSerializer<VirtualInventory> {
const val INVENTORY = "INVENTORY"
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
return VirtualInventory(items.map {
it as NbtCompound
if (it.isEmpty) ItemStack.EMPTY
else runCatching {
ItemStack.CODEC.parse(NbtOps.INSTANCE, it).orThrow
}.getOrElse { ItemStack.EMPTY }
})
}
override fun serialize(encoder: Encoder, value: VirtualInventory) {
val list = NbtList()
value.stacks.forEach {
if (it.isEmpty) list.add(NbtCompound())
else list.add(runCatching { ItemStack.CODEC.encode(it, NbtOps.INSTANCE, NbtCompound()).orThrow }
.getOrElse { NbtCompound() })
}
val baos = ByteArrayOutputStream()
NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos)
encoder.encodeString(baos.toByteArray().encodeBase64())
}
}
}

View File

@@ -0,0 +1,81 @@
package moe.nea.firmament.features.mining
import java.util.*
import kotlin.time.Duration
import moe.nea.firmament.util.TimeMark
class Histogram<T>(
val maxSize: Int,
val maxDuration: Duration,
) {
data class OrderedTimestamp(val timestamp: TimeMark, val order: Int) : Comparable<OrderedTimestamp> {
override fun compareTo(other: OrderedTimestamp): Int {
val o = timestamp.compareTo(other.timestamp)
if (o != 0) return o
return order.compareTo(other.order)
}
}
val size: Int get() = dataPoints.size
private val dataPoints: NavigableMap<OrderedTimestamp, T> = TreeMap()
private var order = Int.MIN_VALUE
fun record(entry: T, timestamp: TimeMark = TimeMark.now()) {
dataPoints[OrderedTimestamp(timestamp, order++)] = entry
trim()
}
fun oldestUpdate(): TimeMark {
trim()
return if (dataPoints.isEmpty()) TimeMark.now() else dataPoints.firstKey().timestamp
}
fun latestUpdate(): TimeMark {
trim()
return if (dataPoints.isEmpty()) TimeMark.farPast() else dataPoints.lastKey().timestamp
}
fun averagePer(valueExtractor: (T) -> Double, perDuration: Duration): Double? {
return aggregate(
seed = 0.0,
operator = { accumulator, entry, _ -> accumulator + valueExtractor(entry) },
finish = { sum, beginning, end ->
val timespan = end - beginning
if (timespan > perDuration)
sum / (timespan / perDuration)
else null
})
}
fun <V, R> aggregate(
seed: V,
operator: (V, T, TimeMark) -> V,
finish: (V, TimeMark, TimeMark) -> R
): R? {
trim()
var accumulator = seed
var min: TimeMark? = null
var max: TimeMark? = null
dataPoints.forEach { (key, value) ->
max = key.timestamp
if (min == null)
min = key.timestamp
accumulator = operator(accumulator, value, key.timestamp)
}
if (min == null)
return null
return finish(accumulator, min!!, max!!)
}
private fun trim() {
while (maxSize < dataPoints.size) {
dataPoints.pollFirstEntry()
}
dataPoints.headMap(OrderedTimestamp(TimeMark.ago(maxDuration), Int.MAX_VALUE)).clear()
}
}

View File

@@ -0,0 +1,176 @@
package moe.nea.firmament.features.mining
import java.util.regex.Pattern
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import net.minecraft.item.ItemStack
import net.minecraft.util.DyeColor
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.SlotClickEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.DurabilityBarEvent
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.TIME_PATTERN
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.extraAttributes
import moe.nea.firmament.util.item.displayNameAccordingToNbt
import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.parseTimePattern
import moe.nea.firmament.util.render.RenderCircleProgress
import moe.nea.firmament.util.render.lerp
import moe.nea.firmament.util.toShedaniel
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
object PickaxeAbility : FirmamentFeature {
override val identifier: String
get() = "pickaxe-info"
object TConfig : ManagedConfig(identifier) {
val cooldownEnabled by toggle("ability-cooldown") { true }
val cooldownScale by integer("ability-scale", 16, 64) { 16 }
val drillFuelBar by toggle("fuel-bar") { true }
}
var lobbyJoinTime = TimeMark.farPast()
var lastUsage = mutableMapOf<String, TimeMark>()
var abilityOverride: String? = null
var defaultAbilityDurations = mutableMapOf<String, Duration>(
"Mining Speed Boost" to 120.seconds,
"Pickobulus" to 110.seconds,
"Gemstone Infusion" to 140.seconds,
"Hazardous Miner" to 140.seconds,
"Maniac Miner" to 59.seconds,
"Vein Seeker" to 60.seconds
)
override val config: ManagedConfig
get() = TConfig
fun getCooldownPercentage(name: String, cooldown: Duration): Double {
val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE
if (sinceLastUsage < cooldown)
return sinceLastUsage / cooldown
val sinceLobbyJoin = lobbyJoinTime.passedTime()
val halfCooldown = cooldown / 2
if (sinceLobbyJoin < halfCooldown) {
return (sinceLobbyJoin / halfCooldown)
}
return 1.0
}
@Subscribe
fun onSlotClick(it: SlotClickEvent) {
if (MC.screen?.title?.unformattedString == "Heart of the Mountain") {
val name = it.stack.displayNameAccordingToNbt?.unformattedString ?: return
val cooldown = it.stack.loreAccordingToNbt.firstNotNullOfOrNull {
cooldownPattern.useMatch(it.unformattedString) {
parseTimePattern(group("cooldown"))
}
} ?: return
defaultAbilityDurations[name] = cooldown
}
}
@Subscribe
fun onDurabilityBar(it: DurabilityBarEvent) {
if (!TConfig.drillFuelBar) return
val lore = it.item.loreAccordingToNbt
if (lore.lastOrNull()?.unformattedString?.contains("DRILL") != true) return
val maxFuel = lore.firstNotNullOfOrNull {
fuelPattern.useMatch(it.unformattedString) {
parseShortNumber(group("maxFuel"))
}
} ?: return
val extra = it.item.extraAttributes
if (!extra.contains("drill_fuel")) return
val fuel = extra.getInt("drill_fuel")
val percentage = fuel / maxFuel.toFloat()
it.barOverride = DurabilityBarEvent.DurabilityBar(
lerp(
DyeColor.RED.toShedaniel(),
DyeColor.GREEN.toShedaniel(),
percentage
), percentage
)
}
@Subscribe
fun onChatMessage(it: ProcessChatEvent) {
abilityUsePattern.useMatch(it.unformattedString) {
lastUsage[group("name")] = TimeMark.now()
}
abilitySwitchPattern.useMatch(it.unformattedString) {
abilityOverride = group("ability")
}
}
@Subscribe
fun onWorldReady(event: WorldReadyEvent) {
lastUsage.clear()
lobbyJoinTime = TimeMark.now()
abilityOverride = null
}
val abilityUsePattern = Pattern.compile("You used your (?<name>.*) Pickaxe Ability!")
val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)")
data class PickaxeAbilityData(
val name: String,
val cooldown: Duration,
)
fun getCooldownFromLore(itemStack: ItemStack): PickaxeAbilityData? {
val lore = itemStack.loreAccordingToNbt
if (!lore.any { it.unformattedString.contains("Breaking Power") == true })
return null
val cooldown = lore.firstNotNullOfOrNull {
cooldownPattern.useMatch(it.unformattedString) {
parseTimePattern(group("cooldown"))
}
} ?: return null
val name = lore.firstNotNullOfOrNull {
abilityPattern.useMatch(it.unformattedString) {
group("name")
}
} ?: return null
return PickaxeAbilityData(name, cooldown)
}
val cooldownPattern = Pattern.compile("Cooldown: (?<cooldown>$TIME_PATTERN)")
val abilityPattern = Pattern.compile("Ability: (?<name>.*) {2}RIGHT CLICK")
val abilitySwitchPattern =
Pattern.compile("You selected (?<ability>.*) as your Pickaxe Ability\\. This ability will apply to all of your pickaxes!")
@Subscribe
fun renderHud(event: HudRenderEvent) {
if (!TConfig.cooldownEnabled) return
var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return
defaultAbilityDurations[ability.name] = ability.cooldown
val ao = abilityOverride
if (ao != ability.name && ao != null) {
ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds)
}
event.context.matrices.push()
event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F)
event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F)
RenderCircleProgress.renderCircle(
event.context, Identifier.of("firmament", "textures/gui/circle.png"),
getCooldownPercentage(ability.name, ability.cooldown).toFloat(),
0f, 1f, 0f, 1f
)
event.context.matrices.pop()
}
}

View File

@@ -0,0 +1,133 @@
package moe.nea.firmament.features.mining
import io.github.notenoughupdates.moulconfig.xml.Bind
import moe.nea.jarvis.api.Point
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import kotlin.time.Duration.Companion.seconds
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.gui.hud.MoulConfigHud
import moe.nea.firmament.util.BazaarPriceStrategy
import moe.nea.firmament.util.FirmFormatters.formatCommas
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.formattedString
import moe.nea.firmament.util.parseIntWithComma
import moe.nea.firmament.util.useMatch
object PristineProfitTracker : FirmamentFeature {
override val identifier: String
get() = "pristine-profit"
enum class GemstoneKind(
val label: String,
val flawedId: SkyblockId,
) {
SAPPHIRE("Sapphire", SkyblockId("FLAWED_SAPPHIRE_GEM")),
RUBY("Ruby", SkyblockId("FLAWED_RUBY_GEM")),
AMETHYST("Amethyst", SkyblockId("FLAWED_AMETHYST_GEM")),
AMBER("Amber", SkyblockId("FLAWED_AMBER_GEM")),
TOPAZ("Topaz", SkyblockId("FLAWED_TOPAZ_GEM")),
JADE("Jade", SkyblockId("FLAWED_JADE_GEM")),
JASPER("Jasper", SkyblockId("FLAWED_JASPER_GEM")),
OPAL("Opal", SkyblockId("FLAWED_OPAL_GEM")),
}
@Serializable
data class Data(
var maxMoneyPerSecond: Double = 1.0,
var maxCollectionPerSecond: Double = 1.0,
)
object DConfig : ProfileSpecificDataHolder<Data>(serializer(), identifier, ::Data)
override val config: ManagedConfig?
get() = TConfig
object TConfig : ManagedConfig(identifier) {
val timeout by duration("timeout", 0.seconds, 120.seconds) { 30.seconds }
val gui by position("position", 80, 30) { Point(0.05, 0.2) }
}
val sellingStrategy = BazaarPriceStrategy.SELL_ORDER
val pristineRegex =
"PRISTINE! You found . Flawed (?<kind>${
GemstoneKind.entries.joinToString("|") { it.label }
}) Gemstone x(?<count>[0-9,]+)!".toPattern()
val collectionHistogram = Histogram<Double>(10000, 180.seconds)
val moneyHistogram = Histogram<Double>(10000, 180.seconds)
object ProfitHud : MoulConfigHud("pristine_profit", TConfig.gui) {
@field:Bind
var moneyCurrent: Double = 0.0
@field:Bind
var moneyMax: Double = 1.0
@field:Bind
var moneyText = ""
@field:Bind
var collectionCurrent = 0.0
@field:Bind
var collectionMax = 1.0
@field:Bind
var collectionText = ""
override fun shouldRender(): Boolean = collectionHistogram.latestUpdate().passedTime() < TConfig.timeout
}
val SECONDS_PER_HOUR = 3600
val ROUGHS_PER_FLAWED = 80
fun updateUi() {
val collectionPerSecond = collectionHistogram.averagePer({ it }, 1.seconds)
val moneyPerSecond = moneyHistogram.averagePer({ it }, 1.seconds)
if (collectionPerSecond == null || moneyPerSecond == null) return
ProfitHud.collectionCurrent = collectionPerSecond
ProfitHud.collectionText = Text.stringifiedTranslatable("firmament.pristine-profit.collection",
formatCommas(collectionPerSecond * SECONDS_PER_HOUR,
1)).formattedString()
ProfitHud.moneyCurrent = moneyPerSecond
ProfitHud.moneyText = Text.stringifiedTranslatable("firmament.pristine-profit.money",
formatCommas(moneyPerSecond * SECONDS_PER_HOUR, 1))
.formattedString()
val data = DConfig.data
if (data != null) {
if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate()
.passedTime() > 30.seconds
) {
data.maxCollectionPerSecond = collectionPerSecond
DConfig.markDirty()
}
if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) {
data.maxMoneyPerSecond = moneyPerSecond
DConfig.markDirty()
}
ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond)
ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond)
}
}
@Subscribe
fun onMessage(it: ProcessChatEvent) {
pristineRegex.useMatch(it.unformattedString) {
val gemstoneKind = GemstoneKind.valueOf(group("kind").uppercase())
val flawedCount = parseIntWithComma(group("count"))
val moneyAmount = sellingStrategy.getSellPrice(gemstoneKind.flawedId) * flawedCount
moneyHistogram.record(moneyAmount)
val collectionAmount = flawedCount * ROUGHS_PER_FLAWED
collectionHistogram.record(collectionAmount.toDouble())
updateUi()
}
}
}

View File

@@ -0,0 +1,7 @@
package moe.nea.firmament.features.notifications
import moe.nea.firmament.features.FirmamentFeature
object Notifications {
}

View File

@@ -0,0 +1,17 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import net.minecraft.item.ItemStack
object AlwaysPredicate : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return true
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
return AlwaysPredicate
}
}
}

View File

@@ -0,0 +1,26 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import net.minecraft.item.ItemStack
class AndPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return children.all { it.test(stack) }
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
val children =
(jsonElement as JsonArray)
.flatMap {
CustomModelOverrideParser.parsePredicates(it as JsonObject)
}
.toTypedArray()
return AndPredicate(children)
}
}
}

View File

@@ -0,0 +1,9 @@
package moe.nea.firmament.features.texturepack
import net.minecraft.client.render.model.BakedModel
interface BakedModelExtra {
fun getHeadModel_firmament(): BakedModel?
fun setHeadModel_firmament(headModel: BakedModel?)
}

View File

@@ -0,0 +1,8 @@
package moe.nea.firmament.features.texturepack
interface BakedOverrideData {
fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
}

View File

@@ -0,0 +1,295 @@
@file:UseSerializers(BlockPosSerializer::class, IdentifierSerializer::class)
package moe.nea.firmament.features.texturepack
import java.util.concurrent.CompletableFuture
import net.fabricmc.loader.api.FabricLoader
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.serializer
import kotlin.jvm.optionals.getOrNull
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.util.ModelIdentifier
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.profiler.Profiler
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.BakeExtraModelsEvent
import moe.nea.firmament.events.EarlyResourceReloadEvent
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.json.BlockPosSerializer
import moe.nea.firmament.util.json.SingletonSerializableList
object CustomBlockTextures {
@Serializable
data class CustomBlockOverride(
val modes: @Serializable(SingletonSerializableList::class) List<String>,
val area: List<Area>? = null,
val replacements: Map<Identifier, Replacement>,
)
@Serializable(with = Replacement.Serializer::class)
data class Replacement(
val block: Identifier,
val sound: Identifier?,
) {
@Transient
val blockModelIdentifier get() = ModelIdentifier(block.withPrefixedPath("block/"), "firmament")
@Transient
val bakedModel: BakedModel by lazy(LazyThreadSafetyMode.NONE) {
MC.instance.bakedModelManager.getModel(blockModelIdentifier)
}
@OptIn(ExperimentalSerializationApi::class)
@kotlinx.serialization.Serializer(Replacement::class)
object DefaultSerializer : KSerializer<Replacement>
object Serializer : KSerializer<Replacement> {
val delegate = serializer<JsonElement>()
override val descriptor: SerialDescriptor
get() = delegate.descriptor
override fun deserialize(decoder: Decoder): Replacement {
val jsonElement = decoder.decodeSerializableValue(delegate)
if (jsonElement is JsonPrimitive) {
require(jsonElement.isString)
return Replacement(Identifier.tryParse(jsonElement.content)!!, null)
}
return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement)
}
override fun serialize(encoder: Encoder, value: Replacement) {
encoder.encodeSerializableValue(DefaultSerializer, value)
}
}
}
@Serializable
data class Area(
val min: BlockPos,
val max: BlockPos,
) {
@Transient
val realMin = BlockPos(
minOf(min.x, max.x),
minOf(min.y, max.y),
minOf(min.z, max.z),
)
@Transient
val realMax = BlockPos(
maxOf(min.x, max.x),
maxOf(min.y, max.y),
maxOf(min.z, max.z),
)
fun roughJoin(other: Area): Area {
return Area(
BlockPos(
minOf(realMin.x, other.realMin.x),
minOf(realMin.y, other.realMin.y),
minOf(realMin.z, other.realMin.z),
),
BlockPos(
maxOf(realMax.x, other.realMax.x),
maxOf(realMax.y, other.realMax.y),
maxOf(realMax.z, other.realMax.z),
)
)
}
fun contains(blockPos: BlockPos): Boolean {
return (blockPos.x in realMin.x..realMax.x) &&
(blockPos.y in realMin.y..realMax.y) &&
(blockPos.z in realMin.z..realMax.z)
}
}
data class LocationReplacements(
val lookup: Map<Block, List<BlockReplacement>>
)
data class BlockReplacement(
val checks: List<Area>?,
val replacement: Replacement,
) {
val roughCheck by lazy(LazyThreadSafetyMode.NONE) {
if (checks == null || checks.size < 3) return@lazy null
checks.reduce { acc, next -> acc.roughJoin(next) }
}
}
data class BakedReplacements(val data: Map<SkyBlockIsland, LocationReplacements>)
var allLocationReplacements: BakedReplacements = BakedReplacements(mapOf())
var currentIslandReplacements: LocationReplacements? = null
fun refreshReplacements() {
val location = SBData.skyblockLocation
val replacements =
if (CustomSkyBlockTextures.TConfig.enableBlockOverrides) location?.let(allLocationReplacements.data::get)
else null
val lastReplacements = currentIslandReplacements
currentIslandReplacements = replacements
if (lastReplacements != replacements) {
MC.nextTick {
MC.worldRenderer.chunks?.chunks?.forEach {
// false schedules rebuilds outside a 27 block radius to happen async
it.scheduleRebuild(false)
}
sodiumReloadTask?.run()
}
}
}
private val sodiumReloadTask = runCatching {
Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader").getConstructor().newInstance() as Runnable
}.getOrElse {
if (FabricLoader.getInstance().isModLoaded("sodium"))
logger.error("Could not create sodium chunk reloader")
null
}
fun matchesPosition(replacement: BlockReplacement, blockPos: BlockPos?): Boolean {
if (blockPos == null) return true
val rc = replacement.roughCheck
if (rc != null && !rc.contains(blockPos)) return false
val areas = replacement.checks
if (areas != null && !areas.any { it.contains(blockPos) }) return false
return true
}
@JvmStatic
fun getReplacementModel(block: BlockState, blockPos: BlockPos?): BakedModel? {
return getReplacement(block, blockPos)?.bakedModel
}
@JvmStatic
fun getReplacement(block: BlockState, blockPos: BlockPos?): Replacement? {
if (isInFallback() && blockPos == null) return null
val replacements = currentIslandReplacements?.lookup?.get(block.block) ?: return null
for (replacement in replacements) {
if (replacement.checks == null || matchesPosition(replacement, blockPos))
return replacement.replacement
}
return null
}
@Subscribe
fun onLocation(event: SkyblockServerUpdateEvent) {
refreshReplacements()
}
@Volatile
var preparationFuture: CompletableFuture<BakedReplacements> = CompletableFuture.completedFuture(BakedReplacements(
mapOf()))
val insideFallbackCall = ThreadLocal.withInitial { 0 }
@JvmStatic
fun enterFallbackCall() {
insideFallbackCall.set(insideFallbackCall.get() + 1)
}
fun isInFallback() = insideFallbackCall.get() > 0
@JvmStatic
fun exitFallbackCall() {
insideFallbackCall.set(insideFallbackCall.get() - 1)
}
@Subscribe
fun onEarlyReload(event: EarlyResourceReloadEvent) {
preparationFuture = CompletableFuture
.supplyAsync(
{ prepare(event.resourceManager) }, event.preparationExecutor)
}
@Subscribe
fun bakeExtraModels(event: BakeExtraModelsEvent) {
preparationFuture.join().data.values
.flatMap { it.lookup.values }
.flatten()
.mapTo(mutableSetOf()) { it.replacement.blockModelIdentifier }
.forEach { event.addNonItemModel(it) }
}
private fun prepare(manager: ResourceManager): BakedReplacements {
val resources = manager.findResources("overrides/blocks") {
it.namespace == "firmskyblock" && it.path.endsWith(".json")
}
val map = mutableMapOf<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>()
for ((file, resource) in resources) {
val json =
Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(resource.inputStream)
.getOrElse { ex ->
logger.error("Failed to load block texture override at $file", ex)
continue
}
for (mode in json.modes) {
val island = SkyBlockIsland.forMode(mode)
val islandMpa = map.getOrPut(island, ::mutableMapOf)
for ((blockId, replacement) in json.replacements) {
val block = MC.defaultRegistries.getWrapperOrThrow(RegistryKeys.BLOCK)
.getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId))
.getOrNull()
if (block == null) {
logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'")
continue
}
val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf)
replacements.add(BlockReplacement(json.area, replacement))
}
}
}
return BakedReplacements(map.mapValues { LocationReplacements(it.value) })
}
@JvmStatic
fun patchIndigo(orig: BakedModel, pos: BlockPos, state: BlockState): BakedModel {
return getReplacementModel(state, pos) ?: orig
}
@Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
event.resourceManager.registerReloader(object :
SinglePreparationResourceReloader<BakedReplacements>() {
override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements {
return preparationFuture.join()
}
override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) {
allLocationReplacements = prepared
refreshReplacements()
}
})
}
}

View File

@@ -0,0 +1,106 @@
@file:UseSerializers(IdentifierSerializer::class)
package moe.nea.firmament.features.texturepack
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.UseSerializers
import net.minecraft.item.ArmorMaterial
import net.minecraft.item.ItemStack
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.util.Identifier
import net.minecraft.util.profiler.Profiler
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.IdentityCharacteristics
import moe.nea.firmament.util.computeNullableFunction
import moe.nea.firmament.util.skyBlockId
object CustomGlobalArmorOverrides : SubscriptionOwner {
@Serializable
data class ArmorOverride(
@SerialName("item_ids")
val itemIds: List<String>,
val layers: List<ArmorOverrideLayer>,
val overrides: List<ArmorOverrideOverride> = listOf(),
) {
@Transient
val bakedLayers = bakeLayers(layers)
}
fun bakeLayers(layers: List<ArmorOverrideLayer>): List<ArmorMaterial.Layer> {
return layers.map { ArmorMaterial.Layer(it.identifier, it.suffix, it.tint) }
}
@Serializable
data class ArmorOverrideLayer(
val tint: Boolean = false,
val identifier: Identifier,
val suffix: String = "",
)
@Serializable
data class ArmorOverrideOverride(
val predicate: FirmamentModelPredicate,
val layers: List<ArmorOverrideLayer>,
) {
@Transient
val bakedLayers = bakeLayers(layers)
}
override val delegateFeature: FirmamentFeature
get() = CustomSkyBlockTextures
val overrideCache = mutableMapOf<IdentityCharacteristics<ItemStack>, Any>()
@JvmStatic
fun overrideArmor(stack: ItemStack): List<ArmorMaterial.Layer>? {
if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null
return overrideCache.computeNullableFunction(IdentityCharacteristics(stack)) {
val id = stack.skyBlockId ?: return@computeNullableFunction null
val override = overrides[id.neuItem] ?: return@computeNullableFunction null
for (suboverride in override.overrides) {
if (suboverride.predicate.test(stack)) {
return@computeNullableFunction suboverride.bakedLayers
}
}
return@computeNullableFunction override.bakedLayers
}
}
var overrides: Map<String, ArmorOverride> = mapOf()
@Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
event.resourceManager.registerReloader(object :
SinglePreparationResourceReloader<Map<String, ArmorOverride>>() {
override fun prepare(manager: ResourceManager, profiler: Profiler): Map<String, ArmorOverride> {
val overrideFiles = manager.findResources("overrides/armor_models") {
it.namespace == "firmskyblock" && it.path.endsWith(".json")
}
val overrides = overrideFiles.mapNotNull {
Firmament.tryDecodeJsonFromStream<ArmorOverride>(it.value.inputStream).getOrElse { ex ->
logger.error("Failed to load armor texture override at ${it.key}", ex)
null
}
}
val associatedMap = overrides.flatMap { obj -> obj.itemIds.map { it to obj } }
.toMap()
return associatedMap
}
override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) {
overrides = prepared
}
})
}
}

View File

@@ -0,0 +1,167 @@
@file:UseSerializers(IdentifierSerializer::class, CustomModelOverrideParser.FirmamentRootPredicateSerializer::class)
package moe.nea.firmament.features.texturepack
import java.util.concurrent.CompletableFuture
import org.slf4j.LoggerFactory
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import kotlin.jvm.optionals.getOrNull
import net.minecraft.client.render.item.ItemModels
import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.util.ModelIdentifier
import net.minecraft.item.ItemStack
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.minecraft.util.profiler.Profiler
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.BakeExtraModelsEvent
import moe.nea.firmament.events.EarlyResourceReloadEvent
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.IdentityCharacteristics
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.computeNullableFunction
import moe.nea.firmament.util.json.SingletonSerializableList
import moe.nea.firmament.util.runNull
object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalTextures.CustomGuiTextureOverride>(),
SubscriptionOwner {
override val delegateFeature: FirmamentFeature
get() = CustomSkyBlockTextures
class CustomGuiTextureOverride(
val classes: List<ItemOverrideCollection>
)
@Serializable
data class GlobalItemOverride(
val screen: @Serializable(SingletonSerializableList::class) List<Identifier>,
val model: Identifier,
val predicate: FirmamentModelPredicate,
)
@Serializable
data class ScreenFilter(
val title: StringMatcher,
)
data class ItemOverrideCollection(
val screenFilter: ScreenFilter,
val overrides: List<GlobalItemOverride>,
)
@Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
MC.resourceManager.registerReloader(this)
}
@Subscribe
fun onEarlyReload(event: EarlyResourceReloadEvent) {
preparationFuture = CompletableFuture
.supplyAsync(
{
prepare(event.resourceManager)
}, event.preparationExecutor)
}
@Subscribe
fun onBakeModels(event: BakeExtraModelsEvent) {
for (guiClassOverride in preparationFuture.join().classes) {
for (override in guiClassOverride.overrides) {
event.addItemModel(ModelIdentifier(override.model, "inventory"))
}
}
}
@Volatile
var preparationFuture: CompletableFuture<CustomGuiTextureOverride> = CompletableFuture.completedFuture(
CustomGuiTextureOverride(listOf()))
override fun prepare(manager: ResourceManager?, profiler: Profiler?): CustomGuiTextureOverride {
return preparationFuture.join()
}
override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) {
this.guiClassOverrides = prepared
}
val logger = LoggerFactory.getLogger(CustomGlobalTextures::class.java)
fun prepare(manager: ResourceManager): CustomGuiTextureOverride {
val overrideResources =
manager.findResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") }
.mapNotNull {
Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.inputStream).getOrElse { ex ->
logger.error("Failed to load global item override at ${it.key}", ex)
null
}
}
val byGuiClass = overrideResources.flatMap { override -> override.screen.toSet().map { it to override } }
.groupBy { it.first }
val guiClasses = byGuiClass.entries
.mapNotNull {
val key = it.key
val guiClassResource =
manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json"))
.getOrNull()
?: return@mapNotNull runNull {
logger.error("Failed to locate screen filter at $key")
}
val screenFilter =
Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream)
.getOrElse { ex ->
logger.error("Failed to load screen filter at $key", ex)
return@mapNotNull null
}
ItemOverrideCollection(screenFilter, it.value.map { it.second })
}
logger.info("Loaded ${overrideResources.size} global item overrides")
return CustomGuiTextureOverride(guiClasses)
}
var guiClassOverrides = CustomGuiTextureOverride(listOf())
var matchingOverrides: Set<ItemOverrideCollection> = setOf()
@Subscribe
fun onOpenGui(event: ScreenChangeEvent) {
val newTitle = event.new?.title ?: Text.empty()
matchingOverrides = guiClassOverrides.classes
.filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) }
}
val overrideCache = mutableMapOf<IdentityCharacteristics<ItemStack>, Any>()
@JvmStatic
fun replaceGlobalModel(
models: ItemModels,
stack: ItemStack,
cir: CallbackInfoReturnable<BakedModel>
) {
val value = overrideCache.computeNullableFunction(IdentityCharacteristics(stack)) {
for (guiClassOverride in matchingOverrides) {
for (override in guiClassOverride.overrides) {
if (override.predicate.test(stack)) {
return@computeNullableFunction models.modelManager.getModel(
ModelIdentifier(override.model, "inventory"))
}
}
}
null
}
if (value != null)
cir.returnValue = value
}
}

View File

@@ -0,0 +1,74 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonObject
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.minecraft.item.ItemStack
import net.minecraft.util.Identifier
object CustomModelOverrideParser {
object FirmamentRootPredicateSerializer : KSerializer<FirmamentModelPredicate> {
val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer()
override val descriptor: SerialDescriptor
get() = SerialDescriptor("FirmamentModelRootPredicate", delegateSerializer.descriptor)
override fun deserialize(decoder: Decoder): FirmamentModelPredicate {
val json = decoder.decodeSerializableValue(delegateSerializer).intoGson() as JsonObject
return AndPredicate(parsePredicates(json).toTypedArray())
}
override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) {
TODO("Cannot serialize firmament predicates")
}
}
val predicateParsers = mutableMapOf<Identifier, FirmamentModelPredicateParser>()
fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) {
predicateParsers[Identifier.of("firmament", name)] = parser
}
init {
registerPredicateParser("display_name", DisplayNamePredicate.Parser)
registerPredicateParser("lore", LorePredicate.Parser)
registerPredicateParser("all", AndPredicate.Parser)
registerPredicateParser("any", OrPredicate.Parser)
registerPredicateParser("not", NotPredicate.Parser)
registerPredicateParser("item", ItemPredicate.Parser)
registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
registerPredicateParser("pet", PetPredicate.Parser)
}
private val neverPredicate = listOf(
object : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return false
}
}
)
fun parsePredicates(predicates: JsonObject): List<FirmamentModelPredicate> {
val parsedPredicates = mutableListOf<FirmamentModelPredicate>()
for (predicateName in predicates.keySet()) {
if (!predicateName.startsWith("firmament:")) continue
val identifier = Identifier.of(predicateName)
val parser = predicateParsers[identifier] ?: return neverPredicate
val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate
parsedPredicates.add(parsedPredicate)
}
return parsedPredicates
}
@JvmStatic
fun parseCustomModelOverrides(jsonObject: JsonObject): Array<FirmamentModelPredicate>? {
val predicates = (jsonObject["predicate"] as? JsonObject) ?: return null
val parsedPredicates = parsePredicates(predicates)
if (parsedPredicates.isEmpty())
return null
return parsedPredicates.toTypedArray()
}
}

View File

@@ -0,0 +1,114 @@
package moe.nea.firmament.features.texturepack
import com.mojang.authlib.minecraft.MinecraftProfileTexture
import com.mojang.authlib.properties.Property
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import net.minecraft.block.SkullBlock
import net.minecraft.client.MinecraftClient
import net.minecraft.client.render.RenderLayer
import net.minecraft.client.util.ModelIdentifier
import net.minecraft.component.type.ProfileComponent
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.BakeExtraModelsEvent
import moe.nea.firmament.events.CustomItemModelEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.IdentityCharacteristics
import moe.nea.firmament.util.item.decodeProfileTextureProperty
import moe.nea.firmament.util.skyBlockId
object CustomSkyBlockTextures : FirmamentFeature {
override val identifier: String
get() = "custom-skyblock-textures"
object TConfig : ManagedConfig(identifier) {
val enabled by toggle("enabled") { true }
val skullsEnabled by toggle("skulls-enabled") { true }
val cacheDuration by integer("cache-duration", 0, 20) { 1 }
val enableModelOverrides by toggle("model-overrides") { true }
val enableArmorOverrides by toggle("armor-overrides") { true }
val enableBlockOverrides by toggle("block-overrides") { true }
}
override val config: ManagedConfig
get() = TConfig
@Subscribe
fun onTick(it: TickEvent) {
if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) {
// TODO: unify all of those caches somehow
CustomItemModelEvent.clearCache()
skullTextureCache.clear()
CustomGlobalTextures.overrideCache.clear()
CustomGlobalArmorOverrides.overrideCache.clear()
}
}
@Subscribe
fun bakeCustomFirmModels(event: BakeExtraModelsEvent) {
val resources =
MinecraftClient.getInstance().resourceManager.findResources("models/item"
) { it: Identifier ->
"firmskyblock" == it.namespace && it.path
.endsWith(".json")
}
for (identifier in resources.keys) {
val modelId = ModelIdentifier.ofInventoryVariant(
Identifier.of(
"firmskyblock",
identifier.path.substring(
"models/item/".length,
identifier.path.length - ".json".length),
))
event.addItemModel(modelId)
}
}
@Subscribe
fun onCustomModelId(it: CustomItemModelEvent) {
if (!TConfig.enabled) return
val id = it.itemStack.skyBlockId ?: return
it.overrideModel = ModelIdentifier.ofInventoryVariant(Identifier.of("firmskyblock", id.identifier.path))
}
private val skullTextureCache = mutableMapOf<IdentityCharacteristics<ProfileComponent>, Any>()
private val sentinelPresentInvalid = Object()
private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex()
fun getSkullId(textureProperty: Property): String? {
val texture = decodeProfileTextureProperty(textureProperty) ?: return null
val textureUrl =
texture.textures[MinecraftProfileTexture.Type.SKIN]?.url ?: return null
val mcUrlData = mcUrlRegex.matchEntire(textureUrl) ?: return null
return mcUrlData.groupValues[1]
}
fun getSkullTexture(profile: ProfileComponent): Identifier? {
val id = getSkullId(profile.properties["textures"].firstOrNull() ?: return null) ?: return null
return Identifier.of("firmskyblock", "textures/placedskull/$id.png")
}
fun modifySkullTexture(
type: SkullBlock.SkullType?,
component: ProfileComponent?,
cir: CallbackInfoReturnable<RenderLayer>
) {
if (type != SkullBlock.Type.PLAYER) return
if (!TConfig.skullsEnabled) return
if (component == null) return
val ic = IdentityCharacteristics(component)
val n = skullTextureCache.getOrPut(ic) {
val id = getSkullTexture(component) ?: return@getOrPut sentinelPresentInvalid
if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) {
return@getOrPut sentinelPresentInvalid
}
return@getOrPut id
}
if (n === sentinelPresentInvalid) return
cir.returnValue = RenderLayer.getEntityTranslucent(n as Identifier)
}
}

View File

@@ -0,0 +1,22 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtString
import moe.nea.firmament.util.item.displayNameAccordingToNbt
import moe.nea.firmament.util.item.loreAccordingToNbt
data class DisplayNamePredicate(val stringMatcher: StringMatcher) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
val display = stack.displayNameAccordingToNbt
return stringMatcher.matches(display)
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
return DisplayNamePredicate(StringMatcher.parse(jsonElement))
}
}
}

View File

@@ -0,0 +1,268 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtByte
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtDouble
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtFloat
import net.minecraft.nbt.NbtInt
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtLong
import net.minecraft.nbt.NbtShort
import net.minecraft.nbt.NbtString
import moe.nea.firmament.util.extraAttributes
fun interface NbtMatcher {
fun matches(nbt: NbtElement): Boolean
object Parser {
fun parse(jsonElement: JsonElement): NbtMatcher? {
if (jsonElement is JsonPrimitive) {
if (jsonElement.isString) {
val string = jsonElement.asString
return MatchStringExact(string)
}
if (jsonElement.isNumber) {
return MatchNumberExact(jsonElement.asLong) //TODO: parse generic number
}
}
if (jsonElement is JsonObject) {
var encounteredParser: NbtMatcher? = null
for (entry in ExclusiveParserType.entries) {
val data = jsonElement[entry.key] ?: continue
if (encounteredParser != null) {
// TODO: warn
return null
}
encounteredParser = entry.parse(data) ?: return null
}
return encounteredParser
}
return null
}
enum class ExclusiveParserType(val key: String) {
STRING("string") {
override fun parse(element: JsonElement): NbtMatcher? {
return MatchString(StringMatcher.parse(element))
}
},
INT("int") {
override fun parse(element: JsonElement): NbtMatcher? {
return parseGenericNumber(element,
{ it.asInt },
{ (it as? NbtInt)?.intValue() },
{ a, b ->
if (a == b) Comparison.EQUAL
else if (a < b) Comparison.LESS_THAN
else Comparison.GREATER
})
}
},
FLOAT("float") {
override fun parse(element: JsonElement): NbtMatcher? {
return parseGenericNumber(element,
{ it.asFloat },
{ (it as? NbtFloat)?.floatValue() },
{ a, b ->
if (a == b) Comparison.EQUAL
else if (a < b) Comparison.LESS_THAN
else Comparison.GREATER
})
}
},
DOUBLE("double") {
override fun parse(element: JsonElement): NbtMatcher? {
return parseGenericNumber(element,
{ it.asDouble },
{ (it as? NbtDouble)?.doubleValue() },
{ a, b ->
if (a == b) Comparison.EQUAL
else if (a < b) Comparison.LESS_THAN
else Comparison.GREATER
})
}
},
LONG("long") {
override fun parse(element: JsonElement): NbtMatcher? {
return parseGenericNumber(element,
{ it.asLong },
{ (it as? NbtLong)?.longValue() },
{ a, b ->
if (a == b) Comparison.EQUAL
else if (a < b) Comparison.LESS_THAN
else Comparison.GREATER
})
}
},
SHORT("short") {
override fun parse(element: JsonElement): NbtMatcher? {
return parseGenericNumber(element,
{ it.asShort },
{ (it as? NbtShort)?.shortValue() },
{ a, b ->
if (a == b) Comparison.EQUAL
else if (a < b) Comparison.LESS_THAN
else Comparison.GREATER
})
}
},
BYTE("byte") {
override fun parse(element: JsonElement): NbtMatcher? {
return parseGenericNumber(element,
{ it.asByte },
{ (it as? NbtByte)?.byteValue() },
{ a, b ->
if (a == b) Comparison.EQUAL
else if (a < b) Comparison.LESS_THAN
else Comparison.GREATER
})
}
},
;
abstract fun parse(element: JsonElement): NbtMatcher?
}
enum class Comparison {
LESS_THAN, EQUAL, GREATER
}
inline fun <T : Any> parseGenericNumber(
jsonElement: JsonElement,
primitiveExtractor: (JsonPrimitive) -> T?,
crossinline nbtExtractor: (NbtElement) -> T?,
crossinline compare: (T, T) -> Comparison
): NbtMatcher? {
if (jsonElement is JsonPrimitive) {
val expected = primitiveExtractor(jsonElement) ?: return null
return NbtMatcher {
val actual = nbtExtractor(it) ?: return@NbtMatcher false
compare(actual, expected) == Comparison.EQUAL
}
}
if (jsonElement is JsonObject) {
val minElement = jsonElement.getAsJsonPrimitive("min")
val min = if (minElement != null) primitiveExtractor(minElement) ?: return null else null
val minExclusive = jsonElement.get("minExclusive")?.asBoolean ?: false
val maxElement = jsonElement.getAsJsonPrimitive("max")
val max = if (maxElement != null) primitiveExtractor(maxElement) ?: return null else null
val maxExclusive = jsonElement.get("maxExclusive")?.asBoolean ?: true
if (min == null && max == null) return null
return NbtMatcher {
val actual = nbtExtractor(it) ?: return@NbtMatcher false
if (max != null) {
val comp = compare(actual, max)
if (comp == Comparison.GREATER) return@NbtMatcher false
if (comp == Comparison.EQUAL && maxExclusive) return@NbtMatcher false
}
if (min != null) {
val comp = compare(actual, min)
if (comp == Comparison.LESS_THAN) return@NbtMatcher false
if (comp == Comparison.EQUAL && minExclusive) return@NbtMatcher false
}
return@NbtMatcher true
}
}
return null
}
}
class MatchNumberExact(val number: Long) : NbtMatcher {
override fun matches(nbt: NbtElement): Boolean {
return when (nbt) {
is NbtByte -> nbt.byteValue().toLong() == number
is NbtInt -> nbt.intValue().toLong() == number
is NbtShort -> nbt.shortValue().toLong() == number
is NbtLong -> nbt.longValue().toLong() == number
else -> false
}
}
}
class MatchStringExact(val string: String) : NbtMatcher {
override fun matches(nbt: NbtElement): Boolean {
return nbt is NbtString && nbt.asString() == string
}
override fun toString(): String {
return "MatchNbtStringExactly($string)"
}
}
class MatchString(val string: StringMatcher) : NbtMatcher {
override fun matches(nbt: NbtElement): Boolean {
return nbt is NbtString && string.matches(nbt.asString())
}
override fun toString(): String {
return "MatchNbtString($string)"
}
}
}
data class ExtraAttributesPredicate(
val path: NbtPrism,
val matcher: NbtMatcher,
) : FirmamentModelPredicate {
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
if (jsonElement !is JsonObject) return null
val path = jsonElement.get("path") ?: return null
val pathSegments = if (path is JsonArray) {
path.map { (it as JsonPrimitive).asString }
} else if (path is JsonPrimitive && path.isString) {
path.asString.split(".")
} else return null
val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement)
?: return null
return ExtraAttributesPredicate(NbtPrism(pathSegments), matcher)
}
}
override fun test(stack: ItemStack): Boolean {
return path.access(stack.extraAttributes)
.any { matcher.matches(it) }
}
}
class NbtPrism(val path: List<String>) {
override fun toString(): String {
return "Prism($path)"
}
fun access(root: NbtElement): Collection<NbtElement> {
var rootSet = mutableListOf(root)
var switch = mutableListOf<NbtElement>()
for (pathSegment in path) {
if (pathSegment == ".") continue
for (element in rootSet) {
if (element is NbtList) {
if (pathSegment == "*")
switch.addAll(element)
val index = pathSegment.toIntOrNull() ?: continue
if (index !in element.indices) continue
switch.add(element[index])
}
if (element is NbtCompound) {
if (pathSegment == "*")
element.keys.mapTo(switch) { element.get(it)!! }
switch.add(element.get(pathSegment) ?: continue)
}
}
val temp = switch
switch = rootSet
rootSet = temp
switch.clear()
}
return rootSet
}
}

View File

@@ -0,0 +1,8 @@
package moe.nea.firmament.features.texturepack
import net.minecraft.item.ItemStack
interface FirmamentModelPredicate {
fun test(stack: ItemStack): Boolean
}

View File

@@ -0,0 +1,8 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
interface FirmamentModelPredicateParser {
fun parse(jsonElement: JsonElement): FirmamentModelPredicate?
}

View File

@@ -0,0 +1,32 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import kotlin.jvm.optionals.getOrNull
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.util.Identifier
import moe.nea.firmament.util.MC
class ItemPredicate(
val item: Item
) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return stack.item == item
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): ItemPredicate? {
if (jsonElement is JsonPrimitive && jsonElement.isString) {
val itemKey = RegistryKey.of(RegistryKeys.ITEM,
Identifier.tryParse(jsonElement.asString)
?: return null)
return ItemPredicate(MC.defaultItems.getOptional(itemKey).getOrNull()?.value() ?: return null)
}
return null
}
}
}

View File

@@ -0,0 +1,10 @@
package moe.nea.firmament.features.texturepack
import net.minecraft.util.Identifier
interface JsonUnbakedModelFirmExtra {
fun setHeadModel_firmament(identifier: Identifier?)
fun getHeadModel_firmament(): Identifier?
}

View File

@@ -0,0 +1,19 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import net.minecraft.item.ItemStack
import moe.nea.firmament.util.item.loreAccordingToNbt
class LorePredicate(val matcher: StringMatcher) : FirmamentModelPredicate {
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
return LorePredicate(StringMatcher.parse(jsonElement))
}
}
override fun test(stack: ItemStack): Boolean {
val lore = stack.loreAccordingToNbt
return lore.any { matcher.matches(it) }
}
}

View File

@@ -0,0 +1,7 @@
package moe.nea.firmament.features.texturepack
interface ModelOverrideData {
fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
}

View File

@@ -0,0 +1,19 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import moe.nea.firmament.util.filter.IteratorFilterSet
class ModelOverrideFilterSet(original: java.util.Set<Map.Entry<String, JsonElement>>) :
IteratorFilterSet<Map.Entry<String, JsonElement>>(original) {
companion object {
@JvmStatic
fun createFilterSet(set: java.util.Set<*>): java.util.Set<*> {
return ModelOverrideFilterSet(set as java.util.Set<Map.Entry<String, JsonElement>>) as java.util.Set<*>
}
}
override fun shouldKeepElement(element: Map.Entry<String, JsonElement>): Boolean {
return !element.key.startsWith("firmament:")
}
}

View File

@@ -0,0 +1,18 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import net.minecraft.item.ItemStack
class NotPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return children.none { it.test(stack) }
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
return NotPredicate(CustomModelOverrideParser.parsePredicates(jsonElement as JsonObject).toTypedArray())
}
}
}

View File

@@ -0,0 +1,125 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import moe.nea.firmament.util.useMatch
abstract class NumberMatcher {
abstract fun test(number: Number): Boolean
companion object {
fun parse(jsonElement: JsonElement): NumberMatcher? {
if (jsonElement is JsonPrimitive) {
if (jsonElement.isString) {
val string = jsonElement.asString
return parseRange(string) ?: parseOperator(string)
}
if (jsonElement.isNumber) {
val number = jsonElement.asNumber
val hasDecimals = (number.toString().contains("."))
return MatchNumberExact(if (hasDecimals) number.toLong() else number.toDouble())
}
}
return null
}
private val intervalSpec =
"(?<beginningOpen>[\\[\\(])(?<beginning>[0-9.]+)?,(?<ending>[0-9.]+)?(?<endingOpen>[\\]\\)])"
.toPattern()
fun parseRange(string: String): RangeMatcher? {
intervalSpec.useMatch<Nothing>(string) {
// Open in the set-theory sense, meaning does not include its end.
val beginningOpen = group("beginningOpen") == "("
val endingOpen = group("endingOpen") == ")"
val beginning = group("beginning")?.toDouble()
val ending = group("ending")?.toDouble()
return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen)
}
return null
}
enum class Operator(val operator: String) {
LESS("<") {
override fun matches(comparisonResult: Int): Boolean {
return comparisonResult < 0
}
},
LESS_EQUALS("<=") {
override fun matches(comparisonResult: Int): Boolean {
return comparisonResult <= 0
}
},
GREATER(">") {
override fun matches(comparisonResult: Int): Boolean {
return comparisonResult > 0
}
},
GREATER_EQUALS(">=") {
override fun matches(comparisonResult: Int): Boolean {
return comparisonResult >= 0
}
},
;
abstract fun matches(comparisonResult: Int): Boolean
}
private val operatorPattern = "(?<operator>${Operator.entries.joinToString("|") {it.operator}})(?<value>[0-9.]+)".toPattern()
fun parseOperator(string: String): OperatorMatcher? {
operatorPattern.useMatch<Nothing>(string) {
val operatorName = group("operator")
val operator = Operator.entries.find { it.operator == operatorName }!!
val value = group("value").toDouble()
return OperatorMatcher(operator, value)
}
return null
}
data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() {
override fun test(number: Number): Boolean {
return operator.matches(number.toDouble().compareTo(value))
}
}
data class MatchNumberExact(val number: Number) : NumberMatcher() {
override fun test(number: Number): Boolean {
return when (this.number) {
is Double -> number.toDouble() == this.number.toDouble()
else -> number.toLong() == this.number.toLong()
}
}
}
data class RangeMatcher(
val beginning: Double?,
val beginningInclusive: Boolean,
val ending: Double?,
val endingInclusive: Boolean,
) : NumberMatcher() {
override fun test(number: Number): Boolean {
val value = number.toDouble()
if (beginning != null) {
if (beginningInclusive) {
if (value < beginning) return false
} else {
if (value <= beginning) return false
}
}
if (ending != null) {
if (endingInclusive) {
if (value > ending) return false
} else {
if (value >= ending) return false
}
}
return true
}
}
}
}

View File

@@ -0,0 +1,26 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import net.minecraft.item.ItemStack
class OrPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return children.any { it.test(stack) }
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
val children =
(jsonElement as JsonArray)
.flatMap {
CustomModelOverrideParser.parsePredicates(it as JsonObject)
}
.toTypedArray()
return OrPredicate(children)
}
}
}

View File

@@ -0,0 +1,66 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import net.minecraft.item.ItemStack
import moe.nea.firmament.repo.ExpLadders
import moe.nea.firmament.util.petData
class PetPredicate(
val petId: StringMatcher?,
val tier: RarityMatcher?,
val exp: NumberMatcher?,
val candyUsed: NumberMatcher?,
val level: NumberMatcher?,
) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
val petData = stack.petData ?: return false
if (petId != null) {
if (!petId.matches(petData.type)) return false
}
if (exp != null) {
if (!exp.test(petData.exp)) return false
}
if (candyUsed != null) {
if (!candyUsed.test(petData.candyUsed)) return false
}
if (tier != null) {
if (!tier.match(petData.tier)) return false
}
val levelData by lazy(LazyThreadSafetyMode.NONE) {
ExpLadders.getExpLadder(petData.type, petData.tier)
.getPetLevel(petData.exp)
}
if (level != null) {
if (!level.test(levelData.currentLevel)) return false
}
return true
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
if (jsonElement.isJsonPrimitive) {
return PetPredicate(StringMatcher.Equals(jsonElement.asString, false), null, null, null, null)
}
if (jsonElement !is JsonObject) return null
val idMatcher = jsonElement["id"]?.let(StringMatcher::parse)
val expMatcher = jsonElement["exp"]?.let(NumberMatcher::parse)
val levelMatcher = jsonElement["level"]?.let(NumberMatcher::parse)
val candyMatcher = jsonElement["candyUsed"]?.let(NumberMatcher::parse)
val tierMatcher = jsonElement["tier"]?.let(RarityMatcher::parse)
return PetPredicate(
idMatcher,
tierMatcher,
expMatcher,
candyMatcher,
levelMatcher,
)
}
}
override fun toString(): String {
return super.toString()
}
}

View File

@@ -0,0 +1,69 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import io.github.moulberry.repo.data.Rarity
import moe.nea.firmament.util.useMatch
abstract class RarityMatcher {
abstract fun match(rarity: Rarity): Boolean
companion object {
fun parse(jsonElement: JsonElement): RarityMatcher {
val string = jsonElement.asString
val range = parseRange(string)
if (range != null) return range
return Exact(Rarity.valueOf(string))
}
private val allRarities = Rarity.entries.joinToString("|", "(?:", ")")
private val intervalSpec =
"(?<beginningOpen>[\\[\\(])(?<beginning>$allRarities)?,(?<ending>$allRarities)?(?<endingOpen>[\\]\\)])"
.toPattern()
fun parseRange(string: String): RangeMatcher? {
intervalSpec.useMatch<Nothing>(string) {
// Open in the set-theory sense, meaning does not include its end.
val beginningOpen = group("beginningOpen") == "("
val endingOpen = group("endingOpen") == ")"
val beginning = group("beginning")?.let(Rarity::valueOf)
val ending = group("ending")?.let(Rarity::valueOf)
return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen)
}
return null
}
}
data class Exact(val expected: Rarity) : RarityMatcher() {
override fun match(rarity: Rarity): Boolean {
return rarity == expected
}
}
data class RangeMatcher(
val beginning: Rarity?,
val beginningInclusive: Boolean,
val ending: Rarity?,
val endingInclusive: Boolean,
) : RarityMatcher() {
override fun match(rarity: Rarity): Boolean {
if (beginning != null) {
if (beginningInclusive) {
if (rarity < beginning) return false
} else {
if (rarity <= beginning) return false
}
}
if (ending != null) {
if (endingInclusive) {
if (rarity > ending) return false
} else {
if (rarity >= ending) return false
}
}
return true
}
}
}

View File

@@ -0,0 +1,159 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.internal.LazilyParsedNumber
import java.util.function.Predicate
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.minecraft.nbt.NbtString
import net.minecraft.text.Text
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.removeColorCodes
@Serializable(with = StringMatcher.Serializer::class)
interface StringMatcher {
fun matches(string: String): Boolean
fun matches(text: Text): Boolean {
return matches(text.string)
}
fun matches(nbt: NbtString): Boolean {
val string = nbt.asString()
val jsonStart = string.indexOf('{')
val stringStart = string.indexOf('"')
val isString = stringStart >= 0 && string.subSequence(0, stringStart).isBlank()
val isJson = jsonStart >= 0 && string.subSequence(0, jsonStart).isBlank()
if (isString || isJson)
return matches(Text.Serialization.fromJson(string, MC.defaultRegistries) ?: return false)
return matches(string)
}
class Equals(input: String, val stripColorCodes: Boolean) : StringMatcher {
private val expected = if (stripColorCodes) input.removeColorCodes() else input
override fun matches(string: String): Boolean {
return expected == (if (stripColorCodes) string.removeColorCodes() else string)
}
override fun toString(): String {
return "Equals($expected, stripColorCodes = $stripColorCodes)"
}
}
class Pattern(val patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher {
private val regex: Predicate<String> = patternWithColorCodes.toPattern().asMatchPredicate()
override fun matches(string: String): Boolean {
return regex.test(if (stripColorCodes) string.removeColorCodes() else string)
}
override fun toString(): String {
return "Pattern($patternWithColorCodes, stripColorCodes = $stripColorCodes)"
}
}
object Serializer : KSerializer<StringMatcher> {
val delegateSerializer = kotlinx.serialization.json.JsonElement.serializer()
override val descriptor: SerialDescriptor
get() = SerialDescriptor("StringMatcher", delegateSerializer.descriptor)
override fun deserialize(decoder: Decoder): StringMatcher {
val delegate = decoder.decodeSerializableValue(delegateSerializer)
val gsonDelegate = delegate.intoGson()
return parse(gsonDelegate)
}
override fun serialize(encoder: Encoder, value: StringMatcher) {
encoder.encodeSerializableValue(delegateSerializer, Companion.serialize(value).intoKotlinJson())
}
}
companion object {
fun serialize(stringMatcher: StringMatcher): JsonElement {
TODO("Cannot serialize string matchers rn")
}
fun parse(jsonElement: JsonElement): StringMatcher {
if (jsonElement is JsonPrimitive) {
return Equals(jsonElement.asString, true)
}
if (jsonElement is JsonObject) {
val regex = jsonElement["regex"] as JsonPrimitive?
val text = jsonElement["equals"] as JsonPrimitive?
val shouldStripColor = when (val color = (jsonElement["color"] as JsonPrimitive?)?.asString) {
"preserve" -> false
"strip", null -> true
else -> error("Unknown color preservation mode: $color")
}
if ((regex == null) == (text == null)) error("Could not parse $jsonElement as string matcher")
if (regex != null)
return Pattern(regex.asString, shouldStripColor)
if (text != null)
return Equals(text.asString, shouldStripColor)
}
error("Could not parse $jsonElement as a string matcher")
}
}
}
fun JsonElement.intoKotlinJson(): kotlinx.serialization.json.JsonElement {
when (this) {
is JsonNull -> return kotlinx.serialization.json.JsonNull
is JsonObject -> {
return kotlinx.serialization.json.JsonObject(this.entrySet()
.associate { it.key to it.value.intoKotlinJson() })
}
is JsonArray -> {
return kotlinx.serialization.json.JsonArray(this.map { it.intoKotlinJson() })
}
is JsonPrimitive -> {
if (this.isString)
return kotlinx.serialization.json.JsonPrimitive(this.asString)
if (this.isBoolean)
return kotlinx.serialization.json.JsonPrimitive(this.asBoolean)
return kotlinx.serialization.json.JsonPrimitive(this.asNumber)
}
else -> error("Unknown json variant $this")
}
}
fun kotlinx.serialization.json.JsonElement.intoGson(): JsonElement {
when (this) {
is kotlinx.serialization.json.JsonNull -> return JsonNull.INSTANCE
is kotlinx.serialization.json.JsonPrimitive -> {
if (this.isString)
return JsonPrimitive(this.content)
if (this.content == "true")
return JsonPrimitive(true)
if (this.content == "false")
return JsonPrimitive(false)
return JsonPrimitive(LazilyParsedNumber(this.content))
}
is kotlinx.serialization.json.JsonObject -> {
val obj = JsonObject()
for ((k, v) in this) {
obj.add(k, v.intoGson())
}
return obj
}
is kotlinx.serialization.json.JsonArray -> {
val arr = JsonArray()
for (v in this) {
arr.add(v.intoGson())
}
return arr
}
}
}

View File

@@ -0,0 +1,131 @@
package moe.nea.firmament.features.world
import io.github.moulberry.repo.data.Coordinate
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import net.minecraft.text.Text
import net.minecraft.util.math.Vec3d
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.blockPos
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld
import moe.nea.firmament.util.unformattedString
object FairySouls : FirmamentFeature {
@Serializable
data class Data(
val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf()
)
override val config: ManagedConfig
get() = TConfig
object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data)
object TConfig : ManagedConfig("fairy-souls") {
val displaySouls by toggle("show") { false }
val resetSouls by button("reset") {
DConfig.data?.foundSouls?.clear() != null
updateMissingSouls()
}
}
override val identifier: String get() = "fairy-souls"
val playerReach = 5
val playerReachSquared = playerReach * playerReach
var currentLocationName: SkyBlockIsland? = null
var currentLocationSouls: List<Coordinate> = emptyList()
var currentMissingSouls: List<Coordinate> = emptyList()
fun updateMissingSouls() {
currentMissingSouls = emptyList()
val c = DConfig.data ?: return
val fi = c.foundSouls[currentLocationName] ?: setOf()
val cms = currentLocationSouls.toMutableList()
fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) }
currentMissingSouls = cms
}
fun updateWorldSouls() {
currentLocationSouls = emptyList()
val loc = currentLocationName ?: return
currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return
}
fun findNearestClickableSoul(): Coordinate? {
val player = MC.player ?: return null
val pos = player.pos
val location = SBData.skyblockLocation ?: return null
val soulLocations: List<Coordinate> =
RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null
return soulLocations
.map { it to it.blockPos.getSquaredDistance(pos) }
.filter { it.second < playerReachSquared }
.minByOrNull { it.second }
?.first
}
private fun markNearestSoul() {
val nearestSoul = findNearestClickableSoul() ?: return
val c = DConfig.data ?: return
val loc = currentLocationName ?: return
val idx = currentLocationSouls.indexOf(nearestSoul)
c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx)
DConfig.markDirty()
updateMissingSouls()
}
@Subscribe
fun onWorldRender(it: WorldRenderLastEvent) {
if (!TConfig.displaySouls) return
renderInWorld(it) {
color(1F, 1F, 0F, 0.8F)
currentMissingSouls.forEach {
block(it.blockPos)
}
color(1f, 0f, 1f, 1f)
currentLocationSouls.forEach {
wireframeCube(it.blockPos)
}
}
}
@Subscribe
fun onProcessChat(it: ProcessChatEvent) {
when (it.text.unformattedString) {
"You have already found that Fairy Soul!" -> {
markNearestSoul()
}
"SOUL! You found a Fairy Soul!" -> {
markNearestSoul()
}
}
}
@Subscribe
fun onLocationChange(it: SkyblockServerUpdateEvent) {
currentLocationName = it.newLocraw?.skyblockLocation
updateWorldSouls()
updateMissingSouls()
}
}

View File

@@ -0,0 +1,40 @@
package moe.nea.firmament.features.world
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.ReloadRegistrationEvent
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil
object NPCWaypoints {
var allNpcWaypoints = listOf<NavigableWaypoint>()
@Subscribe
fun onRepoReloadRegistration(event: ReloadRegistrationEvent) {
event.repo.registerReloadListener {
allNpcWaypoints = it.items.items.values
.asSequence()
.filter { !it.island.isNullOrBlank() }
.map {
NavigableWaypoint.NPCWaypoint(it)
}
.toList()
}
}
@Subscribe
fun onOpenGui(event: CommandEvent.SubCommand) {
event.subcommand("npcs") {
thenExecute {
ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen(
"npc_waypoints",
NpcWaypointGui(allNpcWaypoints),
null))
}
}
}
}

View File

@@ -0,0 +1,22 @@
package moe.nea.firmament.features.world
import io.github.moulberry.repo.data.NEUItem
import net.minecraft.util.math.BlockPos
import moe.nea.firmament.util.SkyBlockIsland
abstract class NavigableWaypoint {
abstract val name: String
abstract val position: BlockPos
abstract val island: SkyBlockIsland
data class NPCWaypoint(
val item: NEUItem,
) : NavigableWaypoint() {
override val name: String
get() = item.displayName
override val position: BlockPos
get() = BlockPos(item.x, item.y, item.z)
override val island: SkyBlockIsland
get() = SkyBlockIsland.forMode(item.island)
}
}

View File

@@ -0,0 +1,121 @@
package moe.nea.firmament.features.world
import io.github.moulberry.repo.constants.Islands
import net.minecraft.text.Text
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Position
import net.minecraft.util.math.Vec3i
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.WarpUtil
import moe.nea.firmament.util.render.RenderInWorldContext
object NavigationHelper {
var targetWaypoint: NavigableWaypoint? = null
set(value) {
field = value
recalculateRoute()
}
var nextTeleporter: Islands.Teleporter? = null
private set
val Islands.Teleporter.toIsland get() = SkyBlockIsland.forMode(this.getTo())
val Islands.Teleporter.fromIsland get() = SkyBlockIsland.forMode(this.getFrom())
val Islands.Teleporter.blockPos get() = BlockPos(x.toInt(), y.toInt(), z.toInt())
@Subscribe
fun onWorldSwitch(event: SkyblockServerUpdateEvent) {
recalculateRoute()
}
fun recalculateRoute() {
val tp = targetWaypoint
val currentIsland = SBData.skyblockLocation
if (tp == null || currentIsland == null) {
nextTeleporter = null
return
}
val route = findRoute(currentIsland, tp.island, mutableSetOf())
nextTeleporter = route?.get(0)
}
private fun findRoute(
fromIsland: SkyBlockIsland,
targetIsland: SkyBlockIsland,
visitedIslands: MutableSet<SkyBlockIsland>
): MutableList<Islands.Teleporter>? {
var shortestChain: MutableList<Islands.Teleporter>? = null
for (it in RepoManager.neuRepo.constants.islands.teleporters) {
if (it.toIsland in visitedIslands) continue
if (it.fromIsland != fromIsland) continue
if (it.toIsland == targetIsland) return mutableListOf(it)
visitedIslands.add(fromIsland)
val nextRoute = findRoute(it.toIsland, targetIsland, visitedIslands) ?: continue
nextRoute.add(0, it)
if (shortestChain == null || shortestChain.size > nextRoute.size) {
shortestChain = nextRoute
}
visitedIslands.remove(fromIsland)
}
return shortestChain
}
@Subscribe
fun onMovement(event: TickEvent) { // TODO: add a movement tick event maybe?
val tp = targetWaypoint ?: return
val p = MC.player ?: return
if (p.squaredDistanceTo(tp.position.toCenterPos()) < 5 * 5) {
targetWaypoint = null
}
}
@Subscribe
fun drawWaypoint(event: WorldRenderLastEvent) {
val tp = targetWaypoint ?: return
val nt = nextTeleporter
RenderInWorldContext.renderInWorld(event) {
if (nt != null) {
waypoint(nt.blockPos,
Text.literal("Teleporter to " + nt.toIsland.userFriendlyName),
Text.literal("(towards " + tp.name + "§f)"))
} else if (tp.island == SBData.skyblockLocation) {
waypoint(tp.position,
Text.literal(tp.name))
}
}
}
fun tryWarpNear() {
val tp = targetWaypoint
if (tp == null) {
MC.sendChat(Text.literal("Could not find a waypoint to warp you to. Select one first."))
return
}
WarpUtil.teleportToNearestWarp(tp.island, tp.position.asPositionView())
}
}
fun Vec3i.asPositionView(): Position {
return object : Position {
override fun getX(): Double {
return this@asPositionView.x.toDouble()
}
override fun getY(): Double {
return this@asPositionView.y.toDouble()
}
override fun getZ(): Double {
return this@asPositionView.z.toDouble()
}
}
}

View File

@@ -0,0 +1,68 @@
package moe.nea.firmament.features.world
import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind
import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures.atOnce
import moe.nea.firmament.keybindings.SavedKeyBinding
class NpcWaypointGui(
val allWaypoints: List<NavigableWaypoint>,
) {
data class NavigableWaypointW(val waypoint: NavigableWaypoint) {
@Bind
fun name() = waypoint.name
@Bind
fun isSelected() = NavigationHelper.targetWaypoint == waypoint
@Bind
fun click() {
if (SavedKeyBinding.isShiftDown()) {
NavigationHelper.targetWaypoint = waypoint
NavigationHelper.tryWarpNear()
} else if (isSelected()) {
NavigationHelper.targetWaypoint = null
} else {
NavigationHelper.targetWaypoint = waypoint
}
}
}
@JvmField
@field:Bind
var search: String = ""
var lastSearch: String? = null
@Bind("results")
fun results(): ObservableList<NavigableWaypointW> {
return results
}
@Bind
fun tick() {
if (search != lastSearch) {
updateSearch()
lastSearch = search
}
}
val results: ObservableList<NavigableWaypointW> = ObservableList(mutableListOf())
fun updateSearch() {
val split = search.split(" +".toRegex())
results.atOnce {
results.clear()
allWaypoints.filter { waypoint ->
if (search.isBlank()) {
true
} else {
split.all { waypoint.name.contains(it, ignoreCase = true) }
}
}.mapTo(results) {
NavigableWaypointW(it)
}
}
}
}

View File

@@ -0,0 +1,297 @@
package moe.nea.firmament.features.world
import com.mojang.brigadier.arguments.IntegerArgumentType
import me.shedaniel.math.Color
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import kotlinx.serialization.Serializable
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import net.minecraft.command.argument.BlockPosArgumentType
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.Text
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.commands.thenLiteral
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.render.RenderInWorldContext
object Waypoints : FirmamentFeature {
override val identifier: String
get() = "waypoints"
object TConfig : ManagedConfig(identifier) {
val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds }
val showIndex by toggle("show-index") { true }
val skipToNearest by toggle("skip-to-nearest") { false }
// TODO: look ahead size
}
data class TemporaryWaypoint(
val pos: BlockPos,
val postedAt: TimeMark,
)
override val config get() = TConfig
val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
val waypoints = mutableListOf<BlockPos>()
var ordered = false
var orderedIndex = 0
@Serializable
data class ColeWeightWaypoint(
val x: Int,
val y: Int,
val z: Int,
val r: Int = 0,
val g: Int = 0,
val b: Int = 0,
)
@Subscribe
fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) {
if (waypoints.isEmpty()) return
RenderInWorldContext.renderInWorld(event) {
if (!ordered) {
waypoints.withIndex().forEach {
color(0f, 0.3f, 0.7f, 0.5f)
block(it.value)
color(1f, 1f, 1f, 1f)
if (TConfig.showIndex)
withFacingThePlayer(it.value.toCenterPos()) {
text(Text.literal(it.index.toString()))
}
}
} else {
orderedIndex %= waypoints.size
val firstColor = Color.ofRGBA(0, 200, 40, 180)
color(firstColor)
tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f)
waypoints.withIndex().toList()
.wrappingWindow(orderedIndex, 3)
.zip(
listOf(
firstColor,
Color.ofRGBA(180, 200, 40, 150),
Color.ofRGBA(180, 80, 20, 140),
)
)
.reversed()
.forEach { (waypoint, col) ->
val (index, pos) = waypoint
color(col)
block(pos)
color(1f, 1f, 1f, 1f)
if (TConfig.showIndex)
withFacingThePlayer(pos.toCenterPos()) {
text(Text.literal(index.toString()))
}
}
}
}
}
@Subscribe
fun onTick(event: TickEvent) {
if (waypoints.isEmpty() || !ordered) return
orderedIndex %= waypoints.size
val p = MC.player?.pos ?: return
if (TConfig.skipToNearest) {
orderedIndex =
(waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size
} else {
if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) {
orderedIndex = (orderedIndex + 1) % waypoints.size
}
}
}
@Subscribe
fun onProcessChat(it: ProcessChatEvent) {
val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString)
if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(
BlockPos(
matcher.group(1).toInt(),
matcher.group(2).toInt(),
matcher.group(3).toInt(),
),
TimeMark.now()
)
}
}
@Subscribe
fun onCommand(event: CommandEvent.SubCommand) {
event.subcommand("waypoint") {
thenArgument("pos", BlockPosArgumentType.blockPos()) { pos ->
thenExecute {
val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer())
waypoints.add(position)
source.sendFeedback(
Text.stringifiedTranslatable(
"firmament.command.waypoint.added",
position.x,
position.y,
position.z
)
)
}
}
}
event.subcommand("waypoints") {
thenLiteral("clear") {
thenExecute {
waypoints.clear()
source.sendFeedback(Text.translatable("firmament.command.waypoint.clear"))
}
}
thenLiteral("toggleordered") {
thenExecute {
ordered = !ordered
if (ordered) {
val p = MC.player?.pos ?: Vec3d.ZERO
orderedIndex =
waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0
}
source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered"))
}
}
thenLiteral("skip") {
thenExecute {
if (ordered && waypoints.isNotEmpty()) {
orderedIndex = (orderedIndex + 1) % waypoints.size
source.sendFeedback(Text.translatable("firmament.command.waypoint.skip"))
} else {
source.sendError(Text.translatable("firmament.command.waypoint.skip.error"))
}
}
}
thenLiteral("remove") {
thenArgument("index", IntegerArgumentType.integer(0)) { indexArg ->
thenExecute {
val index = get(indexArg)
if (index in waypoints.indices) {
waypoints.removeAt(index)
source.sendFeedback(Text.stringifiedTranslatable(
"firmament.command.waypoint.remove",
index))
} else {
source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error"))
}
}
}
}
thenLiteral("import") {
thenExecute {
val contents = ClipboardUtils.getTextContents()
val data = try {
Firmament.json.decodeFromString<List<ColeWeightWaypoint>>(contents)
} catch (ex: Exception) {
Firmament.logger.error("Could not load waypoints from clipboard", ex)
source.sendError(Text.translatable("firmament.command.waypoint.import.error"))
return@thenExecute
}
waypoints.clear()
data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) }
source.sendFeedback(
Text.stringifiedTranslatable(
"firmament.command.waypoint.import",
data.size
)
)
}
}
}
}
@Subscribe
fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) {
temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
if (temporaryPlayerWaypointList.isEmpty()) return
RenderInWorldContext.renderInWorld(event) {
color(1f, 1f, 0f, 1f)
temporaryPlayerWaypointList.forEach { (player, waypoint) ->
block(waypoint.pos)
}
color(1f, 1f, 1f, 1f)
temporaryPlayerWaypointList.forEach { (player, waypoint) ->
val skin =
MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }
?.skinTextures
?.texture
withFacingThePlayer(waypoint.pos.toCenterPos()) {
waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player))
if (skin != null) {
matrixStack.translate(0F, -20F, 0F)
// Head front
texture(
skin, 16, 16,
1 / 8f, 1 / 8f,
2 / 8f, 2 / 8f,
)
// Head overlay
texture(
skin, 16, 16,
5 / 8f, 1 / 8f,
6 / 8f, 2 / 8f,
)
}
}
}
}
}
@Subscribe
fun onWorldReady(event: WorldReadyEvent) {
temporaryPlayerWaypointList.clear()
}
}
fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
val result = ArrayList<E>(windowSize)
if (startIndex + windowSize < size) {
result.addAll(subList(startIndex, startIndex + windowSize))
} else {
result.addAll(subList(startIndex, size))
result.addAll(subList(0, minOf(windowSize - (size - startIndex), startIndex)))
}
return result
}
fun FabricClientCommandSource.asFakeServer(): ServerCommandSource {
val source = this
return ServerCommandSource(
source.player,
source.position,
source.rotation,
null,
0,
"FakeServerCommandSource",
Text.literal("FakeServerCommandSource"),
null,
source.player
)
}