XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

MyDimension

【Java】我的维度模组

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/MyDimension

修复部分问题

5c184b6
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

10 个文件 +853 -156
Added src/main/java/com/xfestudio/mydimension/builder/BuilderBlockCompatibility.java +53 -0
@@ -0,0 +1,53 @@
1 package com.xfestudio.mydimension.builder;
2
3 import net.minecraft.world.item.Item;
4 import net.minecraft.world.item.Items;
5 import net.minecraft.world.level.block.Block;
6 import net.minecraft.world.level.block.Blocks;
7 import net.minecraft.world.level.block.state.BlockState;
8
9 import java.util.List;
10 import java.util.Map;
11 import java.util.Set;
12
13 /**
14 * Explicit compatibility rules shared by surface discovery and construction material selection.
15 * Rules deliberately name concrete blocks/items rather than broad tags: a dirt-like mod block,
16 * mycelium or podzol must never become an implicit substitute merely because it shares a tag.
17 */
18 public final class BuilderBlockCompatibility {
19 private static final List<Rule> RULES = List.of(
20 new Rule(Set.of(Blocks.GRASS_BLOCK, Blocks.DIRT),
21 Map.of(Blocks.GRASS_BLOCK, List.of(Items.DIRT)))
22 );
23
24 private BuilderBlockCompatibility() {
25 }
26
27 /** Same-block matching with the small, explicit equivalence groups above. */
28 public static boolean sameSurfaceType(BlockState first, BlockState second) {
29 Block firstBlock = first.getBlock();
30 Block secondBlock = second.getBlock();
31 if (firstBlock == secondBlock) return true;
32 for (Rule rule : RULES) {
33 if (rule.surfaceBlocks.contains(firstBlock) && rule.surfaceBlocks.contains(secondBlock)) return true;
34 }
35 return false;
36 }
37
38 /** Ordered fallbacks after the target block's own item has been tried. */
39 public static List<Item> constructionFallbacks(BlockState target) {
40 for (Rule rule : RULES) {
41 List<Item> fallbacks = rule.fallbacks.get(target.getBlock());
42 if (fallbacks != null) return fallbacks;
43 }
44 return List.of();
45 }
46
47 private record Rule(Set<Block> surfaceBlocks, Map<Block, List<Item>> fallbacks) {
48 private Rule {
49 surfaceBlocks = Set.copyOf(surfaceBlocks);
50 fallbacks = Map.copyOf(fallbacks);
51 }
52 }
53 }
Modified src/main/java/com/xfestudio/mydimension/builder/BuilderGameTests.java +159 -0
@@ -1,31 +1,42 @@
1 1 package com.xfestudio.mydimension.builder;
2 2
3 import com.mojang.authlib.GameProfile;
3 4 import com.xfestudio.mydimension.MyDimension;
4 5 import com.xfestudio.mydimension.builder.anchor.AnchorContainerResolver;
6 import com.xfestudio.mydimension.builder.blueprint.BlueprintCapture;
5 7 import com.xfestudio.mydimension.builder.blueprint.BlueprintData;
6 8 import com.xfestudio.mydimension.builder.blueprint.BlueprintIo;
9 import com.xfestudio.mydimension.builder.blueprint.BlueprintPlacementPlan;
7 10 import com.xfestudio.mydimension.builder.blueprint.BlueprintSaveMode;
11 import com.xfestudio.mydimension.builder.blueprint.BlueprintTransform;
8 12 import com.xfestudio.mydimension.builder.history.BuilderTransaction;
9 13 import com.xfestudio.mydimension.builder.history.WorldDelta;
10 14 import net.minecraft.core.BlockPos;
11 15 import net.minecraft.core.Direction;
12 16 import net.minecraft.gametest.framework.GameTest;
13 17 import net.minecraft.gametest.framework.GameTestHelper;
18 import net.minecraft.server.level.ServerPlayer;
14 19 import net.minecraft.world.level.block.Blocks;
15 20 import net.minecraft.world.Container;
16 21 import net.minecraft.world.entity.item.ItemEntity;
17 22 import net.minecraft.world.item.ItemStack;
18 23 import net.minecraft.world.item.Items;
19 24 import net.minecraft.world.level.block.Block;
25 import net.minecraft.world.level.block.LayeredCauldronBlock;
20 26 import net.minecraft.world.level.block.state.BlockState;
21 27 import net.minecraft.world.level.block.state.properties.BlockStateProperties;
22 28 import net.minecraft.world.level.material.Fluids;
23 29 import net.minecraft.world.phys.AABB;
30 import net.minecraftforge.common.MinecraftForge;
31 import net.minecraftforge.event.level.BlockEvent;
32 import net.minecraftforge.eventbus.api.EventPriority;
24 33 import net.minecraftforge.gametest.GameTestHolder;
25 34 import net.minecraftforge.gametest.PrefixGameTestTemplate;
26 35
27 36 import java.util.List;
28 37 import java.util.UUID;
38 import java.util.concurrent.atomic.AtomicBoolean;
39 import java.util.function.Consumer;
29 40
30 41 @GameTestHolder(MyDimension.MOD_ID)
31 42 @PrefixGameTestTemplate(false)
@@ -45,6 +56,30 @@ public final class BuilderGameTests {
45 56 helper.succeed();
46 57 }
47 58
59 @GameTest(template = "empty")
60 public static void grassAndDirtShareOnlyTheirExplicitSurfaceGroup(GameTestHelper helper) {
61 BlockPos grass = helper.absolutePos(new BlockPos(2, 2, 2));
62 BlockPos dirt = grass.east();
63 BlockPos podzol = grass.west();
64 BlockPos mycelium = grass.north();
65 helper.getLevel().setBlock(grass, Blocks.GRASS_BLOCK.defaultBlockState(), Block.UPDATE_ALL);
66 helper.getLevel().setBlock(dirt, Blocks.DIRT.defaultBlockState(), Block.UPDATE_ALL);
67 helper.getLevel().setBlock(podzol, Blocks.PODZOL.defaultBlockState(), Block.UPDATE_ALL);
68 helper.getLevel().setBlock(mycelium, Blocks.MYCELIUM.defaultBlockState(), Block.UPDATE_ALL);
69
70 SurfacePlanner.Plan plan = SurfacePlanner.plan(helper.getLevel(), grass, Direction.UP,
71 BuilderMode.DEMOLISH, SurfaceMatchMode.SAME_BLOCK, 16, null);
72
73 helper.assertTrue(plan.candidates().size() == 2
74 && plan.candidates().stream().anyMatch(candidate -> candidate.reference().equals(grass))
75 && plan.candidates().stream().anyMatch(candidate -> candidate.reference().equals(dirt)),
76 "Same-block traversal did not treat grass and ordinary dirt as one surface type");
77 helper.assertTrue(plan.candidates().stream().noneMatch(candidate ->
78 candidate.reference().equals(podzol) || candidate.reference().equals(mycelium)),
79 "The explicit grass/dirt surface group accidentally included another dirt-like block");
80 helper.succeed();
81 }
82
48 83 @GameTest(template = "empty")
49 84 public static void verticalWallUsesOnlyExposedSelectedFace(GameTestHelper helper) {
50 85 BlockPos lower = helper.absolutePos(new BlockPos(2, 2, 3));
@@ -137,6 +172,130 @@ public final class BuilderGameTests {
137 172 }
138 173 }
139 174
175 @GameTest(template = "empty")
176 public static void blueprintPreservesWaterCauldronLevel(GameTestHelper helper) {
177 BlockPos sourcePosition = helper.absolutePos(new BlockPos(1, 1, 1));
178 BlockPos targetPosition = helper.absolutePos(new BlockPos(3, 1, 1));
179 BlockPos protectedPosition = helper.absolutePos(new BlockPos(5, 1, 1));
180 BlockState sourceState = Blocks.WATER_CAULDRON.defaultBlockState()
181 .setValue(LayeredCauldronBlock.LEVEL, 3);
182 helper.getLevel().setBlock(sourcePosition, sourceState, Block.UPDATE_ALL);
183 ServerPlayer player = new ServerPlayer(helper.getLevel().getServer(), helper.getLevel(),
184 new GameProfile(UUID.randomUUID(), "blueprint-test-player"));
185 player.getInventory().setItem(9, new ItemStack(Items.CAULDRON, 2));
186 AtomicBoolean placementEventObserved = new AtomicBoolean();
187 Consumer<BlockEvent.EntityPlaceEvent> listener = event -> {
188 if (event.getEntity() != player) return;
189 if (event.getPos().equals(targetPosition)) placementEventObserved.set(true);
190 if (event.getPos().equals(protectedPosition)) event.setCanceled(true);
191 };
192
193 try {
194 MinecraftForge.EVENT_BUS.addListener(EventPriority.NORMAL, false,
195 BlockEvent.EntityPlaceEvent.class, listener);
196 BlueprintData captured = BlueprintCapture.capture(helper.getLevel(), player,
197 sourcePosition, sourcePosition, BlueprintSaveMode.BLOCKS_ONLY,
198 "Water cauldron state");
199 BlueprintData decoded = BlueprintIo.decode(BlueprintIo.encode(captured));
200 BlockState decodedState = decoded.state(decoded.blocks().get(0));
201 helper.assertTrue(decodedState.equals(sourceState),
202 "Blueprint capture/transfer changed the water-cauldron level");
203
204 ItemStack cost = BuilderOperationManager.constructionCost(decodedState);
205 helper.assertTrue(cost.is(Items.CAULDRON) && cost.getCount() == 1,
206 "A water cauldron must consume one base cauldron item");
207
208 BlueprintPlacementPlan plan = BlueprintPlacementPlan.create(decoded,
209 BlueprintTransform.NONE, targetPosition);
210 BuilderOperationManager.BlueprintBatchResult result =
211 BuilderOperationManager.executeBlueprintBatch(player, ItemStack.EMPTY,
212 plan.blocks(), UUID.randomUUID(), false);
213 helper.assertTrue(result.changed() == 1 && result.blocked() == 0
214 && result.missing().isEmpty() && result.committed(),
215 "Blueprint execution rejected a legal water-cauldron state");
216 helper.assertTrue(helper.getLevel().getBlockState(targetPosition).equals(sourceState),
217 "Blueprint placement did not retain the exact water-cauldron level");
218 helper.assertTrue(player.getInventory().getItem(9).is(Items.CAULDRON)
219 && player.getInventory().getItem(9).getCount() == 1,
220 "Blueprint placement did not debit exactly one base cauldron item");
221 helper.assertTrue(placementEventObserved.get(),
222 "Blueprint placement bypassed the Forge entity-place event");
223
224 BlueprintPlacementPlan protectedPlan = BlueprintPlacementPlan.create(decoded,
225 BlueprintTransform.NONE, protectedPosition);
226 BuilderOperationManager.BlueprintBatchResult protectedResult =
227 BuilderOperationManager.executeBlueprintBatch(player, ItemStack.EMPTY,
228 protectedPlan.blocks(), UUID.randomUUID(), false);
229 helper.assertTrue(protectedResult.changed() == 0 && protectedResult.blocked() == 1
230 && helper.getLevel().getBlockState(protectedPosition).isAir(),
231 "A cancelled Forge placement event did not protect the blueprint target");
232 helper.assertTrue(player.getInventory().getItem(9).is(Items.CAULDRON)
233 && player.getInventory().getItem(9).getCount() == 1,
234 "A cancelled Forge placement event did not refund its reserved material");
235 helper.succeed();
236 } catch (Exception exception) {
237 helper.fail(exception.getMessage());
238 } finally {
239 MinecraftForge.EVENT_BUS.unregister(listener);
240 }
241 }
242
243 @GameTest(template = "empty")
244 public static void grassConstructionPrefersGrassThenFallsBackToDirt(GameTestHelper helper) {
245 BlockPos exactTarget = helper.absolutePos(new BlockPos(1, 1, 1));
246 BlockPos fallbackTarget = helper.absolutePos(new BlockPos(3, 1, 1));
247 BlockPos disallowedTarget = helper.absolutePos(new BlockPos(5, 1, 1));
248 BlockState grass = Blocks.GRASS_BLOCK.defaultBlockState();
249 // The final coordinate may sit just outside a small GameTest template's structure-void
250 // envelope, so make every material-policy target deterministic instead of inheriting the
251 // flat test world's grass surface.
252 helper.getLevel().setBlock(exactTarget, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
253 helper.getLevel().setBlock(fallbackTarget, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
254 helper.getLevel().setBlock(disallowedTarget, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
255 ServerPlayer player = new ServerPlayer(helper.getLevel().getServer(), helper.getLevel(),
256 new GameProfile(UUID.randomUUID(), "grass-material-test-player"));
257 player.getInventory().setItem(9, new ItemStack(Items.GRASS_BLOCK));
258 player.getInventory().setItem(10, new ItemStack(Items.DIRT));
259
260 BuilderOperationManager.BlueprintBatchResult exact = BuilderOperationManager.executeBlueprintBatch(
261 player, ItemStack.EMPTY,
262 List.of(new BlueprintPlacementPlan.PlannedBlock(
263 BlockPos.ZERO, exactTarget, grass, null)), UUID.randomUUID(), false);
264 helper.assertTrue(exact.changed() == 1 && exact.missing().isEmpty()
265 && helper.getLevel().getBlockState(exactTarget).equals(grass),
266 "Grass construction did not consume its exact grass-block material first");
267 helper.assertTrue(player.getInventory().getItem(9).isEmpty()
268 && player.getInventory().getItem(10).is(Items.DIRT),
269 "Grass construction consumed dirt while an exact grass block was available");
270
271 BuilderOperationManager.BlueprintBatchResult fallback = BuilderOperationManager.executeBlueprintBatch(
272 player, ItemStack.EMPTY,
273 List.of(new BlueprintPlacementPlan.PlannedBlock(
274 BlockPos.ZERO, fallbackTarget, grass, null)), UUID.randomUUID(), false);
275 helper.assertTrue(fallback.changed() == 1 && fallback.missing().isEmpty()
276 && helper.getLevel().getBlockState(fallbackTarget).equals(grass),
277 "Grass construction reported missing material instead of falling back to ordinary dirt");
278 helper.assertTrue(player.getInventory().getItem(10).isEmpty(),
279 "Grass construction did not debit its dirt fallback");
280
281 player.getInventory().setItem(11, new ItemStack(Items.PODZOL));
282 BuilderOperationManager.BlueprintBatchResult disallowed = BuilderOperationManager.executeBlueprintBatch(
283 player, ItemStack.EMPTY,
284 List.of(new BlueprintPlacementPlan.PlannedBlock(
285 BlockPos.ZERO, disallowedTarget, grass, null)), UUID.randomUUID(), false);
286 helper.assertTrue(disallowed.changed() == 0,
287 "Podzol-only supply unexpectedly placed " + disallowed.changed() + " grass blocks");
288 helper.assertTrue(disallowed.missing().size() == 1,
289 "Podzol-only supply produced " + disallowed.missing().size()
290 + " missing entries instead of one");
291 helper.assertTrue(helper.getLevel().getBlockState(disallowedTarget).isAir(),
292 "Podzol-only supply changed the missing target to "
293 + helper.getLevel().getBlockState(disallowedTarget));
294 helper.assertTrue(player.getInventory().getItem(11).is(Items.PODZOL),
295 "A dirt-like block outside the explicit rule was consumed as a grass substitute");
296 helper.succeed();
297 }
298
140 299 @GameTest(template = "empty")
141 300 public static void dropFreeRemovalSuppressesContainerContentsAndRestoresFluid(GameTestHelper helper) {
142 301 BlockPos chest = helper.absolutePos(new BlockPos(1, 1, 1));
Modified src/main/java/com/xfestudio/mydimension/builder/BuilderOperationManager.java +99 -43
@@ -13,15 +13,12 @@ import net.minecraft.sounds.SoundSource;
13 13 import net.minecraft.tags.BlockTags;
14 14 import net.minecraft.world.InteractionHand;
15 15 import net.minecraft.world.item.BlockItem;
16 import net.minecraft.world.item.Item;
16 17 import net.minecraft.world.item.ItemStack;
17 18 import net.minecraft.world.item.context.BlockPlaceContext;
18 19 import net.minecraft.world.item.context.UseOnContext;
19 20 import net.minecraft.world.level.block.Block;
20 import net.minecraft.world.level.block.BeehiveBlock;
21 21 import net.minecraft.world.level.block.CandleBlock;
22 import net.minecraft.world.level.block.ComposterBlock;
23 import net.minecraft.world.level.block.LayeredCauldronBlock;
24 import net.minecraft.world.level.block.RespawnAnchorBlock;
25 22 import net.minecraft.world.level.block.SeaPickleBlock;
26 23 import net.minecraft.world.level.block.SlabBlock;
27 24 import net.minecraft.world.level.block.SnowLayerBlock;
@@ -309,23 +306,24 @@ public final class BuilderOperationManager {
309 306 List<SupplyPool> pools = new ArrayList<>();
310 307 if (!free) {
311 308 for (BuildAttempt attempt : attempts) {
312 SupplyPool pool = findPool(pools, attempt.cost);
309 List<Item> fallbacks = BuilderBlockCompatibility.constructionFallbacks(attempt.desired);
310 SupplyPool pool = findPool(pools, attempt.cost, fallbacks);
313 311 if (pool == null) {
314 pool = new SupplyPool(attempt.cost.copyWithCount(1));
312 pool = new SupplyPool(attempt.cost.copyWithCount(1), fallbacks);
315 313 pools.add(pool);
316 314 }
317 315 pool.requested += attempt.cost.getCount();
318 316 }
317 // Reserve every target's exact item before requesting any substitution. Thus ordinary dirt
318 // targets keep first claim on dirt, while grass targets consume grass blocks whenever present.
319 319 for (SupplyPool pool : pools) {
320 try {
321 BuilderMaterials.Extraction extraction = BuilderMaterials.extract(player, scepter,
322 pool.template, pool.requested);
323 pool.available = extraction.count();
324 pool.extracted = pool.available;
325 } catch (Throwable throwable) {
326 pool.available = 0;
327 MyDimension.LOGGER.warn("Builder material extraction failed; this item is left pending",
328 throwable);
320 extractSupply(player, scepter, pool, pool.template, pool.requested);
321 }
322 for (SupplyPool pool : pools) {
323 for (Item fallback : pool.fallbacks) {
324 int missing = pool.requested - pool.available();
325 if (missing <= 0) break;
326 extractSupply(player, scepter, pool, new ItemStack(fallback), missing);
329 327 }
330 328 }
331 329 }
@@ -336,7 +334,7 @@ public final class BuilderOperationManager {
336 334 stabilizeBuildBatch(level, result.successfulBuildPositions);
337 335 if (captureHistory && !captureStableBuildHistory(level, batchBeforeImages, result)) {
338 336 restoreSnapshots(level, batchBeforeImages);
339 for (SupplyPool pool : pools) pool.available = pool.extracted;
337 for (SupplyPool pool : pools) pool.resetAvailable();
340 338 result.changed.clear();
341 339 result.missing.clear();
342 340 result.successfulBuildPositions.clear();
@@ -344,16 +342,28 @@ public final class BuilderOperationManager {
344 342 result.blocked += result.changedCount;
345 343 result.changedCount = 0;
346 344 }
345 List<ItemStack> unusedSupplies = new ArrayList<>();
347 346 for (SupplyPool pool : pools) {
348 if (pool.available > 0) {
349 dropOverflow(player, transactionId, BuilderMaterials.insert(player, scepter,
350 List.of(pool.template.copyWithCount(pool.available))));
351 }
347 unusedSupplies.addAll(pool.unusedStacks());
348 }
349 if (!unusedSupplies.isEmpty()) {
350 dropOverflow(player, transactionId, BuilderMaterials.insert(player, scepter, unusedSupplies));
352 351 }
353 352 result.offhandAfter = player.getOffhandItem().copy();
354 353 return result;
355 354 }
356 355
356 private static void extractSupply(ServerPlayer player, ItemStack scepter, SupplyPool pool,
357 ItemStack requested, int amount) {
358 if (amount <= 0 || requested.isEmpty()) return;
359 try {
360 BuilderMaterials.Extraction extraction = BuilderMaterials.extract(player, scepter, requested, amount);
361 pool.addReserve(requested, extraction.count());
362 } catch (Throwable throwable) {
363 MyDimension.LOGGER.warn("Builder material extraction failed; this item is left pending", throwable);
364 }
365 }
366
357 367 private static ItemStack placementStack(ItemStack offhand, BlockState desired, ItemStack cost) {
358 368 if (offhand.getItem() instanceof BlockItem blockItem
359 369 && blockItem.getBlock() == desired.getBlock()) {
@@ -525,7 +535,8 @@ public final class BuilderOperationManager {
525 535 ServerLevel level = player.serverLevel();
526 536 BlockPos pos = attempt.candidate.target();
527 537 BlockState desired = attempt.desired;
528 SupplyPool pool = free ? null : findPool(pools, attempt.cost);
538 SupplyPool pool = free ? null : findPool(pools, attempt.cost,
539 BuilderBlockCompatibility.constructionFallbacks(desired));
529 540 int costCount = attempt.cost.getCount();
530 541 try {
531 542 BlockState existing = level.getBlockState(pos);
@@ -545,7 +556,7 @@ public final class BuilderOperationManager {
545 556 result.blocked++;
546 557 return true;
547 558 }
548 if (pool != null && pool.available < costCount) {
559 if (pool != null && pool.available() < costCount) {
549 560 result.missing.add(new PendingBuildData.Entry(pos, desired, attempt.blockEntityTag));
550 561 return true;
551 562 }
@@ -572,8 +583,8 @@ public final class BuilderOperationManager {
572 583 placed = false;
573 584 }
574 585 }
575 if (placed && attempt.placementStack.getItem() instanceof BlockItem blockItem
576 && blockItem.getBlock() == desired.getBlock()) {
586 if (placed && !attempt.placementStack.isEmpty()
587 && attempt.placementStack.is(desired.getBlock().asItem())) {
577 588 BlockState placedState = level.getBlockState(pos);
578 589 placedState.getBlock().setPlacedBy(level, pos, placedState, player,
579 590 attempt.placementStack.copy());
@@ -601,9 +612,9 @@ public final class BuilderOperationManager {
601 612 return true;
602 613 }
603 614 if (pool != null) {
604 pool.available -= costCount;
615 List<ItemStack> consumed = pool.consume(costCount);
605 616 if (result.captureHistory) {
606 addLedgerStack(result.debits, pool.template.copyWithCount(costCount));
617 consumed.forEach(stack -> addLedgerStack(result.debits, stack));
607 618 }
608 619 }
609 620 BlockState successfulState = level.getBlockState(pos);
@@ -944,22 +955,13 @@ public final class BuilderOperationManager {
944 955 }
945 956
946 957 /**
947 * Returns the exact item debit for one state, or an empty stack when a state cannot be reproduced
948 * without inventing hidden fluid/content. This closes imported-blueprint shortcuts such as one item
949 * becoming a double slab, eight snow layers, a full composter or a charged respawn anchor.
958 * Returns the base block-item debit for a state. State properties are part of the blueprint payload,
959 * not separate materials: for example, every water-cauldron level maps through vanilla's block/item
960 * alias to one cauldron item, and a waterlogged slab still maps to its slab item. Component-count
961 * properties remain additive because those states physically represent more than one placed item.
950 962 */
951 private static ItemStack constructionCost(BlockState state) {
952 if (!state.getFluidState().isEmpty()) return ItemStack.EMPTY;
963 static ItemStack constructionCost(BlockState state) {
953 964 Block block = state.getBlock();
954 if (block instanceof ComposterBlock && state.getValue(ComposterBlock.LEVEL) > 0) return ItemStack.EMPTY;
955 if (block instanceof BeehiveBlock && state.getValue(BeehiveBlock.HONEY_LEVEL) > 0) return ItemStack.EMPTY;
956 if (block instanceof RespawnAnchorBlock && state.getValue(RespawnAnchorBlock.CHARGE) > 0) {
957 return ItemStack.EMPTY;
958 }
959 if (block instanceof LayeredCauldronBlock && state.getValue(LayeredCauldronBlock.LEVEL) > 0) {
960 return ItemStack.EMPTY;
961 }
962
963 965 ItemStack cost = new ItemStack(block.asItem());
964 966 if (cost.isEmpty()) return ItemStack.EMPTY;
965 967 int count = 1;
@@ -1373,18 +1375,72 @@ public final class BuilderOperationManager {
1373 1375
1374 1376 private static final class SupplyPool {
1375 1377 private final ItemStack template;
1378 private final List<Item> fallbacks;
1379 private final List<SupplyReserve> reserves = new ArrayList<>();
1376 1380 private int requested;
1381
1382 private SupplyPool(ItemStack template, List<Item> fallbacks) {
1383 this.template = template;
1384 this.fallbacks = List.copyOf(fallbacks);
1385 }
1386
1387 private void addReserve(ItemStack material, int count) {
1388 if (count <= 0 || material.isEmpty()) return;
1389 for (SupplyReserve reserve : reserves) {
1390 if (!ItemStack.isSameItemSameTags(reserve.template, material)) continue;
1391 reserve.available += count;
1392 reserve.extracted += count;
1393 return;
1394 }
1395 reserves.add(new SupplyReserve(material.copyWithCount(1), count));
1396 }
1397
1398 private int available() {
1399 return reserves.stream().mapToInt(reserve -> reserve.available).sum();
1400 }
1401
1402 /** Exact material is stored first, followed by fallbacks in policy order. */
1403 private List<ItemStack> consume(int count) {
1404 if (count <= 0 || available() < count) return List.of();
1405 int remaining = count;
1406 List<ItemStack> consumed = new ArrayList<>();
1407 for (SupplyReserve reserve : reserves) {
1408 if (remaining <= 0) break;
1409 int taken = Math.min(remaining, reserve.available);
1410 if (taken <= 0) continue;
1411 reserve.available -= taken;
1412 consumed.add(reserve.template.copyWithCount(taken));
1413 remaining -= taken;
1414 }
1415 return List.copyOf(consumed);
1416 }
1417
1418 private void resetAvailable() {
1419 reserves.forEach(reserve -> reserve.available = reserve.extracted);
1420 }
1421
1422 private List<ItemStack> unusedStacks() {
1423 return reserves.stream().filter(reserve -> reserve.available > 0)
1424 .map(reserve -> reserve.template.copyWithCount(reserve.available)).toList();
1425 }
1426 }
1427
1428 private static final class SupplyReserve {
1429 private final ItemStack template;
1377 1430 private int available;
1378 1431 private int extracted;
1379 1432
1380 private SupplyPool(ItemStack template) {
1433 private SupplyReserve(ItemStack template, int extracted) {
1381 1434 this.template = template;
1435 this.available = extracted;
1436 this.extracted = extracted;
1382 1437 }
1383 1438 }
1384 1439
1385 private static SupplyPool findPool(List<SupplyPool> pools, ItemStack stack) {
1440 private static SupplyPool findPool(List<SupplyPool> pools, ItemStack stack, List<Item> fallbacks) {
1386 1441 for (SupplyPool pool : pools) {
1387 if (ItemStack.isSameItemSameTags(pool.template, stack)) return pool;
1442 if (ItemStack.isSameItemSameTags(pool.template, stack)
1443 && pool.fallbacks.equals(fallbacks)) return pool;
1388 1444 }
1389 1445 return null;
1390 1446 }
Modified src/main/java/com/xfestudio/mydimension/builder/SurfacePlanner.java +2 -1
@@ -67,7 +67,8 @@ public final class SurfacePlanner {
67 67 if (!SurfacePlaneTraversal.isReference(state)) {
68 68 return false;
69 69 }
70 return mode == SurfaceMatchMode.ANY_BLOCK || state.getBlock() == seed.getBlock();
70 return mode == SurfaceMatchMode.ANY_BLOCK
71 || BuilderBlockCompatibility.sameSurfaceType(seed, state);
71 72 }
72 73
73 74 public record Candidate(BlockPos reference, BlockPos target, BlockState desiredState, int distance) {
Modified src/main/java/com/xfestudio/mydimension/client/builder/BuilderPreviewRenderer.java +10 -3
@@ -16,6 +16,7 @@ import net.minecraftforge.client.event.RenderLevelStageEvent;
16 16
17 17 import javax.annotation.Nullable;
18 18 import java.util.List;
19 import java.util.Set;
19 20 import java.util.function.Function;
20 21
21 22 /** Renders cached projection models, rift veils, and shader-safe solid outlines. */
@@ -61,6 +62,7 @@ public final class BuilderPreviewRenderer {
61 62
62 63 public static void render(RenderLevelStageEvent event) {
63 64 if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) return;
65 RenderSystem.assertOnRenderThread();
64 66 Minecraft minecraft = Minecraft.getInstance();
65 67 if (!BuilderClientServices.isHoldingRealmwright(minecraft) || minecraft.level == null) {
66 68 SECTION_CACHE.clear();
@@ -95,12 +97,16 @@ public final class BuilderPreviewRenderer {
95 97 (double) FULL_MODEL_DISTANCE * FULL_MODEL_DISTANCE,
96 98 SECTION_UPLOADS_PER_FRAME, uploadDeadline);
97 99 BuilderPreviewState.Snapshot renderedSnapshot = SECTION_CACHE.renderSnapshot(snapshot);
100 BuilderPreviewSectionMeshCache.ModelResidency modelResidency =
101 SECTION_CACHE.modelResidency(camera,
102 (double) FULL_MODEL_DISTANCE * FULL_MODEL_DISTANCE);
98 103 List<BuilderPreviewSectionMeshCache.SectionMesh> visibleSections = renderedSnapshot == null
99 104 ? List.of() : SECTION_CACHE.visibleSections(
100 105 event.getFrustum(), camera, previewDistanceSqr);
101 SECTION_CACHE.prepareVisibleSections(minecraft, visibleSections, camera,
102 (double) FULL_MODEL_DISTANCE * FULL_MODEL_DISTANCE,
106 SECTION_CACHE.prepareVisibleSections(minecraft, modelResidency, camera,
103 107 Math.max(0, SECTION_UPLOADS_PER_FRAME - stagedUploads), uploadDeadline);
108 Set<BuilderPreviewSectionMeshCache.SectionKey> drawableGhostSections =
109 modelResidency.drawableGhostSections();
104 110 BuilderPreviewState.Focus focus = BuilderPreviewState.get().focus();
105 111 if (focus != null && focus.kind() != BuilderPreviewState.FocusKind.CANDIDATE
106 112 && !SECTION_CACHE.represents(snapshot)) {
@@ -126,7 +132,8 @@ public final class BuilderPreviewRenderer {
126 132 try {
127 133 drawCachedSections(poseStack, event, visibleSections,
128 134 BuilderPreviewRenderTypes.ghostModel(),
129 BuilderPreviewSectionMeshCache.SectionMesh::ghostBuffers, true, 1.0F);
135 section -> section.ghostBuffers(drawableGhostSections),
136 true, 1.0F);
130 137 BuilderPreviewRenderTypes.updateWaveUniforms();
131 138 drawCachedSections(poseStack, event, visibleSections,
132 139 BuilderPreviewRenderTypes.projectionWave(),
Modified src/main/java/com/xfestudio/mydimension/client/builder/BuilderPreviewSectionMeshCache.java +365 -48
@@ -74,6 +74,7 @@ final class BuilderPreviewSectionMeshCache {
74 74 @Nullable
75 75 private BuilderPreviewState.Snapshot source;
76 76 private Map<SectionKey, SectionMesh> sections = new LinkedHashMap<>();
77 private final Set<SectionMesh> activeGhostUploads = identitySet();
77 78 @Nullable
78 79 private PendingGeneration pending;
79 80 /**
@@ -101,6 +102,22 @@ final class BuilderPreviewSectionMeshCache {
101 102 return visible;
102 103 }
103 104
105 /**
106 * Computes the concrete-model working set once for an active frame. Sections touching the
107 * distance-limited seed set across an occupied projection boundary form a one-section guard
108 * band. Preparation and rendering consume this same immutable result, so neither side can
109 * make a subtly different decision at the distance boundary.
110 */
111 synchronized ModelResidency modelResidency(Vec3 camera, double modelDistanceSqr) {
112 Set<SectionKey> keys = modelResidentSectionKeys(sections, camera, modelDistanceSqr);
113 Set<SectionMesh> meshes = identitySet();
114 for (SectionKey key : keys) {
115 SectionMesh section = sections.get(key);
116 if (section != null) meshes.add(section);
117 }
118 return new ModelResidency(keys, meshes);
119 }
120
104 121 /**
105 122 * Builds section back buffers without exposing partial VBOs. Sections whose shared boundary
106 123 * changed are published as one dependency group; unrelated sections still publish
@@ -114,7 +131,10 @@ final class BuilderPreviewSectionMeshCache {
114 131 PendingGeneration generation = pending;
115 132 if (generation == null) return 0;
116 133
117 promoteReadyGroups(generation, camera, modelDistanceSqr);
134 Set<SectionKey> modelResidency = modelResidentSectionKeys(
135 generation.sections(), camera, modelDistanceSqr);
136 generation.initializePublicationGroups(modelResidency);
137 promoteReadyGroups(generation, modelResidency);
118 138 if (generation.fullyPublished()) {
119 139 finishGeneration(generation);
120 140 return 0;
@@ -133,11 +153,12 @@ final class BuilderPreviewSectionMeshCache {
133 153 || generation.isPublished(entry.getKey())) continue;
134 154 SectionMesh section = entry.getValue();
135 155 boolean requireModels = requireGhostCompletion(
136 section.closestDistanceToSqr(camera) <= modelDistanceSqr,
156 modelResidency.contains(entry.getKey()),
137 157 section.ghostUploadStarted());
138 158 if (section.ready(requireModels)) continue;
139 if (section.uploadNext(minecraft, requireModels)) {
140 uploaded++;
159 int uploadCost = section.uploadNext(minecraft, requireModels);
160 if (uploadCost > 0) {
161 uploaded += uploadCost;
141 162 progressed = true;
142 163 // Spend the next slot on this same nearest section. It can therefore publish
143 164 // in ceil(sectionUploads / frameBudget) frames instead of waiting behind every
@@ -149,7 +170,7 @@ final class BuilderPreviewSectionMeshCache {
149 170 }
150 171
151 172 if (pending == generation) {
152 promoteReadyGroups(generation, camera, modelDistanceSqr);
173 promoteReadyGroups(generation, modelResidency);
153 174 if (generation.fullyPublished()) {
154 175 finishGeneration(generation);
155 176 }
@@ -157,17 +178,34 @@ final class BuilderPreviewSectionMeshCache {
157 178 return uploaded;
158 179 }
159 180
160 /** Uploads one dirty section, then continues only while both budgets allow. */
161 synchronized void prepareVisibleSections(Minecraft minecraft, List<SectionMesh> visible,
162 Vec3 camera, double modelDistanceSqr,
163 int uploadBudget, long deadlineNanos) {
181 /**
182 * Uploads active concrete models selected by {@code residency}. A section whose model upload
183 * already started remains in the work queue after leaving residency so it cannot become a
184 * permanently partial VBO. Rendering still filters it through the original residency set.
185 */
186 synchronized void prepareVisibleSections(Minecraft minecraft, ModelResidency residency,
187 Vec3 camera, int uploadBudget,
188 long deadlineNanos) {
164 189 if (uploadBudget <= 0) return;
190 Set<SectionMesh> candidates = identitySet();
191 for (SectionMesh section : residency.meshes()) {
192 if (!section.ghostModelsComplete()) candidates.add(section);
193 }
194 candidates.addAll(activeGhostUploads);
195 List<SectionMesh> work = new ArrayList<>(candidates);
196 work.sort(Comparator.comparingDouble(section -> section.closestDistanceToSqr(camera)));
165 197 int uploaded = 0;
166 for (SectionMesh section : visible) {
198 for (SectionMesh section : work) {
167 199 if (uploaded >= uploadBudget) break;
168 200 if (uploaded > 0 && System.nanoTime() >= deadlineNanos) break;
169 boolean buildModels = section.closestDistanceToSqr(camera) <= modelDistanceSqr;
170 if (section.uploadNext(minecraft, buildModels)) uploaded++;
201 boolean buildModels = requireGhostCompletion(
202 residency.contains(section), section.ghostUploadStarted());
203 int uploadCost = section.uploadNext(minecraft, buildModels);
204 if (uploadCost > 0) {
205 uploaded += uploadCost;
206 if (section.ghostUploadInProgress()) activeGhostUploads.add(section);
207 else activeGhostUploads.remove(section);
208 }
171 209 }
172 210 }
173 211
@@ -193,6 +231,7 @@ final class BuilderPreviewSectionMeshCache {
193 231 }
194 232 source = null;
195 233 sections.clear();
234 activeGhostUploads.clear();
196 235 pending = null;
197 236 queued.clear();
198 237 }
@@ -221,6 +260,7 @@ final class BuilderPreviewSectionMeshCache {
221 260 queued.clear();
222 261 sections.values().forEach(SectionMesh::close);
223 262 sections = new LinkedHashMap<>();
263 activeGhostUploads.clear();
224 264 source = snapshot;
225 265 return;
226 266 }
@@ -233,7 +273,8 @@ final class BuilderPreviewSectionMeshCache {
233 273 queued.clear();
234 274 return;
235 275 }
236 if (source != null && sameGeometry(source, snapshot) && !pending.mutatedActive) {
276 if (source != null && canFastRevertToSource(
277 sameGeometry(source, snapshot), pending.mutatedActive)) {
237 278 // No staged section has been published yet, so returning to the committed frame
238 279 // can safely cancel the transition without generating anything else.
239 280 discardPending();
@@ -300,14 +341,15 @@ final class BuilderPreviewSectionMeshCache {
300 341 }
301 342 }
302 343 Set<SectionKey> changed = changedSectionKeys(sections, replacement);
303 Set<SectionBoundary> dependencies = source == null || sections.isEmpty()
304 ? Set.of() : changedBoundaryDependencies(changed, sections, replacement);
344 boolean initialGeneration = source == null || sections.isEmpty();
345 Set<SectionBoundary> dependencies = changedBoundaryDependencies(
346 changed, sections, replacement);
305 347 pending = new PendingGeneration(snapshot, replacement, changed,
306 connectedPublicationGroups(changed, dependencies));
348 dependencies, initialGeneration);
307 349 }
308 350
309 private void promoteReadyGroups(PendingGeneration generation, Vec3 camera,
310 double modelDistanceSqr) {
351 private void promoteReadyGroups(PendingGeneration generation,
352 Set<SectionKey> modelResidency) {
311 353 if (pending != generation) return;
312 354 for (Set<SectionKey> group : generation.publicationGroups()) {
313 355 if (generation.isPublished(group)) continue;
@@ -315,7 +357,7 @@ final class BuilderPreviewSectionMeshCache {
315 357 SectionMesh section = generation.sections().get(key);
316 358 if (section == null) return true; // Removed sections need no back buffer.
317 359 boolean requireModels = requireGhostCompletion(
318 section.closestDistanceToSqr(camera) <= modelDistanceSqr,
360 modelResidency.contains(key),
319 361 section.ghostUploadStarted());
320 362 return section.ready(requireModels);
321 363 };
@@ -331,10 +373,14 @@ final class BuilderPreviewSectionMeshCache {
331 373 SectionMesh replacement = generation.sections().get(key);
332 374 SectionMesh old = replacement == null
333 375 ? sections.remove(key) : sections.put(key, replacement);
376 if (sectionMapEntryChanged(old, replacement)) generation.mutatedActive = true;
377 if (old != null && old != replacement) activeGhostUploads.remove(old);
378 if (replacement != null && replacement.ghostUploadInProgress()) {
379 activeGhostUploads.add(replacement);
380 }
334 381 if (old != null && old != replacement) replaced.add(old);
335 382 }
336 383 generation.markPublished(group);
337 if (!replaced.isEmpty()) generation.mutatedActive = true;
338 384
339 385 // A mesh may be shared with the target map when its cells did not change. Never close a
340 386 // buffer that is still reachable after the whole dependency group has been installed.
@@ -345,6 +391,16 @@ final class BuilderPreviewSectionMeshCache {
345 391 });
346 392 }
347 393
394 /** Identity, rather than value equality, defines whether the active VBO map was mutated. */
395 static boolean sectionMapEntryChanged(@Nullable Object before, @Nullable Object after) {
396 return before != after;
397 }
398
399 static boolean canFastRevertToSource(boolean committedGeometryMatches,
400 boolean activeMapMutated) {
401 return committedGeometryMatches && !activeMapMutated;
402 }
403
348 404 private void finishGeneration(PendingGeneration generation) {
349 405 if (pending != generation) return;
350 406 // Rebuild only the small map shell to restore deterministic section order. Every changed
@@ -366,6 +422,7 @@ final class BuilderPreviewSectionMeshCache {
366 422 } else if (latest.cells().isEmpty()) {
367 423 sections.values().forEach(SectionMesh::close);
368 424 sections = new LinkedHashMap<>();
425 activeGhostUploads.clear();
369 426 source = latest;
370 427 } else {
371 428 stageSnapshot(latest);
@@ -401,6 +458,47 @@ final class BuilderPreviewSectionMeshCache {
401 458 if (closed.add(mesh)) mesh.close();
402 459 }
403 460
461 private static Set<SectionKey> modelResidentSectionKeys(
462 Map<SectionKey, SectionMesh> candidates,
463 Vec3 camera, double modelDistanceSqr) {
464 Set<SectionKey> seeds = new LinkedHashSet<>();
465 for (Map.Entry<SectionKey, SectionMesh> entry : candidates.entrySet()) {
466 if (entry.getValue().closestDistanceToSqr(camera) <= modelDistanceSqr) {
467 seeds.add(entry.getKey());
468 }
469 }
470
471 Set<SectionBoundary> connectedEdges = new LinkedHashSet<>();
472 // Deliberately iterate only the original seeds. Newly included neighbours form a guard
473 // band, not a transitive closure that could upload an entire remote blueprint.
474 for (SectionKey key : seeds) {
475 SectionMesh section = candidates.get(key);
476 if (section == null) continue;
477 for (Direction direction : Direction.values()) {
478 SectionKey neighbourKey = key.relative(direction);
479 SectionMesh neighbour = candidates.get(neighbourKey);
480 if (neighbour != null
481 && projectionBoundaryConnected(section, neighbour, direction)) {
482 connectedEdges.add(new SectionBoundary(key, neighbourKey));
483 }
484 }
485 }
486 return expandModelResidency(seeds, connectedEdges);
487 }
488
489 /** Expands only the supplied seed set by one layer of connected section-boundary edges. */
490 static Set<SectionKey> expandModelResidency(
491 Set<SectionKey> seeds, Set<SectionBoundary> connectedEdges) {
492 Set<SectionKey> result = new LinkedHashSet<>(seeds);
493 for (SectionBoundary edge : connectedEdges) {
494 boolean firstSeed = seeds.contains(edge.first());
495 boolean secondSeed = seeds.contains(edge.second());
496 if (firstSeed) result.add(edge.second());
497 if (secondSeed) result.add(edge.first());
498 }
499 return Set.copyOf(result);
500 }
501
404 502 private static Set<SectionKey> changedSectionKeys(
405 503 Map<SectionKey, SectionMesh> current,
406 504 Map<SectionKey, SectionMesh> replacement) {
@@ -428,9 +526,17 @@ final class BuilderPreviewSectionMeshCache {
428 526 for (Direction direction : positiveDirections) {
429 527 SectionKey neighbour = key.relative(direction);
430 528 if (!changed.contains(neighbour)) continue;
431 if (boundaryChanged(current.get(key), replacement.get(key), direction)
432 || boundaryChanged(current.get(neighbour), replacement.get(neighbour),
433 direction.getOpposite())) {
529 boolean connectedBefore = projectionBoundaryConnected(
530 current.get(key), current.get(neighbour), direction);
531 boolean connectedAfter = projectionBoundaryConnected(
532 replacement.get(key), replacement.get(neighbour), direction);
533 boolean firstChanged = boundaryChanged(
534 current.get(key), replacement.get(key), direction);
535 boolean secondChanged = boundaryChanged(
536 current.get(neighbour), replacement.get(neighbour),
537 direction.getOpposite());
538 if (requiresAtomicBoundaryPublication(connectedBefore, connectedAfter,
539 firstChanged, secondChanged)) {
434 540 dependencies.add(new SectionBoundary(key, neighbour));
435 541 }
436 542 }
@@ -438,6 +544,20 @@ final class BuilderPreviewSectionMeshCache {
438 544 return dependencies;
439 545 }
440 546
547 static boolean requiresAtomicBoundaryPublication(
548 boolean connectedBefore, boolean connectedAfter,
549 boolean firstBoundaryChanged, boolean secondBoundaryChanged) {
550 return (connectedBefore || connectedAfter)
551 && (firstBoundaryChanged || secondBoundaryChanged);
552 }
553
554 private static boolean projectionBoundaryConnected(
555 @Nullable SectionMesh section, @Nullable SectionMesh neighbour,
556 Direction direction) {
557 return section != null && neighbour != null
558 && section.projectionBoundaryConnects(neighbour, direction);
559 }
560
441 561 private static boolean boundaryChanged(@Nullable SectionMesh before,
442 562 @Nullable SectionMesh after,
443 563 Direction direction) {
@@ -477,6 +597,23 @@ final class BuilderPreviewSectionMeshCache {
477 597 return true;
478 598 }
479 599
600 /**
601 * The first generation has no old concrete models to preserve outside the current residency.
602 * Restricting its atomic edges to that working set prevents one long, connected blueprint from
603 * delaying the nearest pair until every remote outline section has uploaded.
604 */
605 static Set<SectionBoundary> publicationDependenciesForResidency(
606 Set<SectionBoundary> dependencies, Set<SectionKey> residency) {
607 Set<SectionBoundary> result = new LinkedHashSet<>();
608 for (SectionBoundary dependency : dependencies) {
609 if (residency.contains(dependency.first())
610 && residency.contains(dependency.second())) {
611 result.add(dependency);
612 }
613 }
614 return Set.copyOf(result);
615 }
616
480 617 private static SectionKey find(Map<SectionKey, SectionKey> parents, SectionKey key) {
481 618 SectionKey parent = parents.get(key);
482 619 if (parent.equals(key)) return key;
@@ -496,23 +633,36 @@ final class BuilderPreviewSectionMeshCache {
496 633 private BuilderPreviewState.Snapshot source;
497 634 private final Map<SectionKey, SectionMesh> sections;
498 635 private final Set<SectionKey> changed;
499 private final List<Set<SectionKey>> publicationGroups;
636 private final Set<SectionBoundary> dependencies;
637 private final boolean initialGeneration;
638 @Nullable private List<Set<SectionKey>> publicationGroups;
500 639 private final Set<SectionKey> published = new HashSet<>();
501 640 private boolean mutatedActive;
502 641
503 642 private PendingGeneration(BuilderPreviewState.Snapshot source,
504 643 Map<SectionKey, SectionMesh> sections,
505 644 Set<SectionKey> changed,
506 List<Set<SectionKey>> publicationGroups) {
645 Set<SectionBoundary> dependencies,
646 boolean initialGeneration) {
507 647 this.source = source;
508 648 this.sections = new LinkedHashMap<>(sections);
509 649 this.changed = Set.copyOf(changed);
510 this.publicationGroups = List.copyOf(publicationGroups);
650 this.dependencies = Set.copyOf(dependencies);
651 this.initialGeneration = initialGeneration;
511 652 }
512 653
513 654 private BuilderPreviewState.Snapshot source() { return source; }
514 655 private Map<SectionKey, SectionMesh> sections() { return sections; }
515 private List<Set<SectionKey>> publicationGroups() { return publicationGroups; }
656 private void initializePublicationGroups(Set<SectionKey> modelResidency) {
657 if (publicationGroups != null) return;
658 Set<SectionBoundary> activeDependencies = initialGeneration
659 ? publicationDependenciesForResidency(dependencies, modelResidency)
660 : dependencies;
661 publicationGroups = connectedPublicationGroups(changed, activeDependencies);
662 }
663 private List<Set<SectionKey>> publicationGroups() {
664 return publicationGroups == null ? List.of() : publicationGroups;
665 }
516 666 private boolean requiresPublication(SectionKey key) { return changed.contains(key); }
517 667 private boolean isPublished(SectionKey key) { return published.contains(key); }
518 668 private boolean isPublished(Set<SectionKey> group) {
@@ -563,6 +713,7 @@ final class BuilderPreviewSectionMeshCache {
563 713 }
564 714
565 715 static final class SectionMesh {
716 private final SectionKey key;
566 717 private final int originX;
567 718 private final int originY;
568 719 private final int originZ;
@@ -571,11 +722,16 @@ final class BuilderPreviewSectionMeshCache {
571 722 private final List<WaveCell> waveCells;
572 723 private final List<Integer> visibleGhostFaces;
573 724 private final BoundarySignature[] boundaries;
725 private final long[][] projectedBoundaryMasks;
574 726 private final boolean includeBuildGhosts;
575 727 private final boolean hasGhosts;
576 728
577 729 @Nullable private VertexBuffer outlineBuffer;
578 730 private final List<VertexBuffer> ghostBuffers = new ArrayList<>();
731 private final List<List<VertexBuffer>> boundaryGhostBuffers = new ArrayList<>(
732 Direction.values().length);
733 private final List<List<VertexBuffer>> ghostDrawLists = new ArrayList<>(
734 1 << Direction.values().length);
579 735 private final List<VertexBuffer> waveBuffers = new ArrayList<>();
580 736 private int ghostCursor;
581 737 private int waveCursor;
@@ -583,6 +739,7 @@ final class BuilderPreviewSectionMeshCache {
583 739 private SectionMesh(SectionKey key, List<BuilderPreviewState.Cell> cells,
584 740 List<WaveCell> waveCells, List<Integer> visibleGhostFaces,
585 741 boolean includeBuildGhosts) {
742 this.key = key;
586 743 originX = key.x() * SECTION_SIZE;
587 744 originY = key.y() * SECTION_SIZE;
588 745 originZ = key.z() * SECTION_SIZE;
@@ -595,7 +752,15 @@ final class BuilderPreviewSectionMeshCache {
595 752 this.visibleGhostFaces = List.copyOf(visibleGhostFaces);
596 753 boundaries = createBoundarySignatures(this.cells, this.waveCells,
597 754 this.visibleGhostFaces, includeBuildGhosts, originX, originY, originZ);
755 projectedBoundaryMasks = createProjectedBoundaryMasks(this.cells,
756 includeBuildGhosts, originX, originY, originZ);
598 757 hasGhosts = cells.stream().anyMatch(cell -> isGhostCell(cell, includeBuildGhosts));
758 for (Direction ignored : Direction.values()) {
759 boundaryGhostBuffers.add(new ArrayList<>());
760 }
761 for (int ignored = 0; ignored < 1 << Direction.values().length; ignored++) {
762 ghostDrawLists.add(null);
763 }
599 764 }
600 765
601 766 private boolean matches(List<BuilderPreviewState.Cell> replacement,
@@ -608,39 +773,47 @@ final class BuilderPreviewSectionMeshCache {
608 773 && visibleGhostFaces.equals(replacementVisibleGhostFaces);
609 774 }
610 775
611 private boolean uploadNext(Minecraft minecraft, boolean buildModels) {
776 /** @return upload-budget slots consumed, or zero when no work was available. */
777 private int uploadNext(Minecraft minecraft, boolean buildModels) {
612 778 RenderSystem.assertOnRenderThread();
613 779 if (outlineBuffer == null) {
614 780 outlineBuffer = uploadOutlineBuffer();
615 return true;
781 return uploadBudgetCost(outlineBuffer == null ? 0 : 1);
616 782 }
617 783 if (waveCursor < waveCells.size()) {
618 784 int end = Math.min(waveCells.size(), waveCursor + WAVE_CELLS_PER_UPLOAD);
619 785 VertexBuffer wave = uploadWaveBuffer(waveCursor, end);
620 786 waveCursor = end;
621 787 if (wave != null) waveBuffers.add(wave);
622 return true;
788 return uploadBudgetCost(wave == null ? 0 : 1);
623 789 }
624 790 if (hasGhosts && buildModels && ghostCursor < cells.size()) {
625 791 int end = Math.min(cells.size(), ghostCursor + MODEL_CELLS_PER_UPLOAD);
626 VertexBuffer ghost = uploadGhostBuffer(minecraft, ghostCursor, end);
792 int uploadedBuffers = uploadGhostBuffers(minecraft, ghostCursor, end);
627 793 ghostCursor = end;
628 if (ghost != null) ghostBuffers.add(ghost);
629 return true;
794 return uploadBudgetCost(uploadedBuffers);
630 795 }
631 return false;
796 return 0;
632 797 }
633 798
634 799 private boolean ready(boolean requireModels) {
635 800 return outlineBuffer != null
636 801 && waveCursor >= waveCells.size()
637 && (!requireModels || !hasGhosts || ghostCursor >= cells.size());
802 && (!requireModels || ghostModelsComplete());
803 }
804
805 private boolean ghostModelsComplete() {
806 return !hasGhosts || ghostCursor >= cells.size();
638 807 }
639 808
640 809 private boolean ghostUploadStarted() {
641 810 return ghostCursor > 0;
642 811 }
643 812
813 private boolean ghostUploadInProgress() {
814 return ghostUploadStarted() && !ghostModelsComplete();
815 }
816
644 817 @Nullable
645 818 private VertexBuffer uploadOutlineBuffer() {
646 819 Set<EdgeKey> edges = new HashSet<>(Math.max(16, cells.size() * 4));
@@ -669,25 +842,29 @@ final class BuilderPreviewSectionMeshCache {
669 842 return upload(builder);
670 843 }
671 844
672 @Nullable
673 private VertexBuffer uploadGhostBuffer(Minecraft minecraft, int start, int end) {
845 private int uploadGhostBuffers(Minecraft minecraft, int start, int end) {
674 846 BufferBuilder builder = new BufferBuilder(Math.max(4096, (end - start) * 1024));
675 847 builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.NEW_ENTITY);
848 BufferBuilder[] boundaryBuilders = new BufferBuilder[Direction.values().length];
676 849 PoseStack pose = new PoseStack();
677 850 for (int index = start; index < end; index++) {
678 851 BuilderPreviewState.Cell cell = cells.get(index);
679 852 if (!isGhostCell(cell, includeBuildGhosts)) continue;
853 int localX = cell.pos().getX() - originX;
854 int localY = cell.pos().getY() - originY;
855 int localZ = cell.pos().getZ() - originZ;
680 856 pose.pushPose();
681 pose.translate(cell.pos().getX() - originX + GHOST_MODEL_INSET,
682 cell.pos().getY() - originY + GHOST_MODEL_INSET,
683 cell.pos().getZ() - originZ + GHOST_MODEL_INSET);
857 pose.translate(localX + GHOST_MODEL_INSET,
858 localY + GHOST_MODEL_INSET,
859 localZ + GHOST_MODEL_INSET);
684 860 float modelScale = 1.0F - GHOST_MODEL_INSET * 2.0F;
685 861 pose.scale(modelScale, modelScale, modelScale);
686 862 VertexConsumer tinted = new GhostVertexConsumer(builder, cell.kind());
687 863 MultiBufferSource singleBuffer = ignored -> tinted;
688 864 try {
689 865 renderGhostBlock(minecraft, cell, visibleGhostFaces.get(index), pose,
690 singleBuffer, tinted);
866 singleBuffer, tinted, boundaryBuilders,
867 localX, localY, localZ);
691 868 } catch (RuntimeException exception) {
692 869 ResourceLocation blockId = ForgeRegistries.BLOCKS.getKey(cell.state().getBlock());
693 870 if (blockId != null && WARNED_MODEL_TYPES.add(blockId)) {
@@ -697,7 +874,25 @@ final class BuilderPreviewSectionMeshCache {
697 874 }
698 875 pose.popPose();
699 876 }
700 return upload(builder);
877 VertexBuffer ghost = upload(builder);
878 int uploadedBuffers = 0;
879 if (ghost != null) {
880 ghostBuffers.add(ghost);
881 uploadedBuffers++;
882 }
883 // Keep the main mesh and its directional fallback faces in one atomic 128-cell upload
884 // step. A cap contains only boundary directional quads from that same bounded batch;
885 // it cannot grow with the rest of the blueprint or leave a drawable mesh uncapped.
886 for (Direction direction : Direction.values()) {
887 BufferBuilder boundaryBuilder = boundaryBuilders[direction.ordinal()];
888 if (boundaryBuilder == null) continue;
889 VertexBuffer boundary = upload(boundaryBuilder);
890 if (boundary != null) {
891 boundaryGhostBuffers.get(direction.ordinal()).add(boundary);
892 uploadedBuffers++;
893 }
894 }
895 return uploadedBuffers;
701 896 }
702 897
703 898 /**
@@ -712,7 +907,9 @@ final class BuilderPreviewSectionMeshCache {
712 907 int visibleFaceMask,
713 908 PoseStack pose,
714 909 MultiBufferSource singleBuffer,
715 VertexConsumer tinted) {
910 VertexConsumer tinted,
911 BufferBuilder[] boundaryBuilders,
912 int localX, int localY, int localZ) {
716 913 if (cell.state().getRenderShape() != RenderShape.MODEL) {
717 914 minecraft.getBlockRenderer().renderSingleBlock(cell.state(), pose, singleBuffer,
718 915 LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY, ModelData.EMPTY,
@@ -729,11 +926,19 @@ final class BuilderPreviewSectionMeshCache {
729 926 for (RenderType renderType : model.getRenderTypes(
730 927 cell.state(), renderTypeRandom, ModelData.EMPTY)) {
731 928 for (Direction direction : Direction.values()) {
732 if (!isFaceVisible(visibleFaceMask, direction)) continue;
733 929 RandomSource quadRandom = RandomSource.create(42L);
734 renderQuadList(pose.last(), tinted,
735 model.getQuads(cell.state(), direction, quadRandom,
736 ModelData.EMPTY, renderType), red, green, blue);
930 List<BakedQuad> quads = model.getQuads(cell.state(), direction, quadRandom,
931 ModelData.EMPTY, renderType);
932 if (isFaceVisible(visibleFaceMask, direction)) {
933 renderQuadList(pose.last(), tinted, quads, red, green, blue);
934 } else if (onBoundary(localX, localY, localZ, direction)) {
935 BufferBuilder boundaryBuilder = boundaryBuilder(
936 boundaryBuilders, direction);
937 VertexConsumer boundaryTinted = new GhostVertexConsumer(
938 boundaryBuilder, cell.kind());
939 renderQuadList(pose.last(), boundaryTinted,
940 quads, red, green, blue);
941 }
737 942 }
738 943 RandomSource unculledRandom = RandomSource.create(42L);
739 944 renderQuadList(pose.last(), tinted,
@@ -742,6 +947,18 @@ final class BuilderPreviewSectionMeshCache {
742 947 }
743 948 }
744 949
950 private static BufferBuilder boundaryBuilder(BufferBuilder[] builders,
951 Direction direction) {
952 int index = direction.ordinal();
953 BufferBuilder builder = builders[index];
954 if (builder == null) {
955 builder = new BufferBuilder(4096);
956 builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.NEW_ENTITY);
957 builders[index] = builder;
958 }
959 return builder;
960 }
961
745 962 private static void renderQuadList(PoseStack.Pose pose, VertexConsumer consumer,
746 963 List<BakedQuad> quads,
747 964 float red, float green, float blue) {
@@ -769,7 +986,29 @@ final class BuilderPreviewSectionMeshCache {
769 986
770 987 AABB bounds() { return bounds; }
771 988 @Nullable VertexBuffer outlineBuffer() { return outlineBuffer; }
772 List<VertexBuffer> ghostBuffers() { return ghostBuffers; }
989 List<VertexBuffer> ghostBuffers(Set<SectionKey> drawableSections) {
990 boolean drawable = drawableSections.contains(key);
991 if (!shouldRenderGhostModels(drawable, ghostModelsComplete())) return List.of();
992 int capMask = 0;
993 for (Direction direction : Direction.values()) {
994 List<VertexBuffer> boundary = boundaryGhostBuffers.get(direction.ordinal());
995 if (boundary.isEmpty() || !shouldRenderBoundaryCap(
996 drawable, drawableSections.contains(key.relative(direction)))) continue;
997 capMask |= 1 << direction.ordinal();
998 }
999 if (capMask == 0) return ghostBuffers;
1000 List<VertexBuffer> result = ghostDrawLists.get(capMask);
1001 if (result != null) return result;
1002 List<VertexBuffer> created = new ArrayList<>(ghostBuffers);
1003 for (Direction direction : Direction.values()) {
1004 if ((capMask & 1 << direction.ordinal()) != 0) {
1005 created.addAll(boundaryGhostBuffers.get(direction.ordinal()));
1006 }
1007 }
1008 result = List.copyOf(created);
1009 ghostDrawLists.set(capMask, result);
1010 return result;
1011 }
773 1012 List<VertexBuffer> waveBuffers() { return waveBuffers; }
774 1013 int originX() { return originX; }
775 1014 int originY() { return originY; }
@@ -778,6 +1017,18 @@ final class BuilderPreviewSectionMeshCache {
778 1017 return boundaries[direction.ordinal()];
779 1018 }
780 1019
1020 private boolean projectionBoundaryConnects(SectionMesh neighbour,
1021 Direction direction) {
1022 if (!key.relative(direction).equals(neighbour.key)) return false;
1023 long[] first = projectedBoundaryMasks[direction.ordinal()];
1024 long[] second = neighbour.projectedBoundaryMasks[
1025 direction.getOpposite().ordinal()];
1026 for (int index = 0; index < first.length; index++) {
1027 if ((first[index] & second[index]) != 0L) return true;
1028 }
1029 return false;
1030 }
1031
781 1032 double closestDistanceToSqr(Vec3 point) {
782 1033 double dx = axisDistance(point.x, bounds.minX, bounds.maxX);
783 1034 double dy = axisDistance(point.y, bounds.minY, bounds.maxY);
@@ -795,9 +1046,13 @@ final class BuilderPreviewSectionMeshCache {
795 1046 private void close() {
796 1047 close(outlineBuffer);
797 1048 ghostBuffers.forEach(SectionMesh::close);
1049 boundaryGhostBuffers.forEach(
1050 buffers -> buffers.forEach(SectionMesh::close));
798 1051 waveBuffers.forEach(SectionMesh::close);
799 1052 outlineBuffer = null;
800 1053 ghostBuffers.clear();
1054 boundaryGhostBuffers.forEach(List::clear);
1055 Collections.fill(ghostDrawLists, null);
801 1056 waveBuffers.clear();
802 1057 }
803 1058
@@ -882,6 +1137,25 @@ final class BuilderPreviewSectionMeshCache {
882 1137
883 1138 record SectionBoundary(SectionKey first, SectionKey second) { }
884 1139
1140 record ModelResidency(Set<SectionKey> keys, Set<SectionMesh> meshes) {
1141 ModelResidency {
1142 keys = Set.copyOf(keys);
1143 meshes = Collections.unmodifiableSet(meshes);
1144 }
1145
1146 boolean contains(SectionMesh section) {
1147 return meshes.contains(section);
1148 }
1149
1150 Set<SectionKey> drawableGhostSections() {
1151 Set<SectionKey> drawable = new LinkedHashSet<>();
1152 for (SectionMesh section : meshes) {
1153 if (section.ghostModelsComplete()) drawable.add(section.key);
1154 }
1155 return Set.copyOf(drawable);
1156 }
1157 }
1158
885 1159 private record BoundarySignature(long[] entries) {
886 1160 private static final BoundarySignature EMPTY = new BoundarySignature(new long[0]);
887 1161
@@ -931,6 +1205,22 @@ final class BuilderPreviewSectionMeshCache {
931 1205 return withinModelDistance || ghostUploadStarted;
932 1206 }
933 1207
1208 /** Partial or out-of-residency concrete buffers are never exposed to the renderer. */
1209 static boolean shouldRenderGhostModels(boolean resident, boolean uploadComplete) {
1210 return resident && uploadComplete;
1211 }
1212
1213 /** A culled cross-section face is restored until its projected neighbour is drawable. */
1214 static boolean shouldRenderBoundaryCap(boolean sectionDrawable,
1215 boolean neighbourDrawable) {
1216 return sectionDrawable && !neighbourDrawable;
1217 }
1218
1219 /** Empty batches still consume one scheduling step; cap VBOs consume their real upload cost. */
1220 static int uploadBudgetCost(int uploadedVertexBuffers) {
1221 return Math.max(1, uploadedVertexBuffers);
1222 }
1223
934 1224 private static int divideRoundUp(int value, int divisor) {
935 1225 return value == 0 ? 0 : 1 + (value - 1) / divisor;
936 1226 }
@@ -1012,6 +1302,33 @@ final class BuilderPreviewSectionMeshCache {
1012 1302 return result;
1013 1303 }
1014 1304
1305 /** Four longs encode the 16x16 occupied ghost cells on each of the six section faces. */
1306 private static long[][] createProjectedBoundaryMasks(
1307 List<BuilderPreviewState.Cell> cells, boolean includeBuildGhosts,
1308 int originX, int originY, int originZ) {
1309 long[][] result = new long[Direction.values().length][4];
1310 for (BuilderPreviewState.Cell cell : cells) {
1311 if (!isGhostCell(cell, includeBuildGhosts)) continue;
1312 int x = cell.pos().getX() - originX;
1313 int y = cell.pos().getY() - originY;
1314 int z = cell.pos().getZ() - originZ;
1315 for (Direction direction : Direction.values()) {
1316 if (!onBoundary(x, y, z, direction)) continue;
1317 int bit = boundaryCellIndex(x, y, z, direction);
1318 result[direction.ordinal()][bit >>> 6] |= 1L << (bit & 63);
1319 }
1320 }
1321 return result;
1322 }
1323
1324 private static int boundaryCellIndex(int x, int y, int z, Direction direction) {
1325 return switch (direction.getAxis()) {
1326 case X -> y << SECTION_SHIFT | z;
1327 case Y -> x << SECTION_SHIFT | z;
1328 case Z -> x << SECTION_SHIFT | y;
1329 };
1330 }
1331
1015 1332 private static LongArrayList boundaryEntries(LongArrayList[] entries,
1016 1333 Direction direction) {
1017 1334 int index = direction.ordinal();
Modified src/main/java/com/xfestudio/mydimension/client/builder/BuilderSurfacePreviewPlanner.java +3 -2
@@ -1,6 +1,7 @@
1 1 package com.xfestudio.mydimension.client.builder;
2 2
3 3 import com.xfestudio.mydimension.builder.BuilderMode;
4 import com.xfestudio.mydimension.builder.BuilderBlockCompatibility;
4 5 import com.xfestudio.mydimension.builder.BuilderTags;
5 6 import com.xfestudio.mydimension.builder.SurfacePlaneTraversal;
6 7 import com.xfestudio.mydimension.builder.SurfaceMatchMode;
@@ -101,7 +102,7 @@ public final class BuilderSurfacePreviewPlanner {
101 102 BlockState state = level.getBlockState(pos);
102 103 return SurfacePlaneTraversal.isReference(state)
103 104 && (settings.surfaceMatch() != SurfaceMatchMode.SAME_BLOCK
104 || state.getBlock() == seed.getBlock())
105 || BuilderBlockCompatibility.sameSurfaceType(seed, state))
105 106 && SurfacePlaneTraversal.hasExposedReferenceFace(
106 107 level, pos, hit.getDirection(), state);
107 108 }, ignored -> true);
@@ -163,7 +164,7 @@ public final class BuilderSurfacePreviewPlanner {
163 164 if (!source.equals(cell.state())) return true;
164 165 } else if (!SurfacePlaneTraversal.isReference(source)
165 166 || key.match() == SurfaceMatchMode.SAME_BLOCK
166 && source.getBlock() != key.seedState().getBlock()) {
167 && !BuilderBlockCompatibility.sameSurfaceType(key.seedState(), source)) {
167 168 return true;
168 169 }
169 170 }
Modified src/main/resources/assets/mydimension/models/item/realmwright_scepter.json +5 -5
@@ -5,12 +5,12 @@
5 5 "particle": "mydimension:item/realmwright_voidstone"
6 6 },
7 7 "display": {
8 "thirdperson_righthand": { "rotation": [0, -90, 52], "translation": [0.2, 4.4, 0.6], "scale": [0.34, 0.34, 0.34] },
9 "thirdperson_lefthand": { "rotation": [0, 90, -52], "translation": [0.2, 4.4, 0.6], "scale": [0.34, 0.34, 0.34] },
8 "thirdperson_righthand": { "rotation": [0, -90, -52], "translation": [0.2, 4.4, 0.9], "scale": [0.34, 0.34, 0.34] },
9 "thirdperson_lefthand": { "rotation": [0, 90, 52], "translation": [0.2, 4.4, 0.9], "scale": [0.34, 0.34, 0.34] },
10 10 "firstperson_righthand": { "rotation": [0, -35, 20], "translation": [1.35, 2.8, 0.35], "scale": [0.52, 0.52, 0.52] },
11 11 "firstperson_lefthand": { "rotation": [0, 35, -20], "translation": [1.35, 2.8, 0.35], "scale": [0.52, 0.52, 0.52] },
12 "gui": { "rotation": [18, -35, -45], "translation": [-0.9, -1.1, 0], "scale": [0.31, 0.31, 0.31] },
13 "ground": { "rotation": [0, 0, -45], "translation": [-0.25, 2.65, 0], "scale": [0.19, 0.19, 0.19] },
14 "fixed": { "rotation": [0, 180, -45], "translation": [-0.7, -0.9, 0], "scale": [0.29, 0.29, 0.29] }
12 "gui": { "rotation": [18, -35, -45], "translation": [-0.9, -0.6, 0], "scale": [0.25, 0.25, 0.25] },
13 "ground": { "rotation": [0, 0, -45], "translation": [-0.25, 2.65, 0], "scale": [0.15, 0.15, 0.15] },
14 "fixed": { "rotation": [0, 180, -45], "translation": [-0.7, -0.4, 0], "scale": [0.23, 0.23, 0.23] }
15 15 }
16 16 }
Modified src/main/resources/assets/mydimension/models/item/realmwright_scepter_base.json +54 -54
@@ -11,31 +11,31 @@
11 11 },
12 12 "elements": [
13 13 {
14 "name": "pommel_crystal", "from": [6.9, -16.0, 6.9], "to": [9.1, -13.2, 9.1],
15 "rotation": { "origin": [8, -14.6, 8], "axis": "z", "angle": 45, "rescale": true }, "shade": false,
14 "name": "pommel_crystal", "from": [6.9, -30.55, 6.9], "to": [9.1, -27.75, 9.1],
15 "rotation": { "origin": [8, -29.15, 8], "axis": "z", "angle": 45, "rescale": true }, "shade": false,
16 16 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "south": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
17 17 },
18 18 {
19 "name": "pommel_cage", "from": [5.9, -13.6, 5.9], "to": [10.1, -11.7, 10.1],
20 "rotation": { "origin": [8, -12.65, 8], "axis": "z", "angle": 45, "rescale": true },
21 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
19 "name": "pommel_cage", "from": [5.9, -28.15, 5.9], "to": [10.1, -26.25, 10.1],
20 "rotation": { "origin": [8, -27.2, 8], "axis": "z", "angle": 45, "rescale": true },
21 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
22 22 },
23 23 {
24 "name": "pommel_fang_right", "from": [8.55, -15.0, 7.1], "to": [10.0, -11.8, 8.9],
25 "rotation": { "origin": [8.7, -12.1, 8], "axis": "z", "angle": -22.5, "rescale": true },
24 "name": "pommel_fang_right", "from": [8.55, -29.55, 7.1], "to": [10.0, -26.35, 8.9],
25 "rotation": { "origin": [8.7, -26.65, 8], "axis": "z", "angle": -22.5, "rescale": true },
26 26 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
27 27 },
28 28 {
29 "name": "pommel_fang_left", "from": [6.0, -15.0, 7.1], "to": [7.45, -11.8, 8.9],
30 "rotation": { "origin": [7.3, -12.1, 8], "axis": "z", "angle": 22.5, "rescale": true },
29 "name": "pommel_fang_left", "from": [6.0, -29.55, 7.1], "to": [7.45, -26.35, 8.9],
30 "rotation": { "origin": [7.3, -26.65, 8], "axis": "z", "angle": 22.5, "rescale": true },
31 31 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
32 32 },
33 33 {
34 "name": "shaft_lower", "from": [7.0, -12.0, 7.0], "to": [9.0, -2.0, 9.0],
34 "name": "shaft_lower", "from": [7.0, -26.55, 7.0], "to": [9.0, -9.28, 9.0],
35 35 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#body" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
36 36 },
37 37 {
38 "name": "shaft_middle", "from": [6.85, -2.05, 6.85], "to": [9.15, 8.0, 9.15],
38 "name": "shaft_middle", "from": [6.85, -9.36, 6.85], "to": [9.15, 8.0, 9.15],
39 39 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#body" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
40 40 },
41 41 {
@@ -43,17 +43,17 @@
43 43 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#body" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
44 44 },
45 45 {
46 "name": "shaft_rift_lower_left", "from": [7.25, -10.9, 6.96], "to": [7.85, -3.0, 6.99],
47 "rotation": { "origin": [7.55, -6.95, 6.98], "axis": "z", "angle": -22.5, "rescale": false }, "shade": false,
46 "name": "shaft_rift_lower_left", "from": [7.25, -24.65, 6.96], "to": [7.85, -11.0, 6.99],
47 "rotation": { "origin": [7.55, -17.82, 6.98], "axis": "z", "angle": -22.5, "rescale": false }, "shade": false,
48 48 "faces": { "north": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
49 49 },
50 50 {
51 "name": "shaft_rift_lower_right", "from": [8.15, -10.9, 6.96], "to": [8.75, -3.0, 6.99],
52 "rotation": { "origin": [8.45, -6.95, 6.98], "axis": "z", "angle": 22.5, "rescale": false }, "shade": false,
51 "name": "shaft_rift_lower_right", "from": [8.15, -24.65, 6.96], "to": [8.75, -11.0, 6.99],
52 "rotation": { "origin": [8.45, -17.82, 6.98], "axis": "z", "angle": 22.5, "rescale": false }, "shade": false,
53 53 "faces": { "north": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
54 54 },
55 55 {
56 "name": "shaft_rift_middle", "from": [7.68, -1.3, 6.81], "to": [8.32, 6.8, 6.84], "shade": false,
56 "name": "shaft_rift_middle", "from": [7.68, -8.06, 6.81], "to": [8.32, 5.93, 6.84], "shade": false,
57 57 "faces": { "north": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
58 58 },
59 59 {
@@ -69,90 +69,90 @@
69 69 {
70 70 "name": "head_collar", "from": [5.6, 16.6, 5.6], "to": [10.4, 18.2, 10.4],
71 71 "rotation": { "origin": [8, 17.4, 8], "axis": "z", "angle": 45, "rescale": true },
72 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
72 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
73 73 },
74 74 {
75 75 "name": "rift_neck", "from": [6.25, 17.8, 6.25], "to": [9.75, 20.1, 9.75],
76 76 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
77 77 },
78 78 {
79 "name": "left_lower_cradle", "from": [3.45, 17.4, 6.15], "to": [6.65, 20.75, 9.85],
80 "rotation": { "origin": [6.35, 18.1, 8], "axis": "z", "angle": 22.5, "rescale": true },
79 "name": "left_lower_cradle", "from": [3.65, 17.4, 6.15], "to": [6.85, 20.75, 9.85],
80 "rotation": { "origin": [6.55, 18.1, 8], "axis": "z", "angle": 22.5, "rescale": true },
81 81 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
82 82 },
83 83 {
84 "name": "right_lower_cradle", "from": [9.35, 17.4, 6.15], "to": [12.55, 20.75, 9.85],
85 "rotation": { "origin": [9.65, 18.1, 8], "axis": "z", "angle": -22.5, "rescale": true },
84 "name": "right_lower_cradle", "from": [9.15, 17.4, 6.15], "to": [12.35, 20.75, 9.85],
85 "rotation": { "origin": [9.45, 18.1, 8], "axis": "z", "angle": -22.5, "rescale": true },
86 86 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
87 87 },
88 88 {
89 "name": "left_lower_jaw", "from": [1.2, 18.2, 6.0], "to": [4.0, 22.2, 10.0],
90 "rotation": { "origin": [3.8, 18.5, 8], "axis": "z", "angle": 22.5, "rescale": true },
89 "name": "left_lower_jaw", "from": [1.5, 18.2, 6.0], "to": [4.3, 22.2, 10.0],
90 "rotation": { "origin": [4.1, 18.5, 8], "axis": "z", "angle": 22.5, "rescale": true },
91 91 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
92 92 },
93 93 {
94 "name": "left_outer_arc", "from": [-1.2, 20.0, 6.0], "to": [1.6, 25.6, 10.0],
95 "rotation": { "origin": [1.45, 20.2, 8], "axis": "z", "angle": 45, "rescale": true },
94 "name": "left_outer_arc", "from": [-0.75, 20.0, 6.0], "to": [2.05, 25.6, 10.0],
95 "rotation": { "origin": [1.9, 20.2, 8], "axis": "z", "angle": 45, "rescale": true },
96 96 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
97 97 },
98 98 {
99 "name": "left_horn_spine", "from": [-3.0, 23.1, 6.1], "to": [-0.35, 27.8, 9.9],
100 "rotation": { "origin": [-0.5, 23.3, 8], "axis": "z", "angle": 22.5, "rescale": true },
99 "name": "left_horn_spine", "from": [-2.45, 23.1, 6.1], "to": [0.2, 28.25, 9.9],
100 "rotation": { "origin": [0.05, 23.3, 8], "axis": "z", "angle": 22.5, "rescale": true },
101 101 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
102 102 },
103 103 {
104 "name": "left_horn_tip", "from": [-3.7, 25.8, 6.45], "to": [-1.6, 29.0, 9.55],
105 "rotation": { "origin": [-1.8, 26.0, 8], "axis": "z", "angle": -22.5, "rescale": true },
104 "name": "left_horn_tip", "from": [-3.1, 25.8, 6.45], "to": [-1.0, 29.6, 9.55],
105 "rotation": { "origin": [-1.2, 26.0, 8], "axis": "z", "angle": -22.5, "rescale": true },
106 106 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
107 107 },
108 108 {
109 "name": "left_inner_prong", "from": [3.45, 25.3, 6.45], "to": [5.05, 28.8, 9.55],
110 "rotation": { "origin": [3.9, 25.45, 8], "axis": "z", "angle": -22.5, "rescale": true },
109 "name": "left_inner_prong", "from": [3.7, 25.3, 6.45], "to": [5.3, 28.8, 9.55],
110 "rotation": { "origin": [4.15, 25.45, 8], "axis": "z", "angle": -22.5, "rescale": true },
111 111 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "east": { "uv": [0, 0, 16, 16], "texture": "#body" } }
112 112 },
113 113 {
114 "name": "right_lower_jaw", "from": [12.0, 18.2, 6.0], "to": [14.8, 22.2, 10.0],
115 "rotation": { "origin": [12.2, 18.5, 8], "axis": "z", "angle": -22.5, "rescale": true },
114 "name": "right_lower_jaw", "from": [11.7, 18.2, 6.0], "to": [14.5, 22.2, 10.0],
115 "rotation": { "origin": [11.9, 18.5, 8], "axis": "z", "angle": -22.5, "rescale": true },
116 116 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
117 117 },
118 118 {
119 "name": "right_outer_arc", "from": [14.4, 20.0, 6.0], "to": [17.2, 25.6, 10.0],
120 "rotation": { "origin": [14.55, 20.2, 8], "axis": "z", "angle": -45, "rescale": true },
119 "name": "right_outer_arc", "from": [13.95, 20.0, 6.0], "to": [16.75, 25.6, 10.0],
120 "rotation": { "origin": [14.1, 20.2, 8], "axis": "z", "angle": -45, "rescale": true },
121 121 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
122 122 },
123 123 {
124 "name": "right_horn_spine", "from": [16.35, 23.1, 6.1], "to": [19.0, 27.8, 9.9],
125 "rotation": { "origin": [16.5, 23.3, 8], "axis": "z", "angle": -22.5, "rescale": true },
124 "name": "right_horn_spine", "from": [15.8, 23.1, 6.1], "to": [18.45, 28.25, 9.9],
125 "rotation": { "origin": [15.95, 23.3, 8], "axis": "z", "angle": -22.5, "rescale": true },
126 126 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
127 127 },
128 128 {
129 "name": "right_horn_tip", "from": [17.6, 25.8, 6.45], "to": [19.7, 29.0, 9.55],
130 "rotation": { "origin": [17.8, 26.0, 8], "axis": "z", "angle": 22.5, "rescale": true },
129 "name": "right_horn_tip", "from": [17.0, 25.8, 6.45], "to": [19.1, 29.6, 9.55],
130 "rotation": { "origin": [17.2, 26.0, 8], "axis": "z", "angle": 22.5, "rescale": true },
131 131 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
132 132 },
133 133 {
134 "name": "right_inner_prong", "from": [10.95, 25.3, 6.45], "to": [12.55, 28.8, 9.55],
135 "rotation": { "origin": [12.1, 25.45, 8], "axis": "z", "angle": 22.5, "rescale": true },
134 "name": "right_inner_prong", "from": [10.7, 25.3, 6.45], "to": [12.3, 28.8, 9.55],
135 "rotation": { "origin": [11.85, 25.45, 8], "axis": "z", "angle": 22.5, "rescale": true },
136 136 "faces": { "down": { "uv": [0, 0, 16, 16], "texture": "#body" }, "up": { "uv": [0, 0, 16, 16], "texture": "#crystal" }, "north": { "uv": [0, 0, 16, 16], "texture": "#body" }, "south": { "uv": [0, 0, 16, 16], "texture": "#body" }, "west": { "uv": [0, 0, 16, 16], "texture": "#body" }, "east": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
137 137 },
138 138 {
139 "name": "left_arc_rift_inlay", "from": [-0.6, 20.9, 5.96], "to": [0.1, 24.7, 5.99],
140 "rotation": { "origin": [0.0, 21.0, 5.98], "axis": "z", "angle": 45, "rescale": false }, "shade": false,
139 "name": "left_arc_rift_inlay", "from": [-0.15, 20.9, 5.96], "to": [0.55, 24.7, 5.99],
140 "rotation": { "origin": [0.45, 21.0, 5.98], "axis": "z", "angle": 45, "rescale": false }, "shade": false,
141 141 "faces": { "north": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
142 142 },
143 143 {
144 "name": "left_horn_rift_inlay", "from": [-2.65, 23.8, 6.06], "to": [-2.0, 27.1, 6.09],
145 "rotation": { "origin": [-2.0, 24.0, 6.08], "axis": "z", "angle": 22.5, "rescale": false }, "shade": false,
144 "name": "left_horn_rift_inlay", "from": [-2.1, 23.8, 6.06], "to": [-1.45, 27.45, 6.09],
145 "rotation": { "origin": [-1.45, 24.0, 6.08], "axis": "z", "angle": 22.5, "rescale": false }, "shade": false,
146 146 "faces": { "north": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
147 147 },
148 148 {
149 "name": "right_arc_rift_inlay", "from": [15.9, 20.9, 5.96], "to": [16.6, 24.7, 5.99],
150 "rotation": { "origin": [16.0, 21.0, 5.98], "axis": "z", "angle": -45, "rescale": false }, "shade": false,
149 "name": "right_arc_rift_inlay", "from": [15.45, 20.9, 5.96], "to": [16.15, 24.7, 5.99],
150 "rotation": { "origin": [15.55, 21.0, 5.98], "axis": "z", "angle": -45, "rescale": false }, "shade": false,
151 151 "faces": { "north": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
152 152 },
153 153 {
154 "name": "right_horn_rift_inlay", "from": [18.0, 23.8, 6.06], "to": [18.65, 27.1, 6.09],
155 "rotation": { "origin": [18.0, 24.0, 6.08], "axis": "z", "angle": -22.5, "rescale": false }, "shade": false,
154 "name": "right_horn_rift_inlay", "from": [17.45, 23.8, 6.06], "to": [18.1, 27.45, 6.09],
155 "rotation": { "origin": [17.45, 24.0, 6.08], "axis": "z", "angle": -22.5, "rescale": false }, "shade": false,
156 156 "faces": { "north": { "uv": [0, 0, 16, 16], "texture": "#crystal" } }
157 157 },
158 158 {
@@ -165,12 +165,12 @@
165 165 }
166 166 ],
167 167 "display": {
168 "thirdperson_righthand": { "rotation": [0, -90, 52], "translation": [0.2, 4.4, 0.6], "scale": [0.34, 0.34, 0.34] },
169 "thirdperson_lefthand": { "rotation": [0, 90, -52], "translation": [0.2, 4.4, 0.6], "scale": [0.34, 0.34, 0.34] },
168 "thirdperson_righthand": { "rotation": [0, -90, -52], "translation": [0.2, 4.4, 0.9], "scale": [0.34, 0.34, 0.34] },
169 "thirdperson_lefthand": { "rotation": [0, 90, 52], "translation": [0.2, 4.4, 0.9], "scale": [0.34, 0.34, 0.34] },
170 170 "firstperson_righthand": { "rotation": [0, -35, 20], "translation": [1.35, 2.8, 0.35], "scale": [0.52, 0.52, 0.52] },
171 171 "firstperson_lefthand": { "rotation": [0, 35, -20], "translation": [1.35, 2.8, 0.35], "scale": [0.52, 0.52, 0.52] },
172 "gui": { "rotation": [18, -35, -45], "translation": [-0.9, -1.1, 0], "scale": [0.31, 0.31, 0.31] },
173 "ground": { "rotation": [0, 0, -45], "translation": [-0.25, 2.65, 0], "scale": [0.19, 0.19, 0.19] },
174 "fixed": { "rotation": [0, 180, -45], "translation": [-0.7, -0.9, 0], "scale": [0.29, 0.29, 0.29] }
172 "gui": { "rotation": [18, -35, -45], "translation": [-0.9, -0.6, 0], "scale": [0.25, 0.25, 0.25] },
173 "ground": { "rotation": [0, 0, -45], "translation": [-0.25, 2.65, 0], "scale": [0.15, 0.15, 0.15] },
174 "fixed": { "rotation": [0, 180, -45], "translation": [-0.7, -0.4, 0], "scale": [0.23, 0.23, 0.23] }
175 175 }
176 176 }
Modified src/test/java/com/xfestudio/mydimension/client/builder/BuilderPreviewGenerationPolicyTest.java +103 -0