返回提交历史
Added
src/main/java/com/xfestudio/mydimension/builder/BuilderBlockCompatibility.java
+53
-0
Modified
src/main/java/com/xfestudio/mydimension/builder/BuilderGameTests.java
+159
-0
Modified
src/main/java/com/xfestudio/mydimension/builder/BuilderOperationManager.java
+99
-43
Modified
src/main/java/com/xfestudio/mydimension/builder/SurfacePlanner.java
+2
-1
Modified
src/main/java/com/xfestudio/mydimension/client/builder/BuilderPreviewRenderer.java
+10
-3
Modified
src/main/java/com/xfestudio/mydimension/client/builder/BuilderPreviewSectionMeshCache.java
+365
-48
Modified
src/main/java/com/xfestudio/mydimension/client/builder/BuilderSurfacePreviewPlanner.java
+3
-2
Modified
src/main/resources/assets/mydimension/models/item/realmwright_scepter.json
+5
-5
Modified
src/main/resources/assets/mydimension/models/item/realmwright_scepter_base.json
+54
-54
Modified
src/test/java/com/xfestudio/mydimension/client/builder/BuilderPreviewGenerationPolicyTest.java
+103
-0
XFEstudio/MyDimension
修复部分问题
5c184b6
代码差异
10 个文件
+853
-156
@@ -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
}
@@ -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));
@@ -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
}
@@ -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) {
@@ -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(),
@@ -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();
@@ -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
}
@@ -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
}
@@ -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
}