diff --git a/build.gradle.kts b/build.gradle.kts index 70c7902..7224a0e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ @file:Suppress("UnstableApiUsage") -import org.gradle.kotlin.dsl.modImplementation import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -92,6 +91,7 @@ compactingResources { configureTask(tasks.named("processResources").get()) compactToArray("rooms") + compactToArray("secrets") } ksp { @@ -111,6 +111,10 @@ tasks.withType().configureEach { tasks.withType().configureEach { compilerOptions.jvmTarget.set(JvmTarget.JVM_21) + compilerOptions.freeCompilerArgs.addAll( + "-Xcontext-sensitive-resolution", + "-Xcontext-parameters" + ) compilerOptions.optIn.add("kotlin.time.ExperimentalTime") } diff --git a/gradle.properties b/gradle.properties index 443b812..995c5d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ kotlin.code.style=official +org.gradle.configuration-cache=true org.gradle.jvmargs=-Xmx4G ksp.incremental=false diff --git a/src/main/kotlin/me/owdding/mortem/config/ConfigManager.kt b/src/main/kotlin/me/owdding/mortem/config/ConfigManager.kt index f349b8e..9cf4f9a 100644 --- a/src/main/kotlin/me/owdding/mortem/config/ConfigManager.kt +++ b/src/main/kotlin/me/owdding/mortem/config/ConfigManager.kt @@ -5,6 +5,7 @@ import com.teamresourceful.resourcefulconfig.api.loader.Configurator import me.owdding.ktmodules.Module import me.owdding.lib.overlays.EditOverlaysScreen import me.owdding.mortem.Mortem +import me.owdding.mortem.core.event.MortemRegisterCommandsEvent import tech.thatgravyboat.skyblockapi.api.events.base.Subscription import tech.thatgravyboat.skyblockapi.api.events.misc.RegisterCommandsEvent import tech.thatgravyboat.skyblockapi.helpers.McClient @@ -19,13 +20,12 @@ object ConfigManager { fun openConfig() = McClient.setScreenAsync { ResourcefulConfigScreen.getFactory(Mortem.MOD_ID).apply(null) } @Subscription - fun onCommand(event: RegisterCommandsEvent) { - event.register("mortem") { - thenCallback("overlays") { - McClient.setScreenAsync { EditOverlaysScreen(Mortem.MOD_ID) } - } - - callback { openConfig() } + fun onCommand(event: MortemRegisterCommandsEvent) { + event.registerBaseCallback { + openConfig() + } + event.registerWithCallback("overlays") { + McClient.setScreenAsync { EditOverlaysScreen(Mortem.MOD_ID) } } } } diff --git a/src/main/kotlin/me/owdding/mortem/core/catacombs/Catacomb.kt b/src/main/kotlin/me/owdding/mortem/core/catacombs/Catacomb.kt index 628e83c..4caac73 100644 --- a/src/main/kotlin/me/owdding/mortem/core/catacombs/Catacomb.kt +++ b/src/main/kotlin/me/owdding/mortem/core/catacombs/Catacomb.kt @@ -6,20 +6,20 @@ import me.owdding.mortem.core.Instance import me.owdding.mortem.core.InstanceType import me.owdding.mortem.core.catacombs.nodes.CatacombNodeType import me.owdding.mortem.core.catacombs.nodes.CatacombsNode +import me.owdding.mortem.core.catacombs.nodes.RoomNode +import me.owdding.mortem.core.event.catacomb.CatacombNodeChangeEvent +import me.owdding.mortem.core.event.catacomb.CatacombRoomChangeEvent import me.owdding.mortem.utils.Utils +import me.owdding.mortem.utils.Utils.post import me.owdding.mortem.utils.Utils.unsafeCast import net.minecraft.core.Direction import org.joml.Vector2i import org.joml.minus import org.joml.plus import tech.thatgravyboat.skyblockapi.api.area.dungeon.DungeonFloor +import tech.thatgravyboat.skyblockapi.helpers.McPlayer import tech.thatgravyboat.skyblockapi.utils.extentions.filterValuesNotNull import java.util.concurrent.ConcurrentHashMap -import me.owdding.mortem.core.catacombs.nodes.RoomNode -import me.owdding.mortem.core.event.catacomb.CatacombNodeChangeEvent -import me.owdding.mortem.core.event.catacomb.CatacombRoomChangeEvent -import me.owdding.mortem.utils.Utils.post -import tech.thatgravyboat.skyblockapi.helpers.McPlayer data class Catacomb( val floor: DungeonFloor, @@ -40,6 +40,8 @@ data class Catacomb( val grid: MutableMap> = ConcurrentHashMap() + operator fun get(position: Vector2i): CatacombsNode<*>? = grid[position] + fun > getOrCreateNode(position: Vector2i, type: CatacombNodeType) : T = grid.getOrPut(position, type.constructor).unsafeCast() inline fun > getNeighbours(position: Vector2i): Map = buildList { @@ -72,18 +74,20 @@ fun interface CatacombsColorProvider { fun getColor(): Int } -enum class CatacombRoomType(val provider: CatacombsColorProvider) : CatacombsColorProvider by provider { - NORMAL({ 0xAb6b00 }), - TRAP({ 0xFF7F0F }), - FAIRY({ 0xF080FF }), - PUZZLE({ 0xe050F0 }), - MINIBOSS({ 0xFFFF00 }), - BLOOD({ 0xFF0000 }), - START({ 0x00FF00 }), - UNKNOWN({ 0xababab }), - DEFAULT({ 0x000000 }), +enum class CatacombRoomType(provider: CatacombsColorProvider) : CatacombsColorProvider by provider { + NORMAL(0xAb6b00), + TRAP(0xFF7F0F), + FAIRY(0xF080FF), + PUZZLE(0xe050F0), + MINIBOSS(0xFFFF00), + BLOOD(0xFF0000), + START(0x00FF00), + UNKNOWN(0xababab), + DEFAULT(0x000000), ; + constructor(color: Int) : this({ color }) + companion object { fun getByColor(color: CatacombMapColor): CatacombRoomType? = when (color) { CatacombMapColor.COMPLETE -> START @@ -101,14 +105,14 @@ enum class CatacombRoomType(val provider: CatacombsColorProvider) : CatacombsCol } enum class CatacombDoorType(val provider: CatacombsColorProvider) : CatacombsColorProvider by provider { - WITHER({ 0x4f4f4f }), + WITHER({ 0x000000 }), BLOOD({ 0xFF0000 }), NORMAL({ 0xab6b00 }), TRAP({ 0xff7f0f }), MINIBOSS({ 0xFFFF00 }), PUZZLE({ 0xe060f0 }), FAIRY({ 0xf080ff }), - DEFAULT({ 0x000000 }), + DEFAULT({ 0x4f4f4f }), ; companion object { @@ -130,7 +134,7 @@ enum class CatacombDoorType(val provider: CatacombsColorProvider) : CatacombsCol data class StoredCatacombRoom( var name: String, var id: String, - var secrets: Int, + var secrets: Int = 0, @FieldName("center") val centerHash: String, @FieldName("directions") val directionalHashes: Map, @FieldName("extra_rotation_handling") val extraRotationHandling: Boolean = false, diff --git a/src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombConstants.kt b/src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombConstants.kt index 4622d29..e0b649a 100644 --- a/src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombConstants.kt +++ b/src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombConstants.kt @@ -1,3 +1,5 @@ package me.owdding.mortem.core.catacombs const val DOOR_WIDTH: Int = 4 +const val WITHER_ESSENCE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzRkYjRhZGZhOWJmNDhmZjVkNDE3MDdhZTM0ZWE3OGJkMjM3MTY1OWZjZDhjZDg5MzQ3NDlhZjRjY2U5YiJ9fX0=" +const val REDSTONE_KEY = "TODO" diff --git a/src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombsManager.kt b/src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombsManager.kt index 45ccd24..40f910b 100644 --- a/src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombsManager.kt +++ b/src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombsManager.kt @@ -8,8 +8,12 @@ import me.owdding.mortem.core.catacombs.roommatching.CatacombMapMatcher import me.owdding.mortem.core.catacombs.roommatching.CatacombWorldMatcher import me.owdding.mortem.core.event.CatacombJoinEvent import me.owdding.mortem.core.event.CatacombLeaveEvent +import me.owdding.mortem.core.event.MortemRegisterCommandsEvent +import me.owdding.mortem.core.event.catacomb.CatacombNodeChangeEvent +import me.owdding.mortem.core.event.catacomb.CatacombRoomChangeEvent import me.owdding.mortem.generated.CodecUtils import me.owdding.mortem.generated.MortemCodecs +import me.owdding.mortem.utils.CommandExceptions.getCatacomb import me.owdding.mortem.utils.Utils import me.owdding.mortem.utils.Utils.post import me.owdding.mortem.utils.colors.CatppuccinColors @@ -28,7 +32,6 @@ import tech.thatgravyboat.skyblockapi.api.events.dungeon.DungeonEnterEvent import tech.thatgravyboat.skyblockapi.api.events.hypixel.ServerChangeEvent import tech.thatgravyboat.skyblockapi.api.events.level.PacketReceivedEvent import tech.thatgravyboat.skyblockapi.api.events.location.ServerDisconnectEvent -import tech.thatgravyboat.skyblockapi.api.events.misc.RegisterCommandsEvent import tech.thatgravyboat.skyblockapi.api.events.time.TickEvent import tech.thatgravyboat.skyblockapi.api.location.SkyBlockIsland import tech.thatgravyboat.skyblockapi.helpers.McClient @@ -43,9 +46,6 @@ import java.nio.file.Path import kotlin.io.path.createDirectories import kotlin.io.path.writeText import kotlin.math.abs -import kotlin.math.floor -import me.owdding.mortem.core.event.catacomb.CatacombNodeChangeEvent -import me.owdding.mortem.core.event.catacomb.CatacombRoomChangeEvent @Module object CatacombsManager { @@ -239,8 +239,12 @@ object CatacombsManager { @Subscription - fun command(event: RegisterCommandsEvent) { - event.registerWithCallback("mortem dev column_hash") { + fun command(event: MortemRegisterCommandsEvent) { + event.registerWithCallback("dev copy grid") { + McClient.clipboard = getCatacomb().grid.toString() + Text.of("Copied catacomb grid to clipboard!").sendWithPrefix() + } + event.registerWithCallback("dev column_hash") { val chunkPos = McPlayer.self!!.chunkPosition() val chunk = McLevel.self.getChunk(chunkPos.x, chunkPos.z) val hash = CatacombWorldMatcher.hashColumn(chunk, McPlayer.self!!.blockPosition().atY(255)) @@ -258,7 +262,7 @@ object CatacombsManager { append(" ") append(coordinate.z().shorten(2)) { color = CatppuccinColors.Mocha.blue } } - event.registerWithCallback("mortem dev room_pos") { + event.registerWithCallback("dev room_pos") { val catacomb = catacomb ?: return@registerWithCallback val playerNode = catacomb.grid[worldPosToGridPos(McPlayer.self!!.blockPosition())] if (playerNode !is RoomNode) { @@ -272,7 +276,7 @@ object CatacombsManager { append(format(roomPos)) }.sendWithPrefix() } - event.registerWithCallback("mortem dev room_pos_test") { + event.registerWithCallback("dev room_pos_test") { val catacomb = catacomb ?: return@registerWithCallback val playerNode = catacomb.grid[worldPosToGridPos(McPlayer.self!!.blockPosition())] val pos = McPlayer.position!!.toVector3dc() @@ -323,8 +327,8 @@ object CatacombsManager { } fun worldPosToGridPos(pos: BlockPos): Vector2i { - val chunkX = floor(pos.x / 16f).toInt() - val chunkY = floor(pos.z / 16f).toInt() + val chunkX = pos.x shr 4 + val chunkY = pos.z shr 4 val chunkRelativeX = pos.x and 15 val chunkRelativeY = pos.z and 15 diff --git a/src/main/kotlin/me/owdding/mortem/core/catacombs/nodes/CatacombsNode.kt b/src/main/kotlin/me/owdding/mortem/core/catacombs/nodes/CatacombsNode.kt index 8a4c4db..4c1f885 100644 --- a/src/main/kotlin/me/owdding/mortem/core/catacombs/nodes/CatacombsNode.kt +++ b/src/main/kotlin/me/owdding/mortem/core/catacombs/nodes/CatacombsNode.kt @@ -6,17 +6,9 @@ import me.owdding.mortem.core.catacombs.CatacombsColorProvider import me.owdding.mortem.core.catacombs.StoredCatacombRoom import me.owdding.mortem.utils.Utils import me.owdding.mortem.utils.extensions.mutableCopy -import me.owdding.mortem.utils.extensions.sendWithPrefix import me.owdding.mortem.utils.extensions.toVec2d import net.minecraft.world.level.block.Rotation -import org.joml.Vector2i -import org.joml.Vector3d -import org.joml.Vector3dc -import org.joml.Vector3i -import org.joml.Vector3ic -import org.joml.component1 -import org.joml.component2 -import tech.thatgravyboat.skyblockapi.utils.text.Text +import org.joml.* import kotlin.math.max import kotlin.math.min @@ -45,7 +37,7 @@ object VoidNode : CatacombsNode(CatacombNodeType.Void, 0) { class DoorNode( var doorType: CatacombDoorType = CatacombDoorType.DEFAULT, ) : CatacombsNode(CatacombNodeType.Door, 10) { - override fun toString() = "Door" + override fun toString() = "Door[type=$doorType]" fun mutateType(type: CatacombDoorType) { doorType = when (doorType) { CatacombDoorType.DEFAULT -> type @@ -63,6 +55,7 @@ class RoomNode( val positions: MutableSet = mutableSetOf() var backingData: StoredCatacombRoom? = null var rotation: Rotation? = null + var secrets: Int = 0 // TODO: implement override fun toString() = "Room[type=$roomType]" fun addPosition(position: Vector2i) { @@ -114,7 +107,7 @@ class RoomNode( else -> null } } - } + }?.mutableCopy() } fun minMiddleChunkPos(): Vector2i = Vector2i( @@ -134,7 +127,6 @@ class RoomNode( fun worldToRoom(vec3d: Vector3dc): Vector3dc { val origin = getCenter().toVec2d().add(0.5, 0.5) - Text.of(origin.toString()).sendWithPrefix() val original = vec3d.mutableCopy().sub(origin.x, 0.0, origin.y) return when (rotation) { Rotation.CLOCKWISE_90 -> Vector3d(original.z(), original.y(), -original.x()) @@ -163,7 +155,6 @@ class RoomNode( else -> vec3i.mutableCopy() } val origin = getCenter() - Text.of(origin.toString()).sendWithPrefix() return room.add(origin.x, 0, origin.y) } diff --git a/src/main/kotlin/me/owdding/mortem/core/catacombs/roommatching/CatacombMapMatcher.kt b/src/main/kotlin/me/owdding/mortem/core/catacombs/roommatching/CatacombMapMatcher.kt index 016273d..7baea6c 100644 --- a/src/main/kotlin/me/owdding/mortem/core/catacombs/roommatching/CatacombMapMatcher.kt +++ b/src/main/kotlin/me/owdding/mortem/core/catacombs/roommatching/CatacombMapMatcher.kt @@ -129,7 +129,7 @@ object CatacombMapMatcher : MortemOverlay { CatacombWorldMatcher.matchData(rooms) } - fun Catacomb.mergeNodes(position: Vector2i, oneOffset: Vector2i, twoOffset: Vector2i) { + fun Catacomb.mergeNodes(position: Vector2i, oneOffset: Vector2ic, twoOffset: Vector2ic) { val room = getOrCreateNode(position - twoOffset, CatacombNodeType.Room) grid[position] = room grid[position - oneOffset] = room @@ -137,7 +137,7 @@ object CatacombMapMatcher : MortemOverlay { } override val name: Component = Text.of("Debug") - override val position: Position = ConfigPosition(0, 0) + override val position: Position = ConfigPosition(-300, 0) override val bounds: Pair = 20 to 20 override fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int) { diff --git a/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretGroup.kt b/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretGroup.kt new file mode 100644 index 0000000..001bfff --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretGroup.kt @@ -0,0 +1,60 @@ +package me.owdding.mortem.core.catacombs.secrets + +import com.mojang.serialization.Codec +import me.owdding.ktcodecs.Compact +import me.owdding.ktcodecs.FieldName +import me.owdding.ktcodecs.GenerateCodec +import me.owdding.ktcodecs.IncludedCodec +import me.owdding.mortem.generated.CodecUtils +import me.owdding.mortem.generated.MortemCodecs + +@GenerateCodec +data class CatacombsSecretRoom( + val id: String, + @Compact @FieldName("secret_groups") val secretGroups: MutableList = mutableListOf(), +) { + fun markDirty() { + needsUpdate = true + } + var needsUpdate = true + fun addSecret(secret: CatacombsSecret) { + removeSecret(secret) + secretGroups.add(CatacombsSecretGroup(secret)) + markDirty() + } + fun removeSecret(secret: CatacombsSecret) { + secretGroups.forEach { it.secrets.remove(secret) } + removeEmpty() + markDirty() + } + fun replaceSecret(old: CatacombsSecret, new: CatacombsSecret) { + secretGroups.forEach { it.replaceSecret(old, new) } + markDirty() + } + + private fun removeEmpty() = secretGroups.removeIf { it.secrets.isEmpty() } + fun reset() = secretGroups.forEach(CatacombsSecretGroup::reset) +} + +data class CatacombsSecretGroup( + val secrets: MutableList = mutableListOf(), +) { + constructor(secret: CatacombsSecret) : this(mutableListOf(secret)) + fun replaceSecret(old: CatacombsSecret, new: CatacombsSecret) { + val index = secrets.indexOf(old) + if (index != -1) secrets[index] = new + } + + val clicked: Boolean get() { + val actualSecrets = secrets.filter { it.type.isSecret } + if (actualSecrets.isEmpty()) return false + return actualSecrets.all { it.clicked } + } + fun reset() = secrets.forEach(CatacombsSecret::reset) + + companion object { + @IncludedCodec + val CODEC: Codec = + CodecUtils.compactMutableList(MortemCodecs.getCodec()).xmap(::CatacombsSecretGroup, CatacombsSecretGroup::secrets) + } +} diff --git a/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretManager.kt b/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretManager.kt new file mode 100644 index 0000000..0b3b385 --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretManager.kt @@ -0,0 +1,59 @@ +package me.owdding.mortem.core.catacombs.secrets + +import me.owdding.ktmodules.Module +import me.owdding.mortem.Mortem +import me.owdding.mortem.core.catacombs.CatacombsManager +import me.owdding.mortem.core.event.CatacombJoinEvent +import me.owdding.mortem.generated.CodecUtils +import me.owdding.mortem.generated.MortemCodecs +import me.owdding.mortem.utils.Utils +import tech.thatgravyboat.skyblockapi.api.events.base.Subscription +import tech.thatgravyboat.skyblockapi.api.events.base.predicates.TimePassed +import tech.thatgravyboat.skyblockapi.api.events.time.TickEvent +import tech.thatgravyboat.skyblockapi.helpers.McClient +import tech.thatgravyboat.skyblockapi.utils.json.Json.toJsonOrThrow +import tech.thatgravyboat.skyblockapi.utils.json.Json.toPrettyString +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +@Module +object CatacombsSecretManager { + + val backingRooms: MutableMap = mutableMapOf() + + fun getOrPut(roomId: String) = backingRooms.getOrPut(roomId) { CatacombsSecretRoom(roomId) } + + init { + val rooms: List = Utils.loadRepoData("secrets", CodecUtils::list) + backingRooms.putAll(rooms.associateBy { it.id }) + + val roomsWithSecrets = CatacombsManager.backingRooms.values.associateBy { it.id }.mapValues { it.value.secrets }.filterValues { it > 0 } + val missingRooms = roomsWithSecrets.filter { (roomId, secrets) -> + val mappedGroups = backingRooms[roomId]?.secretGroups ?: return@filter true + val mappedSecrets = mappedGroups.flatMap { it.secrets }.count { it.type.isSecret } + secrets != mappedSecrets + } + if (missingRooms.isNotEmpty()) { + Mortem.error("Missing secrets for rooms: ${missingRooms.keys.joinToString()}") + } + } + + private val defaultPath: Path = McClient.config.resolve("mortem/data") + + @Subscription(TickEvent::class) + @TimePassed("5s") + fun onTick() { + val rooms = defaultPath.resolve("secrets").createDirectories() + this.backingRooms.values.forEach { + if (!it.needsUpdate) return@forEach + it.needsUpdate = false + rooms.resolve("${it.id}.json").writeText(it.toJsonOrThrow(MortemCodecs.getCodec()).toPrettyString()) + } + } + + + @Subscription(CatacombJoinEvent::class) + fun resetClicked() = backingRooms.values.forEach(CatacombsSecretRoom::reset) + +} diff --git a/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretType.kt b/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretType.kt new file mode 100644 index 0000000..ed0cf1c --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/core/catacombs/secrets/CatacombsSecretType.kt @@ -0,0 +1,113 @@ +package me.owdding.mortem.core.catacombs.secrets + +import me.owdding.ktcodecs.FieldName +import me.owdding.ktcodecs.GenerateCodec +import me.owdding.ktcodecs.GenerateDispatchCodec +import me.owdding.mortem.core.catacombs.CatacombsColorProvider +import me.owdding.mortem.core.catacombs.nodes.RoomNode +import me.owdding.mortem.generated.DispatchHelper +import me.owdding.mortem.utils.colors.CatppuccinColors +import me.owdding.mortem.utils.extensions.toVec3 +import net.minecraft.world.phys.AABB +import org.joml.Vector3ic +import tech.thatgravyboat.skyblockapi.utils.extentions.valueOfOrNull +import kotlin.reflect.KClass + +// TODO: make colors not be hardcoded +@GenerateDispatchCodec(CatacombsSecret::class) +enum class CatacombsSecretType( + override val type: KClass, + val constructor: (Vector3ic) -> CatacombsSecret, + colorProvider: CatacombsColorProvider, + val isSecret: Boolean = true, +) : DispatchHelper, CatacombsColorProvider by colorProvider { + CHEST(ChestSecret::class, ::ChestSecret, CatppuccinColors.Latte::base), + ITEM(ItemSecret::class, ::ItemSecret, CatppuccinColors.Mocha::sapphire), + BAT(BatSecret::class, ::BatSecret, CatppuccinColors.Frappe::peach), + ESSENCE(EssenceSecret::class, ::EssenceSecret, CatppuccinColors.Latte::mauve), + + LEVER(LeverSecret::class, { LeverSecret(it, fullBlockAABB) }, { 0xf2da6d }, isSecret = false), + REDSTONE_KEY(RedstoneKeySecret::class, ::RedstoneKeySecret, CatppuccinColors.Macchiato::red, isSecret = false), + + NONE(NoSecret::class, ::NoSecret, CatppuccinColors.Mocha::rosewater, isSecret = false), + ; + + val ignore: Boolean get() = this == NONE + + companion object { + fun getType(string: String): CatacombsSecretType = valueOfOrNull(string.uppercase()) ?: NONE + } +} + +sealed class CatacombsSecret(val type: CatacombsSecretType) { + abstract var pos: Vector3ic + abstract val aabb: AABB + + var clicked: Boolean = false + private set + + fun click() { + clicked = true + } + + fun reset() { + clicked = false + } + + fun realPos(room: RoomNode): Vector3ic = room.roomToWorld(pos) + fun realAABB(room: RoomNode): AABB = aabb.move(realPos(room).toVec3()) +} + +sealed class PlayerHeadSecret(type: CatacombsSecretType) : CatacombsSecret(type) { + override val aabb: AABB get() = skullAABB +} + +@GenerateCodec +data class ChestSecret( + override var pos: Vector3ic, +) : CatacombsSecret(CHEST) { + override val aabb: AABB get() = chestAABB +} + +@GenerateCodec +data class ItemSecret( + override var pos: Vector3ic, +) : CatacombsSecret(ITEM) { + override val aabb: AABB get() = skullAABB +} + +@GenerateCodec +data class BatSecret( + override var pos: Vector3ic, +) : CatacombsSecret(BAT) { + override val aabb: AABB get() = fullBlockAABB +} + +@GenerateCodec +data class EssenceSecret( + override var pos: Vector3ic, +) : PlayerHeadSecret(ESSENCE) + +@GenerateCodec +data class RedstoneKeySecret( + override var pos: Vector3ic, + /** [pickUp] is `true` when the waypoint indicates the position the redstone key needs to be picked up at */ + @FieldName("pick_up") var pickUp: Boolean = true, +) : PlayerHeadSecret(REDSTONE_KEY) + +@GenerateCodec +data class LeverSecret( + override var pos: Vector3ic, + override var aabb: AABB, +) : CatacombsSecret(LEVER) + +@GenerateCodec +data class NoSecret( + override var pos: Vector3ic, +) : CatacombsSecret(NONE) { + override val aabb: AABB get() = fullBlockAABB +} + +private val fullBlockAABB = AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0) +private val chestAABB = AABB(0.0625, 0.0, 0.0625, 0.9375, 0.875, 0.9375) +private val skullAABB = AABB(0.25, 0.0, 0.25, 0.75, 0.5, 0.75) diff --git a/src/main/kotlin/me/owdding/mortem/core/event/MortemRegisterCommandsEvent.kt b/src/main/kotlin/me/owdding/mortem/core/event/MortemRegisterCommandsEvent.kt new file mode 100644 index 0000000..359de0c --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/core/event/MortemRegisterCommandsEvent.kt @@ -0,0 +1,17 @@ +package me.owdding.mortem.core.event + +import me.owdding.ktmodules.Module +import me.owdding.mortem.utils.Utils.post +import tech.thatgravyboat.skyblockapi.api.events.base.Subscription +import tech.thatgravyboat.skyblockapi.api.events.misc.AbstractModRegisterCommandsEvent +import tech.thatgravyboat.skyblockapi.api.events.misc.RegisterCommandsEvent + +class MortemRegisterCommandsEvent(baseEvent: RegisterCommandsEvent) : AbstractModRegisterCommandsEvent(baseEvent, "mortem") { + @Module + companion object { + @Subscription + fun onRegisterCommands(event: RegisterCommandsEvent) { + MortemRegisterCommandsEvent(event).post() + } + } +} diff --git a/src/main/kotlin/me/owdding/mortem/features/ItemRefill.kt b/src/main/kotlin/me/owdding/mortem/features/ItemRefill.kt index 529d4b0..d021c69 100644 --- a/src/main/kotlin/me/owdding/mortem/features/ItemRefill.kt +++ b/src/main/kotlin/me/owdding/mortem/features/ItemRefill.kt @@ -2,6 +2,7 @@ package me.owdding.mortem.features import me.owdding.ktmodules.Module import me.owdding.mortem.config.category.MiscConfig +import me.owdding.mortem.core.event.MortemRegisterCommandsEvent import me.owdding.mortem.utils.GfsQueue import me.owdding.mortem.utils.extensions.sendWithPrefix import tech.thatgravyboat.skyblockapi.api.events.base.Subscription @@ -18,8 +19,8 @@ import tech.thatgravyboat.skyblockapi.utils.text.Text object ItemRefill { @Subscription - fun onCommand(event: RegisterCommandsEvent) { - event.register("mortem refill") { + fun onCommand(event: MortemRegisterCommandsEvent) { + event.register("refill") { thenCallback("item", EnumArgument()) { refill(argument("item")) } diff --git a/src/main/kotlin/me/owdding/mortem/features/map/CatacombsMap.kt b/src/main/kotlin/me/owdding/mortem/features/map/CatacombsMap.kt new file mode 100644 index 0000000..b008efb --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/features/map/CatacombsMap.kt @@ -0,0 +1,252 @@ +package me.owdding.mortem.features.map + +import me.owdding.ktmodules.Module +import me.owdding.lib.overlays.ConfigPosition +import me.owdding.lib.overlays.Position +import me.owdding.mortem.core.catacombs.CatacombsManager +import me.owdding.mortem.core.catacombs.nodes.CatacombNodeType +import me.owdding.mortem.core.catacombs.nodes.CatacombsNode +import me.owdding.mortem.core.catacombs.nodes.RoomNode +import me.owdding.mortem.utils.MortemOverlay +import me.owdding.mortem.utils.Overlay +import me.owdding.mortem.utils.Utils +import me.owdding.mortem.utils.extensions.mutableCopy +import me.owdding.mortem.utils.extensions.toVector3d +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.PlayerFaceRenderer +import net.minecraft.client.player.AbstractClientPlayer +import net.minecraft.network.chat.Component +import net.minecraft.util.ARGB +import net.minecraft.world.entity.player.Player +import org.joml.Vector2i +import org.joml.Vector3d +import org.joml.component1 +import org.joml.component2 +import tech.thatgravyboat.skyblockapi.helpers.McFont +import tech.thatgravyboat.skyblockapi.helpers.McPlayer +import tech.thatgravyboat.skyblockapi.platform.rotate +import tech.thatgravyboat.skyblockapi.platform.skin +import tech.thatgravyboat.skyblockapi.utils.extentions.isRealPlayer +import tech.thatgravyboat.skyblockapi.utils.extentions.scaled +import tech.thatgravyboat.skyblockapi.utils.extentions.translated +import tech.thatgravyboat.skyblockapi.utils.text.Text +import tech.thatgravyboat.skyblockapi.utils.text.TextColor +import tech.thatgravyboat.skyblockapi.utils.text.TextProperties.stripped + + +private const val ROOM_WIDTH = 35 +private const val SPACING = 5 + +private const val DOOR_SPACING = SPACING +private const val DOOR_WIDTH = ROOM_WIDTH - (DOOR_SPACING * 2) + + +private const val AVERAGE = (ROOM_WIDTH + SPACING) / 2 +private const val DIFFERENCE = ROOM_WIDTH - AVERAGE +private const val SCALE_FACTOR = AVERAGE / 16.0 + +private const val BACKGROUND_COLOR = 0x80FF5555.toInt() +private const val SHOW_NAMES = false +private const val HEAD_SIZE = 6 + +@Module +@Overlay +object CatacombsMap : MortemOverlay { + + override val name: Component = Text.of("Dungeon Map") + override val position: Position = ConfigPosition(0, 0) + override val bounds: Pair = 20 to 20 + + //region DrawData + data class DrawData( + val minX: Int, + val minY: Int, + val maxX: Int, + val maxY: Int, + ) { + fun center(): Vector2i = Vector2i(((maxX - minX) / 2) + minX, ((maxY - minY) / 2) + minY) + } + //endregion + + private fun RoomNode.getCenterStringDrawPos(): Vector2i { + val minX = positions.minOf { it.x } + val minY = positions.minOf { it.y } + val min = Vector2i(minX, minY).mul(2) + val offset = when (shape) { + STAIR -> { + val temp = getMiddleChunkOffset() ?: Utils.vectorZeroZero.mutableCopy() + if (temp.x == 0) temp.add(1, 0) + else temp.sub(1, 0) + } + + else -> getMiddleChunkOffset() ?: Utils.vectorZeroZero + } + + val drawPos = min.add(offset).drawPos(this) + val center = drawPos.center() + return center + } + + private fun RoomNode.maxWidth(): Int { + val positions = positions.groupBy { it.y }.values.maxOf { it.size } + return (positions * ROOM_WIDTH) + + ((positions - 1) / SPACING) + } + + private fun Vector2i.drawPos(node: CatacombsNode<*>): DrawData { + val isDoor = node.type == CatacombNodeType.Door + + val (x, y) = this + + val isHorizontal = (x % 2) == 1 + val isVertical = (y % 2) == 1 + + fun roomsBefore(int: Int): Int = (int + 1) / 2 + fun doorsBefore(int: Int) = int - roomsBefore(int) + + val minX = SPACING + + (roomsBefore(x) * ROOM_WIDTH) + + (doorsBefore(x) * SPACING) + + (if (isDoor && isVertical) DOOR_SPACING else 0) + + val minY = SPACING + + (roomsBefore(y) * ROOM_WIDTH) + + (doorsBefore(y) * SPACING) + + (if (isDoor && isHorizontal) DOOR_SPACING else 0) + + + val maxX = minX + when { + isHorizontal -> SPACING + isDoor -> DOOR_WIDTH + else -> ROOM_WIDTH + } + val maxY = minY + when { + isVertical -> SPACING + isDoor -> DOOR_WIDTH + else -> ROOM_WIDTH + } + + return DrawData(minX, minY, maxX, maxY) + } + + override fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int) { + val catacomb = CatacombsManager.catacomb ?: return + + val mapSize = catacomb.size + + val (xSize, ySize) = mapSize.xRooms to mapSize.yRooms + + graphics.fill( + 0, + 0, + AVERAGE * xSize * 2 + SPACING, + AVERAGE * ySize * 2 + SPACING, + BACKGROUND_COLOR, + ) + + + for ((pos, node) in catacomb.grid) { + val drawData = pos.drawPos(node) + + val (minX, minY, maxX, maxY) = drawData + graphics.fill( + minX, + minY, + maxX, + maxY, + ARGB.color(180, node.getColor()).let { + if (node is RoomNode && node.backingData == null) ARGB.scaleRGB(it, 0.8f) else it + }, + ) + } + + val roomNodes = catacomb.grid.values.filterIsInstance().distinct() + + for (room in roomNodes) { + val (x, y) = room.getCenterStringDrawPos() + val backingData = room.backingData + if (backingData == null) { + graphics.drawCenteredString( + McFont.self, + "Unknown", + x, + y - (McFont.height / 2), + ARGB.opaque(TextColor.RED), + ) + continue + } + + val secretString = if (backingData.secrets > 0) "" + else " (${room.secrets}/${backingData.secrets})" + + val split = (backingData.name + secretString).split(" ") + + var yOffset = -(split.size * McFont.height / 2) + val maxWidth = room.maxWidth() + + split.forEach { string -> + val width = McFont.width(string) + fun drawString() = graphics.drawString( + McFont.self, + string, + x - (width / 2), + y + yOffset, + -1, // TODO: proper color depending on completion + true + ) + if (width > maxWidth) { + graphics.scaled(maxWidth / width.toFloat()) { + drawString() + } + } else drawString() + + yOffset += McFont.height + } + } + + fun renderRealPlayer(player: Player) { + if (player !is AbstractClientPlayer) return + if (!player.isRealPlayer()) return + val playerPos = player.position().toVector3d().toMapPos() + + graphics.translated(playerPos.x, playerPos.y) { + if (SHOW_NAMES) { + graphics.drawCenteredString( + McFont.self, + player.name.stripped, + 0, + McFont.height, + -1, + ) + } + graphics.rotate(180f + player.rotationVector.y) + graphics.fill( + -(HEAD_SIZE * 1.2).toInt(), + -(HEAD_SIZE * 1.2).toInt(), + (HEAD_SIZE * 1.2).toInt(), + (HEAD_SIZE * 1.2).toInt(), + ARGB.opaque(TextColor.GREEN), + ) + PlayerFaceRenderer.draw( + graphics, + player.skin(), + -HEAD_SIZE, + -HEAD_SIZE, + 2 * HEAD_SIZE, + ) + } + + } + McPlayer.self?.let(::renderRealPlayer) + + super.render(graphics, mouseX, mouseY) + } + + private fun Vector3d.toMapPos(): Vector2i { + return this.add(208.0, 0.0, 208.0) + .mul(SCALE_FACTOR, 1.0, SCALE_FACTOR) + .sub(DIFFERENCE / 2.0, 0.0, DIFFERENCE / 2.0) + .let { Vector2i(it.x.toInt(), it.z.toInt()) } + } + +} diff --git a/src/main/kotlin/me/owdding/mortem/features/secrets/DungeonSecretsEditor.kt b/src/main/kotlin/me/owdding/mortem/features/secrets/DungeonSecretsEditor.kt new file mode 100644 index 0000000..c6934e1 --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/features/secrets/DungeonSecretsEditor.kt @@ -0,0 +1,458 @@ +package me.owdding.mortem.features.secrets + +import com.mojang.brigadier.arguments.BoolArgumentType +import com.mojang.brigadier.arguments.IntegerArgumentType +import me.owdding.ktmodules.Module +import me.owdding.lib.utils.RenderUtils.renderBox +import me.owdding.lib.utils.RenderUtils.renderTextInWorld +import me.owdding.mortem.core.catacombs.CatacombsManager +import me.owdding.mortem.core.catacombs.REDSTONE_KEY +import me.owdding.mortem.core.catacombs.WITHER_ESSENCE +import me.owdding.mortem.core.catacombs.nodes.RoomNode +import me.owdding.mortem.core.catacombs.secrets.* +import me.owdding.mortem.core.event.MortemRegisterCommandsEvent +import me.owdding.mortem.core.event.catacomb.CatacombRoomChangeEvent +import me.owdding.mortem.utils.CommandExceptions +import me.owdding.mortem.utils.CommandExceptions.assertInCatacombs +import me.owdding.mortem.utils.CommandExceptions.assertInCatacombsRoom +import me.owdding.mortem.utils.CommandExceptions.getCatacomb +import me.owdding.mortem.utils.CommandExceptions.getCatacombsRoom +import me.owdding.mortem.utils.RenderUtils.renderOutlineBox +import me.owdding.mortem.utils.Utils.getTexture +import me.owdding.mortem.utils.Utils.minByWithOrNull +import me.owdding.mortem.utils.Utils.unsafeCast +import me.owdding.mortem.utils.colors.CatppuccinColors +import me.owdding.mortem.utils.extensions.mutableCopy +import me.owdding.mortem.utils.extensions.sendWithPrefix +import me.owdding.mortem.utils.extensions.toBlockPos +import me.owdding.mortem.utils.extensions.toVector3ic +import net.minecraft.core.BlockPos +import net.minecraft.util.ARGB +import net.minecraft.world.entity.item.ItemEntity +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.level.block.entity.BlockEntityType +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.world.phys.Vec3 +import org.joml.Vector3ic +import tech.thatgravyboat.skyblockapi.api.events.base.Subscription +import tech.thatgravyboat.skyblockapi.api.events.base.predicates.OnlyIn +import tech.thatgravyboat.skyblockapi.api.events.hypixel.ServerChangeEvent +import tech.thatgravyboat.skyblockapi.api.events.location.ServerDisconnectEvent +import tech.thatgravyboat.skyblockapi.api.events.misc.RegisterCommandsEvent.Companion.argument +import tech.thatgravyboat.skyblockapi.api.events.render.RenderWorldEvent +import tech.thatgravyboat.skyblockapi.helpers.McClient +import tech.thatgravyboat.skyblockapi.helpers.McLevel +import tech.thatgravyboat.skyblockapi.utils.command.EnumArgument +import tech.thatgravyboat.skyblockapi.utils.text.Text +import tech.thatgravyboat.skyblockapi.utils.text.TextBuilder.append +import tech.thatgravyboat.skyblockapi.utils.text.TextColor +import kotlin.jvm.optionals.getOrNull + +@Module +object DungeonSecretsEditor { + + var editing: Boolean = false + private set + + private val RoomNode.secretData: CatacombsSecretRoom? + get() = backingData?.id?.let(CatacombsSecretManager::getOrPut) + + var editingSecret: CatacombsSecret? = null + private set + + private var groupingSecrets = false + private val selectedSecrets = mutableSetOf() + + fun lookedAtPos(): Vector3ic? { + val hit = McClient.self.cameraEntity?.pick(70.0, 0f, false) ?: return null + if (hit.type != HitResult.Type.BLOCK) return null + return hit.unsafeCast().blockPos.toVector3ic() + } + + private fun CatacombsSecret.tryAutoModify(onlyAABB: Boolean = false) { + when (this) { + is RedstoneKeySecret -> { + if (onlyAABB) return + val room = CatacombsManager.catacomb?.lastRoom ?: return + val state = McLevel[room.roomToWorld(this.pos).toBlockPos()] + this.pickUp = state.block == Blocks.PLAYER_HEAD + } + + is LeverSecret -> { + val room = CatacombsManager.catacomb?.lastRoom ?: return + val realPos = room.roomToWorld(this.pos).toBlockPos() + val state = McLevel[realPos] + val shape = state.getShape(McLevel.self, realPos) + if (!shape.isEmpty) this.aabb = shape.bounds() + } + + else -> return + } + } + + private fun assertEditing() { + if (!editing) throw CommandExceptions.create("Not editing secrets!") + } + private fun assertGrouping() { + if (!groupingSecrets) throw CommandExceptions.create("Not grouping secrets!") + } + private fun currentSecret(): CatacombsSecret { + return editingSecret ?: throw CommandExceptions.create("Theres no secret selected!") + } + + private fun guessType(pos: BlockPos, state: BlockState): Pair { + return when (state.block) { + Blocks.CHEST, Blocks.TRAPPED_CHEST -> pos to CHEST + Blocks.PLAYER_HEAD -> { + val texture = McLevel.level.getBlockEntity(pos, BlockEntityType.SKULL).getOrNull()?.getTexture() + pos to when (texture) { + WITHER_ESSENCE -> ESSENCE + REDSTONE_KEY -> CatacombsSecretType.REDSTONE_KEY + else -> ESSENCE + } + } + + Blocks.LEVER -> pos to LEVER + else -> { + val vec3 = Vec3(pos) + val aabb = AABB(pos).inflate(4.0) + val closestItem = McLevel.level.getEntitiesOfClass(ItemEntity::class.java, aabb).minByWithOrNull { + it.distanceToSqr(vec3) + }?.takeIf { it.second < 25 }?.first + if (closestItem != null) { + return closestItem.blockPosition() to ITEM + } + + // TODO: add item/bat secret if recently collected one of them near it + pos to NONE + } + } + } + + @Subscription + fun onCommand(event: MortemRegisterCommandsEvent) { + event.register("dev secrets edit") { + thenCallback("toggle") { + assertInCatacombs() + editing = !editing + if (!editing) reset() + Text.of("Secrets editing ") { + if (editing) append("Enabled", TextColor.GREEN) + else append("Disabled", TextColor.RED) + }.sendWithPrefix() + } + then("create") { + thenCallback("type", EnumArgument()) { + val catacomb = getCatacomb() + assertEditing() + val room = getCatacombsRoom() + val type = argument("type") + val pos = lookedAtPos() ?: throw CommandExceptions.create("Not looking at any position!") + val gridPos = CatacombsManager.worldPosToGridPos(pos.toBlockPos()) + if (catacomb[gridPos] != room) throw CommandExceptions.create("Position is in a different room!") + val roomPos = room.worldToRoom(pos) + val secret = type.constructor(roomPos) + secret.tryAutoModify() + editingSecret = secret + room.secretData?.addSecret(secret) + Text.of("Added $type secret to ${room.backingData?.name ?: "Unknown"}!").sendWithPrefix() + } + callback { + val catacomb = getCatacomb() + assertEditing() + val room = getCatacombsRoom() + val originalPos = lookedAtPos() ?: throw CommandExceptions.create("Not looking at any position!") + val originalBlockPos = originalPos.toBlockPos() + val (blockPos, type) = guessType(originalBlockPos, McLevel[originalBlockPos]) + val gridPos = CatacombsManager.worldPosToGridPos(blockPos) + if (catacomb[gridPos] != room) throw CommandExceptions.create("Position is in a different room!") + val pos = blockPos.toVector3ic() + val roomPos = room.worldToRoom(pos) + val secret = type.constructor(roomPos) + secret.tryAutoModify() + editingSecret = secret + room.secretData?.addSecret(secret) + Text.of("Added $type secret to ${room.backingData?.name ?: "Unknown"}!").sendWithPrefix() + } + } + then("type") { + thenCallback("type", EnumArgument()) { + assertInCatacombs() + assertEditing() + val room = getCatacombsRoom() + val secret = currentSecret() + val type = argument("type") + if (secret.type == type) throw CommandExceptions.create("Secret already has type $type!", false) + val newSecret = type.constructor(secret.pos) + newSecret.tryAutoModify() + editingSecret = newSecret + room.secretData?.replaceSecret(secret, newSecret) + Text.of("Changed secret type to $type!").sendWithPrefix() + } + thenCallback("guess") { + assertInCatacombs() + assertEditing() + val room = getCatacombsRoom() + val secret = currentSecret() + val worldPos = room.roomToWorld(secret.pos).toBlockPos() + val (blockPos, type) = guessType(worldPos, McLevel[worldPos]) + val roomPos = room.worldToRoom(blockPos.toVector3ic()) + if (type == secret.type && roomPos == secret.pos) return@thenCallback + val newSecret = type.constructor(roomPos) + newSecret.tryAutoModify() + editingSecret = newSecret + room.secretData?.replaceSecret(secret, newSecret) + Text.of("Added $type secret to ${room.backingData?.name ?: "Unknown"}!").sendWithPrefix() + } + } + then("move") { + then("up") { + callback { + assertInCatacombs() + assertEditing() + val secret = currentSecret() + secret.pos = secret.pos.mutableCopy().add(0, 1, 0) + Text.of("Moved ${secret.type} secret up by 1 block").sendWithPrefix() + } + thenCallback("amount", IntegerArgumentType.integer()) { + assertInCatacombs() + assertEditing() + assertInCatacombsRoom() + val secret = currentSecret() + val amount = argument("amount") + secret.pos = secret.pos.mutableCopy().add(0, amount, 0) + secret.tryAutoModify(true) + Text.of("Moved ${secret.type} secret up by $amount blocks").sendWithPrefix() + } + } + then("down") { + callback { + assertInCatacombs() + assertEditing() + assertInCatacombsRoom() + val secret = currentSecret() + secret.pos = secret.pos.mutableCopy().sub(0, 1, 0) + secret.tryAutoModify(true) + Text.of("Moved ${secret.type} secret down by 1 block").sendWithPrefix() + } + thenCallback("amount", IntegerArgumentType.integer()) { + assertInCatacombs() + assertEditing() + val secret = currentSecret() + val amount = argument("amount") + secret.pos = secret.pos.mutableCopy().sub(0, amount, 0) + secret.tryAutoModify(true) + Text.of("Moved ${secret.type} secret down by $amount blocks").sendWithPrefix() + } + } + thenCallback("x amount", IntegerArgumentType.integer()) { + assertInCatacombs() + assertEditing() + val room = getCatacombsRoom() + val secret = currentSecret() + val x = argument("amount") + if (x == 0) throw CommandExceptions.create("Amount cannot be zero!") + secret.pos = room.worldToRoom(room.roomToWorld(secret.pos).mutableCopy().add(x, 0, 0)) + secret.tryAutoModify(true) + Text.of("Moved ${secret.type} secret in x axis by $x blocks").sendWithPrefix() + } + thenCallback("z amount", IntegerArgumentType.integer()) { + assertInCatacombs() + assertEditing() + val room = getCatacombsRoom() + val secret = currentSecret() + val z = argument("amount") + if (z == 0) throw CommandExceptions.create("Amount cannot be zero!") + secret.pos = room.worldToRoom(room.roomToWorld(secret.pos).mutableCopy().add(0, 0, z)) + secret.tryAutoModify(true) + Text.of("Moved ${secret.type} secret in z axis by $z blocks").sendWithPrefix() + } + } + thenCallback("set redstone_key pickup value", BoolArgumentType.bool()) { + assertInCatacombs() + assertEditing() + assertInCatacombsRoom() + val secret = currentSecret() + if (secret.type != CatacombsSecretType.REDSTONE_KEY) throw CommandExceptions.create("Only redstone keys waypoints have \"pickup\" value!") + val value = argument("value") + val redstoneKey = secret.unsafeCast() + if (redstoneKey.pickUp == value) throw CommandExceptions.create("Redstone key already had pickup value as $value!", false) + redstoneKey.pickUp = value + Text.of("Set ${secret.type} secret pickup value to $value").sendWithPrefix() + } + then("remove") { + callback { + assertInCatacombs() + assertEditing() + val room = getCatacombsRoom() + val secret = currentSecret() + room.secretData?.removeSecret(secret) + Text.of("Removed ${secret.type} secret from ${room.backingData?.name ?: "Unknown"}").sendWithPrefix() + } + thenCallback("all") { + assertInCatacombs() + assertEditing() + val room = getCatacombsRoom() + room.secretData?.let { + it.secretGroups.clear() + it.markDirty() + } + Text.of("Removed all secrets from room ${room.backingData?.name ?: "Unknown"}").sendWithPrefix() + } + } + thenCallback("select") { + assertInCatacombs() + assertEditing() + val room = getCatacombsRoom() + val secret = room.getFirstIntersected() + editingSecret = secret + if (secret == null) throw CommandExceptions.create("Not looking at any waypoint!") + Text.of("Selected ${secret.type} secret!") + } + then("group") { + thenCallback("toggle") { + assertInCatacombs() + assertEditing() + groupingSecrets = !groupingSecrets + Text.of("Secrets grouping ") { + if (editing) append("Enabled", TextColor.GREEN) + else append("Disabled", TextColor.RED) + }.sendWithPrefix() + } + thenCallback("add") { + assertInCatacombs() + assertEditing() + assertGrouping() + val room = getCatacombsRoom() + val secret = room.getFirstIntersected() ?: throw CommandExceptions.create("Not looking at any waypoint!") + selectedSecrets.add(secret) + Text.of("Selected ${secret.type} secret to group!").sendWithPrefix() + } + thenCallback("remove") { + assertInCatacombs() + assertEditing() + assertGrouping() + val room = getCatacombsRoom() + val secret = room.getFirstIntersected() ?: throw CommandExceptions.create("Not looking at any waypoint!") + selectedSecrets.remove(secret) + Text.of("Unselected ${secret.type} secret to group!").sendWithPrefix() + } + thenCallback("clear") { + assertInCatacombs() + assertEditing() + assertGrouping() + assertInCatacombsRoom() + selectedSecrets.clear() + Text.of("Cleared selected secrets!").sendWithPrefix() + } + thenCallback("finish") { + assertInCatacombs() + assertEditing() + assertGrouping() + val room = getCatacombsRoom() + val secrets = selectedSecrets + if (secrets.isEmpty()) throw CommandExceptions.create("No secrets selected!") + val data = room.secretData ?: throw CommandExceptions.create("No secret data!") + secrets.forEach(data::removeSecret) + val group = CatacombsSecretGroup(secrets.toMutableList()) + data.secretGroups.add(group) + data.markDirty() + Text.of("Grouped ${secrets.size} secrets!").sendWithPrefix() + selectedSecrets.clear() + } + } + } + } + + @Subscription(ServerChangeEvent::class, ServerDisconnectEvent::class) + fun reset() { + editingSecret = null + groupingSecrets = false + selectedSecrets.clear() + } + + @Subscription + @OnlyIn(THE_CATACOMBS) + private fun RenderWorldEvent.onRenderWorld() { + val catacomb = CatacombsManager.catacomb ?: return + val room = catacomb.lastRoom ?: return + val data = room.secretData ?: return + val secrets = data.secretGroups.flatMapIndexed { index, group -> group.secrets.map { it to index + 1 } } + + //region Render normal secrets + for ((secret, group) in secrets) { + val fillColor = ARGB.color(100, secret.type.getColor()) + val aabb = secret.realAABB(room) + renderBox(aabb, fillColor) + val textColor = ARGB.color(255, secret.type.getColor()) + renderTextInWorld(aabb.bottomCenter.add(0.0, aabb.ysize + 0.5, 0.0), secret.type.toString(), textColor) + renderTextInWorld(aabb.bottomCenter.add(0.0, aabb.ysize + 1.0, 0.0), "GROUP $group", textColor) + } + //endregion + + if (!editing) return + + //region Editing secret + val currentSecret = editingSecret + if (currentSecret != null) { + val aabb = currentSecret.realAABB(room) + val color = ARGB.color(255, CatppuccinColors.Latte.lavender) + renderOutlineBox( + aabb.inflate(0.05), + color, + 15f, + ) + renderTextInWorld(aabb.center, "EDITING", color) + } + //endregion + + //region Render Grouping Secrets + if (groupingSecrets) { + for (secret in selectedSecrets) { + val color = ARGB.color(255, CatppuccinColors.Latte.green) + val aabb = secret.realAABB(room) + renderOutlineBox( + aabb.inflate(0.05), + color, + 10f, + ) + renderTextInWorld(aabb.bottomCenter.subtract(0.0, 0.5, 0.0), "GROUPING", color) + } + } + //endregion + + val pos = lookedAtPos() + if (pos != null) { + val blockPos = pos.toBlockPos() + val state = McLevel[blockPos] + val shape = state.getShape(McLevel.self, blockPos) + val aabb = if (shape.isEmpty) AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0) + else shape.bounds() + val color = ARGB.color(255, CatppuccinColors.Latte.text) + renderOutlineBox(aabb, color, 5f) + } + } + + @Subscription(CatacombRoomChangeEvent::class) + fun onCatacombRoomChange() { + editingSecret = null + selectedSecrets.clear() + } + + private fun RoomNode.getFirstIntersected(): CatacombsSecret? { + val data = secretData ?: return null + val secrets = data.secretGroups.flatMap { it.secrets } + val self = McClient.self.cameraEntity ?: return null + val pos = self.eyePosition + val rayEnd = pos.add(self.lookAngle.scale(70.0)) + + return secrets.mapNotNull { secret -> + val hit = secret.realAABB(this).clip(pos, rayEnd).getOrNull() ?: return@mapNotNull null + secret to pos.distanceTo(hit) + }.minByOrNull { it.second }?.first + } + +} diff --git a/src/main/kotlin/me/owdding/mortem/utils/CommandExceptions.kt b/src/main/kotlin/me/owdding/mortem/utils/CommandExceptions.kt new file mode 100644 index 0000000..caccb5a --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/utils/CommandExceptions.kt @@ -0,0 +1,39 @@ +package me.owdding.mortem.utils + +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.CommandExceptionType +import com.mojang.brigadier.exceptions.CommandSyntaxException +import me.owdding.mortem.core.catacombs.Catacomb +import me.owdding.mortem.core.catacombs.CatacombsManager +import me.owdding.mortem.core.catacombs.nodes.RoomNode +import net.minecraft.network.chat.Component +import tech.thatgravyboat.skyblockapi.utils.text.Text +import tech.thatgravyboat.skyblockapi.utils.text.TextColor +import tech.thatgravyboat.skyblockapi.utils.text.TextStyle.color + +// Even though its unused, we make the functions have CommandContext as a receiver parameter to make it so you can only +// throw them inside commands. +@Suppress("UnusedReceiverParameter") +object CommandExceptions : CommandExceptionType { + fun create(message: String, error: Boolean = true): CommandSyntaxException { + val text = Text.of(message) { + if (error) color = TextColor.RED + } + return CommandSyntaxException(this, text) + } + fun create(component: Component) = CommandSyntaxException(this, component) + + fun CommandContext<*>.getCatacomb(): Catacomb = CatacombsManager.catacomb ?: throw create("Not in a catacomb!") + + fun CommandContext<*>.assertInCatacombs() { + if (CatacombsManager.catacomb == null) throw create("Not in a catacomb!") + } + + fun CommandContext<*>.getCatacombsRoom(): RoomNode = CatacombsManager.catacomb?.lastRoom ?: throw create("Not in a room!") + + fun CommandContext<*>.assertInCatacombsRoom() { + if (CatacombsManager.catacomb?.lastRoom == null) throw create("Not in a room!") + } + + +} diff --git a/src/main/kotlin/me/owdding/mortem/utils/PosCodecs.kt b/src/main/kotlin/me/owdding/mortem/utils/PosCodecs.kt new file mode 100644 index 0000000..2717c66 --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/utils/PosCodecs.kt @@ -0,0 +1,85 @@ +package me.owdding.mortem.utils + +import com.mojang.datafixers.util.Either +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import me.owdding.ktcodecs.IncludedCodec +import me.owdding.mortem.utils.extensions.mutableCopy +import net.minecraft.Util +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.Vec3 +import org.joml.Vector3f +import org.joml.Vector3i +import org.joml.Vector3ic + +object PosCodecs { + + val vec3: Codec = createCodec( + ::Vec3, + Codec.DOUBLE, + { toDouble() }, + { x }, + { y }, + { z }, + ) + + @IncludedCodec + val aabb: Codec = Codec.withAlternative( + Codec.DOUBLE.listOf().comapFlatMap( + { list -> Util.fixedSize(list, 6).map { AABB(it[0], it[1], it[2], it[3], it[4], it[5]) } }, + { listOf(it.minX, it.minY, it.minZ, it.maxX, it.maxY, it.maxZ) }, + ), + RecordCodecBuilder.create { + it.group( + vec3.fieldOf("min").forGetter(AABB::getMinPosition), + vec3.fieldOf("max").forGetter(AABB::getMaxPosition), + ).apply(it, ::AABB) + } + ) + + @IncludedCodec + val vector3iCodec: Codec = createCodec( + ::Vector3i, + Codec.INT, + String::toInt, + { x }, { y }, { z }, + ) + + @IncludedCodec + val vector3icCodec: Codec = vector3iCodec.xmap({ it }, { it.mutableCopy() }) + + fun createCodec( + constructor: (NumberType, NumberType, NumberType) -> VecType, + numberCodec: Codec, + toNumber: String.() -> NumberType, + first: VecType.() -> NumberType, + second: VecType.() -> NumberType, + third: VecType.() -> NumberType, + asString: NumberType.() -> String = Number::toString, + ): Codec { + return Codec.either( + Codec.STRING.xmap( + { + it.split(":").let { elements -> + constructor(elements[0].toNumber(), elements[1].toNumber(), elements[2].toNumber()) + } + }, + { "${it.first().asString()}:${it.second().asString()}:${it.third().asString()}" }, + ), + Codec.either( + numberCodec.listOf().comapFlatMap( + { list -> Util.fixedSize(list, 3).map { constructor(it[0], it[1], it[2]) } }, + { listOf(it.first(), it.second(), it.third()) }, + ), + RecordCodecBuilder.create { + it.group( + numberCodec.fieldOf("x").forGetter(first), + numberCodec.fieldOf("y").forGetter(second), + numberCodec.fieldOf("z").forGetter(third), + ).apply(it, constructor) + }, + ).xmap(Either::unwrap) { Either.left(it) }, + ).xmap(Either::unwrap) { Either.left(it) } + } + +} diff --git a/src/main/kotlin/me/owdding/mortem/utils/RenderUtils.kt b/src/main/kotlin/me/owdding/mortem/utils/RenderUtils.kt new file mode 100644 index 0000000..ba218a0 --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/utils/RenderUtils.kt @@ -0,0 +1,50 @@ +package me.owdding.mortem.utils + +import com.mojang.blaze3d.pipeline.RenderPipeline +import com.mojang.blaze3d.platform.DepthTestFunction +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.renderer.RenderStateShard +import net.minecraft.client.renderer.RenderType +import net.minecraft.client.renderer.ShapeRenderer +import net.minecraft.util.ARGB +import net.minecraft.world.phys.AABB +import tech.thatgravyboat.skyblockapi.api.events.render.RenderWorldEvent +import java.util.* + +// TODO: move to mlib +object RenderUtils { + + fun RenderWorldEvent.renderOutlineBox( + position: AABB, + color: Int, + lineWidth: Float = 1f + ) { + atCamera { + val prev = RenderSystem.getShaderLineWidth() + RenderSystem.lineWidth(lineWidth) + ShapeRenderer.renderLineBox( + /*? >=1.21.10 {*/ poseStack.last(), /*?} else*/ /* poseStack,*/ + buffer.getBuffer(NO_DEPTH_LINES), + position.minX - 0.005, position.minY - 0.005, position.minZ - 0.005, + position.maxX + 0.005, position.maxY + 0.005, position.maxZ + 0.005, + ARGB.redFloat(color), ARGB.greenFloat(color), ARGB.blueFloat(color), ARGB.alphaFloat(color), + ) + RenderSystem.lineWidth(prev) + } + } + + val NO_DEPTH_LINES: RenderType = RenderType.create( + "mortem/no_depth_lines", + 1536, + RenderPipeline.builder(RenderPipelines.LINES_SNIPPET) + .withLocation("pipeline/lines") + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .build(), + RenderType.CompositeState.builder() + .setLineState(RenderStateShard.LineStateShard(OptionalDouble.empty())) + .setLayeringState(RenderStateShard.VIEW_OFFSET_Z_LAYERING) + .setOutputState(RenderStateShard.ITEM_ENTITY_TARGET) + .createCompositeState(false), + ) +} diff --git a/src/main/kotlin/me/owdding/mortem/utils/Utils.kt b/src/main/kotlin/me/owdding/mortem/utils/Utils.kt index e781e4b..12da9b5 100644 --- a/src/main/kotlin/me/owdding/mortem/utils/Utils.kt +++ b/src/main/kotlin/me/owdding/mortem/utils/Utils.kt @@ -5,14 +5,20 @@ import com.mojang.serialization.Codec import kotlinx.coroutines.runBlocking import me.owdding.mortem.Mortem import me.owdding.mortem.generated.MortemCodecs +import net.minecraft.core.BlockPos +import net.minecraft.world.level.block.entity.BlockEntityType +import net.minecraft.world.level.block.entity.SkullBlockEntity import org.joml.Vector2i +import org.joml.Vector2ic import org.joml.times import tech.thatgravyboat.skyblockapi.api.SkyBlockAPI import tech.thatgravyboat.skyblockapi.api.events.base.SkyBlockEvent +import tech.thatgravyboat.skyblockapi.helpers.McLevel import tech.thatgravyboat.skyblockapi.utils.json.Json import tech.thatgravyboat.skyblockapi.utils.json.Json.readJson import tech.thatgravyboat.skyblockapi.utils.json.Json.toDataOrThrow import java.nio.file.Files +import kotlin.jvm.optionals.getOrNull import kotlin.reflect.jvm.javaType import kotlin.reflect.typeOf @@ -20,16 +26,44 @@ object Utils { internal fun SkyBlockEvent.post() = this.post(SkyBlockAPI.eventBus) + fun McLevel.getSkullTexture(pos: BlockPos): String? { + return level.getBlockEntity(pos, BlockEntityType.SKULL).getOrNull()?.getTexture() + } + + fun SkullBlockEntity.getTexture(): String? { + //? >=1.21.10 { + return this.ownerProfile?.partialProfile()?.id?.toString() + //?} else + /*return ownerProfile?.id?.get()?.toString()*/ + } + + inline fun > Iterable.minByWithOrNull(selector: (T) -> R): Pair? { + val iterator = iterator() + if (!iterator.hasNext()) return null + var minElem = iterator.next() + var minValue = selector(minElem) + if (!iterator.hasNext()) return minElem to minValue + do { + val e = iterator.next() + val v = selector(e) + if (minValue > v) { + minElem = e + minValue = v + } + } while (iterator.hasNext()) + return minElem to minValue + } + @Suppress("UNCHECKED_CAST") - fun From.unsafeCast() = (this as To) - - val vectorZeroZero = Vector2i(0, 0) - val vectorZeroOne = Vector2i(0, 1) - val vectorOneZero = Vector2i(1, 0) - val vectorOneOne = Vector2i(1, 1) - val vectorZeroTwo = vectorZeroOne * 2 - val vectorTwoZero = vectorOneZero * 2 - val vectorTwoTwo = vectorOneOne * 2 + fun Any.unsafeCast() = (this as To) + + val vectorZeroZero: Vector2ic = Vector2i(0, 0) + val vectorZeroOne: Vector2ic = Vector2i(0, 1) + val vectorOneZero: Vector2ic = Vector2i(1, 0) + val vectorOneOne: Vector2ic = Vector2i(1, 1) + val vectorZeroTwo: Vector2ic = vectorZeroOne * 2 + val vectorTwoZero: Vector2ic = vectorOneZero * 2 + val vectorTwoTwo: Vector2ic = vectorOneOne * 2 @OptIn(ExperimentalStdlibApi::class) inline fun loadFromRepo(file: String): T? = runBlocking { diff --git a/src/repo/rooms/boulder_0.json b/src/repo/rooms/boulder_0.json index 079b0a7..80acdb7 100644 --- a/src/repo/rooms/boulder_0.json +++ b/src/repo/rooms/boulder_0.json @@ -7,7 +7,7 @@ "a699a685ed6a811931ed53ad9d80e8a2dcd7d313c209f6601e60420a807f69f9": "NORTH" }, "extra_rotation_handling": true, - "name": "Boulder 0", + "name": "Boulder", "id": "boulder_0", "secrets": 0 } diff --git a/src/repo/rooms/boulder_1.json b/src/repo/rooms/boulder_1.json index e68c527..621901c 100644 --- a/src/repo/rooms/boulder_1.json +++ b/src/repo/rooms/boulder_1.json @@ -7,7 +7,7 @@ "35040134f435f1b00745926d5f58f3b06d11e76f472b581f36c9090ed1065bc4": "NORTH" }, "extra_rotation_handling": true, - "name": "Boulder 1", + "name": "Boulder", "id": "boulder_1", "secrets": 0 } diff --git a/src/repo/rooms/entry_0.json b/src/repo/rooms/entry_0.json index 5879af2..b0f3d84 100644 --- a/src/repo/rooms/entry_0.json +++ b/src/repo/rooms/entry_0.json @@ -7,6 +7,6 @@ "e3190167f7ea2496700c9951eb2a60e7fb4c827b3074cdba3be84b1371caa5b6": "EAST", "dfba625e8ddf57c00d9e8c47c4c42edba35c17685e15c2607db9286a6147bf7f": "WEST" }, - "name": "Entry 0", + "name": "Entry", "id": "entry_0" -} \ No newline at end of file +} diff --git a/src/repo/rooms/entry_1.json b/src/repo/rooms/entry_1.json index d87f812..eb6dc19 100644 --- a/src/repo/rooms/entry_1.json +++ b/src/repo/rooms/entry_1.json @@ -7,6 +7,6 @@ "93808ecbce84b3583b7500df2ed5bcaf301f34ee9182e62328708478206b2d7b": "EAST", "efa6060a36cf5fd761a4d974c7c35bb477ac558bcac006f0784af26bda6d66bc": "WEST" }, - "name": "Entry 1", + "name": "Entry", "id": "entry_1" -} \ No newline at end of file +} diff --git a/src/repo/rooms/entry_2.json b/src/repo/rooms/entry_2.json index 7e74518..56356c1 100644 --- a/src/repo/rooms/entry_2.json +++ b/src/repo/rooms/entry_2.json @@ -7,6 +7,6 @@ "7cdb5d1c46925f10312c14f17f13ce1dbc946bcc3acb44b89e74443016eb063f": "WEST", "fad4d79254c1261ea3e74a2f68c142ddb6ae5f62378c2065bf0ade7e081517e9": "EAST" }, - "name": "Entry 2", + "name": "Entry", "id": "entry_2" -} \ No newline at end of file +} diff --git a/src/repo/secrets/admin.json b/src/repo/secrets/admin.json new file mode 100644 index 0000000..0975f1c --- /dev/null +++ b/src/repo/secrets/admin.json @@ -0,0 +1,4 @@ +{ + "id": "admin", + "groups": [] +} diff --git a/src/repo/secrets/entry_0.json b/src/repo/secrets/entry_0.json new file mode 100644 index 0000000..7850000 --- /dev/null +++ b/src/repo/secrets/entry_0.json @@ -0,0 +1,4 @@ +{ + "id": "entry_0", + "secret_groups": [] +} \ No newline at end of file diff --git a/src/repo/secrets/gravel.json b/src/repo/secrets/gravel.json new file mode 100644 index 0000000..a072876 --- /dev/null +++ b/src/repo/secrets/gravel.json @@ -0,0 +1,43 @@ +{ + "id": "gravel", + "secret_groups": [ + { + "pos": "6:69:41", + "type": "ITEM" + }, + { + "pos": "12:69:45", + "type": "ESSENCE" + }, + { + "pos": "3:87:20", + "type": "ITEM" + }, + { + "pos": "1:87:19", + "type": "ESSENCE" + }, + { + "pos": "2:92:-13", + "type": "BAT" + }, + [ + { + "pos": "-11:69:16", + "aabb": [ + 0.0, + 0.25, + 0.3125, + 0.375, + 0.75, + 0.6875 + ], + "type": "LEVER" + }, + { + "pos": "4:69:-12", + "type": "CHEST" + } + ] + ] +} \ No newline at end of file diff --git a/src/repo/secrets/purple_flags.json b/src/repo/secrets/purple_flags.json new file mode 100644 index 0000000..661b7ae --- /dev/null +++ b/src/repo/secrets/purple_flags.json @@ -0,0 +1,39 @@ +{ + "id": "purple_flags", + "secret_groups": [ + { + "pos": "1:67:-8", + "type": "CHEST" + }, + { + "pos": "-25:57:13", + "type": "ESSENCE" + }, + { + "pos": "-4:49:13", + "type": "ITEM" + }, + { + "pos": "8:49:14", + "type": "CHEST" + }, + [ + { + "pos": "28:49:9", + "type": "CHEST" + }, + { + "pos": "4:55:14", + "aabb": [ + 0.0, + 0.25, + 0.3125, + 0.375, + 0.75, + 0.6875 + ], + "type": "LEVER" + } + ] + ] +} \ No newline at end of file diff --git a/src/repo/secrets/red_blue.json b/src/repo/secrets/red_blue.json new file mode 100644 index 0000000..10e1f08 --- /dev/null +++ b/src/repo/secrets/red_blue.json @@ -0,0 +1,4 @@ +{ + "id": "red_blue", + "secret_groups": [] +} \ No newline at end of file