Initial release

This commit is contained in:
2022-10-27 10:28:51 +02:00
commit e7f9022dfb
55 changed files with 3781 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
package de.nicolasklier.custom_structures;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.item.v1.FabricItemSettings;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroup;
import net.minecraft.util.Identifier;
import net.minecraft.util.registry.Registry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.nicolasklier.custom_structures.events.PlayerTick;
import de.nicolasklier.custom_structures.items.StructureSpawner;
public class CustomStructures implements ModInitializer {
// This logger is used to write text to the console and the log file.
// It is considered best practice to use your mod id as the logger's name.
// That way, it's clear which mod wrote info, warnings, and errors.
public static final Logger LOGGER = LoggerFactory.getLogger("customstructure");
public static final Item STRUCTURE_SPAWNER = new StructureSpawner(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.
// However, some things (like resources) may still be uninitialized.
// Proceed with mild caution.
Registry.register(Registry.ITEM, new Identifier("customstructures", "structure_spawner"), STRUCTURE_SPAWNER);
new PlayerTick();
}
}

View File

@@ -0,0 +1,84 @@
package de.nicolasklier.custom_structures;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.decoration.ItemFrameEntity;
import net.minecraft.entity.projectile.ProjectileUtil;
import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.hit.HitResult;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.RaycastContext;
public class Helper {
public static HitResult raycastInDirection(MinecraftClient client, float tickDelta, Vec3d direction) {
Entity entity = client.getCameraEntity();
if (entity == null || client.world == null) {
return null;
}
double reachDistance = 100f; // Custom reach size of 100 blocks.
HitResult target = raycast(entity, reachDistance, tickDelta, false, direction);
double extendedReach = reachDistance;
Vec3d cameraPos = entity.getCameraPosVec(tickDelta);
extendedReach = extendedReach * extendedReach;
if (target != null) {
extendedReach = target.getPos().squaredDistanceTo(cameraPos);
}
Vec3d vec3d3 = cameraPos.add(direction.multiply(reachDistance));
Box box = entity
.getBoundingBox()
.stretch(entity.getRotationVec(1.0F).multiply(reachDistance))
.expand(1.0D, 1.0D, 1.0D);
EntityHitResult entityHitResult = ProjectileUtil.raycast(
entity,
cameraPos,
vec3d3,
box,
(entityx) -> !entityx.isSpectator(),
extendedReach
);
if (entityHitResult == null) {
return target;
}
Entity entity2 = entityHitResult.getEntity();
Vec3d vec3d4 = entityHitResult.getPos();
double g = cameraPos.squaredDistanceTo(vec3d4);
if (g > reachDistance) {
return null;
} else if (g < extendedReach || target == null) {
target = entityHitResult;
if (entity2 instanceof LivingEntity || entity2 instanceof ItemFrameEntity) {
client.targetedEntity = entity2;
}
}
return target;
}
private static HitResult raycast(
Entity entity,
double maxDistance,
float tickDelta,
boolean includeFluids,
Vec3d direction
) {
Vec3d end = entity.getCameraPosVec(tickDelta).add(direction.multiply(maxDistance));
return entity.world.raycast(new RaycastContext(
entity.getCameraPosVec(tickDelta),
end,
RaycastContext.ShapeType.OUTLINE,
includeFluids ? RaycastContext.FluidHandling.ANY : RaycastContext.FluidHandling.NONE,
entity
));
}
}

View File

