Archived
0

Merge branch 'release/zero'

This commit is contained in:
2019-01-29 15:45:18 +03:00
72 changed files with 3502 additions and 1 deletions

11
.gitignore vendored
View File

@@ -5,3 +5,14 @@ out/
*.ipr
*.iws
*.ids
## GRADLE ##
.gradle/
build/
gradle/
gradlew
gradlew.*
## PROJECT ##
libs/
*.log

View File

@@ -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` - указание этого параметра создаст скрипт-запускатор

168
build.gradle Normal file
View File

@@ -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=<TOKEN>
* где
* - <TOKEN> - сгенерированный токен учетки "сонара"
*/
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
}

85
core/README.MD Normal file
View File

@@ -0,0 +1,85 @@
# Core
Ядро сервера
## Spring beans
### ConfigFromSpring
Настройка параметров сервера через конфигурацию "спринга".
Имеются следующие настройки:
* `descriptionServer` - описание сервера (aka "Motd")
* `favicon` - файл с иконкой сервера
* `maxPlayers` - максимальная вместимость сервера
**Implements:** `mc.core.Config`
**Bean example:**
```xml
<bean id="config" class="mc.core.embedded.ConfigFromSpring">
<property name="descriptionServer" value="MC Core"/>
<property name="maxPlayers" value="100"/>
<property name="favicon" value="icon.png"/>
</bean>
```
### GameLoop
**Bean example:**
Доступные параметры:
* `gameTimer` - бин, управляющий ходом времени
```xml
<bean id="gameLoop" class="mc.core.GameLoop">
<property name="gameTimer" ref="timeProcessor"/>
</bean>
```
### IdleTime
Игровое время суток застывает на указанной отметке.
Доступные параметры:
* `gameTime` - отметка времени (long)
**Implements:** `mc.core.time.TimeProcessor`
**Bean example:**
```xml
<bean id="idleTime" class="mc.core.time.IdleTime">
<property name="gameTime" value="1000"/>
</bean>
```
### TimePerTick
Игровое время суток соответствует игровым тикам (20 tps)
Доступные параметры:
* `startGameTime` - стартовое время (long)
**Implements:** `mc.core.time.TimeProcessor`
**Bean example:**
```xml
<bean id="timePerTick" class="mc.core.time.TimePerTick">
<property name="startGameTime" value="1000"/>
</bean>
```
### RealTime
Игровое время суток соответствует реальному времени
**Implements:** `mc.core.time.TimeProcessor`
**Bean example:**
```xml
<bean id="realTime" class="mc.core.time.RealTime"/>
```

14
core/build.gradle Normal file
View File

@@ -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')
}

View File

@@ -0,0 +1,7 @@
package mc.core;
public interface Config {
int getMaxPlayers();
String getDescriptionServer();
byte[] getFaviconBase64();
}

View File

@@ -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<Integer> 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()
);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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) {
}
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,13 @@
package mc.core.chat;
import mc.core.player.Player;
import java.util.Optional;
public interface CommandExecutor {
String getName();
Optional<String[]> getAliases();
Optional<String> getUsage();
String getDescription();
void execute(Player sender, String... args);
}

View File

@@ -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<String, CommandExecutor> commands = new HashMap<>();
@PostConstruct
public void init() {
Map<String, CommandExecutor> 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<CommandExecutor> getAllCommands() {
return commands.values();
}
}

View File

@@ -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;
}

View File

@@ -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()
);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<Player> 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;
}
}

View File

@@ -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() {
}
}

View File

@@ -0,0 +1,4 @@
package mc.core.eventbus;
public interface Event {
}

View File

@@ -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<Event> eventQueue = new ConcurrentLinkedQueue<>();
private Map<Class<? extends Event>, List<Pair<Object, Method>>> subscribes = new HashMap<>();
private Stream<Method> 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<? extends Event> type = (Class<? extends Event>) method.getParameterTypes()[0];
final List<Pair<Object, Method>> 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<? extends Event> type = (Class<? extends Event>) method.getParameterTypes()[0];
if (subscribes.containsKey(type)) {
final List<Pair<Object, Method>> 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<? extends Event> type = event.getClass();
if (subscribes.containsKey(type)) {
final List<Pair<Object, Method>> pairs = subscribes.get(type);
for (Pair<Object, Method> 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
);
}
}
}
}
}
}

View File

@@ -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 {
}

View File

@@ -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);
}
}

View File

@@ -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<Integer> needLoadChunks = new ArrayList<>();
}

View File

@@ -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<Integer> needUnloadChunks = new ArrayList<>();
}

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
package mc.core.exception;
public class ResourceUnloadedException extends RuntimeException {
public ResourceUnloadedException(String msg) {
super(msg);
}
}

View File

@@ -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<Player> 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());
}
}

View File

@@ -0,0 +1,8 @@
package mc.core.network;
/**
* Пакеты Client->Server
*/
public interface CSPacket {
void readSelf(NetInputStream netStream);
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,8 @@
package mc.core.network;
/**
* Пакеты Server->Client
*/
public interface SCPacket {
void writeSelf(NetOutputStream netStream);
}

View File

@@ -0,0 +1,6 @@
package mc.core.network;
public interface Server {
void start() throws StartServerException;
void stop();
}

View File

@@ -0,0 +1,7 @@
package mc.core.network;
public class StartServerException extends Exception {
public StartServerException(Throwable cause) {
super(cause);
}
}

View File

@@ -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<Integer> 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);
}

View File

@@ -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<Player> getPlayers();
int getCountPlayers();
NetChannel getBroadcastChannel();
Player getOfflinePlayer(String name);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<Text> children;
private Text() {
content = "";
color = null;
style = null;
children = null;
}
private Text(String content, TextColor color, TextStyle style, ImmutableList<Text> 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<Text> 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<Text> 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);
}
}
}

View File

@@ -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;
}

View File

@@ -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<Boolean> TRUE = Optional.of(true);
private static final Optional<Boolean> FALSE = Optional.of(false);
private static final Optional<Boolean> NONE = Optional.empty();
static Optional<Boolean> of(boolean bool) {
return bool ? TRUE : FALSE;
}
static Optional<Boolean> of(@Nullable Boolean bool) {
if (bool != null) {
return of(bool.booleanValue());
}
return NONE;
}
}
private Optional<Boolean> bold;
private Optional<Boolean> italic;
private Optional<Boolean> underline;
private Optional<Boolean> strikethrough;
private Optional<Boolean> 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);
}
}

View File

@@ -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<Object> elements;
private TextTemplate(ImmutableList<Object> elements) {
this.elements = elements;
}
public Text apply(Object... objects) {
Map<String, Object> 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<String, Object> 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<Object> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,10 @@
package mc.core.time;
public abstract class AbstractTimeProcessor implements TimeProcessor {
private long worldAge = 0;
@Override
public long getWorldAge() {
return worldAge++;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
package mc.core.time;
public interface TimeProcessor {
long getGameTime();
long getWorldAge();
}

View File

@@ -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
};
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}

View File

@@ -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));
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<BlockType> 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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config />
<bean id="playerManager" class="mc.core.embedded.FakePlayerManager"/>
<bean id="gameLoop" class="mc.core.GameLoop">
<property name="gameTimer">
<bean class="mc.core.time.TimePerTick"/>
</property>
</bean>
<bean id="server" class="mc.core.embedded.FakeServer"/>
</beans>

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> resultList = new ArrayList<>();
@SuppressWarnings("unchecked")
private Map<Class<? extends Event>, List<Pair<Object, Method>>> getEventBusFieldSubscribes() {
return (Map<Class<? extends Event>, List<Pair<Object, Method>>>)
Whitebox.getInternalState(EventBus.getInstance(), "subscribes");
}
@BeforeEach
@SuppressWarnings("unchecked")
void before() {
getEventBusFieldSubscribes().clear();
((Queue<Event>) Whitebox.getInternalState(EventBus.getInstance(), "eventQueue")).clear();
}
@Test
void testRegisterSubscribes() {
DumbEventHandler handler = new DumbEventHandler();
EventBus.getInstance().registerSubscribes(handler);
Map<Class<? extends Event>, List<Pair<Object, Method>>> subscribes = getEventBusFieldSubscribes();
assertEquals(1, subscribes.size());
List<Pair<Object, Method>> pairs = subscribes.values().iterator().next();
assertEquals(1, pairs.size());
Pair<Object, Method> 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<Class<? extends Event>, List<Pair<Object, Method>>> subscribes = getEventBusFieldSubscribes();
assertEquals(0, subscribes.size());
}
@Test
@SuppressWarnings("unchecked")
void testPost() {
EventBus.getInstance().post(new DumbEvent());
Queue<Event> eventQueue = (Queue<Event>) 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() {
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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<Arguments> 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]));
}
}

View File

@@ -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);
}
}

3
settings.gradle Normal file
View File

@@ -0,0 +1,3 @@
rootProject.name = 'mc-server'
include('core') // Core