Skip to content

Commit d45fef8

Browse files
committed
Export Loot JSON
1 parent 6db6120 commit d45fef8

File tree

3 files changed

+428
-0
lines changed

3 files changed

+428
-0
lines changed

src/main/java/dev/xpple/seedmapper/SeedMapper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,7 @@ private static void registerCommands(CommandDispatcher<FabricClientCommandSource
117117
DiscordCommand.register(dispatcher);
118118
SampleCommand.register(dispatcher);
119119
WorldPresetCommand.register(dispatcher);
120+
// Export loot command
121+
dev.xpple.seedmapper.command.commands.ExportLootCommand.register(dispatcher);
120122
}
121123
}
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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

Comments
 (0)