Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
This commit is contained in:
120
src/main/kotlin/features/FeatureManager.kt
Normal file
120
src/main/kotlin/features/FeatureManager.kt
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
23
src/main/kotlin/features/FirmamentFeature.kt
Normal file
23
src/main/kotlin/features/FirmamentFeature.kt
Normal 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() {}
|
||||
|
||||
}
|
||||
57
src/main/kotlin/features/chat/AutoCompletions.kt
Normal file
57
src/main/kotlin/features/chat/AutoCompletions.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/main/kotlin/features/chat/ChatLinks.kt
Normal file
161
src/main/kotlin/features/chat/ChatLinks.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/main/kotlin/features/chat/QuickCommands.kt
Normal file
100
src/main/kotlin/features/chat/QuickCommands.kt
Normal 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
|
||||
}
|
||||
}
|
||||
13
src/main/kotlin/features/debug/DebugLogger.kt
Normal file
13
src/main/kotlin/features/debug/DebugLogger.kt
Normal 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()))
|
||||
}
|
||||
}
|
||||
38
src/main/kotlin/features/debug/DebugView.kt
Normal file
38
src/main/kotlin/features/debug/DebugView.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/main/kotlin/features/debug/DeveloperFeatures.kt
Normal file
55
src/main/kotlin/features/debug/DeveloperFeatures.kt
Normal 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() }
|
||||
}
|
||||
}
|
||||
|
||||
27
src/main/kotlin/features/debug/MinorTrolling.kt
Normal file
27
src/main/kotlin/features/debug/MinorTrolling.kt
Normal 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("&", "§"))
|
||||
}
|
||||
}
|
||||
193
src/main/kotlin/features/debug/PowerUserTools.kt
Normal file
193
src/main/kotlin/features/debug/PowerUserTools.kt
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
131
src/main/kotlin/features/diana/AncestralSpadeSolver.kt
Normal file
131
src/main/kotlin/features/diana/AncestralSpadeSolver.kt
Normal 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
|
||||
|
||||
}
|
||||
35
src/main/kotlin/features/diana/DianaWaypoints.kt
Normal file
35
src/main/kotlin/features/diana/DianaWaypoints.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
144
src/main/kotlin/features/diana/NearbyBurrowsSolver.kt
Normal file
144
src/main/kotlin/features/diana/NearbyBurrowsSolver.kt
Normal 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))
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
17
src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
Normal file
17
src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
Normal 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"
|
||||
}
|
||||
276
src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
Normal file
276
src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
51
src/main/kotlin/features/fixes/CompatibliltyFeatures.kt
Normal file
51
src/main/kotlin/features/fixes/CompatibliltyFeatures.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/main/kotlin/features/fixes/Fixes.kt
Normal file
71
src/main/kotlin/features/fixes/Fixes.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
66
src/main/kotlin/features/inventory/CraftingOverlay.kt
Normal file
66
src/main/kotlin/features/inventory/CraftingOverlay.kt
Normal 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()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
Normal file
85
src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
51
src/main/kotlin/features/inventory/PriceData.kt
Normal file
51
src/main/kotlin/features/inventory/PriceData.kt
Normal 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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/main/kotlin/features/inventory/SaveCursorPosition.kt
Normal file
66
src/main/kotlin/features/inventory/SaveCursorPosition.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
203
src/main/kotlin/features/inventory/SlotLocking.kt
Normal file
203
src/main/kotlin/features/inventory/SlotLocking.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ?: "")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/main/kotlin/features/mining/Histogram.kt
Normal file
81
src/main/kotlin/features/mining/Histogram.kt
Normal 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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
176
src/main/kotlin/features/mining/PickaxeAbility.kt
Normal file
176
src/main/kotlin/features/mining/PickaxeAbility.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
133
src/main/kotlin/features/mining/PristineProfitTracker.kt
Normal file
133
src/main/kotlin/features/mining/PristineProfitTracker.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/main/kotlin/features/notifications/Notifications.kt
Normal file
7
src/main/kotlin/features/notifications/Notifications.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
package moe.nea.firmament.features.notifications
|
||||
|
||||
import moe.nea.firmament.features.FirmamentFeature
|
||||
|
||||
object Notifications {
|
||||
}
|
||||
17
src/main/kotlin/features/texturepack/AlwaysPredicate.kt
Normal file
17
src/main/kotlin/features/texturepack/AlwaysPredicate.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/main/kotlin/features/texturepack/AndPredicate.kt
Normal file
26
src/main/kotlin/features/texturepack/AndPredicate.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
9
src/main/kotlin/features/texturepack/BakedModelExtra.kt
Normal file
9
src/main/kotlin/features/texturepack/BakedModelExtra.kt
Normal 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?)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
package moe.nea.firmament.features.texturepack
|
||||
|
||||
interface BakedOverrideData {
|
||||
fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
|
||||
fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
|
||||
|
||||
}
|
||||
295
src/main/kotlin/features/texturepack/CustomBlockTextures.kt
Normal file
295
src/main/kotlin/features/texturepack/CustomBlockTextures.kt
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
167
src/main/kotlin/features/texturepack/CustomGlobalTextures.kt
Normal file
167
src/main/kotlin/features/texturepack/CustomGlobalTextures.kt
Normal 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
114
src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt
Normal file
114
src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
22
src/main/kotlin/features/texturepack/DisplayNamePredicate.kt
Normal file
22
src/main/kotlin/features/texturepack/DisplayNamePredicate.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
268
src/main/kotlin/features/texturepack/ExtraAttributesPredicate.kt
Normal file
268
src/main/kotlin/features/texturepack/ExtraAttributesPredicate.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
package moe.nea.firmament.features.texturepack
|
||||
|
||||
import net.minecraft.item.ItemStack
|
||||
|
||||
interface FirmamentModelPredicate {
|
||||
fun test(stack: ItemStack): Boolean
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
package moe.nea.firmament.features.texturepack
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
|
||||
interface FirmamentModelPredicateParser {
|
||||
fun parse(jsonElement: JsonElement): FirmamentModelPredicate?
|
||||
}
|
||||
32
src/main/kotlin/features/texturepack/ItemPredicate.kt
Normal file
32
src/main/kotlin/features/texturepack/ItemPredicate.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
19
src/main/kotlin/features/texturepack/LorePredicate.kt
Normal file
19
src/main/kotlin/features/texturepack/LorePredicate.kt
Normal 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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
package moe.nea.firmament.features.texturepack
|
||||
|
||||
interface ModelOverrideData {
|
||||
fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
|
||||
fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
|
||||
}
|
||||
@@ -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:")
|
||||
}
|
||||
}
|
||||
18
src/main/kotlin/features/texturepack/NotPredicate.kt
Normal file
18
src/main/kotlin/features/texturepack/NotPredicate.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/main/kotlin/features/texturepack/NumberMatcher.kt
Normal file
125
src/main/kotlin/features/texturepack/NumberMatcher.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/kotlin/features/texturepack/OrPredicate.kt
Normal file
26
src/main/kotlin/features/texturepack/OrPredicate.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
66
src/main/kotlin/features/texturepack/PetPredicate.kt
Normal file
66
src/main/kotlin/features/texturepack/PetPredicate.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
69
src/main/kotlin/features/texturepack/RarityMatcher.kt
Normal file
69
src/main/kotlin/features/texturepack/RarityMatcher.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
159
src/main/kotlin/features/texturepack/StringMatcher.kt
Normal file
159
src/main/kotlin/features/texturepack/StringMatcher.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/main/kotlin/features/world/FairySouls.kt
Normal file
131
src/main/kotlin/features/world/FairySouls.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
40
src/main/kotlin/features/world/NPCWaypoints.kt
Normal file
40
src/main/kotlin/features/world/NPCWaypoints.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
22
src/main/kotlin/features/world/NavigableWaypoint.kt
Normal file
22
src/main/kotlin/features/world/NavigableWaypoint.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
121
src/main/kotlin/features/world/NavigationHelper.kt
Normal file
121
src/main/kotlin/features/world/NavigationHelper.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/main/kotlin/features/world/NpcWaypointGui.kt
Normal file
68
src/main/kotlin/features/world/NpcWaypointGui.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
297
src/main/kotlin/features/world/Waypoints.kt
Normal file
297
src/main/kotlin/features/world/Waypoints.kt
Normal 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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user