@@ -0,0 +1,67 @@
package de.nicolasklier.custom_structures.events;
import java.util.HashMap;
import java.util.Iterator;
import de.nicolasklier.custom_structures.CustomStructures;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.block.BlockState;
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;
/**
* How many ticks to wait until the next block will be placed.
*/
public static int placeDelay = 3;
private static int tickCount = 0;
public PlayerTick() {
registerClientEndTick();
CustomStructures.LOGGER.info("Client end tick event registered.");
}
private void registerClientEndTick() {
ClientTickEvents.END_CLIENT_TICK.register((instance) -> {
if (instance.world == null) {
return;
}
if (tickCount < placeDelay) {
tickCount++;
return;
}
tickCount = 0;
if (queue.isEmpty())
return;
for (int i = 0; i < 100; i++) {
if (queueKeys == null || !queueKeys.hasNext())
return;
BlockPos pos = queueKeys.next();
BlockState state = queue.get(pos);
if (state == null)
return;
//CustomStructures.LOGGER.info("Build at " + pos.toShortString() + "\t Q:" + queue.size());
instance.getServer().getOverworld().setBlockState(pos, state);
instance.world.addBlockBreakParticles(pos, state);
instance.world.playSound(pos, SoundEvents.BLOCK_STONE_PLACE, SoundCategory.BLOCKS, 1f, 1f, true);
queue.remove(pos);
}
});
}
}

View File

@@ -0,0 +1,48 @@
package de.nicolasklier.custom_structures.items;
import de.nicolasklier.custom_structures.Helper;
import de.nicolasklier.custom_structures.structures.Generations;
import de.nicolasklier.custom_structures.structures.StaircaseNoiseOptions;
import de.nicolasklier.custom_structures.structures.StaircaseOptions;
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.world.World;
public class StructureSpawner extends Item {
public StructureSpawner(Settings settings) {
super(settings);
}
@Override
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
MinecraftClient client = MinecraftClient.getInstance();
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;
client.player.sendChatMessage("Coords: " + result.getBlockPos().toShortString(), null);
// Spawn structure at raycast hit
StaircaseOptions options = new StaircaseOptions();
options.stretch = 2;
options.height = 10;
options.noise = new StaircaseNoiseOptions();
options.mirror = true;
gen.spawnStaircase(result.getBlockPos().add(0, 1, 0), options);
}
return TypedActionResult.success(user.getStackInHand(hand));
}
}

View File

@@ -0,0 +1,17 @@
package de.nicolasklier.custom_structures.mixin;
import net.minecraft.client.gui.screen.TitleScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import de.nicolasklier.custom_structures.CustomStructures;
@Mixin(TitleScreen.class)
public class ExampleMixin {
@Inject(at = @At("HEAD"), method = "init()V")
private void init(CallbackInfo info) {
CustomStructures.LOGGER.info("This line is printed by an example mod mixin!");
}
}

View File

@@ -0,0 +1,53 @@
package de.nicolasklier.custom_structures.mixin;
import java.util.HashMap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.block.BlockState;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.BlockPos;
@Mixin(PlayerEntity.class)
public class PlayerTickMixin {
private static HashMap<BlockPos, BlockState> queue = new HashMap<>();
/**
* How many ticks to wait until the next block will be placed.
*/
private static int placeDelay = 2;
private static int tickCount = 0;
@Inject(method = "tick", at = @At("RETURN"))
private void tick(CallbackInfo info) {
if (tickCount < placeDelay) {
tickCount++;
return;
}
tickCount = 0;
MinecraftClient instance = MinecraftClient.getInstance();
if (queue.isEmpty())
return;
BlockPos pos = (BlockPos) queue.keySet().toArray()[0];
BlockState state = queue.get(pos);
queue.remove(pos);
System.out.println("Build at " + pos.toShortString());
instance.world.setBlockState(pos, state);
instance.world.addBlockBreakParticles(pos, state);
instance.world.playSound(pos, SoundEvents.BLOCK_STONE_PLACE, SoundCategory.BLOCKS, 1f, 1f, true);
}
}

View File

