commit
ab5eb31a28
28 files changed
+1936
No files matched your search
@@ -0,0 +1,9 @@
|
||||
package com.mr.client
|
||||
|
||||
import net.fabricmc.api.ClientModInitializer
|
||||
|
||||
object TurtleReproduceClient : ClientModInitializer {
|
||||
override fun onInitializeClient() {
|
||||
// This entrypoint is suitable for setting up client-specific logic, such as rendering.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.mr.client
|
||||
|
||||
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint
|
||||
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator
|
||||
|
||||
object TurtleReproduceDataGenerator : DataGeneratorEntrypoint {
|
||||
override fun onInitializeDataGenerator(fabricDataGenerator: FabricDataGenerator) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.mr.gametest;
|
||||
|
||||
import com.mr.mixin.TurtleAccessor;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.animal.turtle.Turtle;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.TurtleEggBlock;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public class TurtleNestingGameTests {
|
||||
private static final String SHORE = "turtle-reproduce-gametest:shore";
|
||||
|
||||
@GameTest(structure = SHORE, maxTicks = 1600)
|
||||
public void breedingFarFromHomeLaysVanillaEggs(GameTestHelper helper) {
|
||||
prepareFloor(helper);
|
||||
makeShore(helper, 8, 9);
|
||||
Turtle first = spawnTurtle(helper, 4.5, 10.5);
|
||||
Turtle second = spawnTurtle(helper, 5.5, 10.5);
|
||||
BlockPos firstHome = accessor(first).turtleReproduce$getHomePos();
|
||||
BlockPos secondHome = accessor(second).turtleReproduce$getHomePos();
|
||||
boolean[] becamePregnant = {false};
|
||||
boolean[] dugSand = {false};
|
||||
first.setInLove(null);
|
||||
second.setInLove(null);
|
||||
|
||||
helper.onEachTick(() -> {
|
||||
becamePregnant[0] |= first.hasEgg() || second.hasEgg();
|
||||
dugSand[0] |= first.isLayingEgg() || second.isLayingEgg();
|
||||
assertPregnantTurtleDoesNotGoHome(helper, first);
|
||||
assertPregnantTurtleDoesNotGoHome(helper, second);
|
||||
});
|
||||
helper.succeedWhen(() -> {
|
||||
BlockPos eggs = findEggsOnShore(helper, 8, 9);
|
||||
helper.assertTrue(eggs != null, "The turtles should breed and lay eggs on the nearby shore");
|
||||
helper.assertTrue(becamePregnant[0], "Breeding should produce a pregnant turtle");
|
||||
helper.assertTrue(dugSand[0], "The turtle should perform the vanilla digging animation");
|
||||
helper.assertFalse(first.hasEgg() || second.hasEgg(), "Laying should consume the carried eggs");
|
||||
helper.assertValueEqual(accessor(first).turtleReproduce$getHomePos(), firstHome, "First birth position");
|
||||
helper.assertValueEqual(accessor(second).turtleReproduce$getHomePos(), secondHome, "Second birth position");
|
||||
assertVanillaEggCount(helper, eggs);
|
||||
});
|
||||
}
|
||||
|
||||
@GameTest(structure = SHORE, maxTicks = 1400)
|
||||
public void choosesTheNearestReachableWaterShore(GameTestHelper helper) {
|
||||
prepareFloor(helper);
|
||||
makeShore(helper, 8, 9);
|
||||
makeShore(helper, 19, 9);
|
||||
Turtle turtle = spawnPregnantTurtle(helper, 4.5, 10.5);
|
||||
expectEggs(helper, turtle, 8, 9, 19, 9);
|
||||
}
|
||||
|
||||
@GameTest(structure = SHORE, maxTicks = 1800)
|
||||
public void reachesAShoreBeyondTheVanillaSixteenBlockRange(GameTestHelper helper) {
|
||||
prepareFloor(helper);
|
||||
makeShore(helper, 22, 9);
|
||||
Turtle turtle = spawnPregnantTurtle(helper, 3.5, 10.5);
|
||||
expectEggs(helper, turtle, 22, 9, 8, 9);
|
||||
}
|
||||
|
||||
// Vanilla turtles move slowly away from home and may pause to retry a long route.
|
||||
@GameTest(structure = SHORE, maxTicks = 3000)
|
||||
public void skipsAnEnclosedNearerShore(GameTestHelper helper) {
|
||||
prepareFloor(helper);
|
||||
makeShore(helper, 8, 7);
|
||||
makeShore(helper, 19, 7);
|
||||
// A three-block-high sealed enclosure makes the nearer sand unreachable.
|
||||
for (int y = 2; y <= 4; y++) {
|
||||
for (int x = 5; x <= 12; x++) {
|
||||
helper.setBlock(x, y, 5, Blocks.STONE);
|
||||
helper.setBlock(x, y, 12, Blocks.STONE);
|
||||
}
|
||||
for (int z = 5; z <= 12; z++) {
|
||||
helper.setBlock(5, y, z, Blocks.STONE);
|
||||
helper.setBlock(12, y, z, Blocks.STONE);
|
||||
}
|
||||
}
|
||||
Turtle turtle = spawnPregnantTurtle(helper, 3.5, 8.5);
|
||||
expectEggs(helper, turtle, 19, 7, 8, 7);
|
||||
}
|
||||
|
||||
@GameTest(structure = SHORE, maxTicks = 650)
|
||||
public void keepsEggsWithoutSandAndDoesNotReturnHome(GameTestHelper helper) {
|
||||
prepareFloor(helper);
|
||||
makeWater(helper, 8, 9);
|
||||
Turtle turtle = spawnPregnantTurtle(helper, 4.5, 10.5);
|
||||
BlockPos home = accessor(turtle).turtleReproduce$getHomePos();
|
||||
helper.onEachTick(() -> {
|
||||
helper.assertTrue(turtle.hasEgg(), "No suitable sand: the turtle must retain its eggs");
|
||||
helper.assertFalse(turtle.isLayingEgg(), "The turtle must not dig on stone");
|
||||
helper.assertFalse(accessor(turtle).turtleReproduce$isGoingHome(), "Pregnancy must not start a trip home");
|
||||
helper.assertValueEqual(accessor(turtle).turtleReproduce$getHomePos(), home, "Birth position");
|
||||
});
|
||||
helper.runAtTickTime(600, helper::succeed);
|
||||
}
|
||||
|
||||
@GameTest(structure = SHORE, maxTicks = 650)
|
||||
public void drySandIsNotANestingShore(GameTestHelper helper) {
|
||||
prepareFloor(helper);
|
||||
for (int z = 9; z <= 11; z++) {
|
||||
helper.setBlock(7, 1, z, Blocks.SAND);
|
||||
}
|
||||
Turtle turtle = spawnPregnantTurtle(helper, 4.5, 10.5);
|
||||
helper.onEachTick(() -> {
|
||||
helper.assertTrue(turtle.hasEgg(), "Sand without a nearby water source must not consume the eggs");
|
||||
helper.assertFalse(turtle.isLayingEgg(), "Dry inland sand is not a valid shore");
|
||||
helper.assertFalse(accessor(turtle).turtleReproduce$isGoingHome(), "Pregnancy must not start a trip home");
|
||||
});
|
||||
helper.runAtTickTime(600, helper::succeed);
|
||||
}
|
||||
|
||||
private static void expectEggs(GameTestHelper helper, Turtle turtle,
|
||||
int expectedWaterX, int expectedWaterZ,
|
||||
int rejectedWaterX, int rejectedWaterZ) {
|
||||
BlockPos home = accessor(turtle).turtleReproduce$getHomePos();
|
||||
boolean[] dugSand = {false};
|
||||
helper.onEachTick(() -> {
|
||||
dugSand[0] |= turtle.isLayingEgg();
|
||||
assertPregnantTurtleDoesNotGoHome(helper, turtle);
|
||||
});
|
||||
helper.succeedWhen(() -> {
|
||||
BlockPos eggs = findEggsOnShore(helper, expectedWaterX, expectedWaterZ);
|
||||
helper.assertTrue(eggs != null, "The turtle should lay eggs on the selected reachable shore");
|
||||
helper.assertTrue(findEggsOnShore(helper, rejectedWaterX, rejectedWaterZ) == null,
|
||||
"The other shore must not receive eggs");
|
||||
helper.assertTrue(dugSand[0], "The turtle should dig before laying");
|
||||
helper.assertFalse(turtle.hasEgg(), "The laid eggs should no longer be carried");
|
||||
helper.assertValueEqual(accessor(turtle).turtleReproduce$getHomePos(), home, "Birth position");
|
||||
assertVanillaEggCount(helper, eggs);
|
||||
});
|
||||
}
|
||||
|
||||
private static Turtle spawnTurtle(GameTestHelper helper, double x, double z) {
|
||||
Turtle turtle = helper.spawn(EntityType.TURTLE, new Vec3(x, 2, z));
|
||||
turtle.setAge(0);
|
||||
turtle.setPersistenceRequired();
|
||||
turtle.getRandom().setSeed(12345L);
|
||||
turtle.setHomePos(helper.absolutePos(new BlockPos(2048, 2, 2048)));
|
||||
return turtle;
|
||||
}
|
||||
|
||||
private static Turtle spawnPregnantTurtle(GameTestHelper helper, double x, double z) {
|
||||
Turtle turtle = spawnTurtle(helper, x, z);
|
||||
accessor(turtle).turtleReproduce$setHasEgg(true);
|
||||
return turtle;
|
||||
}
|
||||
|
||||
private static void prepareFloor(GameTestHelper helper) {
|
||||
for (int x = 0; x < 26; x++) {
|
||||
for (int z = 0; z < 24; z++) {
|
||||
helper.setBlock(x, 0, z, Blocks.STONE);
|
||||
helper.setBlock(x, 1, z, Blocks.STONE);
|
||||
for (int y = 2; y < 6; y++) {
|
||||
helper.setBlock(x, y, z, Blocks.AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void makeShore(GameTestHelper helper, int waterX, int waterZ) {
|
||||
makeWater(helper, waterX, waterZ);
|
||||
for (int z = waterZ; z < waterZ + 3; z++) {
|
||||
helper.setBlock(waterX - 1, 1, z, Blocks.SAND);
|
||||
}
|
||||
}
|
||||
|
||||
private static void makeWater(GameTestHelper helper, int waterX, int waterZ) {
|
||||
for (int x = waterX; x < waterX + 3; x++) {
|
||||
for (int z = waterZ; z < waterZ + 3; z++) {
|
||||
helper.setBlock(x, 1, z, Blocks.WATER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockPos findEggsOnShore(GameTestHelper helper, int waterX, int waterZ) {
|
||||
for (int z = waterZ; z < waterZ + 3; z++) {
|
||||
BlockPos pos = new BlockPos(waterX - 1, 2, z);
|
||||
if (helper.getBlockState(pos).is(Blocks.TURTLE_EGG)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void assertVanillaEggCount(GameTestHelper helper, BlockPos pos) {
|
||||
int count = helper.getBlockState(pos).getValue(TurtleEggBlock.EGGS);
|
||||
helper.assertTrue(count >= 1 && count <= 4, "Vanilla laying should produce one to four hatchable eggs");
|
||||
}
|
||||
|
||||
private static void assertPregnantTurtleDoesNotGoHome(GameTestHelper helper, Turtle turtle) {
|
||||
if (turtle.hasEgg()) {
|
||||
helper.assertFalse(accessor(turtle).turtleReproduce$isGoingHome(), "A pregnant turtle must ignore its distant home");
|
||||
}
|
||||
}
|
||||
|
||||
private static TurtleAccessor accessor(Turtle turtle) {
|
||||
return (TurtleAccessor) turtle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.mr.gametest;
|
||||
|
||||
import com.mr.ai.NestingSiteFinder;
|
||||
import com.mr.mixin.TurtleAccessor;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.animal.turtle.Turtle;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.TurtleEggBlock;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.pathfinder.Path;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public class WetNestingGameTests {
|
||||
private static final BlockPos NEAR_SAND = new BlockPos(7, 1, 10);
|
||||
private static final BlockPos FAR_SAND = new BlockPos(13, 1, 10);
|
||||
|
||||
@GameTest(structure = "turtle-reproduce-gametest:shore", maxTicks = 1800)
|
||||
public void skipsDrySandWhoseStandingTurtleWouldTouchWater(GameTestHelper helper) {
|
||||
prepareFloor(helper);
|
||||
helper.setBlock(NEAR_SAND, Blocks.SAND);
|
||||
helper.setBlock(FAR_SAND, Blocks.SAND);
|
||||
helper.setBlock(8, 1, 10, Blocks.WATER);
|
||||
helper.setBlock(15, 1, 10, Blocks.WATER);
|
||||
|
||||
// The pane's solid shape stays in the middle of its block. The turtle
|
||||
// only extends 0.1 blocks into that block, touching fluid but no glass.
|
||||
// Keep the pane north of the sand, outside the eastbound route to the
|
||||
// fallback, so this tests destination validation rather than navigation
|
||||
// through a waterlogged block's solid center.
|
||||
// Place this last without neighbor updates so the fixture's contained
|
||||
// water is not scheduled to flow across the otherwise dry sand.
|
||||
helper.getLevel().setBlock(helper.absolutePos(new BlockPos(7, 2, 9)),
|
||||
Blocks.GLASS_PANE.defaultBlockState().setValue(BlockStateProperties.WATERLOGGED, true),
|
||||
Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
|
||||
|
||||
Turtle turtle = helper.spawn(EntityType.TURTLE, new Vec3(4.5, 2, 10.5));
|
||||
turtle.setAge(0);
|
||||
turtle.setPersistenceRequired();
|
||||
turtle.getRandom().setSeed(12345L);
|
||||
BlockPos home = helper.absolutePos(new BlockPos(2048, 2, 2048));
|
||||
turtle.setHomePos(home);
|
||||
|
||||
BlockPos nearAbsolute = helper.absolutePos(NEAR_SAND);
|
||||
BlockPos farAbsolute = helper.absolutePos(FAR_SAND);
|
||||
Vec3 nearDestination = Vec3.atBottomCenterOf(nearAbsolute.above());
|
||||
AABB nearBounds = turtle.getBoundingBox().move(nearDestination.subtract(turtle.position()));
|
||||
assertWetButOtherwiseUsable(helper, turtle, nearAbsolute, nearBounds);
|
||||
|
||||
List<NestingSiteFinder.Candidate> candidates = NestingSiteFinder.findCandidates(turtle);
|
||||
helper.assertTrue(!candidates.isEmpty() && candidates.getFirst().sandPos().equals(nearAbsolute),
|
||||
"The wet standing position must be the first candidate before the body-fluid check");
|
||||
helper.assertTrue(candidates.stream().anyMatch(candidate -> candidate.sandPos().equals(farAbsolute)),
|
||||
"The dry fallback sand must also be a valid shore candidate");
|
||||
Path nearPath = turtle.getNavigation().createPath(nearAbsolute.above(), 0, 48);
|
||||
helper.assertTrue(nearPath != null && nearPath.canReach(),
|
||||
"The near sand must have a navigation path, so only the body-fluid check rejects it");
|
||||
|
||||
((TurtleAccessor) turtle).turtleReproduce$setHasEgg(true);
|
||||
boolean[] dugSand = {false};
|
||||
helper.onEachTick(() -> {
|
||||
// If the fixture ever floods, fail instead of mistaking ordinary
|
||||
// invalid sand for the intended bounding-box regression coverage.
|
||||
assertWetButOtherwiseUsable(helper, turtle, nearAbsolute, nearBounds);
|
||||
dugSand[0] |= turtle.isLayingEgg();
|
||||
if (turtle.hasEgg()) {
|
||||
helper.assertFalse(((TurtleAccessor) turtle).turtleReproduce$isGoingHome(),
|
||||
"A pregnant turtle must not return to its distant birth position");
|
||||
}
|
||||
});
|
||||
helper.succeedWhen(() -> {
|
||||
helper.assertTrue(helper.getBlockState(FAR_SAND.above()).is(Blocks.TURTLE_EGG),
|
||||
"The turtle must lay real eggs on the farther, dry shore");
|
||||
helper.assertFalse(helper.getBlockState(NEAR_SAND.above()).is(Blocks.TURTLE_EGG),
|
||||
"Sand whose standing turtle touches water must not receive eggs");
|
||||
helper.assertTrue(dugSand[0], "The turtle must perform the vanilla digging animation");
|
||||
helper.assertFalse(turtle.hasEgg(), "Successful laying must consume the carried eggs");
|
||||
int eggs = helper.getBlockState(FAR_SAND.above()).getValue(TurtleEggBlock.EGGS);
|
||||
helper.assertTrue(eggs >= 1 && eggs <= 4, "The result must retain the vanilla egg count");
|
||||
helper.assertValueEqual(((TurtleAccessor) turtle).turtleReproduce$getHomePos(), home,
|
||||
"Birth position");
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertWetButOtherwiseUsable(GameTestHelper helper, Turtle turtle,
|
||||
BlockPos nearAbsolute, AABB nearBounds) {
|
||||
helper.assertTrue(helper.getBlockState(NEAR_SAND.above()).isAir(),
|
||||
"The near sand's own standing block must remain air");
|
||||
helper.assertTrue(NestingSiteFinder.isValidSite(helper.getLevel(), nearAbsolute),
|
||||
"The near sand must remain valid under the sand, air and source-water rules");
|
||||
helper.assertTrue(helper.getLevel().noCollision(turtle, nearBounds),
|
||||
"No solid collision may explain rejection of the near sand");
|
||||
helper.assertTrue(helper.getLevel().containsAnyLiquid(nearBounds.deflate(0.001)),
|
||||
"The standing turtle's width must overlap the neighboring waterlogged pane's fluid");
|
||||
}
|
||||
|
||||
private static void prepareFloor(GameTestHelper helper) {
|
||||
for (int x = 0; x < 26; x++) {
|
||||
for (int z = 0; z < 24; z++) {
|
||||
helper.setBlock(x, 0, z, Blocks.STONE);
|
||||
helper.setBlock(x, 1, z, Blocks.STONE);
|
||||
for (int y = 2; y < 6; y++) {
|
||||
helper.setBlock(x, y, z, Blocks.AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
DataVersion: 2730,
|
||||
size: [26, 6, 24],
|
||||
data: [{pos: [0, 0, 0], state: "minecraft:air"}],
|
||||
entities: [],
|
||||
palette: ["minecraft:air"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "turtle-reproduce-gametest",
|
||||
"version": "1.0.0",
|
||||
"name": "Turtle Reproduce Game Tests",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"fabric-gametest": [
|
||||
"com.mr.gametest.TurtleNestingGameTests",
|
||||
"com.mr.gametest.WetNestingGameTests"
|
||||
]
|
||||
},
|
||||
"depends": {
|
||||
"turtle-reproduce": "*",
|
||||
"fabric-gametest-api-v1": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.mr.ai;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.entity.animal.turtle.Turtle;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.block.TurtleEggBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Finds dry nesting blocks beside nearby, exposed source water.
|
||||
*
|
||||
* <p>This class was rewritten for throughput. The scan is the hot path: one call
|
||||
* covers 73 x 73 columns x 19 vertical steps, and a pregnant turtle re-runs it
|
||||
* whenever it drifts more than four blocks from the previous search origin or
|
||||
* exhausts its cached candidate list.
|
||||
*
|
||||
* <p>Every result-affecting rule is unchanged:
|
||||
* <ul>
|
||||
* <li>the same box is visited (32 in X/Z for nests, plus a 4 block halo for
|
||||
* water, 8 up/down for nests, plus 1 for water);</li>
|
||||
* <li>the same conditions are tested, in the same order;</li>
|
||||
* <li>unloaded chunks are still skipped, and are still never loaded or
|
||||
* generated ({@code loadOrGenerate = false});</li>
|
||||
* <li>the candidate list is still ordered by the same comparator, which ends
|
||||
* in the block coordinates and therefore is a total order. Visit order no
|
||||
* longer matters, so the sort result is identical.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>What changed, and why it is equivalent:
|
||||
* <ul>
|
||||
* <li>blocks are read through the chunk section that holds them instead of
|
||||
* {@code Level#getBlockState}, which re-resolves the chunk, the section
|
||||
* index and the build-height check on every single block. Sections that
|
||||
* {@code hasOnlyAir()} are skipped entirely: they cannot contribute water
|
||||
* or sand;</li>
|
||||
* <li>the same availability test as {@code Level#hasChunk} is used, but the
|
||||
* resolved chunk is kept for the section reads instead of being thrown
|
||||
* away and looked up again per block;</li>
|
||||
* <li>sand positions are collected as packed longs instead of thousands of
|
||||
* {@code BlockPos} objects, so a scan allocates no per-block garbage;</li>
|
||||
* <li>{@code isValidSite} hoists its loop invariants and remembers the last
|
||||
* chunk availability answer; the disk it walks spans at most four chunks,
|
||||
* so this only removes repeated lookups.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class NestingSiteFinder {
|
||||
public static final int SEARCH_RADIUS = 32;
|
||||
public static final int VERTICAL_SEARCH_RADIUS = 8;
|
||||
public static final int WATER_RADIUS = 4;
|
||||
public static final int WATER_VERTICAL_RADIUS = 1;
|
||||
|
||||
private static final Comparator<Candidate> CANDIDATE_ORDER = Comparator
|
||||
.comparingDouble(Candidate::waterDistanceSquared)
|
||||
.thenComparingDouble(Candidate::sandDistanceSquared)
|
||||
.thenComparingInt(candidate -> candidate.sandPos().getX())
|
||||
.thenComparingInt(candidate -> candidate.sandPos().getY())
|
||||
.thenComparingInt(candidate -> candidate.sandPos().getZ());
|
||||
|
||||
private NestingSiteFinder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a square extending 32 blocks in X/Z and 8 blocks in Y from the
|
||||
* turtle. A four-block halo includes water beside nests at the boundary.
|
||||
* The caller must check the turtle's collision box and navigation path.
|
||||
*/
|
||||
public static List<Candidate> findCandidates(Turtle turtle) {
|
||||
Level level = turtle.level();
|
||||
BlockPos origin = turtle.blockPosition();
|
||||
int originX = origin.getX();
|
||||
int originY = origin.getY();
|
||||
int originZ = origin.getZ();
|
||||
int scanRadius = SEARCH_RADIUS + WATER_RADIUS;
|
||||
int minY = Math.max(level.getMinY(), originY - VERTICAL_SEARCH_RADIUS - WATER_VERTICAL_RADIUS);
|
||||
int maxY = Math.min(level.getMaxY() - 1, originY + VERTICAL_SEARCH_RADIUS + WATER_VERTICAL_RADIUS);
|
||||
int nestMinX = originX - SEARCH_RADIUS;
|
||||
int nestMaxX = originX + SEARCH_RADIUS;
|
||||
int nestMinZ = originZ - SEARCH_RADIUS;
|
||||
int nestMaxZ = originZ + SEARCH_RADIUS;
|
||||
int nestMinY = originY - VERTICAL_SEARCH_RADIUS;
|
||||
int nestMaxY = originY + VERTICAL_SEARCH_RADIUS;
|
||||
|
||||
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
|
||||
LongArrayList sandBlocks = new LongArrayList();
|
||||
Long2DoubleOpenHashMap waterDistances = new Long2DoubleOpenHashMap();
|
||||
waterDistances.defaultReturnValue(Double.POSITIVE_INFINITY);
|
||||
|
||||
// Read each block once, instead of repeatedly querying the world around
|
||||
// every sand block. Unloaded chunks are never requested or generated.
|
||||
if (minY <= maxY) {
|
||||
Vec3 turtlePos = turtle.position();
|
||||
int minSection = level.getSectionIndex(minY);
|
||||
int maxSection = level.getSectionIndex(maxY);
|
||||
for (int chunkX = (originX - scanRadius) >> 4; chunkX <= (originX + scanRadius) >> 4; chunkX++) {
|
||||
int fromX = Math.max(originX - scanRadius, chunkX << 4);
|
||||
int toX = Math.min(originX + scanRadius, (chunkX << 4) + 15);
|
||||
for (int chunkZ = (originZ - scanRadius) >> 4; chunkZ <= (originZ + scanRadius) >> 4; chunkZ++) {
|
||||
// The same test Level#hasChunk performs, except the chunk is
|
||||
// kept: loadOrGenerate = false never loads or generates.
|
||||
ChunkAccess chunk = level.getChunk(chunkX, chunkZ, ChunkStatus.FULL, false);
|
||||
if (chunk == null) {
|
||||
continue;
|
||||
}
|
||||
int fromZ = Math.max(originZ - scanRadius, chunkZ << 4);
|
||||
int toZ = Math.min(originZ + scanRadius, (chunkZ << 4) + 15);
|
||||
LevelChunkSection[] sections = chunk.getSections();
|
||||
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
|
||||
if (sectionIndex < 0 || sectionIndex >= sections.length) {
|
||||
continue;
|
||||
}
|
||||
LevelChunkSection section = sections[sectionIndex];
|
||||
// An all-air section holds neither water nor sand, so
|
||||
// skipping it reads no block the loops could have used.
|
||||
if (section == null || section.hasOnlyAir()) {
|
||||
continue;
|
||||
}
|
||||
int sectionMinY = SectionPos.sectionToBlockCoord(level.getMinSectionY() + sectionIndex);
|
||||
int fromY = Math.max(minY, sectionMinY);
|
||||
int toY = Math.min(maxY, sectionMinY + 15);
|
||||
boolean nestSection = fromY <= nestMaxY && toY >= nestMinY;
|
||||
for (int x = fromX; x <= toX; x++) {
|
||||
int localX = x & 15;
|
||||
boolean nestX = x >= nestMinX && x <= nestMaxX;
|
||||
for (int z = fromZ; z <= toZ; z++) {
|
||||
boolean inNestRange = nestSection && nestX && z >= nestMinZ && z <= nestMaxZ;
|
||||
int localZ = z & 15;
|
||||
for (int y = fromY; y <= toY; y++) {
|
||||
BlockState state = section.getBlockState(localX, y & 15, localZ);
|
||||
if (isExposedSourceWater(level, cursor, x, y, z, state)) {
|
||||
cursor.set(x, y, z);
|
||||
waterDistances.put(BlockPos.asLong(x, y, z),
|
||||
cursor.distToCenterSqr(turtlePos));
|
||||
}
|
||||
if (inNestRange && y >= nestMinY && y <= nestMaxY
|
||||
&& state.is(BlockTags.SAND)
|
||||
&& level.isEmptyBlock(cursor.set(x, y + 1, z))) {
|
||||
sandBlocks.add(BlockPos.asLong(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
if (waterDistances.isEmpty()) {
|
||||
return candidates;
|
||||
}
|
||||
Vec3 turtlePos = turtle.position();
|
||||
for (int index = 0; index < sandBlocks.size(); index++) {
|
||||
long packedSand = sandBlocks.getLong(index);
|
||||
int sandX = BlockPos.getX(packedSand);
|
||||
int sandY = BlockPos.getY(packedSand);
|
||||
int sandZ = BlockPos.getZ(packedSand);
|
||||
double nearestWaterDistance = Double.POSITIVE_INFINITY;
|
||||
for (int dx = -WATER_RADIUS; dx <= WATER_RADIUS; dx++) {
|
||||
for (int dz = -WATER_RADIUS; dz <= WATER_RADIUS; dz++) {
|
||||
if (dx * dx + dz * dz > WATER_RADIUS * WATER_RADIUS) {
|
||||
continue;
|
||||
}
|
||||
for (int dy = -WATER_VERTICAL_RADIUS; dy <= WATER_VERTICAL_RADIUS; dy++) {
|
||||
long waterPosition = BlockPos.asLong(sandX + dx, sandY + dy, sandZ + dz);
|
||||
nearestWaterDistance = Math.min(nearestWaterDistance, waterDistances.get(waterPosition));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Double.isFinite(nearestWaterDistance)) {
|
||||
cursor.set(sandX, sandY, sandZ);
|
||||
candidates.add(new Candidate(new BlockPos(sandX, sandY, sandZ), nearestWaterDistance,
|
||||
cursor.distToCenterSqr(turtlePos)));
|
||||
}
|
||||
}
|
||||
candidates.sort(CANDIDATE_ORDER);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/** Rechecks a chosen nest, including changes to its sand, air, or water. */
|
||||
public static boolean isValidSite(LevelReader level, BlockPos sandPos) {
|
||||
if (!level.isInsideBuildHeight(sandPos) || !level.isInsideBuildHeight(sandPos.above())
|
||||
|| !level.hasChunkAt(sandPos)
|
||||
|| !TurtleEggBlock.isSand(level, sandPos)
|
||||
|| !level.isEmptyBlock(sandPos.above())) {
|
||||
return false;
|
||||
}
|
||||
int sandX = sandPos.getX();
|
||||
int sandY = sandPos.getY();
|
||||
int sandZ = sandPos.getZ();
|
||||
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
|
||||
int lastChunkX = Integer.MIN_VALUE;
|
||||
int lastChunkZ = Integer.MIN_VALUE;
|
||||
boolean lastChunkLoaded = false;
|
||||
for (int dx = -WATER_RADIUS; dx <= WATER_RADIUS; dx++) {
|
||||
for (int dz = -WATER_RADIUS; dz <= WATER_RADIUS; dz++) {
|
||||
if (dx * dx + dz * dz > WATER_RADIUS * WATER_RADIUS) {
|
||||
continue;
|
||||
}
|
||||
int x = sandX + dx;
|
||||
int z = sandZ + dz;
|
||||
int chunkX = x >> 4;
|
||||
int chunkZ = z >> 4;
|
||||
// The disk above spans at most four chunks, so this only removes
|
||||
// repeated queries for columns that share a chunk.
|
||||
if (chunkX != lastChunkX || chunkZ != lastChunkZ) {
|
||||
lastChunkX = chunkX;
|
||||
lastChunkZ = chunkZ;
|
||||
lastChunkLoaded = level.hasChunk(chunkX, chunkZ);
|
||||
}
|
||||
if (!lastChunkLoaded) {
|
||||
continue;
|
||||
}
|
||||
for (int dy = -WATER_VERTICAL_RADIUS; dy <= WATER_VERTICAL_RADIUS; dy++) {
|
||||
int y = sandY + dy;
|
||||
cursor.set(x, y, z);
|
||||
if (level.isInsideBuildHeight(y) && level.isInsideBuildHeight(y + 1)
|
||||
&& isExposedSourceWater(level, cursor, x, y, z, level.getBlockState(cursor))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isExposedSourceWater(LevelReader level, BlockPos.MutableBlockPos cursor,
|
||||
int x, int y, int z, BlockState state) {
|
||||
// Requiring a liquid block excludes waterlogged fences, slabs and other
|
||||
// solid blocks even though their fluid state may report source water.
|
||||
if (!state.liquid()) {
|
||||
return false;
|
||||
}
|
||||
FluidState fluid = state.getFluidState();
|
||||
return fluid.is(FluidTags.WATER) && fluid.isSource() && level.isEmptyBlock(cursor.set(x, y + 1, z));
|
||||
}
|
||||
|
||||
public record Candidate(BlockPos sandPos, double waterDistanceSquared, double sandDistanceSquared) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.mr.mixin;
|
||||
|
||||
import net.minecraft.world.entity.ai.navigation.PathNavigation;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(PathNavigation.class)
|
||||
public interface PathNavigationAccessor {
|
||||
@Accessor("maxVisitedNodesMultiplier")
|
||||
float turtleReproduce$getMaxVisitedNodesMultiplier();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.mr.mixin;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.animal.turtle.Turtle;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
@Mixin(Turtle.class)
|
||||
public interface TurtleAccessor {
|
||||
@Invoker("setHasEgg")
|
||||
void turtleReproduce$setHasEgg(boolean hasEgg);
|
||||
|
||||
@Invoker("setLayingEgg")
|
||||
void turtleReproduce$setLayingEgg(boolean layingEgg);
|
||||
|
||||
@Accessor("homePos")
|
||||
BlockPos turtleReproduce$getHomePos();
|
||||
|
||||
@Accessor("goingHome")
|
||||
boolean turtleReproduce$isGoingHome();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.mr.mixin;
|
||||
|
||||
import net.minecraft.world.entity.animal.turtle.Turtle;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(targets = "net.minecraft.world.entity.animal.turtle.Turtle$TurtleGoHomeGoal")
|
||||
public abstract class TurtleGoHomeGoalMixin {
|
||||
@Shadow @Final
|
||||
private Turtle turtle;
|
||||
|
||||
// This goal has no MOVE flag, so priority alone cannot stop it fighting nesting.
|
||||
@Inject(method = {"canUse", "canContinueToUse"}, at = @At("HEAD"), cancellable = true)
|
||||
private void turtleReproduce$stayLocalWhileCarryingEggs(CallbackInfoReturnable<Boolean> cir) {
|
||||
if (this.turtle.hasEgg()) {
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "stop", at = @At("TAIL"))
|
||||
private void turtleReproduce$discardOldHomeRoute(CallbackInfo ci) {
|
||||
if (this.turtle.hasEgg()) {
|
||||
// The vanilla stop only clears goingHome, leaving its last route active.
|
||||
this.turtle.getNavigation().stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.mr.mixin;
|
||||
|
||||
import com.mr.ai.NestingSiteFinder;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.PathfinderMob;
|
||||
import net.minecraft.world.entity.ai.goal.MoveToBlockGoal;
|
||||
import net.minecraft.world.entity.ai.navigation.PathNavigation;
|
||||
import net.minecraft.world.entity.animal.turtle.Turtle;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.pathfinder.Path;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(targets = "net.minecraft.world.entity.animal.turtle.Turtle$TurtleLayEggGoal")
|
||||
public abstract class TurtleLayEggGoalMixin extends MoveToBlockGoal {
|
||||
@Unique private static final int PATH_SEARCH_RANGE = 48;
|
||||
@Unique private static final int PATH_ATTEMPTS_PER_CHECK = 16;
|
||||
|
||||
@Shadow @Final
|
||||
private Turtle turtle;
|
||||
@Unique private List<NestingSiteFinder.Candidate> turtleReproduce$candidates = List.of();
|
||||
@Unique private int turtleReproduce$candidateIndex;
|
||||
@Unique private BlockPos turtleReproduce$searchOrigin = BlockPos.ZERO;
|
||||
@Unique private Path turtleReproduce$nestingPath;
|
||||
|
||||
protected TurtleLayEggGoalMixin(PathfinderMob mob, double speed, int range) {
|
||||
super(mob, speed, range);
|
||||
}
|
||||
|
||||
@Inject(method = "canUse", at = @At("HEAD"), cancellable = true)
|
||||
private void turtleReproduce$findLocalShore(CallbackInfoReturnable<Boolean> cir) {
|
||||
cir.setReturnValue(this.turtle.hasEgg() && !this.turtle.isBaby() && super.canUse());
|
||||
}
|
||||
|
||||
@Inject(method = "canContinueToUse", at = @At("HEAD"), cancellable = true)
|
||||
private void turtleReproduce$continueAwayFromHome(CallbackInfoReturnable<Boolean> cir) {
|
||||
cir.setReturnValue(this.turtle.hasEgg() && super.canContinueToUse());
|
||||
}
|
||||
|
||||
@Inject(method = "isValidTarget", at = @At("HEAD"), cancellable = true)
|
||||
private void turtleReproduce$requireDrySandNearWater(LevelReader level, BlockPos pos,
|
||||
CallbackInfoReturnable<Boolean> cir) {
|
||||
cir.setReturnValue(NestingSiteFinder.isValidSite(level, pos));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean findNearestBlock() {
|
||||
if (this.turtleReproduce$candidateIndex >= this.turtleReproduce$candidates.size()
|
||||
|| this.turtle.blockPosition().distSqr(this.turtleReproduce$searchOrigin) > 16.0) {
|
||||
this.turtleReproduce$candidates = NestingSiteFinder.findCandidates(this.turtle);
|
||||
this.turtleReproduce$candidateIndex = 0;
|
||||
this.turtleReproduce$searchOrigin = this.turtle.blockPosition();
|
||||
}
|
||||
|
||||
// Spread expensive path checks over AI updates when a shore is inaccessible.
|
||||
int attempts = 0;
|
||||
while (this.turtleReproduce$candidateIndex < this.turtleReproduce$candidates.size()
|
||||
&& attempts++ < PATH_ATTEMPTS_PER_CHECK) {
|
||||
BlockPos sand = this.turtleReproduce$candidates.get(this.turtleReproduce$candidateIndex++).sandPos();
|
||||
if (!this.turtle.isWithinHome(sand) || !NestingSiteFinder.isValidSite(this.turtle.level(), sand)) {
|
||||
continue;
|
||||
}
|
||||
Vec3 destination = Vec3.atBottomCenterOf(sand.above());
|
||||
AABB destinationBounds = this.turtle.getBoundingBox().move(destination.subtract(this.turtle.position()));
|
||||
// Adult turtles are wider than one block. An air block alone does not
|
||||
// guarantee that their body is clear of adjacent water or walls.
|
||||
if (!this.turtle.level().noCollision(this.turtle, destinationBounds)
|
||||
|| this.turtle.level().containsAnyLiquid(destinationBounds.deflate(0.001))) {
|
||||
continue;
|
||||
}
|
||||
Path path = this.turtleReproduce$createNestingPath(sand.above());
|
||||
if (path != null && path.canReach()) {
|
||||
this.blockPos = sand;
|
||||
this.turtleReproduce$nestingPath = path;
|
||||
this.turtleReproduce$candidates = List.of();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.turtleReproduce$candidateIndex < this.turtleReproduce$candidates.size()) {
|
||||
this.nextStartTick = 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Unique
|
||||
private Path turtleReproduce$createNestingPath(BlockPos destination) {
|
||||
PathNavigation navigation = this.turtle.getNavigation();
|
||||
float previousMultiplier = ((PathNavigationAccessor) navigation).turtleReproduce$getMaxVisitedNodesMultiplier();
|
||||
// A larger distance alone leaves vanilla's small node budget unchanged,
|
||||
// incorrectly rejecting shores that require a detour around an obstacle.
|
||||
navigation.setMaxVisitedNodesMultiplier(Math.max(previousMultiplier, 8.0F));
|
||||
try {
|
||||
return navigation.createPath(destination, 0, PATH_SEARCH_RANGE);
|
||||
} finally {
|
||||
navigation.setMaxVisitedNodesMultiplier(previousMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void moveMobToBlock() {
|
||||
this.turtle.getNavigation().moveTo(this.turtleReproduce$nestingPath, this.speedModifier);
|
||||
}
|
||||
|
||||
@Inject(method = "tick", at = @At("TAIL"))
|
||||
private void turtleReproduce$retryLongerPath(CallbackInfo ci) {
|
||||
if (!this.isReachedTarget() && this.tryTicks % 40 == 0 && this.turtle.getNavigation().isDone()) {
|
||||
this.turtleReproduce$nestingPath = this.turtleReproduce$createNestingPath(this.blockPos.above());
|
||||
this.moveMobToBlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
this.turtle.getNavigation().stop();
|
||||
((TurtleAccessor) this.turtle).turtleReproduce$setLayingEgg(false);
|
||||
this.turtleReproduce$nestingPath = null;
|
||||
this.turtleReproduce$candidates = List.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.mr
|
||||
|
||||
import net.fabricmc.api.ModInitializer
|
||||
import net.minecraft.resources.Identifier
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object TurtleReproduce : ModInitializer {
|
||||
const val MOD_ID: String = "turtle-reproduce"
|
||||
|
||||
private val LOGGER = LoggerFactory.getLogger(MOD_ID)
|
||||
|
||||
override fun onInitialize() {
|
||||
LOGGER.info("Turtle Reproduce: local waterside nesting enabled")
|
||||
}
|
||||
|
||||
fun id(path: String): Identifier
|
||||
= Identifier.fromNamespaceAndPath(MOD_ID, path)
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "turtle-reproduce",
|
||||
"version": "${version}",
|
||||
"name": "Turtle-Reproduce",
|
||||
"description": "Turtles lay eggs on reachable sand near the closest local water source instead of returning to their birthplace.",
|
||||
"authors": [
|
||||
"Me!"
|
||||
],
|
||||
"license": "CC0-1.0",
|
||||
"icon": "assets/turtle-reproduce/icon.png",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
{
|
||||
"value": "com.mr.TurtleReproduce",
|
||||
"adapter": "kotlin"
|
||||
}
|
||||
],
|
||||
"client": [
|
||||
{
|
||||
"value": "com.mr.client.TurtleReproduceClient",
|
||||
"adapter": "kotlin"
|
||||
}
|
||||
],
|
||||
"fabric-datagen": [
|
||||
{
|
||||
"value": "com.mr.client.TurtleReproduceDataGenerator",
|
||||
"adapter": "kotlin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"turtle-reproduce.mixins.json"
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=0.19.5",
|
||||
"minecraft": "~26.1.2",
|
||||
"java": ">=25",
|
||||
"fabric-api": "*",
|
||||
"fabric-language-kotlin": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "com.mr.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"PathNavigationAccessor",
|
||||
"TurtleAccessor",
|
||||
"TurtleGoHomeGoalMixin",
|
||||
"TurtleLayEggGoalMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"overwrites": {
|
||||
"requireAnnotations": true
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user