Merged in forwolk/dev/world_gen (pull request #2)
Forwolk/dev/world gen
This commit is contained in:
@@ -33,6 +33,7 @@ subprojects {
|
|||||||
|
|
||||||
/* Components */
|
/* Components */
|
||||||
compile (group: 'org.projectlombok', name: 'lombok', version: '1.16.16')
|
compile (group: 'org.projectlombok', name: 'lombok', version: '1.16.16')
|
||||||
|
compile 'com.flowpowered:flow-nbt:1.0.0' //Named Binary Tags
|
||||||
}
|
}
|
||||||
|
|
||||||
task copyDep(type: Copy) {
|
task copyDep(type: Copy) {
|
||||||
|
|||||||
8
core/src/main/java/mc/core/Direction.java
Normal file
8
core/src/main/java/mc/core/Direction.java
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package mc.core;
|
||||||
|
|
||||||
|
public enum Direction {
|
||||||
|
NORTH,
|
||||||
|
EAST,
|
||||||
|
WEST,
|
||||||
|
SOUTH
|
||||||
|
}
|
||||||
@@ -7,9 +7,11 @@ package mc.core;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Data
|
@Data
|
||||||
public class Location {
|
public class Location implements Serializable{
|
||||||
private double x, y, z;
|
private double x, y, z;
|
||||||
|
|
||||||
private static int floor_double(double value) {
|
private static int floor_double(double value) {
|
||||||
@@ -25,6 +27,10 @@ public class Location {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Location startPointLocation () {
|
||||||
|
return new Location(0,10,0);
|
||||||
|
}
|
||||||
|
|
||||||
public Location(long compactValue) {
|
public Location(long compactValue) {
|
||||||
set(compactValue);
|
set(compactValue);
|
||||||
}
|
}
|
||||||
|
|||||||
14
core/src/main/java/mc/core/WarpPosition.java
Normal file
14
core/src/main/java/mc/core/WarpPosition.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package mc.core;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import mc.core.player.Look;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class WarpPosition implements Serializable {
|
||||||
|
private Location location;
|
||||||
|
private Look look;
|
||||||
|
}
|
||||||
49
core/src/main/java/mc/core/block/AbstractBlock.java
Normal file
49
core/src/main/java/mc/core/block/AbstractBlock.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package mc.core.block;
|
||||||
|
|
||||||
|
import com.flowpowered.nbt.Tag;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import mc.core.Location;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public abstract class AbstractBlock implements Block {
|
||||||
|
@Getter@Setter
|
||||||
|
private Location location;
|
||||||
|
@Getter
|
||||||
|
private int meta;
|
||||||
|
@Getter
|
||||||
|
private final BlockType blockType;
|
||||||
|
private final Map<String, Tag<?>> nbtTagsMap = new HashMap<>();
|
||||||
|
|
||||||
|
protected AbstractBlock(BlockType type) {
|
||||||
|
this.blockType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AbstractBlock(BlockType type, int meta) {
|
||||||
|
this.blockType = type;
|
||||||
|
this.meta = meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return blockType.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tag<?> getTag(String name) {
|
||||||
|
return nbtTagsMap.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTag(Tag<?> tag) {
|
||||||
|
nbtTagsMap.put(tag.getName(), tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<Tag<?>> tagStream() {
|
||||||
|
return nbtTagsMap.values().stream();
|
||||||
|
}
|
||||||
|
}
|
||||||
64
core/src/main/java/mc/core/block/Block.java
Normal file
64
core/src/main/java/mc/core/block/Block.java
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package mc.core.block;
|
||||||
|
|
||||||
|
import com.flowpowered.nbt.Tag;
|
||||||
|
import mc.core.Location;
|
||||||
|
import mc.core.nbt.Taggable;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialization block info
|
||||||
|
*
|
||||||
|
* +------------+--------+------------+
|
||||||
|
* | param | range | bits |
|
||||||
|
* +------------+--------+------------+
|
||||||
|
* | id | 0:255 | 8 |
|
||||||
|
* +------------+--------+------------+
|
||||||
|
* | meta | 0:15 | 4 |
|
||||||
|
* +------------+--------+------------+
|
||||||
|
* | x | 0:15 | 4 |
|
||||||
|
* +------------+--------+------------+
|
||||||
|
* | y | 0:15 | 4 |
|
||||||
|
* +------------+--------+------------+
|
||||||
|
* | z | 0:15 | 4 |
|
||||||
|
* +------------+--------+------------+
|
||||||
|
*
|
||||||
|
* Total: 24 bits per block (3 bytes)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface Block extends Taggable, Serializable{
|
||||||
|
|
||||||
|
static Block airBlock (int x, int y, int z) {
|
||||||
|
return new AbstractBlock(BlockType.AIR) {
|
||||||
|
@Override
|
||||||
|
public Tag<?> getTag(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Location getLocation() {
|
||||||
|
return new Location(x, y, z);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Block id */
|
||||||
|
int getId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Addition in 0-15
|
||||||
|
* F.e. 35:0 - white wool
|
||||||
|
* 35:15 - black wool
|
||||||
|
*/
|
||||||
|
int getMeta();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting block type
|
||||||
|
*/
|
||||||
|
BlockType getBlockType();
|
||||||
|
|
||||||
|
/** Block location */
|
||||||
|
Location getLocation();
|
||||||
|
|
||||||
|
}
|
||||||
20
core/src/main/java/mc/core/block/BlockFactory.java
Normal file
20
core/src/main/java/mc/core/block/BlockFactory.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package mc.core.block;
|
||||||
|
|
||||||
|
import mc.core.Location;
|
||||||
|
|
||||||
|
public class BlockFactory {
|
||||||
|
|
||||||
|
public Block create(BlockType blockType, int meta) {
|
||||||
|
return new EmbeddedBlock(blockType, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For first-time generation
|
||||||
|
*/
|
||||||
|
private class EmbeddedBlock extends AbstractBlock {
|
||||||
|
EmbeddedBlock(BlockType type, int meta) {
|
||||||
|
super(type, meta);
|
||||||
|
super.setLocation(new Location(0,0,0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
core/src/main/java/mc/core/block/BlockType.java
Normal file
25
core/src/main/java/mc/core/block/BlockType.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package mc.core.block;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public enum BlockType {
|
||||||
|
STONE(1, "Stone"),
|
||||||
|
GRASS(2, "Grass"),
|
||||||
|
DIRT(3, "Dirt"),
|
||||||
|
BEDROCK(7, "Bedrock"),
|
||||||
|
WATER(8, "Water"),
|
||||||
|
SAND(12, "Sand"),
|
||||||
|
SNOW(32, "Snow"),
|
||||||
|
AIR(0, "Air");
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final int id;
|
||||||
|
@Getter
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
BlockType(int id, String name) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
core/src/main/java/mc/core/nbt/Taggable.java
Normal file
11
core/src/main/java/mc/core/nbt/Taggable.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package mc.core.nbt;
|
||||||
|
|
||||||
|
import com.flowpowered.nbt.Tag;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public interface Taggable {
|
||||||
|
Tag<?> getTag(String name);
|
||||||
|
void setTag(Tag<?> tag);
|
||||||
|
Stream<Tag<?>> tagStream();
|
||||||
|
}
|
||||||
@@ -7,9 +7,11 @@ package mc.core.player;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class Look {
|
public class Look implements Serializable{
|
||||||
private float yaw, pitch;
|
private float yaw, pitch;
|
||||||
|
|
||||||
public void set(Look look) {
|
public void set(Look look) {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package mc.core.serialization;
|
||||||
|
|
||||||
|
public interface Deserializer<T> {
|
||||||
|
T deserialize (byte[] bytes);
|
||||||
|
}
|
||||||
10
core/src/main/java/mc/core/serialization/IChunkReader.java
Normal file
10
core/src/main/java/mc/core/serialization/IChunkReader.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package mc.core.serialization;
|
||||||
|
|
||||||
|
import mc.core.world.Chunk;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface IChunkReader {
|
||||||
|
Chunk read (Region region, int x, int y, int z) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package mc.core.serialization;
|
||||||
|
|
||||||
|
import mc.core.world.Region;
|
||||||
|
import mc.core.world.World;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface IRegionReaderWriter {
|
||||||
|
|
||||||
|
Region read (int x, int z, World world) throws IOException;
|
||||||
|
void write (Region region) throws IOException;
|
||||||
|
}
|
||||||
5
core/src/main/java/mc/core/serialization/Serializer.java
Normal file
5
core/src/main/java/mc/core/serialization/Serializer.java
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package mc.core.serialization;
|
||||||
|
|
||||||
|
public interface Serializer<T> {
|
||||||
|
byte[] serialize (T t);
|
||||||
|
}
|
||||||
21
core/src/main/java/mc/core/utils/UuidUtils.java
Normal file
21
core/src/main/java/mc/core/utils/UuidUtils.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package mc.core.utils;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class UuidUtils {
|
||||||
|
|
||||||
|
public static UUID asUuid(byte[] bytes) {
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bytes);
|
||||||
|
long firstLong = bb.getLong();
|
||||||
|
long secondLong = bb.getLong();
|
||||||
|
return new UUID(firstLong, secondLong);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] asBytes(UUID uuid) {
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
|
||||||
|
bb.putLong(uuid.getMostSignificantBits());
|
||||||
|
bb.putLong(uuid.getLeastSignificantBits());
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
core/src/main/java/mc/core/world/Biome.java
Normal file
50
core/src/main/java/mc/core/world/Biome.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package mc.core.world;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public enum Biome {
|
||||||
|
OCEAN(0, "Ocean", 0x0000cd),
|
||||||
|
PLAINS(1, "Plains", 0x008000),
|
||||||
|
DESERT(2, "Desert", 0xffe4b5),
|
||||||
|
EXTREME_HILLS(3, "Extreme hills", 0xffffff),
|
||||||
|
FOREST(4, "Forest", 0x006400),
|
||||||
|
TAIGA(5, "Taiga", 0xf0f8ff),
|
||||||
|
SWAMPLAND(6, "Swampland", 0x808000),
|
||||||
|
RIVER(7, "River", 0x0000cd),
|
||||||
|
HELL(8, "Hell", 0x800000),
|
||||||
|
SKY(9, "Sky", 0xffffff),
|
||||||
|
FROZEN_OCEAN(10, "Frozen ocean", 0xe0ffff),
|
||||||
|
FROZEN_RIVER(11, "Frozen river", 0xe0ffff),
|
||||||
|
ICE_PLAINS(12, "Ice plains", 0xfffafa),
|
||||||
|
ICE_MOUNTAINS(13, "Ice mountains", 0xfffafa),
|
||||||
|
MUSHROOM_ISLAND(14, "Mushroom island", 0xffffff),
|
||||||
|
MUSHROOM_ISLAND_SHORE(15, "Mushroom island shore", 0xffffff),
|
||||||
|
BEACH(16, "Beach", 0xffffff),
|
||||||
|
DESERT_HILLS(17, "Desert hills", 0xffe4b5),
|
||||||
|
FOREST_HILLS(18, "Forest hills", 0x006400),
|
||||||
|
TAIGA_HILLS(19, "Taiga hills", 0xf0f8ff),
|
||||||
|
EXTREME_HILLS_EDGE(20, "Extreme hills edge", 0xffffff),
|
||||||
|
JUNGLE(21, "Jungle", 0xadff2f),
|
||||||
|
JUNGLE_HILLS(22, "Jungle hills", 0xadff2f),
|
||||||
|
DEEP_OCEAN(23, "Deep ocean", 0x000080),
|
||||||
|
TUNDRA(24, "Tundra", 0xc0c0c0),
|
||||||
|
SAVANNA(25, "Savana", 0xcd8513),
|
||||||
|
SAVANNA_FOREST(26, "Savana forest", 0x8b4513);
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final int id;
|
||||||
|
@Getter
|
||||||
|
private final String name;
|
||||||
|
@Getter
|
||||||
|
private final int color;
|
||||||
|
|
||||||
|
Biome(int id, String name, int color) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Biome getById(int id) {
|
||||||
|
return Biome.values()[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,25 @@
|
|||||||
*/
|
*/
|
||||||
package mc.core.world;
|
package mc.core.world;
|
||||||
|
|
||||||
|
import mc.core.block.Block;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialization chunk info
|
||||||
|
*
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | param | range | bits |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | blocks | array | 24*count |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
*
|
||||||
|
* Total: 24 * block_count bits (3 * block_count bytes)
|
||||||
|
* Max size: 12288 bytes (~12 Kb per chunk)
|
||||||
|
*
|
||||||
|
*/
|
||||||
/* 16x16x16 */
|
/* 16x16x16 */
|
||||||
public interface Chunk {
|
public interface Chunk extends Serializable{
|
||||||
int getBlockType(int x, int y, int z);
|
int getBlockType(int x, int y, int z);
|
||||||
void setBlockType(int x, int y, int z, int type);
|
void setBlockType(int x, int y, int z, int type);
|
||||||
|
|
||||||
@@ -21,6 +38,13 @@ public interface Chunk {
|
|||||||
int getAddition(int x, int y, int z);
|
int getAddition(int x, int y, int z);
|
||||||
void setAddition(int x, int y, int z, int value);
|
void setAddition(int x, int y, int z, int value);
|
||||||
|
|
||||||
int getBiome(int x, int z);
|
Biome getBiome(int x, int z);
|
||||||
void setBiome(int x, int z, int value);
|
void setBiome(int x, int z, Biome biome);
|
||||||
|
|
||||||
|
int getX();
|
||||||
|
int getY();
|
||||||
|
int getZ();
|
||||||
|
|
||||||
|
void setBlock (int x, int y, int z, Block block);
|
||||||
|
Block getBlock (int x, int y, int z);
|
||||||
}
|
}
|
||||||
|
|||||||
27
core/src/main/java/mc/core/world/ChunkLoader.java
Normal file
27
core/src/main/java/mc/core/world/ChunkLoader.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package mc.core.world;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface ChunkLoader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads chunk from cache. If chunk in cache doesn't exist, loads from file (or other storage)
|
||||||
|
*
|
||||||
|
* @param x chunk position
|
||||||
|
* @param y chunk position
|
||||||
|
* @param z chunk position
|
||||||
|
* @return optional of chunk (nullable)
|
||||||
|
*/
|
||||||
|
Optional<Chunk> loadChunk (int x, int y, int z);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to load chunk like {@link #loadChunk(int, int, int)}
|
||||||
|
* If chunk doesn't exist, generates it with selected world generator
|
||||||
|
*
|
||||||
|
* @param x chunk position
|
||||||
|
* @param y chunk position
|
||||||
|
* @param z chunk position
|
||||||
|
* @return chunk
|
||||||
|
*/
|
||||||
|
Chunk loadOrGenerateChunk (int x, int y, int z);
|
||||||
|
}
|
||||||
6
core/src/main/java/mc/core/world/IWorldType.java
Normal file
6
core/src/main/java/mc/core/world/IWorldType.java
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package mc.core.world;
|
||||||
|
|
||||||
|
public interface IWorldType {
|
||||||
|
String name();
|
||||||
|
String description();
|
||||||
|
}
|
||||||
34
core/src/main/java/mc/core/world/Region.java
Normal file
34
core/src/main/java/mc/core/world/Region.java
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package mc.core.world;
|
||||||
|
|
||||||
|
import mc.core.serialization.IRegionReaderWriter;
|
||||||
|
import mc.core.serialization.Serializer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple world generation unit
|
||||||
|
* 16x16x16 chunks
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | param | range | bits |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | biome_map | 256x256 0-128 | 524288 |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
*
|
||||||
|
* Total: 524288 bits (64 Kb)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface Region extends Serializable{
|
||||||
|
Chunk getChunkAt(int x, int y, int z);
|
||||||
|
void setChunk(int x, int y, int z, Chunk chunk);
|
||||||
|
|
||||||
|
int getX();
|
||||||
|
int getZ();
|
||||||
|
|
||||||
|
Biome getBiomeAt (int x, int z);
|
||||||
|
void setBiome (int x, int z, Biome biome);
|
||||||
|
|
||||||
|
void save(Serializer<Chunk> chunkSerializer, IRegionReaderWriter regionReaderWritter) throws IOException;
|
||||||
|
}
|
||||||
@@ -5,11 +5,58 @@
|
|||||||
package mc.core.world;
|
package mc.core.world;
|
||||||
|
|
||||||
import mc.core.Location;
|
import mc.core.Location;
|
||||||
|
import mc.core.WarpPosition;
|
||||||
|
import mc.core.nbt.Taggable;
|
||||||
|
|
||||||
public interface World {
|
import java.io.Serializable;
|
||||||
Location getSpawn();
|
import java.util.UUID;
|
||||||
void setSpawn(Location location);
|
|
||||||
|
|
||||||
Chunk getChunk(int x, int z);
|
/**
|
||||||
void setChunk(int x, int z, Chunk chunk);
|
* WorldInfo
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | param | range | bits |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | worldId | uuid | 128 |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | worldName | string [0-64] | 512 |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | spawnX | -524288:524287 | 20 |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | spawnY | 0:255 | 8 |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | spawnZ | -524288:524287 | 20 |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | seed | long | 64 |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
* | type | 0-255 | 8 |
|
||||||
|
* +-------------+----------------+------------+
|
||||||
|
*
|
||||||
|
* /worlds/
|
||||||
|
* --> []/world_uuid/
|
||||||
|
* --> world.dat
|
||||||
|
* --> []/r.X.Z/
|
||||||
|
* --> biomes.dat
|
||||||
|
* --> []chunk_x_y_z.dat
|
||||||
|
* --> entities.dat
|
||||||
|
* --> /playerdata/
|
||||||
|
* --> []player_uuid.dat
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface World extends Taggable, Serializable{
|
||||||
|
UUID getWorldId();
|
||||||
|
IWorldType getWorldType();
|
||||||
|
|
||||||
|
WarpPosition getSpawn();
|
||||||
|
void setSpawn(WarpPosition location);
|
||||||
|
|
||||||
|
Chunk getChunk(int x, int y, int z);
|
||||||
|
void setChunk(int x, int y, int z, Chunk chunk);
|
||||||
|
|
||||||
|
Region getRegion(int x, int z);
|
||||||
|
void setRegion(int x, int z, Region region);
|
||||||
|
|
||||||
|
int getSeed();
|
||||||
|
|
||||||
|
String getName();
|
||||||
|
void setName(String name);
|
||||||
}
|
}
|
||||||
|
|||||||
6
core/src/main/java/mc/core/world/WorldGenerator.java
Normal file
6
core/src/main/java/mc/core/world/WorldGenerator.java
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package mc.core.world;
|
||||||
|
|
||||||
|
public interface WorldGenerator {
|
||||||
|
|
||||||
|
Region generateRegion (int x, int z, World world);
|
||||||
|
}
|
||||||
18
core/src/main/java/mc/core/world/WorldType.java
Normal file
18
core/src/main/java/mc/core/world/WorldType.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package mc.core.world;
|
||||||
|
|
||||||
|
public enum WorldType implements IWorldType {
|
||||||
|
GENERAL("Standard world type"),
|
||||||
|
NETHER ("Nether world type"),
|
||||||
|
END ("End world type");
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
WorldType(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,25 +4,71 @@
|
|||||||
*/
|
*/
|
||||||
package mc.world.flat;
|
package mc.world.flat;
|
||||||
|
|
||||||
|
import com.flowpowered.nbt.Tag;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import mc.core.Location;
|
import mc.core.Location;
|
||||||
import mc.core.world.Chunk;
|
import mc.core.WarpPosition;
|
||||||
import mc.core.world.World;
|
import mc.core.player.Look;
|
||||||
|
import mc.core.world.*;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class FlatWorld implements World {
|
public class FlatWorld implements World {
|
||||||
|
|
||||||
|
@Getter@Setter
|
||||||
|
private UUID worldId = UUID.fromString("00000000-0000-0000-C000-000000000046");
|
||||||
|
@Getter@Setter
|
||||||
|
private String name;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private Location spawn = new Location(0, 6, 0);
|
private WarpPosition spawn = new WarpPosition(new Location(0, 6, 0), new Look(0, 0));
|
||||||
private Chunk chunk = new SimpleChunk();
|
private Chunk chunk = new SimpleChunk();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Chunk getChunk(int x, int z) {
|
public IWorldType getWorldType() {
|
||||||
|
return WorldType.GENERAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Chunk getChunk(int x, int y, int z) {
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setChunk(int x, int z, Chunk chunk) {
|
public void setChunk(int x, int y, int z, Chunk chunk) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region getRegion(int x, int z) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRegion(int x, int z, Region region) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSeed() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tag<?> getTag(String name) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTag(Tag<?> tag) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<Tag<?>> tagStream() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
package mc.world.flat;
|
package mc.world.flat;
|
||||||
|
|
||||||
|
import mc.core.block.Block;
|
||||||
|
import mc.core.block.BlockFactory;
|
||||||
|
import mc.core.block.BlockType;
|
||||||
|
import mc.core.world.Biome;
|
||||||
import mc.core.world.Chunk;
|
import mc.core.world.Chunk;
|
||||||
|
|
||||||
public class SimpleChunk implements Chunk {
|
public class SimpleChunk implements Chunk {
|
||||||
@@ -62,12 +66,42 @@ public class SimpleChunk implements Chunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBiome(int x, int z) {
|
public Biome getBiome(int x, int z) {
|
||||||
|
return Biome.PLAINS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBiome(int x, int z, Biome biome) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getX() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBiome(int x, int z, int value) {
|
public int getY() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getZ() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlock(int x, int y, int z, Block block) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Block getBlock(int x, int y, int z) {
|
||||||
|
BlockFactory blockFactory = new BlockFactory();
|
||||||
|
|
||||||
|
if (y == 0) return blockFactory.create(BlockType.BEDROCK, 0);
|
||||||
|
else if (y >= 1 && y <= 2) return blockFactory.create(BlockType.DIRT, 0);
|
||||||
|
else if (y == 3) return blockFactory.create(BlockType.GRASS, 0);
|
||||||
|
else return Block.airBlock(x, y, z);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
generated_world/README.MD
Normal file
3
generated_world/README.MD
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
### System properties:
|
||||||
|
|
||||||
|
* `worlds.folder` -- folder where worlds will be located
|
||||||
7
generated_world/build.gradle
Normal file
7
generated_world/build.gradle
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
group 'mc'
|
||||||
|
version '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile_excludeCopy project(':core')
|
||||||
|
testCompile group: 'junit', name: 'junit', version: '4.12'
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package mc.world.generated_world;
|
||||||
|
|
||||||
|
public final class WorldConstants {
|
||||||
|
|
||||||
|
public static final boolean DEBUG_ENABLED = true;
|
||||||
|
|
||||||
|
public static final String CHUNK_FILE_NAME_TEMPLATE = "chunk_{0}_{1}_{2}.dat";
|
||||||
|
public static final String BIOME_FILE_NAME_TEMPLATE = "biomes.dat";
|
||||||
|
public static final String WORLD_INFO_FILE_NAME_TEMPLATE = "world.dat";
|
||||||
|
public static final String REGION_FILE_NAME_TEMPLATE = "r.{0}.{1}";
|
||||||
|
|
||||||
|
public static final int WORLD_MAX_HEIGHT = 256;
|
||||||
|
public static final int WORLD_SEA_LEVEL = 64;
|
||||||
|
public static final int WORLD_MIN_GENERATION_HEIGHT = 36;
|
||||||
|
public static final int WORLD_MAX_GENERATION_HEIGHT = 128;
|
||||||
|
public static final int WORLD_REGION_SIZE = 256;
|
||||||
|
public static final int WORLD_CHUNK_SIZE = 16;
|
||||||
|
public static final int WORLD_MAX_TEMPERATURE = 100;
|
||||||
|
public static final int WORLD_MAX_WETNESS = 100;
|
||||||
|
public static final int WORLD_BASE_WETNESS = 80;
|
||||||
|
|
||||||
|
public static final double WORLD_LAND_SIZE = 63.03;
|
||||||
|
public static final double WORLD_LAKE_SIZE = 9.3;
|
||||||
|
public static final double WORLD_WET_SEA_PERCENT = 0.8;
|
||||||
|
public static final double WORLD_TEMPERATURE_SIZE = 41.0;
|
||||||
|
public static final double WORLD_TEMPERATURE_ZONE_SIZE = 2.99;
|
||||||
|
public static final double WORLD_TEMPERATURE_HEIGHT_GRAD_SIZE = 1.1;
|
||||||
|
|
||||||
|
public static final int LANDFILL_GRASS_SURFACE_THIN = 5;
|
||||||
|
public static final double WORLD_ROUGHNESS = 0.35;
|
||||||
|
|
||||||
|
private WorldConstants () {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package mc.world.generated_world.chunk;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import mc.core.block.Block;
|
||||||
|
import mc.core.block.BlockType;
|
||||||
|
import mc.core.world.Biome;
|
||||||
|
import mc.core.world.Chunk;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChunkImpl implements Chunk{
|
||||||
|
@Getter
|
||||||
|
private final int x;
|
||||||
|
@Getter
|
||||||
|
private final int y;
|
||||||
|
@Getter
|
||||||
|
private final int z;
|
||||||
|
private final Block[][][] blocks = new Block[WORLD_CHUNK_SIZE][WORLD_CHUNK_SIZE][WORLD_CHUNK_SIZE];
|
||||||
|
private final transient Region region;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBlockType(int x, int y, int z) {
|
||||||
|
return blocks[x][y][z].getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockType(int x, int y, int z, int type) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBlockMetadata(int x, int y, int z) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockMetadata(int x, int y, int z, int metadata) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBlockLight(int x, int y, int z) {
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockLight(int x, int y, int z, int lightLevel) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSkyLight(int x, int y, int z) {
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSkyLight(int x, int y, int z, int lightLevel) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAddition(int x, int y, int z) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAddition(int x, int y, int z, int value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Biome getBiome(int x, int z) {
|
||||||
|
return region.getBiomeAt(x + this.x * WORLD_CHUNK_SIZE,z + this.z * WORLD_CHUNK_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBiome(int x, int z, Biome biome) {
|
||||||
|
region.setBiome(x + this.x * WORLD_CHUNK_SIZE,z + this.z * WORLD_CHUNK_SIZE, biome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlock(int x, int y, int z, Block block) {
|
||||||
|
if (block.getBlockType() == BlockType.AIR) {
|
||||||
|
blocks[x][y][z] = null;
|
||||||
|
}
|
||||||
|
blocks[x][y][z] = block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Block getBlock(int x, int y, int z) {
|
||||||
|
Block block = blocks[x][y][z];
|
||||||
|
if (block == null) {
|
||||||
|
return Block.airBlock(x, y, z);
|
||||||
|
}
|
||||||
|
return blocks[x][y][z];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package mc.world.generated_world.chunk;
|
||||||
|
|
||||||
|
import mc.core.block.Block;
|
||||||
|
import mc.core.world.Biome;
|
||||||
|
import mc.core.world.Chunk;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
|
public class ChunkProxy implements Chunk {
|
||||||
|
private final Chunk chunk;
|
||||||
|
private volatile transient long lastUsage = System.currentTimeMillis();
|
||||||
|
private final transient ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
|
public ChunkProxy(Chunk chunk) {
|
||||||
|
this.chunk = chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastUsage() {
|
||||||
|
synchronized (chunk) {
|
||||||
|
return lastUsage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final void use () {
|
||||||
|
synchronized (chunk) {
|
||||||
|
lastUsage = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBlockType(int x, int y, int z) {
|
||||||
|
use();
|
||||||
|
return chunk.getBlockType(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockType(int x, int y, int z, int type) {
|
||||||
|
use();
|
||||||
|
chunk.setBlockType(x, y, z, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBlockMetadata(int x, int y, int z) {
|
||||||
|
use();
|
||||||
|
return chunk.getBlockMetadata(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockMetadata(int x, int y, int z, int metadata) {
|
||||||
|
use();
|
||||||
|
chunk.setBlockMetadata(x, y, z, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBlockLight(int x, int y, int z) {
|
||||||
|
use();
|
||||||
|
return chunk.getBlockLight(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockLight(int x, int y, int z, int lightLevel) {
|
||||||
|
use();
|
||||||
|
chunk.setBlockLight(x, y, z, lightLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSkyLight(int x, int y, int z) {
|
||||||
|
use();
|
||||||
|
return chunk.getSkyLight(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSkyLight(int x, int y, int z, int lightLevel) {
|
||||||
|
use();
|
||||||
|
chunk.setSkyLight(x, y, z, lightLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAddition(int x, int y, int z) {
|
||||||
|
use();
|
||||||
|
return chunk.getAddition(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAddition(int x, int y, int z, int value) {
|
||||||
|
use();
|
||||||
|
chunk.setAddition(x, y, z, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Biome getBiome(int x, int z) {
|
||||||
|
use();
|
||||||
|
return chunk.getBiome(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBiome(int x, int z, Biome biome) {
|
||||||
|
use();
|
||||||
|
chunk.setBiome(x, z, biome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getX() {
|
||||||
|
use();
|
||||||
|
return chunk.getX();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getY() {
|
||||||
|
use();
|
||||||
|
return chunk.getY();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getZ() {
|
||||||
|
use();
|
||||||
|
return chunk.getZ();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlock(int x, int y, int z, Block block) {
|
||||||
|
use();
|
||||||
|
chunk.setBlock(x, y, z, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Block getBlock(int x, int y, int z) {
|
||||||
|
use();
|
||||||
|
return chunk.getBlock(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package mc.world.generated_world.chunk;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.serialization.Serializer;
|
||||||
|
import mc.core.world.*;
|
||||||
|
import mc.world.generated_world.serialization.ChunkReader;
|
||||||
|
import mc.world.generated_world.serialization.RegionReaderWriter;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class InMemoryCacheChunkLoader implements ChunkLoader {
|
||||||
|
|
||||||
|
private final World world;
|
||||||
|
private File worldFolder;
|
||||||
|
@Autowired
|
||||||
|
private WorldGenerator worldGenerator;
|
||||||
|
@Autowired
|
||||||
|
private ChunkReader chunkReader;
|
||||||
|
@Autowired
|
||||||
|
private Serializer<Chunk> chunkSerializer;
|
||||||
|
@Autowired
|
||||||
|
private RegionReaderWriter regionReaderWritter;
|
||||||
|
|
||||||
|
public InMemoryCacheChunkLoader(World world) {
|
||||||
|
this.world = world;
|
||||||
|
String worldPath = System.getProperty("worlds.folder", "worlds");
|
||||||
|
worldFolder = new File(worldPath, world.getWorldId().toString());
|
||||||
|
if (!worldFolder.exists()) {
|
||||||
|
log.info("Created folder for world with uuid '{}'", world.getWorldId());
|
||||||
|
worldFolder.mkdirs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getChuckFile(int x, int y, int z) {
|
||||||
|
return new File(worldFolder, MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Chunk> loadChunk(int x, int y, int z) {
|
||||||
|
File file = getChuckFile(x, y, z);
|
||||||
|
if (!file.exists()) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Chunk chunk = chunkReader.read(world.getRegion(x / WORLD_CHUNK_SIZE, z / WORLD_CHUNK_SIZE), x, y, z);
|
||||||
|
return Optional.of(chunk);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while reading chunk file: " + file.getAbsolutePath(), e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Chunk loadOrGenerateChunk(int x, int y, int z) {
|
||||||
|
int regX = x / WORLD_CHUNK_SIZE;
|
||||||
|
int regZ = z / WORLD_CHUNK_SIZE;
|
||||||
|
File regionFile = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, regX, regZ));
|
||||||
|
Region region;
|
||||||
|
Chunk chunk;
|
||||||
|
if (!regionFile.exists()) {
|
||||||
|
log.debug("Region [{}, {}] not found. Generating!", regX, regZ);
|
||||||
|
regionFile.mkdirs();
|
||||||
|
region = worldGenerator.generateRegion(regX, regZ, world);
|
||||||
|
try {
|
||||||
|
regionReaderWritter.write(region);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while writting biome file", e);
|
||||||
|
}
|
||||||
|
saveRegion(region);
|
||||||
|
chunk = region.getChunkAt(x % WORLD_CHUNK_SIZE, y % WORLD_CHUNK_SIZE, z % WORLD_CHUNK_SIZE);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
region = regionReaderWritter.read(regX, regZ, world);
|
||||||
|
chunk = chunkReader.read(region, x, y, z);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while reading chunk file", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveRegion (Region region) {
|
||||||
|
File file = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, region.getX(), region.getZ()));
|
||||||
|
for (int x = 0; x < WORLD_REGION_SIZE / WORLD_CHUNK_SIZE; x ++) {
|
||||||
|
for (int y = 0; y < WORLD_REGION_SIZE / WORLD_CHUNK_SIZE; y ++) {
|
||||||
|
for (int z = 0; z < WORLD_REGION_SIZE / WORLD_CHUNK_SIZE; z ++) {
|
||||||
|
byte[] chunkBytes = chunkSerializer.serialize(region.getChunkAt(x, y, z));
|
||||||
|
File chunkFile = new File(file, MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z));
|
||||||
|
try (FileOutputStream writer = new FileOutputStream(chunkFile)) {
|
||||||
|
writer.write(chunkBytes);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while writting chunk to file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
package mc.world.generated_world.generator;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.block.BlockFactory;
|
||||||
|
import mc.core.block.BlockType;
|
||||||
|
import mc.core.world.*;
|
||||||
|
import mc.world.generated_world.region.RegionImpl;
|
||||||
|
import mc.world.generated_world.serialization.ChunkSerializer;
|
||||||
|
import mc.world.generated_world.serialization.RegionReaderWriter;
|
||||||
|
import mc.world.generated_world.serialization.WorldReaderWriter;
|
||||||
|
import mc.world.generated_world.world.CubicWorld;
|
||||||
|
import mc.world.generated_world.world.Temperature;
|
||||||
|
import mc.world.generated_world.world.Wetness;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class SeedBasedWorldGenerator implements WorldGenerator {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception{
|
||||||
|
WorldGenerator worldGenerator = new SeedBasedWorldGenerator();
|
||||||
|
World world = new CubicWorld(UUID.fromString("00000000-0000-0000-C000-000000000046"), 2626949);
|
||||||
|
Region region = worldGenerator.generateRegion(0, 0, world);
|
||||||
|
region.save(new ChunkSerializer(), new RegionReaderWriter(new File("worlds", world.getWorldId().toString())));
|
||||||
|
new WorldReaderWriter(new File("worlds")).writeWorldInfo(world);
|
||||||
|
/*worldGenerator.generateRegion(1, 0, world);
|
||||||
|
worldGenerator.generateRegion(-1, 0, world);
|
||||||
|
worldGenerator.generateRegion(0, 1, world);
|
||||||
|
worldGenerator.generateRegion(0, -1, world);
|
||||||
|
worldGenerator.generateRegion(-1, -1, world);
|
||||||
|
worldGenerator.generateRegion(1, -1, world);
|
||||||
|
worldGenerator.generateRegion(-1, 1, world);
|
||||||
|
worldGenerator.generateRegion(1, 1, world);
|
||||||
|
BufferedImage image = new BufferedImage(3 * 256, 3 * 256, BufferedImage.TYPE_INT_RGB);
|
||||||
|
BufferedImage currentImage;
|
||||||
|
int shiftX;
|
||||||
|
int shiftY;
|
||||||
|
currentImage = ImageIO.read(new File("out/0.0", "biomeMap.png"));
|
||||||
|
shiftX = 1;
|
||||||
|
shiftY = 1;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentImage = ImageIO.read(new File("out/0.1", "biomeMap.png"));
|
||||||
|
shiftX = 1;
|
||||||
|
shiftY = 2;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentImage = ImageIO.read(new File("out/1.0", "biomeMap.png"));
|
||||||
|
shiftX = 2;
|
||||||
|
shiftY = 1;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentImage = ImageIO.read(new File("out/-1.0", "biomeMap.png"));
|
||||||
|
shiftX = 0;
|
||||||
|
shiftY = 1;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentImage = ImageIO.read(new File("out/0.-1", "biomeMap.png"));
|
||||||
|
shiftX = 1;
|
||||||
|
shiftY = 0;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentImage = ImageIO.read(new File("out/-1.-1", "biomeMap.png"));
|
||||||
|
shiftX = 0;
|
||||||
|
shiftY = 0;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentImage = ImageIO.read(new File("out/1.-1", "biomeMap.png"));
|
||||||
|
shiftX = 2;
|
||||||
|
shiftY = 0;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentImage = ImageIO.read(new File("out/1.1", "biomeMap.png"));
|
||||||
|
shiftX = 2;
|
||||||
|
shiftY = 2;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentImage = ImageIO.read(new File("out/-1.1", "biomeMap.png"));
|
||||||
|
shiftX = 0;
|
||||||
|
shiftY = 2;
|
||||||
|
for (int x = 0; x < 256; x ++){
|
||||||
|
for (int y = 0; y < 256; y ++){
|
||||||
|
int tx = 256 * shiftX + x;
|
||||||
|
int ty = 256 * shiftY + y;
|
||||||
|
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageIO.write(image, "png", new File("out", "merged.png"));*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region generateRegion(int x, int z, World world) {
|
||||||
|
log.info("Generating region [{},{}]...", x, z);
|
||||||
|
Region region = new RegionImpl(x, z, world);
|
||||||
|
RegionGenerator regionGenerator = new RegionGenerator(world, region);
|
||||||
|
regionGenerator.generate();
|
||||||
|
log.info("Region [{},{}] is generated", x, z);
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
private class RegionGenerator {
|
||||||
|
private final World world;
|
||||||
|
private final Region region;
|
||||||
|
private NoiseGenerator noiseGenerator;
|
||||||
|
private BlockFactory blockFactory = new BlockFactory();
|
||||||
|
|
||||||
|
private double sigmoid (double x) {
|
||||||
|
x -= 0.5;
|
||||||
|
x *= 15;
|
||||||
|
return 1.0 / (1.0 + Math.exp(-x));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int convert (int x) {
|
||||||
|
return 40960 + x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void generate() {
|
||||||
|
log.debug("Starting generating region [{}, {}] for world '{}' with seed '{}'", region.getX(), region.getZ(), world.getWorldId(), world.getSeed());
|
||||||
|
|
||||||
|
noiseGenerator = new NoiseGenerator(world.getSeed());
|
||||||
|
noiseGenerator.init();
|
||||||
|
|
||||||
|
int[][] heightMap = new int[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||||
|
int[][] grassMap = new int[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||||
|
int[][] temperatureMap = new int[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||||
|
int[][] wetMap = new int[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||||
|
Biome[][] biomes = new Biome[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||||
|
|
||||||
|
for (int x = 0; x < WORLD_REGION_SIZE; x ++) {
|
||||||
|
for (int z = 0; z < WORLD_REGION_SIZE; z ++) {
|
||||||
|
int tx = convert(x + region.getX() * WORLD_REGION_SIZE);
|
||||||
|
int tz = convert(z + region.getZ() * WORLD_REGION_SIZE);
|
||||||
|
double p = sigmoid(noiseGenerator.noise(tx / WORLD_LAND_SIZE, tz / WORLD_LAND_SIZE));
|
||||||
|
double r = Math.sqrt(noiseGenerator.noise(tx / WORLD_LAKE_SIZE, tz / WORLD_LAKE_SIZE));
|
||||||
|
double h = (WORLD_MAX_GENERATION_HEIGHT - WORLD_MIN_GENERATION_HEIGHT) * Math.min(p * r, 1);
|
||||||
|
h = Math.min(WORLD_MAX_GENERATION_HEIGHT, h + WORLD_MIN_GENERATION_HEIGHT);
|
||||||
|
heightMap[x][z] = (int)(h);
|
||||||
|
grassMap[x][z] = (int) (1 + SeedRandomGenerator.random(tx, tz, world.getSeed()) * (LANDFILL_GRASS_SURFACE_THIN - 1));
|
||||||
|
double k = Math.sqrt(noiseGenerator.noise(tx * WORLD_TEMPERATURE_ZONE_SIZE, tz * WORLD_TEMPERATURE_ZONE_SIZE));
|
||||||
|
double q = Math.sqrt(noiseGenerator.noise(tx / WORLD_TEMPERATURE_SIZE, tz / WORLD_TEMPERATURE_SIZE));
|
||||||
|
temperatureMap[x][z] = (int) (WORLD_MAX_TEMPERATURE * Math.min((k * k + q * q + k * q) * k * q, 0.99));
|
||||||
|
if (heightMap[x][z] < WORLD_SEA_LEVEL) {
|
||||||
|
biomes[x][z] = Biome.OCEAN;
|
||||||
|
wetMap[x][z] = (int) (WORLD_MAX_WETNESS * WORLD_WET_SEA_PERCENT *noiseGenerator.noise(tx, tz));
|
||||||
|
} else {
|
||||||
|
int th = heightMap[x][z] - WORLD_SEA_LEVEL;
|
||||||
|
th = (int) (th * (1 + 1.25 * th / (WORLD_MAX_GENERATION_HEIGHT - WORLD_SEA_LEVEL)));
|
||||||
|
heightMap[x][z] = Math.min(WORLD_SEA_LEVEL + th, WORLD_MAX_GENERATION_HEIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int x = 1; x < WORLD_REGION_SIZE - 1; x ++) {
|
||||||
|
for (int z = 1; z < WORLD_REGION_SIZE - 1; z++) {
|
||||||
|
int mid = 0;
|
||||||
|
for (int tx = x - 1; tx <= x + 1; tx ++) {
|
||||||
|
for (int tz = z - 1; tz <= z + 1; tz ++) {
|
||||||
|
mid += wetMap[tx][tz];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wetMap[x][z] = mid / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int z = 1; z < WORLD_REGION_SIZE - 1; z++) {
|
||||||
|
for (int x = 1; x < WORLD_REGION_SIZE - 1; x ++) {
|
||||||
|
int mid = 0;
|
||||||
|
for (int tx = x - 1; tx <= x + 1; tx ++) {
|
||||||
|
for (int tz = z - 1; tz <= z + 1; tz ++) {
|
||||||
|
mid += wetMap[tx][tz];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wetMap[x][z] = (int) (mid / 9 * (1 + 0.4 * SeedRandomGenerator.random(x, z, world.getSeed())));
|
||||||
|
temperatureMap[x][z] = (int) Math.min(Math.max(temperatureMap[x][z] - WORLD_TEMPERATURE_HEIGHT_GRAD_SIZE * SeedRandomGenerator.random(x, z, world.getSeed()) * (heightMap[x][z] - WORLD_SEA_LEVEL), 0), WORLD_MAX_TEMPERATURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int z = 1; z < WORLD_REGION_SIZE - 1; z++) {
|
||||||
|
for (int x = 1; x < WORLD_REGION_SIZE - 1; x ++) {
|
||||||
|
wetMap[x][z] = (int) Math.min(WORLD_MAX_WETNESS, WORLD_BASE_WETNESS * noiseGenerator.noise(x / 31d, z / 31d) + wetMap[x][z] * (1 + 0.2 * (SeedRandomGenerator.random(x, z, world.getSeed()))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
smooth(grassMap);
|
||||||
|
smooth(temperatureMap);
|
||||||
|
smooth(wetMap);
|
||||||
|
//smooth(heightMap);
|
||||||
|
|
||||||
|
// ================================ DEBUG =======================================
|
||||||
|
if (DEBUG_ENABLED) {
|
||||||
|
log.debug("Creating debug images");
|
||||||
|
File outFile;
|
||||||
|
outFile = new File("out", region.getX() + "." + region.getZ());
|
||||||
|
outFile.mkdirs();
|
||||||
|
BufferedImage tempImg = new BufferedImage(WORLD_REGION_SIZE, WORLD_REGION_SIZE, BufferedImage.TYPE_INT_RGB);
|
||||||
|
BufferedImage wetImg = new BufferedImage(WORLD_REGION_SIZE, WORLD_REGION_SIZE, BufferedImage.TYPE_INT_RGB);
|
||||||
|
for (int x = 0; x < WORLD_REGION_SIZE; x ++) {
|
||||||
|
for (int z = 0; z < WORLD_REGION_SIZE; z ++) {
|
||||||
|
Temperature temperature = Temperature.values()[Temperature.values().length * temperatureMap[x][z] / WORLD_MAX_TEMPERATURE];
|
||||||
|
Wetness wetness = Wetness.values()[ Wetness.values().length * (Math.min(wetMap[x][z], WORLD_MAX_WETNESS) - 1) / WORLD_MAX_WETNESS];
|
||||||
|
biomes[x][z] = selectBiome(temperature, wetness, heightMap[x][z]);
|
||||||
|
tempImg.setRGB(x, z, temperature.ordinal() * 0xff / Temperature.values().length);
|
||||||
|
wetImg.setRGB(x, z, wetness.ordinal() * 0xff / Wetness.values().length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ImageIO.write(tempImg, "png", new File(outFile, "temp_img.png"));
|
||||||
|
ImageIO.write(wetImg, "png", new File(outFile, "wet_img.png"));
|
||||||
|
|
||||||
|
BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||||
|
BufferedImage subImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||||
|
for (int x = 0; x < 256; x++) {
|
||||||
|
for (int z = 0; z < 256; z++) {
|
||||||
|
int h = heightMap[x][z];
|
||||||
|
h = h << 16 | h << 8 | h;
|
||||||
|
image.setRGB(x, z, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageIO.write(image, "png", new File(outFile, "heightmap.png"));
|
||||||
|
image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||||
|
for (int x = 0; x < 256; x++) {
|
||||||
|
for (int z = 0; z < 256; z++) {
|
||||||
|
int temp = 0xff * temperatureMap[x][z] / 100;
|
||||||
|
temp = temp << 16;
|
||||||
|
image.setRGB(x, z, temp);
|
||||||
|
subImage.setRGB(x, z, (0xff * (int) (temperatureMap[x][z] / 20) / 5) << 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageIO.write(image, "png", new File(outFile, "temperatureMap.png"));
|
||||||
|
ImageIO.write(subImage, "png", new File(outFile, "reg_temperatureMap.png"));
|
||||||
|
subImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||||
|
image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||||
|
for (int x = 0; x < 256; x++) {
|
||||||
|
for (int z = 0; z < 256; z++) {
|
||||||
|
int wet = 0xff * wetMap[x][z] / 100;
|
||||||
|
image.setRGB(x, z, wet);
|
||||||
|
subImage.setRGB(x, z, 0xff * (int) (Wetness.values().length * wetMap[x][z] / (WORLD_MAX_WETNESS)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageIO.write(image, "png", new File(outFile, "wetMap.png"));
|
||||||
|
ImageIO.write(subImage, "png", new File(outFile, "reg_wetMap.png"));
|
||||||
|
image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||||
|
for (int x = 0; x < 256; x++) {
|
||||||
|
for (int z = 0; z < 256; z++) {
|
||||||
|
image.setRGB(x, z, biomes[x][z].getColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageIO.write(image, "png", new File(outFile, "biomeMap.png"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error occurred while creating debug images", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ================================ DEBUG FINISH =======================================
|
||||||
|
|
||||||
|
log.debug("Creating chunks...");
|
||||||
|
|
||||||
|
for (int x = 0; x < WORLD_REGION_SIZE; x ++) {
|
||||||
|
for (int z = 0; z < WORLD_REGION_SIZE; z ++) {
|
||||||
|
region.setBiome(x, z, biomes[x][z]);
|
||||||
|
if (heightMap[x][z] < WORLD_SEA_LEVEL) {
|
||||||
|
for (int y = 0; y < WORLD_SEA_LEVEL; y ++) {
|
||||||
|
Chunk chunk = region.getChunkAt(x / 16, y / 16, z / 16);
|
||||||
|
if (y == 0) {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.BEDROCK, 0));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (y < heightMap[x][z]) {
|
||||||
|
if (y < heightMap[x][z] - grassMap[x][z]) {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.STONE, 0));
|
||||||
|
} else {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.SAND, 0));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.WATER, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int y = 0; y < heightMap[x][z]; y++) {
|
||||||
|
Chunk chunk = region.getChunkAt(x / 16, y / 16, z / 16);
|
||||||
|
if (y == 0) {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.BEDROCK, 0));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (y < heightMap[x][z] - grassMap[x][z]) {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.STONE, 0));
|
||||||
|
} else {
|
||||||
|
if (biomes[x][z] == Biome.DESERT || biomes[x][z] == Biome.DESERT_HILLS) {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.SAND, 0));
|
||||||
|
} else if (biomes[x][z] == Biome.TAIGA || biomes[x][z] == Biome.TAIGA_HILLS) {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.DIRT, 0));
|
||||||
|
} else {
|
||||||
|
chunk.setBlock(x % 16, y % 16, z % 16, blockFactory.create(BlockType.GRASS, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO
|
||||||
|
log.debug("Creating rivers...");
|
||||||
|
log.debug("Creating caves...");
|
||||||
|
log.debug("Generating ores...");
|
||||||
|
log.debug("Creating structures...");
|
||||||
|
log.debug("Planting trees...");
|
||||||
|
log.debug("Spawning animals...");
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Biome selectBiome (Temperature temperature, Wetness wetness, int height) {
|
||||||
|
|
||||||
|
if (wetness == Wetness.WATER || height < WORLD_SEA_LEVEL) {
|
||||||
|
if (temperature == Temperature.FROST) {
|
||||||
|
if (height < WORLD_SEA_LEVEL) {
|
||||||
|
return Biome.FROZEN_OCEAN;
|
||||||
|
} else {
|
||||||
|
return Biome.ICE_PLAINS;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (height < WORLD_SEA_LEVEL) {
|
||||||
|
if (height < WORLD_MIN_GENERATION_HEIGHT + (WORLD_SEA_LEVEL - WORLD_MIN_GENERATION_HEIGHT) / 2) {
|
||||||
|
return Biome.DEEP_OCEAN;
|
||||||
|
} else {
|
||||||
|
return Biome.OCEAN;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Biome.SWAMPLAND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int HILLS_HEIGHT = WORLD_SEA_LEVEL + (WORLD_MAX_GENERATION_HEIGHT - WORLD_SEA_LEVEL) / 3;
|
||||||
|
|
||||||
|
if (temperature == Temperature.FROST) {
|
||||||
|
if (wetness == Wetness.DRIEST || wetness == Wetness.DRY) {
|
||||||
|
return Biome.TUNDRA;
|
||||||
|
} else {
|
||||||
|
if (height > HILLS_HEIGHT) {
|
||||||
|
return Biome.ICE_MOUNTAINS;
|
||||||
|
} else {
|
||||||
|
return Biome.ICE_PLAINS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wetness == Wetness.DRIEST) {
|
||||||
|
if (temperature == Temperature.COLD || temperature == Temperature.WARM) {
|
||||||
|
return Biome.PLAINS;
|
||||||
|
} else {
|
||||||
|
if (height > HILLS_HEIGHT) {
|
||||||
|
return Biome.DESERT_HILLS;
|
||||||
|
} else {
|
||||||
|
return Biome.DESERT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temperature == Temperature.COLD) {
|
||||||
|
if (wetness == Wetness.DRY || wetness == Wetness.WET) {
|
||||||
|
if (height > HILLS_HEIGHT) {
|
||||||
|
return Biome.TAIGA_HILLS;
|
||||||
|
} else {
|
||||||
|
return Biome.TAIGA;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Biome.SWAMPLAND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wetness == Wetness.WETTEST) {
|
||||||
|
if (temperature == Temperature.WARM) {
|
||||||
|
return Biome.SWAMPLAND;
|
||||||
|
} else {
|
||||||
|
if (height > HILLS_HEIGHT) {
|
||||||
|
return Biome.JUNGLE_HILLS;
|
||||||
|
} else {
|
||||||
|
return Biome.JUNGLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wetness == Wetness.WETTER) {
|
||||||
|
if (temperature == Temperature.WARM) {
|
||||||
|
if (height > HILLS_HEIGHT) {
|
||||||
|
return Biome.FOREST_HILLS;
|
||||||
|
} else {
|
||||||
|
return Biome.FOREST;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Biome.SAVANNA_FOREST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temperature == Temperature.HOTTEST) {
|
||||||
|
return Biome.SAVANNA;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wetness == Wetness.WET) {
|
||||||
|
if (height > HILLS_HEIGHT) {
|
||||||
|
return Biome.FOREST_HILLS;
|
||||||
|
} else {
|
||||||
|
return Biome.FOREST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Biome.PLAINS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void smooth (int [][] map) {
|
||||||
|
final int[][] original = map.clone();
|
||||||
|
for (int y = 1; y < map.length - 1; y ++) {
|
||||||
|
for (int x = 1; x < map[0].length - 1; x ++) {
|
||||||
|
int mid = 0;
|
||||||
|
for (int tx = x - 1; tx <= x + 1; tx ++) {
|
||||||
|
for (int ty = y - 1; ty <= y + 1; ty ++) {
|
||||||
|
mid += original[tx][ty];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map[x][y] = mid / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
private class NoiseGenerator {
|
||||||
|
int mask = WORLD_REGION_SIZE - 1;
|
||||||
|
int[] perm = new int[WORLD_REGION_SIZE];
|
||||||
|
double[] gradsX = new double[WORLD_REGION_SIZE];
|
||||||
|
double[] gradsY = new double[WORLD_REGION_SIZE];
|
||||||
|
private final int seed;
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
for (int i = 0; i < WORLD_REGION_SIZE; ++i) {
|
||||||
|
int other = rand(i) % (i + 1);
|
||||||
|
if (i > other)
|
||||||
|
perm[i] = perm[other];
|
||||||
|
perm[other] = i;
|
||||||
|
gradsX[i] = Math.cos(2.0f * Math.PI * i / WORLD_REGION_SIZE);
|
||||||
|
gradsY[i] = Math.sin(2.0f * Math.PI * i / WORLD_REGION_SIZE);
|
||||||
|
}
|
||||||
|
log.debug("Noise generator is initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
double f(double t) {
|
||||||
|
t = Math.abs(t);
|
||||||
|
return t >= 1.0f ? 0.0f : 1.0f -
|
||||||
|
(3.0f - 2.0f * t) * t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
double surflet(double x, double y, double gradX, double gradY) {
|
||||||
|
return f(x) * f(y) * (gradX * x + gradY * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
double noise(double x, double y) {
|
||||||
|
float result = 0.0f;
|
||||||
|
int cellX = (int)(x);
|
||||||
|
int cellY = (int)(y);
|
||||||
|
for (int gridY = cellY; gridY <= cellY + 1; ++gridY)
|
||||||
|
for (int gridX = cellX; gridX <= cellX + 1; ++gridX) {
|
||||||
|
int hash = perm[(perm[gridX & mask] + gridY) & mask];
|
||||||
|
result += surflet(x - gridX, y - gridY,
|
||||||
|
gradsX[hash], gradsY[hash]);
|
||||||
|
}
|
||||||
|
return (result + 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rand(int i) {
|
||||||
|
int x = (i * i) % WORLD_REGION_SIZE;
|
||||||
|
int y = (i + i * x) % WORLD_REGION_SIZE;
|
||||||
|
return (int) (Integer.MAX_VALUE * SeedRandomGenerator.random(x, y, seed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package mc.world.generated_world.generator;
|
||||||
|
|
||||||
|
public final class SeedRandomGenerator {
|
||||||
|
|
||||||
|
public static double random (int x, int y, int seed) {
|
||||||
|
x = Math.abs(x - y) + 1;
|
||||||
|
y = Math.abs(y - x) + 1;
|
||||||
|
for (int i = 0; i < 20; i ++) {
|
||||||
|
int a1 = x % 13;
|
||||||
|
int a2 = x % 31;
|
||||||
|
int a3 = x % 89;
|
||||||
|
int a4 = y % 359;
|
||||||
|
int a5 = y % 7;
|
||||||
|
int a6 = y % 313;
|
||||||
|
int a7 = y % 8461;
|
||||||
|
int a8 = y % 105467;
|
||||||
|
int a9 = x % 105943;
|
||||||
|
y = x + seed;
|
||||||
|
x += a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9;
|
||||||
|
}
|
||||||
|
return ((x + y) % 100000) / 100000d;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package mc.world.generated_world.region;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.serialization.IRegionReaderWriter;
|
||||||
|
import mc.core.serialization.Serializer;
|
||||||
|
import mc.core.world.*;
|
||||||
|
import mc.world.generated_world.chunk.InMemoryCacheChunkLoader;
|
||||||
|
import mc.world.generated_world.chunk.ChunkImpl;
|
||||||
|
import mc.world.generated_world.chunk.ChunkProxy;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RegionImpl implements Region{
|
||||||
|
@Getter
|
||||||
|
private final int x;
|
||||||
|
@Getter
|
||||||
|
private final int z;
|
||||||
|
private final ChunkProxy[][][] chunks = new ChunkProxy[WORLD_REGION_SIZE/WORLD_CHUNK_SIZE][WORLD_REGION_SIZE/WORLD_CHUNK_SIZE][WORLD_REGION_SIZE/WORLD_CHUNK_SIZE];
|
||||||
|
private final Biome[][] biomes = new Biome[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||||
|
@Getter
|
||||||
|
private final transient World world;
|
||||||
|
@Autowired
|
||||||
|
private ChunkLoader chunkLoader;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Chunk getChunkAt(int x, int y, int z) {
|
||||||
|
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= 16 || z >= 16) {
|
||||||
|
throw new RuntimeException(MessageFormat.format("Invalid chunk coordinates [{0} {1} {2}]", x, y, z));
|
||||||
|
}
|
||||||
|
if (chunkLoader == null) {
|
||||||
|
chunkLoader = new InMemoryCacheChunkLoader(world);
|
||||||
|
}
|
||||||
|
Chunk chunk = chunks[x][y][z];
|
||||||
|
if (chunk == null) {
|
||||||
|
chunk = chunkLoader.loadChunk(x + this.x * WORLD_REGION_SIZE, y, this.z * WORLD_REGION_SIZE).orElse(new ChunkImpl(x, y, z, this));
|
||||||
|
chunks[x][y][z] = new ChunkProxy(chunk);
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setChunk(int x, int y, int z, Chunk chunk) {
|
||||||
|
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= 16 || z >= 16) {
|
||||||
|
throw new RuntimeException(MessageFormat.format("Invalid chunk coordinates [{0} {1} {2}]", x, y, z));
|
||||||
|
}
|
||||||
|
chunks[x][y][z] = new ChunkProxy(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Biome getBiomeAt(int x, int z) {
|
||||||
|
if (x < 0 || z < 0 || x >= 256 || z >= 256) {
|
||||||
|
throw new RuntimeException(MessageFormat.format("Invalid biome coordinates [{0} {1}]", x, z));
|
||||||
|
}
|
||||||
|
return biomes[x][z];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBiome(int x, int z, Biome biome) {
|
||||||
|
if (x < 0 || z < 0 || x >= 256 || z >= 256) {
|
||||||
|
throw new RuntimeException(MessageFormat.format("Invalid biome coordinates [{0} {1}]", x, z));
|
||||||
|
}
|
||||||
|
biomes[x][z] = biome;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(Serializer<Chunk> chunkSerializer, IRegionReaderWriter regionReaderWriter) throws IOException {
|
||||||
|
String worldPath = System.getProperty("worlds.folder", "worlds");
|
||||||
|
File worldFile = new File(worldPath, world.getWorldId().toString());
|
||||||
|
File regionFile = new File(worldFile, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, this.getX(), this.getZ()));
|
||||||
|
if (!regionFile.exists()) {
|
||||||
|
regionFile.mkdirs();
|
||||||
|
}
|
||||||
|
regionReaderWriter.write(this);
|
||||||
|
for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) {
|
||||||
|
for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) {
|
||||||
|
for (int y = 0; y < WORLD_CHUNK_SIZE; y++) {
|
||||||
|
Chunk chunk = this.getChunkAt(x, y, z);
|
||||||
|
byte[] chunkBytes = chunkSerializer.serialize(chunk);
|
||||||
|
if (chunkBytes.length > 0) {
|
||||||
|
File chunkFile = new File(regionFile, MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z));
|
||||||
|
try (FileOutputStream fileOutputStream = new FileOutputStream(chunkFile)) {
|
||||||
|
fileOutputStream.write(chunkBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package mc.world.generated_world.serialization;
|
||||||
|
|
||||||
|
import mc.core.block.Block;
|
||||||
|
import mc.core.block.BlockFactory;
|
||||||
|
import mc.core.block.BlockType;
|
||||||
|
import mc.core.serialization.Deserializer;
|
||||||
|
import mc.core.serialization.Serializer;
|
||||||
|
import mc.core.world.Chunk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prototype
|
||||||
|
*/
|
||||||
|
public class BlockSerializerDeserializer implements Serializer<Block>, Deserializer<Block> {
|
||||||
|
|
||||||
|
private BlockFactory blockFactory;
|
||||||
|
private Chunk chunk;
|
||||||
|
|
||||||
|
public BlockSerializerDeserializer(BlockFactory blockFactory, Chunk chunk) {
|
||||||
|
this.blockFactory = blockFactory;
|
||||||
|
this.chunk = chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Block deserialize(byte[] bytes) {
|
||||||
|
int id = bytes[0] + 128;
|
||||||
|
int meta = bytes[1] >> 4;
|
||||||
|
int x = (bytes[1] & 0xf) + chunk.getX() * 16;
|
||||||
|
int y = bytes[2] >> 4 + chunk.getY() * 16;
|
||||||
|
int z = (bytes[2] & 0xf) + chunk.getZ() * 16;
|
||||||
|
BlockType type = BlockType.values()[id];
|
||||||
|
Block block = blockFactory.create(type, meta);
|
||||||
|
block.getLocation().setX(x);
|
||||||
|
block.getLocation().setY(y);
|
||||||
|
block.getLocation().setZ(z);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] serialize(Block block) {
|
||||||
|
byte[] bytes = new byte[3];
|
||||||
|
bytes[0] = (byte) ((block.getId() - 128) & 0xff);
|
||||||
|
bytes[1] = (byte) ((block.getMeta() << 4) | (block.getLocation().getBlockX() % 16));
|
||||||
|
bytes[2] = (byte) (((block.getLocation().getBlockZ() % 16) << 4) | (block.getLocation().getBlockZ() % 16));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package mc.world.generated_world.serialization;
|
||||||
|
|
||||||
|
import mc.core.Location;
|
||||||
|
import mc.core.block.Block;
|
||||||
|
import mc.core.serialization.Deserializer;
|
||||||
|
import mc.core.serialization.IChunkReader;
|
||||||
|
import mc.core.world.Chunk;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
import mc.world.generated_world.chunk.ChunkImpl;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.*;
|
||||||
|
|
||||||
|
public class ChunkReader implements IChunkReader{
|
||||||
|
private final File worldFolder;
|
||||||
|
@Autowired
|
||||||
|
private Deserializer<Block> blockDeserializer;
|
||||||
|
|
||||||
|
public ChunkReader (File worldFolder) {
|
||||||
|
this.worldFolder = worldFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Chunk read (Region region, int x, int y, int z) throws IOException {
|
||||||
|
x %= WORLD_REGION_SIZE;
|
||||||
|
y %= WORLD_REGION_SIZE;
|
||||||
|
z %= WORLD_REGION_SIZE;
|
||||||
|
File chunkFile = new File(new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, region.getX(), region.getZ())), MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z));
|
||||||
|
byte[] chunkBytes = Files.readAllBytes(Paths.get(chunkFile.toURI()));
|
||||||
|
int blocks = (chunkBytes.length) / 3;
|
||||||
|
Chunk chunk = new ChunkImpl(x, y, z, region);
|
||||||
|
for (int i = 0; i < blocks; i ++) {
|
||||||
|
byte[] blockBytes = new byte[3];
|
||||||
|
blockBytes[0] = chunkBytes[3 * i];
|
||||||
|
blockBytes[1] = chunkBytes[1 + 3 * i];
|
||||||
|
blockBytes[2] = chunkBytes[2 + 3 * i];
|
||||||
|
Block block = blockDeserializer.deserialize(blockBytes);
|
||||||
|
Location blockLocation = block.getLocation();
|
||||||
|
chunk.setBlock(blockLocation.getBlockX(), blockLocation.getBlockY(), blockLocation.getBlockZ(), block);
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package mc.world.generated_world.serialization;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.block.Block;
|
||||||
|
import mc.core.block.BlockFactory;
|
||||||
|
import mc.core.block.BlockType;
|
||||||
|
import mc.core.serialization.Serializer;
|
||||||
|
import mc.core.world.Chunk;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ChunkSerializer implements Serializer<Chunk> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Serializer<Block> blockSerializer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] serialize(Chunk chunk) {
|
||||||
|
Serializer<Block> blockSerializer = new BlockSerializerDeserializer(new BlockFactory(), chunk);
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
Block current;
|
||||||
|
for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) {
|
||||||
|
for (int y = 0; y < WORLD_CHUNK_SIZE; y ++) {
|
||||||
|
for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) {
|
||||||
|
current = chunk.getBlock(x, y, z);
|
||||||
|
if (current != null && current.getBlockType() != BlockType.AIR) {
|
||||||
|
try {
|
||||||
|
baos.write(blockSerializer.serialize(current));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while writing serialized block to byte array", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baos.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package mc.world.generated_world.serialization;
|
||||||
|
|
||||||
|
import mc.core.serialization.IRegionReaderWriter;
|
||||||
|
import mc.core.world.Biome;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
import mc.core.world.World;
|
||||||
|
import mc.world.generated_world.region.RegionImpl;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.*;
|
||||||
|
|
||||||
|
public class RegionReaderWriter implements IRegionReaderWriter {
|
||||||
|
private final File worldFolder;
|
||||||
|
|
||||||
|
public RegionReaderWriter(File worldFolder) {
|
||||||
|
this.worldFolder = worldFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region read (int x, int z, World world) throws IOException{
|
||||||
|
File regionFolder = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, x, z));
|
||||||
|
File biomesFile = new File(regionFolder, BIOME_FILE_NAME_TEMPLATE);
|
||||||
|
byte[] biomesBytes = Files.readAllBytes(Paths.get(biomesFile.toURI()));
|
||||||
|
Region region = new RegionImpl(x, z, world);
|
||||||
|
for (int tx = 0; tx < WORLD_REGION_SIZE; tx ++) {
|
||||||
|
for (int tz = 0; tz < WORLD_REGION_SIZE; tz ++) {
|
||||||
|
region.setBiome(tx, tz, Biome.getById(biomesBytes[tx * WORLD_REGION_SIZE + tz]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write (Region region) throws IOException{
|
||||||
|
File regionFolder = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, region.getX(), region.getZ()));
|
||||||
|
if (!regionFolder.exists()) {
|
||||||
|
regionFolder.mkdirs();
|
||||||
|
}
|
||||||
|
File biomesFile = new File(regionFolder, BIOME_FILE_NAME_TEMPLATE);
|
||||||
|
byte[] biomesBytes = new byte[WORLD_REGION_SIZE * WORLD_REGION_SIZE];
|
||||||
|
for (int x = 0; x < WORLD_REGION_SIZE; x ++) {
|
||||||
|
for (int z = 0; z < WORLD_REGION_SIZE; z ++) {
|
||||||
|
biomesBytes[x * WORLD_REGION_SIZE + z] = (byte) region.getBiomeAt(x, z).getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(biomesFile)) {
|
||||||
|
fos.write(biomesBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package mc.world.generated_world.serialization;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.WarpPosition;
|
||||||
|
import mc.core.world.World;
|
||||||
|
import mc.world.generated_world.world.CubicWorld;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.WORLD_INFO_FILE_NAME_TEMPLATE;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class WorldReaderWriter {
|
||||||
|
private final File worldsFolder;
|
||||||
|
|
||||||
|
public WorldReaderWriter(File worldsFolder) {
|
||||||
|
this.worldsFolder = worldsFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public World readWorld (UUID uuid) throws IOException {
|
||||||
|
World world = null;
|
||||||
|
File worldFolder = new File(worldsFolder, uuid.toString());
|
||||||
|
if (!worldFolder.exists()) {
|
||||||
|
throw new FileNotFoundException("World folder is not exist");
|
||||||
|
}
|
||||||
|
File worldInfoFile = new File(worldFolder, WORLD_INFO_FILE_NAME_TEMPLATE);
|
||||||
|
WorldInfo worldInfo;
|
||||||
|
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(worldInfoFile))) {
|
||||||
|
worldInfo = (WorldInfo) ois.readObject();
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
log.error("Error occurred while reading world info file", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
world = new CubicWorld(uuid, worldInfo.getSeed());
|
||||||
|
world.setSpawn(worldInfo.getSpawn());
|
||||||
|
world.setName(worldInfo.getName());
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeWorldInfo (World world) throws IOException {
|
||||||
|
File worldFolder = new File(worldsFolder, world.getWorldId().toString());
|
||||||
|
worldFolder.mkdirs();
|
||||||
|
File worldInfoFile = new File(worldFolder, WORLD_INFO_FILE_NAME_TEMPLATE);
|
||||||
|
WorldInfo worldInfo = new WorldInfo();
|
||||||
|
worldInfo.setName(world.getName());
|
||||||
|
worldInfo.setSeed(world.getSeed());
|
||||||
|
worldInfo.setSpawn(world.getSpawn());
|
||||||
|
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(worldInfoFile))) {
|
||||||
|
oos.writeObject(worldInfo);
|
||||||
|
oos.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class WorldInfo implements Serializable {
|
||||||
|
private WarpPosition spawn;
|
||||||
|
private String name;
|
||||||
|
private int seed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package mc.world.generated_world.world;
|
||||||
|
|
||||||
|
import com.flowpowered.nbt.Tag;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.Location;
|
||||||
|
import mc.core.WarpPosition;
|
||||||
|
import mc.core.block.BlockType;
|
||||||
|
import mc.core.player.Look;
|
||||||
|
import mc.core.world.Chunk;
|
||||||
|
import mc.core.world.IWorldType;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
import mc.core.world.World;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
||||||
|
import static mc.world.generated_world.WorldConstants.WORLD_MAX_HEIGHT;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class CubicWorld implements World {
|
||||||
|
@Getter
|
||||||
|
private final UUID worldId;
|
||||||
|
private final int seed;
|
||||||
|
private volatile WarpPosition warpPosition;
|
||||||
|
private final transient Object spawnLocationLock = new Object();
|
||||||
|
private final Map<String, Tag<?>> nbtTagMap = new HashMap<>();
|
||||||
|
@Autowired
|
||||||
|
private RegionManager regionManager;
|
||||||
|
@Getter@Setter
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public CubicWorld(UUID worldId, int seed) {
|
||||||
|
this.worldId = worldId;
|
||||||
|
this.seed = seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CubicWorld(int seed) {
|
||||||
|
this.worldId = UUID.randomUUID();
|
||||||
|
this.seed = seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CubicWorld(UUID worldId) {
|
||||||
|
this.worldId = worldId;
|
||||||
|
this.seed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CubicWorld () {
|
||||||
|
this.worldId = UUID.randomUUID();
|
||||||
|
this.seed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IWorldType getWorldType() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WarpPosition getSpawn() {
|
||||||
|
if (warpPosition == null) {
|
||||||
|
synchronized (spawnLocationLock) {
|
||||||
|
if (warpPosition == null) {
|
||||||
|
log.warn("Spawn location is not defined. Trying to select best location");
|
||||||
|
warpPosition = new WarpPosition(Location.startPointLocation(), new Look(0, 0));
|
||||||
|
for (int y = WORLD_MAX_HEIGHT; y > 0; y --) {
|
||||||
|
Chunk chunk = getChunk(0,y / WORLD_CHUNK_SIZE, 0);
|
||||||
|
if (chunk.getBlock(0, y, 0).getBlockType() != BlockType.AIR) {
|
||||||
|
warpPosition = new WarpPosition(new Location(0, y + 1, 0), new Look(0, 0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warpPosition = new WarpPosition(Location.startPointLocation(), new Look(0,0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return warpPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSpawn(WarpPosition warpPosition) {
|
||||||
|
synchronized (spawnLocationLock) {
|
||||||
|
this.warpPosition = warpPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Chunk getChunk(int x, int y, int z) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setChunk(int x, int y, int z, Chunk chunk) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region getRegion(int x, int z) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRegion(int x, int z, Region region) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSeed() {
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tag<?> getTag(String name) {
|
||||||
|
return nbtTagMap.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTag(Tag<?> tag) {
|
||||||
|
nbtTagMap.put(tag.getName(), tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<Tag<?>> tagStream() {
|
||||||
|
return nbtTagMap.values().stream();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package mc.world.generated_world.world;
|
||||||
|
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.Direction;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
import mc.core.world.World;
|
||||||
|
import mc.core.world.WorldGenerator;
|
||||||
|
import mc.world.generated_world.serialization.RegionReaderWriter;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.REGION_FILE_NAME_TEMPLATE;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NORTH
|
||||||
|
*
|
||||||
|
* EAST WEST
|
||||||
|
*
|
||||||
|
* SOUTH
|
||||||
|
*
|
||||||
|
* + ----> X
|
||||||
|
* |
|
||||||
|
* |
|
||||||
|
* |
|
||||||
|
* V Z
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class RegionManager {
|
||||||
|
private final World world;
|
||||||
|
private int pointX = -1;
|
||||||
|
private int pointZ = -1;
|
||||||
|
private int sizeX = 2;
|
||||||
|
private int sizeZ = 2;
|
||||||
|
private Region[][] regions = new Region[sizeX][sizeZ];
|
||||||
|
private final Lock regionSaveLock = new ReentrantLock();
|
||||||
|
@Autowired
|
||||||
|
private RegionReaderWriter regionReaderWriter;
|
||||||
|
@Autowired
|
||||||
|
private WorldGenerator worldGenerator;
|
||||||
|
@Setter
|
||||||
|
private boolean autoSaveRegionAfterGenerating = true;
|
||||||
|
|
||||||
|
|
||||||
|
public RegionManager(World world) {
|
||||||
|
this.world = world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegion (Region region) {
|
||||||
|
int x = region.getX();
|
||||||
|
int z = region.getZ();
|
||||||
|
|
||||||
|
try {
|
||||||
|
regionSaveLock.lock();
|
||||||
|
regions[x - pointX][z - pointZ] = region;
|
||||||
|
} finally {
|
||||||
|
regionSaveLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkCoordsInCache (int x, int z) {
|
||||||
|
if (x < pointX) {
|
||||||
|
addLines(Direction.EAST, pointX - x);
|
||||||
|
} else if (x > pointX + sizeX) {
|
||||||
|
addLines(Direction.WEST, x - (pointX + sizeX));
|
||||||
|
} else if (z < pointZ) {
|
||||||
|
addLines(Direction.NORTH, pointZ - z);
|
||||||
|
} else if (z > pointZ + sizeZ) {
|
||||||
|
addLines(Direction.SOUTH, z - (pointZ + sizeZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Region getRegion (int x, int z) {
|
||||||
|
checkCoordsInCache(x, z);
|
||||||
|
Region region;
|
||||||
|
if (regions[x - pointX][z - pointZ] == null) {
|
||||||
|
File file = new File(new File("worlds", world.getWorldId().toString()), MessageFormat.format(REGION_FILE_NAME_TEMPLATE, x, z));
|
||||||
|
if (!file.exists()) {
|
||||||
|
region = worldGenerator.generateRegion(x, z, world);
|
||||||
|
if (autoSaveRegionAfterGenerating) {
|
||||||
|
try {
|
||||||
|
regionReaderWriter.write(region);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while saving region data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
region = regionReaderWriter.read(x, z, world);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while loading region");
|
||||||
|
region = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRegion(region);
|
||||||
|
} else {
|
||||||
|
region = regions[x - pointX][z - pointZ];
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLines (Direction direction, int amount) {
|
||||||
|
int addBeforeX = 0;
|
||||||
|
int addAfterX = 0;
|
||||||
|
int addBeforeZ = 0;
|
||||||
|
int addAfterZ = 0;
|
||||||
|
switch (direction) {
|
||||||
|
case NORTH:
|
||||||
|
addBeforeZ = amount;
|
||||||
|
break;
|
||||||
|
case EAST:
|
||||||
|
addBeforeX = amount;
|
||||||
|
break;
|
||||||
|
case WEST:
|
||||||
|
addAfterX = amount;
|
||||||
|
break;
|
||||||
|
case SOUTH:
|
||||||
|
addAfterZ = amount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int tempSizeX = sizeX + addAfterX + addBeforeX;
|
||||||
|
int tempSizeZ = sizeZ + addAfterZ + addBeforeZ;
|
||||||
|
Region[][] temp = new Region[tempSizeX][tempSizeZ];
|
||||||
|
for (int x = 0; x < sizeX; x ++) {
|
||||||
|
System.arraycopy(regions[x], 0, temp[x + addBeforeX], addBeforeZ, sizeZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sizeX = tempSizeX;
|
||||||
|
this.sizeZ = tempSizeZ;
|
||||||
|
this.pointX = pointX - addBeforeX;
|
||||||
|
this.pointZ = pointZ - addBeforeZ;
|
||||||
|
} finally {
|
||||||
|
regionSaveLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package mc.world.generated_world.world;
|
||||||
|
|
||||||
|
public enum Temperature {
|
||||||
|
FROST,
|
||||||
|
COLD,
|
||||||
|
WARM,
|
||||||
|
HOT,
|
||||||
|
HOTTEST
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package mc.world.generated_world.world;
|
||||||
|
|
||||||
|
public enum Wetness {
|
||||||
|
DRIEST,
|
||||||
|
DRY,
|
||||||
|
WET,
|
||||||
|
WETTER,
|
||||||
|
WETTEST,
|
||||||
|
WATER
|
||||||
|
}
|
||||||
18
generated_world/src/main/resources/log4j2.xml
Normal file
18
generated_world/src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration>
|
||||||
|
<Appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="[%d{HH:mm:ss}] [%-5p] %m%n"/>
|
||||||
|
</Console>
|
||||||
|
<RollingFile name="File" fileName="log/log_file.log" filePattern="log/log_file-%d{MM-dd-yyyy}.log.gz">
|
||||||
|
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
|
||||||
|
<TimeBasedTriggeringPolicy />
|
||||||
|
</RollingFile>
|
||||||
|
</Appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="Trace">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
<AppenderRef ref="File"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package mc.world.generated_world;
|
||||||
|
|
||||||
|
import mc.world.generated_world.generator.SeedRandomGenerator;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
public class SeedRandomGeneratorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void randomGenSpeed () {
|
||||||
|
SeedRandomGenerator.random(0, 0, 0);
|
||||||
|
long avg = 0;
|
||||||
|
long min = -1;
|
||||||
|
long max = 0;
|
||||||
|
for (int i = 0; i < 500; i ++) {
|
||||||
|
int x = (int) (Math.random() * 10000);
|
||||||
|
int y = (int) (Math.random() * 10000);
|
||||||
|
int seed = (int) (Math.random() * 10000);
|
||||||
|
long time = System.nanoTime();
|
||||||
|
SeedRandomGenerator.random(x, y, seed);
|
||||||
|
time = System.nanoTime() - time;
|
||||||
|
System.out.printf("[%s] \t%.3fms\n", i+1, time/1000d);
|
||||||
|
avg += time;
|
||||||
|
if (min == -1) {
|
||||||
|
min = time;
|
||||||
|
} else if (min > time) {
|
||||||
|
min = time;
|
||||||
|
}
|
||||||
|
if (max < time) {
|
||||||
|
max = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
System.out.printf("Average time: %.3fms\n", avg/500000d);
|
||||||
|
System.out.printf("Minimum time: %.3fms\n", min/1000d);
|
||||||
|
System.out.printf("Maximum time: %.3fms\n", max/1000d);
|
||||||
|
assertTrue(avg/500 < 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void randomTest() throws Exception {
|
||||||
|
double maxDiff = 0;
|
||||||
|
double maxDisp = 0;
|
||||||
|
for (int i = 0; i < 100; i ++) {
|
||||||
|
double mid = 0;
|
||||||
|
double disp = 0;
|
||||||
|
int seed = (int) (Math.random() * Integer.MAX_VALUE);
|
||||||
|
for (int x = -1000; x < 1000; x++) {
|
||||||
|
for (int y = -1000; y < 1000; y++) {
|
||||||
|
double rnd = SeedRandomGenerator.random(x, y, seed);
|
||||||
|
mid += rnd;
|
||||||
|
disp += (rnd - 0.5) * (rnd - 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mid = mid/4000000;
|
||||||
|
disp = Math.sqrt(disp)/4000000;
|
||||||
|
if (maxDiff < Math.abs(mid - 0.5)) {
|
||||||
|
maxDiff = Math.abs(mid - 0.5);
|
||||||
|
}
|
||||||
|
if (maxDisp < disp) {
|
||||||
|
maxDisp = disp;
|
||||||
|
}
|
||||||
|
System.out.printf("Iteration %d.\t mid: %.3f, \tdisp %.6f\n", i + 1, mid, disp);
|
||||||
|
assertTrue(Math.abs(mid - 0.5) < 0.15);
|
||||||
|
}
|
||||||
|
System.out.printf("Max diff: %.3f\n", maxDiff);
|
||||||
|
System.out.printf("Max disp: %.6f\n", maxDisp);
|
||||||
|
|
||||||
|
|
||||||
|
assertTrue(maxDiff > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generateImage () throws Exception {
|
||||||
|
int h = 500;
|
||||||
|
int w = 500;
|
||||||
|
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||||
|
|
||||||
|
int seed = (int) (Math.random() * Integer.MAX_VALUE) / 1024;
|
||||||
|
for (int x = 0; x < w; x ++) {
|
||||||
|
for (int y = 0; y < h; y ++) {
|
||||||
|
image.setRGB(x, y, (int) (0xffffff * SeedRandomGenerator.random(x, y, seed)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageIO.write(image, "bmp", new File("out", "seed_random.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,3 +5,5 @@ include('flat_world')
|
|||||||
include('vanilla_commands')
|
include('vanilla_commands')
|
||||||
include('proto_1.12.2') // Protocol 1.12.2
|
include('proto_1.12.2') // Protocol 1.12.2
|
||||||
include('proto_1.12.2_netty') // Protocol 1.12.2 (Netty impl.)
|
include('proto_1.12.2_netty') // Protocol 1.12.2 (Netty impl.)
|
||||||
|
include 'generated_world'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user