@@ -0,0 +1,60 @@
package de.nicolasklier.custom_structures.mixin;
import java.util.Map;
import java.util.Optional;
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.CallbackInfoReturnable;
import de.nicolasklier.custom_structures.worldgen.StairChunkGenerator;
import net.minecraft.util.Identifier;
import net.minecraft.util.registry.BuiltinRegistries;
import net.minecraft.util.registry.Registry;
import net.minecraft.util.registry.RegistryEntry;
import net.minecraft.util.registry.RegistryKey;
import net.minecraft.world.dimension.DimensionOptions;
import net.minecraft.world.dimension.DimensionTypes;
import net.minecraft.world.gen.WorldPreset;
import net.minecraft.world.gen.WorldPresets;
import net.minecraft.world.gen.chunk.ChunkGenerator;
import net.minecraft.world.gen.chunk.FlatChunkGenerator;
import net.minecraft.world.gen.chunk.FlatChunkGeneratorConfig;
@Mixin(WorldPresets.Registrar.class)
public abstract class WorldPresetMixin {
// defining our registry key. this key provides an Identifier for our preset, that we can use for our lang files and data elements.
private static final RegistryKey<WorldPreset> VOID_WORLD = RegistryKey.of(Registry.WORLD_PRESET_KEY, new Identifier("customstructures", "stair_world"));
@Shadow protected abstract RegistryEntry<WorldPreset> register(RegistryKey<WorldPreset> key, DimensionOptions dimensionOptions);
@Shadow protected abstract DimensionOptions createOverworldOptions(ChunkGenerator chunkGenerator);
/*private static final DimensionOptions OVERWORLD_OPTIONS = new DimensionOptions(
BuiltinRegistries.DIMENSION_TYPE.getOrCreateEntry(DimensionTypes.OVERWORLD),
StairChunkGenerator.forOverworld()
);*/
@Inject(method = "initAndGetDefault", at = @At("RETURN"))
private void addPresets(CallbackInfoReturnable<RegistryEntry<WorldPreset>> cir) {
// the register() method is shadowed from the target class
/*this.register(VOID_WORLD, this.createOverworldOptions(
// a FlatChunkGenerator is the easiest way to get a void world, but you can replace this FlatChunkGenerator constructor with a NoiseChunkGenerator, or your own custom ChunkGenerator.
new FlatChunkGenerator(
// passing null will use the default structure set
null,
new FlatChunkGeneratorConfig(
// we don't need to overwrite the structure set
Optional.empty(),
BuiltinRegistries.BIOME)
)
)
);
BuiltinRegistries.add(BuiltinRegistries.WORLD_PRESET, new Identifier("customstructures", "stair_world"), new WorldPreset(
Map.of(
DimensionOptions.OVERWORLD, OVERWORLD_OPTIONS
))
);*/
}
}

View File

