diff --git a/build.gradle b/build.gradle index 74a7f0e..a657d38 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -subprojects { +allprojects { apply plugin: 'java' compileJava { @@ -10,7 +10,9 @@ subprojects { repositories { mavenCentral() } +} +subprojects { ext { slf4j_version = '1.7.21' spring_version = '4.2.5.RELEASE' @@ -33,6 +35,9 @@ subprojects { /* Components */ compile (group: 'org.projectlombok', name: 'lombok', version: '1.16.16') + + /* Test */ + testCompile (group: 'junit', name: 'junit', version: '4.12') } task copyDep(type: Copy) { @@ -44,3 +49,23 @@ subprojects { 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 +} diff --git a/core/src/main/java/mc/core/Location.java b/core/src/main/java/mc/core/Location.java index 09e6c19..7da4513 100644 --- a/core/src/main/java/mc/core/Location.java +++ b/core/src/main/java/mc/core/Location.java @@ -6,18 +6,21 @@ package mc.core; import lombok.Getter; import lombok.Setter; +import mc.core.exception.ResourceUnloadedException; import mc.core.world.World; +import mc.core.world.chunk.Chunk; import java.io.Serializable; +import java.lang.ref.Reference; import java.lang.ref.WeakReference; public class Location implements Serializable, Cloneable { @Getter @Setter private double x, y, z; - private WeakReference refWorld; + private Reference 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); setWorld(world); } @@ -33,10 +36,16 @@ public class Location implements Serializable, Cloneable { } public World getWorld() { - return refWorld.get(); + if (refWorld == null) { + return null; + } else if (refWorld.get() == null) { + throw new ResourceUnloadedException("You're trying to get unloaded world"); + } else { + return refWorld.get(); + } } - public void setWorld(World world) { + public void setWorld (World world) { this.refWorld = new WeakReference<>(world); } @@ -52,11 +61,22 @@ public class Location implements Serializable, Cloneable { 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 public Location clone() { try { return (Location) super.clone(); } catch (CloneNotSupportedException e) { // такое в нашем случае вообще возможно? + e.printStackTrace(); return null; } } diff --git a/core/src/main/java/mc/core/exception/McCoreUncheckedException.java b/core/src/main/java/mc/core/exception/McCoreUncheckedException.java new file mode 100644 index 0000000..15313f3 --- /dev/null +++ b/core/src/main/java/mc/core/exception/McCoreUncheckedException.java @@ -0,0 +1,12 @@ +package mc.core.exception; + +public abstract class McCoreUncheckedException extends RuntimeException { + + public McCoreUncheckedException() { + super(); + } + + public McCoreUncheckedException(String msg) { + super(msg); + } +} diff --git a/core/src/main/java/mc/core/exception/ResourceUnloadedException.java b/core/src/main/java/mc/core/exception/ResourceUnloadedException.java new file mode 100644 index 0000000..07ac21f --- /dev/null +++ b/core/src/main/java/mc/core/exception/ResourceUnloadedException.java @@ -0,0 +1,8 @@ +package mc.core.exception; + +public class ResourceUnloadedException extends McCoreUncheckedException { + + public ResourceUnloadedException(String msg) { + super(msg); + } +} diff --git a/core/src/main/java/mc/core/serialization/IChunkReader.java b/core/src/main/java/mc/core/serialization/IChunkReader.java index 4cb4530..a332385 100644 --- a/core/src/main/java/mc/core/serialization/IChunkReader.java +++ b/core/src/main/java/mc/core/serialization/IChunkReader.java @@ -1,10 +1,10 @@ package mc.core.serialization; -import mc.core.world.chunk.Chunk; +import mc.core.world.ChunkSection; import mc.core.world.Region; import java.io.IOException; 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; } diff --git a/core/src/main/java/mc/core/world/ChunkSection.java b/core/src/main/java/mc/core/world/ChunkSection.java new file mode 100644 index 0000000..8a0e13f --- /dev/null +++ b/core/src/main/java/mc/core/world/ChunkSection.java @@ -0,0 +1,45 @@ +/* + * DmitriyMX + * 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(); +} diff --git a/core/src/main/java/mc/core/world/Region.java b/core/src/main/java/mc/core/world/Region.java index 8448422..def5584 100644 --- a/core/src/main/java/mc/core/world/Region.java +++ b/core/src/main/java/mc/core/world/Region.java @@ -22,8 +22,13 @@ import java.io.Serializable; * */ public interface Region extends Serializable{ - Chunk getChunkAt(int x, int y, int z); - void setChunk(int x, int y, int z, Chunk chunk); + Chunk getChunk (int x, int z); + 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 getZ(); @@ -31,5 +36,7 @@ public interface Region extends Serializable{ Biome getBiomeAt (int x, int z); void setBiome (int x, int z, Biome biome); - void save(Serializer chunkSerializer, IRegionReaderWriter regionReaderWritter) throws IOException; + World getWorld(); + + void save(Serializer chunkSerializer, IRegionReaderWriter regionReaderWritter) throws IOException; } diff --git a/core/src/main/java/mc/core/world/World.java b/core/src/main/java/mc/core/world/World.java index c19a9c5..482dfb8 100644 --- a/core/src/main/java/mc/core/world/World.java +++ b/core/src/main/java/mc/core/world/World.java @@ -6,7 +6,6 @@ package mc.core.world; import mc.core.EntityLocation; import mc.core.nbt.Taggable; -import mc.core.world.chunk.Chunk; import java.io.Serializable; import java.util.UUID; @@ -49,8 +48,8 @@ public interface World extends Taggable, Serializable{ EntityLocation getSpawn(); void setSpawn(EntityLocation location); - Chunk getChunk(int x, int y, int z); - void setChunk(int x, int y, int z, Chunk chunk); + ChunkSection getChunk(int x, int y, int z); + void setChunk(int x, int y, int z, ChunkSection chunkSection); Region getRegion(int x, int z); void setRegion(int x, int z, Region region); diff --git a/core/src/main/java/mc/core/world/chunk/Chunk.java b/core/src/main/java/mc/core/world/chunk/Chunk.java index 38593d4..9196607 100644 --- a/core/src/main/java/mc/core/world/chunk/Chunk.java +++ b/core/src/main/java/mc/core/world/chunk/Chunk.java @@ -1,46 +1,16 @@ -/* - * DmitriyMX - * 2018-04-15 - */ package mc.core.world.chunk; -import mc.core.Location; -import mc.core.world.Biome; -import mc.core.world.block.Block; +import mc.core.world.ChunkSection; +import mc.core.world.Region; +import mc.core.world.World; -import java.io.Serializable; +public interface Chunk { -/** - * 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 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); + World getWorld(); + ChunkSection getChunkSection(int height); + ChunkSection setChunkSection(int height, ChunkSection chunkSection); + Region getRegion(); int getX(); - int getY(); int getZ(); } diff --git a/core/src/main/java/mc/core/world/chunk/ChunkLoader.java b/core/src/main/java/mc/core/world/chunk/ChunkLoader.java index cd0f06f..473bb47 100644 --- a/core/src/main/java/mc/core/world/chunk/ChunkLoader.java +++ b/core/src/main/java/mc/core/world/chunk/ChunkLoader.java @@ -1,5 +1,7 @@ package mc.core.world.chunk; +import mc.core.world.ChunkSection; + import java.util.Optional; public interface ChunkLoader { @@ -12,7 +14,7 @@ public interface ChunkLoader { * @param z chunk position * @return optional of chunk (nullable) */ - Optional loadChunk (int x, int y, int z); + Optional loadChunk (int x, int y, int z); /** * Tries to load chunk like {@link #loadChunk(int, int, int)} @@ -23,5 +25,5 @@ public interface ChunkLoader { * @param z chunk position * @return chunk */ - Chunk loadOrGenerateChunk (int x, int y, int z); + ChunkSection loadOrGenerateChunk (int x, int y, int z); } diff --git a/event-loop/TODO b/event-loop/TODO new file mode 100644 index 0000000..373999f --- /dev/null +++ b/event-loop/TODO @@ -0,0 +1,7 @@ +- Система иерархических блокировок ресурсов (чанки в мире) + - Нужно что-то делать с подгрузкой отсутсвующих чанков для таких блокировок +- Возможность вызвать событие из EventHandler +- Performance Monitor +- Возможная проблема с переполнением очереди при спаме пакетами от игрока +- Добавить поля с замками для ресурсов (Player, World, Chunk) +- Time Scheduler \ No newline at end of file diff --git a/event-loop/build.gradle b/event-loop/build.gradle new file mode 100644 index 0000000..0a1c7d0 --- /dev/null +++ b/event-loop/build.gradle @@ -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" +} \ No newline at end of file diff --git a/event-loop/src/main/java/mc/core/events/EventPipelineTask.java b/event-loop/src/main/java/mc/core/events/EventPipelineTask.java new file mode 100644 index 0000000..f79634f --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/EventPipelineTask.java @@ -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}. + *

+ * 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 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 + } +} diff --git a/event-loop/src/main/java/mc/core/events/FullAsyncEventLoop.java b/event-loop/src/main/java/mc/core/events/FullAsyncEventLoop.java new file mode 100644 index 0000000..ec8fa85 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/FullAsyncEventLoop.java @@ -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. + *

+ * 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> eventQueue = new ConcurrentHashMap<>(); + private Map, List> registeredHandlers = new HashMap<>(); + @SuppressWarnings("SpringJavaAutowiredMembersInspection") + @Autowired + @Setter + private ResourceAwareExecutorService resourceAwareExecutorService; + @Getter + private SharedResourceManager resourceManager = new SharedResourceManager(); + + public void addEventHandler(Plugin plugin, Object object) { + Map candidates = getEventHandlerCandidates(object); + + for (Map.Entry pair : candidates.entrySet()) { + @SuppressWarnings("unchecked") Class eventType = (Class) pair.getKey().getParameterTypes()[0]; + List 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 getPipelineForEvent(Event event) { + return registeredHandlers.get(event.getClass()); + } + + private Map getEventHandlerCandidates(Object object) { + Map 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 handlers = getPipelineForEvent(event); + if (handlers == null) + return; + + Queue 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: + *

+ * - 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 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(); + } + } +} diff --git a/event-loop/src/main/java/mc/core/events/RegisteredEventHandler.java b/event-loop/src/main/java/mc/core/events/RegisteredEventHandler.java new file mode 100644 index 0000000..f0333f7 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/RegisteredEventHandler.java @@ -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; +} diff --git a/event-loop/src/main/java/mc/core/events/SharedResourceManager.java b/event-loop/src/main/java/mc/core/events/SharedResourceManager.java new file mode 100644 index 0000000..ad9f55e --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/SharedResourceManager.java @@ -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 pluginLocks = new ConcurrentHashMap<>(); + // TODO: Memory leak HERE. Fix with introducing field to Player class + private Map playerLocks = new ConcurrentHashMap<>(); + // TODO: Memory leak HERE. Fix with introducing field to World class + private Map 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 require(LockableResource resource, Event event, Class 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 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(); + } + } + +} diff --git a/event-loop/src/main/java/mc/core/events/api/EventHandler.java b/event-loop/src/main/java/mc/core/events/api/EventHandler.java new file mode 100644 index 0000000..dbf255d --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/EventHandler.java @@ -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 {}; +} diff --git a/event-loop/src/main/java/mc/core/events/api/EventPriority.java b/event-loop/src/main/java/mc/core/events/api/EventPriority.java new file mode 100644 index 0000000..0c6fdce --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/EventPriority.java @@ -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; + } +} diff --git a/event-loop/src/main/java/mc/core/events/api/EventQueueOwner.java b/event-loop/src/main/java/mc/core/events/api/EventQueueOwner.java new file mode 100644 index 0000000..49f4fd4 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/EventQueueOwner.java @@ -0,0 +1,4 @@ +package mc.core.events.api; + +public interface EventQueueOwner { +} diff --git a/event-loop/src/main/java/mc/core/events/api/LockableResource.java b/event-loop/src/main/java/mc/core/events/api/LockableResource.java new file mode 100644 index 0000000..5b86b0a --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/LockableResource.java @@ -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 +} diff --git a/event-loop/src/main/java/mc/core/events/api/Plugin.java b/event-loop/src/main/java/mc/core/events/api/Plugin.java new file mode 100644 index 0000000..040ab60 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/Plugin.java @@ -0,0 +1,4 @@ +package mc.core.events.api; + +public interface Plugin { +} diff --git a/event-loop/src/main/java/mc/core/events/api/interfaces/LocationProvidingEvent.java b/event-loop/src/main/java/mc/core/events/api/interfaces/LocationProvidingEvent.java new file mode 100644 index 0000000..bec7040 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/interfaces/LocationProvidingEvent.java @@ -0,0 +1,9 @@ +package mc.core.events.api.interfaces; + +import mc.core.Location; + +import java.util.Collection; + +public interface LocationProvidingEvent { + Collection getAssociatedLocations(); +} diff --git a/event-loop/src/main/java/mc/core/events/api/interfaces/PlayerProvidingEvent.java b/event-loop/src/main/java/mc/core/events/api/interfaces/PlayerProvidingEvent.java new file mode 100644 index 0000000..d05af39 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/interfaces/PlayerProvidingEvent.java @@ -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 getAssociatedPlayers(); + + @Override + default Collection getAssociatedLocations() { + List players = getAssociatedPlayers(); + if (players.size() == 1) + return Collections.singletonList(players.get(0).getLocation()); + else + throw new RuntimeException("This method is not implemented."); + } +} diff --git a/event-loop/src/main/java/mc/core/events/api/interfaces/WorldProvidingEvent.java b/event-loop/src/main/java/mc/core/events/api/interfaces/WorldProvidingEvent.java new file mode 100644 index 0000000..9f561e5 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/interfaces/WorldProvidingEvent.java @@ -0,0 +1,10 @@ +package mc.core.events.api.interfaces; + +import mc.core.world.World; + +import java.util.Collection; + +public interface WorldProvidingEvent { + Collection getAssociatedWorlds(); + +} diff --git a/event-loop/src/main/java/mc/core/events/api/samples/BlockBreakEvent.java b/event-loop/src/main/java/mc/core/events/api/samples/BlockBreakEvent.java new file mode 100644 index 0000000..a515022 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/api/samples/BlockBreakEvent.java @@ -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 getAssociatedPlayers() { + return Collections.singletonList(player); + } + + @Override + public Collection getAssociatedLocations() { + return Collections.singletonList(block.getLocation()); + } +} diff --git a/event-loop/src/main/java/mc/core/events/runner/AllInScheduleStrategy.java b/event-loop/src/main/java/mc/core/events/runner/AllInScheduleStrategy.java new file mode 100644 index 0000000..ce275bd --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/runner/AllInScheduleStrategy.java @@ -0,0 +1,59 @@ +package mc.core.events.runner; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; + +/** + * Simple scheduling strategy. + *

+ * 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 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. + *

+ * 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(); + } + } + } +} diff --git a/event-loop/src/main/java/mc/core/events/runner/ExecutorWorkerThread.java b/event-loop/src/main/java/mc/core/events/runner/ExecutorWorkerThread.java new file mode 100644 index 0000000..e1e5f7b --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/runner/ExecutorWorkerThread.java @@ -0,0 +1,55 @@ +package mc.core.events.runner; + +/** + * Worker thread for {@link ResourceAwareExecutorService}. + *

+ * - 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(); + } + } + } +} diff --git a/event-loop/src/main/java/mc/core/events/runner/ResourceAwareExecutorService.java b/event-loop/src/main/java/mc/core/events/runner/ResourceAwareExecutorService.java new file mode 100644 index 0000000..0ec99ca --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/runner/ResourceAwareExecutorService.java @@ -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 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 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 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(); + } + } +} diff --git a/event-loop/src/main/java/mc/core/events/runner/ResourceAwareRunnable.java b/event-loop/src/main/java/mc/core/events/runner/ResourceAwareRunnable.java new file mode 100644 index 0000000..6f88476 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/runner/ResourceAwareRunnable.java @@ -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() { + + } +} diff --git a/event-loop/src/main/java/mc/core/events/runner/ScheduleStrategy.java b/event-loop/src/main/java/mc/core/events/runner/ScheduleStrategy.java new file mode 100644 index 0000000..10cee55 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/runner/ScheduleStrategy.java @@ -0,0 +1,5 @@ +package mc.core.events.runner; + +public interface ScheduleStrategy { + ResourceAwareRunnable getTask() throws InterruptedException; +} diff --git a/event-loop/src/main/java/mc/core/events/runner/lock/LockObserveList.java b/event-loop/src/main/java/mc/core/events/runner/lock/LockObserveList.java new file mode 100644 index 0000000..157d325 --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/runner/lock/LockObserveList.java @@ -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 { + public static LockObserveList EMPTY_LIST = new LockObserveList(); + private List 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 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(); + } + } + } +} diff --git a/event-loop/src/main/java/mc/core/events/runner/lock/PoorMansLock.java b/event-loop/src/main/java/mc/core/events/runner/lock/PoorMansLock.java new file mode 100644 index 0000000..ace2edc --- /dev/null +++ b/event-loop/src/main/java/mc/core/events/runner/lock/PoorMansLock.java @@ -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> callbacks = new CopyOnWriteArraySet<>(); + + public void addCallback(Consumer callback) { + callbacks.add(callback); + } + + public void removeCallback(Consumer callback) { + callbacks.remove(callback); + } + + + public boolean isLocked() { + return owner != null; + } + + private void triggerUpdate() { + for (Consumer 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(); + } +} diff --git a/event-loop/src/main/java/mc/core/timings/ThreadTimings.java b/event-loop/src/main/java/mc/core/timings/ThreadTimings.java new file mode 100644 index 0000000..408842b --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/ThreadTimings.java @@ -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 stack = new Stack<>(); + + public ThreadTimings() { + this.threadId = IDS.getAndIncrement(); + } + + public Stack 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); + } + } +} diff --git a/event-loop/src/main/java/mc/core/timings/Timings.java b/event-loop/src/main/java/mc/core/timings/Timings.java new file mode 100644 index 0000000..b9d1252 --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/Timings.java @@ -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); + } +} diff --git a/event-loop/src/main/java/mc/core/timings/TimingsEventType.java b/event-loop/src/main/java/mc/core/timings/TimingsEventType.java new file mode 100644 index 0000000..181643a --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/TimingsEventType.java @@ -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; +} \ No newline at end of file diff --git a/event-loop/src/main/java/mc/core/timings/TimingsManager.java b/event-loop/src/main/java/mc/core/timings/TimingsManager.java new file mode 100644 index 0000000..3e8ea66 --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/TimingsManager.java @@ -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 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 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 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; + } + } + } +} diff --git a/event-loop/src/main/java/mc/core/timings/TimingsRecord.java b/event-loop/src/main/java/mc/core/timings/TimingsRecord.java new file mode 100644 index 0000000..3d3e3f6 --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/TimingsRecord.java @@ -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); + } +} diff --git a/event-loop/src/main/java/mc/core/timings/TimingsStaticAccessor.java b/event-loop/src/main/java/mc/core/timings/TimingsStaticAccessor.java new file mode 100644 index 0000000..35d0ed4 --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/TimingsStaticAccessor.java @@ -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()); + } +} diff --git a/event-loop/src/main/java/mc/core/timings/io/DefaultWriterFactory.java b/event-loop/src/main/java/mc/core/timings/io/DefaultWriterFactory.java new file mode 100644 index 0000000..192ee03 --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/io/DefaultWriterFactory.java @@ -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); + } +} diff --git a/event-loop/src/main/java/mc/core/timings/io/TimingsFileWriter.java b/event-loop/src/main/java/mc/core/timings/io/TimingsFileWriter.java new file mode 100644 index 0000000..e2b593c --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/io/TimingsFileWriter.java @@ -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(); + } +} diff --git a/event-loop/src/main/java/mc/core/timings/io/TimingsLogWriter.java b/event-loop/src/main/java/mc/core/timings/io/TimingsLogWriter.java new file mode 100644 index 0000000..0fa5fa3 --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/io/TimingsLogWriter.java @@ -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 { + + } +} diff --git a/event-loop/src/main/java/mc/core/timings/io/TimingsWriter.java b/event-loop/src/main/java/mc/core/timings/io/TimingsWriter.java new file mode 100644 index 0000000..640bdd5 --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/io/TimingsWriter.java @@ -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; +} diff --git a/event-loop/src/main/java/mc/core/timings/io/TimingsWriterFactory.java b/event-loop/src/main/java/mc/core/timings/io/TimingsWriterFactory.java new file mode 100644 index 0000000..db12e6a --- /dev/null +++ b/event-loop/src/main/java/mc/core/timings/io/TimingsWriterFactory.java @@ -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; +} diff --git a/event-loop/src/test/java/mc/core/events/EventExecutorTest.java b/event-loop/src/test/java/mc/core/events/EventExecutorTest.java new file mode 100644 index 0000000..a6c6c14 --- /dev/null +++ b/event-loop/src/test/java/mc/core/events/EventExecutorTest.java @@ -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()); + + } +} diff --git a/event-loop/src/test/java/mc/core/events/EventLoopTest.java b/event-loop/src/test/java/mc/core/events/EventLoopTest.java new file mode 100644 index 0000000..a92e738 --- /dev/null +++ b/event-loop/src/test/java/mc/core/events/EventLoopTest.java @@ -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 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 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()); + } +} diff --git a/event-loop/src/test/java/mc/core/events/LockTest.java b/event-loop/src/test/java/mc/core/events/LockTest.java new file mode 100644 index 0000000..e1e65a2 --- /dev/null +++ b/event-loop/src/test/java/mc/core/events/LockTest.java @@ -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()); + + } +} diff --git a/event-loop/src/test/java/mc/core/timings/TimingsTest.java b/event-loop/src/test/java/mc/core/timings/TimingsTest.java new file mode 100644 index 0000000..a3e6200 --- /dev/null +++ b/event-loop/src/test/java/mc/core/timings/TimingsTest.java @@ -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(); + } +} diff --git a/flat_world/src/main/java/mc/world/flat/FlatWorld.java b/flat_world/src/main/java/mc/world/flat/FlatWorld.java index 29ea78f..29c2839 100644 --- a/flat_world/src/main/java/mc/world/flat/FlatWorld.java +++ b/flat_world/src/main/java/mc/world/flat/FlatWorld.java @@ -8,11 +8,7 @@ import com.flowpowered.nbt.Tag; import lombok.Getter; import lombok.Setter; import mc.core.EntityLocation; -import mc.core.world.IWorldType; -import mc.core.world.Region; -import mc.core.world.World; -import mc.core.world.WorldType; -import mc.core.world.chunk.Chunk; +import mc.core.world.*; import java.util.UUID; import java.util.stream.Stream; @@ -29,7 +25,7 @@ public class FlatWorld implements World { @Getter @Setter 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 public IWorldType getWorldType() { @@ -37,12 +33,12 @@ public class FlatWorld implements World { } @Override - public Chunk getChunk(int x, int y, int z) { - return chunk; + public ChunkSection getChunk(int x, int y, int z) { + return chunkSection; } @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(); } diff --git a/flat_world/src/main/java/mc/world/flat/SimpleChunk.java b/flat_world/src/main/java/mc/world/flat/SimpleChunkSection.java similarity index 58% rename from flat_world/src/main/java/mc/world/flat/SimpleChunk.java rename to flat_world/src/main/java/mc/world/flat/SimpleChunkSection.java index 7a9cb4a..f8e5234 100644 --- a/flat_world/src/main/java/mc/world/flat/SimpleChunk.java +++ b/flat_world/src/main/java/mc/world/flat/SimpleChunkSection.java @@ -5,34 +5,14 @@ package mc.world.flat; 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.chunk.Chunk; - -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) { - } +import mc.core.world.block.BlockType; +public class SimpleChunkSection implements ChunkSection { @Override public int getSkyLight(int x, int y, int z) { if (y <= 3) return 0; @@ -66,16 +46,41 @@ public class SimpleChunk implements Chunk { @Override public int getX() { - return x; + return 0; } @Override public int getY() { - return y; + return 0; } @Override 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; } } diff --git a/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkImpl.java b/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkImpl.java index f2cd8fd..7bb26a0 100644 --- a/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkImpl.java +++ b/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkImpl.java @@ -1,77 +1,61 @@ package mc.world.generated_world.chunk; import lombok.Getter; -import lombok.RequiredArgsConstructor; -import mc.core.world.block.Block; -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.exception.ResourceUnloadedException; +import mc.core.world.ChunkSection; 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; -@RequiredArgsConstructor -public class ChunkImpl implements Chunk{ - private static final BlockFactory blockFactory = new BlockFactory(); +public class ChunkImpl implements Chunk { @Getter private final int x; @Getter - private final int y; - @Getter private final int z; - private final Block[][][] blocks = new Block[WORLD_CHUNK_SIZE][WORLD_CHUNK_SIZE][WORLD_CHUNK_SIZE]; - private final transient Region region; + private Reference regionReference; + private ChunkSection[] sections = new ChunkSection[WORLD_CHUNK_SIZE]; + + public ChunkImpl (int x, int z, Region region) { + this.x = x; + this.z = z; + this.regionReference = new WeakReference<>(region); + } @Override - public Block getBlock(int x, int y, int z) { - Block block = blocks[x][y][z]; - if (block == null) { - block = blockFactory.create(BlockType.AIR, 0, x, y, z); + public World getWorld() { + Region region = getRegion(); + if (region == null) { + throw new ResourceUnloadedException("Region is unloaded"); } - block.setLight(15); - return block; + return region.getWorld(); } @Override - public void setBlock(Block block) { - if (block.getBlockType() == BlockType.AIR) { - blocks[block.getLocation().getBlockX()] - [block.getLocation().getBlockY()] - [block.getLocation().getBlockZ()] = null; + public ChunkSection getChunkSection(int height) { + return sections[height]; + } + + @Override + public ChunkSection setChunkSection(int height, ChunkSection chunkSection) { + sections[height] = chunkSection; + return chunkSection; + } + + @Override + public Region getRegion() { + if (regionReference == null) { + return null; } - blocks[block.getLocation().getBlockX()] - [block.getLocation().getBlockY()] - [block.getLocation().getBlockZ()] = block; - } - @Override - public int getSkyLight(int x, int y, int z) { - return 15; - } + if (regionReference.get() == null) { + throw new ResourceUnloadedException("Region is unloaded"); + } - @Override - public void setSkyLight(int x, int y, int z, int lightLevel) { - throw new UnsupportedOperationException(); - } - - @Override - public int getAddition(int x, int y, int z) { - return 0; - } - - @Override - public void setAddition(int x, int y, int z, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public Biome getBiome(int x, int z) { - return region.getBiomeAt(x + this.x * WORLD_CHUNK_SIZE,z + this.z * WORLD_CHUNK_SIZE); - } - - @Override - public void setBiome(int x, int z, Biome biome) { - region.setBiome(x + this.x * WORLD_CHUNK_SIZE,z + this.z * WORLD_CHUNK_SIZE, biome); + return regionReference.get(); } } diff --git a/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkSectionImpl.java b/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkSectionImpl.java new file mode 100644 index 0000000..e1e3fc3 --- /dev/null +++ b/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkSectionImpl.java @@ -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; + 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(); + } +} diff --git a/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkProxy.java b/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkSectionProxy.java similarity index 82% rename from generated_world/src/main/java/mc/world/generated_world/chunk/ChunkProxy.java rename to generated_world/src/main/java/mc/world/generated_world/chunk/ChunkSectionProxy.java index 12e628e..5b4c58e 100644 --- a/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkProxy.java +++ b/generated_world/src/main/java/mc/world/generated_world/chunk/ChunkSectionProxy.java @@ -1,18 +1,20 @@ 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.Biome; -import mc.core.world.chunk.Chunk; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -public class ChunkProxy implements Chunk { - private final Chunk chunk; +public class ChunkSectionProxy implements ChunkSection { + private final ChunkSection chunk; private volatile transient long lastUsage = System.currentTimeMillis(); private final transient ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - public ChunkProxy(Chunk chunk) { + public ChunkSectionProxy(ChunkSection chunk) { this.chunk = chunk; } @@ -34,6 +36,16 @@ public class ChunkProxy implements Chunk { return chunk.getBlock(x, y, z); } + @Override + public Region getRegion() { + return chunk.getRegion(); + } + + @Override + public World getWorld() { + return chunk.getWorld(); + } + @Override public void setBlock(Block block) { use(); diff --git a/generated_world/src/main/java/mc/world/generated_world/chunk/InMemoryCacheChunkLoader.java b/generated_world/src/main/java/mc/world/generated_world/chunk/InMemoryCacheChunkLoader.java index c499d7f..ebc350b 100644 --- a/generated_world/src/main/java/mc/world/generated_world/chunk/InMemoryCacheChunkLoader.java +++ b/generated_world/src/main/java/mc/world/generated_world/chunk/InMemoryCacheChunkLoader.java @@ -27,7 +27,7 @@ public class InMemoryCacheChunkLoader implements ChunkLoader { @Autowired private ChunkReader chunkReader; @Autowired - private Serializer chunkSerializer; + private Serializer chunkSerializer; @Autowired private RegionReaderWriter regionReaderWritter; @@ -46,14 +46,14 @@ public class InMemoryCacheChunkLoader implements ChunkLoader { } @Override - public Optional loadChunk(int x, int y, int z) { + public Optional loadChunk(int x, int y, int z) { File file = getChuckFile(x, y, z); if (!file.exists()) { return Optional.empty(); } else { try { - Chunk chunk = chunkReader.read(world.getRegion(x / WORLD_CHUNK_SIZE, z / WORLD_CHUNK_SIZE), x, y, z); - return Optional.of(chunk); + ChunkSection chunkSection = chunkReader.read(world.getRegion(x / WORLD_CHUNK_SIZE, z / WORLD_CHUNK_SIZE), x, y, z); + return Optional.of(chunkSection); } catch (IOException e) { log.error("Error occurred while reading chunk file: " + file.getAbsolutePath(), e); return Optional.empty(); @@ -62,12 +62,12 @@ public class InMemoryCacheChunkLoader implements ChunkLoader { } @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 regZ = z / WORLD_CHUNK_SIZE; File regionFile = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, regX, regZ)); Region region; - Chunk chunk; + ChunkSection chunkSection; if (!regionFile.exists()) { log.debug("Region [{}, {}] not found. Generating!", regX, regZ); regionFile.mkdirs(); @@ -78,17 +78,17 @@ public class InMemoryCacheChunkLoader implements ChunkLoader { log.error("Error occurred while writting biome file", e); } saveRegion(region); - chunk = region.getChunkAt(x % WORLD_CHUNK_SIZE, y % WORLD_CHUNK_SIZE, z % WORLD_CHUNK_SIZE); + chunkSection = region.getChunkAt(x % WORLD_CHUNK_SIZE, y % WORLD_CHUNK_SIZE, z % WORLD_CHUNK_SIZE); } else { try { region = regionReaderWritter.read(regX, regZ, world); - chunk = chunkReader.read(region, x, y, z); + chunkSection = chunkReader.read(region, x, y, z); } catch (IOException e) { - log.error("Error occurred while reading chunk file", e); + log.error("Error occurred while reading chunkSection file", e); return null; } } - return chunk; + return chunkSection; } private void saveRegion (Region region) { diff --git a/generated_world/src/main/java/mc/world/generated_world/generator/SeedBasedWorldGenerator.java b/generated_world/src/main/java/mc/world/generated_world/generator/SeedBasedWorldGenerator.java index dedaf29..11a83d1 100644 --- a/generated_world/src/main/java/mc/world/generated_world/generator/SeedBasedWorldGenerator.java +++ b/generated_world/src/main/java/mc/world/generated_world/generator/SeedBasedWorldGenerator.java @@ -5,7 +5,6 @@ import lombok.extern.slf4j.Slf4j; import mc.core.world.block.BlockFactory; import mc.core.world.block.BlockType; import mc.core.world.*; -import mc.core.world.chunk.Chunk; import mc.world.generated_world.region.RegionImpl; import mc.world.generated_world.world.CubicWorld; import mc.world.generated_world.world.Temperature; @@ -226,7 +225,7 @@ public class SeedBasedWorldGenerator implements WorldGenerator { region.setBiome(x, z, biomes[x][z]); if (heightMap[x][z] < WORLD_SEA_LEVEL) { for (int y = 0; y < WORLD_SEA_LEVEL; y ++) { - Chunk chunk = region.getChunkAt(x / 16, y / 16, z / 16); + ChunkSection chunk = region.getChunkAt(x / 16, y / 16, z / 16); if (y == 0) { chunk.setBlock(blockFactory.create(BlockType.BEDROCK, 0, x % 16, y % 16, z % 16)); continue; @@ -243,7 +242,7 @@ public class SeedBasedWorldGenerator implements WorldGenerator { } } else { 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) { chunk.setBlock(blockFactory.create(BlockType.BEDROCK, 0, x % 16, y % 16, z % 16)); continue; diff --git a/generated_world/src/main/java/mc/world/generated_world/region/RegionImpl.java b/generated_world/src/main/java/mc/world/generated_world/region/RegionImpl.java index 83cb6c6..66abe5d 100644 --- a/generated_world/src/main/java/mc/world/generated_world/region/RegionImpl.java +++ b/generated_world/src/main/java/mc/world/generated_world/region/RegionImpl.java @@ -1,61 +1,90 @@ package mc.world.generated_world.region; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import mc.core.exception.ResourceUnloadedException; import mc.core.serialization.IRegionReaderWriter; import mc.core.serialization.Serializer; import mc.core.world.*; import mc.core.world.chunk.Chunk; 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.ChunkImpl; -import mc.world.generated_world.chunk.ChunkProxy; +import mc.world.generated_world.chunk.ChunkSectionImpl; import org.springframework.beans.factory.annotation.Autowired; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; import java.text.MessageFormat; import static mc.world.generated_world.WorldConstants.*; @Slf4j -@RequiredArgsConstructor public class RegionImpl implements Region{ @Getter private final int x; @Getter private final int z; - private final ChunkProxy[][][] chunks = new ChunkProxy[WORLD_REGION_SIZE/WORLD_CHUNK_SIZE][WORLD_REGION_SIZE/WORLD_CHUNK_SIZE][WORLD_REGION_SIZE/WORLD_CHUNK_SIZE]; + private final 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]; - @Getter - private final transient World world; + private final transient Reference world; + private final Chunk[][] chunks = new Chunk[WORLD_REGION_SIZE/WORLD_CHUNK_SIZE][WORLD_REGION_SIZE/WORLD_CHUNK_SIZE]; @Autowired private ChunkLoader chunkLoader; + public RegionImpl (int x, int z, World world) { + this.x = x; + this.z = z; + this.world = new WeakReference<>(world); + } + @Override - public Chunk getChunkAt(int x, int y, int z) { - if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= 16 || z >= 16) { - throw new RuntimeException(MessageFormat.format("Invalid chunk coordinates [{0} {1} {2}]", x, y, z)); + public Chunk getChunk(int x, int z) { + if (x < 0 || z < 0 || x >= 16 || z >= 16) { + throw new RuntimeException(MessageFormat.format("Invalid chunk coordinates [{0} {1}]", x, z)); } - if (chunkLoader == null) { - chunkLoader = new InMemoryCacheChunkLoader(world); - } - Chunk chunk = chunks[x][y][z]; + + Chunk chunk = chunks[x][z]; if (chunk == null) { - chunk = chunkLoader.loadChunk(x + this.x * WORLD_REGION_SIZE, y, this.z * WORLD_REGION_SIZE).orElse(new ChunkImpl(x, y, z, this)); - chunks[x][y][z] = new ChunkProxy(chunk); + chunk = new ChunkImpl(x, z, this); + for (int y = 0; y < WORLD_CHUNK_SIZE; y ++) { + chunk.setChunkSection(y, getChunkAt(x, y, z)); + } } return chunk; } @Override - public void setChunk(int x, int y, int z, Chunk chunk) { + public void setChunk(int x, int z, Chunk chunk) { + chunks[x][z] = 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 chunk coordinates [{0} {1} {2}]", x, y, z)); + throw new RuntimeException(MessageFormat.format("Invalid chunkSection coordinates [{0} {1} {2}]", x, y, z)); } - chunks[x][y][z] = new ChunkProxy(chunk); + 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 @@ -75,9 +104,20 @@ public class RegionImpl implements Region{ } @Override - public void save(Serializer 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 chunkSerializer, IRegionReaderWriter regionReaderWriter) throws IOException { 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())); if (!regionFile.exists()) { regionFile.mkdirs(); @@ -86,8 +126,8 @@ public class RegionImpl implements Region{ for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) { for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) { for (int y = 0; y < WORLD_CHUNK_SIZE; y++) { - Chunk chunk = this.getChunkAt(x, y, z); - byte[] chunkBytes = chunkSerializer.serialize(chunk); + ChunkSection chunkSection = this.getChunkAt(x, y, z); + byte[] chunkBytes = chunkSerializer.serialize(chunkSection); if (chunkBytes.length > 0) { File chunkFile = new File(regionFile, MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z)); try (FileOutputStream fileOutputStream = new FileOutputStream(chunkFile)) { diff --git a/generated_world/src/main/java/mc/world/generated_world/serialization/BlockSerializerDeserializer.java b/generated_world/src/main/java/mc/world/generated_world/serialization/BlockSerializerDeserializer.java index 3863fb0..5291a23 100644 --- a/generated_world/src/main/java/mc/world/generated_world/serialization/BlockSerializerDeserializer.java +++ b/generated_world/src/main/java/mc/world/generated_world/serialization/BlockSerializerDeserializer.java @@ -5,7 +5,7 @@ import mc.core.world.block.BlockFactory; import mc.core.world.block.BlockType; import mc.core.serialization.Deserializer; import mc.core.serialization.Serializer; -import mc.core.world.chunk.Chunk; +import mc.core.world.ChunkSection; /** * Prototype @@ -13,20 +13,20 @@ import mc.core.world.chunk.Chunk; public class BlockSerializerDeserializer implements Serializer, Deserializer { 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.chunk = chunk; + this.chunkSection = chunkSection; } @Override public Block deserialize(byte[] bytes) { int id = bytes[0] + 128; int meta = bytes[1] >> 4; - int x = (bytes[1] & 0xf) + chunk.getX() * 16; - int y = bytes[2] >> 4 + chunk.getY() * 16; - int z = (bytes[2] & 0xf) + chunk.getZ() * 16; + int x = (bytes[1] & 0xf) + chunkSection.getX() * 16; + int y = bytes[2] >> 4 + chunkSection.getY() * 16; + int z = (bytes[2] & 0xf) + chunkSection.getZ() * 16; BlockType type = BlockType.values()[id]; Block block = blockFactory.create(type, meta); block.getLocation().setX(x); diff --git a/generated_world/src/main/java/mc/world/generated_world/serialization/ChunkReader.java b/generated_world/src/main/java/mc/world/generated_world/serialization/ChunkReader.java index 5e7c419..f6e44cb 100644 --- a/generated_world/src/main/java/mc/world/generated_world/serialization/ChunkReader.java +++ b/generated_world/src/main/java/mc/world/generated_world/serialization/ChunkReader.java @@ -1,11 +1,12 @@ package mc.world.generated_world.serialization; +import mc.core.Location; import mc.core.world.block.Block; import mc.core.serialization.Deserializer; import mc.core.serialization.IChunkReader; -import mc.core.world.chunk.Chunk; +import mc.core.world.ChunkSection; 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 java.io.File; @@ -26,22 +27,22 @@ public class ChunkReader implements IChunkReader{ } @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; y %= WORLD_REGION_SIZE; z %= WORLD_REGION_SIZE; File chunkFile = new File(new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, region.getX(), region.getZ())), MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z)); byte[] chunkBytes = Files.readAllBytes(Paths.get(chunkFile.toURI())); int blocks = (chunkBytes.length) / 3; - Chunk chunk = new ChunkImpl(x, y, z, region); + ChunkSection chunkSection = new ChunkSectionImpl(x, y, z, region); for (int i = 0; i < blocks; i ++) { byte[] blockBytes = new byte[3]; blockBytes[0] = chunkBytes[3 * i]; blockBytes[1] = chunkBytes[1 + 3 * i]; blockBytes[2] = chunkBytes[2 + 3 * i]; Block block = blockDeserializer.deserialize(blockBytes); - chunk.setBlock(block); + chunkSection.setBlock(block); } - return chunk; + return chunkSection; } } diff --git a/generated_world/src/main/java/mc/world/generated_world/serialization/ChunkSerializer.java b/generated_world/src/main/java/mc/world/generated_world/serialization/ChunkSerializer.java index cba4572..46fdede 100644 --- a/generated_world/src/main/java/mc/world/generated_world/serialization/ChunkSerializer.java +++ b/generated_world/src/main/java/mc/world/generated_world/serialization/ChunkSerializer.java @@ -5,7 +5,7 @@ import mc.core.world.block.Block; import mc.core.world.block.BlockFactory; import mc.core.world.block.BlockType; import mc.core.serialization.Serializer; -import mc.core.world.chunk.Chunk; +import mc.core.world.ChunkSection; import org.springframework.beans.factory.annotation.Autowired; import java.io.ByteArrayOutputStream; @@ -14,20 +14,20 @@ import java.io.IOException; import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE; @Slf4j -public class ChunkSerializer implements Serializer { +public class ChunkSerializer implements Serializer { @Autowired private Serializer blockSerializer; @Override - public byte[] serialize(Chunk chunk) { - Serializer blockSerializer = new BlockSerializerDeserializer(new BlockFactory(), chunk); + public byte[] serialize(ChunkSection chunkSection) { + Serializer blockSerializer = new BlockSerializerDeserializer(new BlockFactory(), chunkSection); ByteArrayOutputStream baos = new ByteArrayOutputStream(); Block current; for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) { for (int y = 0; y < WORLD_CHUNK_SIZE; y ++) { for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) { - current = chunk.getBlock(x, y, z); + current = chunkSection.getBlock(x, y, z); if (current != null && current.getBlockType() != BlockType.AIR) { try { baos.write(blockSerializer.serialize(current)); diff --git a/generated_world/src/main/java/mc/world/generated_world/world/CubicWorld.java b/generated_world/src/main/java/mc/world/generated_world/world/CubicWorld.java index e649b23..f4c2f3a 100644 --- a/generated_world/src/main/java/mc/world/generated_world/world/CubicWorld.java +++ b/generated_world/src/main/java/mc/world/generated_world/world/CubicWorld.java @@ -4,32 +4,58 @@ import com.flowpowered.nbt.Tag; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import mc.core.Direction; import mc.core.EntityLocation; -import mc.core.world.IWorldType; -import mc.core.world.Region; -import mc.core.world.World; -import mc.core.world.block.BlockType; -import mc.core.world.chunk.Chunk; +import mc.core.world.*; +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.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; -import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE; -import static mc.world.generated_world.WorldConstants.WORLD_MAX_HEIGHT; +import static mc.world.generated_world.WorldConstants.REGION_FILE_NAME_TEMPLATE; + +/* + * NORTH + * + * EAST WEST + * + * SOUTH + * + * + ----> X + * | + * | + * | + * V Z + */ @Slf4j 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 private final UUID worldId; private final int seed; - private volatile EntityLocation warpPosition; + private volatile EntityLocation spawn; private final transient Object spawnLocationLock = new Object(); private final Map> nbtTagMap = new HashMap<>(); - @Autowired - private RegionManager regionManager; @Getter@Setter private String name; @@ -55,55 +81,76 @@ public class CubicWorld implements World { @Override public IWorldType getWorldType() { - return null; + return null; //FIXME } @Override public EntityLocation getSpawn() { - if (warpPosition == null) { - synchronized (spawnLocationLock) { - if (warpPosition == null) { - log.warn("Spawn location is not defined. Trying to select best location"); - 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); - } - } + /* 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 warpPosition; + return spawn; } @Override - public void setSpawn(EntityLocation warpPosition) { + public void setSpawn(EntityLocation entityLocation) { synchronized (spawnLocationLock) { - this.warpPosition = warpPosition; + entityLocation.setWorld(this); + this.spawn = entityLocation; } } @Override - public Chunk getChunk(int x, int y, int z) { - return null; + public ChunkSection getChunk(int x, int y, int z) { + Region region = getRegion(x / 16, z / 16); + return region.getChunkAt(x % 16, y % 16, z % 16); } @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 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 public void setRegion(int x, int z, Region region) { - + try { + regionSaveLock.lock(); + regions[x - pointX][z - pointZ] = region; + } finally { + regionSaveLock.unlock(); + } } @Override @@ -125,4 +172,53 @@ public class CubicWorld implements World { public Stream> tagStream() { 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(); + } + } } diff --git a/generated_world/src/main/java/mc/world/generated_world/world/RegionManager.java b/generated_world/src/main/java/mc/world/generated_world/world/RegionManager.java deleted file mode 100644 index 0cc0ef4..0000000 --- a/generated_world/src/main/java/mc/world/generated_world/world/RegionManager.java +++ /dev/null @@ -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(); - } - } - -} diff --git a/generated_world/src/test/java/mc/world/generated_world/SeedRandomGeneratorTest.java b/generated_world/src/test/java/mc/world/generated_world/SeedRandomGeneratorTest.java index a99a251..6c5fbe4 100644 --- a/generated_world/src/test/java/mc/world/generated_world/SeedRandomGeneratorTest.java +++ b/generated_world/src/test/java/mc/world/generated_world/SeedRandomGeneratorTest.java @@ -1,6 +1,7 @@ package mc.world.generated_world; import mc.world.generated_world.generator.SeedRandomGenerator; +import org.junit.Ignore; import org.junit.Test; import javax.imageio.ImageIO; @@ -9,6 +10,7 @@ import java.io.File; import static org.junit.Assert.*; +@Ignore public class SeedRandomGeneratorTest { @Test diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacket.java index 122ff39..d700abf 100644 --- a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacket.java +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacket.java @@ -11,8 +11,8 @@ import lombok.extern.slf4j.Slf4j; import mc.core.network.NetOutputStream; import mc.core.network.SCPacket; import mc.core.network.proto_1_12_2.ByteArrayOutputNetStream; +import mc.core.world.ChunkSection; import mc.core.world.block.Block; -import mc.core.world.chunk.Chunk; import java.util.ArrayList; import java.util.List; @@ -76,7 +76,7 @@ public class ChunkDataPacket implements SCPacket { @Setter private boolean initChunk = true; // "Ground-Up Continuous" @Getter - private List chunks = new ArrayList<>(); + private List chunks = new ArrayList<>(); private int serializeBlockState(int id, int meta) { return (id << 4) | meta; @@ -93,7 +93,7 @@ public class ChunkDataPacket implements SCPacket { int dataItems = 0; final int airBlockPalette = serializeBlockState(0, 0); - for (Chunk chunk : chunks) { + for (ChunkSection chunk : chunks) { final List palette = new ArrayList<>(); palette.add(airBlockPalette); final ByteArrayOutputNetStream dataArray = new ByteArrayOutputNetStream(); diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/SpawnPositionPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/SpawnPositionPacket.java index ee18ebd..5264c7d 100644 --- a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/SpawnPositionPacket.java +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/SpawnPositionPacket.java @@ -8,7 +8,7 @@ import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; -import mc.core.Location; +import mc.core.EntityLocation; import mc.core.network.NetOutputStream; import mc.core.network.SCPacket; @@ -17,17 +17,17 @@ import mc.core.network.SCPacket; @Setter @ToString public class SpawnPositionPacket implements SCPacket { - private Location location; + private EntityLocation location; private int floor_double(double value) { int i = (int)value; return value < (double)i ? i - 1 : i; } - private long location2long(Location location) { - return ((floor_double(location.getX()) & 0x3FFFFFF) << 38) - | ((floor_double(location.getY()) & 0xFFF) << 26) - | (floor_double(location.getZ()) & 0x3FFFFFF); + private long location2long(EntityLocation entityLocation) { + return ((floor_double(entityLocation.getX()) & 0x3FFFFFF) << 38) + | ((floor_double(entityLocation.getY()) & 0xFFF) << 26) + | (floor_double(entityLocation.getZ()) & 0x3FFFFFF); } @Override diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java index 6f0b90b..f0d4905 100644 --- a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java @@ -61,7 +61,7 @@ public class LoginHandler extends AbstractStateHandler implements LoginStateHand pkt1.setMode(PlayerMode.CREATIVE); pkt1.setDimension(0/*Overworld*/); pkt1.setDifficulty(0/*Peaceful*/); - pkt1.setLevelType("flat"); + pkt1.setLevelType("flat"); //FIXME channel.write(pkt1); // Spawn Position diff --git a/settings.gradle b/settings.gradle index b865638..bc4f7bf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,4 +6,4 @@ include('vanilla_commands') include('proto_1.12.2') // Protocol 1.12.2 include('proto_1.12.2_netty') // Protocol 1.12.2 (Netty impl.) include('generated_world') - +include('event-loop') \ No newline at end of file