Skip to content

Commit f51ce07

Browse files
authored
feat: room exporting (#6)
* allow for room exporting (to structure block structures) * evil and mysterious commit
1 parent d619384 commit f51ce07

5 files changed

Lines changed: 306 additions & 7 deletions

File tree

src/main/kotlin/me/owdding/mortem/core/catacombs/CatacombsManager.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ import kotlin.math.abs
4747
import kotlin.math.floor
4848
import me.owdding.mortem.core.event.catacomb.CatacombNodeChangeEvent
4949
import me.owdding.mortem.core.event.catacomb.CatacombRoomChangeEvent
50+
import net.minecraft.nbt.NbtIo
5051
import tech.thatgravyboat.skyblockapi.utils.extentions.filterKeysNotNull
52+
import tech.thatgravyboat.skyblockapi.utils.text.TextStyle.hover
5153
import tech.thatgravyboat.skyblockapi.utils.text.TextStyle.onClick
54+
import java.util.concurrent.CompletableFuture
55+
import kotlin.io.path.createParentDirectories
5256

5357
@Module
5458
object CatacombsManager {
@@ -326,6 +330,41 @@ object CatacombsManager {
326330
append(format(playerNode.roomToWorld(roomPos)))
327331
}.sendWithPrefix()
328332
}
333+
event.registerWithCallback("mortem dev export_room") {
334+
val catacomb = catacomb ?: return@registerWithCallback
335+
336+
val playerNode = catacomb.grid[worldPosToGridPos(McPlayer.self!!.blockPosition())]
337+
338+
if (playerNode !is RoomNode) return@registerWithCallback
339+
340+
CompletableFuture.runAsync {
341+
val structureData = playerNode.exportToStructure()
342+
343+
if (structureData == null) {
344+
Text.of("Failed to export room") {
345+
color = CatppuccinColors.Mocha.red
346+
}.sendWithPrefix()
347+
348+
return@runAsync
349+
}
350+
351+
val fileName = "structures/${System.currentTimeMillis()}.nbt"
352+
353+
val savePath = defaultPath.resolve(fileName)
354+
355+
savePath.createParentDirectories()
356+
357+
NbtIo.writeCompressed(structureData, savePath)
358+
359+
Text.of("Wrote current room to ") {
360+
append(fileName) {
361+
color = CatppuccinColors.Mocha.pink
362+
hover = Text.of(savePath.toString())
363+
}
364+
color = CatppuccinColors.Mocha.lavender
365+
}.sendWithPrefix()
366+
}
367+
}
329368
}
330369

331370
private val defaultPath: Path = McClient.config.resolve("mortem/data")