@@ -0,0 +1,183 @@
package de.nicolasklier.custom_structures.structures;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import de.nicolasklier.custom_structures.CustomStructures;
import de.nicolasklier.custom_structures.events.PlayerTick;
import de.nicolasklier.custom_structures.utils.PerlinNoise;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.Material;
import net.minecraft.client.MinecraftClient;
import net.minecraft.state.property.Properties;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Direction.Axis;
public class Generations {
List<Block> fence_whitelist = new ArrayList<>();
public Generations() {
fence_whitelist.add(Blocks.OAK_FENCE);
fence_whitelist.add(Blocks.BIRCH_FENCE);
fence_whitelist.add(Blocks.SPRUCE_FENCE);
fence_whitelist.add(Blocks.JUNGLE_FENCE);
}
public void spawnStaircase(BlockPos at, StaircaseOptions options) {
Map<BlockPos, BlockState> placeQueue = new HashMap<>();
MinecraftClient instance = MinecraftClient.getInstance();
int length = options.height * options.stretch * (options.mirror ? 2 : 1);
Random rng = new Random();
PerlinNoise noise = null;
int y = -(options.weight);
int step = 0;
if (options.noise != null) {
noise = new PerlinNoise(rng, options.noise.roughness, length, length);
noise.initialise();
}
// Final length is height times stretch factor. If mirroring is enabled, then it will be twice as long.
int x = 0;
boolean isMirroring = y > options.height * options.stretch;
if (step >= options.stretch) {
step = 0;
// Check if we already reached our final height. If yes, go downwards
if (isMirroring) {
y -= 1;
} else {
y++;
}
}
for (int z = 0; z < options.width; z++) {
boolean isNextHeight = true;
if (options.stretch > 1) {
if (isMirroring) {
isNextHeight = step == 0;
} else {
isNextHeight = step + 1 == options.stretch;
}
}
Block block = Blocks.COBBLESTONE;
// Build supporting fence
if (x % options.fenceVerticalDensity == 0 && y > 0) {
// 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));
int y2 = y - options.weight;
while (true) {
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));
} else {
break;
}
y2--;
}
}
}
boolean putStair = false;
// Decide if there should be mossy cobblestone or glass. If yes, then don't put a stair there.
if (rng.nextFloat() < options.chanceMossyCobblestone / 100f) {
block = Blocks.MOSSY_COBBLESTONE;
} else if (rng.nextFloat() < options.chanceGlass / 100f) {
block = Blocks.GLASS;
} else {
putStair = true;
}
// Build actual block
for (int j = 0; j < options.weight; j++) {
// Building imperfection
if (rng.nextFloat() < 0.001) continue;
placeQueue.put(at.add(x, y + j, z), block.getDefaultState());
}
Direction facing = isMirroring ? Direction.WEST : Direction.EAST;
// Build stair or not
if (putStair && isNextHeight) {
BlockState stairState = Blocks.COBBLESTONE_STAIRS.getDefaultState().with(Properties.HORIZONTAL_FACING, facing);
if (isMirroring)
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());
} else {
CustomStructures.LOGGER.info("Noise [" + x + ", " + y + "]: " + noise.getValueAt(x, y + options.weight));
if (noise.getValueAt(x, y + options.weight) < options.chanceWoodStair / 100f) {
// Override stair as wood stair, copying the facing.
stairState = Blocks.OAK_STAIRS.getDefaultState()
.with(Properties.HORIZONTAL_FACING, stairState.get(Properties.HORIZONTAL_FACING));
} else if (rng.nextFloat() > options.chanceMossyCobblestone / 100f) {
stairState = Blocks.MOSSY_COBBLESTONE_STAIRS.getDefaultState()
.with(Properties.HORIZONTAL_FACING, stairState.get(Properties.HORIZONTAL_FACING));
}
placeQueue.put(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());
}
}
// 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.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());
if (isNextHeight) {
placeQueue.put(at.add(x, y + options.wallHeight + options.weight + 3, z), Blocks.OAK_STAIRS.getDefaultState()
.with(Properties.HORIZONTAL_FACING, facing));
}
}
}
step++;
}
PlayerTick.queue.putAll(placeQueue);
Set<BlockPos> set = placeQueue.keySet();
List<BlockPos> list = new ArrayList<>(set);
//Collections.shuffle(list);
PlayerTick.queueKeys = list.iterator();
}
private BlockState getRandomFance(Random rng) {
return fence_whitelist.get(rng.nextInt(0, fence_whitelist.size() - 1)).getDefaultState();
}
}

View File

@@ -0,0 +1,11 @@
package de.nicolasklier.custom_structures.structures;
import java.util.Random;
public class StaircaseNoiseOptions {
public float roughness = 0.1f;
public Random rng = new Random();
}

View File

@@ -0,0 +1,74 @@
package de.nicolasklier.custom_structures.structures;
public class StaircaseOptions {
/**
* Width of the staircase on the Z-axis.
*/
public int width = 21;
/**
* Thickness of the staircase.
*/
public int weight = 3;
/**
* Height of the staircase on the Y-axis.
*/
public int height = 32;
/**
* Padding between the support fences vertically (XY-plane).
*/
public int fenceVerticalDensity = 10;
/**
* Padding between the support fences horizontally (ZY-plane).
*/
public int fenceHorizontalDensity = 3;
/**
* Height of the wall left & right. Set to 0 to disable this feature.
*/
public int wallHeight = 2;
/**
* Scratch the staircase along the X-axis.
*/
public int stretch = 4;
/**
* Mirror staircase when height is reached. This will double your length.
*/
public boolean mirror = false;
/**
* Value between 0% and 100% being the chance to spawn glass.
*/
public short chanceGlass = 10;
/**
* Value between 0% and 100% being the chance to spawn glowstone.
*/
public short chanceGlowstone = 4;
/**
* Value between 0% and 100% being the chance to spawn mossy cobblestone.
*/
public short chanceMossyCobblestone = 20;
/**
* Value between 0% and 100% being the chance to spawn a slap instead of an stair.
*/
public short chanceSlap = 5;
/**
* Value between 0% and 100% being the chance to spawn a wooden stair instead of an cobblestone stair.
*/
public short chanceWoodStair = 50;
/**
* Adds variation to the stretch of the staircase. This will override `stretch`.
* To disable this feature set it to null.
*/
public StaircaseNoiseOptions noise = null;
}

