remove modules
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
- Система иерархических блокировок ресурсов (чанки в мире)
|
||||
- Нужно что-то делать с подгрузкой отсутсвующих чанков для таких блокировок
|
||||
- Возможность вызвать событие из EventHandler
|
||||
- Performance Monitor
|
||||
- Возможная проблема с переполнением очереди при спаме пакетами от игрока
|
||||
- Добавить поля с замками для ресурсов (Player, World, Chunk)
|
||||
- Time Scheduler
|
||||
@@ -1,15 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package mc.core.events;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import mc.core.events.api.EventQueueOwner;
|
||||
import mc.core.events.api.LockableResource;
|
||||
import mc.core.events.runner.lock.LockObserveList;
|
||||
import mc.core.events.runner.ResourceAwareExecutorService;
|
||||
import mc.core.events.runner.ResourceAwareRunnable;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Holds processing pipeline for every event
|
||||
* that enters {@link FullAsyncEventLoop}.
|
||||
* <p>
|
||||
* Ensures that EventHandlers will never be called in a wrong
|
||||
* order by feeding only one task at a time to the {@link ResourceAwareExecutorService}
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
@Slf4j
|
||||
public class EventPipelineTask {
|
||||
private final ResourceAwareExecutorService service;
|
||||
private final List<RegisteredEventHandler> handlers;
|
||||
private final FullAsyncEventLoop manager;
|
||||
private final Event event;
|
||||
private final EventQueueOwner owner;
|
||||
private int currentIndex = 0;
|
||||
@Setter
|
||||
private PipelineState state = PipelineState.IDLE;
|
||||
|
||||
public void next() {
|
||||
if (updatePipelineState()) return;
|
||||
|
||||
RegisteredEventHandler handler = handlers.get(currentIndex);
|
||||
// If event has been already cancelled and current handler
|
||||
// ignores cancelled events
|
||||
if (event.isCanceled() && handler.isIgnoreCancelled()) {
|
||||
// Just skip current event handler
|
||||
currentIndex++;
|
||||
next();
|
||||
} else {
|
||||
feedTask(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current pipeline status
|
||||
*
|
||||
* @return true if pipeline has been completed
|
||||
*/
|
||||
private boolean updatePipelineState() {
|
||||
if (state == PipelineState.IDLE) {
|
||||
state = PipelineState.WORKING;
|
||||
}
|
||||
if (currentIndex >= handlers.size() && state == PipelineState.WORKING) {
|
||||
state = PipelineState.FINISHED;
|
||||
manager.update(owner);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state == PipelineState.FINISHED) {
|
||||
throw new IllegalStateException("Attempted to call next step on a FINISHED pipeline");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void feedTask(RegisteredEventHandler handler) {
|
||||
LockObserveList locks = getLocks(handler);
|
||||
service.addTask(new ResourceAwareRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
handler.getMethod().invoke(handler.getObject(), event);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
log.error("Unable to dispatch event " + event.getClass().getSimpleName() + " to handler " + event.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void after() {
|
||||
currentIndex++;
|
||||
next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockObserveList getLocks() {
|
||||
return locks;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private LockObserveList getLocks(RegisteredEventHandler handler) {
|
||||
LockObserveList locks = new LockObserveList();
|
||||
|
||||
if (handler.isPluginSynchronize())
|
||||
locks.add(manager.getResourceManager().getPluginLock(handler.getPlugin()));
|
||||
|
||||
for (LockableResource resource : handler.getLock()) {
|
||||
locks.addAll(manager.getResourceManager().getAnnotationLocks(resource, event));
|
||||
}
|
||||
|
||||
return locks;
|
||||
}
|
||||
|
||||
public enum PipelineState {
|
||||
IDLE, WORKING, FINISHED
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package mc.core.events;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import mc.core.events.api.EventHandler;
|
||||
import mc.core.events.api.EventQueueOwner;
|
||||
import mc.core.events.api.Plugin;
|
||||
import mc.core.events.runner.ResourceAwareExecutorService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Event loop core. Manages event handler registration process,
|
||||
* maintains event queues.
|
||||
* <p>
|
||||
* This event loop guarantees that events, assigned to the {@link EventQueueOwner}
|
||||
* will be handler in order of scheduling
|
||||
*/
|
||||
@Slf4j
|
||||
public class FullAsyncEventLoop {
|
||||
// Item leaves this queue only when EventPipeline is fully executed
|
||||
private Map<EventQueueOwner, Queue<EventPipelineTask>> eventQueue = new ConcurrentHashMap<>();
|
||||
private Map<Class<? extends Event>, List<RegisteredEventHandler>> registeredHandlers = new HashMap<>();
|
||||
@SuppressWarnings("SpringJavaAutowiredMembersInspection")
|
||||
@Autowired
|
||||
@Setter
|
||||
private ResourceAwareExecutorService resourceAwareExecutorService;
|
||||
@Getter
|
||||
private SharedResourceManager resourceManager = new SharedResourceManager();
|
||||
|
||||
public void addEventHandler(Plugin plugin, Object object) {
|
||||
Map<Method, EventHandler> candidates = getEventHandlerCandidates(object);
|
||||
|
||||
for (Map.Entry<Method, EventHandler> pair : candidates.entrySet()) {
|
||||
@SuppressWarnings("unchecked") Class<? extends Event> eventType = (Class<? extends Event>) pair.getKey().getParameterTypes()[0];
|
||||
List<RegisteredEventHandler> handlers = this.registeredHandlers.computeIfAbsent(eventType, e -> new ArrayList<>());
|
||||
handlers.add(new RegisteredEventHandler(plugin, object, pair.getKey(), pair.getValue().lock(), pair.getValue().pluginSynchronize(), pair.getValue().priority().getValue(), pair.getValue().ignoreCancelled()));
|
||||
handlers.sort(Comparator.comparingInt(RegisteredEventHandler::getPriority));
|
||||
}
|
||||
}
|
||||
|
||||
public List<RegisteredEventHandler> getPipelineForEvent(Event event) {
|
||||
return registeredHandlers.get(event.getClass());
|
||||
}
|
||||
|
||||
private Map<Method, EventHandler> getEventHandlerCandidates(Object object) {
|
||||
Map<Method, EventHandler> candidates;
|
||||
candidates = new HashMap<>();
|
||||
for (Method method : object.getClass().getDeclaredMethods()) {
|
||||
EventHandler annotation = method.getAnnotation(EventHandler.class);
|
||||
if (annotation == null)
|
||||
continue;
|
||||
|
||||
if (!Modifier.isPublic(method.getModifiers())) {
|
||||
log.error("Unable to register {} as an EventHandler. Method must have a 'public' access modifier.", method.toString());
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
if (method.getParameterCount() != 1) {
|
||||
log.error("Unable to register {} as an EventHandler. Method must have exactly one argument.", method.toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
Class<?> firstParamType = method.getParameterTypes()[0];
|
||||
if (!Event.class.isAssignableFrom(firstParamType)) {
|
||||
log.error("Unable to register {} as an EventHandler. First parameter type must implement 'Event' interface.", method.toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
method.setAccessible(true);
|
||||
candidates.put(method, annotation);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
public void asyncFireEvent(EventQueueOwner owner, Event event) {
|
||||
List<RegisteredEventHandler> handlers = getPipelineForEvent(event);
|
||||
if (handlers == null)
|
||||
return;
|
||||
|
||||
Queue<EventPipelineTask> queue = eventQueue.computeIfAbsent(owner, s -> new ArrayDeque<>());
|
||||
queue.add(new EventPipelineTask(resourceAwareExecutorService, handlers, this, event, owner));
|
||||
update(owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates queue state for a given owner:
|
||||
* <p>
|
||||
* - Removes first element of a queue if it is marked as FINISHED
|
||||
* - Starts executing first pipeline from the queue if it is marked with IDLE
|
||||
*
|
||||
* @param owner queue owner
|
||||
*/
|
||||
public synchronized void update(EventQueueOwner owner) {
|
||||
if (!eventQueue.containsKey(owner)) {
|
||||
log.warn("Unable to update pipeline executor: unable to find queue");
|
||||
return;
|
||||
}
|
||||
Queue<EventPipelineTask> queue = eventQueue.get(owner);
|
||||
if (queue.isEmpty()) {
|
||||
log.warn("Unable to update pipeline executor: queue is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (queue.peek().getState() == EventPipelineTask.PipelineState.FINISHED) {
|
||||
queue.poll();
|
||||
}
|
||||
|
||||
EventPipelineTask pipeline;
|
||||
if ((pipeline = queue.peek()) != null
|
||||
&& pipeline.getState() == EventPipelineTask.PipelineState.IDLE) {
|
||||
pipeline.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package mc.core.events;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import mc.core.Location;
|
||||
import mc.core.events.api.LockableResource;
|
||||
import mc.core.events.api.Plugin;
|
||||
import mc.core.events.api.interfaces.LocationProvidingEvent;
|
||||
import mc.core.events.api.interfaces.PlayerProvidingEvent;
|
||||
import mc.core.events.api.interfaces.WorldProvidingEvent;
|
||||
import mc.core.events.runner.lock.PoorMansLock;
|
||||
import mc.core.player.Player;
|
||||
import mc.core.world.World;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class SharedResourceManager {
|
||||
private Map<Plugin, PoorMansLock> pluginLocks = new ConcurrentHashMap<>();
|
||||
// TODO: Memory leak HERE. Fix with introducing field to Player class
|
||||
private Map<Player, PoorMansLock> playerLocks = new ConcurrentHashMap<>();
|
||||
// TODO: Memory leak HERE. Fix with introducing field to World class
|
||||
private Map<World, PoorMansLock> worldLocks = new ConcurrentHashMap<>();
|
||||
|
||||
public PoorMansLock getPluginLock(Plugin plugin) {
|
||||
return pluginLocks.computeIfAbsent(plugin, s -> new PoorMansLock());
|
||||
}
|
||||
|
||||
public PoorMansLock getPlayerLock(Player player) {
|
||||
return playerLocks.computeIfAbsent(player, s -> new PoorMansLock());
|
||||
}
|
||||
|
||||
public PoorMansLock getWorldLock(World world) {
|
||||
return worldLocks.computeIfAbsent(world, s -> new PoorMansLock());
|
||||
}
|
||||
|
||||
private <T> T require(LockableResource resource, Event event, Class<T> inter) {
|
||||
if (inter.isInstance(event)) {
|
||||
//noinspection unchecked
|
||||
return (T) event;
|
||||
} else
|
||||
throw new IllegalArgumentException("Unable to lock " + resource + " while attempting to process event. Event " + event.getClass().getSimpleName() + " must implement " + inter);
|
||||
}
|
||||
|
||||
public Collection<PoorMansLock> getAnnotationLocks(LockableResource resource, Event event) {
|
||||
switch (resource) {
|
||||
case PLAYER:
|
||||
return require(resource, event, PlayerProvidingEvent.class).getAssociatedPlayers().stream().map(this::getPlayerLock).collect(Collectors.toList());
|
||||
case PLAYER_WORLD:
|
||||
return require(resource, event, PlayerProvidingEvent.class).getAssociatedPlayers().stream().map(s -> s.getLocation().getWorld()).map(this::getWorldLock).collect(Collectors.toList());
|
||||
case EVENT_LOCATION_WORLD:
|
||||
return require(resource, event, LocationProvidingEvent.class).getAssociatedLocations().stream().map(Location::getWorld).map(this::getWorldLock).collect(Collectors.toList());
|
||||
case EVENT_WORLD:
|
||||
return require(resource, event, WorldProvidingEvent.class).getAssociatedWorlds().stream().map(this::getWorldLock).collect(Collectors.toList());
|
||||
default:
|
||||
log.warn("Unable to find action for " + resource + " resource definition.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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 {};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package mc.core.events.api;
|
||||
|
||||
public interface EventQueueOwner {
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package mc.core.events.api;
|
||||
|
||||
public enum LockableResource {
|
||||
PLAYER,
|
||||
PLAYER_WORLD,
|
||||
EVENT_LOCATION_WORLD,
|
||||
EVENT_WORLD
|
||||
|
||||
// TODO: Add entity-related constants
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package mc.core.events.api;
|
||||
|
||||
public interface Plugin {
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package mc.core.events.api.interfaces;
|
||||
|
||||
import mc.core.Location;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface LocationProvidingEvent {
|
||||
Collection<Location> getAssociatedLocations();
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package mc.core.events.api.interfaces;
|
||||
|
||||
import mc.core.Location;
|
||||
import mc.core.player.Player;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public interface PlayerProvidingEvent extends LocationProvidingEvent {
|
||||
List<Player> getAssociatedPlayers();
|
||||
|
||||
@Override
|
||||
default Collection<Location> getAssociatedLocations() {
|
||||
List<Player> players = getAssociatedPlayers();
|
||||
if (players.size() == 1)
|
||||
return Collections.singletonList(players.get(0).getLocation());
|
||||
else
|
||||
throw new RuntimeException("This method is not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package mc.core.events.api.interfaces;
|
||||
|
||||
import mc.core.world.World;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface WorldProvidingEvent {
|
||||
Collection<World> getAssociatedWorlds();
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package mc.core.events.api.samples;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import mc.core.Location;
|
||||
import mc.core.events.EventBase;
|
||||
import mc.core.events.api.interfaces.LocationProvidingEvent;
|
||||
import mc.core.events.api.interfaces.PlayerProvidingEvent;
|
||||
import mc.core.player.Player;
|
||||
import mc.core.world.block.Block;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public class BlockBreakEvent extends EventBase implements PlayerProvidingEvent, LocationProvidingEvent {
|
||||
private final Player player;
|
||||
private final Block block;
|
||||
|
||||
@Override
|
||||
public List<Player> getAssociatedPlayers() {
|
||||
return Collections.singletonList(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Location> getAssociatedLocations() {
|
||||
return Collections.singletonList(block.getLocation());
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package mc.core.events.runner;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/**
|
||||
* Simple scheduling strategy.
|
||||
* <p>
|
||||
* We wait until the first task in a queue will be able to acquire all
|
||||
* the necessary resources and then we schedule it for execution
|
||||
*/
|
||||
public class AllInScheduleStrategy implements ScheduleStrategy {
|
||||
private BlockingQueue<ResourceAwareRunnable> globalQueue;
|
||||
private ResourceAwareExecutorService resourceAwareExecutorService;
|
||||
|
||||
public AllInScheduleStrategy(ResourceAwareExecutorService resourceAwareExecutorService) {
|
||||
this.globalQueue = resourceAwareExecutorService.queue;
|
||||
this.resourceAwareExecutorService = resourceAwareExecutorService;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public synchronized ResourceAwareRunnable getTask() throws InterruptedException {
|
||||
waitForResourceLockComplete();
|
||||
|
||||
// Wait for new task in queue
|
||||
ResourceAwareRunnable runnable = globalQueue.take();
|
||||
while (!runnable.getLocks().isReady()) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
runnable.getLocks().setCallback(latch::countDown);
|
||||
// Prevent situations where dependencies were resolved
|
||||
// while we were setting up the callback
|
||||
if (runnable.getLocks().isReady())
|
||||
continue;
|
||||
latch.await();
|
||||
}
|
||||
|
||||
// Lock execution for the next thread
|
||||
// (wait until resources for previous task will be blocked)
|
||||
resourceAwareExecutorService.waitForLock.set(true);
|
||||
return runnable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the last scheduled task will lock all the necessary resources.
|
||||
* <p>
|
||||
* It is required to avoid race-condition when an execution candidate task (first task in a queue)
|
||||
* skips lock-await procedure due to the last scheduled task not having locked necessary resources yet.
|
||||
*
|
||||
* @throws InterruptedException if current thread is interrupted
|
||||
*/
|
||||
private void waitForResourceLockComplete() throws InterruptedException {
|
||||
synchronized (resourceAwareExecutorService.waitForLock) {
|
||||
while (resourceAwareExecutorService.waitForLock.get()) {
|
||||
resourceAwareExecutorService.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package mc.core.events.runner;
|
||||
|
||||
/**
|
||||
* Worker thread for {@link ResourceAwareExecutorService}.
|
||||
* <p>
|
||||
* - Awaits for tasks from {@link ScheduleStrategy}
|
||||
* - Locks up resources for this task
|
||||
* - Notifies {@link ScheduleStrategy} when resource-locking procedure is complete
|
||||
* - Executes the runnable in this thread
|
||||
* - Unlocks all the resources
|
||||
* - Calls {@link ResourceAwareRunnable#after()} callback
|
||||
*/
|
||||
public class ExecutorWorkerThread extends Thread {
|
||||
private ResourceAwareExecutorService service;
|
||||
|
||||
public ExecutorWorkerThread(String name, ResourceAwareExecutorService service) {
|
||||
super(name);
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (!isInterrupted() && isAlive()) {
|
||||
ResourceAwareRunnable runnable;
|
||||
try {
|
||||
runnable = service.getStrategy().getTask();
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
executeTask(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
void executeTask(ResourceAwareRunnable runnable) {
|
||||
runnable.getLocks().lockAll();
|
||||
notifyLockingDone();
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
runnable.getLocks().unlockAll();
|
||||
runnable.getLocks().release();
|
||||
}
|
||||
runnable.after();
|
||||
}
|
||||
|
||||
private void notifyLockingDone() {
|
||||
synchronized (service.waitForLock) {
|
||||
if (service.waitForLock.get()) {
|
||||
service.waitForLock.set(false);
|
||||
service.waitForLock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package mc.core.events.runner;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
||||
/**
|
||||
* Custom implementation of an ExecutorService.
|
||||
*
|
||||
* Holds a queue of {@link ResourceAwareRunnable} and executes them in a thread pool.
|
||||
*
|
||||
* Warning! This class doesn't guarantee, that tasks will be executed in any specific order.
|
||||
* In fact, it's up to {@link ScheduleStrategy} to decide which task will be scheduled for
|
||||
* execution next.
|
||||
*/
|
||||
public class ResourceAwareExecutorService {
|
||||
private static final boolean WORKER_INSTANT_EXECUTE = false;
|
||||
BlockingQueue<ResourceAwareRunnable> queue = new ArrayBlockingQueue<>(100);
|
||||
// A synchronize aid, that prevents ScheduleStrategy from returning
|
||||
// wrong tasks when executor is late in blocking resources
|
||||
final AtomicBoolean waitForLock = new AtomicBoolean(false);
|
||||
private ScheduleStrategy strategy = new AllInScheduleStrategy(this);
|
||||
private Set<Thread> executorThreads = new HashSet<>();
|
||||
private int threadCount;
|
||||
|
||||
public ResourceAwareExecutorService(int threadCount) {
|
||||
this.threadCount = threadCount;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (executorThreads.size() > 0)
|
||||
throw new RuntimeException("This executor service was already started.");
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
Thread thread = new ExecutorWorkerThread("Event Loop #" + i, this);
|
||||
executorThreads.add(thread);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (executorThreads.size() == 0)
|
||||
throw new RuntimeException("This executor service was not initialized yet.");
|
||||
|
||||
Iterator<Thread> iterator = executorThreads.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Thread thread = iterator.next();
|
||||
thread.interrupt();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public void addTask(ResourceAwareRunnable task) {
|
||||
if (WORKER_INSTANT_EXECUTE && Thread.currentThread() instanceof ExecutorWorkerThread) {
|
||||
((ExecutorWorkerThread) Thread.currentThread()).executeTask(task);
|
||||
} else
|
||||
queue.offer(task);
|
||||
}
|
||||
|
||||
|
||||
public ScheduleStrategy getStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
private class DefaultScheduleStrategy implements ScheduleStrategy {
|
||||
public ResourceAwareRunnable getTask() throws InterruptedException {
|
||||
return queue.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package mc.core.events.runner;
|
||||
|
||||
public interface ScheduleStrategy {
|
||||
ResourceAwareRunnable getTask() throws InterruptedException;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package mc.core.events.runner.lock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class LockObserveList implements Consumer<PoorMansLock> {
|
||||
public static LockObserveList EMPTY_LIST = new LockObserveList();
|
||||
private List<PoorMansLock> locks = new ArrayList<>();
|
||||
private Runnable callback;
|
||||
|
||||
public void setCallback(Runnable callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void add(PoorMansLock lock) {
|
||||
locks.add(lock);
|
||||
lock.addCallback(this);
|
||||
}
|
||||
|
||||
public void addAll(Iterable<PoorMansLock> locks) {
|
||||
for (PoorMansLock lock : locks)
|
||||
add(lock);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
callback = null;
|
||||
for (PoorMansLock lock : locks) {
|
||||
lock.removeCallback(this);
|
||||
}
|
||||
locks.clear();
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
for (PoorMansLock lock : locks) {
|
||||
if (lock.isLocked())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void lockAll() {
|
||||
for (PoorMansLock lock : locks)
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
public void unlockAll() {
|
||||
for (PoorMansLock lock : locks)
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(PoorMansLock lock) {
|
||||
if (!lock.isLocked()) {
|
||||
if (isReady()) {
|
||||
if (callback != null)
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package mc.core.events.runner.lock;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class PoorMansLock {
|
||||
private Thread owner = null;
|
||||
private Set<Consumer<PoorMansLock>> callbacks = new CopyOnWriteArraySet<>();
|
||||
|
||||
public void addCallback(Consumer<PoorMansLock> callback) {
|
||||
callbacks.add(callback);
|
||||
}
|
||||
|
||||
public void removeCallback(Consumer<PoorMansLock> callback) {
|
||||
callbacks.remove(callback);
|
||||
}
|
||||
|
||||
|
||||
public boolean isLocked() {
|
||||
return owner != null;
|
||||
}
|
||||
|
||||
private void triggerUpdate() {
|
||||
for (Consumer<PoorMansLock> consumer : callbacks)
|
||||
consumer.accept(this);
|
||||
}
|
||||
|
||||
public synchronized void lock() {
|
||||
if(owner == Thread.currentThread())
|
||||
return;
|
||||
|
||||
if (owner != null) {
|
||||
throw new RuntimeException("Unable to lock this resource: already in use");
|
||||
}
|
||||
|
||||
owner = Thread.currentThread();
|
||||
triggerUpdate();
|
||||
}
|
||||
|
||||
public synchronized void unlock() {
|
||||
if (owner == null)
|
||||
return;
|
||||
|
||||
if (owner != Thread.currentThread()) {
|
||||
throw new RuntimeException("Attempt to unlock resource from non-owning thread");
|
||||
}
|
||||
|
||||
owner = null;
|
||||
triggerUpdate();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package mc.core.timings;
|
||||
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class ThreadTimings {
|
||||
private static AtomicInteger IDS = new AtomicInteger();
|
||||
private int threadId;
|
||||
private Stack<Timings> stack = new Stack<>();
|
||||
|
||||
public ThreadTimings() {
|
||||
this.threadId = IDS.getAndIncrement();
|
||||
}
|
||||
|
||||
public Stack<Timings> getStack() {
|
||||
return stack;
|
||||
}
|
||||
|
||||
public int getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public Timings start() {
|
||||
Timings timings = new Timings(this, stack.size());
|
||||
getTimingsManager().waitForTimingsInitialize();
|
||||
stack.push(timings);
|
||||
getTimingsManager().notifyTimings(this, timings, true);
|
||||
return timings;
|
||||
}
|
||||
|
||||
private TimingsManager getTimingsManager() {
|
||||
return Timings.getTimingsManager();
|
||||
}
|
||||
|
||||
public void end(Timings finished) {
|
||||
Timings timings = null;
|
||||
while (!stack.isEmpty() && timings != finished) {
|
||||
getTimingsManager().waitForTimingsInitialize();
|
||||
timings = stack.pop();
|
||||
if (!timings.hasFinished())
|
||||
timings.finish();
|
||||
getTimingsManager().notifyTimings(this, timings, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package mc.core.timings;
|
||||
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import mc.core.timings.io.DefaultWriterFactory;
|
||||
import mc.core.timings.io.TimingsWriter;
|
||||
import mc.core.timings.io.TimingsWriterFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
public class TimingsManager {
|
||||
private final Map<Thread, ThreadTimings> threadTimings = new ConcurrentHashMap<>();
|
||||
// These variables are essential in Timings thread synchronization
|
||||
private final AtomicBoolean waitForFile = new AtomicBoolean(false);
|
||||
private TimingsWriter writer;
|
||||
private Thread timingsIoThread;
|
||||
private CountDownLatch ioThreadStopMutex;
|
||||
private BlockingQueue<TimingsRecord> queue;
|
||||
private ReentrantLock queueAccessLock = new ReentrantLock();
|
||||
// For modularity purposes
|
||||
@Autowired
|
||||
@Setter
|
||||
private TimingsWriterFactory writerFactory = new DefaultWriterFactory();
|
||||
|
||||
public TimingsManager() {
|
||||
TimingsStaticAccessor.TIMINGS_MANAGER = this;
|
||||
}
|
||||
|
||||
public void startRecording(File file) {
|
||||
synchronized (waitForFile) {
|
||||
waitForFile.set(true);
|
||||
}
|
||||
try {
|
||||
writer = writerFactory.newInstance(file);
|
||||
writer.writeEvent(0, 0, System.nanoTime(), TimingsEventType.TIMINGS_FILE_INITIALIZING);
|
||||
// Synchronize current thread state
|
||||
for (Map.Entry<Thread, ThreadTimings> pair : threadTimings.entrySet()) {
|
||||
writer.writeEvent(pair.getValue().getThreadId(), 0, System.nanoTime(), TimingsEventType.TIMINGS_CHANGE_THREAD_OPTIONS, "name: " + pair.getKey().getName());
|
||||
for (Timings timings : pair.getValue().getStack()) {
|
||||
writer.writeEvent(pair.getValue().getThreadId(), timings.getId(), timings.getAcquireTime(), TimingsEventType.TIMINGS_START);
|
||||
}
|
||||
}
|
||||
writer.writeEvent(0, 0, System.nanoTime(), TimingsEventType.TIMINGS_FILE_INITIALIZED);
|
||||
queue = new ArrayBlockingQueue<>(200);
|
||||
ioThreadStopMutex = new CountDownLatch(1);
|
||||
timingsIoThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (!isInterrupted() && isAlive()) {
|
||||
TimingsRecord record;
|
||||
try {
|
||||
if (queue == null)
|
||||
return;
|
||||
record = queue.take();
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
record.writeToFile(writer);
|
||||
}
|
||||
} finally {
|
||||
ioThreadStopMutex.countDown();
|
||||
}
|
||||
}
|
||||
};
|
||||
timingsIoThread.setName("Timings IO thread");
|
||||
timingsIoThread.start();
|
||||
} catch (Exception e) {
|
||||
log.error("Unable to start timings recording", e);
|
||||
}
|
||||
synchronized (waitForFile) {
|
||||
waitForFile.set(false);
|
||||
waitForFile.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void stopRecording() {
|
||||
// Disable write queue
|
||||
queueAccessLock.lock();
|
||||
queue = null;
|
||||
queueAccessLock.unlock();
|
||||
// Interrupt thread and wait until in finished writing the last task
|
||||
timingsIoThread.interrupt();
|
||||
try {
|
||||
ioThreadStopMutex.await();
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Unable to wait until last record would be written to file", e);
|
||||
}
|
||||
// Write EOF event
|
||||
writer.writeEvent(0, 0, System.nanoTime(), TimingsEventType.TIMINGS_FILE_END);
|
||||
// Unload file
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
log.error("Unable to close timings file", e);
|
||||
}
|
||||
writer = null;
|
||||
}
|
||||
|
||||
void notifyTimings(ThreadTimings thread, Timings timings, boolean start) {
|
||||
if (queue == null)
|
||||
return;
|
||||
queueAccessLock.lock();
|
||||
try {
|
||||
if (queue != null)
|
||||
queue.offer(
|
||||
new TimingsRecord(thread.getThreadId(),
|
||||
timings.getId(),
|
||||
start ? timings.getAcquireTime() : timings.getEndTime(),
|
||||
start ? TimingsEventType.TIMINGS_START : TimingsEventType.TIMINGS_END
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
queueAccessLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void waitForTimingsInitialize() {
|
||||
synchronized (waitForFile) {
|
||||
while (waitForFile.get()) {
|
||||
try {
|
||||
waitForFile.wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public ThreadTimings getCurrentThreadTimings() {
|
||||
|
||||
synchronized (this.threadTimings) {
|
||||
if (this.threadTimings.containsKey(Thread.currentThread())) {
|
||||
return this.threadTimings.get(Thread.currentThread());
|
||||
} else {
|
||||
ThreadTimings timings = new ThreadTimings();
|
||||
this.threadTimings.put(Thread.currentThread(), timings);
|
||||
if (queue != null) {
|
||||
try {
|
||||
writer.writeEvent(timings.getThreadId(), 0, System.nanoTime(), TimingsEventType.TIMINGS_CHANGE_THREAD_OPTIONS, "name: " + Thread.currentThread().getName());
|
||||
} catch (NullPointerException ignored) {
|
||||
// It means that there the file recording was stopped
|
||||
// we don't actually care about it
|
||||
}
|
||||
}
|
||||
return timings;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package mc.core.timings.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface TimingsWriterFactory {
|
||||
TimingsWriter newInstance(File file) throws IOException;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package mc.core.events;
|
||||
|
||||
import mc.core.events.api.EventHandler;
|
||||
import mc.core.events.api.EventPriority;
|
||||
import mc.core.events.api.EventQueueOwner;
|
||||
import mc.core.events.api.Plugin;
|
||||
import mc.core.events.runner.ResourceAwareExecutorService;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public class EventLoopTest {
|
||||
|
||||
@Test
|
||||
public void basicTest() throws InterruptedException {
|
||||
Plugin plugin = new Plugin() {
|
||||
};
|
||||
|
||||
EventQueueOwner queueOwner = new EventQueueOwner() {
|
||||
};
|
||||
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
FullAsyncEventLoop eventLoop = new FullAsyncEventLoop();
|
||||
eventLoop.addEventHandler(plugin, new Object() {
|
||||
@EventHandler
|
||||
public void onLoginEvent(LoginEvent event) {
|
||||
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||
service.start();
|
||||
|
||||
eventLoop.setResourceAwareExecutorService(service);
|
||||
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
Assert.assertEquals("Event was not called", 0, latch.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void consecutiveExecutionTest() throws InterruptedException {
|
||||
Plugin plugin = new Plugin() {
|
||||
};
|
||||
|
||||
EventQueueOwner queueOwner = new EventQueueOwner() {
|
||||
};
|
||||
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(2);
|
||||
FullAsyncEventLoop eventLoop = new FullAsyncEventLoop();
|
||||
eventLoop.addEventHandler(plugin, new Object() {
|
||||
@EventHandler
|
||||
public void onLoginEvent(LoginEvent event) {
|
||||
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||
service.start();
|
||||
|
||||
eventLoop.setResourceAwareExecutorService(service);
|
||||
|
||||
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
Assert.assertEquals("Event was not called", 0, latch.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prioritySystemTest() throws InterruptedException {
|
||||
Plugin plugin = new Plugin() {
|
||||
};
|
||||
|
||||
EventQueueOwner queueOwner = new EventQueueOwner() {
|
||||
};
|
||||
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(3);
|
||||
FullAsyncEventLoop eventLoop = new FullAsyncEventLoop();
|
||||
List<Integer> priorities = new ArrayList<>(3);
|
||||
|
||||
eventLoop.addEventHandler(plugin, new Object() {
|
||||
@EventHandler(priority = EventPriority.NORMAL)
|
||||
public void login1(LoginEvent event) {
|
||||
priorities.add(0);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void login2(LoginEvent event) {
|
||||
priorities.add(1);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void login3(LoginEvent event) {
|
||||
priorities.add(2);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||
service.start();
|
||||
|
||||
eventLoop.setResourceAwareExecutorService(service);
|
||||
|
||||
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
Assert.assertEquals("Incorrect call sequence", "[2, 0, 1]", priorities.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoreCancelledTest() throws InterruptedException {
|
||||
Plugin plugin = new Plugin() {
|
||||
};
|
||||
|
||||
EventQueueOwner queueOwner = new EventQueueOwner() {
|
||||
};
|
||||
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
FullAsyncEventLoop eventLoop = new FullAsyncEventLoop();
|
||||
List<Integer> priorities = new ArrayList<>(2);
|
||||
|
||||
eventLoop.addEventHandler(plugin, new Object() {
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
public void login1(LoginEvent event) {
|
||||
priorities.add(0);
|
||||
event.setCanceled(true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void login2(LoginEvent event) {
|
||||
priorities.add(1);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
public void login3(LoginEvent event) {
|
||||
priorities.add(2);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void monitor(LoginEvent event) {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
ResourceAwareExecutorService service = new ResourceAwareExecutorService(1);
|
||||
service.start();
|
||||
|
||||
eventLoop.setResourceAwareExecutorService(service);
|
||||
|
||||
eventLoop.asyncFireEvent(queueOwner, new LoginEvent(null));
|
||||
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
Assert.assertEquals("Incorrect call sequence", "[2, 0]", priorities.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
### System properties:
|
||||
|
||||
* `worlds.folder` -- folder where worlds will be located
|
||||
@@ -1,7 +0,0 @@
|
||||
group 'mc'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
dependencies {
|
||||
compile_excludeCopy project(':core')
|
||||
testCompile group: 'junit', name: 'junit', version: '4.12'
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package mc.world.generated_world;
|
||||
|
||||
public final class WorldConstants {
|
||||
|
||||
public static final boolean DEBUG_ENABLED = true;
|
||||
|
||||
public static final String CHUNK_FILE_NAME_TEMPLATE = "chunk_{0}_{1}_{2}.dat";
|
||||
public static final String BIOME_FILE_NAME_TEMPLATE = "biomes.dat";
|
||||
public static final String WORLD_INFO_FILE_NAME_TEMPLATE = "world.dat";
|
||||
public static final String REGION_FILE_NAME_TEMPLATE = "r.{0}.{1}";
|
||||
|
||||
public static final int WORLD_MAX_HEIGHT = 256;
|
||||
public static final int WORLD_SEA_LEVEL = 64;
|
||||
public static final int WORLD_MIN_GENERATION_HEIGHT = 36;
|
||||
public static final int WORLD_MAX_GENERATION_HEIGHT = 128;
|
||||
public static final int WORLD_REGION_SIZE = 256;
|
||||
public static final int WORLD_CHUNK_SIZE = 16;
|
||||
public static final int WORLD_MAX_TEMPERATURE = 100;
|
||||
public static final int WORLD_MAX_WETNESS = 100;
|
||||
public static final int WORLD_BASE_WETNESS = 80;
|
||||
|
||||
public static final double WORLD_LAND_SIZE = 63.03;
|
||||
public static final double WORLD_LAKE_SIZE = 9.3;
|
||||
public static final double WORLD_WET_SEA_PERCENT = 0.8;
|
||||
public static final double WORLD_TEMPERATURE_SIZE = 41.0;
|
||||
public static final double WORLD_TEMPERATURE_ZONE_SIZE = 2.99;
|
||||
public static final double WORLD_TEMPERATURE_HEIGHT_GRAD_SIZE = 1.1;
|
||||
|
||||
public static final int LANDFILL_GRASS_SURFACE_THIN = 5;
|
||||
public static final double WORLD_ROUGHNESS = 0.35;
|
||||
|
||||
private WorldConstants () {}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package mc.world.generated_world.chunk;
|
||||
|
||||
import lombok.Getter;
|
||||
import mc.core.exception.ResourceUnloadedException;
|
||||
import mc.core.world.chunk.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;
|
||||
|
||||
public class ChunkImpl implements Chunk {
|
||||
@Getter
|
||||
private final int x;
|
||||
@Getter
|
||||
private final int z;
|
||||
private Reference<Region> 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 World getWorld() {
|
||||
Region region = getRegion();
|
||||
if (region == null) {
|
||||
throw new ResourceUnloadedException("Region is unloaded");
|
||||
}
|
||||
return region.getWorld();
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
if (regionReference.get() == null) {
|
||||
throw new ResourceUnloadedException("Region is unloaded");
|
||||
}
|
||||
|
||||
return regionReference.get();
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package mc.world.generated_world.chunk;
|
||||
|
||||
import lombok.Getter;
|
||||
import mc.core.exception.ResourceUnloadedException;
|
||||
import mc.core.world.Biome;
|
||||
import mc.core.world.chunk.ChunkSection;
|
||||
import mc.core.world.Region;
|
||||
import mc.core.world.World;
|
||||
import mc.core.world.block.Block;
|
||||
import mc.core.world.block.BlockFactory;
|
||||
import mc.core.world.block.BlockType;
|
||||
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
||||
|
||||
public class ChunkSectionImpl implements ChunkSection {
|
||||
@Getter
|
||||
private final int x;
|
||||
@Getter
|
||||
private final int y;
|
||||
@Getter
|
||||
private final int z;
|
||||
private final Block[][][] blocks = new Block[WORLD_CHUNK_SIZE][WORLD_CHUNK_SIZE][WORLD_CHUNK_SIZE];
|
||||
private final transient Reference<Region> region;
|
||||
private BlockFactory blockFactory = new BlockFactory();
|
||||
|
||||
public ChunkSectionImpl(int x, int y, int z, Region region) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.region = new WeakReference<>(region);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSkyLight(int x, int y, int z) {
|
||||
return 15;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSkyLight(int x, int y, int z, int lightLevel) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAddition(int x, int y, int z) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAddition(int x, int y, int z, int value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiome(int x, int z) {
|
||||
return getRegion().getBiomeAt(x + this.x * WORLD_CHUNK_SIZE,z + this.z * WORLD_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome biome) {
|
||||
getRegion().setBiome(x + this.x * WORLD_CHUNK_SIZE,z + this.z * WORLD_CHUNK_SIZE, biome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlock(Block block) {
|
||||
if (block.getBlockType() == BlockType.AIR) {
|
||||
blocks[block.getLocation().getBlockX()][block.getLocation().getBlockY()][block.getLocation().getBlockZ()] = null;
|
||||
return;
|
||||
}
|
||||
blocks[block.getLocation().getBlockX()][block.getLocation().getBlockY()][block.getLocation().getBlockZ()] = block;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block getBlock(int x, int y, int z) {
|
||||
Block block = blocks[x][y][z];
|
||||
if (block == null) {
|
||||
return blockFactory.create(BlockType.AIR, 0, x, y, z);
|
||||
}
|
||||
return blocks[x][y][z];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Region getRegion() {
|
||||
if (region == null) {
|
||||
return null;
|
||||
}
|
||||
if (region.get() == null) {
|
||||
throw new ResourceUnloadedException("Region is unloaded");
|
||||
}
|
||||
return region.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public World getWorld() {
|
||||
return getRegion().getWorld();
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package mc.world.generated_world.chunk;
|
||||
|
||||
import mc.core.world.chunk.ChunkSection;
|
||||
import mc.core.world.Region;
|
||||
import mc.core.world.World;
|
||||
import mc.core.world.block.Block;
|
||||
import mc.core.world.Biome;
|
||||
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class ChunkSectionProxy implements ChunkSection {
|
||||
private final ChunkSection chunk;
|
||||
private volatile transient long lastUsage = System.currentTimeMillis();
|
||||
private final transient ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
|
||||
|
||||
public ChunkSectionProxy(ChunkSection chunk) {
|
||||
this.chunk = chunk;
|
||||
}
|
||||
|
||||
public long getLastUsage() {
|
||||
synchronized (chunk) {
|
||||
return lastUsage;
|
||||
}
|
||||
}
|
||||
|
||||
private final void use () {
|
||||
synchronized (chunk) {
|
||||
lastUsage = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block getBlock(int x, int y, int z) {
|
||||
use();
|
||||
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();
|
||||
chunk.setBlock(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSkyLight(int x, int y, int z) {
|
||||
use();
|
||||
return chunk.getSkyLight(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSkyLight(int x, int y, int z, int lightLevel) {
|
||||
use();
|
||||
chunk.setSkyLight(x, y, z, lightLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAddition(int x, int y, int z) {
|
||||
use();
|
||||
return chunk.getAddition(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAddition(int x, int y, int z, int value) {
|
||||
use();
|
||||
chunk.setAddition(x, y, z, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiome(int x, int z) {
|
||||
use();
|
||||
return chunk.getBiome(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome biome) {
|
||||
use();
|
||||
chunk.setBiome(x, z, biome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getX() {
|
||||
use();
|
||||
return chunk.getX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getY() {
|
||||
use();
|
||||
return chunk.getY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getZ() {
|
||||
use();
|
||||
return chunk.getZ();
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package mc.world.generated_world.chunk;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import mc.core.serialization.Serializer;
|
||||
import mc.core.world.*;
|
||||
import mc.core.world.chunk.ChunkLoader;
|
||||
import mc.core.world.chunk.ChunkSection;
|
||||
import mc.world.generated_world.serialization.ChunkReader;
|
||||
import mc.world.generated_world.serialization.RegionReaderWriter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Optional;
|
||||
|
||||
import static mc.world.generated_world.WorldConstants.*;
|
||||
|
||||
@Slf4j
|
||||
public class InMemoryCacheChunkLoader implements ChunkLoader {
|
||||
|
||||
private final World world;
|
||||
private File worldFolder;
|
||||
@Autowired
|
||||
private WorldGenerator worldGenerator;
|
||||
@Autowired
|
||||
private ChunkReader chunkReader;
|
||||
@Autowired
|
||||
private Serializer<ChunkSection> chunkSerializer;
|
||||
@Autowired
|
||||
private RegionReaderWriter regionReaderWritter;
|
||||
|
||||
public InMemoryCacheChunkLoader(World world) {
|
||||
this.world = world;
|
||||
String worldPath = System.getProperty("worlds.folder", "worlds");
|
||||
worldFolder = new File(worldPath, world.getWorldId().toString());
|
||||
if (!worldFolder.exists()) {
|
||||
log.info("Created folder for world with uuid '{}'", world.getWorldId());
|
||||
worldFolder.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
private File getChuckFile(int x, int y, int z) {
|
||||
return new File(worldFolder, MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ChunkSection> loadChunk(int x, int y, int z) {
|
||||
File file = getChuckFile(x, y, z);
|
||||
if (!file.exists()) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
try {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
ChunkSection chunkSection;
|
||||
if (!regionFile.exists()) {
|
||||
log.debug("Region [{}, {}] not found. Generating!", regX, regZ);
|
||||
regionFile.mkdirs();
|
||||
region = worldGenerator.generateRegion(regX, regZ, world);
|
||||
try {
|
||||
regionReaderWritter.write(region);
|
||||
} catch (IOException e) {
|
||||
log.error("Error occurred while writting biome file", e);
|
||||
}
|
||||
saveRegion(region);
|
||||
chunkSection = region.getChunkAt(x % WORLD_CHUNK_SIZE, y % WORLD_CHUNK_SIZE, z % WORLD_CHUNK_SIZE);
|
||||
} else {
|
||||
try {
|
||||
region = regionReaderWritter.read(regX, regZ, world);
|
||||
chunkSection = chunkReader.read(region, x, y, z);
|
||||
} catch (IOException e) {
|
||||
log.error("Error occurred while reading chunkSection file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return chunkSection;
|
||||
}
|
||||
|
||||
private void saveRegion (Region region) {
|
||||
File file = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, region.getX(), region.getZ()));
|
||||
for (int x = 0; x < WORLD_REGION_SIZE / WORLD_CHUNK_SIZE; x ++) {
|
||||
for (int y = 0; y < WORLD_REGION_SIZE / WORLD_CHUNK_SIZE; y ++) {
|
||||
for (int z = 0; z < WORLD_REGION_SIZE / WORLD_CHUNK_SIZE; z ++) {
|
||||
byte[] chunkBytes = chunkSerializer.serialize(region.getChunkAt(x, y, z));
|
||||
File chunkFile = new File(file, MessageFormat.format(CHUNK_FILE_NAME_TEMPLATE, x, y, z));
|
||||
try (FileOutputStream writer = new FileOutputStream(chunkFile)) {
|
||||
writer.write(chunkBytes);
|
||||
} catch (IOException e) {
|
||||
log.error("Error occurred while writting chunk to file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package mc.world.generated_world.generator;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static mc.world.generated_world.WorldConstants.WORLD_REGION_SIZE;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class NoiseGenerator {
|
||||
private int[] perm = new int[WORLD_REGION_SIZE];
|
||||
private double[] gradsX = new double[WORLD_REGION_SIZE];
|
||||
private double[] gradsY = new double[WORLD_REGION_SIZE];
|
||||
private final int seed;
|
||||
|
||||
void init() {
|
||||
for (int i = 0; i < WORLD_REGION_SIZE; ++i) {
|
||||
int other = rand(i) % (i + 1);
|
||||
if (i > other)
|
||||
perm[i] = perm[other];
|
||||
perm[other] = i;
|
||||
gradsX[i] = Math.cos(2.0f * Math.PI * i / WORLD_REGION_SIZE);
|
||||
gradsY[i] = Math.sin(2.0f * Math.PI * i / WORLD_REGION_SIZE);
|
||||
}
|
||||
log.debug("Noise generator is initialized");
|
||||
}
|
||||
|
||||
double f(double t) {
|
||||
t = Math.abs(t);
|
||||
return t >= 1.0f ? 0.0f : 1.0f -
|
||||
(3.0f - 2.0f * t) * t * t;
|
||||
}
|
||||
|
||||
private double surflet(double x, double y, double gradX, double gradY) {
|
||||
return f(x) * f(y) * (gradX * x + gradY * y);
|
||||
}
|
||||
|
||||
double noise(double x, double y) {
|
||||
float result = 0.0f;
|
||||
int cellX = (int)(x);
|
||||
int cellY = (int)(y);
|
||||
int mask = WORLD_REGION_SIZE - 1;
|
||||
for (int gridY = cellY; gridY <= cellY + 1; ++gridY)
|
||||
for (int gridX = cellX; gridX <= cellX + 1; ++gridX) {
|
||||
int hash = perm[(perm[gridX & mask] + gridY) & mask];
|
||||
result += surflet(x - gridX, y - gridY,
|
||||
gradsX[hash], gradsY[hash]);
|
||||
}
|
||||
return (result + 1) / 2;
|
||||
}
|
||||
|
||||
private int rand(int i) {
|
||||
int x = (i * i) % WORLD_REGION_SIZE;
|
||||
int y = (i + i * x) % WORLD_REGION_SIZE;
|
||||
return (int) (Integer.MAX_VALUE * SeedRandomGenerator.random(x, y, seed));
|
||||
}
|
||||
}
|
||||
@@ -1,392 +0,0 @@
|
||||
package mc.world.generated_world.generator;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.ChunkSection;
|
||||
import mc.world.generated_world.region.RegionImpl;
|
||||
import mc.world.generated_world.world.CubicWorld;
|
||||
import mc.world.generated_world.world.Temperature;
|
||||
import mc.world.generated_world.world.Wetness;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static mc.world.generated_world.WorldConstants.*;
|
||||
|
||||
@Slf4j
|
||||
public class SeedBasedWorldGenerator implements WorldGenerator {
|
||||
|
||||
public static void main(String[] args) throws Exception{
|
||||
WorldGenerator worldGenerator = new SeedBasedWorldGenerator();
|
||||
World world = new CubicWorld(UUID.fromString("00000000-0000-0000-C000-000000000046"), 2626949);
|
||||
/*Region region = worldGenerator.generateRegion(0, 0, world);
|
||||
region.save(new ChunkSerializer(), new RegionReaderWriter(new File("worlds", world.getWorldId().toString())));
|
||||
new WorldReaderWriter(new File("worlds")).writeWorldInfo(world);*/
|
||||
|
||||
createBigImage(worldGenerator, world);
|
||||
}
|
||||
|
||||
private static void createBigImage (WorldGenerator worldGenerator, World world) throws IOException {
|
||||
BufferedImage image = new BufferedImage(3 * 256, 3 * 256, BufferedImage.TYPE_INT_RGB);
|
||||
for (int x = 0; x <= 2; x ++) {
|
||||
for (int z = 0; z <= 2; z ++) {
|
||||
worldGenerator.generateRegion(x - 1, z - 1, world);
|
||||
addToBigImage(x, z, image);
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", new File("out", "merged.png"));
|
||||
}
|
||||
|
||||
private static void addToBigImage (int shiftX, int shiftY, BufferedImage image) throws IOException{
|
||||
BufferedImage currentImage = ImageIO.read(new File("out/" + (shiftX - 1) + "." + (shiftY - 1), "biomeMap.png"));
|
||||
for (int x = 0; x < 256; x ++){
|
||||
for (int y = 0; y < 256; y ++){
|
||||
int tx = 256 * shiftX + x;
|
||||
int ty = 256 * shiftY + y;
|
||||
image.setRGB(tx, ty, currentImage.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Region generateRegion(int x, int z, World world) {
|
||||
log.info("Generating region [{},{}]...", x, z);
|
||||
Region region = new RegionImpl(x, z, world);
|
||||
RegionGenerator regionGenerator = new RegionGenerator(world, region);
|
||||
regionGenerator.generate();
|
||||
log.info("Region [{},{}] is generated", x, z);
|
||||
return region;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private class RegionGenerator {
|
||||
private final World world;
|
||||
private final Region region;
|
||||
private NoiseGenerator noiseGenerator;
|
||||
private BlockFactory blockFactory = new BlockFactory();
|
||||
|
||||
private double sigmoid (double x) {
|
||||
x -= 0.5;
|
||||
x *= 15;
|
||||
return 1.0 / (1.0 + Math.exp(-x));
|
||||
}
|
||||
|
||||
private int convert (int x) {
|
||||
return 40960 + x;
|
||||
}
|
||||
|
||||
private void generate() {
|
||||
log.debug("Starting generating region [{}, {}] for world '{}' with seed '{}'", region.getX(), region.getZ(), world.getWorldId(), world.getSeed());
|
||||
|
||||
noiseGenerator = new NoiseGenerator(world.getSeed());
|
||||
noiseGenerator.init();
|
||||
|
||||
int[][] heightMap = new int[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||
int[][] grassMap = new int[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||
int[][] temperatureMap = new int[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||
int[][] wetMap = new int[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||
Biome[][] biomes = new Biome[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||
|
||||
for (int x = 0; x < WORLD_REGION_SIZE; x ++) {
|
||||
for (int z = 0; z < WORLD_REGION_SIZE; z ++) {
|
||||
int tx = convert(x + region.getX() * WORLD_REGION_SIZE);
|
||||
int tz = convert(z + region.getZ() * WORLD_REGION_SIZE);
|
||||
double p = sigmoid(noiseGenerator.noise(tx / WORLD_LAND_SIZE, tz / WORLD_LAND_SIZE));
|
||||
double r = Math.sqrt(noiseGenerator.noise(tx / WORLD_LAKE_SIZE, tz / WORLD_LAKE_SIZE));
|
||||
double h = (WORLD_MAX_GENERATION_HEIGHT - WORLD_MIN_GENERATION_HEIGHT) * Math.min(p * r, 1);
|
||||
h = Math.min(WORLD_MAX_GENERATION_HEIGHT, h + WORLD_MIN_GENERATION_HEIGHT);
|
||||
heightMap[x][z] = (int)(h);
|
||||
grassMap[x][z] = (int) (1 + SeedRandomGenerator.random(tx, tz, world.getSeed()) * (LANDFILL_GRASS_SURFACE_THIN - 1));
|
||||
double k = Math.sqrt(noiseGenerator.noise(tx * WORLD_TEMPERATURE_ZONE_SIZE, tz * WORLD_TEMPERATURE_ZONE_SIZE));
|
||||
double q = Math.sqrt(noiseGenerator.noise(tx / WORLD_TEMPERATURE_SIZE, tz / WORLD_TEMPERATURE_SIZE));
|
||||
temperatureMap[x][z] = (int) (WORLD_MAX_TEMPERATURE * Math.min((k * k + q * q + k * q) * k * q, 0.99));
|
||||
if (heightMap[x][z] < WORLD_SEA_LEVEL) {
|
||||
biomes[x][z] = Biome.OCEAN;
|
||||
wetMap[x][z] = (int) (WORLD_MAX_WETNESS * WORLD_WET_SEA_PERCENT *noiseGenerator.noise(tx, tz));
|
||||
} else {
|
||||
int th = heightMap[x][z] - WORLD_SEA_LEVEL;
|
||||
th = (int) (th * (1 + 1.25 * th / (WORLD_MAX_GENERATION_HEIGHT - WORLD_SEA_LEVEL)));
|
||||
heightMap[x][z] = Math.min(WORLD_SEA_LEVEL + th, WORLD_MAX_GENERATION_HEIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int x = 1; x < WORLD_REGION_SIZE - 1; x ++) {
|
||||
for (int z = 1; z < WORLD_REGION_SIZE - 1; z++) {
|
||||
int mid = 0;
|
||||
for (int tx = x - 1; tx <= x + 1; tx ++) {
|
||||
for (int tz = z - 1; tz <= z + 1; tz ++) {
|
||||
mid += wetMap[tx][tz];
|
||||
}
|
||||
}
|
||||
wetMap[x][z] = mid / 9;
|
||||
}
|
||||
}
|
||||
for (int z = 1; z < WORLD_REGION_SIZE - 1; z++) {
|
||||
for (int x = 1; x < WORLD_REGION_SIZE - 1; x ++) {
|
||||
int mid = 0;
|
||||
for (int tx = x - 1; tx <= x + 1; tx ++) {
|
||||
for (int tz = z - 1; tz <= z + 1; tz ++) {
|
||||
mid += wetMap[tx][tz];
|
||||
}
|
||||
}
|
||||
wetMap[x][z] = (int) (mid / 9 * (1 + 0.4 * SeedRandomGenerator.random(x, z, world.getSeed())));
|
||||
temperatureMap[x][z] = (int) Math.min(Math.max(temperatureMap[x][z] - WORLD_TEMPERATURE_HEIGHT_GRAD_SIZE * SeedRandomGenerator.random(x, z, world.getSeed()) * (heightMap[x][z] - WORLD_SEA_LEVEL), 0), WORLD_MAX_TEMPERATURE);
|
||||
}
|
||||
}
|
||||
|
||||
for (int z = 1; z < WORLD_REGION_SIZE - 1; z++) {
|
||||
for (int x = 1; x < WORLD_REGION_SIZE - 1; x ++) {
|
||||
wetMap[x][z] = (int) Math.min(WORLD_MAX_WETNESS, WORLD_BASE_WETNESS * noiseGenerator.noise(x / 31d, z / 31d) + wetMap[x][z] * (1 + 0.2 * (SeedRandomGenerator.random(x, z, world.getSeed()))));
|
||||
}
|
||||
}
|
||||
|
||||
smooth(grassMap);
|
||||
smooth(temperatureMap);
|
||||
smooth(wetMap);
|
||||
//smooth(heightMap);
|
||||
|
||||
// ================================ DEBUG =======================================
|
||||
if (DEBUG_ENABLED) {
|
||||
log.debug("Creating debug images");
|
||||
File outFile;
|
||||
outFile = new File("out", region.getX() + "." + region.getZ());
|
||||
outFile.mkdirs();
|
||||
BufferedImage tempImg = new BufferedImage(WORLD_REGION_SIZE, WORLD_REGION_SIZE, BufferedImage.TYPE_INT_RGB);
|
||||
BufferedImage wetImg = new BufferedImage(WORLD_REGION_SIZE, WORLD_REGION_SIZE, BufferedImage.TYPE_INT_RGB);
|
||||
for (int x = 0; x < WORLD_REGION_SIZE; x ++) {
|
||||
for (int z = 0; z < WORLD_REGION_SIZE; z ++) {
|
||||
Temperature temperature = Temperature.values()[Temperature.values().length * temperatureMap[x][z] / WORLD_MAX_TEMPERATURE];
|
||||
Wetness wetness = Wetness.values()[ Wetness.values().length * (Math.min(wetMap[x][z], WORLD_MAX_WETNESS) - 1) / WORLD_MAX_WETNESS];
|
||||
biomes[x][z] = selectBiome(temperature, wetness, heightMap[x][z]);
|
||||
tempImg.setRGB(x, z, temperature.ordinal() * 0xff / Temperature.values().length);
|
||||
wetImg.setRGB(x, z, wetness.ordinal() * 0xff / Wetness.values().length);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ImageIO.write(tempImg, "png", new File(outFile, "temp_img.png"));
|
||||
ImageIO.write(wetImg, "png", new File(outFile, "wet_img.png"));
|
||||
|
||||
BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||
BufferedImage subImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||
for (int x = 0; x < 256; x++) {
|
||||
for (int z = 0; z < 256; z++) {
|
||||
int h = heightMap[x][z];
|
||||
h = h << 16 | h << 8 | h;
|
||||
image.setRGB(x, z, h);
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", new File(outFile, "heightmap.png"));
|
||||
image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||
for (int x = 0; x < 256; x++) {
|
||||
for (int z = 0; z < 256; z++) {
|
||||
int temp = 0xff * temperatureMap[x][z] / 100;
|
||||
temp = temp << 16;
|
||||
image.setRGB(x, z, temp);
|
||||
subImage.setRGB(x, z, (0xff * (int) (temperatureMap[x][z] / 20) / 5) << 16);
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", new File(outFile, "temperatureMap.png"));
|
||||
ImageIO.write(subImage, "png", new File(outFile, "reg_temperatureMap.png"));
|
||||
subImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||
image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||
for (int x = 0; x < 256; x++) {
|
||||
for (int z = 0; z < 256; z++) {
|
||||
int wet = 0xff * wetMap[x][z] / 100;
|
||||
image.setRGB(x, z, wet);
|
||||
subImage.setRGB(x, z, 0xff * (int) (Wetness.values().length * wetMap[x][z] / (WORLD_MAX_WETNESS)));
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", new File(outFile, "wetMap.png"));
|
||||
ImageIO.write(subImage, "png", new File(outFile, "reg_wetMap.png"));
|
||||
image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
|
||||
for (int x = 0; x < 256; x++) {
|
||||
for (int z = 0; z < 256; z++) {
|
||||
image.setRGB(x, z, biomes[x][z].getColor());
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", new File(outFile, "biomeMap.png"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error occurred while creating debug images", e);
|
||||
}
|
||||
}
|
||||
// ================================ DEBUG FINISH =======================================
|
||||
|
||||
log.debug("Creating chunks...");
|
||||
|
||||
for (int x = 0; x < WORLD_REGION_SIZE; x ++) {
|
||||
for (int z = 0; z < WORLD_REGION_SIZE; z ++) {
|
||||
region.setBiome(x, z, biomes[x][z]);
|
||||
if (heightMap[x][z] < WORLD_SEA_LEVEL) {
|
||||
for (int y = 0; y < WORLD_SEA_LEVEL; y ++) {
|
||||
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;
|
||||
}
|
||||
if (y < heightMap[x][z]) {
|
||||
if (y < heightMap[x][z] - grassMap[x][z]) {
|
||||
chunk.setBlock(blockFactory.create(BlockType.STONE, 0, x % 16, y % 16, z % 16));
|
||||
} else {
|
||||
chunk.setBlock(blockFactory.create(BlockType.SAND, 0, x % 16, y % 16, z % 16));
|
||||
}
|
||||
} else {
|
||||
chunk.setBlock(blockFactory.create(BlockType.WATER, 0, x % 16, y % 16, z % 16));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int y = 0; y < heightMap[x][z]; y++) {
|
||||
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;
|
||||
}
|
||||
if (y < heightMap[x][z] - grassMap[x][z]) {
|
||||
chunk.setBlock(blockFactory.create(BlockType.STONE, 0, x % 16, y % 16, z % 16));
|
||||
} else {
|
||||
if (biomes[x][z] == Biome.DESERT || biomes[x][z] == Biome.DESERT_HILLS) {
|
||||
chunk.setBlock(blockFactory.create(BlockType.SAND, 0, x % 16, y % 16, z % 16));
|
||||
} else if (biomes[x][z] == Biome.TAIGA || biomes[x][z] == Biome.TAIGA_HILLS) {
|
||||
chunk.setBlock(blockFactory.create(BlockType.DIRT, 0, x % 16, y % 16, z % 16));
|
||||
} else {
|
||||
chunk.setBlock(blockFactory.create(BlockType.GRASS, 0, x % 16, y % 16, z % 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO
|
||||
log.debug("Creating rivers...");
|
||||
log.debug("Creating caves...");
|
||||
log.debug("Generating ores...");
|
||||
log.debug("Creating structures...");
|
||||
log.debug("Planting trees...");
|
||||
log.debug("Spawning animals...");
|
||||
*/
|
||||
}
|
||||
|
||||
private Biome selectBiome (Temperature temperature, Wetness wetness, int height) {
|
||||
|
||||
if (wetness == Wetness.WATER || height < WORLD_SEA_LEVEL) {
|
||||
if (temperature == Temperature.FROST) {
|
||||
if (height < WORLD_SEA_LEVEL) {
|
||||
return Biome.FROZEN_OCEAN;
|
||||
} else {
|
||||
return Biome.ICE_PLAINS;
|
||||
}
|
||||
} else {
|
||||
if (height < WORLD_SEA_LEVEL) {
|
||||
if (height < WORLD_MIN_GENERATION_HEIGHT + (WORLD_SEA_LEVEL - WORLD_MIN_GENERATION_HEIGHT) / 2) {
|
||||
return Biome.DEEP_OCEAN;
|
||||
} else {
|
||||
return Biome.OCEAN;
|
||||
}
|
||||
} else {
|
||||
return Biome.SWAMPLAND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final int HILLS_HEIGHT = WORLD_SEA_LEVEL + (WORLD_MAX_GENERATION_HEIGHT - WORLD_SEA_LEVEL) / 3;
|
||||
|
||||
if (temperature == Temperature.FROST) {
|
||||
if (wetness == Wetness.DRIEST || wetness == Wetness.DRY) {
|
||||
return Biome.COLD_TAIGA;
|
||||
} else {
|
||||
if (height > HILLS_HEIGHT) {
|
||||
return Biome.ICE_MOUNTAINS;
|
||||
} else {
|
||||
return Biome.ICE_PLAINS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wetness == Wetness.DRIEST) {
|
||||
if (temperature == Temperature.COLD || temperature == Temperature.WARM) {
|
||||
return Biome.PLAINS;
|
||||
} else {
|
||||
if (height > HILLS_HEIGHT) {
|
||||
return Biome.DESERT_HILLS;
|
||||
} else {
|
||||
return Biome.DESERT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (temperature == Temperature.COLD) {
|
||||
if (wetness == Wetness.DRY || wetness == Wetness.WET) {
|
||||
if (height > HILLS_HEIGHT) {
|
||||
return Biome.TAIGA_HILLS;
|
||||
} else {
|
||||
return Biome.TAIGA;
|
||||
}
|
||||
} else {
|
||||
return Biome.SWAMPLAND;
|
||||
}
|
||||
}
|
||||
|
||||
if (wetness == Wetness.WETTEST) {
|
||||
if (temperature == Temperature.WARM) {
|
||||
return Biome.SWAMPLAND;
|
||||
} else {
|
||||
if (height > HILLS_HEIGHT) {
|
||||
return Biome.JUNGLE_HILLS;
|
||||
} else {
|
||||
return Biome.JUNGLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wetness == Wetness.WETTER) {
|
||||
if (temperature == Temperature.WARM) {
|
||||
if (height > HILLS_HEIGHT) {
|
||||
return Biome.FOREST_HILLS;
|
||||
} else {
|
||||
return Biome.FOREST;
|
||||
}
|
||||
} else {
|
||||
return Biome.SAVANNA_PLATO;
|
||||
}
|
||||
}
|
||||
|
||||
if (temperature == Temperature.HOTTEST) {
|
||||
return Biome.SAVANNA;
|
||||
}
|
||||
|
||||
if (wetness == Wetness.WET) {
|
||||
if (height > HILLS_HEIGHT) {
|
||||
return Biome.FOREST_HILLS;
|
||||
} else {
|
||||
return Biome.FOREST;
|
||||
}
|
||||
}
|
||||
|
||||
return Biome.PLAINS;
|
||||
}
|
||||
|
||||
private void smooth (int [][] map) {
|
||||
final int[][] original = map.clone();
|
||||
for (int y = 1; y < map.length - 1; y ++) {
|
||||
for (int x = 1; x < map[0].length - 1; x ++) {
|
||||
int mid = 0;
|
||||
for (int tx = x - 1; tx <= x + 1; tx ++) {
|
||||
for (int ty = y - 1; ty <= y + 1; ty ++) {
|
||||
mid += original[tx][ty];
|
||||
}
|
||||
}
|
||||
map[x][y] = mid / 9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package mc.world.generated_world.generator;
|
||||
|
||||
public final class SeedRandomGenerator {
|
||||
|
||||
public static double random (int x, int y, int seed) {
|
||||
x = Math.abs(x - y) + 1;
|
||||
y = Math.abs(y - x) + 1;
|
||||
for (int i = 0; i < 20; i ++) {
|
||||
int a1 = x % 13;
|
||||
int a2 = x % 31;
|
||||
int a3 = x % 89;
|
||||
int a4 = y % 359;
|
||||
int a5 = y % 7;
|
||||
int a6 = y % 313;
|
||||
int a7 = y % 8461;
|
||||
int a8 = y % 105467;
|
||||
int a9 = x % 105943;
|
||||
y = x + seed;
|
||||
x += a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9;
|
||||
}
|
||||
return ((x + y) % 100000) / 100000d;
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package mc.world.generated_world.region;
|
||||
|
||||
import lombok.Getter;
|
||||
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.core.world.chunk.ChunkSection;
|
||||
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.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
|
||||
public class RegionImpl implements Region{
|
||||
@Getter
|
||||
private final int x;
|
||||
@Getter
|
||||
private final int z;
|
||||
private final ChunkSection[][][] chunkSectionProxies = new ChunkSectionProxy[WORLD_REGION_SIZE/WORLD_CHUNK_SIZE][WORLD_REGION_SIZE/WORLD_CHUNK_SIZE][WORLD_REGION_SIZE/WORLD_CHUNK_SIZE];
|
||||
private final Biome[][] biomes = new Biome[WORLD_REGION_SIZE][WORLD_REGION_SIZE];
|
||||
private final transient Reference<World> 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 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));
|
||||
}
|
||||
|
||||
Chunk chunk = chunks[x][z];
|
||||
if (chunk == null) {
|
||||
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 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 chunkSection coordinates [{0} {1} {2}]", x, y, z));
|
||||
}
|
||||
if (chunkLoader == null) {
|
||||
chunkLoader = new InMemoryCacheChunkLoader(getWorld());
|
||||
}
|
||||
ChunkSection chunkSection = chunkSectionProxies[x][y][z];
|
||||
if (chunkSection == null) {
|
||||
chunkSection = chunkLoader.loadChunk(x + this.x * WORLD_REGION_SIZE, y, this.z * WORLD_REGION_SIZE).orElse(new ChunkSectionImpl(x, y, z, this));
|
||||
chunkSectionProxies[x][y][z] = new ChunkSectionProxy(chunkSection);
|
||||
}
|
||||
return chunkSection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChunk(int x, int y, int z, ChunkSection chunkSection) {
|
||||
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= 16 || z >= 16) {
|
||||
throw new RuntimeException(MessageFormat.format("Invalid chunkSection coordinates [{0} {1} {2}]", x, y, z));
|
||||
}
|
||||
chunkSectionProxies[x][y][z] = new ChunkSectionProxy(chunkSection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiomeAt(int x, int z) {
|
||||
if (x < 0 || z < 0 || x >= 256 || z >= 256) {
|
||||
throw new RuntimeException(MessageFormat.format("Invalid biome coordinates [{0} {1}]", x, z));
|
||||
}
|
||||
return biomes[x][z];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome biome) {
|
||||
if (x < 0 || z < 0 || x >= 256 || z >= 256) {
|
||||
throw new RuntimeException(MessageFormat.format("Invalid biome coordinates [{0} {1}]", x, z));
|
||||
}
|
||||
biomes[x][z] = biome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public World getWorld() {
|
||||
if (world == null) {
|
||||
return null;
|
||||
}
|
||||
if (world.get() == null) {
|
||||
throw new ResourceUnloadedException("World is unloaded");
|
||||
}
|
||||
return world.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Serializer<ChunkSection> chunkSerializer, IRegionReaderWriter regionReaderWriter) throws IOException {
|
||||
String worldPath = System.getProperty("worlds.folder", "worlds");
|
||||
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();
|
||||
}
|
||||
regionReaderWriter.write(this);
|
||||
for (int x = 0; x < WORLD_CHUNK_SIZE; x ++) {
|
||||
for (int z = 0; z < WORLD_CHUNK_SIZE; z ++) {
|
||||
for (int y = 0; y < WORLD_CHUNK_SIZE; y++) {
|
||||
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)) {
|
||||
fileOutputStream.write(chunkBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package mc.world.generated_world.serialization;
|
||||
|
||||
import mc.core.world.block.Block;
|
||||
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.ChunkSection;
|
||||
|
||||
/**
|
||||
* Prototype
|
||||
*/
|
||||
public class BlockSerializerDeserializer implements Serializer<Block>, Deserializer<Block> {
|
||||
|
||||
private BlockFactory blockFactory;
|
||||
private ChunkSection chunkSection;
|
||||
|
||||
public BlockSerializerDeserializer(BlockFactory blockFactory, ChunkSection chunkSection) {
|
||||
this.blockFactory = blockFactory;
|
||||
this.chunkSection = chunkSection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block deserialize(byte[] bytes) {
|
||||
int id = bytes[0] + 128;
|
||||
int meta = bytes[1] >> 4;
|
||||
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);
|
||||
block.getLocation().setY(y);
|
||||
block.getLocation().setZ(z);
|
||||
return block;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Block block) {
|
||||
byte[] bytes = new byte[3];
|
||||
bytes[0] = (byte) ((block.getId() - 128) & 0xff);
|
||||
bytes[1] = (byte) ((block.getMeta() << 4) | (block.getLocation().getBlockX() % 16));
|
||||
bytes[2] = (byte) (((block.getLocation().getBlockZ() % 16) << 4) | (block.getLocation().getBlockZ() % 16));
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package mc.world.generated_world.serialization;
|
||||
|
||||
import mc.core.world.block.Block;
|
||||
import mc.core.serialization.Deserializer;
|
||||
import mc.core.serialization.IChunkReader;
|
||||
import mc.core.world.chunk.ChunkSection;
|
||||
import mc.core.world.Region;
|
||||
import mc.world.generated_world.chunk.ChunkSectionImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.MessageFormat;
|
||||
|
||||
import static mc.world.generated_world.WorldConstants.*;
|
||||
|
||||
public class ChunkReader implements IChunkReader{
|
||||
private final File worldFolder;
|
||||
@Autowired
|
||||
private Deserializer<Block> blockDeserializer;
|
||||
|
||||
public ChunkReader (File worldFolder) {
|
||||
this.worldFolder = worldFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public 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;
|
||||
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);
|
||||
chunkSection.setBlock(block);
|
||||
}
|
||||
return chunkSection;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package mc.world.generated_world.serialization;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.ChunkSection;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static mc.world.generated_world.WorldConstants.WORLD_CHUNK_SIZE;
|
||||
|
||||
@Slf4j
|
||||
public class ChunkSerializer implements Serializer<ChunkSection> {
|
||||
|
||||
@Autowired
|
||||
private Serializer<Block> blockSerializer;
|
||||
|
||||
@Override
|
||||
public byte[] serialize(ChunkSection chunkSection) {
|
||||
Serializer<Block> 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 = chunkSection.getBlock(x, y, z);
|
||||
if (current != null && current.getBlockType() != BlockType.AIR) {
|
||||
try {
|
||||
baos.write(blockSerializer.serialize(current));
|
||||
} catch (IOException e) {
|
||||
log.error("Error occurred while writing serialized block to byte array", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package mc.world.generated_world.serialization;
|
||||
|
||||
import mc.core.serialization.IRegionReaderWriter;
|
||||
import mc.core.world.Biome;
|
||||
import mc.core.world.Region;
|
||||
import mc.core.world.World;
|
||||
import mc.world.generated_world.region.RegionImpl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.MessageFormat;
|
||||
|
||||
import static mc.world.generated_world.WorldConstants.*;
|
||||
|
||||
public class RegionReaderWriter implements IRegionReaderWriter {
|
||||
private final File worldFolder;
|
||||
|
||||
public RegionReaderWriter(File worldFolder) {
|
||||
this.worldFolder = worldFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Region read (int x, int z, World world) throws IOException{
|
||||
File regionFolder = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, x, z));
|
||||
File biomesFile = new File(regionFolder, BIOME_FILE_NAME_TEMPLATE);
|
||||
byte[] biomesBytes = Files.readAllBytes(Paths.get(biomesFile.toURI()));
|
||||
Region region = new RegionImpl(x, z, world);
|
||||
for (int tx = 0; tx < WORLD_REGION_SIZE; tx ++) {
|
||||
for (int tz = 0; tz < WORLD_REGION_SIZE; tz ++) {
|
||||
region.setBiome(tx, tz, Biome.getById(biomesBytes[tx * WORLD_REGION_SIZE + tz]));
|
||||
}
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write (Region region) throws IOException{
|
||||
File regionFolder = new File(worldFolder, MessageFormat.format(REGION_FILE_NAME_TEMPLATE, region.getX(), region.getZ()));
|
||||
if (!regionFolder.exists()) {
|
||||
regionFolder.mkdirs();
|
||||
}
|
||||
File biomesFile = new File(regionFolder, BIOME_FILE_NAME_TEMPLATE);
|
||||
byte[] biomesBytes = new byte[WORLD_REGION_SIZE * WORLD_REGION_SIZE];
|
||||
for (int x = 0; x < WORLD_REGION_SIZE; x ++) {
|
||||
for (int z = 0; z < WORLD_REGION_SIZE; z ++) {
|
||||
biomesBytes[x * WORLD_REGION_SIZE + z] = (byte) region.getBiomeAt(x, z).getId();
|
||||
}
|
||||
}
|
||||
try (FileOutputStream fos = new FileOutputStream(biomesFile)) {
|
||||
fos.write(biomesBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package mc.world.generated_world.serialization;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import mc.core.EntityLocation;
|
||||
import mc.core.world.World;
|
||||
import mc.world.generated_world.world.CubicWorld;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.UUID;
|
||||
|
||||
import static mc.world.generated_world.WorldConstants.WORLD_INFO_FILE_NAME_TEMPLATE;
|
||||
|
||||
@Slf4j
|
||||
public class WorldReaderWriter {
|
||||
private final File worldsFolder;
|
||||
|
||||
public WorldReaderWriter(File worldsFolder) {
|
||||
this.worldsFolder = worldsFolder;
|
||||
}
|
||||
|
||||
public World readWorld (UUID uuid) throws IOException {
|
||||
World world = null;
|
||||
File worldFolder = new File(worldsFolder, uuid.toString());
|
||||
if (!worldFolder.exists()) {
|
||||
throw new FileNotFoundException("World folder is not exist");
|
||||
}
|
||||
File worldInfoFile = new File(worldFolder, WORLD_INFO_FILE_NAME_TEMPLATE);
|
||||
WorldInfo worldInfo;
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(worldInfoFile))) {
|
||||
worldInfo = (WorldInfo) ois.readObject();
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.error("Error occurred while reading world info file", e);
|
||||
return null;
|
||||
}
|
||||
world = new CubicWorld(uuid, worldInfo.getSeed());
|
||||
world.setSpawn(worldInfo.getSpawn());
|
||||
world.setName(worldInfo.getName());
|
||||
return world;
|
||||
}
|
||||
|
||||
public void writeWorldInfo (World world) throws IOException {
|
||||
File worldFolder = new File(worldsFolder, world.getWorldId().toString());
|
||||
worldFolder.mkdirs();
|
||||
File worldInfoFile = new File(worldFolder, WORLD_INFO_FILE_NAME_TEMPLATE);
|
||||
WorldInfo worldInfo = new WorldInfo();
|
||||
worldInfo.setName(world.getName());
|
||||
worldInfo.setSeed(world.getSeed());
|
||||
worldInfo.setSpawn(world.getSpawn());
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(worldInfoFile))) {
|
||||
oos.writeObject(worldInfo);
|
||||
oos.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class WorldInfo implements Serializable {
|
||||
private EntityLocation spawn;
|
||||
private String name;
|
||||
private int seed;
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
package mc.world.generated_world.world;
|
||||
|
||||
import com.flowpowered.nbt.Tag;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import mc.core.Direction;
|
||||
import mc.core.EntityLocation;
|
||||
import mc.core.world.*;
|
||||
import mc.core.world.chunk.ChunkSection;
|
||||
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.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 spawn;
|
||||
private final transient Object spawnLocationLock = new Object();
|
||||
private final Map<String, Tag<?>> nbtTagMap = new HashMap<>();
|
||||
@Getter@Setter
|
||||
private String name;
|
||||
|
||||
public CubicWorld(UUID worldId, int seed) {
|
||||
this.worldId = worldId;
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
public CubicWorld(int seed) {
|
||||
this.worldId = UUID.randomUUID();
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
public CubicWorld(UUID worldId) {
|
||||
this.worldId = worldId;
|
||||
this.seed = 0;
|
||||
}
|
||||
|
||||
public CubicWorld () {
|
||||
this.worldId = UUID.randomUUID();
|
||||
this.seed = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IWorldType getWorldType() {
|
||||
return null; //FIXME
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityLocation getSpawn() {
|
||||
/* FIXME */
|
||||
if (spawn == null) {
|
||||
log.warn("Spawn is not defined! Set default spawn: [8, 128, 8]");
|
||||
setSpawn(new EntityLocation(8d, 128d, 8d, 0f, 0f, this));
|
||||
}
|
||||
return spawn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSpawn(EntityLocation entityLocation) {
|
||||
synchronized (spawnLocationLock) {
|
||||
entityLocation.setWorld(this);
|
||||
this.spawn = entityLocation;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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, ChunkSection chunkSection) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
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", 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
|
||||
public int getSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tag<?> getTag(String name) {
|
||||
return nbtTagMap.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTag(Tag<?> tag) {
|
||||
nbtTagMap.put(tag.getName(), tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Tag<?>> tagStream() {
|
||||
return nbtTagMap.values().stream();
|
||||
}
|
||||
|
||||
private void checkCoordsInCache (int x, int z) {
|
||||
if (x < pointX) {
|
||||
addLines(Direction.EAST, pointX - x);
|
||||
} else if (x > pointX + sizeX) {
|
||||
addLines(Direction.WEST, x - (pointX + sizeX));
|
||||
} else if (z < pointZ) {
|
||||
addLines(Direction.NORTH, pointZ - z);
|
||||
} else if (z > pointZ + sizeZ) {
|
||||
addLines(Direction.SOUTH, z - (pointZ + sizeZ));
|
||||
}
|
||||
}
|
||||
|
||||
private void addLines (Direction direction, int amount) {
|
||||
int addBeforeX = 0;
|
||||
int addAfterX = 0;
|
||||
int addBeforeZ = 0;
|
||||
int addAfterZ = 0;
|
||||
switch (direction) {
|
||||
case NORTH:
|
||||
addBeforeZ = amount;
|
||||
break;
|
||||
case EAST:
|
||||
addBeforeX = amount;
|
||||
break;
|
||||
case WEST:
|
||||
addAfterX = amount;
|
||||
break;
|
||||
case SOUTH:
|
||||
addAfterZ = amount;
|
||||
break;
|
||||
}
|
||||
try {
|
||||
regionSaveLock.lock();
|
||||
int tempSizeX = sizeX + addAfterX + addBeforeX;
|
||||
int tempSizeZ = sizeZ + addAfterZ + addBeforeZ;
|
||||
Region[][] temp = new Region[tempSizeX][tempSizeZ];
|
||||
for (int x = 0; x < sizeX; x ++) {
|
||||
System.arraycopy(regions[x], 0, temp[x + addBeforeX], addBeforeZ, sizeZ);
|
||||
}
|
||||
|
||||
this.sizeX = tempSizeX;
|
||||
this.sizeZ = tempSizeZ;
|
||||
this.pointX = pointX - addBeforeX;
|
||||
this.pointZ = pointZ - addBeforeZ;
|
||||
} finally {
|
||||
regionSaveLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package mc.world.generated_world.world;
|
||||
|
||||
public enum Temperature {
|
||||
FROST,
|
||||
COLD,
|
||||
WARM,
|
||||
HOT,
|
||||
HOTTEST
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package mc.world.generated_world.world;
|
||||
|
||||
public enum Wetness {
|
||||
DRIEST,
|
||||
DRY,
|
||||
WET,
|
||||
WETTER,
|
||||
WETTEST,
|
||||
WATER
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration>
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{HH:mm:ss}] [%-5p] %m%n"/>
|
||||
</Console>
|
||||
<RollingFile name="File" fileName="log/log_file.log" filePattern="log/log_file-%d{MM-dd-yyyy}.log.gz">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
|
||||
<TimeBasedTriggeringPolicy />
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="Trace">
|
||||
<AppenderRef ref="Console"/>
|
||||
<AppenderRef ref="File"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -1,95 +0,0 @@
|
||||
package mc.world.generated_world;
|
||||
|
||||
import mc.world.generated_world.generator.SeedRandomGenerator;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@Ignore
|
||||
public class SeedRandomGeneratorTest {
|
||||
|
||||
@Test
|
||||
public void randomGenSpeed () {
|
||||
SeedRandomGenerator.random(0, 0, 0);
|
||||
long avg = 0;
|
||||
long min = -1;
|
||||
long max = 0;
|
||||
for (int i = 0; i < 500; i ++) {
|
||||
int x = (int) (Math.random() * 10000);
|
||||
int y = (int) (Math.random() * 10000);
|
||||
int seed = (int) (Math.random() * 10000);
|
||||
long time = System.nanoTime();
|
||||
SeedRandomGenerator.random(x, y, seed);
|
||||
time = System.nanoTime() - time;
|
||||
System.out.printf("[%s] \t%.3fms\n", i+1, time/1000d);
|
||||
avg += time;
|
||||
if (min == -1) {
|
||||
min = time;
|
||||
} else if (min > time) {
|
||||
min = time;
|
||||
}
|
||||
if (max < time) {
|
||||
max = time;
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
System.out.printf("Average time: %.3fms\n", avg/500000d);
|
||||
System.out.printf("Minimum time: %.3fms\n", min/1000d);
|
||||
System.out.printf("Maximum time: %.3fms\n", max/1000d);
|
||||
assertTrue(avg/500 < 5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomTest() throws Exception {
|
||||
double maxDiff = 0;
|
||||
double maxDisp = 0;
|
||||
for (int i = 0; i < 100; i ++) {
|
||||
double mid = 0;
|
||||
double disp = 0;
|
||||
int seed = (int) (Math.random() * Integer.MAX_VALUE);
|
||||
for (int x = -1000; x < 1000; x++) {
|
||||
for (int y = -1000; y < 1000; y++) {
|
||||
double rnd = SeedRandomGenerator.random(x, y, seed);
|
||||
mid += rnd;
|
||||
disp += (rnd - 0.5) * (rnd - 0.5);
|
||||
}
|
||||
}
|
||||
mid = mid/4000000;
|
||||
disp = Math.sqrt(disp)/4000000;
|
||||
if (maxDiff < Math.abs(mid - 0.5)) {
|
||||
maxDiff = Math.abs(mid - 0.5);
|
||||
}
|
||||
if (maxDisp < disp) {
|
||||
maxDisp = disp;
|
||||
}
|
||||
System.out.printf("Iteration %d.\t mid: %.3f, \tdisp %.6f\n", i + 1, mid, disp);
|
||||
assertTrue(Math.abs(mid - 0.5) < 0.15);
|
||||
}
|
||||
System.out.printf("Max diff: %.3f\n", maxDiff);
|
||||
System.out.printf("Max disp: %.6f\n", maxDisp);
|
||||
|
||||
|
||||
assertTrue(maxDiff > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateImage () throws Exception {
|
||||
int h = 500;
|
||||
int w = 500;
|
||||
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||
|
||||
int seed = (int) (Math.random() * Integer.MAX_VALUE) / 1024;
|
||||
for (int x = 0; x < w; x ++) {
|
||||
for (int y = 0; y < h; y ++) {
|
||||
image.setRGB(x, y, (int) (0xffffff * SeedRandomGenerator.random(x, y, seed)));
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "bmp", new File("out", "seed_random.png"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,5 +5,3 @@ include('flat_world')
|
||||
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')
|
||||
Reference in New Issue
Block a user