src/main/kotlin/me/owdding/mortem/core/catacombs/nodes/CatacombsNode.kt

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import me.owdding.mortem.core.catacombs.CatacombDoorType
44
import me.owdding.mortem.core.catacombs.CatacombRoomType
55
import me.owdding.mortem.core.catacombs.CatacombsColorProvider
66
import me.owdding.mortem.core.catacombs.StoredCatacombRoom
7+
import me.owdding.mortem.generated.MortemCodecs
8+
import me.owdding.mortem.utils.StructureUtils
79
import me.owdding.mortem.utils.Utils
810
import me.owdding.mortem.utils.extensions.mutableCopy
911
import me.owdding.mortem.utils.extensions.sendWithPrefix
1012
import me.owdding.mortem.utils.extensions.toVec2d
13+
import net.minecraft.core.BlockPos
14+
import net.minecraft.nbt.CompoundTag
15+
import net.minecraft.nbt.NbtOps
1116
import net.minecraft.world.level.block.Rotation
12-
import org.joml.Vector2i
13-
import org.joml.Vector3d
14-
import org.joml.Vector3dc
15-
import org.joml.Vector3i
16-
import org.joml.Vector3ic
17-
import org.joml.component1
18-
import org.joml.component2
17+
import org.joml.*
18+
import tech.thatgravyboat.skyblockapi.helpers.McLevel
1919
import tech.thatgravyboat.skyblockapi.utils.text.Text
2020
import kotlin.math.max
2121
import kotlin.math.min
@@ -176,6 +176,59 @@ class RoomNode(
176176
val origin = getCenter().toVec2d().add(0.5, 0.5)
177177
return room.add(origin.x, 0.0, origin.y)
178178
}
179+
180+
fun exportToStructure(): CompoundTag? {
181+
182+
val chunks = buildList {
183+
for (first in positions) {
184+
add(
185+
Vector2i(
186+
(-12 + first.x * 2) * 16 + 7,
187+
(-12 + first.y * 2) * 16 + 7,
188+
),
189+
)
190+
for (second in positions) {
191+
if ((first.x - second.x == 1) != (first.y - second.y == 1)) {
192+
add(
193+
Vector2i(
194+
(-12 + first.x + second.x) * 16 + 7,
195+
(-12 + first.y + second.y) * 16 + 7,
196+
),
197+
)
198+
}
199+
}
200+
}
201+
if (shape == CatacombRoomShape.TWO_BY_TWO) {
202+
val centerX = positions.sumOf { 2 * it.x } / positions.size
203+
val centerY = positions.sumOf { 2 * it.y } / positions.size
204+
205+
add(
206+
Vector2i(
207+
(-12 + centerX) * 16 + 7,
208+
(-12 + centerY) * 16 + 7,
209+
),
210+
)
211+
}
212+
}
213+
214+
val roomStructure = StructureUtils.encodeStructureFromRegions(
215+
McLevel.level,
216+
chunks.map { center ->
217+
BlockPos(center.x - 15, 0, center.y - 15) to BlockPos(center.x + 15, 255, center.y + 15)
218+
},
219+
rotation ?: Rotation.NONE,
220+
) ?: return null
221+
222+
if (backingData != null) {
223+
val storedRoomCodec = MortemCodecs.getCodec<StoredCatacombRoom>()
224+
225+
val roomData = storedRoomCodec.encodeStart(NbtOps.INSTANCE, backingData).getOrThrow()
226+
227+
roomStructure.put("backing_room_data", roomData)
228+
}
229+
230+
return roomStructure
231+
}
179232
}
180233