View File

@@ -0,0 +1,110 @@
package de.nicolasklier.custom_structures.utils;
import java.util.Random;
public class PerlinNoise {
/** Source of entropy */
private Random rand_;
/** Amount of roughness */
float roughness_;
/** Plasma fractal grid */
private float[][] grid_;
/** Generate a noise source based upon the midpoint displacement fractal.
*
* @param rand The random number generator
* @param roughness a roughness parameter
* @param width the width of the grid
* @param height the height of the grid
*/
public PerlinNoise(Random rand, float roughness, int width, int height) {
roughness_ = roughness / width;
grid_ = new float[width][height];
rand_ = (rand == null) ? new Random() : rand;
}
public void initialise() {
int xh = grid_.length - 1;
int yh = grid_[0].length - 1;
// set the corner points
grid_[0][0] = rand_.nextFloat() - 0.5f;
grid_[0][yh] = rand_.nextFloat() - 0.5f;
grid_[xh][0] = rand_.nextFloat() - 0.5f;
grid_[xh][yh] = rand_.nextFloat() - 0.5f;
// generate the fractal
generate(0, 0, xh, yh);
}
// Add a suitable amount of random displacement to a point
private float roughen(float v, int l, int h) {
return v + roughness_ * (float) (rand_.nextGaussian() * (h - l));
}
// generate the fractal
private void generate(int xl, int yl, int xh, int yh) {
int xm = (xl + xh) / 2;
int ym = (yl + yh) / 2;
if ((xl == xm) && (yl == ym)) return;
grid_[xm][yl] = 0.5f * (grid_[xl][yl] + grid_[xh][yl]);
grid_[xm][yh] = 0.5f * (grid_[xl][yh] + grid_[xh][yh]);
grid_[xl][ym] = 0.5f * (grid_[xl][yl] + grid_[xl][yh]);
grid_[xh][ym] = 0.5f * (grid_[xh][yl] + grid_[xh][yh]);
float v = roughen(0.5f * (grid_[xm][yl] + grid_[xm][yh]), xl + yl, yh
+ xh);
grid_[xm][ym] = v;
grid_[xm][yl] = roughen(grid_[xm][yl], xl, xh);
grid_[xm][yh] = roughen(grid_[xm][yh], xl, xh);
grid_[xl][ym] = roughen(grid_[xl][ym], yl, yh);
grid_[xh][ym] = roughen(grid_[xh][ym], yl, yh);
generate(xl, yl, xm, ym);
generate(xm, yl, xh, ym);
generate(xl, ym, xm, yh);
generate(xm, ym, xh, yh);
}
public float getValueAt(int x, int y) {
return grid_[x][y];
}
/**
* Dump out as a CSV
*/
public void printAsCSV() {
for(int i = 0;i < grid_.length;i++) {
for(int j = 0;j < grid_[0].length;j++) {
System.out.print(grid_[i][j]);
System.out.print(",");
}
System.out.println();
}
}
/**
* Convert to a Boolean array
* @return the boolean array
*/
public boolean[][] toBooleans() {
int w = grid_.length;
int h = grid_[0].length;
boolean[][] ret = new boolean[w][h];
for(int i = 0;i < w;i++) {
for(int j = 0;j < h;j++) {
ret[i][j] = grid_[i][j] < 0;
}
}
return ret;
}
}