Add z-shift to staircase.

Add broken game of life implementation.
This commit is contained in:
2022-10-28 17:06:13 +02:00
parent eb5a3e1a76
commit e4a65316fa
13 changed files with 425 additions and 68 deletions

View File

@@ -4,6 +4,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.nicolasklier.custom_structures.events.PlayerTick;
import de.nicolasklier.custom_structures.items.GameOfLifeSpawner;
import de.nicolasklier.custom_structures.items.StructureSpawner;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.item.v1.FabricItemSettings;
@@ -23,6 +24,9 @@ public class CustomStructures implements ModInitializer {
public static final Item STRUCTURE_SPAWNER = new StructureSpawner(new FabricItemSettings().group(ItemGroup.MISC)
.maxCount(1));
public static final Item GOL_SPAWNER = new GameOfLifeSpawner(new FabricItemSettings().group(ItemGroup.MISC)
.maxCount(1));
@Override
public void onInitialize() {
// This code runs as soon as Minecraft is in a mod-load-ready state.
@@ -30,6 +34,7 @@ public class CustomStructures implements ModInitializer {
// Proceed with mild caution.
Registry.register(Registry.ITEM, new Identifier("customstructures", "structure_spawner"), STRUCTURE_SPAWNER);
Registry.register(Registry.ITEM, new Identifier("customstructures", "gameoflife_spawner"), GOL_SPAWNER);
new PlayerTick();
}

View File

@@ -0,0 +1,145 @@
package de.nicolasklier.custom_structures;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.nicolasklier.custom_structures.utils.Vector2;
import net.minecraft.util.math.BlockPos;
public class GameOfLife {
public static final Logger LOGGER = LoggerFactory.getLogger("customstructure:gameoflife");
private boolean[][] grid; // Finished grid
private boolean[][] wGrid; // Working grid
private int size;
/**
* Position within Minecraft world.
*/
private BlockPos pos;
public GameOfLife(int size, BlockPos pos) {
this.size = size;
this.pos = pos;
long begin = System.currentTimeMillis();
grid = new boolean[size][size];
wGrid = new boolean[size][size];
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
grid[x][y] = false;
wGrid[x][y] = false;
}
}
long end = System.currentTimeMillis();
LOGGER.info("Initialized grid with " + (size ^ size) + " (" + size + "x" + size + ") cells in " + (end - begin) + "ms.");
}
public boolean[][] getGrid() {
return grid;
}
public int getSize() {
return size;
}
public BlockPos getPosition() {
return pos;
}
// Look away.
private int booleanToInt(boolean bool ) {
return bool ? 1 : 0;
}
/**
* Project out of bounds coordinates on finite space.
* @param c Coordinates
* @return Fixed coordinates
*/
private Vector2 project(Vector2 c) {
// Do nothing if coordinate is within bounds.
if ((c.x > 0 && c.y > 0) && (c.x < size && c.y < size))
return c;
// If x/y is negative, move them to the end of the array.
if (c.x < 0) c.x = size + c.x;
if (c.y < 0) c.y = size + c.y;
// If x/y are greater than our size, move them to the beginning of the array.
if (c.x >= size) c.x = c.x % size;
if (c.y >= size) c.y = c.y % size;
return c;
}
/**
* 0 0 1
* 0 C 1
* 1 0 0
*
* C = Our target cell.
* Result should be three according to Moore' neighborhood
*
* @param c Coordinate to check
**/
private int getNeighbours(Vector2 c) {
return booleanToInt(getCell(new Vector2(c.x - 1, c.y + 1))) + // Top left
booleanToInt(getCell(new Vector2(c.x, c.y + 1))) + // Top center
booleanToInt(getCell(new Vector2(c.x + 1, c.y + 1))) + // Top right
booleanToInt(getCell(new Vector2(c.x - 1, c.y))) + // Left
booleanToInt(getCell(new Vector2(c.x + 1, c.y))) + // Right
booleanToInt(getCell(new Vector2(c.x - 1, c.y - 1))) + // Bottom left
booleanToInt(getCell(new Vector2(c.x, c.y - 1))) + // Bottom center
booleanToInt(getCell(new Vector2(c.x + 1, c.y - 1))); // Bottom right
}
public void createCell(Vector2 c) {
c = project(c);
grid[c.x][c.y] = true;
}
public void killCell(Vector2 c) {
c = project(c);
grid[c.x][c.y] = false;
}
public boolean getCell(Vector2 c) {
Vector2 proj = project(c);
return grid[proj.x][proj.y];
}
/**
* Simulate next generation.
*/
public void tick() {
System.out.println("Tick!");
// Copy current displayed grid to a working grid.
wGrid = grid;
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
Vector2 c = new Vector2(x, y);
int neighbours = getNeighbours(c);
if (wGrid[x][y]) {
if (neighbours < 2) wGrid[x][y] = false;
else if (neighbours == 2 || neighbours == 3) wGrid[x][y] = true;
else if (neighbours > 3) wGrid[x][y] = false;
} else {
// Creates new alive cell because of reproduction.
if (neighbours == 3) wGrid[x][y] = true;
}
}
}
grid = wGrid;
}
}

