Merge branch 'develop' into dmitriymx/location-refactory
# Conflicts: # core/src/main/java/mc/core/Location.java # flat_world/src/main/java/mc/world/flat/FlatWorld.java # generated_world/src/main/java/mc/world/generated_world/world/CubicWorld.java # proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java
This commit is contained in:
27
build.gradle
27
build.gradle
@@ -1,4 +1,4 @@
|
|||||||
subprojects {
|
allprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
|
|
||||||
compileJava {
|
compileJava {
|
||||||
@@ -10,7 +10,9 @@ subprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
ext {
|
ext {
|
||||||
slf4j_version = '1.7.21'
|
slf4j_version = '1.7.21'
|
||||||
spring_version = '4.2.5.RELEASE'
|
spring_version = '4.2.5.RELEASE'
|
||||||
@@ -33,6 +35,9 @@ subprojects {
|
|||||||
|
|
||||||
/* Components */
|
/* Components */
|
||||||
compile (group: 'org.projectlombok', name: 'lombok', version: '1.16.16')
|
compile (group: 'org.projectlombok', name: 'lombok', version: '1.16.16')
|
||||||
|
|
||||||
|
/* Test */
|
||||||
|
testCompile (group: 'junit', name: 'junit', version: '4.12')
|
||||||
}
|
}
|
||||||
|
|
||||||
task copyDep(type: Copy) {
|
task copyDep(type: Copy) {
|
||||||
@@ -44,3 +49,23 @@ subprojects {
|
|||||||
delete 'libs'
|
delete 'libs'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task runApp(type: JavaExec) {
|
||||||
|
main = 'mc.core.Main'
|
||||||
|
|
||||||
|
workingDir = (project.hasProperty("workDir") ? project.workDir : '.')
|
||||||
|
|
||||||
|
subprojects.findAll().each{ prj ->
|
||||||
|
classpath += prj.sourceSets.main.runtimeClasspath
|
||||||
|
}
|
||||||
|
/* Uncomment, if your Log Implements are folder '{workDir}/log-impl' */
|
||||||
|
//classpath += files(fileTree(dir: new File(workingDir, "log-impl")))
|
||||||
|
|
||||||
|
/* Uncomment, if you used VM args */
|
||||||
|
//jvmArgs = [
|
||||||
|
// "-DspringConfig=app.xml",
|
||||||
|
// "-Dlog4j.configurationFile=log4j2.xml"
|
||||||
|
//]
|
||||||
|
|
||||||
|
ignoreExitValue = true
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,18 +6,21 @@ package mc.core;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import mc.core.exception.ResourceUnloadedException;
|
||||||
import mc.core.world.World;
|
import mc.core.world.World;
|
||||||
|
import mc.core.world.chunk.Chunk;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.lang.ref.Reference;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
public class Location implements Serializable, Cloneable {
|
public class Location implements Serializable, Cloneable {
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private double x, y, z;
|
private double x, y, z;
|
||||||
private WeakReference<World> refWorld;
|
private Reference<World> refWorld;
|
||||||
|
|
||||||
public Location(double x, double y, double z, World world) {
|
public Location (double x, double y, double z, World world) {
|
||||||
setXYZ(x, y, z);
|
setXYZ(x, y, z);
|
||||||
setWorld(world);
|
setWorld(world);
|
||||||
}
|
}
|
||||||
@@ -33,10 +36,16 @@ public class Location implements Serializable, Cloneable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public World getWorld() {
|
public World getWorld() {
|
||||||
|
if (refWorld == null) {
|
||||||
|
return null;
|
||||||
|
} else if (refWorld.get() == null) {
|
||||||
|
throw new ResourceUnloadedException("You're trying to get unloaded world");
|
||||||
|
} else {
|
||||||
return refWorld.get();
|
return refWorld.get();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setWorld(World world) {
|
public void setWorld (World world) {
|
||||||
this.refWorld = new WeakReference<>(world);
|
this.refWorld = new WeakReference<>(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +61,22 @@ public class Location implements Serializable, Cloneable {
|
|||||||
return (int) z;
|
return (int) z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Chunk getChunk() {
|
||||||
|
World world;
|
||||||
|
if ((world = getWorld()) == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return world.getRegion((int) (x / 256), (int) (z / 256))
|
||||||
|
.getChunk((int) ((x % 256) / 16), (int) ((z % 256) / 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Location clone() {
|
public Location clone() {
|
||||||
try {
|
try {
|
||||||
return (Location) super.clone();
|
return (Location) super.clone();
|
||||||
} catch (CloneNotSupportedException e) { // такое в нашем случае вообще возможно?
|
} catch (CloneNotSupportedException e) { // такое в нашем случае вообще возможно?
|
||||||
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package mc.core.exception;
|
||||||
|
|
||||||
|
public abstract class McCoreUncheckedException extends RuntimeException {
|
||||||
|
|
||||||
|
public McCoreUncheckedException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public McCoreUncheckedException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package mc.core.exception;
|
||||||
|
|
||||||
|
public class ResourceUnloadedException extends McCoreUncheckedException {
|
||||||
|
|
||||||
|
public ResourceUnloadedException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package mc.core.serialization;
|
package mc.core.serialization;
|
||||||
|
|
||||||
import mc.core.world.chunk.Chunk;
|
import mc.core.world.ChunkSection;
|
||||||
import mc.core.world.Region;
|
import mc.core.world.Region;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public interface IChunkReader {
|
public interface IChunkReader {
|
||||||
Chunk read (Region region, int x, int y, int z) throws IOException;
|
ChunkSection read (Region region, int x, int y, int z) throws IOException;
|
||||||
}
|
}
|
||||||
|
|||||||
45
core/src/main/java/mc/core/world/ChunkSection.java
Normal file
45
core/src/main/java/mc/core/world/ChunkSection.java
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* DmitriyMX <dimon550@gmail.com>
|
||||||
|
* 2018-04-15
|
||||||
|
*/
|
||||||
|
package mc.core.world;
|
||||||
|
|
||||||
|
import mc.core.world.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 */
|
||||||
|
public interface ChunkSection extends Serializable{
|
||||||
|
|
||||||
|
int getSkyLight(int x, int y, int z);
|
||||||
|
void setSkyLight(int x, int y, int z, int lightLevel);
|
||||||
|
|
||||||
|
int getAddition(int x, int y, int z);
|
||||||
|
void setAddition(int x, int y, int z, int value);
|
||||||
|
|
||||||
|
Biome getBiome(int x, int z);
|
||||||
|
void setBiome(int x, int z, Biome biome);
|
||||||
|
|
||||||
|
int getX();
|
||||||
|
int getY();
|
||||||
|
int getZ();
|
||||||
|
|
||||||
|
void setBlock(Block block);
|
||||||
|
Block getBlock(int x, int y, int z);
|
||||||
|
|
||||||
|
Region getRegion();
|
||||||
|
World getWorld();
|
||||||
|
}
|
||||||
@@ -22,8 +22,13 @@ import java.io.Serializable;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public interface Region extends Serializable{
|
public interface Region extends Serializable{
|
||||||
Chunk getChunkAt(int x, int y, int z);
|
Chunk getChunk (int x, int z);
|
||||||
void setChunk(int x, int y, int z, Chunk chunk);
|
void setChunk(int x, int z, Chunk chunk);
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
ChunkSection getChunkAt(int x, int y, int z);
|
||||||
|
@Deprecated
|
||||||
|
void setChunk(int x, int y, int z, ChunkSection chunkSection);
|
||||||
|
|
||||||
int getX();
|
int getX();
|
||||||
int getZ();
|
int getZ();
|
||||||
@@ -31,5 +36,7 @@ public interface Region extends Serializable{
|
|||||||
Biome getBiomeAt (int x, int z);
|
Biome getBiomeAt (int x, int z);
|
||||||
void setBiome (int x, int z, Biome biome);
|
void setBiome (int x, int z, Biome biome);
|
||||||
|
|
||||||
void save(Serializer<Chunk> chunkSerializer, IRegionReaderWriter regionReaderWritter) throws IOException;
|
World getWorld();
|
||||||
|
|
||||||
|
void save(Serializer<ChunkSection> chunkSerializer, IRegionReaderWriter regionReaderWritter) throws IOException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package mc.core.world;
|
|||||||
|
|
||||||
import mc.core.EntityLocation;
|
import mc.core.EntityLocation;
|
||||||
import mc.core.nbt.Taggable;
|
import mc.core.nbt.Taggable;
|
||||||
import mc.core.world.chunk.Chunk;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -49,8 +48,8 @@ public interface World extends Taggable, Serializable{
|
|||||||
EntityLocation getSpawn();
|
EntityLocation getSpawn();
|
||||||
void setSpawn(EntityLocation location);
|
void setSpawn(EntityLocation location);
|
||||||
|
|
||||||
Chunk getChunk(int x, int y, int z);
|
ChunkSection getChunk(int x, int y, int z);
|
||||||
void setChunk(int x, int y, int z, Chunk chunk);
|
void setChunk(int x, int y, int z, ChunkSection chunkSection);
|
||||||
|
|
||||||
Region getRegion(int x, int z);
|
Region getRegion(int x, int z);
|
||||||
void setRegion(int x, int z, Region region);
|
void setRegion(int x, int z, Region region);
|
||||||
|
|||||||
@@ -1,46 +1,16 @@
|
|||||||
/*
|
|
||||||
* DmitriyMX <dimon550@gmail.com>
|
|
||||||
* 2018-04-15
|
|
||||||
*/
|
|
||||||
package mc.core.world.chunk;
|
package mc.core.world.chunk;
|
||||||
|
|
||||||
import mc.core.Location;
|
import mc.core.world.ChunkSection;
|
||||||
import mc.core.world.Biome;
|
import mc.core.world.Region;
|
||||||
import mc.core.world.block.Block;
|
import mc.core.world.World;
|
||||||
|
|
||||||
import java.io.Serializable;
|
public interface Chunk {
|
||||||
|
|
||||||
/**
|
World getWorld();
|
||||||
* Serialization chunk info
|
ChunkSection getChunkSection(int height);
|
||||||
*
|
ChunkSection setChunkSection(int height, ChunkSection chunkSection);
|
||||||
* +-------------+----------------+------------+
|
Region getRegion();
|
||||||
* | 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 */
|
|
||||||
public interface Chunk extends Serializable{
|
|
||||||
Block getBlock(int x, int y, int z);
|
|
||||||
default Block getBlock(Location location) {
|
|
||||||
return getBlock(location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
|
||||||
}
|
|
||||||
void setBlock(Block block);
|
|
||||||
|
|
||||||
int getSkyLight(int x, int y, int z);
|
|
||||||
void setSkyLight(int x, int y, int z, int lightLevel);
|
|
||||||
|
|
||||||
int getAddition(int x, int y, int z);
|
|
||||||
void setAddition(int x, int y, int z, int value);
|
|
||||||
|
|
||||||
Biome getBiome(int x, int z);
|
|
||||||
void setBiome(int x, int z, Biome biome);
|
|
||||||
|
|
||||||
int getX();
|
int getX();
|
||||||
int getY();
|
|
||||||
int getZ();
|
int getZ();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package mc.core.world.chunk;
|
package mc.core.world.chunk;
|
||||||
|
|
||||||
|
import mc.core.world.ChunkSection;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface ChunkLoader {
|
public interface ChunkLoader {
|
||||||
@@ -12,7 +14,7 @@ public interface ChunkLoader {
|
|||||||
* @param z chunk position
|
* @param z chunk position
|
||||||
* @return optional of chunk (nullable)
|
* @return optional of chunk (nullable)
|
||||||
*/
|
*/
|
||||||
Optional<Chunk> loadChunk (int x, int y, int z);
|
Optional<ChunkSection> loadChunk (int x, int y, int z);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to load chunk like {@link #loadChunk(int, int, int)}
|
* Tries to load chunk like {@link #loadChunk(int, int, int)}
|
||||||
@@ -23,5 +25,5 @@ public interface ChunkLoader {
|
|||||||
* @param z chunk position
|
* @param z chunk position
|
||||||
* @return chunk
|
* @return chunk
|
||||||
*/
|
*/
|
||||||
Chunk loadOrGenerateChunk (int x, int y, int z);
|
ChunkSection loadOrGenerateChunk (int x, int y, int z);
|
||||||
}
|
}
|
||||||
|
|||||||
7
event-loop/TODO
Normal file
7
event-loop/TODO
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
- Система иерархических блокировок ресурсов (чанки в мире)
|
||||||
|
- Нужно что-то делать с подгрузкой отсутсвующих чанков для таких блокировок
|
||||||
|
- Возможность вызвать событие из EventHandler
|
||||||
|
- Performance Monitor
|
||||||
|
- Возможная проблема с переполнением очереди при спаме пакетами от игрока
|
||||||
|
- Добавить поля с замками для ресурсов (Player, World, Chunk)
|
||||||
|
- Time Scheduler
|
||||||
15
event-loop/build.gradle
Normal file
15
event-loop/build.gradle
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
group 'mc'
|
||||||
|
version '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
/* Core */
|
||||||
|
compile_excludeCopy project(':core')
|
||||||
|
|
||||||
|
testCompile group: 'org.slf4j', name: 'slf4j-simple', version: '1.6.1'
|
||||||
|
testCompile group: 'com.carrotsearch', name: 'junit-benchmarks', version: '0.7.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
test {
|
||||||
|
exclude "ru/core/events/*Benchmark.class"
|
||||||
|
}
|
||||||
113
event-loop/src/main/java/mc/core/events/EventPipelineTask.java
Normal file
113
event-loop/src/main/java/mc/core/events/EventPipelineTask.java
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package mc.core.events;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.events.api.EventQueueOwner;
|
||||||
|
import mc.core.events.api.LockableResource;
|
||||||
|
import mc.core.events.runner.lock.LockObserveList;
|
||||||
|
import mc.core.events.runner.ResourceAwareExecutorService;
|
||||||
|
import mc.core.events.runner.ResourceAwareRunnable;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds processing pipeline for every event
|
||||||
|
* that enters {@link FullAsyncEventLoop}.
|
||||||
|
* <p>
|
||||||
|
* Ensures that EventHandlers will never be called in a wrong
|
||||||
|
* order by feeding only one task at a time to the {@link ResourceAwareExecutorService}
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Slf4j
|
||||||
|
public class EventPipelineTask {
|
||||||
|
private final ResourceAwareExecutorService service;
|
||||||
|
private final List<RegisteredEventHandler> handlers;
|
||||||
|
private final FullAsyncEventLoop manager;
|
||||||
|
private final Event event;
|
||||||
|
private final EventQueueOwner owner;
|
||||||
|
private int currentIndex = 0;
|
||||||
|
@Setter
|
||||||
|
private PipelineState state = PipelineState.IDLE;
|
||||||
|
|
||||||
|
public void next() {
|
||||||
|
if (updatePipelineState()) return;
|
||||||
|
|
||||||
|
RegisteredEventHandler handler = handlers.get(currentIndex);
|
||||||
|
// If event has been already cancelled and current handler
|
||||||
|
// ignores cancelled events
|
||||||
|
if (event.isCanceled() && handler.isIgnoreCancelled()) {
|
||||||
|
// Just skip current event handler
|
||||||
|
currentIndex++;
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
feedTask(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update current pipeline status
|
||||||
|
*
|
||||||
|
* @return true if pipeline has been completed
|
||||||
|
*/
|
||||||
|
private boolean updatePipelineState() {
|
||||||
|
if (state == PipelineState.IDLE) {
|
||||||
|
state = PipelineState.WORKING;
|
||||||
|
}
|
||||||
|
if (currentIndex >= handlers.size() && state == PipelineState.WORKING) {
|
||||||
|
state = PipelineState.FINISHED;
|
||||||
|
manager.update(owner);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == PipelineState.FINISHED) {
|
||||||
|
throw new IllegalStateException("Attempted to call next step on a FINISHED pipeline");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void feedTask(RegisteredEventHandler handler) {
|
||||||
|
LockObserveList locks = getLocks(handler);
|
||||||
|
service.addTask(new ResourceAwareRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
handler.getMethod().invoke(handler.getObject(), event);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||||
|
log.error("Unable to dispatch event " + event.getClass().getSimpleName() + " to handler " + event.getClass().getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void after() {
|
||||||
|
currentIndex++;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LockObserveList getLocks() {
|
||||||
|
return locks;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private LockObserveList getLocks(RegisteredEventHandler handler) {
|
||||||
|
LockObserveList locks = new LockObserveList();
|
||||||
|
|
||||||
|
if (handler.isPluginSynchronize())
|
||||||
|
locks.add(manager.getResourceManager().getPluginLock(handler.getPlugin()));
|
||||||
|
|
||||||
|
for (LockableResource resource : handler.getLock()) {
|
||||||
|
locks.addAll(manager.getResourceManager().getAnnotationLocks(resource, event));
|
||||||
|
}
|
||||||
|
|
||||||
|
return locks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PipelineState {
|
||||||
|
IDLE, WORKING, FINISHED
|
||||||
|
}
|
||||||
|
}
|
||||||
121
event-loop/src/main/java/mc/core/events/FullAsyncEventLoop.java
Normal file
121
event-loop/src/main/java/mc/core/events/FullAsyncEventLoop.java
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package mc.core.events;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.events.api.EventHandler;
|
||||||
|
import mc.core.events.api.EventQueueOwner;
|
||||||
|
import mc.core.events.api.Plugin;
|
||||||
|
import mc.core.events.runner.ResourceAwareExecutorService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event loop core. Manages event handler registration process,
|
||||||
|
* maintains event queues.
|
||||||
|
* <p>
|
||||||
|
* This event loop guarantees that events, assigned to the {@link EventQueueOwner}
|
||||||
|
* will be handler in order of scheduling
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class FullAsyncEventLoop {
|
||||||
|
// Item leaves this queue only when EventPipeline is fully executed
|
||||||
|
private Map<EventQueueOwner, Queue<EventPipelineTask>> eventQueue = new ConcurrentHashMap<>();
|
||||||
|
private Map<Class<? extends Event>, List<RegisteredEventHandler>> registeredHandlers = new HashMap<>();
|
||||||
|
@SuppressWarnings("SpringJavaAutowiredMembersInspection")
|
||||||
|
@Autowired
|
||||||
|
@Setter
|
||||||
|
private ResourceAwareExecutorService resourceAwareExecutorService;
|
||||||
|
@Getter
|
||||||
|
private SharedResourceManager resourceManager = new SharedResourceManager();
|
||||||
|
|
||||||
|
public void addEventHandler(Plugin plugin, Object object) {
|
||||||
|
Map<Method, EventHandler> candidates = getEventHandlerCandidates(object);
|
||||||
|
|
||||||
|
for (Map.Entry<Method, EventHandler> pair : candidates.entrySet()) {
|
||||||
|
@SuppressWarnings("unchecked") Class<? extends Event> eventType = (Class<? extends Event>) pair.getKey().getParameterTypes()[0];
|
||||||
|
List<RegisteredEventHandler> handlers = this.registeredHandlers.computeIfAbsent(eventType, e -> new ArrayList<>());
|
||||||
|
handlers.add(new RegisteredEventHandler(plugin, object, pair.getKey(), pair.getValue().lock(), pair.getValue().pluginSynchronize(), pair.getValue().priority().getValue(), pair.getValue().ignoreCancelled()));
|
||||||
|
handlers.sort(Comparator.comparingInt(RegisteredEventHandler::getPriority));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RegisteredEventHandler> getPipelineForEvent(Event event) {
|
||||||
|
return registeredHandlers.get(event.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Method, EventHandler> getEventHandlerCandidates(Object object) {
|
||||||
|
Map<Method, EventHandler> candidates;
|
||||||
|
candidates = new HashMap<>();
|
||||||
|
for (Method method : object.getClass().getDeclaredMethods()) {
|
||||||
|
EventHandler annotation = method.getAnnotation(EventHandler.class);
|
||||||
|
if (annotation == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!Modifier.isPublic(method.getModifiers())) {
|
||||||
|
log.error("Unable to register {} as an EventHandler. Method must have a 'public' access modifier.", method.toString());
|
||||||
|
continue;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method.getParameterCount() != 1) {
|
||||||
|
log.error("Unable to register {} as an EventHandler. Method must have exactly one argument.", method.toString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> firstParamType = method.getParameterTypes()[0];
|
||||||
|
if (!Event.class.isAssignableFrom(firstParamType)) {
|
||||||
|
log.error("Unable to register {} as an EventHandler. First parameter type must implement 'Event' interface.", method.toString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
method.setAccessible(true);
|
||||||
|
candidates.put(method, annotation);
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void asyncFireEvent(EventQueueOwner owner, Event event) {
|
||||||
|
List<RegisteredEventHandler> handlers = getPipelineForEvent(event);
|
||||||
|
if (handlers == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Queue<EventPipelineTask> queue = eventQueue.computeIfAbsent(owner, s -> new ArrayDeque<>());
|
||||||
|
queue.add(new EventPipelineTask(resourceAwareExecutorService, handlers, this, event, owner));
|
||||||
|
update(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates queue state for a given owner:
|
||||||
|
* <p>
|
||||||
|
* - Removes first element of a queue if it is marked as FINISHED
|
||||||
|
* - Starts executing first pipeline from the queue if it is marked with IDLE
|
||||||
|
*
|
||||||
|
* @param owner queue owner
|
||||||
|
*/
|
||||||
|
public synchronized void update(EventQueueOwner owner) {
|
||||||
|
if (!eventQueue.containsKey(owner)) {
|
||||||
|
log.warn("Unable to update pipeline executor: unable to find queue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Queue<EventPipelineTask> queue = eventQueue.get(owner);
|
||||||
|
if (queue.isEmpty()) {
|
||||||
|
log.warn("Unable to update pipeline executor: queue is empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queue.peek().getState() == EventPipelineTask.PipelineState.FINISHED) {
|
||||||
|
queue.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
EventPipelineTask pipeline;
|
||||||
|
if ((pipeline = queue.peek()) != null
|
||||||
|
&& pipeline.getState() == EventPipelineTask.PipelineState.IDLE) {
|
||||||
|
pipeline.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package mc.core.events;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import mc.core.events.api.LockableResource;
|
||||||
|
import mc.core.events.api.Plugin;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds all the information necessary to register an
|
||||||
|
* event handler in an event loop
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class RegisteredEventHandler {
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final Object object;
|
||||||
|
private final Method method;
|
||||||
|
private final LockableResource[] lock;
|
||||||
|
private final boolean pluginSynchronize;
|
||||||
|
private final int priority;
|
||||||
|
private final boolean ignoreCancelled;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package mc.core.events;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.Location;
|
||||||
|
import mc.core.events.api.LockableResource;
|
||||||
|
import mc.core.events.api.Plugin;
|
||||||
|
import mc.core.events.api.interfaces.LocationProvidingEvent;
|
||||||
|
import mc.core.events.api.interfaces.PlayerProvidingEvent;
|
||||||
|
import mc.core.events.api.interfaces.WorldProvidingEvent;
|
||||||
|
import mc.core.events.runner.lock.PoorMansLock;
|
||||||
|
import mc.core.player.Player;
|
||||||
|
import mc.core.world.World;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class SharedResourceManager {
|
||||||
|
private Map<Plugin, PoorMansLock> pluginLocks = new ConcurrentHashMap<>();
|
||||||
|
// TODO: Memory leak HERE. Fix with introducing field to Player class
|
||||||
|
private Map<Player, PoorMansLock> playerLocks = new ConcurrentHashMap<>();
|
||||||
|
// TODO: Memory leak HERE. Fix with introducing field to World class
|
||||||
|
private Map<World, PoorMansLock> worldLocks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public PoorMansLock getPluginLock(Plugin plugin) {
|
||||||
|
return pluginLocks.computeIfAbsent(plugin, s -> new PoorMansLock());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PoorMansLock getPlayerLock(Player player) {
|
||||||
|
return playerLocks.computeIfAbsent(player, s -> new PoorMansLock());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PoorMansLock getWorldLock(World world) {
|
||||||
|
return worldLocks.computeIfAbsent(world, s -> new PoorMansLock());
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T require(LockableResource resource, Event event, Class<T> inter) {
|
||||||
|
if (inter.isInstance(event)) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (T) event;
|
||||||
|
} else
|
||||||
|
throw new IllegalArgumentException("Unable to lock " + resource + " while attempting to process event. Event " + event.getClass().getSimpleName() + " must implement " + inter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<PoorMansLock> getAnnotationLocks(LockableResource resource, Event event) {
|
||||||
|
switch (resource) {
|
||||||
|
case PLAYER:
|
||||||
|
return require(resource, event, PlayerProvidingEvent.class).getAssociatedPlayers().stream().map(this::getPlayerLock).collect(Collectors.toList());
|
||||||
|
case PLAYER_WORLD:
|
||||||
|
return require(resource, event, PlayerProvidingEvent.class).getAssociatedPlayers().stream().map(s -> s.getLocation().getWorld()).map(this::getWorldLock).collect(Collectors.toList());
|
||||||
|
case EVENT_LOCATION_WORLD:
|
||||||
|
return require(resource, event, LocationProvidingEvent.class).getAssociatedLocations().stream().map(Location::getWorld).map(this::getWorldLock).collect(Collectors.toList());
|
||||||
|
case EVENT_WORLD:
|
||||||
|
return require(resource, event, WorldProvidingEvent.class).getAssociatedWorlds().stream().map(this::getWorldLock).collect(Collectors.toList());
|
||||||
|
default:
|
||||||
|
log.warn("Unable to find action for " + resource + " resource definition.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package mc.core.events.api;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Inherited
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface EventHandler {
|
||||||
|
EventPriority priority() default EventPriority.NORMAL;
|
||||||
|
|
||||||
|
boolean ignoreCancelled() default false;
|
||||||
|
|
||||||
|
boolean pluginSynchronize() default true;
|
||||||
|
|
||||||
|
LockableResource[] lock() default {};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package mc.core.events.api;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public enum EventPriority {
|
||||||
|
LOWEST(0),
|
||||||
|
LOW(1),
|
||||||
|
NORMAL(2),
|
||||||
|
HIGH(3),
|
||||||
|
HIGHEST(4),
|
||||||
|
MONITOR(5);
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
EventPriority(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package mc.core.events.api;
|
||||||
|
|
||||||
|
public interface EventQueueOwner {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package mc.core.events.api;
|
||||||
|
|
||||||
|
public enum LockableResource {
|
||||||
|
PLAYER,
|
||||||
|
PLAYER_WORLD,
|
||||||
|
EVENT_LOCATION_WORLD,
|
||||||
|
EVENT_WORLD
|
||||||
|
|
||||||
|
// TODO: Add entity-related constants
|
||||||
|
}
|
||||||
4
event-loop/src/main/java/mc/core/events/api/Plugin.java
Normal file
4
event-loop/src/main/java/mc/core/events/api/Plugin.java
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package mc.core.events.api;
|
||||||
|
|
||||||
|
public interface Plugin {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package mc.core.events.api.interfaces;
|
||||||
|
|
||||||
|
import mc.core.Location;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public interface LocationProvidingEvent {
|
||||||
|
Collection<Location> getAssociatedLocations();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package mc.core.events.api.interfaces;
|
||||||
|
|
||||||
|
import mc.core.Location;
|
||||||
|
import mc.core.player.Player;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PlayerProvidingEvent extends LocationProvidingEvent {
|
||||||
|
List<Player> getAssociatedPlayers();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default Collection<Location> getAssociatedLocations() {
|
||||||
|
List<Player> players = getAssociatedPlayers();
|
||||||
|
if (players.size() == 1)
|
||||||
|
return Collections.singletonList(players.get(0).getLocation());
|
||||||
|
else
|
||||||
|
throw new RuntimeException("This method is not implemented.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package mc.core.events.api.interfaces;
|
||||||
|
|
||||||
|
import mc.core.world.World;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public interface WorldProvidingEvent {
|
||||||
|
Collection<World> getAssociatedWorlds();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package mc.core.events.api.samples;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import mc.core.Location;
|
||||||
|
import mc.core.events.EventBase;
|
||||||
|
import mc.core.events.api.interfaces.LocationProvidingEvent;
|
||||||
|
import mc.core.events.api.interfaces.PlayerProvidingEvent;
|
||||||
|
import mc.core.player.Player;
|
||||||
|
import mc.core.world.block.Block;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class BlockBreakEvent extends EventBase implements PlayerProvidingEvent, LocationProvidingEvent {
|
||||||
|
private final Player player;
|
||||||
|
private final Block block;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Player> getAssociatedPlayers() {
|
||||||
|
return Collections.singletonList(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Location> getAssociatedLocations() {
|
||||||
|
return Collections.singletonList(block.getLocation());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package mc.core.events.runner;
|
||||||
|
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple scheduling strategy.
|
||||||
|
* <p>
|
||||||
|
* We wait until the first task in a queue will be able to acquire all
|
||||||
|
* the necessary resources and then we schedule it for execution
|
||||||
|
*/
|
||||||
|
public class AllInScheduleStrategy implements ScheduleStrategy {
|
||||||
|
private BlockingQueue<ResourceAwareRunnable> globalQueue;
|
||||||
|
private ResourceAwareExecutorService resourceAwareExecutorService;
|
||||||
|
|
||||||
|
public AllInScheduleStrategy(ResourceAwareExecutorService resourceAwareExecutorService) {
|
||||||
|
this.globalQueue = resourceAwareExecutorService.queue;
|
||||||
|
this.resourceAwareExecutorService = resourceAwareExecutorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized ResourceAwareRunnable getTask() throws InterruptedException {
|
||||||
|
waitForResourceLockComplete();
|
||||||
|
|
||||||
|
// Wait for new task in queue
|
||||||
|
ResourceAwareRunnable runnable = globalQueue.take();
|
||||||
|
while (!runnable.getLocks().isReady()) {
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
runnable.getLocks().setCallback(latch::countDown);
|
||||||
|
// Prevent situations where dependencies were resolved
|
||||||
|
// while we were setting up the callback
|
||||||
|
if (runnable.getLocks().isReady())
|
||||||
|
continue;
|
||||||
|
latch.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock execution for the next thread
|
||||||
|
// (wait until resources for previous task will be blocked)
|
||||||
|
resourceAwareExecutorService.waitForLock.set(true);
|
||||||
|
return runnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until the last scheduled task will lock all the necessary resources.
|
||||||
|
* <p>
|
||||||
|
* It is required to avoid race-condition when an execution candidate task (first task in a queue)
|
||||||
|
* skips lock-await procedure due to the last scheduled task not having locked necessary resources yet.
|
||||||
|
*
|
||||||
|
* @throws InterruptedException if current thread is interrupted
|
||||||
|
*/
|
||||||
|
private void waitForResourceLockComplete() throws InterruptedException {
|
||||||
|
synchronized (resourceAwareExecutorService.waitForLock) {
|
||||||
|
while (resourceAwareExecutorService.waitForLock.get()) {
|
||||||
|
resourceAwareExecutorService.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package mc.core.events.runner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker thread for {@link ResourceAwareExecutorService}.
|
||||||
|
* <p>
|
||||||
|
* - Awaits for tasks from {@link ScheduleStrategy}
|
||||||
|
* - Locks up resources for this task
|
||||||
|
* - Notifies {@link ScheduleStrategy} when resource-locking procedure is complete
|
||||||
|
* - Executes the runnable in this thread
|
||||||
|
* - Unlocks all the resources
|
||||||
|
* - Calls {@link ResourceAwareRunnable#after()} callback
|
||||||
|
*/
|
||||||
|
public class ExecutorWorkerThread extends Thread {
|
||||||
|
private ResourceAwareExecutorService service;
|
||||||
|
|
||||||
|
public ExecutorWorkerThread(String name, ResourceAwareExecutorService service) {
|
||||||
|
super(name);
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted() && isAlive()) {
|
||||||
|
ResourceAwareRunnable runnable;
|
||||||
|
try {
|
||||||
|
runnable = service.getStrategy().getTask();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executeTask(runnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void executeTask(ResourceAwareRunnable runnable) {
|
||||||
|
runnable.getLocks().lockAll();
|
||||||
|
notifyLockingDone();
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} finally {
|
||||||
|
runnable.getLocks().unlockAll();
|
||||||
|
runnable.getLocks().release();
|
||||||
|
}
|
||||||
|
runnable.after();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyLockingDone() {
|
||||||
|
synchronized (service.waitForLock) {
|
||||||
|
if (service.waitForLock.get()) {
|
||||||
|
service.waitForLock.set(false);
|
||||||
|
service.waitForLock.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package mc.core.events.runner;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom implementation of an ExecutorService.
|
||||||
|
*
|
||||||
|
* Holds a queue of {@link ResourceAwareRunnable} and executes them in a thread pool.
|
||||||
|
*
|
||||||
|
* Warning! This class doesn't guarantee, that tasks will be executed in any specific order.
|
||||||
|
* In fact, it's up to {@link ScheduleStrategy} to decide which task will be scheduled for
|
||||||
|
* execution next.
|
||||||
|
*/
|
||||||
|
public class ResourceAwareExecutorService {
|
||||||
|
private static final boolean WORKER_INSTANT_EXECUTE = false;
|
||||||
|
BlockingQueue<ResourceAwareRunnable> queue = new ArrayBlockingQueue<>(100);
|
||||||
|
// A synchronize aid, that prevents ScheduleStrategy from returning
|
||||||
|
// wrong tasks when executor is late in blocking resources
|
||||||
|
final AtomicBoolean waitForLock = new AtomicBoolean(false);
|
||||||
|
private ScheduleStrategy strategy = new AllInScheduleStrategy(this);
|
||||||
|
private Set<Thread> executorThreads = new HashSet<>();
|
||||||
|
private int threadCount;
|
||||||
|
|
||||||
|
public ResourceAwareExecutorService(int threadCount) {
|
||||||
|
this.threadCount = threadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (executorThreads.size() > 0)
|
||||||
|
throw new RuntimeException("This executor service was already started.");
|
||||||
|
|
||||||
|
for (int i = 0; i < threadCount; i++) {
|
||||||
|
Thread thread = new ExecutorWorkerThread("Event Loop #" + i, this);
|
||||||
|
executorThreads.add(thread);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (executorThreads.size() == 0)
|
||||||
|
throw new RuntimeException("This executor service was not initialized yet.");
|
||||||
|
|
||||||
|
Iterator<Thread> iterator = executorThreads.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Thread thread = iterator.next();
|
||||||
|
thread.interrupt();
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTask(ResourceAwareRunnable task) {
|
||||||
|
if (WORKER_INSTANT_EXECUTE && Thread.currentThread() instanceof ExecutorWorkerThread) {
|
||||||
|
((ExecutorWorkerThread) Thread.currentThread()).executeTask(task);
|
||||||
|
} else
|
||||||
|
queue.offer(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ScheduleStrategy getStrategy() {
|
||||||
|
return strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DefaultScheduleStrategy implements ScheduleStrategy {
|
||||||
|
public ResourceAwareRunnable getTask() throws InterruptedException {
|
||||||
|
return queue.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package mc.core.events.runner;
|
||||||
|
|
||||||
|
import mc.core.events.runner.lock.LockObserveList;
|
||||||
|
|
||||||
|
public interface ResourceAwareRunnable extends Runnable {
|
||||||
|
default LockObserveList getLocks() {
|
||||||
|
return LockObserveList.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
default void after() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package mc.core.events.runner;
|
||||||
|
|
||||||
|
public interface ScheduleStrategy {
|
||||||
|
ResourceAwareRunnable getTask() throws InterruptedException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package mc.core.events.runner.lock;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class LockObserveList implements Consumer<PoorMansLock> {
|
||||||
|
public static LockObserveList EMPTY_LIST = new LockObserveList();
|
||||||
|
private List<PoorMansLock> locks = new ArrayList<>();
|
||||||
|
private Runnable callback;
|
||||||
|
|
||||||
|
public void setCallback(Runnable callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(PoorMansLock lock) {
|
||||||
|
locks.add(lock);
|
||||||
|
lock.addCallback(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAll(Iterable<PoorMansLock> locks) {
|
||||||
|
for (PoorMansLock lock : locks)
|
||||||
|
add(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
callback = null;
|
||||||
|
for (PoorMansLock lock : locks) {
|
||||||
|
lock.removeCallback(this);
|
||||||
|
}
|
||||||
|
locks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReady() {
|
||||||
|
for (PoorMansLock lock : locks) {
|
||||||
|
if (lock.isLocked())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void lockAll() {
|
||||||
|
for (PoorMansLock lock : locks)
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockAll() {
|
||||||
|
for (PoorMansLock lock : locks)
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(PoorMansLock lock) {
|
||||||
|
if (!lock.isLocked()) {
|
||||||
|
if (isReady()) {
|
||||||
|
if (callback != null)
|
||||||
|
callback.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package mc.core.events.runner.lock;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class PoorMansLock {
|
||||||
|
private Thread owner = null;
|
||||||
|
private Set<Consumer<PoorMansLock>> callbacks = new CopyOnWriteArraySet<>();
|
||||||
|
|
||||||
|
public void addCallback(Consumer<PoorMansLock> callback) {
|
||||||
|
callbacks.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCallback(Consumer<PoorMansLock> callback) {
|
||||||
|
callbacks.remove(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isLocked() {
|
||||||
|
return owner != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerUpdate() {
|
||||||
|
for (Consumer<PoorMansLock> consumer : callbacks)
|
||||||
|
consumer.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void lock() {
|
||||||
|
if(owner == Thread.currentThread())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (owner != null) {
|
||||||
|
throw new RuntimeException("Unable to lock this resource: already in use");
|
||||||
|
}
|
||||||
|
|
||||||
|
owner = Thread.currentThread();
|
||||||
|
triggerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void unlock() {
|
||||||
|
if (owner == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (owner != Thread.currentThread()) {
|
||||||
|
throw new RuntimeException("Attempt to unlock resource from non-owning thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
owner = null;
|
||||||
|
triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
45
event-loop/src/main/java/mc/core/timings/ThreadTimings.java
Normal file
45
event-loop/src/main/java/mc/core/timings/ThreadTimings.java
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package mc.core.timings;
|
||||||
|
|
||||||
|
import java.util.Stack;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public class ThreadTimings {
|
||||||
|
private static AtomicInteger IDS = new AtomicInteger();
|
||||||
|
private int threadId;
|
||||||
|
private Stack<Timings> stack = new Stack<>();
|
||||||
|
|
||||||
|
public ThreadTimings() {
|
||||||
|
this.threadId = IDS.getAndIncrement();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stack<Timings> getStack() {
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getThreadId() {
|
||||||
|
return threadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Timings start() {
|
||||||
|
Timings timings = new Timings(this, stack.size());
|
||||||
|
getTimingsManager().waitForTimingsInitialize();
|
||||||
|
stack.push(timings);
|
||||||
|
getTimingsManager().notifyTimings(this, timings, true);
|
||||||
|
return timings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimingsManager getTimingsManager() {
|
||||||
|
return Timings.getTimingsManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void end(Timings finished) {
|
||||||
|
Timings timings = null;
|
||||||
|
while (!stack.isEmpty() && timings != finished) {
|
||||||
|
getTimingsManager().waitForTimingsInitialize();
|
||||||
|
timings = stack.pop();
|
||||||
|
if (!timings.hasFinished())
|
||||||
|
timings.finish();
|
||||||
|
getTimingsManager().notifyTimings(this, timings, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
event-loop/src/main/java/mc/core/timings/Timings.java
Normal file
51
event-loop/src/main/java/mc/core/timings/Timings.java
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package mc.core.timings;
|
||||||
|
|
||||||
|
public class Timings implements AutoCloseable {
|
||||||
|
private ThreadTimings threadTimings;
|
||||||
|
private long acquireTime;
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private long endTime = -1;
|
||||||
|
private int id;
|
||||||
|
|
||||||
|
public Timings(ThreadTimings threadTimings, int id) {
|
||||||
|
this.id = id;
|
||||||
|
this.threadTimings = threadTimings;
|
||||||
|
this.acquireTime = System.nanoTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Timings start() {
|
||||||
|
return TimingsStaticAccessor.getTimingsManager().getCurrentThreadTimings().start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimingsManager getTimingsManager() {
|
||||||
|
return TimingsStaticAccessor.getTimingsManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getEndTime() {
|
||||||
|
return endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAcquireTime() {
|
||||||
|
return acquireTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasFinished() {
|
||||||
|
return endTime != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void finish() {
|
||||||
|
if (hasFinished())
|
||||||
|
throw new IllegalStateException("This timing was already finished");
|
||||||
|
this.endTime = System.nanoTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
finish();
|
||||||
|
this.threadTimings.end(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package mc.core.timings;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public enum TimingsEventType {
|
||||||
|
TIMINGS_START((short) 0),
|
||||||
|
TIMINGS_END((short) 1),
|
||||||
|
TIMINGS_FILE_INITIALIZING((short) 2),
|
||||||
|
TIMINGS_FILE_INITIALIZED((short) 3),
|
||||||
|
TIMINGS_CHANGE_THREAD_OPTIONS((short) 4),
|
||||||
|
TIMINGS_FILE_END((short) 5);
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final short id;
|
||||||
|
}
|
||||||
161
event-loop/src/main/java/mc/core/timings/TimingsManager.java
Normal file
161
event-loop/src/main/java/mc/core/timings/TimingsManager.java
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package mc.core.timings;
|
||||||
|
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.timings.io.DefaultWriterFactory;
|
||||||
|
import mc.core.timings.io.TimingsWriter;
|
||||||
|
import mc.core.timings.io.TimingsWriterFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class TimingsManager {
|
||||||
|
private final Map<Thread, ThreadTimings> threadTimings = new ConcurrentHashMap<>();
|
||||||
|
// These variables are essential in Timings thread synchronization
|
||||||
|
private final AtomicBoolean waitForFile = new AtomicBoolean(false);
|
||||||
|
private TimingsWriter writer;
|
||||||
|
private Thread timingsIoThread;
|
||||||
|
private CountDownLatch ioThreadStopMutex;
|
||||||
|
private BlockingQueue<TimingsRecord> queue;
|
||||||
|
private ReentrantLock queueAccessLock = new ReentrantLock();
|
||||||
|
// For modularity purposes
|
||||||
|
@Autowired
|
||||||
|
@Setter
|
||||||
|
private TimingsWriterFactory writerFactory = new DefaultWriterFactory();
|
||||||
|
|
||||||
|
public TimingsManager() {
|
||||||
|
TimingsStaticAccessor.TIMINGS_MANAGER = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startRecording(File file) {
|
||||||
|
synchronized (waitForFile) {
|
||||||
|
waitForFile.set(true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writer = writerFactory.newInstance(file);
|
||||||
|
writer.writeEvent(0, 0, System.nanoTime(), TimingsEventType.TIMINGS_FILE_INITIALIZING);
|
||||||
|
// Synchronize current thread state
|
||||||
|
for (Map.Entry<Thread, ThreadTimings> pair : threadTimings.entrySet()) {
|
||||||
|
writer.writeEvent(pair.getValue().getThreadId(), 0, System.nanoTime(), TimingsEventType.TIMINGS_CHANGE_THREAD_OPTIONS, "name: " + pair.getKey().getName());
|
||||||
|
for (Timings timings : pair.getValue().getStack()) {
|
||||||
|
writer.writeEvent(pair.getValue().getThreadId(), timings.getId(), timings.getAcquireTime(), TimingsEventType.TIMINGS_START);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.writeEvent(0, 0, System.nanoTime(), TimingsEventType.TIMINGS_FILE_INITIALIZED);
|
||||||
|
queue = new ArrayBlockingQueue<>(200);
|
||||||
|
ioThreadStopMutex = new CountDownLatch(1);
|
||||||
|
timingsIoThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
while (!isInterrupted() && isAlive()) {
|
||||||
|
TimingsRecord record;
|
||||||
|
try {
|
||||||
|
if (queue == null)
|
||||||
|
return;
|
||||||
|
record = queue.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
record.writeToFile(writer);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ioThreadStopMutex.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timingsIoThread.setName("Timings IO thread");
|
||||||
|
timingsIoThread.start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Unable to start timings recording", e);
|
||||||
|
}
|
||||||
|
synchronized (waitForFile) {
|
||||||
|
waitForFile.set(false);
|
||||||
|
waitForFile.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopRecording() {
|
||||||
|
// Disable write queue
|
||||||
|
queueAccessLock.lock();
|
||||||
|
queue = null;
|
||||||
|
queueAccessLock.unlock();
|
||||||
|
// Interrupt thread and wait until in finished writing the last task
|
||||||
|
timingsIoThread.interrupt();
|
||||||
|
try {
|
||||||
|
ioThreadStopMutex.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.error("Unable to wait until last record would be written to file", e);
|
||||||
|
}
|
||||||
|
// Write EOF event
|
||||||
|
writer.writeEvent(0, 0, System.nanoTime(), TimingsEventType.TIMINGS_FILE_END);
|
||||||
|
// Unload file
|
||||||
|
try {
|
||||||
|
writer.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Unable to close timings file", e);
|
||||||
|
}
|
||||||
|
writer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyTimings(ThreadTimings thread, Timings timings, boolean start) {
|
||||||
|
if (queue == null)
|
||||||
|
return;
|
||||||
|
queueAccessLock.lock();
|
||||||
|
try {
|
||||||
|
if (queue != null)
|
||||||
|
queue.offer(
|
||||||
|
new TimingsRecord(thread.getThreadId(),
|
||||||
|
timings.getId(),
|
||||||
|
start ? timings.getAcquireTime() : timings.getEndTime(),
|
||||||
|
start ? TimingsEventType.TIMINGS_START : TimingsEventType.TIMINGS_END
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
queueAccessLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void waitForTimingsInitialize() {
|
||||||
|
synchronized (waitForFile) {
|
||||||
|
while (waitForFile.get()) {
|
||||||
|
try {
|
||||||
|
waitForFile.wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThreadTimings getCurrentThreadTimings() {
|
||||||
|
|
||||||
|
synchronized (this.threadTimings) {
|
||||||
|
if (this.threadTimings.containsKey(Thread.currentThread())) {
|
||||||
|
return this.threadTimings.get(Thread.currentThread());
|
||||||
|
} else {
|
||||||
|
ThreadTimings timings = new ThreadTimings();
|
||||||
|
this.threadTimings.put(Thread.currentThread(), timings);
|
||||||
|
if (queue != null) {
|
||||||
|
try {
|
||||||
|
writer.writeEvent(timings.getThreadId(), 0, System.nanoTime(), TimingsEventType.TIMINGS_CHANGE_THREAD_OPTIONS, "name: " + Thread.currentThread().getName());
|
||||||
|
} catch (NullPointerException ignored) {
|
||||||
|
// It means that there the file recording was stopped
|
||||||
|
// we don't actually care about it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return timings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
event-loop/src/main/java/mc/core/timings/TimingsRecord.java
Normal file
33
event-loop/src/main/java/mc/core/timings/TimingsRecord.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package mc.core.timings;
|
||||||
|
|
||||||
|
import mc.core.timings.io.TimingsWriter;
|
||||||
|
|
||||||
|
class TimingsRecord {
|
||||||
|
private int threadId;
|
||||||
|
private int stackId;
|
||||||
|
private long time;
|
||||||
|
private TimingsEventType eventType;
|
||||||
|
private String data;
|
||||||
|
|
||||||
|
public TimingsRecord(int threadId, int stackId, long time, TimingsEventType eventType) {
|
||||||
|
this.threadId = threadId;
|
||||||
|
this.stackId = stackId;
|
||||||
|
this.time = time;
|
||||||
|
this.eventType = eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimingsRecord(int threadId, int stackId, long time, TimingsEventType eventType, String data) {
|
||||||
|
this.threadId = threadId;
|
||||||
|
this.stackId = stackId;
|
||||||
|
this.time = time;
|
||||||
|
this.eventType = eventType;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeToFile(TimingsWriter fileWriter) {
|
||||||
|
if (data == null)
|
||||||
|
fileWriter.writeEvent(threadId, stackId, time, eventType);
|
||||||
|
else
|
||||||
|
fileWriter.writeEvent(threadId, stackId, time, eventType, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package mc.core.timings;
|
||||||
|
|
||||||
|
public class TimingsStaticAccessor {
|
||||||
|
static TimingsManager TIMINGS_MANAGER;
|
||||||
|
|
||||||
|
public static TimingsManager getTimingsManager() {
|
||||||
|
return TIMINGS_MANAGER != null ? TIMINGS_MANAGER : (TIMINGS_MANAGER = new TimingsManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package mc.core.timings.io;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class DefaultWriterFactory implements TimingsWriterFactory {
|
||||||
|
@Override
|
||||||
|
public TimingsWriter newInstance(File file) throws IOException {
|
||||||
|
return new TimingsFileWriter(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package mc.core.timings.io;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.timings.TimingsEventType;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
|
@Slf4j
|
||||||
|
public class TimingsFileWriter implements TimingsWriter {
|
||||||
|
private FileOutputStream fileOutputStream;
|
||||||
|
private ObjectOutputStream writer;
|
||||||
|
private ReentrantLock lock = new ReentrantLock();
|
||||||
|
|
||||||
|
public TimingsFileWriter(File saveFile) throws IOException {
|
||||||
|
fileOutputStream = new FileOutputStream(saveFile);
|
||||||
|
writer = new ObjectOutputStream(fileOutputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeEvent(int threadId, int stackId, long time, TimingsEventType type) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
writer.writeInt(threadId);
|
||||||
|
writer.writeInt(stackId);
|
||||||
|
writer.writeLong(time);
|
||||||
|
writer.writeShort(type.getId());
|
||||||
|
writer.writeBoolean(false);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Unable to write timings record", e);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeEvent(int threadId, int stackId, long time, TimingsEventType type, String data) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
writer.writeInt(threadId);
|
||||||
|
writer.writeInt(stackId);
|
||||||
|
writer.writeLong(time);
|
||||||
|
writer.writeShort(type.getId());
|
||||||
|
writer.writeBoolean(true);
|
||||||
|
writer.writeUTF(data);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Unable to write timings record", e);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
writer.close();
|
||||||
|
fileOutputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package mc.core.timings.io;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.timings.TimingsEventType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class TimingsLogWriter implements TimingsWriter {
|
||||||
|
@Override
|
||||||
|
public void writeEvent(int threadId, int stackId, long time, TimingsEventType type) {
|
||||||
|
log.info("[{}] Thread #{}, Stack #{}: {}", time, threadId, stackId, type.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeEvent(int threadId, int stackId, long time, TimingsEventType type, String data) {
|
||||||
|
log.info("[{}] Thread #{}, Stack #{}: {} ({})", time, threadId, stackId, type.toString(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package mc.core.timings.io;
|
||||||
|
|
||||||
|
import mc.core.timings.TimingsEventType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface TimingsWriter {
|
||||||
|
void writeEvent(int threadId, int stackId, long time, TimingsEventType type);
|
||||||
|
|
||||||
|
void writeEvent(int threadId, int stackId, long time, TimingsEventType type, String data);
|
||||||
|
|
||||||
|
void close() throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package mc.core.timings.io;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface TimingsWriterFactory {
|
||||||
|
TimingsWriter newInstance(File file) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package mc.core.events;
|
||||||
|
|
||||||
|
import mc.core.events.runner.ResourceAwareExecutorService;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class EventExecutorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void basicTest() throws InterruptedException {
|
||||||
|
AtomicBoolean testVariable = new AtomicBoolean(false);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||||
|
service.start();
|
||||||
|
service.addTask(() -> {
|
||||||
|
testVariable.set(true);
|
||||||
|
latch.countDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
latch.await(1, TimeUnit.SECONDS);
|
||||||
|
service.stop();
|
||||||
|
Assert.assertTrue("Scheduled task was not executed", testVariable.get());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
169
event-loop/src/test/java/mc/core/events/EventLoopTest.java
Normal file
169
event-loop/src/test/java/mc/core/events/EventLoopTest.java
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package mc.core.events;
|
||||||
|
|
||||||
|
import mc.core.events.api.EventHandler;
|
||||||
|
import mc.core.events.api.EventPriority;
|
||||||
|
import mc.core.events.api.EventQueueOwner;
|
||||||
|
import mc.core.events.api.Plugin;
|
||||||
|
import mc.core.events.runner.ResourceAwareExecutorService;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
|
public class EventLoopTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void basicTest() throws InterruptedException {
|
||||||
|
Plugin plugin = new Plugin() {
|
||||||
|
};
|
||||||
|
|
||||||
|
EventQueueOwner queueOwner = new EventQueueOwner() {
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
FullAsyncEventLoop eventLoop = new FullAsyncEventLoop();
|
||||||
|
eventLoop.addEventHandler(plugin, new Object() {
|
||||||
|
@EventHandler
|
||||||
|
public void onLoginEvent(LoginEvent event) {
|
||||||
|
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||||
|
service.start();
|
||||||
|
|
||||||
|
eventLoop.setResourceAwareExecutorService(service);
|
||||||
|
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||||
|
|
||||||
|
latch.await(1, TimeUnit.SECONDS);
|
||||||
|
Assert.assertEquals("Event was not called", 0, latch.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void consecutiveExecutionTest() throws InterruptedException {
|
||||||
|
Plugin plugin = new Plugin() {
|
||||||
|
};
|
||||||
|
|
||||||
|
EventQueueOwner queueOwner = new EventQueueOwner() {
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(2);
|
||||||
|
FullAsyncEventLoop eventLoop = new FullAsyncEventLoop();
|
||||||
|
eventLoop.addEventHandler(plugin, new Object() {
|
||||||
|
@EventHandler
|
||||||
|
public void onLoginEvent(LoginEvent event) {
|
||||||
|
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||||
|
service.start();
|
||||||
|
|
||||||
|
eventLoop.setResourceAwareExecutorService(service);
|
||||||
|
|
||||||
|
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||||
|
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||||
|
|
||||||
|
latch.await(1, TimeUnit.SECONDS);
|
||||||
|
Assert.assertEquals("Event was not called", 0, latch.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prioritySystemTest() throws InterruptedException {
|
||||||
|
Plugin plugin = new Plugin() {
|
||||||
|
};
|
||||||
|
|
||||||
|
EventQueueOwner queueOwner = new EventQueueOwner() {
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(3);
|
||||||
|
FullAsyncEventLoop eventLoop = new FullAsyncEventLoop();
|
||||||
|
List<Integer> priorities = new ArrayList<>(3);
|
||||||
|
|
||||||
|
eventLoop.addEventHandler(plugin, new Object() {
|
||||||
|
@EventHandler(priority = EventPriority.NORMAL)
|
||||||
|
public void login1(LoginEvent event) {
|
||||||
|
priorities.add(0);
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void login2(LoginEvent event) {
|
||||||
|
priorities.add(1);
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOWEST)
|
||||||
|
public void login3(LoginEvent event) {
|
||||||
|
priorities.add(2);
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||||
|
service.start();
|
||||||
|
|
||||||
|
eventLoop.setResourceAwareExecutorService(service);
|
||||||
|
|
||||||
|
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||||
|
|
||||||
|
latch.await(1, TimeUnit.SECONDS);
|
||||||
|
Assert.assertEquals("Incorrect call sequence", "[2, 0, 1]", priorities.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ignoreCancelledTest() throws InterruptedException {
|
||||||
|
Plugin plugin = new Plugin() {
|
||||||
|
};
|
||||||
|
|
||||||
|
EventQueueOwner queueOwner = new EventQueueOwner() {
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
FullAsyncEventLoop eventLoop = new FullAsyncEventLoop();
|
||||||
|
List<Integer> priorities = new ArrayList<>(2);
|
||||||
|
|
||||||
|
eventLoop.addEventHandler(plugin, new Object() {
|
||||||
|
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||||
|
public void login1(LoginEvent event) {
|
||||||
|
priorities.add(0);
|
||||||
|
event.setCanceled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void login2(LoginEvent event) {
|
||||||
|
priorities.add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||||
|
public void login3(LoginEvent event) {
|
||||||
|
priorities.add(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void monitor(LoginEvent event) {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||||
|
service.start();
|
||||||
|
|
||||||
|
eventLoop.setResourceAwareExecutorService(service);
|
||||||
|
|
||||||
|
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||||
|
|
||||||
|
latch.await(1, TimeUnit.SECONDS);
|
||||||
|
Assert.assertEquals("Incorrect call sequence", "[2, 0]", priorities.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
90
event-loop/src/test/java/mc/core/events/LockTest.java
Normal file
90
event-loop/src/test/java/mc/core/events/LockTest.java
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package mc.core.events;
|
||||||
|
|
||||||
|
import mc.core.events.runner.lock.LockObserveList;
|
||||||
|
import mc.core.events.runner.lock.PoorMansLock;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class LockTest {
|
||||||
|
@Test
|
||||||
|
public void basicTest() throws InterruptedException {
|
||||||
|
AtomicBoolean engageCallbackCalled = new AtomicBoolean(false);
|
||||||
|
AtomicBoolean disengageCallbackCalled = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
PoorMansLock lock = new PoorMansLock();
|
||||||
|
lock.addCallback(lock1 -> {
|
||||||
|
if (lock1.isLocked())
|
||||||
|
engageCallbackCalled.set(true);
|
||||||
|
else
|
||||||
|
disengageCallbackCalled.set(true);
|
||||||
|
});
|
||||||
|
lock.lock();
|
||||||
|
Assert.assertTrue("Lock is not locked", lock.isLocked());
|
||||||
|
Assert.assertTrue("Engage callback was not called", engageCallbackCalled.get());
|
||||||
|
|
||||||
|
engageCallbackCalled.set(false);
|
||||||
|
try {
|
||||||
|
lock.lock();
|
||||||
|
Assert.assertFalse("Engage callback was called from attempt to block from the same thread", engageCallbackCalled.get());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Assert.fail("Exception fired while attempting to lock from the same thread");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.assertFalse("Disengage callback was called while not actually disengaging [x1]", disengageCallbackCalled.get());
|
||||||
|
|
||||||
|
AtomicBoolean lockExceptionFired = new AtomicBoolean(false);
|
||||||
|
AtomicBoolean unlockExceptionFired = new AtomicBoolean(false);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
lock.lock();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
lockExceptionFired.set(true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
lock.unlock();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
unlockExceptionFired.set(true);
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
latch.await();
|
||||||
|
Assert.assertTrue("Exception was not fired on concurrent lock attempt", lockExceptionFired.get());
|
||||||
|
Assert.assertTrue("Exception was not fired on non-owner unlock attempt", unlockExceptionFired.get());
|
||||||
|
Assert.assertFalse("Disengage callback was called while not actually disengaging [x2]", disengageCallbackCalled.get());
|
||||||
|
|
||||||
|
lock.unlock();
|
||||||
|
Assert.assertTrue("Disengage callback was on called on lock disengage", disengageCallbackCalled.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void observeListTest() {
|
||||||
|
PoorMansLock lock1 = new PoorMansLock();
|
||||||
|
PoorMansLock lock2 = new PoorMansLock();
|
||||||
|
|
||||||
|
LockObserveList list = new LockObserveList();
|
||||||
|
list.add(lock1);
|
||||||
|
list.add(lock2);
|
||||||
|
|
||||||
|
Assert.assertTrue("LockObserveList was no able to correctly identify lock states for unlocked locks", list.isReady());
|
||||||
|
lock1.lock();
|
||||||
|
Assert.assertFalse("LockObserveList was no able to correctly identify lock states for list with one locked lock", list.isReady());
|
||||||
|
|
||||||
|
|
||||||
|
AtomicBoolean listReadyCallbackCalled = new AtomicBoolean(false);
|
||||||
|
list.setCallback(() -> listReadyCallbackCalled.set(true));
|
||||||
|
lock2.lock();
|
||||||
|
|
||||||
|
Assert.assertFalse("Callback was called when another lock got engaged", listReadyCallbackCalled.get());
|
||||||
|
lock1.unlock();
|
||||||
|
Assert.assertFalse("Callback was called while one lock is still locked", listReadyCallbackCalled.get());
|
||||||
|
lock2.unlock();
|
||||||
|
Assert.assertTrue("Callback was not called when both locks are actually free", listReadyCallbackCalled.get());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
51
event-loop/src/test/java/mc/core/timings/TimingsTest.java
Normal file
51
event-loop/src/test/java/mc/core/timings/TimingsTest.java
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package mc.core.timings;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class TimingsTest {
|
||||||
|
@Test
|
||||||
|
public void basicTest() {
|
||||||
|
try (Timings timings = Timings.start()) {
|
||||||
|
System.out.println("Test code");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void brokenTimingTest() {
|
||||||
|
try (Timings timings = Timings.start()) {
|
||||||
|
Timings t1 = Timings.start();
|
||||||
|
Timings.start();
|
||||||
|
System.out.println("Pre Close t1");
|
||||||
|
t1.close();
|
||||||
|
System.out.println("Finished");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fileRecording() throws IOException {
|
||||||
|
Timings.getTimingsManager().startRecording(new File("test.timings"));
|
||||||
|
|
||||||
|
try (Timings t1 = Timings.start()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(20);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
try (Timings t2 = Timings.start()) {
|
||||||
|
Thread.sleep(10);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
Thread.sleep(5);
|
||||||
|
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Timings.getTimingsManager().stopRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,7 @@ import com.flowpowered.nbt.Tag;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import mc.core.EntityLocation;
|
import mc.core.EntityLocation;
|
||||||
import mc.core.world.IWorldType;
|
import mc.core.world.*;
|
||||||
import mc.core.world.Region;
|
|
||||||
import mc.core.world.World;
|
|
||||||
import mc.core.world.WorldType;
|
|
||||||
import mc.core.world.chunk.Chunk;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
@@ -29,7 +25,7 @@ public class FlatWorld implements World {
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private EntityLocation spawn = new EntityLocation(0d, 6d, 0d, 0f, 0f, this);
|
private EntityLocation spawn = new EntityLocation(0d, 6d, 0d, 0f, 0f, this);
|
||||||
private Chunk chunk = new SimpleChunk(0, 0, 0); //FIXME temporary dummy
|
private ChunkSection chunkSection = new SimpleChunkSection();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IWorldType getWorldType() {
|
public IWorldType getWorldType() {
|
||||||
@@ -37,12 +33,12 @@ public class FlatWorld implements World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Chunk getChunk(int x, int y, int z) {
|
public ChunkSection getChunk(int x, int y, int z) {
|
||||||
return chunk;
|
return chunkSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setChunk(int x, int y, int z, Chunk chunk) {
|
public void setChunk(int x, int y, int z, ChunkSection chunkSection) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,34 +5,14 @@
|
|||||||
package mc.world.flat;
|
package mc.world.flat;
|
||||||
|
|
||||||
import mc.core.world.Biome;
|
import mc.core.world.Biome;
|
||||||
|
import mc.core.world.ChunkSection;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
import mc.core.world.World;
|
||||||
import mc.core.world.block.Block;
|
import mc.core.world.block.Block;
|
||||||
import mc.core.world.block.BlockFactory;
|
import mc.core.world.block.BlockFactory;
|
||||||
import mc.core.world.chunk.Chunk;
|
import mc.core.world.block.BlockType;
|
||||||
|
|
||||||
import static mc.core.world.block.BlockType.*;
|
|
||||||
|
|
||||||
public class SimpleChunk implements Chunk {
|
|
||||||
private static BlockFactory blockFactory = new BlockFactory();
|
|
||||||
private final int x, y, z;
|
|
||||||
|
|
||||||
public SimpleChunk(int x, int y, int z) {
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Block getBlock(int x, int y, int z) {
|
|
||||||
if (y == 0) return blockFactory.create(BEDROCK);
|
|
||||||
else if (y >= 1 && y <= 2) return blockFactory.create(DIRT);
|
|
||||||
else if (y == 3) return blockFactory.create(GRASS);
|
|
||||||
else return blockFactory.create(AIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setBlock(Block block) {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public class SimpleChunkSection implements ChunkSection {
|
||||||
@Override
|
@Override
|
||||||
public int getSkyLight(int x, int y, int z) {
|
public int getSkyLight(int x, int y, int z) {
|
||||||
if (y <= 3) return 0;
|
if (y <= 3) return 0;
|
||||||
@@ -66,16 +46,41 @@ public class SimpleChunk implements Chunk {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getX() {
|
public int getX() {
|
||||||
return x;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getY() {
|
public int getY() {
|
||||||
return y;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getZ() {
|
public int getZ() {
|
||||||
return z;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlock(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 blockFactory.create(BlockType.AIR, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region getRegion() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public World getWorld() {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,77 +1,61 @@
|
|||||||
package mc.world.generated_world.chunk;
|
package mc.world.generated_world.chunk;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import mc.core.exception.ResourceUnloadedException;
|
||||||
import mc.core.world.block.Block;
|
import mc.core.world.ChunkSection;
|
||||||
import mc.core.world.block.BlockFactory;
|
|
||||||
import mc.core.world.block.BlockType;
|
|
||||||
import mc.core.world.Biome;
|
|
||||||
import mc.core.world.chunk.Chunk;
|
|
||||||
import mc.core.world.Region;
|
import mc.core.world.Region;
|
||||||
|
import mc.core.world.World;
|
||||||
|
import mc.core.world.chunk.Chunk;
|
||||||
|
|
||||||
|
import java.lang.ref.Reference;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
public class ChunkImpl implements Chunk {
|
||||||
public class ChunkImpl implements Chunk{
|
|
||||||
private static final BlockFactory blockFactory = new BlockFactory();
|
|
||||||
@Getter
|
@Getter
|
||||||
private final int x;
|
private final int x;
|
||||||
@Getter
|
@Getter
|
||||||
private final int y;
|
|
||||||
@Getter
|
|
||||||
private final int z;
|
private final int z;
|
||||||
private final Block[][][] blocks = new Block[WORLD_CHUNK_SIZE][WORLD_CHUNK_SIZE][WORLD_CHUNK_SIZE];
|
private Reference<Region> regionReference;
|
||||||
private final transient Region region;
|
private ChunkSection[] sections = new ChunkSection[WORLD_CHUNK_SIZE];
|
||||||
|
|
||||||
@Override
|
public ChunkImpl (int x, int z, Region region) {
|
||||||
public Block getBlock(int x, int y, int z) {
|
this.x = x;
|
||||||
Block block = blocks[x][y][z];
|
this.z = z;
|
||||||
if (block == null) {
|
this.regionReference = new WeakReference<>(region);
|
||||||
block = blockFactory.create(BlockType.AIR, 0, x, y, z);
|
|
||||||
}
|
|
||||||
block.setLight(15);
|
|
||||||
return block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBlock(Block block) {
|
public World getWorld() {
|
||||||
if (block.getBlockType() == BlockType.AIR) {
|
Region region = getRegion();
|
||||||
blocks[block.getLocation().getBlockX()]
|
if (region == null) {
|
||||||
[block.getLocation().getBlockY()]
|
throw new ResourceUnloadedException("Region is unloaded");
|
||||||
[block.getLocation().getBlockZ()] = null;
|
|
||||||
}
|
}
|
||||||
blocks[block.getLocation().getBlockX()]
|
return region.getWorld();
|
||||||
[block.getLocation().getBlockY()]
|
|
||||||
[block.getLocation().getBlockZ()] = block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSkyLight(int x, int y, int z) {
|
public ChunkSection getChunkSection(int height) {
|
||||||
return 15;
|
return sections[height];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setSkyLight(int x, int y, int z, int lightLevel) {
|
public ChunkSection setChunkSection(int height, ChunkSection chunkSection) {
|
||||||
throw new UnsupportedOperationException();
|
sections[height] = chunkSection;
|
||||||
|
return chunkSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAddition(int x, int y, int z) {
|
public Region getRegion() {
|
||||||
return 0;
|
if (regionReference == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
if (regionReference.get() == null) {
|
||||||
public void setAddition(int x, int y, int z, int value) {
|
throw new ResourceUnloadedException("Region is unloaded");
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
return regionReference.get();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package mc.world.generated_world.chunk;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import mc.core.exception.ResourceUnloadedException;
|
||||||
|
import mc.core.world.Biome;
|
||||||
|
import mc.core.world.ChunkSection;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
import mc.core.world.World;
|
||||||
|
import mc.core.world.block.Block;
|
||||||
|
import mc.core.world.block.BlockFactory;
|
||||||
|
import mc.core.world.block.BlockType;
|
||||||
|
|
||||||
|
import java.lang.ref.Reference;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
||||||
|
|
||||||
|
public class ChunkSectionImpl implements ChunkSection {
|
||||||
|
@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 Reference<Region> region;
|
||||||
|
private BlockFactory blockFactory = new BlockFactory();
|
||||||
|
|
||||||
|
public ChunkSectionImpl(int x, int y, int z, Region region) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.region = new WeakReference<>(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 getRegion().getBiomeAt(x + this.x * WORLD_CHUNK_SIZE,z + this.z * WORLD_CHUNK_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBiome(int x, int z, Biome biome) {
|
||||||
|
getRegion().setBiome(x + this.x * WORLD_CHUNK_SIZE,z + this.z * WORLD_CHUNK_SIZE, biome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlock(Block block) {
|
||||||
|
if (block.getBlockType() == BlockType.AIR) {
|
||||||
|
blocks[block.getLocation().getBlockX()][block.getLocation().getBlockY()][block.getLocation().getBlockZ()] = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
blocks[block.getLocation().getBlockX()][block.getLocation().getBlockY()][block.getLocation().getBlockZ()] = block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Block getBlock(int x, int y, int z) {
|
||||||
|
Block block = blocks[x][y][z];
|
||||||
|
if (block == null) {
|
||||||
|
return blockFactory.create(BlockType.AIR, 0, x, y, z);
|
||||||
|
}
|
||||||
|
return blocks[x][y][z];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region getRegion() {
|
||||||
|
if (region == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (region.get() == null) {
|
||||||
|
throw new ResourceUnloadedException("Region is unloaded");
|
||||||
|
}
|
||||||
|
return region.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public World getWorld() {
|
||||||
|
return getRegion().getWorld();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
package mc.world.generated_world.chunk;
|
package mc.world.generated_world.chunk;
|
||||||
|
|
||||||
|
import mc.core.world.ChunkSection;
|
||||||
|
import mc.core.world.Region;
|
||||||
|
import mc.core.world.World;
|
||||||
import mc.core.world.block.Block;
|
import mc.core.world.block.Block;
|
||||||
import mc.core.world.Biome;
|
import mc.core.world.Biome;
|
||||||
import mc.core.world.chunk.Chunk;
|
|
||||||
|
|
||||||
import java.util.concurrent.locks.ReadWriteLock;
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
public class ChunkProxy implements Chunk {
|
public class ChunkSectionProxy implements ChunkSection {
|
||||||
private final Chunk chunk;
|
private final ChunkSection chunk;
|
||||||
private volatile transient long lastUsage = System.currentTimeMillis();
|
private volatile transient long lastUsage = System.currentTimeMillis();
|
||||||
private final transient ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
|
private final transient ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
public ChunkProxy(Chunk chunk) {
|
public ChunkSectionProxy(ChunkSection chunk) {
|
||||||
this.chunk = chunk;
|
this.chunk = chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,16 @@ public class ChunkProxy implements Chunk {
|
|||||||
return chunk.getBlock(x, y, z);
|
return chunk.getBlock(x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region getRegion() {
|
||||||
|
return chunk.getRegion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public World getWorld() {
|
||||||
|
return chunk.getWorld();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBlock(Block block) {
|
public void setBlock(Block block) {
|
||||||
use();
|
use();
|
||||||
@@ -27,7 +27,7 @@ public class InMemoryCacheChunkLoader implements ChunkLoader {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ChunkReader chunkReader;
|
private ChunkReader chunkReader;
|
||||||
@Autowired
|
@Autowired
|
||||||
private Serializer<Chunk> chunkSerializer;
|
private Serializer<ChunkSection> chunkSerializer;
|
||||||
@Autowired
|
@Autowired
|
||||||
private RegionReaderWriter regionReaderWritter;
|
private RegionReaderWriter regionReaderWritter;
|
||||||
|
|
||||||
@@ -46,14 +46,14 @@ public class InMemoryCacheChunkLoader implements ChunkLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Chunk> loadChunk(int x, int y, int z) {
|
public Optional<ChunkSection> loadChunk(int x, int y, int z) {
|
||||||
File file = getChuckFile(x, y, z);
|
File file = getChuckFile(x, y, z);
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
Chunk chunk = chunkReader.read(world.getRegion(x / WORLD_CHUNK_SIZE, z / WORLD_CHUNK_SIZE), x, y, z);
|
ChunkSection chunkSection = chunkReader.read(world.getRegion(x / WORLD_CHUNK_SIZE, z / WORLD_CHUNK_SIZE), x, y, z);
|
||||||
return Optional.of(chunk);
|
return Optional.of(chunkSection);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error occurred while reading chunk file: " + file.getAbsolutePath(), e);
|
log.error("Error occurred while reading chunk file: " + file.getAbsolutePath(), e);
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -62,12 +62,12 @@ public class InMemoryCacheChunkLoader implements ChunkLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Chunk loadOrGenerateChunk(int x, int y, int z) {
|
public ChunkSection loadOrGenerateChunk(int x, int y, int z) {
|
||||||
int regX = x / WORLD_CHUNK_SIZE;
|
int regX = x / WORLD_CHUNK_SIZE;
|
||||||
int regZ = z / WORLD_CHUNK_SIZE;
|
int regZ = z / WORLD_CHUNK_SIZE;
|
||||||
File regionFile = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, regX, regZ));
|
File regionFile = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, regX, regZ));
|
||||||
Region region;
|
Region region;
|
||||||
Chunk chunk;
|
ChunkSection chunkSection;
|
||||||
if (!regionFile.exists()) {
|
if (!regionFile.exists()) {
|
||||||
log.debug("Region [{}, {}] not found. Generating!", regX, regZ);
|
log.debug("Region [{}, {}] not found. Generating!", regX, regZ);
|
||||||
regionFile.mkdirs();
|
regionFile.mkdirs();
|
||||||
@@ -78,17 +78,17 @@ public class InMemoryCacheChunkLoader implements ChunkLoader {
|
|||||||
log.error("Error occurred while writting biome file", e);
|
log.error("Error occurred while writting biome file", e);
|
||||||
}
|
}
|
||||||
saveRegion(region);
|
saveRegion(region);
|
||||||
chunk = region.getChunkAt(x % WORLD_CHUNK_SIZE, y % WORLD_CHUNK_SIZE, z % WORLD_CHUNK_SIZE);
|
chunkSection = region.getChunkAt(x % WORLD_CHUNK_SIZE, y % WORLD_CHUNK_SIZE, z % WORLD_CHUNK_SIZE);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
region = regionReaderWritter.read(regX, regZ, world);
|
region = regionReaderWritter.read(regX, regZ, world);
|
||||||
chunk = chunkReader.read(region, x, y, z);
|
chunkSection = chunkReader.read(region, x, y, z);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error occurred while reading chunk file", e);
|
log.error("Error occurred while reading chunkSection file", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return chunk;
|
return chunkSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveRegion (Region region) {
|
private void saveRegion (Region region) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import mc.core.world.block.BlockFactory;
|
import mc.core.world.block.BlockFactory;
|
||||||
import mc.core.world.block.BlockType;
|
import mc.core.world.block.BlockType;
|
||||||
import mc.core.world.*;
|
import mc.core.world.*;
|
||||||
import mc.core.world.chunk.Chunk;
|
|
||||||
import mc.world.generated_world.region.RegionImpl;
|
import mc.world.generated_world.region.RegionImpl;
|
||||||
import mc.world.generated_world.world.CubicWorld;
|
import mc.world.generated_world.world.CubicWorld;
|
||||||
import mc.world.generated_world.world.Temperature;
|
import mc.world.generated_world.world.Temperature;
|
||||||
@@ -226,7 +225,7 @@ public class SeedBasedWorldGenerator implements WorldGenerator {
|
|||||||
region.setBiome(x, z, biomes[x][z]);
|
region.setBiome(x, z, biomes[x][z]);
|
||||||
if (heightMap[x][z] < WORLD_SEA_LEVEL) {
|
if (heightMap[x][z] < WORLD_SEA_LEVEL) {
|
||||||
for (int y = 0; y < WORLD_SEA_LEVEL; y ++) {
|
for (int y = 0; y < WORLD_SEA_LEVEL; y ++) {
|
||||||
Chunk chunk = region.getChunkAt(x / 16, y / 16, z / 16);
|
ChunkSection chunk = region.getChunkAt(x / 16, y / 16, z / 16);
|
||||||
if (y == 0) {
|
if (y == 0) {
|
||||||
chunk.setBlock(blockFactory.create(BlockType.BEDROCK, 0, x % 16, y % 16, z % 16));
|
chunk.setBlock(blockFactory.create(BlockType.BEDROCK, 0, x % 16, y % 16, z % 16));
|
||||||
continue;
|
continue;
|
||||||
@@ -243,7 +242,7 @@ public class SeedBasedWorldGenerator implements WorldGenerator {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (int y = 0; y < heightMap[x][z]; y++) {
|
for (int y = 0; y < heightMap[x][z]; y++) {
|
||||||
Chunk chunk = region.getChunkAt(x / 16, y / 16, z / 16);
|
ChunkSection chunk = region.getChunkAt(x / 16, y / 16, z / 16);
|
||||||
if (y == 0) {
|
if (y == 0) {
|
||||||
chunk.setBlock(blockFactory.create(BlockType.BEDROCK, 0, x % 16, y % 16, z % 16));
|
chunk.setBlock(blockFactory.create(BlockType.BEDROCK, 0, x % 16, y % 16, z % 16));
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,61 +1,90 @@
|
|||||||
package mc.world.generated_world.region;
|
package mc.world.generated_world.region;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.exception.ResourceUnloadedException;
|
||||||
import mc.core.serialization.IRegionReaderWriter;
|
import mc.core.serialization.IRegionReaderWriter;
|
||||||
import mc.core.serialization.Serializer;
|
import mc.core.serialization.Serializer;
|
||||||
import mc.core.world.*;
|
import mc.core.world.*;
|
||||||
import mc.core.world.chunk.Chunk;
|
import mc.core.world.chunk.Chunk;
|
||||||
import mc.core.world.chunk.ChunkLoader;
|
import mc.core.world.chunk.ChunkLoader;
|
||||||
|
import mc.world.generated_world.chunk.ChunkSectionProxy;
|
||||||
import mc.world.generated_world.chunk.InMemoryCacheChunkLoader;
|
import mc.world.generated_world.chunk.InMemoryCacheChunkLoader;
|
||||||
import mc.world.generated_world.chunk.ChunkImpl;
|
import mc.world.generated_world.chunk.ChunkImpl;
|
||||||
import mc.world.generated_world.chunk.ChunkProxy;
|
import mc.world.generated_world.chunk.ChunkSectionImpl;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.ref.Reference;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
import static mc.world.generated_world.WorldConstants.*;
|
import static mc.world.generated_world.WorldConstants.*;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class RegionImpl implements Region{
|
public class RegionImpl implements Region{
|
||||||
@Getter
|
@Getter
|
||||||
private final int x;
|
private final int x;
|
||||||
@Getter
|
@Getter
|
||||||
private final int z;
|
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 ChunkSection[][][] chunkSectionProxies = new ChunkSectionProxy[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];
|
private final Biome[][] biomes = new Biome[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||||
@Getter
|
private final transient Reference<World> world;
|
||||||
private final transient World world;
|
private final Chunk[][] chunks = new Chunk[WORLD_REGION_SIZE/WORLD_CHUNK_SIZE][WORLD_REGION_SIZE/WORLD_CHUNK_SIZE];
|
||||||
@Autowired
|
@Autowired
|
||||||
private ChunkLoader chunkLoader;
|
private ChunkLoader chunkLoader;
|
||||||
|
|
||||||
|
public RegionImpl (int x, int z, World world) {
|
||||||
|
this.x = x;
|
||||||
|
this.z = z;
|
||||||
|
this.world = new WeakReference<>(world);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Chunk getChunkAt(int x, int y, int z) {
|
public Chunk getChunk(int x, int z) {
|
||||||
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= 16 || z >= 16) {
|
if (x < 0 || z < 0 || x >= 16 || z >= 16) {
|
||||||
throw new RuntimeException(MessageFormat.format("Invalid chunk coordinates [{0} {1} {2}]", x, y, z));
|
throw new RuntimeException(MessageFormat.format("Invalid chunk coordinates [{0} {1}]", x, z));
|
||||||
}
|
}
|
||||||
if (chunkLoader == null) {
|
|
||||||
chunkLoader = new InMemoryCacheChunkLoader(world);
|
Chunk chunk = chunks[x][z];
|
||||||
}
|
|
||||||
Chunk chunk = chunks[x][y][z];
|
|
||||||
if (chunk == null) {
|
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));
|
chunk = new ChunkImpl(x, z, this);
|
||||||
chunks[x][y][z] = new ChunkProxy(chunk);
|
for (int y = 0; y < WORLD_CHUNK_SIZE; y ++) {
|
||||||
|
chunk.setChunkSection(y, getChunkAt(x, y, z));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setChunk(int x, int y, int z, Chunk chunk) {
|
public void setChunk(int x, int z, Chunk chunk) {
|
||||||
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= 16 || z >= 16) {
|
chunks[x][z] = chunk;
|
||||||
throw new RuntimeException(MessageFormat.format("Invalid chunk coordinates [{0} {1} {2}]", x, y, z));
|
|
||||||
}
|
}
|
||||||
chunks[x][y][z] = new ChunkProxy(chunk);
|
|
||||||
|
@Override
|
||||||
|
public ChunkSection 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 chunkSection coordinates [{0} {1} {2}]", x, y, z));
|
||||||
|
}
|
||||||
|
if (chunkLoader == null) {
|
||||||
|
chunkLoader = new InMemoryCacheChunkLoader(getWorld());
|
||||||
|
}
|
||||||
|
ChunkSection chunkSection = chunkSectionProxies[x][y][z];
|
||||||
|
if (chunkSection == null) {
|
||||||
|
chunkSection = chunkLoader.loadChunk(x + this.x * WORLD_REGION_SIZE, y, this.z * WORLD_REGION_SIZE).orElse(new ChunkSectionImpl(x, y, z, this));
|
||||||
|
chunkSectionProxies[x][y][z] = new ChunkSectionProxy(chunkSection);
|
||||||
|
}
|
||||||
|
return chunkSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setChunk(int x, int y, int z, ChunkSection chunkSection) {
|
||||||
|
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= 16 || z >= 16) {
|
||||||
|
throw new RuntimeException(MessageFormat.format("Invalid chunkSection coordinates [{0} {1} {2}]", x, y, z));
|
||||||
|
}
|
||||||
|
chunkSectionProxies[x][y][z] = new ChunkSectionProxy(chunkSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -75,9 +104,20 @@ public class RegionImpl implements Region{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(Serializer<Chunk> chunkSerializer, IRegionReaderWriter regionReaderWriter) throws IOException {
|
public World getWorld() {
|
||||||
|
if (world == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (world.get() == null) {
|
||||||
|
throw new ResourceUnloadedException("World is unloaded");
|
||||||
|
}
|
||||||
|
return world.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(Serializer<ChunkSection> chunkSerializer, IRegionReaderWriter regionReaderWriter) throws IOException {
|
||||||
String worldPath = System.getProperty("worlds.folder", "worlds");
|
String worldPath = System.getProperty("worlds.folder", "worlds");
|
||||||
File worldFile = new File(worldPath, world.getWorldId().toString());
|
File worldFile = new File(worldPath, getWorld().getWorldId().toString());
|
||||||
File regionFile = new File(worldFile, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, this.getX(), this.getZ()));
|
File regionFile = new File(worldFile, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, this.getX(), this.getZ()));
|
||||||
if (!regionFile.exists()) {
|
if (!regionFile.exists()) {
|
||||||
regionFile.mkdirs();
|
regionFile.mkdirs();
|
||||||
@@ -86,8 +126,8 @@ public class RegionImpl implements Region{
|
|||||||
for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) {
|
for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) {
|
||||||
for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) {
|
for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) {
|
||||||
for (int y = 0; y < WORLD_CHUNK_SIZE; y++) {
|
for (int y = 0; y < WORLD_CHUNK_SIZE; y++) {
|
||||||
Chunk chunk = this.getChunkAt(x, y, z);
|
ChunkSection chunkSection = this.getChunkAt(x, y, z);
|
||||||
byte[] chunkBytes = chunkSerializer.serialize(chunk);
|
byte[] chunkBytes = chunkSerializer.serialize(chunkSection);
|
||||||
if (chunkBytes.length > 0) {
|
if (chunkBytes.length > 0) {
|
||||||
File chunkFile = new File(regionFile, MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z));
|
File chunkFile = new File(regionFile, MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z));
|
||||||
try (FileOutputStream fileOutputStream = new FileOutputStream(chunkFile)) {
|
try (FileOutputStream fileOutputStream = new FileOutputStream(chunkFile)) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import mc.core.world.block.BlockFactory;
|
|||||||
import mc.core.world.block.BlockType;
|
import mc.core.world.block.BlockType;
|
||||||
import mc.core.serialization.Deserializer;
|
import mc.core.serialization.Deserializer;
|
||||||
import mc.core.serialization.Serializer;
|
import mc.core.serialization.Serializer;
|
||||||
import mc.core.world.chunk.Chunk;
|
import mc.core.world.ChunkSection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prototype
|
* Prototype
|
||||||
@@ -13,20 +13,20 @@ import mc.core.world.chunk.Chunk;
|
|||||||
public class BlockSerializerDeserializer implements Serializer<Block>, Deserializer<Block> {
|
public class BlockSerializerDeserializer implements Serializer<Block>, Deserializer<Block> {
|
||||||
|
|
||||||
private BlockFactory blockFactory;
|
private BlockFactory blockFactory;
|
||||||
private Chunk chunk;
|
private ChunkSection chunkSection;
|
||||||
|
|
||||||
public BlockSerializerDeserializer(BlockFactory blockFactory, Chunk chunk) {
|
public BlockSerializerDeserializer(BlockFactory blockFactory, ChunkSection chunkSection) {
|
||||||
this.blockFactory = blockFactory;
|
this.blockFactory = blockFactory;
|
||||||
this.chunk = chunk;
|
this.chunkSection = chunkSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Block deserialize(byte[] bytes) {
|
public Block deserialize(byte[] bytes) {
|
||||||
int id = bytes[0] + 128;
|
int id = bytes[0] + 128;
|
||||||
int meta = bytes[1] >> 4;
|
int meta = bytes[1] >> 4;
|
||||||
int x = (bytes[1] & 0xf) + chunk.getX() * 16;
|
int x = (bytes[1] & 0xf) + chunkSection.getX() * 16;
|
||||||
int y = bytes[2] >> 4 + chunk.getY() * 16;
|
int y = bytes[2] >> 4 + chunkSection.getY() * 16;
|
||||||
int z = (bytes[2] & 0xf) + chunk.getZ() * 16;
|
int z = (bytes[2] & 0xf) + chunkSection.getZ() * 16;
|
||||||
BlockType type = BlockType.values()[id];
|
BlockType type = BlockType.values()[id];
|
||||||
Block block = blockFactory.create(type, meta);
|
Block block = blockFactory.create(type, meta);
|
||||||
block.getLocation().setX(x);
|
block.getLocation().setX(x);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package mc.world.generated_world.serialization;
|
package mc.world.generated_world.serialization;
|
||||||
|
|
||||||
|
import mc.core.Location;
|
||||||
import mc.core.world.block.Block;
|
import mc.core.world.block.Block;
|
||||||
import mc.core.serialization.Deserializer;
|
import mc.core.serialization.Deserializer;
|
||||||
import mc.core.serialization.IChunkReader;
|
import mc.core.serialization.IChunkReader;
|
||||||
import mc.core.world.chunk.Chunk;
|
import mc.core.world.ChunkSection;
|
||||||
import mc.core.world.Region;
|
import mc.core.world.Region;
|
||||||
import mc.world.generated_world.chunk.ChunkImpl;
|
import mc.world.generated_world.chunk.ChunkSectionImpl;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -26,22 +27,22 @@ public class ChunkReader implements IChunkReader{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Chunk read (Region region, int x, int y, int z) throws IOException {
|
public ChunkSection read (Region region, int x, int y, int z) throws IOException {
|
||||||
x %= WORLD_REGION_SIZE;
|
x %= WORLD_REGION_SIZE;
|
||||||
y %= WORLD_REGION_SIZE;
|
y %= WORLD_REGION_SIZE;
|
||||||
z %= 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));
|
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()));
|
byte[] chunkBytes = Files.readAllBytes(Paths.get(chunkFile.toURI()));
|
||||||
int blocks = (chunkBytes.length) / 3;
|
int blocks = (chunkBytes.length) / 3;
|
||||||
Chunk chunk = new ChunkImpl(x, y, z, region);
|
ChunkSection chunkSection = new ChunkSectionImpl(x, y, z, region);
|
||||||
for (int i = 0; i < blocks; i ++) {
|
for (int i = 0; i < blocks; i ++) {
|
||||||
byte[] blockBytes = new byte[3];
|
byte[] blockBytes = new byte[3];
|
||||||
blockBytes[0] = chunkBytes[3 * i];
|
blockBytes[0] = chunkBytes[3 * i];
|
||||||
blockBytes[1] = chunkBytes[1 + 3 * i];
|
blockBytes[1] = chunkBytes[1 + 3 * i];
|
||||||
blockBytes[2] = chunkBytes[2 + 3 * i];
|
blockBytes[2] = chunkBytes[2 + 3 * i];
|
||||||
Block block = blockDeserializer.deserialize(blockBytes);
|
Block block = blockDeserializer.deserialize(blockBytes);
|
||||||
chunk.setBlock(block);
|
chunkSection.setBlock(block);
|
||||||
}
|
}
|
||||||
return chunk;
|
return chunkSection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import mc.core.world.block.Block;
|
|||||||
import mc.core.world.block.BlockFactory;
|
import mc.core.world.block.BlockFactory;
|
||||||
import mc.core.world.block.BlockType;
|
import mc.core.world.block.BlockType;
|
||||||
import mc.core.serialization.Serializer;
|
import mc.core.serialization.Serializer;
|
||||||
import mc.core.world.chunk.Chunk;
|
import mc.core.world.ChunkSection;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@@ -14,20 +14,20 @@ import java.io.IOException;
|
|||||||
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ChunkSerializer implements Serializer<Chunk> {
|
public class ChunkSerializer implements Serializer<ChunkSection> {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Serializer<Block> blockSerializer;
|
private Serializer<Block> blockSerializer;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] serialize(Chunk chunk) {
|
public byte[] serialize(ChunkSection chunkSection) {
|
||||||
Serializer<Block> blockSerializer = new BlockSerializerDeserializer(new BlockFactory(), chunk);
|
Serializer<Block> blockSerializer = new BlockSerializerDeserializer(new BlockFactory(), chunkSection);
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
Block current;
|
Block current;
|
||||||
for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) {
|
for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) {
|
||||||
for (int y = 0; y < WORLD_CHUNK_SIZE; y ++) {
|
for (int y = 0; y < WORLD_CHUNK_SIZE; y ++) {
|
||||||
for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) {
|
for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) {
|
||||||
current = chunk.getBlock(x, y, z);
|
current = chunkSection.getBlock(x, y, z);
|
||||||
if (current != null && current.getBlockType() != BlockType.AIR) {
|
if (current != null && current.getBlockType() != BlockType.AIR) {
|
||||||
try {
|
try {
|
||||||
baos.write(blockSerializer.serialize(current));
|
baos.write(blockSerializer.serialize(current));
|
||||||
|
|||||||
@@ -4,32 +4,58 @@ import com.flowpowered.nbt.Tag;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import mc.core.Direction;
|
||||||
import mc.core.EntityLocation;
|
import mc.core.EntityLocation;
|
||||||
import mc.core.world.IWorldType;
|
import mc.core.world.*;
|
||||||
import mc.core.world.Region;
|
import mc.world.generated_world.serialization.RegionReaderWriter;
|
||||||
import mc.core.world.World;
|
|
||||||
import mc.core.world.block.BlockType;
|
|
||||||
import mc.core.world.chunk.Chunk;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
import static mc.world.generated_world.WorldConstants.REGION_FILE_NAME_TEMPLATE;
|
||||||
import static mc.world.generated_world.WorldConstants.WORLD_MAX_HEIGHT;
|
|
||||||
|
/*
|
||||||
|
* NORTH
|
||||||
|
*
|
||||||
|
* EAST WEST
|
||||||
|
*
|
||||||
|
* SOUTH
|
||||||
|
*
|
||||||
|
* + ----> X
|
||||||
|
* |
|
||||||
|
* |
|
||||||
|
* |
|
||||||
|
* V Z
|
||||||
|
*/
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CubicWorld implements World {
|
public class CubicWorld implements 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;
|
||||||
@Getter
|
@Getter
|
||||||
private final UUID worldId;
|
private final UUID worldId;
|
||||||
private final int seed;
|
private final int seed;
|
||||||
private volatile EntityLocation warpPosition;
|
private volatile EntityLocation spawn;
|
||||||
private final transient Object spawnLocationLock = new Object();
|
private final transient Object spawnLocationLock = new Object();
|
||||||
private final Map<String, Tag<?>> nbtTagMap = new HashMap<>();
|
private final Map<String, Tag<?>> nbtTagMap = new HashMap<>();
|
||||||
@Autowired
|
|
||||||
private RegionManager regionManager;
|
|
||||||
@Getter@Setter
|
@Getter@Setter
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@@ -55,55 +81,76 @@ public class CubicWorld implements World {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IWorldType getWorldType() {
|
public IWorldType getWorldType() {
|
||||||
return null;
|
return null; //FIXME
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EntityLocation getSpawn() {
|
public EntityLocation getSpawn() {
|
||||||
if (warpPosition == null) {
|
/* FIXME */
|
||||||
|
if (spawn == null) {
|
||||||
|
log.warn("Spawn is not defined! Set default spawn: [8, 128, 8]");
|
||||||
|
setSpawn(new EntityLocation(8d, 128d, 8d, 0f, 0f, this));
|
||||||
|
}
|
||||||
|
return spawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSpawn(EntityLocation entityLocation) {
|
||||||
synchronized (spawnLocationLock) {
|
synchronized (spawnLocationLock) {
|
||||||
if (warpPosition == null) {
|
entityLocation.setWorld(this);
|
||||||
log.warn("Spawn location is not defined. Trying to select best location");
|
this.spawn = entityLocation;
|
||||||
warpPosition = new EntityLocation(0d, 10d, 0d, 0f, 0f, this);
|
|
||||||
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 EntityLocation(0d, y + 1d, 0d, 0f, 0f, this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
warpPosition = new EntityLocation(0d, 10d, 0d, 0f, 0f, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return warpPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSpawn(EntityLocation warpPosition) {
|
|
||||||
synchronized (spawnLocationLock) {
|
|
||||||
this.warpPosition = warpPosition;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Chunk getChunk(int x, int y, int z) {
|
public ChunkSection getChunk(int x, int y, int z) {
|
||||||
return null;
|
Region region = getRegion(x / 16, z / 16);
|
||||||
|
return region.getChunkAt(x % 16, y % 16, z % 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setChunk(int x, int y, int z, Chunk chunk) {
|
public void setChunk(int x, int y, int z, ChunkSection chunkSection) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Region getRegion(int x, int z) {
|
public Region getRegion(int x, int z) {
|
||||||
return null;
|
checkCoordsInCache(x, z);
|
||||||
|
Region region;
|
||||||
|
if (regions[x - pointX][z - pointZ] == null) {
|
||||||
|
File file = new File(new File("worlds", this.getWorldId().toString()), MessageFormat.format(REGION_FILE_NAME_TEMPLATE, x, z));
|
||||||
|
if (!file.exists()) {
|
||||||
|
region = worldGenerator.generateRegion(x, z, this);
|
||||||
|
if (autoSaveRegionAfterGenerating) {
|
||||||
|
try {
|
||||||
|
regionReaderWriter.write(region);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while saving region data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
region = regionReaderWriter.read(x, z, this);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error occurred while loading region");
|
||||||
|
region = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRegion(region.getX(), region.getZ(), region);
|
||||||
|
} else {
|
||||||
|
region = regions[x - pointX][z - pointZ];
|
||||||
|
}
|
||||||
|
return region;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setRegion(int x, int z, Region region) {
|
public void setRegion(int x, int z, Region region) {
|
||||||
|
try {
|
||||||
|
regionSaveLock.lock();
|
||||||
|
regions[x - pointX][z - pointZ] = region;
|
||||||
|
} finally {
|
||||||
|
regionSaveLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -125,4 +172,53 @@ public class CubicWorld implements World {
|
|||||||
public Stream<Tag<?>> tagStream() {
|
public Stream<Tag<?>> tagStream() {
|
||||||
return nbtTagMap.values().stream();
|
return nbtTagMap.values().stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
regionSaveLock.lock();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package mc.world.generated_world;
|
package mc.world.generated_world;
|
||||||
|
|
||||||
import mc.world.generated_world.generator.SeedRandomGenerator;
|
import mc.world.generated_world.generator.SeedRandomGenerator;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@@ -9,6 +10,7 @@ import java.io.File;
|
|||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
@Ignore
|
||||||
public class SeedRandomGeneratorTest {
|
public class SeedRandomGeneratorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import mc.core.network.NetOutputStream;
|
import mc.core.network.NetOutputStream;
|
||||||
import mc.core.network.SCPacket;
|
import mc.core.network.SCPacket;
|
||||||
import mc.core.network.proto_1_12_2.ByteArrayOutputNetStream;
|
import mc.core.network.proto_1_12_2.ByteArrayOutputNetStream;
|
||||||
|
import mc.core.world.ChunkSection;
|
||||||
import mc.core.world.block.Block;
|
import mc.core.world.block.Block;
|
||||||
import mc.core.world.chunk.Chunk;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -76,7 +76,7 @@ public class ChunkDataPacket implements SCPacket {
|
|||||||
@Setter
|
@Setter
|
||||||
private boolean initChunk = true; // "Ground-Up Continuous"
|
private boolean initChunk = true; // "Ground-Up Continuous"
|
||||||
@Getter
|
@Getter
|
||||||
private List<Chunk> chunks = new ArrayList<>();
|
private List<ChunkSection> chunks = new ArrayList<>();
|
||||||
|
|
||||||
private int serializeBlockState(int id, int meta) {
|
private int serializeBlockState(int id, int meta) {
|
||||||
return (id << 4) | meta;
|
return (id << 4) | meta;
|
||||||
@@ -93,7 +93,7 @@ public class ChunkDataPacket implements SCPacket {
|
|||||||
int dataItems = 0;
|
int dataItems = 0;
|
||||||
final int airBlockPalette = serializeBlockState(0, 0);
|
final int airBlockPalette = serializeBlockState(0, 0);
|
||||||
|
|
||||||
for (Chunk chunk : chunks) {
|
for (ChunkSection chunk : chunks) {
|
||||||
final List<Integer> palette = new ArrayList<>();
|
final List<Integer> palette = new ArrayList<>();
|
||||||
palette.add(airBlockPalette);
|
palette.add(airBlockPalette);
|
||||||
final ByteArrayOutputNetStream dataArray = new ByteArrayOutputNetStream();
|
final ByteArrayOutputNetStream dataArray = new ByteArrayOutputNetStream();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import mc.core.Location;
|
import mc.core.EntityLocation;
|
||||||
import mc.core.network.NetOutputStream;
|
import mc.core.network.NetOutputStream;
|
||||||
import mc.core.network.SCPacket;
|
import mc.core.network.SCPacket;
|
||||||
|
|
||||||
@@ -17,17 +17,17 @@ import mc.core.network.SCPacket;
|
|||||||
@Setter
|
@Setter
|
||||||
@ToString
|
@ToString
|
||||||
public class SpawnPositionPacket implements SCPacket {
|
public class SpawnPositionPacket implements SCPacket {
|
||||||
private Location location;
|
private EntityLocation location;
|
||||||
|
|
||||||
private int floor_double(double value) {
|
private int floor_double(double value) {
|
||||||
int i = (int)value;
|
int i = (int)value;
|
||||||
return value < (double)i ? i - 1 : i;
|
return value < (double)i ? i - 1 : i;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long location2long(Location location) {
|
private long location2long(EntityLocation entityLocation) {
|
||||||
return ((floor_double(location.getX()) & 0x3FFFFFF) << 38)
|
return ((floor_double(entityLocation.getX()) & 0x3FFFFFF) << 38)
|
||||||
| ((floor_double(location.getY()) & 0xFFF) << 26)
|
| ((floor_double(entityLocation.getY()) & 0xFFF) << 26)
|
||||||
| (floor_double(location.getZ()) & 0x3FFFFFF);
|
| (floor_double(entityLocation.getZ()) & 0x3FFFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public class LoginHandler extends AbstractStateHandler implements LoginStateHand
|
|||||||
pkt1.setMode(PlayerMode.CREATIVE);
|
pkt1.setMode(PlayerMode.CREATIVE);
|
||||||
pkt1.setDimension(0/*Overworld*/);
|
pkt1.setDimension(0/*Overworld*/);
|
||||||
pkt1.setDifficulty(0/*Peaceful*/);
|
pkt1.setDifficulty(0/*Peaceful*/);
|
||||||
pkt1.setLevelType("flat");
|
pkt1.setLevelType("flat"); //FIXME
|
||||||
channel.write(pkt1);
|
channel.write(pkt1);
|
||||||
|
|
||||||
// Spawn Position
|
// Spawn Position
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ 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')
|
include('generated_world')
|
||||||
|
include('event-loop')
|
||||||
Reference in New Issue
Block a user