181234
enum class CatacombRoomShape {
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package me.owdding.mortem.utils
2+
3+
import net.minecraft.core.BlockPos
4+
import net.minecraft.nbt.CompoundTag
5+
import net.minecraft.nbt.NbtOps
6+
import net.minecraft.util.ProblemReporter
7+
import net.minecraft.world.level.EmptyBlockGetter
8+
import net.minecraft.world.level.Level
9+
import net.minecraft.world.level.block.Block
10+
import net.minecraft.world.level.block.Blocks
11+
import net.minecraft.world.level.block.Rotation
12+
import net.minecraft.world.level.block.state.BlockState
13+
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate
14+
import net.minecraft.world.level.storage.TagValueOutput
15+
import tech.thatgravyboat.skyblockapi.utils.extentions.minus
16+
import tech.thatgravyboat.skyblockapi.utils.extentions.plus
17+
import kotlin.jvm.optionals.getOrNull
18+
import kotlin.math.max
19+
import kotlin.math.min
20+
21+
object StructureUtils {
22+
// mostest efficientest code ever written
23+
fun encodeStructureFromRegions(
24+
level: Level,
25+
regions: Iterable<Pair<BlockPos, BlockPos>>,
26+
rotation: Rotation = Rotation.NONE,
27+
ignored: List<Block> = listOf(Blocks.AIR, Blocks.VOID_AIR, Blocks.CAVE_AIR, Blocks.STRUCTURE_VOID),
28+
): CompoundTag? {
29+
30+
var minCorner: BlockPos? = null
31+
var maxCorner: BlockPos? = null
32+
33+
val queuedBlocks = mutableMapOf<BlockPos, BlockState>()
34+
35+
for (region in regions) {
36+
val regionMin = BlockPos(
37+
min(region.first.x, region.second.x),
38+
min(region.first.y, region.second.y),
39+
min(region.first.z, region.second.z),
40+
)
41+
val regionMax = BlockPos(
42+
max(region.first.x, region.second.x),
43+
max(region.first.y, region.second.y),
44+
max(region.first.z, region.second.z),
45+
)
46+
47+
if (!level.isLoaded(regionMin) || !level.isLoaded(regionMax)) return null
48+
49+
var blockMin = regionMax
50+
var blockMax = regionMin
51+
52+
for (blockPos in BlockPos.betweenClosed(regionMin, regionMax)) {
53+
if (blockPos in queuedBlocks) continue
54+
55+
val blockState = level.getBlockState(blockPos)
56+
if (!ignored.stream().anyMatch(blockState::`is`)) {
57+
if (blockMax.x > blockPos.x || blockMax.y > blockPos.y || blockMax.z > blockPos.z) {
58+
blockMin = BlockPos(
59+
min(blockPos.x, blockMin.x),
60+
min(blockPos.y, blockMin.y),
61+
min(blockPos.z, blockMin.z),
62+
)
63+
}
64+
if (blockMax.x < blockPos.x || blockMax.y < blockPos.y || blockMax.z < blockPos.z) {
65+
blockMax = BlockPos(
66+
max(blockPos.x, blockMax.x),
67+
max(blockPos.y, blockMax.y),
68+
max(blockPos.z, blockMax.z),
69+
)
70+
}
71+
queuedBlocks[blockPos.immutable()] = blockState
72+
}
73+
}
74+
75+
if (blockMin == regionMax) blockMin = regionMin
76+
if (blockMax == regionMin) blockMax = regionMax
77+
78+
if (minCorner == null || maxCorner == null) {
79+
minCorner = blockMin
80+
maxCorner = blockMax
81+
} else {
82+
minCorner = BlockPos(
83+
min(blockMin.x, minCorner.x),
84+
min(blockMin.y, minCorner.y),
85+
min(blockMin.z, minCorner.z),
86+
)
87+
maxCorner = BlockPos(
88+
max(blockMax.x, maxCorner.x),
89+
max(blockMax.y, maxCorner.y),
90+
max(blockMax.z, maxCorner.z),
91+
)
92+
}
93+
}
94+
95+
if (minCorner == null || maxCorner == null) return null
96+
97+
val size = maxCorner - minCorner + BlockPos(1, 1, 1)
98+
99+
val rotatedSize = when (rotation) {
100+
Rotation.NONE, Rotation.CLOCKWISE_180 -> size
101+
Rotation.CLOCKWISE_90, Rotation.COUNTERCLOCKWISE_90 -> BlockPos(size.z, size.y, size.x)
102+
}
103+
104+
val template = StructureTemplate()
105+
val fullBlockList = mutableListOf<StructureTemplate.StructureBlockInfo>()
106+
val blockEntitiesList = mutableListOf<StructureTemplate.StructureBlockInfo>()
107+
val otherBlocksList = mutableListOf<StructureTemplate.StructureBlockInfo>()
108+
109+
val structureEntities = mutableListOf<StructureTemplate.StructureEntityInfo>()
110+
111+
for ((blockPos, blockState) in queuedBlocks) {
112+
if (isOutOfBounds(minCorner, maxCorner, blockPos)) continue
113+
if (ignored.stream().anyMatch(blockState::`is`)) continue
114+
115+
val relativeBlockPos = blockPos.subtract(minCorner)
116+
117+
val rotatedBlockPos = when (rotation) {
118+
Rotation.NONE -> relativeBlockPos
119+
Rotation.CLOCKWISE_90 -> BlockPos(
120+
rotatedSize.x - relativeBlockPos.z - 1,
121+
relativeBlockPos.y,
122+
relativeBlockPos.x,
123+
)
124+
125+
Rotation.CLOCKWISE_180 -> BlockPos(
126+
rotatedSize.x - relativeBlockPos.x - 1,
127+
relativeBlockPos.y,
128+
rotatedSize.z - relativeBlockPos.z - 1,
129+
)
130+
131+
Rotation.COUNTERCLOCKWISE_90 -> BlockPos(
132+
relativeBlockPos.z,
133+
relativeBlockPos.y,
134+
rotatedSize.z - relativeBlockPos.x - 1,
135+
)
136+
}
137+
138+
val rotatedBlockState = blockState.rotate(rotation)
139+
140+
val blockEntity = level.getBlockEntity(blockPos)
141+
142+
val info = if (blockEntity != null) {
143+
val tagValueOutput =
144+
TagValueOutput.createWithContext(ProblemReporter.DISCARDING, level.registryAccess())
145+
blockEntity.saveWithId(tagValueOutput)
146+
StructureTemplate.StructureBlockInfo(rotatedBlockPos, rotatedBlockState, tagValueOutput.buildResult())
147+
} else {
148+
StructureTemplate.StructureBlockInfo(rotatedBlockPos, rotatedBlockState, null)
149+
}
150+
151+
when {
152+
info.nbt != null -> blockEntitiesList.add(info)
153+
info.state.block.hasDynamicShape() || !info.state.isCollisionShapeFullBlock(
154+
EmptyBlockGetter.INSTANCE,
155+
BlockPos.ZERO,
156+
) -> otherBlocksList.add(info)
157+
158+
else -> fullBlockList.add(info)
159+
}
160+
}
161+
162+
val blockInfoList = buildList {
163+
val comparator = Comparator.comparingInt<StructureTemplate.StructureBlockInfo> { it.pos.y }
164+
.thenComparingInt { it.pos.x }
165+
.thenComparingInt { it.pos.z }
166+
167+
fullBlockList.sortWith(comparator)
168+
addAll(fullBlockList)
169+
otherBlocksList.sortWith(comparator)
170+
addAll(otherBlocksList)
171+
blockEntitiesList.sortWith(comparator)
172+
addAll(blockEntitiesList)
173+
}
174+
175+
template.palettes.clear()
176+
template.palettes.add(StructureTemplate.Palette(blockInfoList))
177+
template.entityInfoList.addAll(structureEntities)
178+
179+
template.size = rotatedSize
180+
181+
val nbt = CompoundTag()
182+
183+
template.save(nbt)
184+
185+
val blockPosCodec = BlockPos.CODEC
186+
187+
val encodedMinPos = blockPosCodec.encodeStart(NbtOps.INSTANCE, minCorner).getOrThrow()
188+
val encodedMaxPos = blockPosCodec.encodeStart(NbtOps.INSTANCE, maxCorner).getOrThrow()
189+
190+
nbt.putString("source_rotation", rotation.serializedName)
191+
nbt.put("source_min_corner", encodedMinPos)
192+
nbt.put("source_max_corner", encodedMaxPos)
193+
nbt.putLong("export_time", System.currentTimeMillis())
194+
195+
return nbt
196+
}
197+
198+
private fun isOutOfBounds(startPos: BlockPos, endPos: BlockPos, query: BlockPos) = query.x > endPos.x || query.x < startPos.x ||
199+
query.y > endPos.y || query.y < startPos.y ||
200+
query.z > endPos.z || query.z < startPos.z
201+
}

src/main/resources/fabric.mod.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"mixins": [
2121
"mortem.mixins.json"
2222
],
23+
"accessWidener": "mortem.accesswidener",
2324
"depends": {
2425
"fabricloader": ">=0.16",
2526
"minecraft": ">=${minecraft}"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
accessWidener v2 named
2+
accessible field net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate entityInfoList Ljava/util/List;
3+
accessible field net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate palettes Ljava/util/List;
4+
accessible field net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate size Lnet/minecraft/core/Vec3i;
5+
accessible method net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate$Palette <init> (Ljava/util/List;)V

0 commit comments

Comments
 (0)