diff --git a/.gitignore b/.gitignore index a43309c..311183a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,14 @@ out/ *.ipr *.iws *.ids + +## GRADLE ## +.gradle/ +build/ +gradle/ +gradlew +gradlew.* + +## PROJECT ## +libs/ +*.log diff --git a/README.MD b/README.MD index 2ff786a..71023a3 100644 --- a/README.MD +++ b/README.MD @@ -1,3 +1,30 @@ # MC-CORE -Minecraft server +![version: v0.1](https://img.shields.io/badge/version-v0.1-0b0.svg?style=flat) +![codename: ZERO](https://img.shields.io/badge/codename-ZERO-000.svg?style=flat) + +Модульный **Minecraft** сервер. + +## Модули + +* **Core** - ядро сервера + +## Сборка + +``` +gradle jar +``` + +Так же можно собрать все необходимые библиотеки в "кучу": + +``` +gradle copyDep +``` + +Или сразу развернув сервер где надо: + +``` +gradle deploy -Ddeploy=path/to/folder -DcreateRunScript=true +``` + +`createRunScript` - указание этого параметра создаст скрипт-запускатор \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..fb10065 --- /dev/null +++ b/build.gradle @@ -0,0 +1,168 @@ +import java.nio.file.Files +import java.nio.file.Paths + +buildscript { + repositories { + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath (group: 'org.sonarsource.scanner.gradle', name: 'sonarqube-gradle-plugin', version: '2.6.2') + } +} + +/** + * Проверка кода в SonarQube. + * Для запуска локальной проверки кода, используются следующий command line: + * gradle sonarqube \ + * -Dsonar.host.url=http://127.0.0.1:9000 + * -Dsonar.login= + * где + * - - сгенерированный токен учетки "сонара" + */ +plugins { + id "org.sonarqube" version "2.6.2" +} + +allprojects { + repositories { + mavenCentral() + maven { url 'https://oss.sonatype.org/content/groups/public/' } + } +} + +subprojects { + apply plugin: 'java' + + compileJava { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + options.encoding = 'UTF-8' + } + + group 'mc' + + ext { + slf4j_version = '1.7.25' + spring_version = '5.1.0.RELEASE' + lombok_version = '1.18.4' + junit_version = '5.3.1' + } + + configurations { + compile_excludeCopy + compile.extendsFrom compile_excludeCopy + } + + dependencies { + compile (group: 'org.jetbrains', name: 'annotations', version: '16.0.3') + + /* Logger */ + compile (group: 'org.slf4j', name: 'slf4j-api', version: slf4j_version) + compile (group: 'org.slf4j', name: 'jcl-over-slf4j', version: slf4j_version) + + /* Spring */ + compile (group: 'org.springframework', name: 'spring-context', version: spring_version) + + /* Lombok */ + annotationProcessor (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + compile (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + testAnnotationProcessor (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + testCompile (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + + /* Testing */ + testImplementation (group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junit_version) + testRuntimeOnly(group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit_version) + testImplementation(group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit_version) + testCompile (group: 'org.slf4j', name: 'slf4j-simple', version: slf4j_version) + testCompile (group: 'org.mockito', name: 'mockito-core', version: '1.10.19') + testCompile (group: 'org.springframework', name: 'spring-test', version: spring_version) + } + + test { + useJUnitPlatform() + } + + task copyDep(type: Copy) { + into 'libs' + from configurations.compile + configurations.runtime - configurations.compile_excludeCopy + } + + task cleanDep(type: Delete) { + delete 'libs' + } + + /** + * Сборка + */ + task deploy() { + dependsOn { jar } + + doLast { + def deployDir = System.getProperty("deploy") + if (deployDir == null) { + println "Need param -Ddeploy=path/to/deploy" + throw new Exception("Need param -Ddir=path/to/deploy") + } + + def target = Paths.get(deployDir, jar.archivePath.getName()) + if (Files.notExists(target)) { + println jar.archivePath + Files.copy(jar.archivePath.toPath(), target) + } + + def libsDir = System.getProperty("libs", deployDir + File.separator + "libs") + if (Files.notExists(Paths.get(libsDir))) { + (new File(libsDir)).mkdirs() + } + + def libsCollection = configurations.compile + configurations.runtime - configurations.compile_excludeCopy + libsCollection.each{ libFile -> + target = Paths.get(libsDir, libFile.getName()) + if (Files.notExists(target)) { + println libFile + Files.copy(libFile.toPath(), target) + } + } + + if (Boolean.valueOf(System.getProperty("createRunScript", "false"))) { + def runnerSh = new File(deployDir, "run.sh") + runnerSh.write "java -cp \"${project(':core').jar.archiveName};" + runnerSh << "${libsDir}/*;./log-impl/*\" mc.core.Main\n" + } + } + } +} + +/** + * Запуск сервера. + * Для указания рабочей папки, указываем JVM параметр + * -DworkDir=path\to\workdir + * Если используется отдельная папка для имплементации логгера, то указываем + * -DlogImplDir=path\to\logimpldir + */ +task runServer(type: JavaExec) { + main = 'mc.core.Main' + + workingDir = System.getProperty("workDir", ".") + + subprojects.findAll().each{ prj -> + classpath += prj.sourceSets.main.runtimeClasspath + } + + if (System.getProperty("logImplDir") != null) { + def logImplDir = new File(System.getProperty("logImplDir")) + + if (logImplDir.isAbsolute()) { + classpath += files(fileTree(dir: logImplDir)) + } else { + classpath += files(fileTree(dir: new File(workingDir, logImplDir.getPath()))) + } + } else { + classpath += files(fileTree(dir: new File(workingDir, "log-impl"))) + } + + systemProperties System.properties + systemProperties.put("user.dir", workingDir) + + ignoreExitValue = true +} diff --git a/core/README.MD b/core/README.MD new file mode 100644 index 0000000..173954c --- /dev/null +++ b/core/README.MD @@ -0,0 +1,85 @@ +# Core + +Ядро сервера + +## Spring beans + +### ConfigFromSpring + +Настройка параметров сервера через конфигурацию "спринга". + +Имеются следующие настройки: +* `descriptionServer` - описание сервера (aka "Motd") +* `favicon` - файл с иконкой сервера +* `maxPlayers` - максимальная вместимость сервера + +**Implements:** `mc.core.Config` + +**Bean example:** + +```xml + + + + + +``` + +### GameLoop + +**Bean example:** + +Доступные параметры: +* `gameTimer` - бин, управляющий ходом времени + +```xml + + + +``` + +### IdleTime + +Игровое время суток застывает на указанной отметке. + +Доступные параметры: +* `gameTime` - отметка времени (long) + +**Implements:** `mc.core.time.TimeProcessor` + +**Bean example:** + +```xml + + + +``` + +### TimePerTick + +Игровое время суток соответствует игровым тикам (20 tps) + +Доступные параметры: +* `startGameTime` - стартовое время (long) + +**Implements:** `mc.core.time.TimeProcessor` + +**Bean example:** + +```xml + + + +``` + +### RealTime + +Игровое время суток соответствует реальному времени + +**Implements:** `mc.core.time.TimeProcessor` + +**Bean example:** + +```xml + +``` diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000..c2585f1 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,14 @@ +version '0.1' + +apply plugin: 'maven' +apply plugin: 'application' + +mainClassName = "mc.core.Main" + +dependencies { + /* Components */ + compile (group: 'commons-io', name: 'commons-io', version: '2.6') + compile (group: 'com.google.guava', name: 'guava', version: '26.0-jre') + /* Named Binary Tags */ + compile (group: 'com.flowpowered', name: 'flow-nbt', version: '1.0.1-SNAPSHOT') +} diff --git a/core/src/main/java/mc/core/Config.java b/core/src/main/java/mc/core/Config.java new file mode 100644 index 0000000..d26aeed --- /dev/null +++ b/core/src/main/java/mc/core/Config.java @@ -0,0 +1,7 @@ +package mc.core; + +public interface Config { + int getMaxPlayers(); + String getDescriptionServer(); + byte[] getFaviconBase64(); +} diff --git a/core/src/main/java/mc/core/CoreEventListener.java b/core/src/main/java/mc/core/CoreEventListener.java new file mode 100644 index 0000000..de3791b --- /dev/null +++ b/core/src/main/java/mc/core/CoreEventListener.java @@ -0,0 +1,80 @@ +package mc.core; + +import lombok.extern.slf4j.Slf4j; +import mc.core.eventbus.EventBus; +import mc.core.eventbus.Subscriber; +import mc.core.eventbus.events.CS_PlayerMoveEvent; +import mc.core.eventbus.events.SC_ChunkLoadEvent; +import mc.core.eventbus.events.SC_ChunkUnloadEvent; +import mc.core.utils.CompactedCoords; +import mc.core.world.chunk.Chunk; + +import javax.annotation.PostConstruct; +import java.util.Iterator; + +@Slf4j +public class CoreEventListener { + @PostConstruct + public void registerEventHandlers() { + EventBus.getInstance().registerSubscribes(this); + } + + @Subscriber + public void handlerPlayerMoveEvent(CS_PlayerMoveEvent event) { + Chunk chunk; + chunk = event.getPlayer().getWorld().getChunk(event.getOldLocation().toBlockLocation()); // Old chunk + if (chunk == null) return; + int ccX = chunk.getX(); + int ccZ = chunk.getZ(); + chunk = event.getPlayer().getWorld().getChunk(event.getNewLocation().toBlockLocation()); // Next chunk + if (chunk == null) return; + int ncX = chunk.getX(); + int ncZ = chunk.getZ(); + + if (event.isRecalcChunk() || (ncX != ccX || ncZ != ccZ)) { + final int viewDistance = event.getPlayer().getSettings().getViewDistance() + 1; + int cMinX = chunk.getX() - viewDistance; + int cMaxX = chunk.getX() + viewDistance; + int cMinZ = chunk.getZ() - viewDistance; + int cMaxZ = chunk.getZ() + viewDistance; + + SC_ChunkLoadEvent eventChunkLoad = new SC_ChunkLoadEvent(event.getPlayer()); + for (int cZ = cMinZ; cZ <= cMaxZ; cZ++) { + for (int cX = cMinX; cX <= cMaxX; cX++) { + int compressXZ = CompactedCoords.compressXZ(cX, cZ); + if (!event.getPlayer().getLoadedChunks().contains(compressXZ)) { + if (!event.getPlayer().getLoadedChunks().contains(compressXZ)) { + eventChunkLoad.getNeedLoadChunks().add(compressXZ); + event.getPlayer().getLoadedChunks().add(compressXZ); + } + } + } + } + + if (!eventChunkLoad.getNeedLoadChunks().isEmpty()) { + EventBus.getInstance().post(eventChunkLoad); + } + + SC_ChunkUnloadEvent eventChunkUnload = new SC_ChunkUnloadEvent(event.getPlayer()); + Iterator itr = event.getPlayer().getLoadedChunks().iterator(); + while(itr.hasNext()) { + int compressXZ = itr.next(); + int[] xz = CompactedCoords.uncompressXZ(compressXZ); + if (xz[0] > cMaxX || xz[0] < cMinX || xz[1] > cMaxZ || xz[1] < cMinZ) { + eventChunkUnload.getNeedUnloadChunks().add(compressXZ); + itr.remove(); + } + } + + if (!eventChunkUnload.getNeedUnloadChunks().isEmpty()) { + EventBus.getInstance().post(eventChunkUnload); + } + } + + event.getPlayer().getLocation().setXYZ( + event.getNewLocation().getX(), + event.getNewLocation().getY(), + event.getNewLocation().getZ() + ); + } +} diff --git a/core/src/main/java/mc/core/EntityLocation.java b/core/src/main/java/mc/core/EntityLocation.java new file mode 100644 index 0000000..53fadcb --- /dev/null +++ b/core/src/main/java/mc/core/EntityLocation.java @@ -0,0 +1,60 @@ +package mc.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import mc.core.world.block.BlockLocation; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class EntityLocation implements Cloneable { + private double x, y, z; + private float yaw, pitch; + + public static EntityLocation ZERO() { + return new EntityLocation(0d,0d,0d,0f,0f); + } + + public void set(EntityLocation location) { + setXYZ(location.x, location.y, location.z); + setYawPitch(location.yaw, location.pitch); + } + + public void setXYZ(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public void setYawPitch(float yaw, float pitch) { + this.yaw = yaw; + this.pitch = pitch; + } + + public int getBlockX() { + return (int) Math.floor(x); + } + + public int getBlockY() { + return (int) Math.floor(y); + } + + public int getBlockZ() { + return (int) Math.floor(z); + } + + public BlockLocation toBlockLocation() { + return new BlockLocation(getBlockX(), getBlockY(), getBlockZ()); + } + + @Override + public EntityLocation clone() { + try { + return (EntityLocation) super.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + return ZERO(); + } + } +} diff --git a/core/src/main/java/mc/core/GameLoop.java b/core/src/main/java/mc/core/GameLoop.java new file mode 100644 index 0000000..02eefa0 --- /dev/null +++ b/core/src/main/java/mc/core/GameLoop.java @@ -0,0 +1,59 @@ +package mc.core; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.eventbus.EventBus; +import mc.core.player.PlayerManager; +import mc.core.time.TimeProcessor; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +public class GameLoop extends Thread { + private final TpsWatcher TPS_WATCHER = TpsWatcher.getInstance(); + @Autowired + private PlayerManager playerManager; + + /* Time */ + @Setter + private TimeProcessor gameTimer; + + public GameLoop() { + super(); + setTps(20); + setPercentWarnLowTps(5); + } + + public void setPercentWarnLowTps(int value) { + TPS_WATCHER.setPercentWarnLowTps(value); + } + + public void setTps(int tps) { + TPS_WATCHER.setTps(tps); + } + + public void setTps(boolean value) { + TPS_WATCHER.setTraceTPS(value); + } + + @Override + public void run() { + TPS_WATCHER.startWatch(); + + while (!isInterrupted()) { + TPS_WATCHER.check(); + + /* --- --- --- */ + + EventBus.getInstance().process(); + + playerManager.getBroadcastChannel().sendTimeUpdate( + gameTimer.getGameTime(), + gameTimer.getWorldAge() + ); + + /* --- --- --- */ + + TPS_WATCHER.tick(); + } + } +} diff --git a/core/src/main/java/mc/core/ImmutableEntityLocation.java b/core/src/main/java/mc/core/ImmutableEntityLocation.java new file mode 100644 index 0000000..f71c1e3 --- /dev/null +++ b/core/src/main/java/mc/core/ImmutableEntityLocation.java @@ -0,0 +1,57 @@ +package mc.core; + +public class ImmutableEntityLocation extends EntityLocation { + public ImmutableEntityLocation(double x, double y, double z, float yaw, float pitch) { + super(x, y, z, yaw, pitch); + } + + public ImmutableEntityLocation(EntityLocation location) { + this( + location.getX(), + location.getY(), + location.getZ(), + location.getYaw(), + location.getPitch() + ); + } + + @Override + public void setX(double x) { + throw new UnsupportedOperationException(); + } + + @Override + public void setY(double y) { + throw new UnsupportedOperationException(); + } + + @Override + public void setZ(double z) { + throw new UnsupportedOperationException(); + } + + @Override + public void setYaw(float yaw) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPitch(float pitch) { + throw new UnsupportedOperationException(); + } + + @Override + public void set(EntityLocation location) { + throw new UnsupportedOperationException(); + } + + @Override + public void setXYZ(double x, double y, double z) { + throw new UnsupportedOperationException(); + } + + @Override + public void setYawPitch(float yaw, float pitch) { + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/java/mc/core/Main.java b/core/src/main/java/mc/core/Main.java new file mode 100644 index 0000000..19dcbb4 --- /dev/null +++ b/core/src/main/java/mc/core/Main.java @@ -0,0 +1,49 @@ +package mc.core; + +import lombok.extern.slf4j.Slf4j; +import mc.core.network.Server; +import mc.core.network.StartServerException; +import org.apache.commons.io.IOUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.FileSystemXmlApplicationContext; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +@Slf4j +public class Main { + private static ApplicationContext createContext() { + final String springXml = System.getProperty("springConfig", "./spring.xml"); + + if (!Files.exists(Paths.get(springXml))) { + log.info("File \"{}\" not found. Get default config.", springXml); + try (FileOutputStream fos = new FileOutputStream(springXml)) { + IOUtils.copy(Main.class.getResourceAsStream("/spring.xml"), fos); + } catch (IOException e) { + log.error("Get default spring config", e); + System.exit(-1); + } + } + + return new FileSystemXmlApplicationContext(springXml); + } + + public static void main(String[] args) { + ApplicationContext appContext = createContext(); + + GameLoop gameLoop = appContext.getBean(GameLoop.class); + gameLoop.start(); + + Server server = appContext.getBean("server", Server.class); + Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); + try { + server.start(); + } catch (StartServerException e) { + log.error("Can't start server", e); + } + + gameLoop.interrupt(); + } +} diff --git a/core/src/main/java/mc/core/TpsWatcher.java b/core/src/main/java/mc/core/TpsWatcher.java new file mode 100644 index 0000000..51d477b --- /dev/null +++ b/core/src/main/java/mc/core/TpsWatcher.java @@ -0,0 +1,78 @@ +package mc.core; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class TpsWatcher { + private static final TpsWatcher instance = new TpsWatcher(); + + private boolean traceTps = false; + + private int tps; + private long pause; + private int lowTps; + private int percentLowTps; + + private int factTps; + private long lastTime; + private long futureTime; + + public static TpsWatcher getInstance() { + return instance; + } + + private TpsWatcher(){ } + + public void setTps(int value) { + if (value > 1000) { + log.warn("TPS can't be '{}'. Set 1000", value); + value = 1000; + } + + tps = value; + pause = (1000 / value); + } + + public void setPercentWarnLowTps(int value) { + if (value > 100) { + log.warn("Percent warn low TPS can't be '{}'. Set 100", value); + value = 100; + } + + lowTps = tps - (int)(tps * (value / 100f)); + percentLowTps = value; + } + + public void setTraceTPS(boolean value) { + traceTps = value; + } + + public void startWatch() { + log.info("Target TPS: {}; Low TPS: {}({}%)", tps, lowTps, percentLowTps); + factTps = 0; + lastTime = System.currentTimeMillis(); + } + + public void check() { + if ((System.currentTimeMillis() - lastTime) > 1000) { + lastTime = System.currentTimeMillis(); + if (factTps < lowTps) { + log.warn("Low TPS: {}/{}", factTps, tps); + } else if (traceTps) { + log.trace("TPS: {}/{}", factTps, tps); + } + factTps = 0; + } + + futureTime = System.currentTimeMillis() + pause; + } + + public void tick() { + factTps++; + try { + long pause = futureTime - System.currentTimeMillis(); + Thread.sleep((pause <= 0 ? 0 : pause)); + } catch (InterruptedException ignored) { + } + } +} diff --git a/core/src/main/java/mc/core/chat/ChatProcessor.java b/core/src/main/java/mc/core/chat/ChatProcessor.java new file mode 100644 index 0000000..fed5fbf --- /dev/null +++ b/core/src/main/java/mc/core/chat/ChatProcessor.java @@ -0,0 +1,11 @@ +package mc.core.chat; + +import mc.core.player.Player; +import org.slf4j.Marker; +import org.slf4j.helpers.BasicMarkerFactory; + +public abstract class ChatProcessor { + protected static final Marker CHAT_MARKER = new BasicMarkerFactory().getMarker("Chat"); + + public abstract void process(Player player, String message); +} diff --git a/core/src/main/java/mc/core/chat/CommandExecutor.java b/core/src/main/java/mc/core/chat/CommandExecutor.java new file mode 100644 index 0000000..39d4b2a --- /dev/null +++ b/core/src/main/java/mc/core/chat/CommandExecutor.java @@ -0,0 +1,13 @@ +package mc.core.chat; + +import mc.core.player.Player; + +import java.util.Optional; + +public interface CommandExecutor { + String getName(); + Optional getAliases(); + Optional getUsage(); + String getDescription(); + void execute(Player sender, String... args); +} diff --git a/core/src/main/java/mc/core/chat/CommanderChatProcessor.java b/core/src/main/java/mc/core/chat/CommanderChatProcessor.java new file mode 100644 index 0000000..7cf5880 --- /dev/null +++ b/core/src/main/java/mc/core/chat/CommanderChatProcessor.java @@ -0,0 +1,90 @@ +package mc.core.chat; + +import lombok.extern.slf4j.Slf4j; +import mc.core.player.Player; +import mc.core.text.Text; +import mc.core.text.TextColor; +import mc.core.text.TextTemplate; +import org.slf4j.Marker; +import org.slf4j.helpers.BasicMarkerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +import javax.annotation.PostConstruct; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +public class CommanderChatProcessor extends SimpleChatProcessor { + private static final Marker COMMAND_MARKER = new BasicMarkerFactory().getMarker("Command"); + private static final TextTemplate UNKNOW_COMMAND_MSG = TextTemplate.builder() + .append(Text.of("Unknown command \"", TextColor.RED)) + .arg("command", TextColor.WHITE) + .append(Text.of("\"", TextColor.RED)) + .build(); + @Autowired + private ApplicationContext applicationContext; + private Map commands = new HashMap<>(); + + @PostConstruct + public void init() { + Map beans = applicationContext.getBeansOfType(CommandExecutor.class); + beans.values().forEach(commandExecutor -> { + log.trace("Add command \"{}\" ({})", commandExecutor.getName(), commandExecutor.getClass().getName()); + if (commands.containsKey(commandExecutor.getName())) { + log.warn("Override command \"{}\"", commandExecutor.getName()); + log.debug("{} -> {}", + commands.get(commandExecutor.getName()).getClass().getName(), + commandExecutor.getClass().getName() + ); + } + commands.put(commandExecutor.getName(), commandExecutor); + + if (commandExecutor.getAliases().isPresent()) { + Arrays.stream(commandExecutor.getAliases().get()).forEach(aliase -> { + log.trace("Add aliase \"{}\" ({})", aliase, commandExecutor.getClass().getName()); + if (commands.containsKey(aliase)) { + log.warn("Override aliase \"{}\"", aliase); + log.debug("{} -> {}", + commands.get(aliase).getClass().getName(), + commandExecutor.getClass().getName() + ); + } + commands.put(aliase, commandExecutor); + }); + } + }); + + log.debug("Load {} commands", commands.size()); + } + + @Override + public void process(Player player, String message) { + if (message.startsWith("/")) { + log.info(COMMAND_MARKER, "<{}> {}", player.getName(), message); + + int idx = message.indexOf(' '); + if (idx == -1) { + idx = message.length(); + } + + String command = message.substring(1, idx).toLowerCase(); + if (commands.containsKey(command)) { + String[] args = message.substring(idx).split(" "); + commands.get(command).execute(player, args); + } else { + player.getChannel().sendChatMessage( + UNKNOW_COMMAND_MSG.apply("command", command), + MessageType.SYSTEM_MESSAGE); + } + } else { + super.process(player, message); + } + } + + public Collection getAllCommands() { + return commands.values(); + } +} diff --git a/core/src/main/java/mc/core/chat/MessageType.java b/core/src/main/java/mc/core/chat/MessageType.java new file mode 100644 index 0000000..667e45e --- /dev/null +++ b/core/src/main/java/mc/core/chat/MessageType.java @@ -0,0 +1,14 @@ +package mc.core.chat; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum MessageType { + CHAT_MESSAGE(0), // chat box + SYSTEM_MESSAGE(1), // chat box + GAME_INFO(2); // above hotbar + + private final int id; +} diff --git a/core/src/main/java/mc/core/chat/SimpleChatProcessor.java b/core/src/main/java/mc/core/chat/SimpleChatProcessor.java new file mode 100644 index 0000000..7742cf6 --- /dev/null +++ b/core/src/main/java/mc/core/chat/SimpleChatProcessor.java @@ -0,0 +1,25 @@ +package mc.core.chat; + +import lombok.extern.slf4j.Slf4j; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import mc.core.text.Text; +import mc.core.text.TextColor; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +public class SimpleChatProcessor extends ChatProcessor { + @Autowired + private PlayerManager playerManager; + + @Override + public void process(Player player, String message) { + log.info(CHAT_MARKER, "<{}> {}", player.getName(), message); + playerManager.getBroadcastChannel().sendChatMessage( + Text.builder(TextColor.GOLD, player.getName()) + .append(Text.of(TextColor.GRAY, ": ")) + .append(Text.of(TextColor.WHITE, message)) + .build() + ); + } +} diff --git a/core/src/main/java/mc/core/embedded/ConfigFromSpring.java b/core/src/main/java/mc/core/embedded/ConfigFromSpring.java new file mode 100644 index 0000000..4645533 --- /dev/null +++ b/core/src/main/java/mc/core/embedded/ConfigFromSpring.java @@ -0,0 +1,33 @@ +package mc.core.embedded; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.Config; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.Base64; + +@Slf4j +@Getter +public class ConfigFromSpring implements Config { + @Setter + private String descriptionServer; + private byte[] faviconBase64; + @Setter + private int maxPlayers; + + public void setFavicon(File faviconImageFile) { + log.debug("faviconImageFile: {}", faviconImageFile.getAbsolutePath()); + try { + faviconBase64 = Base64.getEncoder().encode( + FileUtils.readFileToByteArray(faviconImageFile) + ); + } catch (IOException e) { + log.warn("Can't load favicon", e); + faviconBase64 = null; + } + } +} diff --git a/core/src/main/java/mc/core/embedded/FakePlayerManager.java b/core/src/main/java/mc/core/embedded/FakePlayerManager.java new file mode 100644 index 0000000..a167573 --- /dev/null +++ b/core/src/main/java/mc/core/embedded/FakePlayerManager.java @@ -0,0 +1,83 @@ +package mc.core.embedded; + +import mc.core.EntityLocation; +import mc.core.chat.MessageType; +import mc.core.network.NetChannel; +import mc.core.network.SCPacket; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import mc.core.text.Text; +import mc.core.text.Title; +import mc.core.world.World; + +import java.util.Collections; +import java.util.List; + +public class FakePlayerManager implements PlayerManager { + public static class FakeNetChannet implements NetChannel { + + @Override + public void sendTimeUpdate(long time, long age) { + } + + @Override + public void sendChatMessage(Text text, MessageType type) { + } + + @Override + public void sendTitle(Title title) { + } + + @Override + public void writeAndFlush(SCPacket pkt) { + } + + @Override + public void write(SCPacket pkt) { + } + + @Override + public void flush() { + } + } + + private static final NetChannel FAKE_NET_CHANNEL = new FakeNetChannet(); + + @Override + public Player createPlayer(String name, EntityLocation location, World world) { + return null; + } + + @Override + public void joinServer(Player player) { + } + + @Override + public void leftServer(Player player) { + } + + @Override + public Player getPlayer(String name) { + return null; + } + + @Override + public List getPlayers() { + return Collections.emptyList(); + } + + @Override + public int getCountPlayers() { + return 0; + } + + @Override + public NetChannel getBroadcastChannel() { + return FAKE_NET_CHANNEL; + } + + @Override + public Player getOfflinePlayer(String name) { + return null; + } +} diff --git a/core/src/main/java/mc/core/embedded/FakeServer.java b/core/src/main/java/mc/core/embedded/FakeServer.java new file mode 100644 index 0000000..6c93937 --- /dev/null +++ b/core/src/main/java/mc/core/embedded/FakeServer.java @@ -0,0 +1,18 @@ +package mc.core.embedded; + +import lombok.extern.slf4j.Slf4j; +import mc.core.network.Server; +import mc.core.network.StartServerException; + +@Slf4j +public class FakeServer implements Server { + + @Override + public void start() { + log.info("Hello. I'm FakeServer. And i do nothing."); + } + + @Override + public void stop() { + } +} diff --git a/core/src/main/java/mc/core/eventbus/Event.java b/core/src/main/java/mc/core/eventbus/Event.java new file mode 100644 index 0000000..c31a008 --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/Event.java @@ -0,0 +1,4 @@ +package mc.core.eventbus; + +public interface Event { +} diff --git a/core/src/main/java/mc/core/eventbus/EventBus.java b/core/src/main/java/mc/core/eventbus/EventBus.java new file mode 100644 index 0000000..1a14331 --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/EventBus.java @@ -0,0 +1,89 @@ +package mc.core.eventbus; + +import javafx.util.Pair; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.helpers.MessageFormatter; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Stream; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EventBus { + @Getter + private static final EventBus instance = new EventBus(); + + private Queue eventQueue = new ConcurrentLinkedQueue<>(); + private Map, List>> subscribes = new HashMap<>(); + + private Stream getMethods(Object subscriberObject) { + return Stream.of(subscriberObject.getClass().getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(Subscriber.class)) + .filter(method -> method.getReturnType().equals(Void.TYPE)) + .filter(method -> method.getParameterCount() == 1) + .filter(method -> Event.class.isAssignableFrom(method.getParameterTypes()[0])); + } + + @SuppressWarnings("unchecked") + public void registerSubscribes(Object subscriberObject) { + getMethods(subscriberObject) + .forEach(method -> { + final Class type = (Class) method.getParameterTypes()[0]; + final List> pairs; + if (subscribes.containsKey(type)) { + pairs = subscribes.get(type); + } else { + pairs = new ArrayList<>(); + subscribes.put(type, pairs); + } + pairs.add(new Pair<>(subscriberObject, method)); + }); + } + + @SuppressWarnings("unchecked") + public void unregisterSubscribes(Object subscriberObject) { + getMethods(subscriberObject) + .forEach(method -> { + final Class type = (Class) method.getParameterTypes()[0]; + if (subscribes.containsKey(type)) { + final List> pairs = subscribes.get(type); + pairs.removeIf(pair -> pair.getKey() == subscriberObject); + + if (pairs.isEmpty()) { + subscribes.remove(type); + } + } + }); + } + + public void post(Event event) { + eventQueue.add(event); + } + + public void process() { + Event event; + while ((event = eventQueue.poll()) != null) { + final Class type = event.getClass(); + if (subscribes.containsKey(type)) { + final List> pairs = subscribes.get(type); + for (Pair pair : pairs) { + try { + pair.getValue().invoke(pair.getKey(), event); + } catch (IllegalAccessException | InvocationTargetException e) { + log.error(MessageFormatter.format("Invoke method '{}#{}'", + pair.getKey().getClass().getSimpleName(), + pair.getValue().getName()).getMessage(), + e + ); + } + } + } + } + } +} diff --git a/core/src/main/java/mc/core/eventbus/Subscriber.java b/core/src/main/java/mc/core/eventbus/Subscriber.java new file mode 100644 index 0000000..9a8aaee --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/Subscriber.java @@ -0,0 +1,11 @@ +package mc.core.eventbus; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(value= ElementType.METHOD) +@Retention(value= RetentionPolicy.RUNTIME) +public @interface Subscriber { +} diff --git a/core/src/main/java/mc/core/eventbus/events/CS_PlayerMoveEvent.java b/core/src/main/java/mc/core/eventbus/events/CS_PlayerMoveEvent.java new file mode 100644 index 0000000..1d9b6ae --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/events/CS_PlayerMoveEvent.java @@ -0,0 +1,23 @@ +package mc.core.eventbus.events; + +import lombok.Getter; +import lombok.Setter; +import mc.core.EntityLocation; +import mc.core.ImmutableEntityLocation; +import mc.core.eventbus.Event; +import mc.core.player.Player; + +@Getter +public class CS_PlayerMoveEvent implements Event { + private final Player player; + private final ImmutableEntityLocation oldLocation; + @Setter + private EntityLocation newLocation; + @Setter + private boolean recalcChunk = false; + + public CS_PlayerMoveEvent(Player player, EntityLocation oldLocation) { + this.player = player; + this.oldLocation = new ImmutableEntityLocation(oldLocation); + } +} diff --git a/core/src/main/java/mc/core/eventbus/events/SC_ChunkLoadEvent.java b/core/src/main/java/mc/core/eventbus/events/SC_ChunkLoadEvent.java new file mode 100644 index 0000000..15148ad --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/events/SC_ChunkLoadEvent.java @@ -0,0 +1,17 @@ +package mc.core.eventbus.events; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.core.eventbus.Event; +import mc.core.player.Player; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class SC_ChunkLoadEvent implements Event { + @Getter + private final Player player; + @Getter + private List needLoadChunks = new ArrayList<>(); +} diff --git a/core/src/main/java/mc/core/eventbus/events/SC_ChunkUnloadEvent.java b/core/src/main/java/mc/core/eventbus/events/SC_ChunkUnloadEvent.java new file mode 100644 index 0000000..d1a9517 --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/events/SC_ChunkUnloadEvent.java @@ -0,0 +1,17 @@ +package mc.core.eventbus.events; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.core.eventbus.Event; +import mc.core.player.Player; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class SC_ChunkUnloadEvent implements Event { + @Getter + private final Player player; + @Getter + private List needUnloadChunks = new ArrayList<>(); +} diff --git a/core/src/main/java/mc/core/eventbus/events/SC_PlayerMoveEvent.java b/core/src/main/java/mc/core/eventbus/events/SC_PlayerMoveEvent.java new file mode 100644 index 0000000..7f479ee --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/events/SC_PlayerMoveEvent.java @@ -0,0 +1,16 @@ +package mc.core.eventbus.events; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import mc.core.EntityLocation; +import mc.core.eventbus.Event; +import mc.core.player.Player; + +@RequiredArgsConstructor +@Getter +public class SC_PlayerMoveEvent implements Event { + private final Player player; + @Setter + private EntityLocation newLocation; +} diff --git a/core/src/main/java/mc/core/exception/ResourceUnloadedException.java b/core/src/main/java/mc/core/exception/ResourceUnloadedException.java new file mode 100644 index 0000000..eede649 --- /dev/null +++ b/core/src/main/java/mc/core/exception/ResourceUnloadedException.java @@ -0,0 +1,7 @@ +package mc.core.exception; + +public class ResourceUnloadedException extends RuntimeException { + public ResourceUnloadedException(String msg) { + super(msg); + } +} diff --git a/core/src/main/java/mc/core/network/BroadcastNetChannel.java b/core/src/main/java/mc/core/network/BroadcastNetChannel.java new file mode 100644 index 0000000..46e4da5 --- /dev/null +++ b/core/src/main/java/mc/core/network/BroadcastNetChannel.java @@ -0,0 +1,44 @@ +package mc.core.network; + +import lombok.RequiredArgsConstructor; +import mc.core.chat.MessageType; +import mc.core.player.Player; +import mc.core.text.Text; +import mc.core.text.Title; + +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class BroadcastNetChannel implements NetChannel { + private final Stream playerStream; + + @Override + public void sendTimeUpdate(final long time, final long age) { + playerStream.forEach(player -> player.getChannel().sendTimeUpdate(time, age)); + } + + @Override + public void sendChatMessage(final Text text, final MessageType type) { + playerStream.forEach(player -> player.getChannel().sendChatMessage(text, type)); + } + + @Override + public void sendTitle(final Title title) { + playerStream.forEach(player -> player.getChannel().sendTitle(title)); + } + + @Override + public void writeAndFlush(final SCPacket pkt) { + playerStream.forEach(player -> player.getChannel().writeAndFlush(pkt)); + } + + @Override + public void write(SCPacket pkt) { + playerStream.forEach(player -> player.getChannel().write(pkt)); + } + + @Override + public void flush() { + playerStream.forEach(player -> player.getChannel().flush()); + } +} diff --git a/core/src/main/java/mc/core/network/CSPacket.java b/core/src/main/java/mc/core/network/CSPacket.java new file mode 100644 index 0000000..a5e4afa --- /dev/null +++ b/core/src/main/java/mc/core/network/CSPacket.java @@ -0,0 +1,8 @@ +package mc.core.network; + +/** + * Пакеты Client->Server + */ +public interface CSPacket { + void readSelf(NetInputStream netStream); +} diff --git a/core/src/main/java/mc/core/network/NetChannel.java b/core/src/main/java/mc/core/network/NetChannel.java new file mode 100644 index 0000000..9714a02 --- /dev/null +++ b/core/src/main/java/mc/core/network/NetChannel.java @@ -0,0 +1,18 @@ +package mc.core.network; + +import mc.core.chat.MessageType; +import mc.core.text.Text; +import mc.core.text.Title; + +public interface NetChannel { + void sendTimeUpdate(long time, long age); + default void sendChatMessage(Text text) { + sendChatMessage(text, MessageType.CHAT_MESSAGE); + } + void sendChatMessage(Text text, MessageType type); + void sendTitle(Title title); + + void writeAndFlush(SCPacket pkt); + void write(SCPacket pkt); + void flush(); +} diff --git a/core/src/main/java/mc/core/network/NetInputStream.java b/core/src/main/java/mc/core/network/NetInputStream.java new file mode 100644 index 0000000..9262610 --- /dev/null +++ b/core/src/main/java/mc/core/network/NetInputStream.java @@ -0,0 +1,58 @@ +package mc.core.network; + +import com.flowpowered.nbt.Tag; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.io.InputStream; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class NetInputStream extends InputStream { + @Getter + @Setter + private int dataSize; + + public abstract boolean readBoolean(); + public abstract byte readByte(); + public int readBytes(byte[] buffer) { + return readBytes(buffer, 0, buffer.length); + } + public abstract int readBytes(byte[] buffer, int offset, int length); + public abstract int readUnsignedByte(); + public abstract int readUnsignedShort(); + public abstract short readShort(); + public abstract int readInt(); + public abstract int readVarInt(); + public abstract int readVarInt(AtomicInteger countReadBytes); + public abstract long readLong(); + public abstract float readFloat(); + public abstract double readDouble(); + public abstract String readString(); + public abstract UUID readUUID(); + public abstract Tag readNBT(); + + public abstract void skipBytes(int count); + + @Override + public int read() { + return readByte(); + } + + @Override + public int read(@NotNull byte[] b) { + return readBytes(b); + } + + @Override + public int read(@NotNull byte[] b, int off, int len) { + return readBytes(b, off, len); + } + + @Override + public long skip(long n) { + skipBytes((int) n); + return n; + } +} diff --git a/core/src/main/java/mc/core/network/NetOutputStream.java b/core/src/main/java/mc/core/network/NetOutputStream.java new file mode 100644 index 0000000..2f29ca5 --- /dev/null +++ b/core/src/main/java/mc/core/network/NetOutputStream.java @@ -0,0 +1,41 @@ +package mc.core.network; + +import com.flowpowered.nbt.Tag; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.UUID; + +public abstract class NetOutputStream extends OutputStream { + public abstract void writeBoolean(boolean value); + public abstract void writeByte(int value); + public abstract void writeUnsignedByte(int value); + public void writeBytes(byte[] buffer) { + writeBytes(buffer, 0, buffer.length); + } + public abstract void writeBytes(byte[] buffer, int offset, int lengtn); + public abstract void writeShort(int value); + public abstract void writeInt(int value); + public abstract void writeVarInt(int value); + public abstract void writeLong(long value); + public abstract void writeFloat(float value); + public abstract void writeDouble(double value); + public abstract void writeString(String value); + public abstract void writeUUID(UUID uuid); + public abstract void writeNBT(Tag tag); + + @Override + public void write(int b) throws IOException { + writeByte(b); + } + + @Override + public void write(byte[] b) throws IOException { + writeBytes(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + writeBytes(b, off, len); + } +} diff --git a/core/src/main/java/mc/core/network/SCPacket.java b/core/src/main/java/mc/core/network/SCPacket.java new file mode 100644 index 0000000..f2d63e5 --- /dev/null +++ b/core/src/main/java/mc/core/network/SCPacket.java @@ -0,0 +1,8 @@ +package mc.core.network; + +/** + * Пакеты Server->Client + */ +public interface SCPacket { + void writeSelf(NetOutputStream netStream); +} diff --git a/core/src/main/java/mc/core/network/Server.java b/core/src/main/java/mc/core/network/Server.java new file mode 100644 index 0000000..5ff2178 --- /dev/null +++ b/core/src/main/java/mc/core/network/Server.java @@ -0,0 +1,6 @@ +package mc.core.network; + +public interface Server { + void start() throws StartServerException; + void stop(); +} diff --git a/core/src/main/java/mc/core/network/StartServerException.java b/core/src/main/java/mc/core/network/StartServerException.java new file mode 100644 index 0000000..f1b7fdc --- /dev/null +++ b/core/src/main/java/mc/core/network/StartServerException.java @@ -0,0 +1,7 @@ +package mc.core.network; + +public class StartServerException extends Exception { + public StartServerException(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/mc/core/player/Player.java b/core/src/main/java/mc/core/player/Player.java new file mode 100644 index 0000000..43b0dc3 --- /dev/null +++ b/core/src/main/java/mc/core/player/Player.java @@ -0,0 +1,31 @@ +package mc.core.player; + +import mc.core.EntityLocation; +import mc.core.network.NetChannel; +import mc.core.world.World; + +import java.util.List; +import java.util.UUID; + +public interface Player { + int getId(); + UUID getUuid(); + String getName(); + boolean isOnline(); + + /** Compacted list of Chunk coords (x,z) */ + List getLoadedChunks(); + + NetChannel getChannel(); + void setChannel(NetChannel channel); + + EntityLocation getLocation(); + World getWorld(); + void setWorld(World world); + + boolean isFlying(); + void setFlying(boolean value); + + PlayerSettings getSettings(); + void setSettings(PlayerSettings settings); +} diff --git a/core/src/main/java/mc/core/player/PlayerManager.java b/core/src/main/java/mc/core/player/PlayerManager.java new file mode 100644 index 0000000..4fe5485 --- /dev/null +++ b/core/src/main/java/mc/core/player/PlayerManager.java @@ -0,0 +1,20 @@ +package mc.core.player; + +import mc.core.EntityLocation; +import mc.core.network.NetChannel; +import mc.core.world.World; + +import java.util.List; + +public interface PlayerManager { + Player createPlayer(String name, EntityLocation location, World world); + void joinServer(Player player); + void leftServer(Player player); + + Player getPlayer(String name); + List getPlayers(); + int getCountPlayers(); + NetChannel getBroadcastChannel(); + + Player getOfflinePlayer(String name); +} diff --git a/core/src/main/java/mc/core/player/PlayerMode.java b/core/src/main/java/mc/core/player/PlayerMode.java new file mode 100644 index 0000000..8927b80 --- /dev/null +++ b/core/src/main/java/mc/core/player/PlayerMode.java @@ -0,0 +1,15 @@ +package mc.core.player; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum PlayerMode { + SURVIVAL(0), + CREATIVE(1), + ADVENTURE(2), + SPECTATOR(3); + + private final int id; +} diff --git a/core/src/main/java/mc/core/player/PlayerSettings.java b/core/src/main/java/mc/core/player/PlayerSettings.java new file mode 100644 index 0000000..02b60d7 --- /dev/null +++ b/core/src/main/java/mc/core/player/PlayerSettings.java @@ -0,0 +1,52 @@ +package mc.core.player; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +public class PlayerSettings { + @RequiredArgsConstructor + public enum ChatMode { + ENABLED(0), + COMMANDS_ONLY(1), + HIDDEN(2); + + public static ChatMode getById(int id) { + if (id == 0) return ENABLED; + else if (id == 1) return COMMANDS_ONLY; + else return HIDDEN; + } + + @Getter + private final int id; + } + + @RequiredArgsConstructor + public enum Hand { + LEFT(0), + RIGHT(1); + + public static Hand getById(int id) { + if (id == 0) return LEFT; + else return RIGHT; + } + + @Getter + private final int id; + } + + private String locate = "en_US"; + private int viewDistance = 8; + private ChatMode chatMode = ChatMode.ENABLED; + private boolean chatColors = true; + private boolean capeEnabled = true, + jacketEnabled = true, + leftSleeveEnabled = true, + rightSleeveEnabled = true, + leftPantsLegEnabled = true, + rightPantsLegEnabled = true, + hatEnabled = true; + private Hand mainHand = Hand.RIGHT; +} diff --git a/core/src/main/java/mc/core/text/Text.java b/core/src/main/java/mc/core/text/Text.java new file mode 100644 index 0000000..4d470fe --- /dev/null +++ b/core/src/main/java/mc/core/text/Text.java @@ -0,0 +1,237 @@ +package mc.core.text; + +import com.google.common.collect.ImmutableList; +import lombok.Getter; + +import java.util.*; + +@Getter +public class Text { + private static final Text EMPTY = new Text(); + private static final Text NEW_LINE = new Text("\n", null, null, null); + + private final String content; + private final TextColor color; + private final TextStyle style; + private final ImmutableList children; + + private Text() { + content = ""; + color = null; + style = null; + children = null; + } + + private Text(String content, TextColor color, TextStyle style, ImmutableList children) { + this.content = content; + this.color = color; + this.style = style; + this.children = children; + } + + public boolean isEmpty() { + boolean result = (content == null || content.isEmpty()); + + if (children != null && !children.isEmpty()) { + for (Text child : children) { + result = result && child.isEmpty(); + } + } + + return result; + } + + public String toPlain() { + if (children != null && !children.isEmpty()) { + final StringJoiner sj = new StringJoiner(""); + children.forEach(child -> sj.add(child.toPlain())); + return sj.toString(); + } else { + return content; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Text text = (Text) o; + return Objects.equals(toPlain(), text.toPlain()); + } + + @Override + public int hashCode() { + return Objects.hash(toPlain()); + } + + public static class Builder { + @Getter + private String content; + @Getter + private TextColor color; + @Getter + private TextStyle style; + private List children; + + public Builder() { + this(""); + } + + public Builder(String content) { + this.content = content; + this.color = null; + this.style = null; + this.children = new ArrayList<>(); + } + + public Builder(Object... objects) { + this.children = new ArrayList<>(); + + for(Object obj : objects) { + if (obj instanceof String) { + if (this.content == null) { + this.content = (String) obj; + } else { + this.content = this.content.concat((String) obj); + } + } else if (obj instanceof TextStyle) { + if (this.style == null) { + this.style = TextStyle.none(); + } else { + this.style.merge((TextStyle) obj); + } + } else if (obj instanceof TextColor) { + this.color = (TextColor) obj; + } else if (obj instanceof Text) { + children.add((Text) obj); + } + } + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + public Builder color(TextColor color) { + this.color = color; + return this; + } + + public Builder style(TextStyle style) { + if (this.style == null) { + this.style = TextStyle.none(); + } else { + this.style.merge(style); + } + + return this; + } + + public Builder style(TextStyle... styles) { + if (this.style == null) { + this.style = TextStyle.none(); + } + + for(TextStyle style : styles) { + this.style.merge(style); + } + + return this; + } + + public Builder append(String string) { + return append(Text.of(string)); + } + + public Builder append(Text child) { + if (child != null) { + this.children.add(child); + } + return this; + } + + public Builder append(Text... children) { + Collections.addAll(this.children, children); + return this; + } + + public Text build() { + if (children.isEmpty() && (content == null || content.isEmpty())) { + return Text.EMPTY; + } + + if (children.size() == 1 && children.get(0) != null) { + return children.get(0); + } else { + return new Text( + content, + color, + style, + ImmutableList.copyOf(children) + ); + } + } + } + + public static Builder builder() { + return new Builder(); + } + + public static Builder builder(String content) { + return new Builder(content); + } + + public static Builder builder(Object... objects) { + return new Builder(objects); + } + + public static Text of() { + return EMPTY; + } + + public static Text of(String string) { + if (string == null || string.isEmpty()) { + return EMPTY; + } else if (string.equals("\n")) { + return NEW_LINE; + } else { + return new Text(string, null, null, null); + } + } + + public static Text of(Object... objects) { + TextColor color = null; + TextStyle style = null; + String content = null; + + for(Object obj : objects) { + if (obj instanceof String) { + if (content == null) { + content = (String) obj; + } else { + content = content.concat((String) obj); + } + } else if (obj instanceof TextStyle) { + if (style == null) { + style = (TextStyle) obj; + } else { + style.merge((TextStyle) obj); + } + } else if (obj instanceof TextColor) { + color = (TextColor) obj; + } else if (obj != null){ + if (content == null) { + content = obj.toString(); + } else { + content = content.concat(obj.toString()); + } + } + } + + if (content == null || content.isEmpty()) { + return EMPTY; + } else { + return new Text(content, color, style, null); + } + } +} diff --git a/core/src/main/java/mc/core/text/TextColor.java b/core/src/main/java/mc/core/text/TextColor.java new file mode 100644 index 0000000..56e3b16 --- /dev/null +++ b/core/src/main/java/mc/core/text/TextColor.java @@ -0,0 +1,28 @@ +package mc.core.text; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum TextColor { + BLACK ("black", '0'), + DARK_BLUE ("dark_blue", '1'), + DARK_GREEN ("dark_green", '2'), + DARK_AQUA ("dark_aqua", '3'), + DARK_RED ("dark_red", '4'), + DARK_PUEPLE("dark_purple", '5'), + GOLD ("gold", '6'), + GRAY ("gray", '7'), + DARK_GRAY ("dark_gray", '8'), + BLUE ("blue", '9'), + GREEN ("green", 'a'), + AQUA ("aqua", 'b'), + RED ("red", 'c'), + PUEPLE ("light_purple",'d'), + YELLOW ("yellow", 'e'), + WHITE ("white", 'f'); + + private final String name; + private final char code; +} diff --git a/core/src/main/java/mc/core/text/TextStyle.java b/core/src/main/java/mc/core/text/TextStyle.java new file mode 100644 index 0000000..9c5faca --- /dev/null +++ b/core/src/main/java/mc/core/text/TextStyle.java @@ -0,0 +1,65 @@ +package mc.core.text; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; +import java.util.Optional; + +@Getter +@Setter +public class TextStyle { + public static final TextStyle BOLD = new TextStyle(true, null, null, null, null); + public static final TextStyle ITALIC = new TextStyle(null, true, null, null, null); + public static final TextStyle UNDERLINE = new TextStyle(null, null, true, null, null); + public static final TextStyle STRIKETHOUGH = new TextStyle(null, null, null, true, null); + public static final TextStyle OBFUSCATED = new TextStyle(null, null, null, null, true); + public static final TextStyle RESET = new TextStyle(false, false, false, false, false); + + private static class OptionalBoolean { + private static final Optional TRUE = Optional.of(true); + private static final Optional FALSE = Optional.of(false); + private static final Optional NONE = Optional.empty(); + + static Optional of(boolean bool) { + return bool ? TRUE : FALSE; + } + + static Optional of(@Nullable Boolean bool) { + if (bool != null) { + return of(bool.booleanValue()); + } + return NONE; + } + } + + private Optional bold; + private Optional italic; + private Optional underline; + private Optional strikethrough; + private Optional obfuscated; + + public TextStyle(@Nullable Boolean bold, + @Nullable Boolean italic, + @Nullable Boolean underline, + @Nullable Boolean strikethrough, + @Nullable Boolean obfuscated) { + this.bold = OptionalBoolean.of(bold); + this.italic = OptionalBoolean.of(italic); + this.underline = OptionalBoolean.of(underline); + this.strikethrough = OptionalBoolean.of(strikethrough); + this.obfuscated = OptionalBoolean.of(obfuscated); + } + + public void merge(TextStyle style) { + if (style.bold.isPresent()) this.bold = style.bold; + if (style.italic.isPresent()) this.italic = style.italic; + if (style.underline.isPresent()) this.underline = style.underline; + if (style.strikethrough.isPresent()) this.strikethrough = style.strikethrough; + if (style.obfuscated.isPresent()) this.obfuscated = style.obfuscated; + } + + public static TextStyle none() { + return new TextStyle(null,null,null,null, null); + } +} diff --git a/core/src/main/java/mc/core/text/TextTemplate.java b/core/src/main/java/mc/core/text/TextTemplate.java new file mode 100644 index 0000000..87d4a70 --- /dev/null +++ b/core/src/main/java/mc/core/text/TextTemplate.java @@ -0,0 +1,146 @@ +package mc.core.text; + +import com.google.common.collect.ImmutableList; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.*; + +public class TextTemplate { + private final ImmutableList elements; + + private TextTemplate(ImmutableList elements) { + this.elements = elements; + } + + public Text apply(Object... objects) { + Map variableMap = new HashMap<>((objects.length % 2) == 1 ? (objects.length / 2) + 1 : (objects.length / 2)); + + boolean skipValue = false; + String key = null; + for (Object obj : objects) { + if (skipValue) { + skipValue = false; + continue; + } + + if (key == null) { + if (obj == null || obj.toString().trim().isEmpty()) { + skipValue = true; + continue; + } + + key = obj.toString().trim(); + } else { + variableMap.put(key, obj); + key = null; + } + } + + if (key != null) { + variableMap.put(key, ""); + } + + return apply(variableMap); + } + + public Text apply(Map variables) { + Text.Builder textBuilder = Text.builder(); + + for(Object obj : elements) { + if (obj instanceof Text) { + textBuilder.append((Text) obj); + } else if (obj instanceof Arg) { + Arg arg = (Arg) obj; + if (variables.containsKey(arg.getKey())) { + Object valueObj = variables.get(arg.getKey()); + + if (valueObj instanceof Text) { + textBuilder.append((Text) valueObj); + } else { + textBuilder.append(Text.of(valueObj, arg.getColor(), arg.getStyle())); + } + } else { + textBuilder.append(Text.of(arg.getDefaultValue(), arg.getColor(), arg.getStyle())); + } + } + } + + return textBuilder.build(); + } + + @RequiredArgsConstructor + @Getter + public static class Arg { + private final String key; + private final String defaultValue; + @Setter + private TextColor color; + @Setter + private TextStyle style; + } + + public static class Builder { + private List elements = new ArrayList<>(); + + public Builder append(Text element) { + this.elements.add(element); + return this; + } + + public Builder append(Text... elements) { + Collections.addAll(this.elements, elements); + return this; + } + + public Builder arg(String name) { + this.elements.add(new Arg(name, null)); + return this; + } + + public Builder arg(String name, String defaultValue) { + this.elements.add(new Arg(name, defaultValue)); + return this; + } + + public Builder arg(Object... objects) { + String key = null, + defaultValue = null; + TextColor color = null; + TextStyle style = null; + + for(Object obj : objects) { + if (obj instanceof String) { + if (key == null) { + key = (String) obj; + } else { + defaultValue = (String) obj; + } + } else if (obj instanceof TextColor) { + color = (TextColor) obj; + } else if (obj instanceof TextStyle) { + if (style == null) { + style = TextStyle.none(); + } + style.merge((TextStyle) obj); + } + } + + Arg arg = new Arg(key, defaultValue); + arg.setColor(color); + arg.setStyle(style); + this.elements.add(arg); + + return this; + } + + public TextTemplate build() { + return new TextTemplate(ImmutableList.copyOf(elements)); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/core/src/main/java/mc/core/text/Title.java b/core/src/main/java/mc/core/text/Title.java new file mode 100644 index 0000000..2a116fd --- /dev/null +++ b/core/src/main/java/mc/core/text/Title.java @@ -0,0 +1,30 @@ +package mc.core.text; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class Title { + private Text title = null; + private Text subtitle = null; + private Text textActionBar = null; + private Integer fadeInTime = null; + private Integer stayTime = null; + private Integer fadeOutTime = null; + private Boolean hide = null; + private Boolean reset = null; + + public void clear() { + this.title = null; + this.subtitle = null; + this.textActionBar = null; + this.fadeInTime = null; + this.stayTime = null; + this.fadeOutTime = null; + this.hide = null; + this.reset = null; + } +} diff --git a/core/src/main/java/mc/core/time/AbstractTimeProcessor.java b/core/src/main/java/mc/core/time/AbstractTimeProcessor.java new file mode 100644 index 0000000..09d7e9b --- /dev/null +++ b/core/src/main/java/mc/core/time/AbstractTimeProcessor.java @@ -0,0 +1,10 @@ +package mc.core.time; + +public abstract class AbstractTimeProcessor implements TimeProcessor { + private long worldAge = 0; + + @Override + public long getWorldAge() { + return worldAge++; + } +} diff --git a/core/src/main/java/mc/core/time/IdleTime.java b/core/src/main/java/mc/core/time/IdleTime.java new file mode 100644 index 0000000..1653d07 --- /dev/null +++ b/core/src/main/java/mc/core/time/IdleTime.java @@ -0,0 +1,15 @@ +package mc.core.time; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class IdleTime extends AbstractTimeProcessor { + + private long gameTime; +} diff --git a/core/src/main/java/mc/core/time/RealTime.java b/core/src/main/java/mc/core/time/RealTime.java new file mode 100644 index 0000000..cd56518 --- /dev/null +++ b/core/src/main/java/mc/core/time/RealTime.java @@ -0,0 +1,35 @@ +package mc.core.time; + +import java.util.Calendar; + +public class RealTime extends AbstractTimeProcessor { + private static final long DIFF = 21600L; + private static final long HOUR24 = 86400L; + private final Calendar calendar = Calendar.getInstance(); + private long lastUpdate = 0; + private long gameTime; + + private void calcRealTime() { + lastUpdate = System.currentTimeMillis(); + + calendar.setTimeInMillis(lastUpdate); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + long time = (lastUpdate - calendar.getTimeInMillis())/1000; + if (time < DIFF) time += HOUR24; + + gameTime = (long) ((time - DIFF) / 3.6); + } + + @Override + public long getGameTime() { + if ((System.currentTimeMillis() - lastUpdate) > 1000) { + calcRealTime(); + } + + return gameTime; + } +} diff --git a/core/src/main/java/mc/core/time/TimePerTick.java b/core/src/main/java/mc/core/time/TimePerTick.java new file mode 100644 index 0000000..0871b62 --- /dev/null +++ b/core/src/main/java/mc/core/time/TimePerTick.java @@ -0,0 +1,17 @@ +package mc.core.time; + +public class TimePerTick extends AbstractTimeProcessor { + private long gameTime; + + public void setStartGameTime(long value) { + gameTime = value; + } + + @Override + public long getGameTime() { + gameTime++; + if (gameTime > 24000) gameTime = 0; + + return gameTime; + } +} diff --git a/core/src/main/java/mc/core/time/TimeProcessor.java b/core/src/main/java/mc/core/time/TimeProcessor.java new file mode 100644 index 0000000..31f3bba --- /dev/null +++ b/core/src/main/java/mc/core/time/TimeProcessor.java @@ -0,0 +1,6 @@ +package mc.core.time; + +public interface TimeProcessor { + long getGameTime(); + long getWorldAge(); +} diff --git a/core/src/main/java/mc/core/utils/CompactedCoords.java b/core/src/main/java/mc/core/utils/CompactedCoords.java new file mode 100644 index 0000000..c05884c --- /dev/null +++ b/core/src/main/java/mc/core/utils/CompactedCoords.java @@ -0,0 +1,22 @@ +package mc.core.utils; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CompactedCoords { + public static int compressXZ(int x, int z) { + if (x < Short.MIN_VALUE || x > Short.MAX_VALUE || + z < Short.MIN_VALUE || z > Short.MAX_VALUE) { + log.warn("Coord over range: [{},{}]", x, z); + } + + return ((x & 0xFFFF) << 16) | (z & 0xFFFF); + } + + public static int[] uncompressXZ(int compactValue) { + return new int[]{ + compactValue >> 16, + (compactValue & 0x8000) > 0 ? compactValue | 0xFFFF0000 : compactValue & 0xFFFF + }; + } +} diff --git a/core/src/main/java/mc/core/utils/NibbleArray.java b/core/src/main/java/mc/core/utils/NibbleArray.java new file mode 100644 index 0000000..e8514f7 --- /dev/null +++ b/core/src/main/java/mc/core/utils/NibbleArray.java @@ -0,0 +1,65 @@ +package mc.core.utils; + +import lombok.RequiredArgsConstructor; +import mc.core.world.block.BlockLocation; + +/** + * Сжатый массив значений 0-15 + */ +@RequiredArgsConstructor +public class NibbleArray { + private final byte[] data; + + public NibbleArray(int capacity) { + this.data = new byte[capacity]; + } + + public NibbleArray() { + this.data = new byte[2048]; + } + + private int coordsToIndex(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + private int nibbleIndex(int index) { + return index >> 1; + } + + private boolean isLowerNibble(int index) { + return (index & 1) == 0; + } + + public int get(BlockLocation location) { + return get(location.getX(), location.getY(), location.getZ()); + } + + public int get(int x, int y, int z) { + final int idx = coordsToIndex(x, y, z); + + final int ni = nibbleIndex(idx); + return isLowerNibble(idx) ? this.data[ni] & 0x0F : this.data[ni] >> 4 & 0x0F; + } + + public void set(BlockLocation location, int value) { + set(location.getX(), location.getY(), location.getZ(), value); + } + + public void set(int x, int y, int z, int value) { + if (value < 0) value = 0; + else if (value > 15) value = 15; + + final int idx = coordsToIndex(x, y, z); + final int ni = nibbleIndex(idx); + + if (isLowerNibble(idx)) { + this.data[ni] = (byte)(value); + } else { + this.data[ni] = (byte)(this.data[ni] | value << 4); + } + } + + public byte[] getRawData() { + return data; + } +} diff --git a/core/src/main/java/mc/core/world/Biome.java b/core/src/main/java/mc/core/world/Biome.java new file mode 100644 index 0000000..3f4d555 --- /dev/null +++ b/core/src/main/java/mc/core/world/Biome.java @@ -0,0 +1,80 @@ +package mc.core.world; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; + +@RequiredArgsConstructor +public enum Biome { + OCEAN(0), + PLAINS(1), + DESERT(2), + EXTREME_HILLS(3), + FOREST(4), + TAIGA(5), + SWAMPLAND(6), + RIVER(7), + HELL(8), + SKY(9), + FROZEN_OCEAN(10), + FROZEN_RIVER(11), + ICE_PLAINS(12), + ICE_MOUNTAINS(13), + MUSHROOM_ISLAND(14), + MUSHROOM_ISLAND_SHORE(15), + BEACH(16), + DESERT_HILLS(17), + FOREST_HILLS(18), + TAIGA_HILLS(19), + EXTREME_HILLS_EDGE(20), + JUNGLE(21), + JUNGLE_HILLS(22), + JUNGLE_EDGE(23), + DEEP_OCEAN(24), + STONE_BEACH(25), + COLD_BEACH(26), + BIRCH_FOREST(27), + BIRCH_FOREST_HILLS(28), + ROOFED_FOREST(29), + TAIGA_COLD(30), + TAIGA_COLD_HILLS(31), + REDWOOD_TAIGA(32), + REDWOOD_TAIGA_HILLS(33), + EXTREME_HILLS_WITH_TREES(34), + SAVANNA(35), + SAVANNA_ROCK(36), + MESA(37), + MESA_ROCK(38), + MESA_CLEAR_ROCK(39), + VOID(127), + MUTATED_PLAINS(129), + MUTATED_DESERT(130), + MUTATED_EXTREME_HILLS(131), + MUTATED_FOREST(132), + MUTATED_TAIGA(133), + MUTATED_SWAMPLAND(134), + MUTATED_ICE_FLATS(140), + MUTATED_JUNGLE(149), + MUTATED_JUNGLE_EDGE(151), + MUTATED_BIRCH_FOREST(155), + MUTATED_BIRCH_FOREST_HILLS(156), + MUTATED_ROOFED_FOREST(157), + MUTATED_TAIGA_COLD(158), + MUTATED_REDWOOD_TAIGA(160), + MUTATED_REDWOOD_TAIGA_HILLS(161), + MUTATED_EXTREME_HILLS_WITH_TREES(162), + MUTATED_SAVANNA(163), + MUTATED_SAVANNA_ROCK(164), + MUTATED_MESA(165), + MUTATED_MESA_ROCK(166), + MUTATED_MESA_CLEAR_ROCK(167); + + public static Biome getById(final int id) { + return Arrays.stream(Biome.values()).filter(biome -> biome.id == id).findFirst().orElse(Biome.PLAINS); + } + + @Getter + private final int id; +} diff --git a/core/src/main/java/mc/core/world/World.java b/core/src/main/java/mc/core/world/World.java new file mode 100644 index 0000000..dc76256 --- /dev/null +++ b/core/src/main/java/mc/core/world/World.java @@ -0,0 +1,63 @@ +package mc.core.world; + +import mc.core.EntityLocation; +import mc.core.world.block.Block; +import mc.core.world.block.BlockLocation; +import mc.core.world.chunk.Chunk; + +public interface World { + String getName(); + WorldType getType(); + + EntityLocation getSpawn(); + + void setSpawn(EntityLocation location); + + default void setSpawn(double x, double y, double z, float yaw, float pitch) { + setSpawn(new EntityLocation(x, y, z, yaw, pitch)); + } + + default void setSpawn(double x, double y, double z) { + setSpawn(x, y, z, 0f, 0f); + } + + /** + * Получить чанк по его координатам + * @param x chunk X + * @param z chunk Z + * @return {@link mc.core.world.chunk.Chunk} + */ + Chunk getChunk(int x, int z); + + /** + * Получить чанк по глобальным координатам блока + * @param location {@link BlockLocation} + * @return {@link Chunk} + */ + default Chunk getChunk(BlockLocation location) { + return getChunk(location.getX() >> 4, location.getZ() >> 4); + } + + /** + * Установить чанк по координатам + * @param x глобальный X + * @param z глобальный Z + * @param chunk {@link mc.core.world.chunk.Chunk} + */ + void setChunk(int x, int z, Chunk chunk); + + /** + * Получить блок по его координатам + * @param x X + * @param y Y + * @param z Z + * @return {@link Block} + */ + Block getBlock(int x, int y, int z); + + default Block getBlock(BlockLocation location) { + return getBlock(location.getX(), location.getY(), location.getZ()); + } + + void setBlock(Block block); +} diff --git a/core/src/main/java/mc/core/world/WorldType.java b/core/src/main/java/mc/core/world/WorldType.java new file mode 100644 index 0000000..86f2680 --- /dev/null +++ b/core/src/main/java/mc/core/world/WorldType.java @@ -0,0 +1,13 @@ +package mc.core.world; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum WorldType { + DEFAULT("default"), + FLAT("flat"); + + private final String name; +} diff --git a/core/src/main/java/mc/core/world/block/AbstractBlock.java b/core/src/main/java/mc/core/world/block/AbstractBlock.java new file mode 100644 index 0000000..ab57849 --- /dev/null +++ b/core/src/main/java/mc/core/world/block/AbstractBlock.java @@ -0,0 +1,23 @@ +package mc.core.world.block; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public abstract class AbstractBlock implements Block { + @Setter + private BlockLocation location; + private int light = 0; + private final BlockType type; + + protected AbstractBlock(BlockType type) { + this.type = type; + } + + @Override + public void setLight(int light) { + if (light > 15) this.light = 15; + else if (light < 0) this.light = 0; + else this.light = light; + } +} diff --git a/core/src/main/java/mc/core/world/block/Block.java b/core/src/main/java/mc/core/world/block/Block.java new file mode 100644 index 0000000..1774a61 --- /dev/null +++ b/core/src/main/java/mc/core/world/block/Block.java @@ -0,0 +1,17 @@ +package mc.core.world.block; + +import com.flowpowered.nbt.CompoundTag; + +public interface Block { + int getLight(); + void setLight(int light); + BlockType getType(); + BlockLocation getLocation(); + + default CompoundTag getNBTData() { + return null; + } + + default void setNBTData(CompoundTag nbtData) { + } +} diff --git a/core/src/main/java/mc/core/world/block/BlockFactory.java b/core/src/main/java/mc/core/world/block/BlockFactory.java new file mode 100644 index 0000000..2f2410a --- /dev/null +++ b/core/src/main/java/mc/core/world/block/BlockFactory.java @@ -0,0 +1,18 @@ +package mc.core.world.block; + +//TODO избавится от этого "аппендикса" +@Deprecated +public class BlockFactory { + + public Block create(BlockType blockType, int x, int y, int z) { + return new EmbeddedBlock(blockType, x, y, z); + } + + /** For first-time generation */ + private class EmbeddedBlock extends AbstractBlock { + EmbeddedBlock(BlockType type, int x, int y, int z) { + super(type); + setLocation(new BlockLocation(x, y, z)); + } + } +} diff --git a/core/src/main/java/mc/core/world/block/BlockLocation.java b/core/src/main/java/mc/core/world/block/BlockLocation.java new file mode 100644 index 0000000..ff6fd5f --- /dev/null +++ b/core/src/main/java/mc/core/world/block/BlockLocation.java @@ -0,0 +1,32 @@ +package mc.core.world.block; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class BlockLocation implements Cloneable { + private int x, y, z; + + public static BlockLocation ZERO() { + return new BlockLocation(0,0,0); + } + + public void setXYZ(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + @Override + public BlockLocation clone() { + try { + return (BlockLocation) super.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + return ZERO(); + } + } +} diff --git a/core/src/main/java/mc/core/world/block/BlockType.java b/core/src/main/java/mc/core/world/block/BlockType.java new file mode 100644 index 0000000..13eb1e6 --- /dev/null +++ b/core/src/main/java/mc/core/world/block/BlockType.java @@ -0,0 +1,515 @@ +package mc.core.world.block; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.stream.Stream; + +@Slf4j +@RequiredArgsConstructor +public enum BlockType { + + AIR(0, 0), + + STONE (1, 0), + STONE_MOSS(48, 0), + + GRANITE (1, 1), + POLISHED_GRANITE(1, 2), + + DIORITE(1, 3), + ANDESITE(1, 5), + + GRASS(2, 0), + PATH(208, 0), + + DIRT(3, 0), + + /** Farmland, Dry, Moisture 0 */ + FARMLAND (60, 0), + /** Farmland, Dry, Moisture 1 */ + FARMLAND_1(60, 1), + /** Farmland, Dry, Moisture 2 */ + FARMLAND_2(60, 2), + /** Farmland, Dry, Moisture 3 */ + FARMLAND_3(60, 3), + /** Farmland, Dry, Moisture 4 */ + FARMLAND_4(60, 4), + /** Farmland, Dry, Moisture 5 */ + FARMLAND_5(60, 5), + /** Farmland, Dry, Moisture 6 */ + FARMLAND_6(60, 6), + /** Farmland, Dry, Moisture 7 */ + FARMLAND_7(60, 7), + + COBBLESTONE(4, 0), + BEDROCK(7, 0), + + /** Water, flowing, Level 7 (Source) */ + WATER_FLOWING (8, 0), + /** Water, flowing, Level 6 */ + WATER_FLOWING_1 (8, 1), + /** Water, flowing, Level 5 */ + WATER_FLOWING_2 (8, 2), + /** Water, flowing, Level 4 */ + WATER_FLOWING_3 (8, 3), + /** Water, flowing, Level 3 */ + WATER_FLOWING_4 (8, 4), + /** Water, flowing, Level 2 */ + WATER_FLOWING_5 (8, 5), + /** Water, flowing, Level 1 */ + WATER_FLOWING_6 (8, 6), + /** Water, flowing, Level 0 */ + WATER_FLOWING_7 (8, 7), + /** Water, flowing, Level 15 */ + WATER_FLOWING_8 (8, 8), + /** Water, flowing, Level 14 */ + WATER_FLOWING_9 (8, 9), + /** Water, flowing, Level 13 */ + WATER_FLOWING_10(8, 10), + /** Water, flowing, Level 12 */ + WATER_FLOWING_11(8, 11), + /** Water, flowing, Level 11 */ + WATER_FLOWING_12(8, 12), + /** Water, flowing, Level 10 */ + WATER_FLOWING_13(8, 13), + /** Water, flowing, Level 9 */ + WATER_FLOWING_14(8, 14), + /** Water, flowing, Level 8 */ + WATER_FLOWING_15(8, 15), + + /** Water, still, Level 7 (Source) */ + WATER_STILL (9, 0), + /** Water, still, Level 6 */ + WATER_STILL_1 (9, 1), + /** Water, still, Level 5 */ + WATER_STILL_2 (9, 2), + /** Water, still, Level 4 */ + WATER_STILL_3 (9, 3), + /** Water, still, Level 3 */ + WATER_STILL_4 (9, 4), + /** Water, still, Level 2 */ + WATER_STILL_5 (9, 5), + /** Water, still, Level 1 */ + WATER_STILL_6 (9, 6), + /** Water, still, Level 0 */ + WATER_STILL_7 (9, 7), + /** Water, still, Level 15 */ + WATER_STILL_8 (9, 8), + /** Water, still, Level 14 */ + WATER_STILL_9 (9, 9), + /** Water, still, Level 13 */ + WATER_STILL_10(9, 10), + /** Water, still, Level 12 */ + WATER_STILL_11(9, 11), + /** Water, still, Level 11 */ + WATER_STILL_12(9, 12), + /** Water, still, Level 10 */ + WATER_STILL_13(9, 13), + /** Water, still, Level 9 */ + WATER_STILL_14(9, 14), + /** Water, still, Level 8 */ + WATER_STILL_15(9, 15), + + /** Lava, flowing, Level 7 (Source) */ + LAVA_FLOWING (10, 0), + /** Lava, flowing, Level 6 */ + LAVA_FLOWING_1 (10, 1), + /** Lava, flowing, Level 5 */ + LAVA_FLOWING_2 (10, 2), + /** Lava, flowing, Level 4 */ + LAVA_FLOWING_3 (10, 3), + /** Lava, flowing, Level 3 */ + LAVA_FLOWING_4 (10, 4), + /** Lava, flowing, Level 2 */ + LAVA_FLOWING_5 (10, 5), + /** Lava, flowing, Level 1 */ + LAVA_FLOWING_6 (10, 6), + /** Lava, flowing, Level 0 */ + LAVA_FLOWING_7 (10, 7), + /** Lava, flowing, Level 15 */ + LAVA_FLOWING_8 (10, 8), + /** Lava, flowing, Level 14 */ + LAVA_FLOWING_9 (10, 9), + /** Lava, flowing, Level 13 */ + LAVA_FLOWING_10(10, 10), + /** Lava, flowing, Level 12 */ + LAVA_FLOWING_11(10, 11), + /** Lava, flowing, Level 11 */ + LAVA_FLOWING_12(10, 12), + /** Lava, flowing, Level 10 */ + LAVA_FLOWING_13(10, 13), + /** Lava, flowing, Level 9 */ + LAVA_FLOWING_14(10, 14), + /** Lava, flowing, Level 8 */ + LAVA_FLOWING_15(10, 15), + + /** Lava, still, Level 7 (Source) */ + LAVA_STILL (11, 0), + /** Lava, still, Level 6 */ + LAVA_STILL_1 (11, 1), + /** Lava, still, Level 5 */ + LAVA_STILL_2 (11, 2), + /** Lava, still, Level 4 */ + LAVA_STILL_3 (11, 3), + /** Lava, still, Level 3 */ + LAVA_STILL_4 (11, 4), + /** Lava, still, Level 2 */ + LAVA_STILL_5 (11, 5), + /** Lava, still, Level 1 */ + LAVA_STILL_6 (11, 6), + /** Lava, still, Level 0 */ + LAVA_STILL_7 (11, 7), + /** Lava, still, Level 15 */ + LAVA_STILL_8 (11, 8), + /** Lava, still, Level 14 */ + LAVA_STILL_9 (11, 9), + /** Lava, still, Level 13 */ + LAVA_STILL_10(11, 10), + /** Lava, still, Level 12 */ + LAVA_STILL_11(11, 11), + /** Lava, still, Level 11 */ + LAVA_STILL_12(11, 12), + /** Lava, still, Level 10 */ + LAVA_STILL_13(11, 13), + /** Lava, still, Level 9 */ + LAVA_STILL_14(11, 14), + /** Lava, still, Level 8 */ + LAVA_STILL_15(11, 15), + + SAND (12, 0), + SANDSTONE(24, 0), + + GRAVEL(13, 0), + + ORE_GOLD (14, 0), + ORE_IRON (15, 0), + ORE_COAL (16, 0), + ORE_LAPIS (21, 0), + ORE_DIAMOND (56, 0), + ORE_REDSTONE (73, 0), + ORE_GLOWING_REDSTONE(74, 0), + ORE_EMERALD (129, 0), + + // Upright + WOOD_OAK (17, 0), + WOOD_SPRUCE (17, 1), + WOOD_BIRCH (17, 2), + WOOD_JUNGLE (17, 3), + WOOD_ACACIA (162, 0), + WOOD_OAK_DARK(162, 1), + + // East/West + WOOD_OAK_EW (17, 4), + WOOD_SPRUCE_EW (17, 5), + WOOD_BIRCH_EW (17, 6), + WOOD_JUNGLE_EW (17, 7), + WOOD_ACACIA_EW (162, 4), + WOOD_OAK_DARK_EW(162, 5), + + // North/South + WOOD_OAK_NS (17, 8), + WOOD_SPRUCE_NS (17, 9), + WOOD_BIRCH_NS (17, 10), + WOOD_JUNGLE_NS (17, 11), + WOOD_ACACIA_NS (162, 8), + WOOD_OAK_DARK_NS(162, 9), + + PLANK_WOOD_OAK (5, 0), + PLANK_WOOD_SPRUCE (5, 1), + PLANK_WOOD_BIRCH (5, 2), + PLANK_WOOD_JUNGLE (5, 3), + PLANK_WOOD_ACACIA (5, 4), + PLANK_WOOD_OAK_DARK(5, 5), + + DOOR_LOW_OAK_EAST(64, 0), + DOOR_LOW_OAK_SOUTH(64, 1), + DOOR_LOW_OAK_WEST(64, 2), + DOOR_LOW_OAK_NORTH(64, 3), + DOOR_LOW_OAK_EAST_OPENED(64, 4), + DOOR_LOW_OAK_SOUTH_OPENED(64, 5), + DOOR_LOW_OAK_WEST_OPENED(64, 6), + DOOR_LOW_OAK_NORTH_OPENED(64, 7), + + DOOR_UP_OAK_LEFT(64, 8), + DOOR_UP_OAK_RIGHT(64, 9), + DOOR_UP_OAK_LEFT_POWERED(64, 10), + DOOR_UP_OAK_RIGHT_POWERED(64, 11), + DOOR_UP_OAK_12(64, 12), + DOOR_UP_OAK_13(64, 13), + DOOR_UP_OAK_14(64, 14), + DOOR_UP_OAK_15(64, 15), + + FENCE_OAK(85, 0), + + // Decay after Tree Update + LEAVES_OAK (18, 0), + LEAVES_SPRUCE (18, 1), + LEAVES_BIRCH (18, 2), + LEAVES_JUNGLE (18, 3), + LEAVES_ACACIA (161, 0), + LEAVES_OAK_DARK(161, 1), + + // No Decay + LEAVES_OAK2 (18, 4), + LEAVES_SPRUCE2 (18, 5), + LEAVES_BIRCH2 (18, 6), + LEAVES_JUNGLE2 (18, 7), + LEAVES_ACACIA2 (161, 4), + LEAVES_OAK_DARK2(161, 5), + + // Decay + LEAVES_OAK3 (18, 8), + LEAVES_SPRUCE3 (18, 9), + LEAVES_BIRCH3 (18, 10), + LEAVES_JUNGLE3 (18, 11), + LEAVES_ACACIA3 (161, 8), + LEAVES_OAK_DARK3(161, 9), + + // No decay, unused + @Deprecated + LEAVES_OAK4 (18, 12), + @Deprecated + LEAVES_SPRUCE4 (18, 13), + @Deprecated + LEAVES_BIRCH4 (18, 14), + @Deprecated + LEAVES_JUNGLE4 (18, 15), + @Deprecated + LEAVES_ACACIA4 (161, 12), + @Deprecated + LEAVES_OAK_DARK4(161, 13), + + COBWEB(30, 0), + TALLGRASS(31, 1), + DANDELION(37, 0), + + FLOWER_POPPY (38, 0), + FLOWER_BLUE_ORCHID (38, 1), + FLOWER_ALLIUM (38, 2), + FLOWER_AZURE_BLUET (38, 3), + FLOWER_TULIP_RED (38, 4), + FLOWER_TULIP_ORANGE(38, 5), + FLOWER_TULIP_WHITE (38, 6), + FLOWER_TULIP_PINK (38, 7), + FLOWER_OXEYE_DAISY (38, 8), + + MUSHROOM_BROWN(39, 0), + MUSHROOM_RED (40, 0), + + MUSHROOM_BLOCK_BROWN_ALL_INSIDE(99, 0), + MUSHROOM_BLOCK_BROWN_NW (99, 1), + MUSHROOM_BLOCK_BROWN_NORT (99, 2), + MUSHROOM_BLOCK_BROWN_NE (99, 3), + MUSHROOM_BLOCK_BROWN_WEST (99, 4), + MUSHROOM_BLOCK_BROWN_CENTER (99, 5), + MUSHROOM_BLOCK_BROWN_EAST (99, 6), + MUSHROOM_BLOCK_BROWN_SW (99, 7), + MUSHROOM_BLOCK_BROWN_SOUTH (99, 8), + MUSHROOM_BLOCK_BROWN_SE (99, 9), + MUSHROOM_BLOCK_BROWN_STEM (99, 10), + MUSHROOM_BLOCK_BROWN_ALL_OUSIDE(99, 14), + MUSHROOM_BLOCK_BROWN_ALL_STEM (99, 15), + + MUSHROOM_BLOCK_RED_ALL_INSIDE(100, 0), + MUSHROOM_BLOCK_RED_NW (100, 1), + MUSHROOM_BLOCK_RED_NORT (100, 2), + MUSHROOM_BLOCK_RED_NE (100, 3), + MUSHROOM_BLOCK_RED_WEST (100, 4), + MUSHROOM_BLOCK_RED_CENTER (100, 5), + MUSHROOM_BLOCK_RED_EAST (100, 6), + MUSHROOM_BLOCK_RED_SW (100, 7), + MUSHROOM_BLOCK_RED_SOUTH (100, 8), + MUSHROOM_BLOCK_RED_SE (100, 9), + MUSHROOM_BLOCK_RED_STEM (100, 10), + MUSHROOM_BLOCK_RED_ALL_OUSIDE(100, 14), + MUSHROOM_BLOCK_RED_ALL_STEM (100, 15), + + OBSIDIAN(49, 0), + + TORCH_EAST (50, 1), + TORCH_WEST (50, 2), + TORCH_SOUTH(50, 3), + TORCH_NORTH(50, 4), + TORCH_UP (50, 5), + + MONSTER_SPAWNER(52, 0), + + CHEST_NORTH(54, 2, "minecraft:chest"), + CHEST_SOUTH(54, 3, "minecraft:chest"), + CHEST_WEST (54, 4, "minecraft:chest"), + CHEST_EAST (54, 5, "minecraft:chest"), + + RAIL_NS (66, 0), + RAIL_EW (66, 1), + RAIL_ASCENDING_EAST (66, 2), + RAIL_ASCENDING_WEST (66, 3), + RAIL_ASCENDING_NORTH(66, 4), + RAIL_ASCENDING_SOUTH(66, 5), + RAIL_CURVED_SE (66, 6), + RAIL_CURVED_SW (66, 7), + RAIL_CURVED_NW (66, 8), + RAIL_CURVED_NE (66, 9), + + SNOW(78, 0), + + CLAY(82, 0), + CLAY_HARDENED(172, 0), + + /** Sugar canes (Age 0) */ + SUGAR_CANES(83, 0), + /** Sugar canes (Age 1) */ + SUGAR_CANES_1(83, 1), + /** Sugar canes (Age 2) */ + SUGAR_CANES_2(83, 2), + /** Sugar canes (Age 3) */ + SUGAR_CANES_3(83, 3), + /** Sugar canes (Age 4) */ + SUGAR_CANES_4(83, 4), + /** Sugar canes (Age 5) */ + SUGAR_CANES_5(83, 5), + /** Sugar canes (Age 6) */ + SUGAR_CANES_6(83, 6), + /** Sugar canes (Age 7) */ + SUGAR_CANES_7(83, 7), + /** Sugar canes (Age 8) */ + SUGAR_CANES_8(83, 8), + /** Sugar canes (Age 9) */ + SUGAR_CANES_9(83, 9), + /** Sugar canes (Age 10) */ + SUGAR_CANES_10(83, 10), + /** Sugar canes (Age 11) */ + SUGAR_CANES_11(83, 11), + /** Sugar canes (Age 12) */ + SUGAR_CANES_12(83, 12), + /** Sugar canes (Age 13) */ + SUGAR_CANES_13(83, 13), + /** Sugar canes (Age 14) */ + SUGAR_CANES_14(83, 14), + /** Sugar canes (Age 15) */ + SUGAR_CANES_15(83, 15), + + PUMPKIN_SOUTH(86, 0), + PUMPKIN_WEST (86, 1), + PUMPKIN_NORTH(86, 2), + PUMPKIN_EAST (86, 3), + + STONE_MONSTER_EGG(97, 0), + + GLASS_PANE(102, 0), + + VINE (106, 0), + VINE_SOUTH(106, 1), + VINE_WEST (106, 2), + VINE_SW (106, 3), + VINE_NORTH(106, 4), + VINE_NS (106, 5), + VINE_NW (106, 6), + VINE_NSW (106, 7), // North, South, West + VINE_EAST (106, 8), + VINE_ES (106, 9), + VINE_EW (106, 10), + VINE_ESW (106, 11), + VINE_EN (106, 12), + VINE_ENS (106, 13), + VINE_ENW (106, 14), + VINE_ENSW (106, 14), + + WATERLILY(111, 0), + + LILAC(175, 1), + DOUBLE_TALLGRASS(175, 2), + ROSE_BUSH(175, 4), + PEONY(175, 5), + ROSE_BUSH_10(175, 10), + + /** Wheat (Age 0) */ + WHEAT (59, 0), + /** Wheat (Age 1) */ + WHEAT_1(59, 1), + /** Wheat (Age 2) */ + WHEAT_2(59, 2), + /** Wheat (Age 3) */ + WHEAT_3(59, 3), + /** Wheat (Age 4) */ + WHEAT_4(59, 4), + /** Wheat (Age 5) */ + WHEAT_5(59, 5), + /** Wheat (Age 6) */ + WHEAT_6(59, 6), + /** Wheat (Age 7) */ + WHEAT_7(59, 7), + + /** Carrots (Age 0) */ + CARROTS(141, 0), + /** Carrots (Age 1) */ + CARROTS_1(141, 1), + /** Carrots (Age 2) */ + CARROTS_2(141, 2), + /** Carrots (Age 3) */ + CARROTS_3(141, 3), + /** Carrots (Age 4) */ + CARROTS_4(141, 4), + /** Carrots (Age 5) */ + CARROTS_5(141, 5), + /** Carrots (Age 6) */ + CARROTS_6(141, 6), + /** Carrots (Age 7) */ + CARROTS_7(141, 7), + + /** Potatoes (Age 0) */ + POTATOES (142, 0), + /** Potatoes (Age 1) */ + POTATOES_1(142, 1), + /** Potatoes (Age 2) */ + POTATOES_2(142, 2), + /** Potatoes (Age 3) */ + POTATOES_3(142, 3), + /** Potatoes (Age 4) */ + POTATOES_4(142, 4), + /** Potatoes (Age 5) */ + POTATOES_5(142, 5), + /** Potatoes (Age 6) */ + POTATOES_6(142, 6), + /** Potatoes (Age 7) */ + POTATOES_7(142, 7), + + /** Beetroot (Age 0) */ + BEETROOT (207, 0), + /** Beetroot (Age 1) */ + BEETROOT_1(207, 1), + /** Beetroot (Age 2) */ + BEETROOT_2(207, 2), + /** Beetroot (Age 3) */ + BEETROOT_3(207, 3); + + BlockType(int id, int meta) { + this.id = id; + this.meta = meta; + this.namedId = null; + } + + public static BlockType getByIdMeta(int id, int meta) { + if (id < 0) { + log.warn("Incorrect id \"{}\"", id); + return BEDROCK; + } + + Stream stream = Arrays.stream(BlockType.values()); + return stream.filter(blockType -> blockType.id == id && blockType.meta == meta) + .findFirst() + .orElseGet(() -> { + log.warn("Unknow block type: {}:{}", id, meta); + return BEDROCK; + }); + } + + @Getter + private final int id; + @Getter + private final int meta; + @Getter + private final String namedId; +} diff --git a/core/src/main/java/mc/core/world/chunk/Chunk.java b/core/src/main/java/mc/core/world/chunk/Chunk.java new file mode 100644 index 0000000..4266487 --- /dev/null +++ b/core/src/main/java/mc/core/world/chunk/Chunk.java @@ -0,0 +1,65 @@ +package mc.core.world.chunk; + +import mc.core.world.Biome; +import mc.core.world.block.Block; + +/* 16x256x16 */ +public interface Chunk { + /** + * Глобальная координата X + * @return + */ + int getX(); + + /** + * Глобальная координата Z + * @return + */ + int getZ(); + + /** + * Получить секцию чанка + * @param height высота (0-15) + * @return {@link mc.core.world.chunk.ChunkSection} + */ + ChunkSection getChunkSection(int height); + + /** + * Установить секцию чанка + * @param height высота (0-15) + * @param chunkSection {@link mc.core.world.chunk.ChunkSection} + */ + void setChunkSection(int height, ChunkSection chunkSection); + + /** + * Получить блок по глобальным координатам секции чанка + * @param x global X + * @param y global Y + * @param z global Z + * @return {@link Block} + */ + Block getBlock(int x, int y, int z); + void setBlock(Block block); + + int getSkyLight(int x, int y, int z); + void setSkyLight(int x, int y, int z, int lightLevel); + + int getAddition(int x, int y, int z); + void setAddition(int x, int y, int z, int value); + + /** + * Получить тип биома по глобальным координатам + * @param x global X + * @param z global Z + * @return {@link mc.core.world.Biome} + */ + Biome getBiome(int x, int z); + + /** + * Указать данные по биому + * @param x global X + * @param z global Z + * @param biome {@link mc.core.world.Biome} + */ + void setBiome(int x, int z, Biome biome); +} diff --git a/core/src/main/java/mc/core/world/chunk/ChunkProvider.java b/core/src/main/java/mc/core/world/chunk/ChunkProvider.java new file mode 100644 index 0000000..d363fdb --- /dev/null +++ b/core/src/main/java/mc/core/world/chunk/ChunkProvider.java @@ -0,0 +1,14 @@ +package mc.core.world.chunk; + +public interface ChunkProvider { + /** + * Получить чанк по координатам + * @param x глобальный X + * @param z глобальный Z + * @return {@link mc.core.world.chunk.Chunk} + */ + Chunk getChunk(int x , int z); + + void saveChunk(Chunk chunk); + void saveChunk(Chunk... chunks); +} diff --git a/core/src/main/java/mc/core/world/chunk/ChunkSection.java b/core/src/main/java/mc/core/world/chunk/ChunkSection.java new file mode 100644 index 0000000..ebe5cd8 --- /dev/null +++ b/core/src/main/java/mc/core/world/chunk/ChunkSection.java @@ -0,0 +1,53 @@ +package mc.core.world.chunk; + +import mc.core.world.block.Block; + +/** + * Секция чанка размером 16x16x16 блоков + */ +public interface ChunkSection { + Chunk getParent(); + void setParent(Chunk chunk); + + /** + * Высота + * @return + */ + int getY(); + + /** + * Получить блок по локальным координатам секции чанка + * @param localX local X (0-15) + * @param localY local Y (0-15) + * @param localZ local Z (0-15) + * @return {@link Block} + */ + Block getBlock(int localX, int localY, int localZ); + + /** + * Установить блок + * @param block {@link mc.core.world.block.Block} + */ + void setBlock(Block block); + + /** + * Получить данные о естественной подсветке + * @param localX локальный X (0-15) + * @param localY локальный Y (0-15) + * @param localZ локальный Z (0-15) + * @return integer значение 0-15, где 0 - это света нет, а 15 - получает прямой солнечный свет + */ + int getSkyLight(int localX, int localY, int localZ); + + /** + * Указать данные о естественной подсветке + * @param localX локальный X (0-15) + * @param localY локальный Y (0-15) + * @param localZ локальный Z (0-15) + * @param lightLevel значение 0-15, где 0 - это света нет, а 15 - получает прямой солнечный свет + */ + void setSkyLight(int localX, int localY, int localZ, int lightLevel); + + int getAddition(int localX, int localY, int localZ); + void setAddition(int localX, int localY, int localZ, int value); +} diff --git a/core/src/main/resources/spring.xml b/core/src/main/resources/spring.xml new file mode 100644 index 0000000..bb81a1a --- /dev/null +++ b/core/src/main/resources/spring.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/java/mc/core/EntityLocationTest.java b/core/src/test/java/mc/core/EntityLocationTest.java new file mode 100644 index 0000000..0e31fdc --- /dev/null +++ b/core/src/test/java/mc/core/EntityLocationTest.java @@ -0,0 +1,91 @@ +package mc.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.*; + +class EntityLocationTest { + private static final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + private static final double minD = 0.0d, maxD = 10.0d; + private static final float minF = 0.0f, maxF = 359.9f; + private double x, y, z; + private float yaw, pitch; + + @BeforeEach + void before() { + x = rnd.nextDouble(minD, maxD); + y = rnd.nextDouble(minD, maxD); + z = rnd.nextDouble(minD, maxD); + yaw = rnd.nextFloat() * (maxF - minF) + minF; + pitch = rnd.nextFloat() * (maxF - minF) + minF; + } + + @Test + void equals_() { + EntityLocation loc1 = new EntityLocation(x, y, z, yaw, pitch); + EntityLocation loc2 = new EntityLocation(x, y, z, yaw, pitch); + assertEquals(loc1, loc2); + + loc2 = new EntityLocation(x+1, y+2, z-3, yaw, pitch); + assertNotEquals(loc1, loc2); + + loc2 = new EntityLocation(x, y, z, yaw-1, pitch+2); + assertNotEquals(loc1, loc2); + } + + @Test + void clone_() { + EntityLocation locOrig = new EntityLocation(x, y, z, yaw, pitch); + EntityLocation locClone = locOrig.clone(); + assertEquals(locOrig, locClone); + assertNotSame(locOrig, locClone); + } + + @Test + void getBlockXZ() { + EntityLocation location; + + location = new EntityLocation(0d, 0, 0d, 0f, 0f); + assertEquals(0, location.getBlockX()); + assertEquals(0, location.getBlockZ()); + + location.setXYZ(0.1d, 0, 0.1d); + assertEquals(0, location.getBlockX()); + assertEquals(0, location.getBlockZ()); + + location.setXYZ(0.5d, 0, 0.5d); + assertEquals(0, location.getBlockX()); + assertEquals(0, location.getBlockZ()); + + location.setXYZ(0.9d, 0, 0.9d); + assertEquals(0, location.getBlockX()); + assertEquals(0, location.getBlockZ()); + + location.setXYZ(1d, 0, 1d); + assertEquals(1, location.getBlockX()); + assertEquals(1, location.getBlockZ()); + + location.setXYZ(-0.1d, 0, -0.1d); + assertEquals(-1, location.getBlockX()); + assertEquals(-1, location.getBlockZ()); + + location.setXYZ(-0.5d, 0, -0.5d); + assertEquals(-1, location.getBlockX()); + assertEquals(-1, location.getBlockZ()); + + location.setXYZ(-0.9d, 0, -0.9d); + assertEquals(-1, location.getBlockX()); + assertEquals(-1, location.getBlockZ()); + + location.setXYZ(-1d, 0, -1d); + assertEquals(-1, location.getBlockX()); + assertEquals(-1, location.getBlockZ()); + + location.setXYZ(-1.1d, 0, -1.1d); + assertEquals(-2, location.getBlockX()); + assertEquals(-2, location.getBlockZ()); + } +} diff --git a/core/src/test/java/mc/core/ImmutableEntityLocationTest.java b/core/src/test/java/mc/core/ImmutableEntityLocationTest.java new file mode 100644 index 0000000..0335538 --- /dev/null +++ b/core/src/test/java/mc/core/ImmutableEntityLocationTest.java @@ -0,0 +1,33 @@ +package mc.core; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ImmutableEntityLocationTest { + + @Test + void setValue() { + EntityLocation location = new ImmutableEntityLocation(1d, 2d, 3d, 4f, 5f); + + assertThrows(UnsupportedOperationException.class, () -> { + location.setX(1); + location.setY(1); + location.setZ(1); + location.setYaw(1); + location.setPitch(1); + location.setXYZ(1, 2, 3); + location.setYawPitch(1, 2); + location.set(EntityLocation.ZERO()); + }); + } + + @Test + void clone_() { + EntityLocation locOrig = new ImmutableEntityLocation(1d, 2d, 3d, 4f, 5f); + EntityLocation locClone = locOrig.clone(); + + assertEquals(locOrig, locClone); + } +} \ No newline at end of file diff --git a/core/src/test/java/mc/core/TestEventBus.java b/core/src/test/java/mc/core/TestEventBus.java new file mode 100644 index 0000000..beb19c8 --- /dev/null +++ b/core/src/test/java/mc/core/TestEventBus.java @@ -0,0 +1,125 @@ +package mc.core; + +import javafx.util.Pair; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import mc.core.eventbus.Event; +import mc.core.eventbus.EventBus; +import mc.core.eventbus.Subscriber; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.internal.util.reflection.Whitebox; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class TestEventBus { + private List resultList = new ArrayList<>(); + + @SuppressWarnings("unchecked") + private Map, List>> getEventBusFieldSubscribes() { + return (Map, List>>) + Whitebox.getInternalState(EventBus.getInstance(), "subscribes"); + } + + @BeforeEach + @SuppressWarnings("unchecked") + void before() { + getEventBusFieldSubscribes().clear(); + ((Queue) Whitebox.getInternalState(EventBus.getInstance(), "eventQueue")).clear(); + } + + + @Test + void testRegisterSubscribes() { + DumbEventHandler handler = new DumbEventHandler(); + EventBus.getInstance().registerSubscribes(handler); + + Map, List>> subscribes = getEventBusFieldSubscribes(); + assertEquals(1, subscribes.size()); + + List> pairs = subscribes.values().iterator().next(); + assertEquals(1, pairs.size()); + + Pair pair = pairs.get(0); + assertSame(handler, pair.getKey()); + assertEquals("corectSubscribe", pair.getValue().getName()); + } + + @Test + void testUnregisterSubscribes() { + DumbEventHandler handler = new DumbEventHandler(); + EventBus.getInstance().registerSubscribes(handler); + + EventBus.getInstance().unregisterSubscribes(handler); + + Map, List>> subscribes = getEventBusFieldSubscribes(); + assertEquals(0, subscribes.size()); + } + + @Test + @SuppressWarnings("unchecked") + void testPost() { + EventBus.getInstance().post(new DumbEvent()); + + Queue eventQueue = (Queue) Whitebox.getInternalState(EventBus.getInstance(), "eventQueue"); + assertEquals(1, eventQueue.size()); + } + + @Test + void testProcess() { + Stream.of(new DumbEventHandler("D1 "), new DumbEventHandler("D2 ")) + .forEach(handler -> EventBus.getInstance().registerSubscribes(handler)); + + Stream.of(new DumbEvent("message 1"), new DumbEvent("message 2")) + .forEach(event -> EventBus.getInstance().post(event)); + + EventBus.getInstance().process(); + + assertEquals(4, resultList.size()); + assertEquals("D1 message 1", resultList.get(0)); + assertEquals("D2 message 1", resultList.get(1)); + assertEquals("D1 message 2", resultList.get(2)); + assertEquals("D2 message 2", resultList.get(3)); + } + + @AllArgsConstructor + @NoArgsConstructor + private class DumbEvent implements Event { + String message; + } + + @AllArgsConstructor + @NoArgsConstructor + public class DumbEventHandler { + private String prefix = ""; + + @Subscriber + public void corectSubscribe(DumbEvent event) { + resultList.add(prefix + event.message); + } + + @Subscriber + public Object incorectSubscribeReturnType(DumbEvent event) { + return null; + } + + @Subscriber + public void incorrectSubscriberTypeParameter(Object object) { + } + + @Subscriber + public void incorrectSubscriberManyParameters(DumbEvent event, Object object) { + } + + public void someMethod() { + } + } +} diff --git a/core/src/test/java/mc/core/TestSpringConfig.java b/core/src/test/java/mc/core/TestSpringConfig.java new file mode 100644 index 0000000..34dc1de --- /dev/null +++ b/core/src/test/java/mc/core/TestSpringConfig.java @@ -0,0 +1,34 @@ +package mc.core; + +import mc.core.world.World; +import mc.core.world.chunk.Chunk; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Configuration +public class TestSpringConfig { + @Bean() + public World simpleMockWorld() { + return mock(World.class); + } + + @Bean + public World chunkedMockWorld() { + World world = mock(World.class); + when(world.getChunk(anyInt(), anyInt())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + + Chunk chunk = mock(Chunk.class); + when(chunk.getX()).thenReturn((int) args[0]); + when(chunk.getZ()).thenReturn((int) args[1]); + + return chunk; + }); + + return world; + } +} diff --git a/core/src/test/java/mc/core/text/TextTest.java b/core/src/test/java/mc/core/text/TextTest.java new file mode 100644 index 0000000..fbb417b --- /dev/null +++ b/core/src/test/java/mc/core/text/TextTest.java @@ -0,0 +1,76 @@ +package mc.core.text; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TextTest { + @Test + void toPlain() { + final String m1 = "mes"; + final String m2 = "sage"; + final String message = m1 + m2; + + assertEquals(message, Text.of(message).toPlain()); + assertEquals(message, Text.builder(message).build().toPlain()); + assertEquals(message, Text.builder(Text.of(message)).build().toPlain()); + assertEquals(message, Text.builder().append(message).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(message)).build().toPlain()); + + assertEquals(message, Text.builder(m1, m2).build().toPlain()); + assertEquals(message, Text.builder(Text.of(m1), Text.of(m2)).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(m1), Text.of(m2)).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(m1)).append(Text.of(m2)).build().toPlain()); + + + } + + @Test + void equals_() { + assertEquals(Text.of(), Text.of("")); + assertEquals(Text.of(), Text.builder().build()); + assertEquals(Text.of(), Text.builder("").build()); + assertEquals(Text.of(), Text.builder().append().build()); + assertEquals(Text.of(), Text.builder().append("").build()); + + assertNotEquals(Text.of(), Text.of("??")); + assertNotEquals(Text.of(), Text.builder("??").build()); + assertNotEquals(Text.of(), Text.builder().append("??").build()); + + assertEquals(Text.of("message"), Text.builder("message").build()); + assertEquals(Text.of("message"), Text.builder(Text.of("message")).build()); + assertEquals(Text.of("message"), Text.builder().append("message").build()); + assertEquals(Text.of("message"), Text.builder().append(Text.of("message")).build()); + } + + @Test + void isEmpty() { + assertTrue(Text.of().isEmpty()); + assertTrue(Text.of((String) null).isEmpty()); + assertTrue(Text.of((Text) null).isEmpty()); + assertTrue(Text.of("").isEmpty()); + assertTrue(Text.of("", "").isEmpty()); + + assertTrue(Text.builder().build().isEmpty()); + assertTrue(Text.builder((String) null).build().isEmpty()); + assertTrue(Text.builder((Text) null).build().isEmpty()); + assertTrue(Text.builder("").build().isEmpty()); + assertTrue(Text.builder("", "").build().isEmpty()); + assertTrue(Text.builder(Text.of()).build().isEmpty()); + assertTrue(Text.builder(Text.of(), Text.of()).build().isEmpty()); + + assertTrue(Text.builder().append().build().isEmpty()); + assertTrue(Text.builder().append((String) null).build().isEmpty()); + assertTrue(Text.builder().append((Text) null).build().isEmpty()); + assertTrue(Text.builder().append("").build().isEmpty()); + assertTrue(Text.builder().append(Text.of()).build().isEmpty()); + assertTrue(Text.builder().append(Text.of(), Text.of()).build().isEmpty()); + assertTrue(Text.builder().append(Text.of()).append(Text.of()).build().isEmpty()); + + assertFalse(Text.of("??").isEmpty()); + assertFalse(Text.builder("??").build().isEmpty()); + assertFalse(Text.builder(Text.of("??")).build().isEmpty()); + assertFalse(Text.builder().append("??").build().isEmpty()); + assertFalse(Text.builder().append(Text.of("??")).build().isEmpty()); + } +} diff --git a/core/src/test/java/mc/core/utils/CompactedCoordsTest.java b/core/src/test/java/mc/core/utils/CompactedCoordsTest.java new file mode 100644 index 0000000..ab75543 --- /dev/null +++ b/core/src/test/java/mc/core/utils/CompactedCoordsTest.java @@ -0,0 +1,37 @@ +package mc.core.utils; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CompactedCoordsTest { + private static Stream streamTestParams() { + return Stream.of( + Arguments.of(Short.MIN_VALUE, Short.MIN_VALUE), + Arguments.of(Short.MIN_VALUE, Short.MAX_VALUE), + Arguments.of(Short.MAX_VALUE, Short.MAX_VALUE), + Arguments.of(Short.MAX_VALUE, Short.MIN_VALUE), + Arguments.of(0, 0), + Arguments.of(-1, -1), + Arguments.of(-1, 1), + Arguments.of(1, 1), + Arguments.of(1, -1) + ); + } + + @ParameterizedTest + @MethodSource("streamTestParams") + void testCompress(int x, int z) { + final int compressXZ = CompactedCoords.compressXZ(x, z); + int[] xz = CompactedCoords.uncompressXZ(compressXZ); + + assertTrue(x == xz[0] && z == xz[1], + String.format("x = %d, vx = %d; z = %d, vz = %d", + x, xz[0], + z, xz[1])); + } +} diff --git a/core/src/test/java/mc/core/world/block/BlockLocationTest.java b/core/src/test/java/mc/core/world/block/BlockLocationTest.java new file mode 100644 index 0000000..d716995 --- /dev/null +++ b/core/src/test/java/mc/core/world/block/BlockLocationTest.java @@ -0,0 +1,39 @@ +package mc.core.world.block; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class BlockLocationTest { + private static final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + private static final int minI = 0, maxI = 10; + private int x, y, z; + + @BeforeEach + void before() { + x = rnd.nextInt(minI, maxI); + y = rnd.nextInt(minI, maxI); + z = rnd.nextInt(minI, maxI); + } + + @Test + void equals_() { + BlockLocation loc1 = new BlockLocation(x, y, z); + BlockLocation loc2 = new BlockLocation(x, y, z); + assertEquals(loc1, loc2); + + loc2 = new BlockLocation(x+1, y+2, z-3); + assertNotEquals(loc1, loc2); + } + + @Test + void clone_() { + BlockLocation locOrig = new BlockLocation(x, y, z); + BlockLocation locClone = locOrig.clone(); + assertEquals(locOrig, locClone); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..aee0b52 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'mc-server' + +include('core') // Core