View File

@@ -1,20 +1,22 @@
package de.nicolasklier.custom_structures.events;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.List;
import de.nicolasklier.custom_structures.CustomStructures;
import de.nicolasklier.custom_structures.GameOfLife;
import de.nicolasklier.custom_structures.utils.BlockStatePosition;
import de.nicolasklier.custom_structures.utils.Vector2;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.BlockPos;
public class PlayerTick {
public static HashMap<BlockPos, BlockState> queue = new HashMap<>();
public static Iterator<BlockPos> queueKeys;
public static List<BlockStatePosition> queue = new ArrayList<>();
/**
* How many ticks to wait until the next block will be placed.
@@ -22,6 +24,9 @@ public class PlayerTick {
public static int placeDelay = 3;
private static int tickCount = 0;
public static GameOfLife gof;
public static boolean gofStart = false;
public PlayerTick() {
registerClientEndTick();
@@ -42,26 +47,56 @@ public class PlayerTick {
tickCount = 0;
// Game of Life simulation
if (gof != null && gofStart) {
// Get current state of the field.
for (int x = 0; x < gof.getSize(); x++) {
for (int z = 0; z < gof.getSize(); z++) {
BlockPos pos = gof.getPosition().add(x, 0, z);
// System.out.println("Search at " + pos.toShortString() + ": " + instance.world.getBlockState(pos).getBlock().getName());
if (instance.world.getBlockState(pos).getBlock() == Blocks.OBSIDIAN) {
gof.createCell(new Vector2(x, z));
} else {
gof.killCell(new Vector2(x, z));
}
}
}
gof.tick();
// Set new simulated field
for (int x = 0; x < gof.getSize(); x++) {
for (int z = 0; z < gof.getSize(); z++) {
BlockPos pos = gof.getPosition().add(x, 0, z);
// System.out.println("Search at " + pos.toShortString() + ": " + instance.world.getBlockState(pos).getBlock().getName());
if (gof.getCell(Vector2.fromBlockPos(pos))) {
instance.getServer().getOverworld().setBlockState(pos, Blocks.OBSIDIAN.getDefaultState());
} else {
instance.getServer().getOverworld().setBlockState(pos, Blocks.AIR.getDefaultState());
}
}
}
}
if (queue.isEmpty())
return;
for (int i = 0; i < 100; i++) {
if (queueKeys == null || !queueKeys.hasNext())
return;
for (int i = 0; i < 50; i++) {
if (queue.isEmpty())
break;
BlockPos pos = queueKeys.next();
BlockState state = queue.get(pos);
BlockStatePosition block = queue.remove(0);
if (state == null)
return;
//CustomStructures.LOGGER.info("Built at " + pos.toShortString() + "\t Q:" + queue.size());
//CustomStructures.LOGGER.info("Build at " + pos.toShortString() + "\t Q:" + queue.size());
instance.getServer().getOverworld().setBlockState(pos, state, Block.NOTIFY_NEIGHBORS);
instance.world.addBlockBreakParticles(pos, state);
instance.world.playSound(pos, SoundEvents.BLOCK_STONE_PLACE, SoundCategory.BLOCKS, 1f, 1f, true);
queue.remove(pos);
instance.getServer().getOverworld().setBlockState(block.pos, block.state, Block.NOTIFY_LISTENERS);
instance.world.addBlockBreakParticles(block.pos, block.state);
instance.world.playSound(block.pos, SoundEvents.BLOCK_STONE_PLACE, SoundCategory.BLOCKS, 1f, 1f, true);
}
});
}

View File

@@ -5,12 +5,9 @@ import io.github.cottonmc.cotton.gui.widget.WButton;
import io.github.cottonmc.cotton.gui.widget.WGridPanel;
import io.github.cottonmc.cotton.gui.widget.WLabel;
import io.github.cottonmc.cotton.gui.widget.WSlider;
import io.github.cottonmc.cotton.gui.widget.WSprite;
import io.github.cottonmc.cotton.gui.widget.data.Axis;
import io.github.cottonmc.cotton.gui.widget.data.Insets;
import net.minecraft.client.gui.hud.MessageIndicator.Icon;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
public class SettingsGui extends LightweightGuiDescription {

View File

@@ -0,0 +1,73 @@
package de.nicolasklier.custom_structures.items;
import java.util.ArrayList;
import java.util.List;
import de.nicolasklier.custom_structures.GameOfLife;
import de.nicolasklier.custom_structures.Helper;
import de.nicolasklier.custom_structures.events.PlayerTick;
import de.nicolasklier.custom_structures.guis.SettingsGui;
import de.nicolasklier.custom_structures.guis.SettingsScreen;
import de.nicolasklier.custom_structures.structures.Generations;
import net.minecraft.block.Blocks;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Hand;
import net.minecraft.util.TypedActionResult;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.hit.HitResult;
import net.minecraft.util.hit.HitResult.Type;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
public class GameOfLifeSpawner extends Item {
public GameOfLifeSpawner(Settings settings) {
super(settings);
}
@Override
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
MinecraftClient client = MinecraftClient.getInstance();
// Block event on server-side.
if(!world.isClient()) {
return TypedActionResult.success(user.getStackInHand(hand));
}
// If the player is sneaking, we open a setting menu.
if (client.player.isSneaking()) {
PlayerTick.gofStart = !PlayerTick.gofStart;
} else {
HitResult hit = Helper.raycastInDirection(client, client.getTickDelta(), client.cameraEntity.getRotationVec(client.getTickDelta()));
Generations gen = new Generations();
if (hit.getType() == Type.BLOCK) {
BlockHitResult result = (BlockHitResult) hit;
final int SPACE = 100;
BlockPos pos = result.getBlockPos();
PlayerTick.gof = new GameOfLife(SPACE, pos.add(0, 1, 0));
// Build box around simulation
// Ground
for (int x = 0; x < SPACE; x++) {
for (int z = 0; z < SPACE; z++) {
if (x == 0 || x == SPACE - 1) {
if (z == 0 || z == SPACE - 1) {
client.getServer().getOverworld().setBlockState(pos.add(x, 1, z), Blocks.QUARTZ_SLAB.getDefaultState());
}
}
client.getServer().getOverworld().setBlockState(pos.add(x, 0, z), Blocks.QUARTZ_BLOCK.getDefaultState());
}
}
}
}
return TypedActionResult.success(user.getStackInHand(hand));
}
}

View File

@@ -25,10 +25,14 @@ public class StructureSpawner extends Item {
@Override
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
System.out.println("Used item!");
MinecraftClient client = MinecraftClient.getInstance();
// Block event on server-side.
if(!world.isClient()) {
return TypedActionResult.success(user.getStackInHand(hand));
}
System.out.println("Used item!");
// If the player is sneaking, we open a setting menu.
if (client.player.isSneaking()) {
@@ -47,6 +51,7 @@ public class StructureSpawner extends Item {
options.width = 5;
options.height = 20;
options.noise = new StaircaseNoiseOptions();
options.noise.shiftZ = true;
options.noise.threshold = 0.64f;
options.minecartTrack = true;
options.mirror = true;

View File

@@ -1,16 +1,13 @@
package de.nicolasklier.custom_structures.structures;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.spongepowered.noise.module.source.Perlin;
import de.nicolasklier.custom_structures.CustomStructures;
import de.nicolasklier.custom_structures.events.PlayerTick;
import de.nicolasklier.custom_structures.utils.BlockStatePosition;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
@@ -32,7 +29,8 @@ public class Generations {
}
public void spawnStaircase(BlockPos at, StaircaseOptions options) {
Map<BlockPos, BlockState> placeQueue = new HashMap<>();
List<BlockStatePosition> placeQueue = new ArrayList<>();
MinecraftClient instance = MinecraftClient.getInstance();
int length = options.height * options.stretch * (options.mirror ? 2 : 1);
@@ -46,6 +44,11 @@ public class Generations {
: 0;
int minecartRedstoneTorchStep = 0;
/**
* Since how many blocks are we going straight?
*/
int straightBlocks = 0;
int y = -(options.weight);
int step = 0;
@@ -66,6 +69,32 @@ public class Generations {
while (true) {
x++;
BlockPos currentShift = null;
BlockPos nextShift = null;
// Calculate z-shift
if (noise != null) {
double currentShiftNoise = noise.get(x + 1_000, -5_000, 0);
double nextShiftNoise = noise.get(x + 1_000 + 1, -5_000, 0);
currentShift = currentShiftNoise < options.noise.shiftRightThreshold ? new BlockPos(0, 0, 1) :
(currentShiftNoise > options.noise.shiftRightThreshold && currentShiftNoise < options.noise.shiftLeftThreshold ? new BlockPos(0, 0, -1) : BlockPos.ORIGIN);
nextShift = nextShiftNoise < options.noise.shiftRightThreshold ? new BlockPos(0, 0, 1) :
(nextShiftNoise > options.noise.shiftRightThreshold && nextShiftNoise < options.noise.shiftLeftThreshold ? new BlockPos(0, 0, -1) : BlockPos.ORIGIN);
if (currentShift.getZ() == 0) {
straightBlocks++;
} else {
// Block shift if we shifted one block before. The reason is that rails are buggy.
if (straightBlocks < 1) {
currentShift = BlockPos.ORIGIN;
nextShift = BlockPos.ORIGIN;
}
straightBlocks = 0;
}
}
boolean isMirroring = y > options.height && options.mirror || reachedTop;
if (isMirroring) {
@@ -88,6 +117,11 @@ public class Generations {
else y++;
}
// Shift staircase if enabled.
if (options.noise.shiftZ) {
at = at.add(currentShift);
}
System.out.println("Noise: " + noise.get(x, 0, 0) + " | X: " + x + " Y: " + y);
} else {
if (step >= options.stretch) {
@@ -132,25 +166,21 @@ public class Generations {
// From left to right
for (int z2 = 0; z2 <= options.width; z2 += (options.width - 1) / options.fenceHorizontalDensity) {
// Place fence manually on ground because the loop below offsets by one.
placeQueue.put(at.add(x + 1, 0, z2), getRandomFance(rng));
placeQueue.add(new BlockStatePosition(new BlockPos(x + 1, 0, z2), getRandomFance(rng)));
int y2 = y + 2;
while (true) {
break;
Material mat = instance.getServer().getOverworld().getBlockState(new BlockPos(x, y2, z2)).getMaterial();
if (mat == Material.AIR || mat == Material.WATER) {
placeQueue.put(at.add(x, y2, z2), getRandomFance(rng));
placeQueue.put(at.add(x + 1, y2 + 1, z2), getRandomFance(rng));
for (int y2 = y + options.weight; y > -64; y2--) {
BlockPos pos = new BlockPos(x, y2, z2);
Block mat = instance.getServer().getOverworld().getBlockState(pos).getBlock();
System.out.println("Fence " + pos.toShortString() + "\t(" + mat.toString() + ")");
if (mat == Blocks.AIR || mat == Blocks.WATER) {
System.out.println("Set fence at " + pos.toShortString());
placeQueue.add(new BlockStatePosition(new BlockPos(x, y2, z2), getRandomFance(rng)));
placeQueue.add(new BlockStatePosition(new BlockPos(x + 1, y2 + 1, z2), getRandomFance(rng)));
} else {
break;
}
System.out.println("Fence stuff " + y2 + "(" + mat.toString() + ")");
if (y2 <= -64)
break;
y2--;
}
}
}*/
@@ -171,7 +201,7 @@ public class Generations {
// Building imperfection
if (rng.nextFloat() < 0.001) continue;
placeQueue.put(at.add(x, y + j, z), block.getDefaultState());
placeQueue.add(new BlockStatePosition(at.add(x, y + j, z), block.getDefaultState()));
}
Direction facing = isMirroring ? Direction.WEST : Direction.EAST;
@@ -184,7 +214,7 @@ public class Generations {
stairState = stairState.with(Properties.HORIZONTAL_FACING, facing);
if (rng.nextFloat() < options.chanceSlap / 100f) {
placeQueue.put(at.add(x, y + options.weight, z), Blocks.SMOOTH_STONE_SLAB.getDefaultState());
placeQueue.add(new BlockStatePosition(at.add(x, y + options.weight, z), Blocks.SMOOTH_STONE_SLAB.getDefaultState()));
} else {
//CustomStructures.LOGGER.info("Noise [" + x + ", " + y + ", " + z + "]: " + noise.get(x, y + options.weight, z));
if (noise.get(x, y + options.weight, 0) < options.chanceWoodStair / 100f) {
@@ -197,40 +227,52 @@ public class Generations {
.with(Properties.HORIZONTAL_FACING, stairState.get(Properties.HORIZONTAL_FACING));
}
placeQueue.put(at.add(x, y + options.weight, z), stairState);
placeQueue.add(new BlockStatePosition(at.add(x, y + options.weight, z), stairState));
}
} else {
if (rng.nextFloat() < 0.04) {
placeQueue.put(at.add(x, y + options.weight - 1, z), Blocks.GLOWSTONE.getDefaultState());
if (rng.nextFloat() < options.chanceGlowstone / 100) {
placeQueue.add(new BlockStatePosition(at.add(x, y + options.weight - 1, z), Blocks.GLOWSTONE.getDefaultState()));
}
}
// Build wall
if ((z == 0 || z == options.width - 1) && options.wallHeight > 0) {
for (int y2 = 0; y2 < options.wallHeight; y2++) {
placeQueue.put(at.add(x, y + y2 + options.weight, z), Blocks.OAK_PLANKS.getDefaultState());
placeQueue.add(new BlockStatePosition(at.add(x, y + y2 + options.weight, z), Blocks.OAK_PLANKS.getDefaultState()));
}
placeQueue.put(at.add(x, y + options.wallHeight + options.weight, z), Blocks.OAK_FENCE.getDefaultState());
placeQueue.put(at.add(x, y + options.wallHeight + options.weight + 1, z), Blocks.OAK_FENCE.getDefaultState());
placeQueue.put(at.add(x, y + options.wallHeight + options.weight + 2, z), Blocks.OAK_PLANKS.getDefaultState());
placeQueue.add(new BlockStatePosition(at.add(x, y + options.wallHeight + options.weight, z), Blocks.OAK_FENCE.getDefaultState()));
placeQueue.add(new BlockStatePosition(at.add(x, y + options.wallHeight + options.weight + 1, z), Blocks.OAK_FENCE.getDefaultState()));
placeQueue.add(new BlockStatePosition(at.add(x, y + options.wallHeight + options.weight + 2, z), Blocks.OAK_PLANKS.getDefaultState()));
if (isNextHeight) {
placeQueue.put(at.add(x, y + options.wallHeight + options.weight + 3, z), Blocks.OAK_STAIRS.getDefaultState()
.with(Properties.HORIZONTAL_FACING, facing));
placeQueue.add(new BlockStatePosition(at.add(x, y + options.wallHeight + options.weight + 3, z), Blocks.OAK_STAIRS.getDefaultState()
.with(Properties.HORIZONTAL_FACING, facing)));
}
}
// Minecart rails
if (minecartTrackPosition > 0 && z == minecartTrackPosition) {
System.array
if (minecartRedstoneTorchStep >= 4) {
placeQueue.put(at.add(x, y + options.weight - 2, z), Blocks.REDSTONE_TORCH.getDefaultState());
placeQueue.put(at.add(x, y + options.weight, z), Blocks.POWERED_RAIL.getDefaultState());
// Place additional rail if we shift staircase to left or right.
if (noise != null && options.noise.shiftZ) {
placeQueue.add(new BlockStatePosition(at.add(x, y + options.weight, z).add(nextShift), Blocks.RAIL.getDefaultState()));
}
if (minecartRedstoneTorchStep >= 4) {
minecartRedstoneTorchStep = 0;
// Don't place powered rail because they can only go straight.
if (noise != null && options.noise.shiftZ) {
if (nextShift.getZ() != 0) {
placeQueue.add(new BlockStatePosition(at.add(x, y + options.weight, z).add(nextShift), Blocks.RAIL.getDefaultState()));
continue;
}
}
placeQueue.add(new BlockStatePosition(at.add(x, y + options.weight - 2, z), Blocks.REDSTONE_TORCH.getDefaultState()));
placeQueue.add(new BlockStatePosition(at.add(x, y + options.weight, z), Blocks.POWERED_RAIL.getDefaultState()));
} else {
placeQueue.put(at.add(x, y + options.weight, z), Blocks.RAIL.getDefaultState());
placeQueue.add(new BlockStatePosition(at.add(x, y + options.weight, z), Blocks.RAIL.getDefaultState()));
}
minecartRedstoneTorchStep++;
@@ -240,12 +282,8 @@ public class Generations {
step++;
}
PlayerTick.queue.putAll(placeQueue);
Set<BlockPos> set = placeQueue.keySet();
List<BlockPos> list = new ArrayList<>(set);
//Collections.shuffle(list);
PlayerTick.queueKeys = list.iterator();
PlayerTick.queue.addAll(placeQueue);
}

View File

@@ -11,6 +11,15 @@ public class StaircaseNoiseOptions {
*/
public float threshold = 0.7f;
/**
* If set to true, the staircase will change its z-coordinate based on the noise.
*/
public boolean shiftZ = false;
public double shiftLeftThreshold = 0.55;
public double shiftRightThreshold = 0.4;
public Random rng = new Random();
}

View File

@@ -49,7 +49,7 @@ public class StaircaseOptions {
/**
* Value between 0% and 100% being the chance to spawn glowstone.
*/
public short chanceGlowstone = 4;
public short chanceGlowstone = 0;
/**
* Value between 0% and 100% being the chance to spawn mossy cobblestone.

View File

@@ -0,0 +1,20 @@
package de.nicolasklier.custom_structures.utils;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
/**
* Tiny struct like class to store position and block state in one object.
*
*/
public class BlockStatePosition {
public BlockPos pos;
public BlockState state;
public BlockStatePosition(BlockPos pos, BlockState state) {
this.pos = pos;
this.state = state;
}
}

View File

@@ -0,0 +1,24 @@
package de.nicolasklier.custom_structures.utils;
import net.minecraft.util.math.BlockPos;
public class Vector2 {
public int x, y;
public Vector2(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return this.x + " " + this.y;
}
public static Vector2 fromBlockPos(BlockPos pos) {
// Minecraft's coordinates are flipped.
return new Vector2(pos.getX(), pos.getZ());
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "customstructures:item/gameoflife_spawner"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B