aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/features')
-rw-r--r--src/main/kotlin/features/FeatureManager.kt147
-rw-r--r--src/main/kotlin/features/debug/AnimatedClothingScanner.kt51
-rw-r--r--src/main/kotlin/features/fixes/Fixes.kt1
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt123
-rw-r--r--src/main/kotlin/features/inventory/TimerInLore.kt3
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt4
-rw-r--r--src/main/kotlin/features/mining/MiningBlockInfoUi.kt54
-rw-r--r--src/main/kotlin/features/world/ColeWeightCompat.kt125
-rw-r--r--src/main/kotlin/features/world/FirmWaypointManager.kt168
-rw-r--r--src/main/kotlin/features/world/FirmWaypoints.kt37
-rw-r--r--src/main/kotlin/features/world/TemporaryWaypoints.kt73
-rw-r--r--src/main/kotlin/features/world/Waypoints.kt317
12 files changed, 803 insertions, 300 deletions
diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt
index 9a3cbf8..f0c1857 100644
--- a/src/main/kotlin/features/FeatureManager.kt
+++ b/src/main/kotlin/features/FeatureManager.kt
@@ -31,89 +31,96 @@ import moe.nea.firmament.features.mining.PickaxeAbility
import moe.nea.firmament.features.mining.PristineProfitTracker
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.features.world.Waypoints
+import moe.nea.firmament.util.compatloader.ICompatMeta
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()
- )
+ @Serializable
+ data class Config(
+ val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf()
+ )
- private val features = mutableMapOf<String, FirmamentFeature>()
+ private val features = mutableMapOf<String, FirmamentFeature>()
- val allFeatures: Collection<FirmamentFeature> get() = features.values
+ val allFeatures: Collection<FirmamentFeature> get() = features.values
- private var hasAutoloaded = false
+ private var hasAutoloaded = false
- 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(PetFeatures)
- loadFeature(SaveCursorPosition)
- 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 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(PetFeatures)
+ loadFeature(SaveCursorPosition)
+ 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() {
- SubscriptionList.allLists.forEach {
- it.provideSubscriptions {
- it.owner.javaClass.classes.forEach {
- runCatching { it.getDeclaredField("INSTANCE").get(null) }
+ fun subscribeEvents() {
+ SubscriptionList.allLists.forEach { list ->
+ if (ICompatMeta.shouldLoad(list.javaClass.name))
+ runCatching {
+ list.provideSubscriptions {
+ it.owner.javaClass.classes.forEach {
+ runCatching { it.getDeclaredField("INSTANCE").get(null) }
+ }
+ subscribeSingleEvent(it)
+ }
+ }.getOrElse {
+ // TODO: allow annotating source sets to specifically opt out of loading for mods, maybe automatically
+ Firmament.logger.info("Ignoring events from $list, likely due to a missing compat mod.", it)
}
- subscribeSingleEvent(it)
- }
- }
- }
+ }
+ }
- private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
- it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke)
- }
+ private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
+ it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", 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 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 isEnabled(identifier: String): Boolean? =
+ data.enabledFeatures[identifier]
- fun setEnabled(identifier: String, value: Boolean) {
- data.enabledFeatures[identifier] = value
- markDirty()
- }
+ fun setEnabled(identifier: String, value: Boolean) {
+ data.enabledFeatures[identifier] = value
+ markDirty()
+ }
}
diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
new file mode 100644
index 0000000..11b47a9
--- /dev/null
+++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
@@ -0,0 +1,51 @@
+package moe.nea.firmament.features.debug
+
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.entity.Entity
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.EntityUpdateEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+
+object AnimatedClothingScanner {
+
+ var observedEntity: Entity? = null
+
+ @OptIn(ExperimentalStdlibApi::class)
+ @Subscribe
+ fun onUpdate(event: EntityUpdateEvent) {
+ if (event.entity != observedEntity) return
+ if (event is EntityUpdateEvent.EquipmentUpdate) {
+ event.newEquipment.forEach {
+ val id = it.second.skyBlockId?.neuItem
+ val colour = it.second.get(DataComponentTypes.DYED_COLOR)
+ ?.rgb?.toHexString(HexFormat.UpperCase)
+ ?.let { " #$it" } ?: ""
+ MC.sendChat(tr("firmament.fitstealer.update",
+ "[FIT CHECK][${MC.currentTick}] ${it.first.asString()} => ${id}${colour}"))
+ }
+ }
+ }
+
+ @Subscribe
+ fun onSubCommand(event: CommandEvent.SubCommand) {
+ event.subcommand("dev") {
+ thenLiteral("stealthisfit") {
+ thenExecute {
+ observedEntity =
+ if (observedEntity == null) MC.instance.targetedEntity else null
+
+ MC.sendChat(
+ observedEntity?.let {
+ tr("firmament.fitstealer.targeted", "Observing the equipment of ${it.name}.")
+ } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt
index 7030319..3dae233 100644
--- a/src/main/kotlin/features/fixes/Fixes.kt
+++ b/src/main/kotlin/features/fixes/Fixes.kt
@@ -23,6 +23,7 @@ object Fixes : FirmamentFeature {
val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
val peekChat by keyBindingWithDefaultUnbound("peek-chat")
val hidePotionEffects by toggle("hide-mob-effects") { false }
+ val noHurtCam by toggle("disable-hurt-cam") { false }
}
override val config: ManagedConfig
diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt
index 7a3a152..0083c40 100644
--- a/src/main/kotlin/features/inventory/SlotLocking.kt
+++ b/src/main/kotlin/features/inventory/SlotLocking.kt
@@ -4,8 +4,17 @@ package moe.nea.firmament.features.inventory
import java.util.UUID
import org.lwjgl.glfw.GLFW
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
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.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.int
import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.entity.player.PlayerInventory
@@ -51,9 +60,66 @@ object SlotLocking : FirmamentFeature {
val lockedSlots: MutableSet<Int> = mutableSetOf(),
val lockedSlotsRift: MutableSet<Int> = mutableSetOf(),
val lockedUUIDs: MutableSet<UUID> = mutableSetOf(),
- val boundSlots: MutableMap<Int, Int> = mutableMapOf()
+ val boundSlots: BoundSlots = BoundSlots()
)
+ @Serializable
+ data class BoundSlot(
+ val hotbar: Int,
+ val inventory: Int,
+ )
+
+ @Serializable(with = BoundSlots.Serializer::class)
+ data class BoundSlots(
+ val pairs: MutableSet<BoundSlot> = mutableSetOf()
+ ) {
+ fun findMatchingSlots(index: Int): List<BoundSlot> {
+ return pairs.filter { it.hotbar == index || it.inventory == index }
+ }
+
+ fun removeDuplicateForInventory(index: Int) {
+ pairs.removeIf { it.inventory == index }
+ }
+
+ fun removeAllInvolving(index: Int): Boolean {
+ return pairs.removeIf { it.inventory == index || it.hotbar == index }
+ }
+
+ fun insert(hotbar: Int, inventory: Int) {
+ if (!TConfig.allowMultiBinding) {
+ removeAllInvolving(hotbar)
+ removeAllInvolving(inventory)
+ }
+ pairs.add(BoundSlot(hotbar, inventory))
+ }
+
+ object Serializer : KSerializer<BoundSlots> {
+ override val descriptor: SerialDescriptor
+ get() = serializer<JsonElement>().descriptor
+
+ override fun serialize(
+ encoder: Encoder,
+ value: BoundSlots
+ ) {
+ serializer<MutableSet<BoundSlot>>()
+ .serialize(encoder, value.pairs)
+ }
+
+ override fun deserialize(decoder: Decoder): BoundSlots {
+ decoder as JsonDecoder
+ val json = decoder.decodeJsonElement()
+ if (json is JsonObject) {
+ return BoundSlots(json.entries.map {
+ BoundSlot(it.key.toInt(), (it.value as JsonPrimitive).int)
+ }.toMutableSet())
+ }
+ return BoundSlots(decoder.json.decodeFromJsonElement(serializer<MutableSet<BoundSlot>>(), json))
+
+ }
+ }
+ }
+
+
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L }
val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") {
@@ -62,6 +128,7 @@ object SlotLocking : FirmamentFeature {
val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L }
val slotBindRequireShift by toggle("require-quick-move") { true }
val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES }
+ val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option
val allowDroppingInDungeons by toggle("drop-in-dungeons") { true }
}
@@ -177,19 +244,19 @@ object SlotLocking : FirmamentFeature {
@Subscribe
fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) {
- val boundSlots = DConfig.data?.boundSlots ?: mapOf()
+ val boundSlots = DConfig.data?.boundSlots ?: BoundSlots()
val isValidAction =
it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift)
if (!isValidAction) return
val handler = MC.handledScreen?.screenHandler ?: return
val slot = it.slot
if (slot != null && it.slot.inventory is PlayerInventory) {
- val boundSlot = boundSlots.entries.find {
- it.value == slot.index || it.key == slot.index
- } ?: return
+ val matchingSlots = boundSlots.findMatchingSlots(slot.index)
+ if (matchingSlots.isEmpty()) return
it.protectSilent()
- val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true)
- inventorySlot?.swapWithHotBar(handler, boundSlot.key)
+ val boundSlot = matchingSlots.singleOrNull() ?: return
+ val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true)
+ inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar)
}
}
@@ -228,10 +295,8 @@ object SlotLocking : FirmamentFeature {
val boundSlots = DConfig.data?.boundSlots ?: return
lockedSlots?.remove(hotBarSlot.index)
lockedSlots?.remove(invSlot.index)
- boundSlots.entries.removeIf {
- it.value == invSlot.index
- }
- boundSlots[hotBarSlot.index] = invSlot.index
+ boundSlots.removeDuplicateForInventory(invSlot.index)
+ boundSlots.insert(hotBarSlot.index, invSlot.index)
DConfig.markDirty()
CommonSoundEffects.playSuccess()
return
@@ -245,9 +310,7 @@ object SlotLocking : FirmamentFeature {
storedLockingSlot = null
val boundSlots = DConfig.data?.boundSlots ?: return
if (slot != null)
- boundSlots.entries.removeIf {
- it.value == slot.index || it.key == slot.index
- }
+ boundSlots.removeAllInvolving(slot.index)
}
}
@@ -258,9 +321,10 @@ object SlotLocking : FirmamentFeature {
val accScreen = event.screen as AccessorHandledScreen
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
- for (it in boundSlots.entries) {
- val hotbarSlot = findByIndex(it.key) ?: continue
- val inventorySlot = findByIndex(it.value) ?: continue
+ val highlitSlots = mutableSetOf<Slot>()
+ for (it in boundSlots.pairs) {
+ val hotbarSlot = findByIndex(it.hotbar) ?: continue
+ val inventorySlot = findByIndex(it.inventory) ?: continue
val (hotX, hotY) = hotbarSlot.lineCenter()
val (invX, invY) = inventorySlot.lineCenter()
@@ -268,22 +332,27 @@ object SlotLocking : FirmamentFeature {
|| accScreen.focusedSlot_Firmament === inventorySlot
if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING)
continue
- val color = if (anyHovered)
- me.shedaniel.math.Color.ofOpaque(0x00FF00)
- else
- me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt())
+ if (anyHovered) {
+ highlitSlots.add(hotbarSlot)
+ highlitSlots.add(inventorySlot)
+ }
+ fun color(highlit: Boolean) =
+ if (highlit)
+ me.shedaniel.math.Color.ofOpaque(0x00FF00)
+ else
+ me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt())
if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered)
event.context.drawLine(
invX + sx, invY + sy,
hotX + sx, hotY + sy,
- color
+ color(anyHovered)
)
event.context.drawBorder(hotbarSlot.x + sx,
hotbarSlot.y + sy,
- 16, 16, color.color)
+ 16, 16, color(hotbarSlot in highlitSlots).color)
event.context.drawBorder(inventorySlot.x + sx,
inventorySlot.y + sy,
- 16, 16, color.color)
+ 16, 16, color(inventorySlot in highlitSlots).color)
}
}
@@ -339,11 +408,9 @@ object SlotLocking : FirmamentFeature {
fun toggleSlotLock(slot: Slot) {
val lockedSlots = lockedSlots ?: return
- val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf()
+ val boundSlots = DConfig.data?.boundSlots ?: BoundSlots()
if (slot.inventory is PlayerInventory) {
- if (boundSlots.entries.removeIf {
- it.value == slot.index || it.key == slot.index
- }) {
+ if (boundSlots.removeAllInvolving(slot.index)) {
// intentionally do nothing
} else if (slot.index in lockedSlots) {
lockedSlots.remove(slot.index)
diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt
index f1b77c6..309ea61 100644
--- a/src/main/kotlin/features/inventory/TimerInLore.kt
+++ b/src/main/kotlin/features/inventory/TimerInLore.kt
@@ -80,7 +80,8 @@ object TimerInLore {
COMMUNITYPROJECTS("Contribute again", "Come back at"),
CHOCOLATEFACTORY("Next Charge", "Available at"),
STONKSAUCTION("Auction ends in", "Ends at"),
- LIZSTONKREDEMPTION("Resets in:", "Resets at");
+ LIZSTONKREDEMPTION("Resets in:", "Resets at"),
+ EVENTENDING("Event ends in:", "Ends at");
}
val regex =
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
index 633a8fe..63a2f54 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
@@ -357,6 +357,10 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
return super.keyReleased(keyCode, scanCode, modifiers)
}
+ override fun shouldCloseOnEsc(): Boolean {
+ return this === MC.screen // Fixes this UI closing the handled screen on Escape press.
+ }
+
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (typeMCComponentInPlace(
controlComponent,
diff --git a/src/main/kotlin/features/mining/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt
new file mode 100644
index 0000000..e8ea4f4
--- /dev/null
+++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt
@@ -0,0 +1,54 @@
+package moe.nea.firmament.features.mining
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.observer.Property
+import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.repo.MiningRepoData
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.SkyBlockIsland
+
+object MiningBlockInfoUi {
+ class MiningInfo(miningData: MiningRepoData) {
+ @field:Bind("search")
+ @JvmField
+ var search = ""
+
+ @get:Bind("ores")
+ val blocks = miningData.customMiningBlocks.mapTo(ObservableList(mutableListOf())) { OreInfo(it, this) }
+ }
+
+ class OreInfo(block: MiningRepoData.CustomMiningBlock, info: MiningInfo) {
+ @get:Bind("oreName")
+ val oreName = block.name ?: "No Name"
+
+ @get:Bind("blocks")
+ val res = ObservableList(block.blocks189.map { BlockInfo(it, info) })
+ }
+
+ class BlockInfo(val block: MiningRepoData.Block189, val info: MiningInfo) {
+ @get:Bind("item")
+ val item = ModernItemStack.of(block.block?.let { ItemStack(it) } ?: ItemStack.EMPTY)
+
+ @get:Bind("isSelected")
+ val isSelected get() = info.search.let { block.isActiveIn(SkyBlockIsland.forMode(it)) }
+
+ @get:Bind("itemName")
+ val itemName get() = item.getDisplayName()
+
+ @get:Bind("restrictions")
+ val res = ObservableList(
+ if (block.onlyIn != null)
+ block.onlyIn.map { " §r- §a${it.userFriendlyName}" }
+ else
+ listOf("Everywhere")
+ )
+ }
+
+ fun makeScreen(): Screen {
+ return MoulConfigUtils.loadScreen("mining_block_info/index", MiningInfo(RepoManager.miningData), null)
+ }
+}
diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt
new file mode 100644
index 0000000..b92a91e
--- /dev/null
+++ b/src/main/kotlin/features/world/ColeWeightCompat.kt
@@ -0,0 +1,125 @@
+package moe.nea.firmament.features.world
+
+import kotlinx.serialization.Serializable
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.DefaultSource
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.tr
+
+object ColeWeightCompat {
+ @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,
+ )
+
+ fun fromFirm(waypoints: FirmWaypoints, relativeTo: BlockPos): List<ColeWeightWaypoint> {
+ return waypoints.waypoints.map {
+ ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z)
+ }
+ }
+
+ fun intoFirm(waypoints: List<ColeWeightWaypoint>, relativeTo: BlockPos): FirmWaypoints {
+ val w = waypoints.map {
+ FirmWaypoints.Waypoint(it.x + relativeTo.x, it.y + relativeTo.y, it.z + relativeTo.z)
+ }
+ return FirmWaypoints(
+ "Imported Waypoints",
+ "imported",
+ null,
+ w.toMutableList(),
+ false
+ )
+ }
+
+ fun copyAndInform(
+ source: DefaultSource,
+ origin: BlockPos,
+ positiveFeedback: (Int) -> Text,
+ ) {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ ?.let { fromFirm(it, origin) }
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return
+ }
+ val data =
+ Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints)
+ ClipboardUtils.setTextContent(data)
+ source.sendFeedback(positiveFeedback(waypoints.size))
+ }
+
+ fun importAndInform(
+ source: DefaultSource,
+ pos: BlockPos?,
+ positiveFeedback: (Int) -> Text
+ ) {
+ val text = ClipboardUtils.getTextContents()
+ val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ORIGIN) }
+ val waypoints = wr.getOrElse {
+ source.sendError(
+ tr("firmament.command.waypoint.import.cw.error",
+ "Could not import ColeWeight waypoints."))
+ Firmament.logger.error(it)
+ return
+ }
+ waypoints.lastRelativeImport = pos
+ Waypoints.waypoints = waypoints
+ source.sendFeedback(positiveFeedback(waypoints.size))
+ }
+
+ @Subscribe
+ fun onEvent(event: CommandEvent.SubCommand) {
+ event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("exportcw") {
+ thenExecute {
+ copyAndInform(source, BlockPos.ORIGIN) {
+ tr("firmament.command.waypoint.export.cw",
+ "Copied $it waypoints to clipboard in ColeWeight format.")
+ }
+ }
+ }
+ thenLiteral("exportrelativecw") {
+ thenExecute {
+ copyAndInform(source, MC.player?.blockPos ?: BlockPos.ORIGIN) {
+ tr("firmament.command.waypoint.export.cw.relative",
+ "Copied $it relative waypoints to clipboard in ColeWeight format. Make sure to stand in the same position when importing.")
+ }
+ }
+ }
+ thenLiteral("importcw") {
+ thenExecute {
+ importAndInform(source, null) {
+ Text.stringifiedTranslatable("firmament.command.waypoint.import.cw",
+ it)
+ }
+ }
+ }
+ thenLiteral("importrelativecw") {
+ thenExecute {
+ importAndInform(source, MC.player!!.blockPos) {
+ tr("firmament.command.waypoint.import.cw.relative",
+ "Imported $it relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly.")
+ }
+ }
+ }
+ }
+ }
+
+ fun tryParse(string: String): Result<List<ColeWeightWaypoint>> {
+ return runCatching {
+ Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(string)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt
new file mode 100644
index 0000000..d18483c
--- /dev/null
+++ b/src/main/kotlin/features/world/FirmWaypointManager.kt
@@ -0,0 +1,168 @@
+package moe.nea.firmament.features.world
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import kotlinx.serialization.serializer
+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.suggestsList
+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.util.ClipboardUtils
+import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TemplateUtil
+import moe.nea.firmament.util.data.MultiFileDataHolder
+import moe.nea.firmament.util.tr
+
+object FirmWaypointManager {
+ object DataHolder : MultiFileDataHolder<FirmWaypoints>(serializer(), "waypoints")
+
+ val SHARE_PREFIX = "FIRM_WAYPOINTS/"
+ val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX)
+
+ fun createExportableCopy(
+ waypoints: FirmWaypoints,
+ ): FirmWaypoints {
+ val copy = waypoints.copy(waypoints = waypoints.waypoints.toMutableList())
+ if (waypoints.isRelativeTo != null) {
+ val origin = waypoints.lastRelativeImport
+ if (origin != null) {
+ copy.waypoints.replaceAll {
+ it.copy(
+ x = it.x - origin.x,
+ y = it.y - origin.y,
+ z = it.z - origin.z,
+ )
+ }
+ } else {
+ TODO("Add warning!")
+ }
+ }
+ return copy
+ }
+
+ fun loadWaypoints(waypoints: FirmWaypoints, sendFeedback: (Text) -> Unit) {
+ val copy = waypoints.deepCopy()
+ if (copy.isRelativeTo != null) {
+ val origin = MC.player!!.blockPos
+ copy.waypoints.replaceAll {
+ it.copy(
+ x = it.x + origin.x,
+ y = it.y + origin.y,
+ z = it.z + origin.z,
+ )
+ }
+ copy.lastRelativeImport = origin.toImmutable()
+ sendFeedback(tr("firmament.command.waypoint.import.ordered.success",
+ "Imported ${copy.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${copy.isRelativeTo}."))
+ } else {
+ sendFeedback(tr("firmament.command.waypoint.import.success",
+ "Imported ${copy.size} waypoints."))
+ }
+ Waypoints.waypoints = copy
+ }
+
+ fun setOrigin(source: DefaultSource, text: String?) {
+ val waypoints = Waypoints.useEditableWaypoints()
+ waypoints.isRelativeTo = text ?: waypoints.isRelativeTo ?: ""
+ val pos = MC.player!!.blockPos
+ waypoints.lastRelativeImport = pos
+ source.sendFeedback(tr("firmament.command.waypoint.originset",
+ "Set the origin of waypoints to ${FirmFormatters.formatPosition(pos)}. Run /firm waypoints export to save the waypoints relative to this position."))
+ }
+
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("setorigin") {
+ thenExecute {
+ setOrigin(source, null)
+ }
+ thenArgument("hint", RestArgumentType) { text ->
+ thenExecute {
+ setOrigin(source, this[text])
+ }
+ }
+ }
+ thenLiteral("clearorigin") {
+ thenExecute {
+ val waypoints = Waypoints.useEditableWaypoints()
+ waypoints.lastRelativeImport = null
+ waypoints.isRelativeTo = null
+ source.sendFeedback(tr("firmament.command.waypoint.originunset",
+ "Unset the origin of the waypoints. Run /firm waypoints export to save the waypoints with absolute coordinates."))
+ }
+ }
+ thenLiteral("save") {
+ thenArgument("name", StringArgumentType.string()) { name ->
+ suggestsList { DataHolder.list().keys }
+ thenExecute {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return@thenExecute
+ }
+ waypoints.id = get(name)
+ val exportableWaypoints = createExportableCopy(waypoints)
+ DataHolder.insert(get(name), exportableWaypoints)
+ DataHolder.save()
+ source.sendFeedback(tr("firmament.command.waypoint.saved",
+ "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again."))
+ }
+ }
+ }
+ thenLiteral("load") {
+ thenArgument("name", StringArgumentType.string()) { name ->
+ suggestsList { DataHolder.list().keys }
+ thenExecute {
+ val name = get(name)
+ val waypoints = DataHolder.list()[name]
+ if (waypoints == null) {
+ source.sendError(
+ tr("firmament.command.waypoint.nosaved",
+ "No saved waypoint for ${name}. Use tab completion to see available names."))
+ return@thenExecute
+ }
+ loadWaypoints(waypoints, source::sendFeedback)
+ }
+ }
+ }
+ thenLiteral("export") {
+ thenExecute {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return@thenExecute
+ }
+ val exportableWaypoints = createExportableCopy(waypoints)
+ val data = TemplateUtil.encodeTemplate(SHARE_PREFIX, exportableWaypoints)
+ ClipboardUtils.setTextContent(data)
+ source.sendFeedback(tr("firmament.command.waypoint.export",
+ "Copied ${exportableWaypoints.size} waypoints to clipboard in Firmament format."))
+ }
+ }
+ thenLiteral("import") {
+ thenExecute {
+ val text = ClipboardUtils.getTextContents()
+ if (text.startsWith("[")) {
+ source.sendError(tr("firmament.command.waypoint.import.lookslikecw",
+ "The waypoints in your clipboard look like they might be ColeWeight waypoints. If so, use /firm waypoints importcw or /firm waypoints importrelativecw."))
+ return@thenExecute
+ }
+ val waypoints = TemplateUtil.maybeDecodeTemplate<FirmWaypoints>(SHARE_PREFIX, text)
+ if (waypoints == null) {
+ source.sendError(tr("firmament.command.waypoint.import.error",
+ "Could not import Firmament waypoints from your clipboard. Make sure they are Firmament compatible waypoints."))
+ return@thenExecute
+ }
+ loadWaypoints(waypoints, source::sendFeedback)
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/FirmWaypoints.kt b/src/main/kotlin/features/world/FirmWaypoints.kt
new file mode 100644
index 0000000..d0cd55a
--- /dev/null
+++ b/src/main/kotlin/features/world/FirmWaypoints.kt
@@ -0,0 +1,37 @@
+package moe.nea.firmament.features.world
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import net.minecraft.util.math.BlockPos
+
+@Serializable
+data class FirmWaypoints(
+ var label: String,
+ var id: String,
+ /**
+ * A hint to indicate where to stand while loading the waypoints.
+ */
+ var isRelativeTo: String?,
+ var waypoints: MutableList<Waypoint>,
+ var isOrdered: Boolean,
+ // TODO: val resetOnSwap: Boolean,
+) {
+
+ fun deepCopy() = copy(waypoints = waypoints.toMutableList())
+ @Transient
+ var lastRelativeImport: BlockPos? = null
+
+ val size get() = waypoints.size
+ @Serializable
+ data class Waypoint(
+ val x: Int,
+ val y: Int,
+ val z: Int,
+ ) {
+ val blockPos get() = BlockPos(x, y, z)
+
+ companion object {
+ fun from(blockPos: BlockPos) = Waypoint(blockPos.x, blockPos.y, blockPos.z)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt
new file mode 100644
index 0000000..b36c49d
--- /dev/null
+++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt
@@ -0,0 +1,73 @@
+package moe.nea.firmament.features.world
+
+import kotlin.compareTo
+import kotlin.text.clear
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.features.world.Waypoints.TConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.render.RenderInWorldContext
+
+object TemporaryWaypoints {
+ data class TemporaryWaypoint(
+ val pos: BlockPos,
+ val postedAt: TimeMark,
+ )
+ val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
+ val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
+ @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 onRenderTemporaryWaypoints(event: WorldRenderLastEvent) {
+ temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
+ if (temporaryPlayerWaypointList.isEmpty()) return
+ RenderInWorldContext.renderInWorld(event) {
+ temporaryPlayerWaypointList.forEach { (_, waypoint) ->
+ block(waypoint.pos, 0xFFFFFF00.toInt())
+ }
+ 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()
+ }
+
+}
diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt
index 3ebfe70..b5c2b66 100644
--- a/src/main/kotlin/features/world/Waypoints.kt
+++ b/src/main/kotlin/features/world/Waypoints.kt
@@ -2,36 +2,24 @@ 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 kotlinx.serialization.encodeToString
-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.CommandOutput
-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.mc.asFakeServer
import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.tr
@@ -43,99 +31,85 @@ object Waypoints : FirmamentFeature {
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 }
+ val resetWaypointOrderOnWorldSwap by toggle("reset-order-on-swap") { true }
// 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 waypoints: FirmWaypoints? = null
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
+ val w = useNonEmptyWaypoints() ?: return
RenderInWorldContext.renderInWorld(event) {
- if (!ordered) {
- waypoints.withIndex().forEach {
- block(it.value, 0x800050A0.toInt())
- if (TConfig.showIndex)
- withFacingThePlayer(it.value.toCenterPos()) {
- text(Text.literal(it.index.toString()))
- }
+ if (!w.isOrdered) {
+ w.waypoints.withIndex().forEach {
+ block(it.value.blockPos, 0x800050A0.toInt())
+ if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.toCenterPos()) {
+ text(Text.literal(it.index.toString()))
+ }
}
} else {
- orderedIndex %= waypoints.size
+ orderedIndex %= w.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
- block(pos, col.color)
- if (TConfig.showIndex)
- withFacingThePlayer(pos.toCenterPos()) {
- text(Text.literal(index.toString()))
- }
+ tracer(w.waypoints[orderedIndex].blockPos.toCenterPos(), lineWidth = 3f)
+ w.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
+ block(pos.blockPos, col.color)
+ if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.toCenterPos()) {
+ text(Text.literal(index.toString()))
}
+ }
}
}
}
@Subscribe
fun onTick(event: TickEvent) {
- if (waypoints.isEmpty() || !ordered) return
- orderedIndex %= waypoints.size
+ val w = useNonEmptyWaypoints() ?: return
+ if (!w.isOrdered) return
+ orderedIndex %= w.waypoints.size
val p = MC.player?.pos ?: return
if (TConfig.skipToNearest) {
orderedIndex =
- (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size
+ (w.waypoints.withIndex().minBy { it.value.blockPos.getSquaredDistance(p) }.index + 1) % w.waypoints.size
+
} else {
- if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) {
- orderedIndex = (orderedIndex + 1) % waypoints.size
+ if (w.waypoints[orderedIndex].blockPos.isWithinDistance(p, 3.0)) {
+ orderedIndex = (orderedIndex + 1) % w.waypoints.size
}
}
}
+
+ fun useEditableWaypoints(): FirmWaypoints {
+ var w = waypoints
+ if (w == null) {
+ w = FirmWaypoints("Unlabeled", "unknown", null, mutableListOf(), false)
+ waypoints = w
+ }
+ return w
+ }
+
+ fun useNonEmptyWaypoints(): FirmWaypoints? {
+ val w = waypoints
+ if (w == null) return null
+ if (w.waypoints.isEmpty()) return null
+ return w
+ }
+
+ val WAYPOINTS_SUBCOMMAND = "waypoints"
+
@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()
- )
+ fun onWorldSwap(event: WorldReadyEvent) {
+ if (TConfig.resetWaypointOrderOnWorldSwap) {
+ orderedIndex = 0
}
}
@@ -144,41 +118,77 @@ object Waypoints : FirmamentFeature {
event.subcommand("waypoint") {
thenArgument("pos", BlockPosArgumentType.blockPos()) { pos ->
thenExecute {
+ source
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
- )
- )
+ val w = useEditableWaypoints()
+ w.waypoints.add(FirmWaypoints.Waypoint.from(position))
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.added",
+ position.x,
+ position.y,
+ position.z))
}
}
}
- event.subcommand("waypoints") {
+ event.subcommand(WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("reset") {
+ thenExecute {
+ orderedIndex = 0
+ source.sendFeedback(tr(
+ "firmament.command.waypoint.reset",
+ "Reset your ordered waypoint index back to 0. If you want to delete all waypoints use /firm waypoints clear instead."))
+ }
+ }
+ thenLiteral("changeindex") {
+ thenArgument("from", IntegerArgumentType.integer(0)) { fromIndex ->
+ thenArgument("to", IntegerArgumentType.integer(0)) { toIndex ->
+ thenExecute {
+ val w = useEditableWaypoints()
+ val toIndex = toIndex.get(this)
+ val fromIndex = fromIndex.get(this)
+ if (fromIndex !in w.waypoints.indices) {
+ source.sendError(textInvalidIndex(fromIndex))
+ return@thenExecute
+ }
+ if (toIndex !in w.waypoints.indices) {
+ source.sendError(textInvalidIndex(toIndex))
+ return@thenExecute
+ }
+ val waypoint = w.waypoints.removeAt(fromIndex)
+ w.waypoints.add(
+ if (toIndex > fromIndex) toIndex - 1
+ else toIndex,
+ waypoint)
+ source.sendFeedback(
+ tr("firmament.command.waypoint.indexchange",
+ "Moved waypoint from index $fromIndex to $toIndex. Note that this only matters for ordered waypoints.")
+ )
+ }
+ }
+ }
+ }
thenLiteral("clear") {
thenExecute {
- waypoints.clear()
+ waypoints = null
source.sendFeedback(Text.translatable("firmament.command.waypoint.clear"))
}
}
thenLiteral("toggleordered") {
thenExecute {
- ordered = !ordered
- if (ordered) {
+ val w = useEditableWaypoints()
+ w.isOrdered = !w.isOrdered
+ if (w.isOrdered) {
val p = MC.player?.pos ?: Vec3d.ZERO
- orderedIndex =
- waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0
+ orderedIndex = // TODO: this should be extracted to a utility method
+ w.waypoints.withIndex().minByOrNull { it.value.blockPos.getSquaredDistance(p) }?.index ?: 0
}
- source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered"))
+ source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}"))
}
}
thenLiteral("skip") {
thenExecute {
- if (ordered && waypoints.isNotEmpty()) {
- orderedIndex = (orderedIndex + 1) % waypoints.size
+ val w = useNonEmptyWaypoints()
+ if (w != null && w.isOrdered) {
+ orderedIndex = (orderedIndex + 1) % w.size
source.sendFeedback(Text.translatable("firmament.command.waypoint.skip"))
} else {
source.sendError(Text.translatable("firmament.command.waypoint.skip.error"))
@@ -189,90 +199,27 @@ object Waypoints : FirmamentFeature {
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))
+ val w = useNonEmptyWaypoints()
+ if (w != null && index in w.waypoints.indices) {
+ w.waypoints.removeAt(index)
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.remove",
+ index))
} else {
source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error"))
}
}
}
}
- thenLiteral("export") {
- thenExecute {
- val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map {
- ColeWeightWaypoint(it.x,
- it.y,
- it.z)
- })
- ClipboardUtils.setTextContent(data)
- source.sendFeedback(tr("firmament.command.waypoint.export", "Copied ${waypoints.size} waypoints to clipboard"))
- }
- }
- thenLiteral("import") {
- thenExecute {
- val contents = ClipboardUtils.getTextContents()
- val data = try {
- Firmament.tightJson.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) {
- temporaryPlayerWaypointList.forEach { (player, waypoint) ->
- block(waypoint.pos, 0xFFFFFF00.toInt())
- }
- 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,
- )
- }
- }
- }
- }
- }
+ fun textInvalidIndex(index: Int) =
+ tr("firmament.command.waypoint.invalid-index",
+ "Invalid index $index provided.")
- @Subscribe
- fun onWorldReady(event: WorldReadyEvent) {
- temporaryPlayerWaypointList.clear()
- }
+ fun textNothingToExport(): Text =
+ tr("firmament.command.waypoint.export.nowaypoints",
+ "No waypoints to export found. Add some with /firm waypoint ~ ~ ~.")
}
fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
@@ -285,35 +232,3 @@ fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
}
return result
}
-
-
-fun FabricClientCommandSource.asFakeServer(): ServerCommandSource {
- val source = this
- return ServerCommandSource(
- object : CommandOutput {
- override fun sendMessage(message: Text?) {
- source.player.sendMessage(message, false)
- }
-
- override fun shouldReceiveFeedback(): Boolean {
- return true
- }
-
- override fun shouldTrackOutput(): Boolean {
- return true
- }
-
- override fun shouldBroadcastConsoleToOps(): Boolean {
- return true
- }
- },
- source.position,
- source.rotation,
- null,
- 0,
- "FakeServerCommandSource",
- Text.literal("FakeServerCommandSource"),
- null,
- source.player
- )
-}