|
| 1 | +package dev.xpple.seedmapper.command.commands; |
| 2 | + |
| 3 | +import com.github.cubiomes.Cubiomes; |
| 4 | +import com.github.cubiomes.ItemStack; |
| 5 | +import com.github.cubiomes.LootTableContext; |
| 6 | +import com.github.cubiomes.Piece; |
| 7 | +import com.github.cubiomes.Pos; |
| 8 | +import com.github.cubiomes.StructureConfig; |
| 9 | +import com.github.cubiomes.StructureSaltConfig; |
| 10 | +import com.github.cubiomes.StructureVariant; |
| 11 | +import com.github.cubiomes.SurfaceNoise; |
| 12 | +import com.mojang.brigadier.CommandDispatcher; |
| 13 | +import com.mojang.brigadier.arguments.StringArgumentType; |
| 14 | +import com.mojang.brigadier.exceptions.CommandSyntaxException; |
| 15 | +import dev.xpple.seedmapper.command.CommandExceptions; |
| 16 | +import dev.xpple.seedmapper.command.CustomClientCommandSource; |
| 17 | +import dev.xpple.seedmapper.feature.StructureChecks; |
| 18 | +import dev.xpple.seedmapper.util.NativeAccess; |
| 19 | +import dev.xpple.seedmapper.world.WorldPreset; |
| 20 | +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; |
| 21 | + |
| 22 | +import java.io.IOException; |
| 23 | +import java.lang.foreign.Arena; |
| 24 | +import java.lang.foreign.MemorySegment; |
| 25 | +import java.lang.foreign.ValueLayout; |
| 26 | +import java.nio.charset.StandardCharsets; |
| 27 | +import java.nio.file.Files; |
| 28 | +import java.nio.file.Path; |
| 29 | +import java.nio.file.StandardOpenOption; |
| 30 | +import java.time.LocalDateTime; |
| 31 | +import java.time.format.DateTimeFormatter; |
| 32 | +import java.util.*; |
| 33 | +import java.util.stream.Collectors; |
| 34 | + |
| 35 | +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; |
| 36 | + |
| 37 | +public class ExportLootCommand { |
| 38 | + |
| 39 | + private static final DateTimeFormatter EXPORT_TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); |
| 40 | + |
| 41 | + public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher) { |
| 42 | + var base = literal("sm:exportLoot"); |
| 43 | + var radiusArg = argument("radius", com.mojang.brigadier.arguments.IntegerArgumentType.integer(1)); |
| 44 | + var dimensionArg = argument("dimension", dev.xpple.seedmapper.command.arguments.DimensionArgument.dimension()); |
| 45 | + var typesArg = argument("types", StringArgumentType.greedyString()); |
| 46 | + |
| 47 | + // radius only: /sm:exportLoot <radius> |
| 48 | + base = base.then(radiusArg.executes(ctx -> exportLoot(CustomClientCommandSource.of(ctx.getSource()), com.mojang.brigadier.arguments.IntegerArgumentType.getInteger(ctx, "radius"), null, ""))); |
| 49 | + |
| 50 | + // radius + dimension: /sm:exportLoot <radius> <dimension> |
| 51 | + radiusArg = radiusArg.then(dimensionArg.executes(ctx -> exportLoot(CustomClientCommandSource.of(ctx.getSource()), com.mojang.brigadier.arguments.IntegerArgumentType.getInteger(ctx, "radius"), dev.xpple.seedmapper.command.arguments.DimensionArgument.getDimension(ctx, "dimension"), ""))); |
| 52 | + |
| 53 | + // radius + dimension + types: /sm:exportLoot <radius> <dimension> <types...> |
| 54 | + dimensionArg = dimensionArg.then(typesArg.executes(ctx -> exportLoot(CustomClientCommandSource.of(ctx.getSource()), com.mojang.brigadier.arguments.IntegerArgumentType.getInteger(ctx, "radius"), dev.xpple.seedmapper.command.arguments.DimensionArgument.getDimension(ctx, "dimension"), StringArgumentType.getString(ctx, "types")))); |
| 55 | + |
| 56 | + // attach the extended chains |
| 57 | + base = base.then(radiusArg); |
| 58 | + radiusArg = radiusArg.then(dimensionArg); |
| 59 | + |
| 60 | + dispatcher.register(base); |
| 61 | + } |
| 62 | + |
| 63 | + private static int exportLoot(CustomClientCommandSource source, int radius, Integer dimensionArg, String types) throws CommandSyntaxException { |
| 64 | + int version = source.getVersion(); |
| 65 | + if (version <= Cubiomes.MC_1_12()) { |
| 66 | + throw CommandExceptions.LOOT_NOT_SUPPORTED_EXCEPTION.create(); |
| 67 | + } |
| 68 | + |
| 69 | + int dimension = dimensionArg == null ? source.getDimension() : dimensionArg; |
| 70 | + long seed = source.getSeed().getSecond(); |
| 71 | + WorldPreset preset = source.getWorldPreset(); |
| 72 | + |
| 73 | + Set<Integer> filterStructures = null; |
| 74 | + if (types != null && !types.isBlank()) { |
| 75 | + if (!"all".equalsIgnoreCase(types.trim())) { |
| 76 | + Set<String> wanted = Arrays.stream(types.split("\\s+")) |
| 77 | + .map(String::trim) |
| 78 | + .filter(s -> !s.isEmpty()) |
| 79 | + .map(String::toLowerCase) |
| 80 | + .collect(Collectors.toSet()); |
| 81 | + filterStructures = new HashSet<>(); |
| 82 | + try (Arena a = Arena.ofConfined()) { |
| 83 | + for (int i = 0; i < Cubiomes.FEATURE_NUM(); i++) { |
| 84 | + MemorySegment sconf = StructureConfig.allocate(a); |
| 85 | + if (Cubiomes.getStructureConfig(i, version, sconf) == 0) continue; |
| 86 | + String name = NativeAccess.readString(Cubiomes.struct2str(StructureConfig.structType(sconf))); |
| 87 | + if (wanted.contains(name.toLowerCase())) { |
| 88 | + filterStructures.add((int) StructureConfig.structType(sconf)); |
| 89 | + } |
| 90 | + } |
| 91 | + } |
| 92 | + if (filterStructures.isEmpty()) filterStructures = null; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + try (Arena arena = Arena.ofConfined()) { |
| 97 | + MemorySegment generator = com.github.cubiomes.Generator.allocate(arena); |
| 98 | + Cubiomes.setupGenerator(generator, version, preset.generatorFlags()); |
| 99 | + Cubiomes.applySeed(generator, dimension, seed); |
| 100 | + |
| 101 | + MemorySegment surfaceNoise = SurfaceNoise.allocate(arena); |
| 102 | + Cubiomes.initSurfaceNoise(surfaceNoise, dimension, seed); |
| 103 | + |
| 104 | + // collect structure configs available in this dimension |
| 105 | + List<MemorySegment> structureConfigs = new ArrayList<>(); |
| 106 | + for (int s = 0; s < Cubiomes.FEATURE_NUM(); s++) { |
| 107 | + MemorySegment sconf = StructureConfig.allocate(arena); |
| 108 | + if (Cubiomes.getStructureConfig(s, version, sconf) == 0) continue; |
| 109 | + if (StructureConfig.dim(sconf) != dimension) continue; |
| 110 | + if (filterStructures != null && !filterStructures.contains(StructureConfig.structType(sconf))) continue; |
| 111 | + structureConfigs.add(sconf); |
| 112 | + } |
| 113 | + |
| 114 | + BlockCollector collector = new BlockCollector(seed, version, generator, surfaceNoise, structureConfigs, radius, source); |
| 115 | + collector.collectAndExport(); |
| 116 | + } |
| 117 | + return 1; |
| 118 | + } |
| 119 | + |
| 120 | + // Helper class to perform collection and export |
| 121 | + private static class BlockCollector { |
| 122 | + private final long seed; |
| 123 | + private final int version; |
| 124 | + private final MemorySegment generator; |
| 125 | + private final MemorySegment surfaceNoise; |
| 126 | + private final List<MemorySegment> structureConfigs; |
| 127 | + private final int radius; |
| 128 | + private final CustomClientCommandSource source; |
| 129 | + |
| 130 | + BlockCollector(long seed, int version, MemorySegment generator, MemorySegment surfaceNoise, List<MemorySegment> structureConfigs, int radius, CustomClientCommandSource source) { |
| 131 | + this.seed = seed; |
| 132 | + this.version = version; |
| 133 | + this.generator = generator; |
| 134 | + this.surfaceNoise = surfaceNoise; |
| 135 | + this.structureConfigs = structureConfigs; |
| 136 | + this.radius = radius; |
| 137 | + this.source = source; |
| 138 | + } |
| 139 | + |
| 140 | + void collectAndExport() { |
| 141 | + try (Arena arena = Arena.ofConfined()) { |
| 142 | + int centerX = (int) Math.floor(this.source.getPosition().x()); |
| 143 | + int centerZ = (int) Math.floor(this.source.getPosition().z()); |
| 144 | + |
| 145 | + JsonArrayBuilder root = new JsonArrayBuilder(); |
| 146 | + |
| 147 | + for (MemorySegment sconf : this.structureConfigs) { |
| 148 | + int structType = (int) StructureConfig.structType(sconf); |
| 149 | + int regionSize = StructureConfig.regionSize(sconf) << 4; |
| 150 | + int minRegionX = (centerX - this.radius) / regionSize; |
| 151 | + int maxRegionX = (centerX + this.radius) / regionSize; |
| 152 | + int minRegionZ = (centerZ - this.radius) / regionSize; |
| 153 | + int maxRegionZ = (centerZ + this.radius) / regionSize; |
| 154 | + |
| 155 | + StructureChecks.GenerationCheck generationCheck = StructureChecks.getGenerationCheck(structType); |
| 156 | + |
| 157 | + for (int rx = minRegionX; rx <= maxRegionX; rx++) { |
| 158 | + for (int rz = minRegionZ; rz <= maxRegionZ; rz++) { |
| 159 | + MemorySegment structurePos = Pos.allocate(arena); |
| 160 | + if (!generationCheck.check(this.generator, this.surfaceNoise, rx, rz, structurePos)) continue; |
| 161 | + int posX = Pos.x(structurePos); |
| 162 | + int posZ = Pos.z(structurePos); |
| 163 | + // distance check |
| 164 | + long dx = posX - centerX; |
| 165 | + long dz = posZ - centerZ; |
| 166 | + if (dx * dx + dz * dz > (long) this.radius * this.radius) continue; |
| 167 | + |
| 168 | + MemorySegment variant = StructureVariant.allocate(arena); |
| 169 | + int biome = Cubiomes.getBiomeAt(this.generator, 4, posX >> 2, 320 >> 2, posZ >> 2); |
| 170 | + Cubiomes.getVariant(variant, structType, this.version, this.seed, posX, posZ, biome); |
| 171 | + biome = StructureVariant.biome(variant) != -1 ? StructureVariant.biome(variant) : biome; |
| 172 | + |
| 173 | + MemorySegment structureSaltConfig = StructureSaltConfig.allocate(arena); |
| 174 | + if (Cubiomes.getStructureSaltConfig(structType, this.version, biome, structureSaltConfig) == 0) continue; |
| 175 | + |
| 176 | + MemorySegment pieces = Piece.allocateArray(StructureChecks.MAX_END_CITY_AND_FORTRESS_PIECES, arena); |
| 177 | + int numPieces = Cubiomes.getStructurePieces(pieces, StructureChecks.MAX_END_CITY_AND_FORTRESS_PIECES, structType, structureSaltConfig, variant, this.version, this.seed, posX, posZ); |
| 178 | + if (numPieces <= 0) continue; |
| 179 | + |
| 180 | + for (int pi = 0; pi < numPieces; pi++) { |
| 181 | + MemorySegment piece = Piece.asSlice(pieces, pi); |
| 182 | + int chestCount = Piece.chestCount(piece); |
| 183 | + if (chestCount == 0) continue; |
| 184 | + MemorySegment lootTables = Piece.lootTables(piece); |
| 185 | + MemorySegment lootSeeds = Piece.lootSeeds(piece); |
| 186 | + MemorySegment chestPoses = Piece.chestPoses(piece); |
| 187 | + for (int ci = 0; ci < chestCount; ci++) { |
| 188 | + MemorySegment lootTable = lootTables.getAtIndex(ValueLayout.ADDRESS, ci).reinterpret(Long.MAX_VALUE); |
| 189 | + String lootTableString = NativeAccess.readString(lootTable); |
| 190 | + MemorySegment lootTableContext = LootTableContext.allocate(arena); |
| 191 | + try { |
| 192 | + if (Cubiomes.init_loot_table_name(lootTableContext, lootTable, this.version) == 0) continue; |
| 193 | + long lootSeed = lootSeeds.getAtIndex(Cubiomes.C_LONG_LONG, ci); |
| 194 | + Cubiomes.set_loot_seed(lootTableContext, lootSeed); |
| 195 | + Cubiomes.generate_loot(lootTableContext); |
| 196 | + int lootCount = LootTableContext.generated_item_count(lootTableContext); |
| 197 | + JsonArrayBuilder items = new JsonArrayBuilder(); |
| 198 | + for (int li = 0; li < lootCount; li++) { |
| 199 | + MemorySegment itemStack = ItemStack.asSlice(LootTableContext.generated_items(lootTableContext), li); |
| 200 | + int itemId = Cubiomes.get_global_item_id(lootTableContext, ItemStack.item(itemStack)); |
| 201 | + int count = ItemStack.count(itemStack); |
| 202 | + String itemName = NativeAccess.readString(Cubiomes.global_id2item_name(itemId, this.version)); |
| 203 | + items.addObject(obj -> { |
| 204 | + obj.addProperty("id", itemName); |
| 205 | + obj.addProperty("count", count); |
| 206 | + }); |
| 207 | + } |
| 208 | + MemorySegment chestPosInternal = Pos.asSlice(chestPoses, ci); |
| 209 | + int chestX = Pos.x(chestPosInternal); |
| 210 | + int chestZ = Pos.z(chestPosInternal); |
| 211 | + root.addObject(r -> { |
| 212 | + r.addProperty("type", NativeAccess.readString(Cubiomes.struct2str(structType))); |
| 213 | + r.addProperty("x", chestX); |
| 214 | + r.addProperty("y", 0); |
| 215 | + r.addProperty("z", chestZ); |
| 216 | + r.addArray("items", items); |
| 217 | + }); |
| 218 | + } finally { |
| 219 | + Cubiomes.free_loot_table_pools(lootTableContext); |
| 220 | + } |
| 221 | + } |
| 222 | + } |
| 223 | + } |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + // write file |
| 228 | + try { |
| 229 | + Path lootDir = this.source.getClient().gameDirectory.toPath().resolve("SeedMapper").resolve("loot"); |
| 230 | + Files.createDirectories(lootDir); |
| 231 | + |
| 232 | + String serverId = "local"; |
| 233 | + try { |
| 234 | + if (this.source.getClient().getConnection() != null && this.source.getClient().getConnection().getConnection() != null) { |
| 235 | + java.net.SocketAddress remote = this.source.getClient().getConnection().getConnection().getRemoteAddress(); |
| 236 | + if (remote instanceof java.net.InetSocketAddress inet) { |
| 237 | + java.net.InetAddress addr = inet.getAddress(); |
| 238 | + if (addr != null) { |
| 239 | + serverId = addr.getHostAddress() + "_" + inet.getPort(); |
| 240 | + } else { |
| 241 | + serverId = inet.getHostString() + "_" + inet.getPort(); |
| 242 | + } |
| 243 | + } else if (remote != null) { |
| 244 | + serverId = remote.toString(); |
| 245 | + } |
| 246 | + } |
| 247 | + } catch (Exception ignored) { |
| 248 | + serverId = "local"; |
| 249 | + } |
| 250 | + |
| 251 | + // sanitize: allow alnum, dot, dash and underscore; collapse repeats and trim |
| 252 | + serverId = serverId.replaceAll("[^A-Za-z0-9._-]", "_"); |
| 253 | + serverId = serverId.replaceAll("_+", "_"); |
| 254 | + serverId = serverId.replaceAll("^[-_]+|[-_]+$", ""); |
| 255 | + if (serverId.isBlank()) serverId = "local"; |
| 256 | + |
| 257 | + String timestamp = EXPORT_TIMESTAMP.format(LocalDateTime.now()); |
| 258 | + String seedStr = Long.toString(this.seed); |
| 259 | + Path out = lootDir.resolve("%s_%s-%s.json".formatted(serverId, seedStr, timestamp)); |
| 260 | + String json = root.build(); |
| 261 | + Files.writeString(out, json, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); |
| 262 | + this.source.sendFeedback(net.minecraft.network.chat.Component.literal("Exported loot to " + out.toAbsolutePath())); |
| 263 | + } catch (IOException e) { |
| 264 | + this.source.sendError(net.minecraft.network.chat.Component.literal("Failed to export loot: " + e.getMessage())); |
| 265 | + } |
| 266 | + } |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + // Minimal JSON builders to avoid adding dependencies |
| 271 | + private static class JsonArrayBuilder { |
| 272 | + private final List<String> items = new ArrayList<>(); |
| 273 | + void addObject(java.util.function.Consumer<JsonObjectBuilder> c) { |
| 274 | + JsonObjectBuilder b = new JsonObjectBuilder(); |
| 275 | + c.accept(b); |
| 276 | + items.add(b.build()); |
| 277 | + } |
| 278 | + void addArray(String name, JsonArrayBuilder arr) { |
| 279 | + // not used at top level |
| 280 | + } |
| 281 | + String build() { |
| 282 | + return "[" + String.join(",", items) + "]"; |
| 283 | + } |
| 284 | + } |
| 285 | + |
| 286 | + private static class JsonObjectBuilder { |
| 287 | + private final List<String> parts = new ArrayList<>(); |
| 288 | + void addProperty(String key, String value) { |
| 289 | + parts.add(quote(key) + ":" + quote(value)); |
| 290 | + } |
| 291 | + void addProperty(String key, int value) { |
| 292 | + parts.add(quote(key) + ":" + value); |
| 293 | + } |
| 294 | + void addArray(String key, JsonArrayBuilder array) { |
| 295 | + parts.add(quote(key) + ":" + array.build()); |
| 296 | + } |
| 297 | + String build() { |
| 298 | + return "{" + String.join(",", parts) + "}"; |
| 299 | + } |
| 300 | + private static String quote(String s) { |
| 301 | + return '"' + s.replace("\\", "\\\\").replace("\"", "\\\"") + '"'; |
| 302 | + } |
| 303 | + } |
| 304 | +} |
0 commit comments