feat: Add firmament waypoint import / export that remembers relative waypoints

This commit is contained in:
Linnea Gräf
2025-03-22 18:10:06 +01:00
parent 487a900f15
commit 40ac970269
6 changed files with 219 additions and 10 deletions

View File

@@ -51,8 +51,7 @@ object ColeWeightCompat {
val waypoints = Waypoints.useNonEmptyWaypoints()
?.let { fromFirm(it, origin) }
if (waypoints == null) {
source.sendError(tr("firmament.command.waypoint.export.nowaypoints",
"No waypoints to export found."))
source.sendError(Waypoints.textNothingToExport())
return
}
val data =
@@ -63,11 +62,11 @@ object ColeWeightCompat {
fun importAndInform(
source: DefaultSource,
pos: BlockPos,
pos: BlockPos?,
positiveFeedback: (Int) -> Text
) {
val text = ClipboardUtils.getTextContents()
val wr = tryParse(text).map { intoFirm(it, pos) }
val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ORIGIN) }
val waypoints = wr.getOrElse {
source.sendError(
tr("firmament.command.waypoint.import.cw.error",
@@ -75,6 +74,7 @@ object ColeWeightCompat {
Firmament.logger.error(it)
return
}
waypoints.lastRelativeImport = pos
Waypoints.waypoints = waypoints
source.sendFeedback(positiveFeedback(waypoints.size))
}
@@ -93,23 +93,23 @@ object ColeWeightCompat {
thenLiteral("exportrelativecw") {
thenExecute {
copyAndInform(source, MC.player?.blockPos ?: BlockPos.ORIGIN) {
tr("firmament.command.waypoint.export.relative",
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("import") {
thenLiteral("importcw") {
thenExecute {
importAndInform(source, BlockPos.ORIGIN) { it: Int ->
Text.stringifiedTranslatable("firmament.command.waypoint.import",
importAndInform(source, null) {
Text.stringifiedTranslatable("firmament.command.waypoint.import.cw",
it)
}
}
}
thenLiteral("importrelative") {
thenLiteral("importrelativecw") {
thenExecute {
importAndInform(source, MC.player!!.blockPos) {
tr("firmament.command.waypoint.import.relative",
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.")
}
}

View File

@@ -0,0 +1,131 @@
package moe.nea.firmament.features.world
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.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) {
if (waypoints.isRelativeTo != null) {
val origin = MC.player!!.blockPos
waypoints.waypoints.replaceAll {
it.copy(
x = it.x + origin.x,
y = it.y + origin.y,
z = it.z + origin.z,
)
}
waypoints.lastRelativeImport = origin.toImmutable()
sendFeedback(tr("firmament.command.waypoint.import.ordered.success",
"Imported ${waypoints.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${waypoints.isRelativeTo}."))
} else {
sendFeedback(tr("firmament.command.waypoint.import.success",
"Imported ${waypoints.size} waypoints."))
}
Waypoints.waypoints = waypoints
}
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("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)
}
}
}
}
}

View File

@@ -1,7 +1,10 @@
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,
@@ -13,7 +16,11 @@ data class FirmWaypoints(
var isOrdered: Boolean,
// TODO: val resetOnSwap: Boolean,
) {
@Transient
var lastRelativeImport: BlockPos? = null
val size get() = waypoints.size
@Serializable
data class Waypoint(
val x: Int,
val y: Int,

View File

@@ -169,6 +169,10 @@ object Waypoints : FirmamentFeature {
}
}
}
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> {

View File

@@ -13,6 +13,7 @@ import kotlin.math.roundToInt
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import net.minecraft.text.Text
import net.minecraft.util.math.BlockPos
object FirmFormatters {
@@ -131,4 +132,7 @@ object FirmFormatters {
return if (boolean == trueIsGood) text.lime() else text.red()
}
fun formatPosition(position: BlockPos): Text {
return Text.literal("x: ${position.x}, y: ${position.y}, z: ${position.z}")
}
}

View File

@@ -0,0 +1,63 @@
package moe.nea.firmament.util.data
import kotlinx.serialization.KSerializer
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteExisting
import kotlin.io.path.exists
import kotlin.io.path.extension
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.nameWithoutExtension
import kotlin.io.path.readText
import kotlin.io.path.writeText
import moe.nea.firmament.Firmament
abstract class MultiFileDataHolder<T>(
val dataSerializer: KSerializer<T>,
val configName: String
) { // TODO: abstract this + ProfileSpecificDataHolder
val configDirectory = Firmament.CONFIG_DIR.resolve(configName)
private var allData = readValues()
protected fun readValues(): MutableMap<String, T> {
if (!configDirectory.exists()) {
configDirectory.createDirectories()
}
val profileFiles = configDirectory.listDirectoryEntries()
return profileFiles
.filter { it.extension == "json" }
.mapNotNull {
try {
it.nameWithoutExtension to Firmament.json.decodeFromString(dataSerializer, it.readText())
} catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
IDataHolder.badLoads.add(configName)
Firmament.logger.error(
"Exception during loading of multi file data holder $it ($configName). This will reset that profiles config.",
e
)
null
}
}.toMap().toMutableMap()
}
fun save() {
if (!configDirectory.exists()) {
configDirectory.createDirectories()
}
val c = allData
configDirectory.listDirectoryEntries().forEach {
if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) {
it.deleteExisting()
}
}
c.forEach { (name, value) ->
val f = configDirectory.resolve("$name.json")
f.writeText(Firmament.json.encodeToString(dataSerializer, value))
}
}
fun list(): Map<String, T> = allData
val validPathRegex = "[a-zA-Z0-9_][a-zA-Z0-9\\-_.]*".toPattern()
fun insert(name: String, value: T) {
require(validPathRegex.matcher(name).matches()) { "Not a valid name: $name" }
allData[name] = value
}
}