diff --git a/README.MD b/README.MD index 3d7113f..352a11a 100644 --- a/README.MD +++ b/README.MD @@ -4,6 +4,104 @@ ![codename: ZERO](https://img.shields.io/badge/codename-ZERO-509.svg?style=flat) ![protocol: 1.12.2](https://img.shields.io/badge/protocol-1.12.2-075.svg?style=flat) +Написанный с нуля сервер **Minecraft 1.12.2**. + +На данный момент может только показывать информацию о себе. Подключение к серверу не возможно. + --- -* Java 11 \ No newline at end of file +## Требования + +* Java 11 + +## Запуск + +Для запуска требуются некоторые файлы настроек. Для их генерации можно воспользоваться командой инициализации окружения: + +```shell +java -jar server.jar --init +``` + +После выполнить запуск самого сервера: + +```shell +java -jar server.jar +``` + +### Параметры командной строки + +`--init` +Инициализация окружения. Генерирует необхидимые для запуска сервера файлы. + +`--config=path/to/config.yml` +Указание альтернативного пути для конфигурационного файла сервера. + +`--logconfig=path/to/logback.xml` +Указание альтернативного пути для конфигурационного файла логгера (logback). + +## Настройки + +### Стилизованный текст + +Файл конфига позволяет использовать специальные коды для добавления цвета и стиля в текст. + +| Код | Цвет | Код | Стиль | +| ---- | ------------------------------------------- | ---- | --------------------------------------------------------------- | +| `&0` | Black | `&l` | Bold | +| `&1` | Dark Blue | `&o` | Italic | +| `&2` | Dark Green | `&n` | Underline | +| `&3` | Dark Aqua | `&m` | Strikethrough | +| `&4` | Dark Red | `&k` | Obfuscated | +| `&5` | Dark Purple | +| `&6` | Gold | +| `&7` | Gray | +| `&8` | Dark Gray | +| `&9` | Blue | +| `&a` | Green | +| `&b` | Aqua | +| `&c` | Red | +| `&d` | Purple | +| `&e` | Yellow | +| `&f` | White | + + +### motd + +```yaml +motd: | + mc-project :: ZERO + develop by DmitriyMX +``` + +Настройка надписи, которая будет отображаться в списке серверов у клиента. Максимум может состоять из двух строк. + +### disconnect-reason + +```yaml +disconnect-reason: Server is not available. +``` + +Причина отключения от сервера. Количество строк не ограничено. + +### players + +```yaml +players: + max-online: 0 + online: 0 +``` + +Фиктивные данные об онлайне сервера. + +### icon + +```yaml +icon: + enable: true + path: favicon.png +``` + +Использовать значок сервера. +Настройка `enable` говорит о факте использования значка, а в настройке `path` указывается путь к значку. + +Формат значка должен быть **PNG** и быть размерами **64x64 px**. Другие форматы или размеры _не поддерживаются_. \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 23c4b67..0000000 --- a/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -/* -Запуск - gradle run - */ - -import ru.dmitriymx.gradle.plugin.LibsPlugin -import ru.dmitriymx.gradle.plugin.LogicPlugin - -plugins { - id 'java' - id 'application' -} - -apply plugin: LibsPlugin -apply plugin: LogicPlugin - -project.group = logic.getProperty1('project.group') -project.version = logic.getProperty1('project.version') -jar.archiveBaseName.set(logic.getProperty1('project.name')) - -compileJava { - sourceCompatibility = targetCompatibility = JavaVersion.VERSION_11 - options.encoding = 'UTF-8' -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - annotationProcessor libs.lombok - compileOnly libs.lombok - compileOnly libs.annotations - - implementation libs.logger.slf4j - implementation libs.logger.logback - - implementation libs.dagger2.implementation - annotationProcessor libs.dagger2.annotationProcessor -} - -application.mainClassName = 'mc.server.Main' \ No newline at end of file diff --git a/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LibsExtention.java b/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LibsExtention.java deleted file mode 100644 index 752fe92..0000000 --- a/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LibsExtention.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.dmitriymx.gradle.extention; - -import java.util.List; - -public class LibsExtention { - - public final String lombok = "org.projectlombok:lombok:1.18.12"; - public final String annotations = "com.google.code.findbugs:jsr305:3.0.2"; - public final LoggerLibs logger = new LoggerLibs(); - public final Dagger2Libs dagger2 = new Dagger2Libs(); - - public static final class LoggerLibs { - private final String slf4j_version = "1.7.30"; - private final String logback_version = "1.2.3"; - - public final List slf4j = List.of( - "org.slf4j:slf4j-api:" + slf4j_version, - "org.slf4j:jcl-over-slf4j:" + slf4j_version - ); - public final String slf4j_simple = "org.slf4j:slf4j-simple:" + slf4j_version; - - public final List logback = List.of( - "ch.qos.logback:logback-core:" + logback_version, - "ch.qos.logback:logback-classic:" + logback_version - ); - } - - public static final class Dagger2Libs { - private final String dagger2_version = "2.33"; - - public final String implementation = "com.google.dagger:dagger:" + dagger2_version; - public final String annotationProcessor = "com.google.dagger:dagger-compiler:" + dagger2_version; - } -} diff --git a/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LogicExtention.java b/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LogicExtention.java deleted file mode 100644 index c596a13..0000000 --- a/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LogicExtention.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.dmitriymx.gradle.extention; - -import org.gradle.api.Project; - -public class LogicExtention { - private final Project project; - - public LogicExtention(Project project) { - this.project = project; - } - - public String getProperty1(String propertyName1, String propertyName2) { - return (String) (project.hasProperty(propertyName1) ? project.property(propertyName1) : project.property(propertyName2)); - } - - public String getProperty1(String propertyName) { - return (String) (project.hasProperty(propertyName) ? project.property(propertyName) : null); - } -} diff --git a/buildSrc/src/main/java/ru/dmitriymx/gradle/plugin/LibsPlugin.java b/buildSrc/src/main/java/ru/dmitriymx/gradle/plugin/LibsPlugin.java deleted file mode 100644 index 243fa5d..0000000 --- a/buildSrc/src/main/java/ru/dmitriymx/gradle/plugin/LibsPlugin.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.dmitriymx.gradle.plugin; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import ru.dmitriymx.gradle.extention.LibsExtention; - -public class LibsPlugin implements Plugin { - - @Override - public void apply(Project project) { - project.getExtensions().create("libs", LibsExtention.class); - } -} diff --git a/buildSrc/src/main/java/ru/dmitriymx/gradle/plugin/LogicPlugin.java b/buildSrc/src/main/java/ru/dmitriymx/gradle/plugin/LogicPlugin.java deleted file mode 100644 index 4068cf4..0000000 --- a/buildSrc/src/main/java/ru/dmitriymx/gradle/plugin/LogicPlugin.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.dmitriymx.gradle.plugin; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import ru.dmitriymx.gradle.extention.LogicExtention; - -public class LogicPlugin implements Plugin { - - @Override - public void apply(Project project) { - project.getExtensions().create("logic", LogicExtention.class, project); - } -} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 442d913..f371643 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/libs.gradle b/libs.gradle new file mode 100644 index 0000000..691322b --- /dev/null +++ b/libs.gradle @@ -0,0 +1,47 @@ +//file:noinspection GroovyAssignabilityCheck +//file:noinspection GrUnresolvedAccess +//file:noinspection GroovyConstructorNamedArguments + +def slf4j_version = '1.7.30' +def logback_version = '1.2.3' +def dagger2_version = '2.33' +def junit_version = '5.5.2' +def netty_version = '4.1.22.Final' + +ext { + libs = [ + lombok : 'org.projectlombok:lombok:1.18.12', + annotations: 'com.google.code.findbugs:jsr305:3.0.2', + lang3 : 'org.apache.commons:commons-lang3:3.11', + netty : ["io.netty:netty-transport:${netty_version}", + "io.netty:netty-handler:${netty_version}"], + reactor : 'io.projectreactor:reactor-core:3.4.5', + yaml : 'org.yaml:snakeyaml:1.28', + json : 'com.eclipsesource.minimal-json:minimal-json:0.9.5', + ioutils : 'commons-io:commons-io:2.6', + jopt : 'net.sf.jopt-simple:jopt-simple:6.0-alpha-3' + ] + + libs.logger = [ + slf4j : ["org.slf4j:slf4j-api:${slf4j_version}", + "org.slf4j:jcl-over-slf4j:${slf4j_version}"], + logback: ["ch.qos.logback:logback-core:${logback_version}", + "ch.qos.logback:logback-classic:${logback_version}"] + ] + + libs.dagger2 = [ + implementation : "com.google.dagger:dagger:${dagger2_version}", + annotationProcessor: "com.google.dagger:dagger-compiler:${dagger2_version}" + ] + + libs.test = [ + logger: "org.slf4j:slf4j-simple:${slf4j_version}" + ] + + libs.test.junit5 = [ + api : "org.junit.jupiter:junit-jupiter-api:${junit_version}", + //runtime only + engine: "org.junit.jupiter:junit-jupiter-engine:${junit_version}", + params: "org.junit.jupiter:junit-jupiter-params:${junit_version}" + ] +} \ No newline at end of file diff --git a/logic.gradle b/logic.gradle new file mode 100644 index 0000000..68886aa --- /dev/null +++ b/logic.gradle @@ -0,0 +1,43 @@ +//file:noinspection GrUnresolvedAccess +apply plugin: 'java' +apply plugin: 'java-library' +apply from: rootDir.toPath().resolve('libs.gradle').toFile() + +String getProperty1(String propertyName1, String propertyName2) { + return (String) (project.hasProperty(propertyName1) ? project.property(propertyName1) : project.property(propertyName2)) +} + +project.group = getProperty1('module.group', 'project.group') +project.version = getProperty1('module.version', 'project.version') +jar.archiveBaseName.set(getProperty1('module.name', 'project.name')) + +compileJava { + sourceCompatibility = targetCompatibility = JavaVersion.VERSION_11 + options.encoding = 'UTF-8' +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + annotationProcessor libs.lombok + compileOnly libs.lombok + compileOnly libs.annotations + + implementation libs.logger.slf4j + + implementation libs.dagger2.implementation + annotationProcessor libs.dagger2.annotationProcessor + + testImplementation libs.test.junit5.api + testImplementation libs.test.junit5.params + testRuntimeOnly libs.test.junit5.engine + + testRuntimeOnly libs.test.logger +} + +test { + useJUnitPlatform() +} diff --git a/protocol/build.gradle b/protocol/build.gradle new file mode 100644 index 0000000..03ac861 --- /dev/null +++ b/protocol/build.gradle @@ -0,0 +1,9 @@ +apply from: rootDir.toPath().resolve('logic.gradle').toFile() + +dependencies { + api libs.netty + api libs.reactor + implementation libs.json + + testImplementation libs.lang3 +} diff --git a/protocol/gradle.properties b/protocol/gradle.properties new file mode 100644 index 0000000..572e805 --- /dev/null +++ b/protocol/gradle.properties @@ -0,0 +1,3 @@ +# suppress inspection "UnusedProperty" for whole file +module.name=protocol +module.version=1.0-SNAPSHOT \ No newline at end of file diff --git a/protocol/src/main/java/mc/protocol/ChannelContext.java b/protocol/src/main/java/mc/protocol/ChannelContext.java new file mode 100644 index 0000000..ba85b49 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/ChannelContext.java @@ -0,0 +1,20 @@ +package mc.protocol; + +import io.netty.channel.ChannelHandlerContext; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.protocol.packets.Packet; + +@RequiredArgsConstructor +public class ChannelContext

{ + + @Getter + private final ChannelHandlerContext ctx; + + @Getter + private final P packet; + + public void setState(State state) { + ctx.channel().attr(NetworkAttributes.STATE).set(state); + } +} diff --git a/protocol/src/main/java/mc/protocol/NettyServer.java b/protocol/src/main/java/mc/protocol/NettyServer.java new file mode 100644 index 0000000..b2f20fc --- /dev/null +++ b/protocol/src/main/java/mc/protocol/NettyServer.java @@ -0,0 +1,43 @@ +package mc.protocol; + +import io.netty.bootstrap.ServerBootstrap; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.protocol.di.DaggerProtocolComponent; +import mc.protocol.di.ProtocolComponent; +import mc.protocol.packets.Packet; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; + +import java.util.Map; + +@SuppressWarnings("rawtypes") +@Slf4j +@RequiredArgsConstructor +public class NettyServer { + + private final ServerBootstrap serverBootstrap; + private final Map, Sinks.Many> observedMap; + + public void bind(String host, int port) { + log.info("Network starting: {}:{}", host, port); + + try { + serverBootstrap.bind(host, port).sync().channel().closeFuture().sync(); + } catch (InterruptedException e) { + if (log.isTraceEnabled()) { + log.trace("{}: {}", e.getClass().getSimpleName(), e.getMessage(), e); + } + } + } + + @SuppressWarnings("unchecked") + public

Flux> packetFlux(Class

packetClass) { + return observedMap.get(packetClass).asFlux().map(ChannelContext.class::cast); + } + + public static NettyServer createServer() { + ProtocolComponent component = DaggerProtocolComponent.create(); + return component.getNettyServer(); + } +} diff --git a/protocol/src/main/java/mc/protocol/NetworkAttributes.java b/protocol/src/main/java/mc/protocol/NetworkAttributes.java new file mode 100644 index 0000000..0be033c --- /dev/null +++ b/protocol/src/main/java/mc/protocol/NetworkAttributes.java @@ -0,0 +1,10 @@ +package mc.protocol; + +import io.netty.util.AttributeKey; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class NetworkAttributes { + + public static final AttributeKey STATE = AttributeKey.newInstance("STATE"); +} diff --git a/protocol/src/main/java/mc/protocol/PacketInboundHandler.java b/protocol/src/main/java/mc/protocol/PacketInboundHandler.java new file mode 100644 index 0000000..451f3b7 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/PacketInboundHandler.java @@ -0,0 +1,21 @@ +package mc.protocol; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.RequiredArgsConstructor; +import mc.protocol.packets.Packet; +import reactor.core.publisher.Sinks; + +import java.util.Map; + +@SuppressWarnings("rawtypes") +@RequiredArgsConstructor +public class PacketInboundHandler extends SimpleChannelInboundHandler { + + private final Map, Sinks.Many> observedMap; + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Packet packet) { + observedMap.get(packet.getClass()).tryEmitNext(new ChannelContext<>(ctx, packet)); + } +} diff --git a/protocol/src/main/java/mc/protocol/ProtocolConstant.java b/protocol/src/main/java/mc/protocol/ProtocolConstant.java new file mode 100644 index 0000000..4fbbada --- /dev/null +++ b/protocol/src/main/java/mc/protocol/ProtocolConstant.java @@ -0,0 +1,10 @@ +package mc.protocol; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ProtocolConstant { + + public static final String PROTOCOL_NAME = "1.12.2"; + public static final int PROTOCOL_NUMBER = 340; +} diff --git a/protocol/src/main/java/mc/protocol/State.java b/protocol/src/main/java/mc/protocol/State.java new file mode 100644 index 0000000..b849adf --- /dev/null +++ b/protocol/src/main/java/mc/protocol/State.java @@ -0,0 +1,78 @@ +package mc.protocol; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.protocol.packets.ClientSidePacket; +import mc.protocol.packets.Packet; +import mc.protocol.packets.PingPacket; +import mc.protocol.packets.ServerSidePacket; +import mc.protocol.packets.client.HandshakePacket; +import mc.protocol.packets.client.LoginStartPacket; +import mc.protocol.packets.client.StatusServerRequestPacket; +import mc.protocol.packets.server.DisconnectPacket; +import mc.protocol.packets.server.StatusServerResponse; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; + +@RequiredArgsConstructor +public enum State { + + HANDSHAKING(-1, + // client side + Map.of(0x00, HandshakePacket.class) + ), + STATUS(1, + // client side + Map.of( + 0x00, StatusServerRequestPacket.class, + 0x01, PingPacket.class + ), + // server side + Map.of( + StatusServerResponse.class, 0x00, + PingPacket.class, 0x01 + ) + ), + LOGIN(2, + // server bound + Map.of(0x00, LoginStartPacket.class), + // client bound + Map.of(DisconnectPacket.class, 0x00) + ); + + @Nullable + public static State getById(int id) { + for (State state : State.values()) { + if (state.id == id) { + return state; + } + } + + return null; + } + + @Getter + private final int id; + + @Getter + private final Map> clientSidePackets; + private final Map, Integer> serverSidePackets; + + State(int id, Map> clientSidePackets) { + this.id = id; + this.clientSidePackets = clientSidePackets; + this.serverSidePackets = Collections.emptyMap(); + } + + @Nullable + public Class getClientSidePacketById(int id) { + return clientSidePackets == null ? null : clientSidePackets.get(id); + } + + @Nullable + public Integer getServerSidePacketId(Class clazz) { + return serverSidePackets == null ? null : serverSidePackets.get(clazz); + } +} diff --git a/protocol/src/main/java/mc/protocol/di/ProtocolComponent.java b/protocol/src/main/java/mc/protocol/di/ProtocolComponent.java new file mode 100644 index 0000000..a433211 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/di/ProtocolComponent.java @@ -0,0 +1,11 @@ +package mc.protocol.di; + +import dagger.Component; +import mc.protocol.NettyServer; + +@Component(modules = ProtocolModule.class) +@ServerScope +public interface ProtocolComponent { + + NettyServer getNettyServer(); +} diff --git a/protocol/src/main/java/mc/protocol/di/ProtocolModule.java b/protocol/src/main/java/mc/protocol/di/ProtocolModule.java new file mode 100644 index 0000000..167d27f --- /dev/null +++ b/protocol/src/main/java/mc/protocol/di/ProtocolModule.java @@ -0,0 +1,89 @@ +package mc.protocol.di; + +import dagger.Module; +import dagger.Provides; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import mc.protocol.ChannelContext; +import mc.protocol.NettyServer; +import mc.protocol.PacketInboundHandler; +import mc.protocol.State; +import mc.protocol.io.codec.ProtocolDecoder; +import mc.protocol.io.codec.ProtocolEncoder; +import mc.protocol.io.codec.ProtocolSplitter; +import mc.protocol.packets.Packet; +import reactor.core.publisher.Sinks; + +import javax.annotation.Nonnull; +import javax.inject.Provider; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Module +public class ProtocolModule { + + @SuppressWarnings("rawtypes") + @Provides + NettyServer provideServer(ServerBootstrap serverBootstrap, + Map, Sinks.Many> observedMap) { + return new NettyServer(serverBootstrap, observedMap); + } + + @Provides + ServerBootstrap provideServerBootstrap(ChannelInitializer channelChannelInitializer) { + ServerBootstrap bootstrap = new ServerBootstrap(); + + bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler(channelChannelInitializer); + + return bootstrap; + } + + @Provides + ChannelInitializer provideChannelChannelInitializer( + Provider> channelHandlerMapProvider) { + + return new ChannelInitializer<>() { + @Override + protected void initChannel(@Nonnull SocketChannel socketChannel) { + ChannelPipeline pipeline = socketChannel.pipeline(); + channelHandlerMapProvider.get().forEach(pipeline::addLast); + } + }; + } + + @SuppressWarnings("rawtypes") + @Provides + Map provideChannelHandlerMap( + Map, Sinks.Many> observedMap) { + + Map map = new LinkedHashMap<>(); + + map.put("packet_splitter", new ProtocolSplitter()); + map.put("logger", new LoggingHandler(LogLevel.DEBUG)); + map.put("packet_decoder", new ProtocolDecoder(true)); + map.put("packet_encoder", new ProtocolEncoder()); + map.put("packet_handler", new PacketInboundHandler(observedMap)); + + return map; + } + + @SuppressWarnings("rawtypes") + @Provides + @ServerScope + Map, Sinks.Many> provideObservedMap() { + return Stream.of(State.values()) + .flatMap(state -> state.getClientSidePackets().values().stream()) + .collect(Collectors.toMap(packetClass -> packetClass, v -> Sinks.many().multicast().directBestEffort())); + } +} diff --git a/protocol/src/main/java/mc/protocol/di/ServerScope.java b/protocol/src/main/java/mc/protocol/di/ServerScope.java new file mode 100644 index 0000000..b90e94e --- /dev/null +++ b/protocol/src/main/java/mc/protocol/di/ServerScope.java @@ -0,0 +1,10 @@ +package mc.protocol.di; + +import javax.inject.Scope; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Retention(RetentionPolicy.RUNTIME) +public @interface ServerScope { +} diff --git a/protocol/src/main/java/mc/protocol/io/DecoderException.java b/protocol/src/main/java/mc/protocol/io/DecoderException.java new file mode 100644 index 0000000..a30ee19 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/io/DecoderException.java @@ -0,0 +1,8 @@ +package mc.protocol.io; + +public class DecoderException extends RuntimeException { + + public DecoderException(String message) { + super(message); + } +} diff --git a/protocol/src/main/java/mc/protocol/io/NetByteBuf.java b/protocol/src/main/java/mc/protocol/io/NetByteBuf.java new file mode 100644 index 0000000..da59f07 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/io/NetByteBuf.java @@ -0,0 +1,180 @@ +package mc.protocol.io; + +import io.netty.buffer.ByteBuf; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import lombok.experimental.Delegate; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * Компонент чтения и записи данных протокола. + * + *

Data types

+ *
+ * | TYPE           | SIZE (bytes)          | ENCODING                                            | NOTES                                                                    |
+ * |----------------|-----------------------|-----------------------------------------------------|--------------------------------------------------------------------------|
+ * | Boolean        | 1                     | True или False                                      | True = 0x01; False = 0x00                                                |
+ * | Byte           | 1                     | Число от -128 до 127                                | 8-bit число со знаком                                                    |
+ * | Unsigned Byte  | 1                     | Число от 0 до 255                                   | 8-bit без знаковое число                                                 |
+ * | Short          | 2                     | Число от -32768 до 32767                            | 16-bit число со знаком                                                   |
+ * | Unsigned Short | 2                     | Число от -32768 до 32767                            | 16-bit без знаковое число                                                |
+ * | Int            | 4                     | Число от -2147483648 и 2147483647                   | 32-bit число со знаком                                                   |
+ * | Long           | 8                     | Число от -9223372036854775808 и 9223372036854775807 | 64-bit число со знаком                                                   |
+ * | Float          | 4                     | 32-bit число одинарной точности (IEEE 754-2008)     | [1]                                                                      |
+ * | Double         | 8                     | 64-bit число одинарной точности (IEEE 754-2008)     | [2]                                                                      |
+ * | String (n)     | >= 1 ; <= (n * 4) + 3 | Последовательность Unicode scalar values            | В начале пишется длина строки в VarInt, после чего записываются символы. |
+ * |                |                       |                                                     | Каждый символ может состоять максимум из 4 байт. [3]                     |
+ * |                |                       |                                                     | Максимальная длина строки - 32767 (3 - это как раз размер VarInt для     |
+ * |                |                       |                                                     | этого числа).                                                            |
+ * | VarInt         | >= 1 ; <= 5           | Число от -2147483648 и 2147483647                   | 32-bit число с плавающей размерностью от 1 до 5 байт                     |
+ * | VarLong        | >= 1 ; <= 10          | Число от -9223372036854775808 и 9223372036854775807 | 64-bit число с плавающей размерностью от 1 до 10 байт                    |
+ *
+ * [1] - Single-precision floating-point format
+ * [2] - Double-precision floating-point format
+ * [3] - Unicode Scalar Value
+ * 
+ * + * @see Data types + */ +@Slf4j +@RequiredArgsConstructor +@EqualsAndHashCode(callSuper = false) +@ToString +public class NetByteBuf extends ByteBuf { + + @Delegate + private final ByteBuf byteBuf; + + //region String + public String readString() { + return readString(Short.MAX_VALUE); + } + + @SuppressWarnings("java:S131") + public String readString(int maxLength) { + int length = readVarInt(); + + if (length == 0) { + return ""; + } else if (length > maxLength) { + throw new DecoderException("String length exceeds maximum length: " + length + " > " + maxLength); + } else if (length < 0) { + throw new DecoderException("String length less zero!"); + } + + byte[] bytes = new byte[length * 4]; + int readbleBytes = 0; + for (int i = 0; i < length && readableBytes() > 0; i++) { + byte b = readByte(); + bytes[readbleBytes++] = b; + + switch ((b & 0xFF) >> 4) { + case 0b1100: + case 0b1101: + bytes[readbleBytes++] = readByte(); + break; + case 0b1110: + bytes[readbleBytes++] = readByte(); + bytes[readbleBytes++] = readByte(); + break; + case 0b1111: + bytes[readbleBytes++] = readByte(); + bytes[readbleBytes++] = readByte(); + bytes[readbleBytes++] = readByte(); + break; + } + } + + return new String(bytes, 0, readbleBytes, StandardCharsets.UTF_8); + } + + public void writeString(String string) { + byte[] buf = string.getBytes(StandardCharsets.UTF_8); + + if (buf.length > Short.MAX_VALUE) { + log.warn("String is too long: {} > {}", buf.length, Short.MAX_VALUE); + writeVarInt(Short.MAX_VALUE); + writeBytes(buf, 0, Short.MAX_VALUE); + } else { + writeVarInt(buf.length); + writeBytes(buf); + } + } + //endregion + + //region VarInt + public int readVarInt() { + int numRead = 0; + int result = 0; + byte read; + do { + if ((numRead + 1) > 5) { + log.warn("VarInt is too big"); + break; + } + read = readByte(); + int value = (read & 0b01111111); + result |= (value << (7 * numRead)); + + numRead++; + } while ((read & 0b10000000) != 0); + + return result; + } + + public void writeVarInt(int value) { + while ((value & -128) != 0) { + writeByte(value & 127 | 128); + value >>>= 7; + } + + writeByte(value); + } + //endregion + + //region VarLong + public long readVarLong() { + int numRead = 0; + long result = 0L; + byte read; + do { + if (numRead > 10) { + log.warn("VarLong is too big"); + break; + } + + read = readByte(); + long value = (read & 0b01111111); + result |= (value << (7 * numRead)); + + numRead++; + } while ((read & 0b10000000) != 0); + + return result; + } + + public void writeVarLong(long value) { + while ((value & -128L) != 0L) { + writeByte((int) (value & 127L) | 128); + value >>>= 7; + } + + writeByte((int) value); + } + //endregion + + //region UUID + public UUID readUUID() { + return new UUID(readLong(), readLong()); + } + + public void writeUUID(UUID uuid) { + writeLong(uuid.getMostSignificantBits()); + writeLong(uuid.getLeastSignificantBits()); + } + //endregion +} diff --git a/protocol/src/main/java/mc/protocol/io/codec/ProtocolDecoder.java b/protocol/src/main/java/mc/protocol/io/codec/ProtocolDecoder.java new file mode 100644 index 0000000..58d22d7 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/io/codec/ProtocolDecoder.java @@ -0,0 +1,58 @@ +package mc.protocol.io.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.protocol.NetworkAttributes; +import mc.protocol.State; +import mc.protocol.io.NetByteBuf; +import mc.protocol.packets.ClientSidePacket; +import mc.protocol.packets.UnknownPacket; + +import java.util.List; +import java.util.Objects; + +@Slf4j +@RequiredArgsConstructor +public class ProtocolDecoder extends ByteToMessageDecoder { + + private final boolean readUnknownPackets; + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.channel().attr(NetworkAttributes.STATE).set(State.HANDSHAKING); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + ctx.channel().attr(NetworkAttributes.STATE).set(null); + super.channelInactive(ctx); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + State state = Objects.requireNonNull(ctx.channel().attr(NetworkAttributes.STATE).get()); + NetByteBuf netByteBuf = new NetByteBuf(in); + + int packetId = netByteBuf.readVarInt(); + Class packetClass = state.getClientSidePacketById(packetId); + if (packetClass == null) { + log.warn("Unkown packet: State {} ; Id {}", state, packetId); + + if (readUnknownPackets) { + UnknownPacket unknownPacket = new UnknownPacket(state, packetId, netByteBuf.readableBytes()); + unknownPacket.readSelf(netByteBuf); + out.add(unknownPacket); + } else { + netByteBuf.skipBytes(netByteBuf.readableBytes()); + } + } else { + ClientSidePacket packet = packetClass.getDeclaredConstructor().newInstance(); + packet.readSelf(netByteBuf); + out.add(packet); + } + } +} diff --git a/protocol/src/main/java/mc/protocol/io/codec/ProtocolEncoder.java b/protocol/src/main/java/mc/protocol/io/codec/ProtocolEncoder.java new file mode 100644 index 0000000..418d3ff --- /dev/null +++ b/protocol/src/main/java/mc/protocol/io/codec/ProtocolEncoder.java @@ -0,0 +1,29 @@ +package mc.protocol.io.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import mc.protocol.NetworkAttributes; +import mc.protocol.State; +import mc.protocol.io.NetByteBuf; +import mc.protocol.packets.ServerSidePacket; + +import java.util.Objects; + +public class ProtocolEncoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, ServerSidePacket packet, ByteBuf out) { + State state = ctx.channel().attr(NetworkAttributes.STATE).get(); + int packetId = Objects.requireNonNull(state.getServerSidePacketId(packet.getClass())); + + NetByteBuf buffer = new NetByteBuf(Unpooled.buffer()); + buffer.writeVarInt(packetId); + packet.writeSelf(buffer); + + NetByteBuf netByteBuf = new NetByteBuf(out); + netByteBuf.writeVarInt(buffer.readableBytes()); + netByteBuf.writeBytes(buffer); + } +} diff --git a/protocol/src/main/java/mc/protocol/io/codec/ProtocolSplitter.java b/protocol/src/main/java/mc/protocol/io/codec/ProtocolSplitter.java new file mode 100644 index 0000000..452bb06 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/io/codec/ProtocolSplitter.java @@ -0,0 +1,40 @@ +package mc.protocol.io.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import mc.protocol.io.NetByteBuf; + +import java.util.List; + +public class ProtocolSplitter extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + NetByteBuf netByteBuf = new NetByteBuf(in); + netByteBuf.markReaderIndex(); + + do { + byte[] sizePacketRaw = new byte[3]; + for (int i = 0; i < 3; ++i) { + sizePacketRaw[i] = netByteBuf.readByte(); + + if (sizePacketRaw[i] >= 0) { + break; + } + } + + int sizePacket = new NetByteBuf(Unpooled.wrappedBuffer(sizePacketRaw)).readVarInt(); + + if (netByteBuf.readableBytes() >= sizePacket) { + byte[] bytes = new byte[sizePacket]; + netByteBuf.readBytes(bytes); + out.add(Unpooled.wrappedBuffer(bytes)); + } else { + netByteBuf.resetReaderIndex(); + break; + } + } while (netByteBuf.readableBytes() > 0); + } +} diff --git a/protocol/src/main/java/mc/protocol/model/ServerInfo.java b/protocol/src/main/java/mc/protocol/model/ServerInfo.java new file mode 100644 index 0000000..e5a7af7 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/model/ServerInfo.java @@ -0,0 +1,50 @@ +package mc.protocol.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.RequiredArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import mc.protocol.model.text.Text; + +import java.util.List; + +@Accessors(fluent = true) +@Getter +@Setter +@ToString +public class ServerInfo { + + private final Version version = new Version(); + private final Players players = new Players(); + + private Text description; + private String favicon; + + @Getter + @Setter + @ToString + public static class Version { + private String name; + private int protocol; + } + + @Getter + @Setter + @ToString + public static class Players { + private int max; + private int online; + private List sample; + } + + @RequiredArgsConstructor + @Getter + @EqualsAndHashCode + @ToString + public static class SamplePlayer { + private final String id; + private final String name; + } +} diff --git a/protocol/src/main/java/mc/protocol/model/text/Text.java b/protocol/src/main/java/mc/protocol/model/text/Text.java new file mode 100644 index 0000000..d7527e2 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/model/text/Text.java @@ -0,0 +1,151 @@ +package mc.protocol.model.text; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.Getter; +import lombok.experimental.Accessors; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +@Accessors(fluent = true) +@AllArgsConstructor +@Data +public class Text { + + private TextColor color; + private TextStyle style; + private String content; + private List children; + + public static Text of(String string) { + return new Text(null, null, string, null); + } + + public static Text of(TextColor color, String string) { + return new Text(color, null, string, null); + } + + public static Text of(TextStyle style, String string) { + return new Text(null, style, string, null); + } + + public static Text of(TextColor color, TextStyle style, String string) { + return new Text(color, style, string, null); + } + + public static Builder builder() { + return new Builder(); + } + + @NoArgsConstructor + @ToString + public static class Builder { + + @Getter(onMethod = @__(@Nullable)) + private StringBuilder contentBuilder; + private TextStyle.Builder styleBuilder; + private TextColor color; + private List children; + + public Builder append(char content) { + if (this.contentBuilder == null) { + this.contentBuilder = new StringBuilder(); + } + + this.contentBuilder.append(content); + return this; + } + + public Builder append(String content) { + if (this.contentBuilder == null) { + this.contentBuilder = new StringBuilder(content); + } else { + this.contentBuilder.append(content); + } + return this; + } + + public Builder append(Text text) { + if (children == null) { + children = new ArrayList<>(); + } + + children.add(text); + return this; + } + + public Builder style(TextStyle style) { + //@formatter:off + if (style.bold() != null) bold(style.bold()); + if (style.italic() != null) italic(style.italic()); + if (style.underline() != null) underline(style.underline()); + if (style.strikethrough() != null) strikethrough(style.strikethrough()); + if (style.obfuscated() != null) obfuscated(style.obfuscated()); + //@formatter:on + + return this; + } + + public Builder color(TextColor color) { + this.color = color; + return this; + } + + public Builder bold(Boolean bold) { + if (this.styleBuilder == null) { + this.styleBuilder = TextStyle.builder(); + } + + this.styleBuilder.bold(bold); + return this; + } + + public Builder italic(Boolean italic) { + if (this.styleBuilder == null) { + this.styleBuilder = TextStyle.builder(); + } + + this.styleBuilder.italic(italic); + return this; + } + + public Builder underline(Boolean underline) { + if (this.styleBuilder == null) { + this.styleBuilder = TextStyle.builder(); + } + + this.styleBuilder.underline(underline); + return this; + } + + public Builder strikethrough(Boolean strikethrough) { + if (this.styleBuilder == null) { + this.styleBuilder = TextStyle.builder(); + } + + this.styleBuilder.strikethrough(strikethrough); + return this; + } + + public Builder obfuscated(Boolean obfuscated) { + if (this.styleBuilder == null) { + this.styleBuilder = TextStyle.builder(); + } + + this.styleBuilder.obfuscated(obfuscated); + return this; + } + + public Text build() { + return new Text( + color, + styleBuilder == null ? null : styleBuilder.build(), + contentBuilder == null ? null : contentBuilder.toString(), + children); + } + } +} diff --git a/protocol/src/main/java/mc/protocol/model/text/TextColor.java b/protocol/src/main/java/mc/protocol/model/text/TextColor.java new file mode 100644 index 0000000..8e914cc --- /dev/null +++ b/protocol/src/main/java/mc/protocol/model/text/TextColor.java @@ -0,0 +1,30 @@ +package mc.protocol.model.text; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum TextColor { + //@formatter:off + 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'), + PURPLE ("light_purple",'d'), + YELLOW ("yellow", 'e'), + WHITE ("white", 'f'); + //@formatter:on + + private final String name; + private final char code; +} diff --git a/protocol/src/main/java/mc/protocol/model/text/TextStyle.java b/protocol/src/main/java/mc/protocol/model/text/TextStyle.java new file mode 100644 index 0000000..06c6040 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/model/text/TextStyle.java @@ -0,0 +1,82 @@ +package mc.protocol.model.text; + +import lombok.*; +import lombok.experimental.Accessors; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Accessors(fluent = true) +@Data +@SuppressWarnings("java:S1845") +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); + public static final TextStyle NONE = new TextStyle(null, null, null, null, null); + + private Boolean bold; + private Boolean italic; + private Boolean underline; + private Boolean strikethrough; + private Boolean obfuscated; + + public static Builder builder() { + return new TextStyle.Builder(); + } + + @NoArgsConstructor + @ToString + public static class Builder { + + private Boolean bold; + private Boolean italic; + private Boolean underline; + private Boolean strikethrough; + private Boolean obfuscated; + + public Builder bold(Boolean bold) { + this.bold = bold; + return this; + } + + public Builder italic(Boolean italic) { + this.italic = italic; + return this; + } + + public Builder underline(Boolean underline) { + this.underline = underline; + return this; + } + + public Builder strikethrough(Boolean strikethrough) { + this.strikethrough = strikethrough; + return this; + } + + public Builder obfuscated(Boolean obfuscated) { + this.obfuscated = obfuscated; + return this; + } + + public Builder merge(TextStyle style) { + //@formatter:off + if (style.bold != null) this.bold = style.bold; + if (style.italic != null) this.italic = style.italic; + if (style.underline != null) this.underline = style.underline; + if (style.strikethrough != null) this.strikethrough = style.strikethrough; + if (style.obfuscated != null) this.obfuscated = style.obfuscated; + //@formatter:on + + return this; + } + + public TextStyle build() { + return new TextStyle(bold, italic, underline, strikethrough, obfuscated); + } + } +} diff --git a/protocol/src/main/java/mc/protocol/packets/ClientSidePacket.java b/protocol/src/main/java/mc/protocol/packets/ClientSidePacket.java new file mode 100644 index 0000000..6629750 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/ClientSidePacket.java @@ -0,0 +1,11 @@ +package mc.protocol.packets; + +import mc.protocol.io.NetByteBuf; + +/** + * Пакеты отправляемые клиентом. + */ +public interface ClientSidePacket extends Packet { + + void readSelf(NetByteBuf netByteBuf); +} diff --git a/protocol/src/main/java/mc/protocol/packets/EmptyPacket.java b/protocol/src/main/java/mc/protocol/packets/EmptyPacket.java new file mode 100644 index 0000000..60c800f --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/EmptyPacket.java @@ -0,0 +1,16 @@ +package mc.protocol.packets; + +import mc.protocol.io.NetByteBuf; + +public abstract class EmptyPacket implements ClientSidePacket, ServerSidePacket { + + @Override + public void readSelf(NetByteBuf netByteBuf) { + // empty + } + + @Override + public void writeSelf(NetByteBuf netByteBuf) { + // empty + } +} diff --git a/protocol/src/main/java/mc/protocol/packets/Packet.java b/protocol/src/main/java/mc/protocol/packets/Packet.java new file mode 100644 index 0000000..02ce207 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/Packet.java @@ -0,0 +1,19 @@ +package mc.protocol.packets; + +/** + * Пакет. + * + *

Структура пакета

+ *
+ * | FIELD       | TYPE   | NOTES                                     |
+ * |-------------|--------|-------------------------------------------|
+ * | SIZE        | VarInt | = sizeOf(PACKET ID) + sizeOf(PACKET DATA) |
+ * | PACKET ID   | VarInt |                                           |
+ * | PACKET DATA | bytes  |                                           |
+ * 
+ * + * @see Packet without compression + */ +public interface Packet { + +} diff --git a/protocol/src/main/java/mc/protocol/packets/PingPacket.java b/protocol/src/main/java/mc/protocol/packets/PingPacket.java new file mode 100644 index 0000000..f40b121 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/PingPacket.java @@ -0,0 +1,41 @@ +package mc.protocol.packets; + +import lombok.Data; +import mc.protocol.io.NetByteBuf; + +/** + * Пинг-пакет. + * + *

Эхо-пакет, которым проверяется качество соединения между Клиентом и Сервером.

+ * + *

По спецификации:

+ * + *
  • если Сервер не ответил Клиенту в течении 20 секунд, Клиент отключается + * и выдаёт ошибку "Timed out".
  • + *
  • если Клиент не отвечает Серверу в течении 30 секунд, Сервер отключает Клиента.
  • + *
    + * + *

    Структура пакета

    + *
    + * | FIELD   | TYPE | NOTES                  |
    + * |---------|------|------------------------|
    + * | Payload | Long | Любое уникальное число |
    + * 
    + * + * @see Keep Alive + */ +@Data +public class PingPacket implements ClientSidePacket, ServerSidePacket { + + private Long payload; + + @Override + public void readSelf(NetByteBuf netByteBuf) { + payload = netByteBuf.readLong(); + } + + @Override + public void writeSelf(NetByteBuf netByteBuf) { + netByteBuf.writeLong(payload); + } +} diff --git a/protocol/src/main/java/mc/protocol/packets/ServerSidePacket.java b/protocol/src/main/java/mc/protocol/packets/ServerSidePacket.java new file mode 100644 index 0000000..086deb6 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/ServerSidePacket.java @@ -0,0 +1,11 @@ +package mc.protocol.packets; + +import mc.protocol.io.NetByteBuf; + +/** + * Пакеты отправляемые сервером. + */ +public interface ServerSidePacket extends Packet { + + void writeSelf(NetByteBuf netByteBuf); +} diff --git a/protocol/src/main/java/mc/protocol/packets/UnknownPacket.java b/protocol/src/main/java/mc/protocol/packets/UnknownPacket.java new file mode 100644 index 0000000..aef7b74 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/UnknownPacket.java @@ -0,0 +1,22 @@ +package mc.protocol.packets; + +import lombok.Data; +import lombok.ToString; +import mc.protocol.State; +import mc.protocol.io.NetByteBuf; + +@Data +@ToString(exclude = "rawData") +public class UnknownPacket implements ClientSidePacket { + + private final State state; + private final int id; + private final int dataSize; + private byte[] rawData; + + @Override + public void readSelf(NetByteBuf netByteBuf) { + rawData = new byte[dataSize]; + netByteBuf.readBytes(rawData); + } +} diff --git a/protocol/src/main/java/mc/protocol/packets/client/HandshakePacket.java b/protocol/src/main/java/mc/protocol/packets/client/HandshakePacket.java new file mode 100644 index 0000000..ccb86b5 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/client/HandshakePacket.java @@ -0,0 +1,50 @@ +package mc.protocol.packets.client; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import mc.protocol.State; +import mc.protocol.io.NetByteBuf; +import mc.protocol.packets.ClientSidePacket; + +/** + * Handshake packet. + * + *

    Данный пакет заставляет сервер переключить текущий {@link State}

    + * + *

    Структура пакета

    + *
    + * | FIELD            | TYPE           | NOTES                                        |
    + * |------------------|----------------|----------------------------------------------|
    + * | Protocol version | VarInt         | Версия протокола [1]                         |
    + * | Server address   | Stirng         | Hostname или IP                              |
    + * | Server port      | Unsigned Short | Порт сервера                                 |
    + * | Next stage       | VarInt         | ID State на который необходимо переключиться |
    + *
    + * [1] - Protocol version numbers
    + * 
    + * + * @see Handshake + * @see State + */ +@NoArgsConstructor +@Getter +@EqualsAndHashCode +@ToString +public class HandshakePacket implements ClientSidePacket { + + private int protocolVersion; + private String host; + private int port; + private State nextState; + + @Override + public void readSelf(NetByteBuf netByteBuf) { + protocolVersion = netByteBuf.readVarInt(); + host = netByteBuf.readString(255); + port = netByteBuf.readUnsignedShort(); + nextState = State.getById(netByteBuf.readVarInt()); + } + +} diff --git a/protocol/src/main/java/mc/protocol/packets/client/LoginStartPacket.java b/protocol/src/main/java/mc/protocol/packets/client/LoginStartPacket.java new file mode 100644 index 0000000..77775b6 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/client/LoginStartPacket.java @@ -0,0 +1,37 @@ +package mc.protocol.packets.client; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import mc.protocol.io.NetByteBuf; +import mc.protocol.packets.ClientSidePacket; + +/** + * Login start packet. + * + *

    Начало авторизации.

    + * + *

    Структура пакета

    + *
    + * | FIELD | TYPE   | NOTES            |
    + * |-------|--------|------------------|
    + * | Name  | String | Имя/Логин игрока |
    + * 
    + * + * @see Login start + */ +@NoArgsConstructor +@Getter +@EqualsAndHashCode +@ToString +public class LoginStartPacket implements ClientSidePacket { + + private String name; + + @Override + public void readSelf(NetByteBuf netByteBuf) { + this.name = netByteBuf.readString(); + } + +} diff --git a/protocol/src/main/java/mc/protocol/packets/client/StatusServerRequestPacket.java b/protocol/src/main/java/mc/protocol/packets/client/StatusServerRequestPacket.java new file mode 100644 index 0000000..5f4e84c --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/client/StatusServerRequestPacket.java @@ -0,0 +1,18 @@ +package mc.protocol.packets.client; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import mc.protocol.packets.EmptyPacket; + +/** + * Status server packet, request. + * + *

    Клиент запрашивает получение информации о сервере

    + */ +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@ToString +public class StatusServerRequestPacket extends EmptyPacket { + +} diff --git a/protocol/src/main/java/mc/protocol/packets/server/DisconnectPacket.java b/protocol/src/main/java/mc/protocol/packets/server/DisconnectPacket.java new file mode 100644 index 0000000..7eae3aa --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/server/DisconnectPacket.java @@ -0,0 +1,44 @@ +package mc.protocol.packets.server; + +import lombok.Data; +import mc.protocol.State; +import mc.protocol.io.NetByteBuf; +import mc.protocol.model.text.Text; +import mc.protocol.packets.ServerSidePacket; +import mc.protocol.serializer.TextSerializer; + +/** + * Diconnect packet. + * + *

    Отключение клиента сервером с указанием причины.

    + * + *

    Структура пакета

    + *
    + * | FIELD       | TYPE   | NOTES                            |
    + * |-------------|--------|----------------------------------|
    + * | JSON Reason | String | Причина отключения. Опционально. |
    + * 
    + * + *

    Пример JSON Reason

    + *
    + * {
    + *     "text": "foo"
    + * }
    + * 
    + * + * @see Disconnect + * @see State + */ +@Data +public class DisconnectPacket implements ServerSidePacket { + + /** + * Причина отключения. + */ + private Text reason; + + @Override + public void writeSelf(NetByteBuf netByteBuf) { + netByteBuf.writeString(TextSerializer.toJsonObject(reason).toString()); + } +} diff --git a/protocol/src/main/java/mc/protocol/packets/server/StatusServerResponse.java b/protocol/src/main/java/mc/protocol/packets/server/StatusServerResponse.java new file mode 100644 index 0000000..35973c1 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/packets/server/StatusServerResponse.java @@ -0,0 +1,61 @@ +package mc.protocol.packets.server; + +import lombok.Data; +import mc.protocol.io.NetByteBuf; +import mc.protocol.model.ServerInfo; +import mc.protocol.packets.ServerSidePacket; +import mc.protocol.serializer.ServerInfoSerializer; + +/** + * Status server packet, response. + * + *

    Информация о сервере

    + * + *

    Структура пакета + *

    + * | FIELD         | TYPE   | NOTES                                   |
    + * |---------------|--------|-----------------------------------------|
    + * | JSON Response | String | Информация о сервере в JSON формате [1] |
    + *
    + * [1] - Server List Ping: Response
    + * 
    + * + *

    Пример JSON Response

    + *
    + * {
    + *     "version": {
    + *         "name": "1.8.7",
    + *         "protocol": 47
    + *     },
    + *     "players": {
    + *         "max": 100,
    + *         "online": 5,
    + *         "sample": [
    + *             {
    + *                 "name": "thinkofdeath",
    + *                 "id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20"
    + *             }
    + *         ]
    + *     },
    + *     "description": {
    + *         "text": "Hello world"
    + *     },
    + *     "favicon": "data:image/png;base64,<data>"
    + * }
    + * 
    + * + *

    `$.favicon` должен быть формата PNG и размеры 64x64 px

    + */ +@Data +public class StatusServerResponse implements ServerSidePacket { + + /** + * Информация о серере. + */ + private ServerInfo info; + + @Override + public void writeSelf(NetByteBuf netByteBuf) { + netByteBuf.writeString(ServerInfoSerializer.toJsonObject(info).toString()); + } +} diff --git a/protocol/src/main/java/mc/protocol/serializer/ServerInfoSerializer.java b/protocol/src/main/java/mc/protocol/serializer/ServerInfoSerializer.java new file mode 100644 index 0000000..f215b55 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/serializer/ServerInfoSerializer.java @@ -0,0 +1,55 @@ +package mc.protocol.serializer; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; +import lombok.experimental.UtilityClass; +import mc.protocol.model.ServerInfo; + +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collector; +import java.util.stream.StreamSupport; + +@UtilityClass +public class ServerInfoSerializer { + + public JsonObject toJsonObject(ServerInfo info) { + JsonObject jsonObject = Json.object() + .add("version", createVersionObj(info)) + .add("players", createPlayersObj(info)) + .add("description", TextSerializer.toJsonObject(info.description())); + + if (info.favicon() != null && !info.favicon().isEmpty()) { + jsonObject.add("favicon", info.favicon()); + } + + return jsonObject; + } + + private JsonObject createVersionObj(ServerInfo info) { + return Json.object() + .add("name", info.version().name()) + .add("protocol", info.version().protocol()); + } + + private JsonObject createPlayersObj(ServerInfo info) { + JsonArray sampleArr = info.players().sample().stream() + .map(samplePlayer -> Json.object() + .add("name", samplePlayer.name()) + .add("id", samplePlayer.id())) + .collect(Collector.of(Json::array, JsonArray::add, ServerInfoSerializer::jsonArrayAddAll)); + + return Json.object() + .add("max", info.players().max()) + .add("online", info.players().online()) + .add("sample", sampleArr); + } + + private static JsonArray jsonArrayAddAll(JsonArray jsonArrayTo, JsonArray jsonArrayFrom) { + StreamSupport.stream( + Spliterators.spliteratorUnknownSize(jsonArrayFrom.iterator(), Spliterator.ORDERED), false) + .forEach(jsonArrayTo::add); + return jsonArrayTo; + } +} diff --git a/protocol/src/main/java/mc/protocol/serializer/TextSerializer.java b/protocol/src/main/java/mc/protocol/serializer/TextSerializer.java new file mode 100644 index 0000000..8633d02 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/serializer/TextSerializer.java @@ -0,0 +1,129 @@ +package mc.protocol.serializer; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; +import lombok.experimental.UtilityClass; +import mc.protocol.model.text.Text; +import mc.protocol.model.text.TextColor; +import mc.protocol.model.text.TextStyle; + +import java.util.Map; + +@UtilityClass +public class TextSerializer { + + private static final Map legacyStyleCodes; + private static final Map legacyColorCodes; + + public JsonObject toJsonObject(Text text) { + JsonObject jsonObject = Json.object(); + + if (text.content() != null) { + jsonObject.add("text", text.content()); + } + + if (text.color() != null) { + jsonObject.add("color", text.color().getName()); + } + + if (text.style() != null) { + //@formatter:off + if (text.style().bold() != null) jsonObject.add("bold", text.style().bold()); + if (text.style().italic() != null) jsonObject.add("italic", text.style().italic()); + if (text.style().underline() != null) jsonObject.add("underline", text.style().underline()); + if (text.style().strikethrough() != null) jsonObject.add("strikethrough", text.style().strikethrough()); + if (text.style().obfuscated() != null) jsonObject.add("obfuscated", text.style().obfuscated()); + //@formatter:on + } + + if (text.children() != null && !text.children().isEmpty()) { + JsonArray extra = Json.array(); + text.children().forEach(child -> extra.add(toJsonObject(child))); + jsonObject.add("extra", extra); + } + + return jsonObject; + } + + /** + * Преобразование строки вида "&4красный" в {@link Text}. + * + * @param string тест + * @return Text + */ + @SuppressWarnings({"java:S3776", "java:S2583", "java:S135"}) + public Text fromPlain(String string) { + boolean flagSys = false; + Text.Builder rootTextBuilder = Text.builder(); + Text.Builder textBuilder = rootTextBuilder; + + for (char ch : string.toCharArray()) { + if (!flagSys) { + if ('&' == ch) { + flagSys = true; + } else { + textBuilder.append(ch); + } + continue; + } + + if (!legacyStyleCodes.containsKey(ch) && !legacyColorCodes.containsKey(ch) && '&' == ch) { + textBuilder.append('&'); + flagSys = false; + continue; + } + + //noinspection ConstantConditions + if (textBuilder.contentBuilder() != null && textBuilder.contentBuilder().length() > 0) { + if (textBuilder != rootTextBuilder) { + rootTextBuilder.append(textBuilder.build()); + } + textBuilder = Text.builder(); + } + + if (legacyStyleCodes.containsKey(ch)) { + textBuilder.style(legacyStyleCodes.get(ch)); + } else { + textBuilder.color(legacyColorCodes.get(ch)); + } + + flagSys = false; + } + + if (textBuilder != rootTextBuilder) { + rootTextBuilder.append(textBuilder.build()); + } + + return rootTextBuilder.build(); + } + + static { + legacyColorCodes = Map.ofEntries( + Map.entry('0', TextColor.BLACK), + Map.entry('1', TextColor.DARK_BLUE), + Map.entry('2', TextColor.DARK_GREEN), + Map.entry('3', TextColor.DARK_AQUA), + Map.entry('4', TextColor.DARK_RED), + Map.entry('5', TextColor.DARK_PUEPLE), + Map.entry('6', TextColor.GOLD), + Map.entry('7', TextColor.GRAY), + Map.entry('8', TextColor.DARK_GRAY), + Map.entry('9', TextColor.BLUE), + Map.entry('a', TextColor.GREEN), + Map.entry('b', TextColor.AQUA), + Map.entry('c', TextColor.RED), + Map.entry('d', TextColor.PURPLE), + Map.entry('e', TextColor.YELLOW), + Map.entry('f', TextColor.WHITE) + ); + + legacyStyleCodes = Map.of( + 'k', TextStyle.OBFUSCATED, + 'l', TextStyle.BOLD, + 'm', TextStyle.STRIKETHOUGH, + 'n', TextStyle.UNDERLINE, + 'o', TextStyle.ITALIC + ); + } +} diff --git a/protocol/src/test/java/mc/protocol/io/NetByteBufReadTest.java b/protocol/src/test/java/mc/protocol/io/NetByteBufReadTest.java new file mode 100644 index 0000000..6337ec5 --- /dev/null +++ b/protocol/src/test/java/mc/protocol/io/NetByteBufReadTest.java @@ -0,0 +1,306 @@ +package mc.protocol.io; + +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class NetByteBufReadTest { + + private static Random random; + private ByteArrayOutputStream baos; + + @BeforeEach + void setUp() { + random = new Random(System.currentTimeMillis()); + baos = new ByteArrayOutputStream(); + } + + @ParameterizedTest + @MethodSource("paramsReadBoolean") + void readBoolean(byte sourceByte, boolean expectedValue) { + baos.write(sourceByte); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(expectedValue, netByteBuf.readBoolean()); + } + + @Test + void readByte() { + byte[] bytes = new byte[1]; + random.nextBytes(bytes); + baos.write(bytes[0]); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(bytes[0], netByteBuf.readByte()); + } + + @ParameterizedTest + @MethodSource("paramsReadUnsignedByte") + void readUnsignedByte(byte sourceByte, int expectedValue) { + baos.write(sourceByte); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(expectedValue, netByteBuf.readUnsignedByte()); + } + + @Test + void readShort() throws IOException { + int value = Integer.valueOf(random.nextInt()).shortValue(); + new DataOutputStream(baos).writeShort(value); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(value, netByteBuf.readShort()); + } + + @Test + void readUnsignedShort() throws IOException { + int value = 32768; + new DataOutputStream(baos).writeShort(value); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(value, netByteBuf.readUnsignedShort()); + } + + @ParameterizedTest + @MethodSource("paramsReadString") + void readString(String string) throws IOException { + final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8); + final byte[] bytes = new byte[strBytes.length + 1]; + bytes[0] = (byte) string.codePoints().count(); // допустим, что размер поместился в один байт + System.arraycopy(strBytes, 0, bytes, 1, strBytes.length); + + baos.write(bytes); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(string, netByteBuf.readString()); + } + + @Test + void readString_overSize() throws IOException { + String string = "123"; + final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8); + final byte[] bytes = new byte[strBytes.length + 1]; + final int length = string.length(); + bytes[0] = (byte) (length + 1); // допустим, что размер поместился в один байт + System.arraycopy(strBytes, 0, bytes, 1, strBytes.length); + + baos.write(bytes); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertThrows(DecoderException.class, () -> netByteBuf.readString(length)); + } + + @Test + void readString_lessZero() throws IOException { + String string = "123"; + final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8); + final byte[] bytes = new byte[strBytes.length + 5]; + bytes[0] = (byte) 0xFF; + bytes[1] = (byte) 0xFF; + bytes[2] = (byte) 0xFF; + bytes[3] = (byte) 0xFF; + bytes[4] = (byte) 0x0F; + System.arraycopy(strBytes, 0, bytes, 5, strBytes.length); + + baos.write(bytes); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertThrows(DecoderException.class, () -> netByteBuf.readString(-1)); + } + + @ParameterizedTest + @MethodSource("paramsReadVarInt") + void readVarInt(byte[] sourceBytes, int expectedValue) throws IOException { + baos.write(sourceBytes); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(expectedValue, netByteBuf.readVarInt()); + } + + @Test + void readVarInt_tooBig() throws IOException { + baos.write(new byte[]{ (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0F }); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(-1, netByteBuf.readVarInt()); + } + + @ParameterizedTest + @MethodSource({"paramsReadVarInt", "paramsReadVarLong"}) + void readVarLong(byte[] sourceBytes, long expectedValue) throws IOException { + baos.write(sourceBytes); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(expectedValue, netByteBuf.readVarLong()); + } + + @Test + void readVarLong_tooBig() throws IOException { + baos.write(new byte[]{ (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0x0F }); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(-1, netByteBuf.readVarLong()); + } + + @Test + void readUUID() throws IOException { + final UUID uuid = UUID.randomUUID(); + final long mostSignificantBits = uuid.getMostSignificantBits(); + final long leastSignificantBits = uuid.getLeastSignificantBits(); + + baos.write(new byte[]{ + (byte) ((mostSignificantBits >>> 56) & 0xFF), + (byte) ((mostSignificantBits >>> 48) & 0xFF), + (byte) ((mostSignificantBits >>> 40) & 0xFF), + (byte) ((mostSignificantBits >>> 32) & 0xFF), + (byte) ((mostSignificantBits >>> 24) & 0xFF), + (byte) ((mostSignificantBits >>> 16) & 0xFF), + (byte) ((mostSignificantBits >>> 8) & 0xFF), + (byte) (mostSignificantBits & 0xFF) + }); + baos.write(new byte[]{ + (byte) ((leastSignificantBits >>> 56) & 0xFF), + (byte) ((leastSignificantBits >>> 48) & 0xFF), + (byte) ((leastSignificantBits >>> 40) & 0xFF), + (byte) ((leastSignificantBits >>> 32) & 0xFF), + (byte) ((leastSignificantBits >>> 24) & 0xFF), + (byte) ((leastSignificantBits >>> 16) & 0xFF), + (byte) ((leastSignificantBits >>> 8) & 0xFF), + (byte) (leastSignificantBits & 0xFF) + }); + + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(uuid, netByteBuf.readUUID()); + } + + @Test + void readBytes() throws IOException { + byte[] bytes = new byte[128]; + random.nextBytes(bytes); + baos.write(bytes); + + byte[] actualBytes = new byte[128]; + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + + assertEquals(bytes.length, netByteBuf.readableBytes()); + + netByteBuf.readBytes(actualBytes); + + assertArrayEquals(bytes, actualBytes); + assertEquals(0, netByteBuf.readableBytes()); + } + + @Test + void read_offset() throws IOException { + byte[] bytes = new byte[128]; + random.nextBytes(bytes); + baos.write(bytes); + + byte[] buff = new byte[128]; + NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray())); + netByteBuf.readBytes(buff, 3, 11); + + byte[] expectedBytes = new byte[11]; + System.arraycopy(bytes, 0, expectedBytes, 0, 11); + byte[] actualBytes = new byte[11]; + System.arraycopy(buff, 3, actualBytes, 0, 11); + + assertArrayEquals(expectedBytes, actualBytes); + } + + @SuppressWarnings("unused") + private static Stream paramsReadBoolean() { + return Stream.of( + Arguments.of((byte) 0x00, false), + Arguments.of((byte) 0x01, true) + ); + } + + @SuppressWarnings("unused") + private static Stream paramsReadUnsignedByte() { + return Stream.of( + Arguments.of((byte) 30, 30), + Arguments.of((byte) (0xFF & 130), 130) + ); + } + + @SuppressWarnings("unused") + private static Stream paramsReadString() { + return Stream.of( + Arguments.of(""), + Arguments.of("Latin"), + Arguments.of("Кириллица"), + Arguments.of("العربية"), + Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления + Arguments.of("\uD800\uDD07") // Эгейские цифры, [один] + ); + } + + @SuppressWarnings("unused") + private static Stream paramsReadVarInt() { + return Stream.of( + Arguments.of(new byte[]{ 0x78 }, 120), + Arguments.of(new byte[]{ (byte) 0xE0, 0x5D }, 12000), + Arguments.of(new byte[]{ (byte) 0xC0, (byte) 0xA9, 0x07 }, 120000), + Arguments.of(new byte[]{ (byte) 0x80, (byte) 0x9C, (byte) 0x9C, (byte) 0x39 }, 120_000_000), + Arguments.of(new byte[]{ (byte) 0x80, (byte) 0x98, (byte) 0x9A, (byte) 0xBC, 0x04 }, 1_200_000_000) + ); + } + + @SuppressWarnings("unused") + private static Stream paramsReadVarLong() { + return Stream.of( + Arguments.of( + new byte[]{ (byte) 0x80, (byte) 0xF0, (byte) 0x85, (byte) 0xDA, 0x2C }, + 12_000_000_000L), + Arguments.of( + new byte[]{ (byte) 0x80, (byte) 0xE0, (byte) 0xBA, (byte) 0x84, (byte) 0xBF, 0x03 }, + 120_000_000_000L), + Arguments.of( + new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xF3, (byte) 0xBD, (byte) 0x9F, (byte) 0xDD, + 0x02 }, + 12_000_000_000_000L), + Arguments.of( + new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xEC, (byte) 0xAD, (byte) 0xCC, (byte) 0xEC, + (byte) 0x90, 0x02}, + 1_200_000_000_000_000L), + Arguments.of( + new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xB0, (byte) 0xE8, (byte) 0xD3, (byte) 0xEB, + (byte) 0x94, (byte) 0xD5, 0x01 }, + 120_000_000_000_000_000L), + Arguments.of( + new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01 }, + Long.MIN_VALUE) + ); + } +} \ No newline at end of file diff --git a/protocol/src/test/java/mc/protocol/io/NetByteBufWriteTest.java b/protocol/src/test/java/mc/protocol/io/NetByteBufWriteTest.java new file mode 100644 index 0000000..3386041 --- /dev/null +++ b/protocol/src/test/java/mc/protocol/io/NetByteBufWriteTest.java @@ -0,0 +1,250 @@ +package mc.protocol.io; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NetByteBufWriteTest { + + private static Random random; + + @BeforeAll + static void setUp() { + random = new Random(System.currentTimeMillis()); + } + + @ParameterizedTest + @MethodSource("paramsWriteBoolean") + void writeBoolean(boolean sourceValue, byte expectedByte) { + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeBoolean(sourceValue); + + assertEquals(expectedByte, byteBuf.array()[0]); + } + + @ParameterizedTest + @MethodSource("paramsWriteByte") + void writeByte(byte sourceValue, byte expectedByte) { + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeByte(sourceValue); + + assertEquals(expectedByte, byteBuf.array()[0]); + } + + @ParameterizedTest + @MethodSource("paramsWriteString") + void writeString(String string, int exceptedLength) { + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeString(string); + + byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array(); + int actualLength = actualArray[0]; // допустим, что размер поместился в один байт + assertEquals(exceptedLength, actualLength); + + byte[] dataBytes = new byte[actualArray.length - 1]; + System.arraycopy(actualArray, 1, dataBytes, 0, dataBytes.length); + assertEquals(string, new String(dataBytes, StandardCharsets.UTF_8)); + } + + //возможно этот тест нужно перенести в NetByteBufReadTest + @Test + void writeString_overSize() { + String overSizeString = RandomStringUtils.randomAscii(Short.MAX_VALUE + Short.MAX_VALUE); + + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeString(overSizeString); + + NetByteBuf netByteBuf2 = new NetByteBuf(byteBuf.copy()); + String actualString = netByteBuf2.readString(); + + String expectedString = overSizeString.substring(0, Short.MAX_VALUE); + + assertEquals(expectedString, actualString); + } + + @ParameterizedTest + @MethodSource("paramsWriteVarInt") + void writeVarInt(int sourceValue, byte[] expectedBytes) { + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeVarInt(sourceValue); + byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array(); + + assertArrayEquals(expectedBytes, actualArray); + } + + @ParameterizedTest + @MethodSource({ "paramsWriteVarInt", "paramsWriteVarLong" }) + void writeVarLong(long sourceValue, byte[] expectedBytes) { + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeVarLong(sourceValue); + byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array(); + + assertArrayEquals(expectedBytes, actualArray); + } + + @Test + void writeUUID() { + final UUID uuid = UUID.randomUUID(); + + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeUUID(uuid); + + final long mostSignificantBits = uuid.getMostSignificantBits(); + final long leastSignificantBits = uuid.getLeastSignificantBits(); + + byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array(); + + assertArrayEquals(new byte[]{ + (byte) ((mostSignificantBits >>> 56) & 0xFF), + (byte) ((mostSignificantBits >>> 48) & 0xFF), + (byte) ((mostSignificantBits >>> 40) & 0xFF), + (byte) ((mostSignificantBits >>> 32) & 0xFF), + (byte) ((mostSignificantBits >>> 24) & 0xFF), + (byte) ((mostSignificantBits >>> 16) & 0xFF), + (byte) ((mostSignificantBits >>> 8) & 0xFF), + (byte) (mostSignificantBits & 0xFF), + + (byte) ((leastSignificantBits >>> 56) & 0xFF), + (byte) ((leastSignificantBits >>> 48) & 0xFF), + (byte) ((leastSignificantBits >>> 40) & 0xFF), + (byte) ((leastSignificantBits >>> 32) & 0xFF), + (byte) ((leastSignificantBits >>> 24) & 0xFF), + (byte) ((leastSignificantBits >>> 16) & 0xFF), + (byte) ((leastSignificantBits >>> 8) & 0xFF), + (byte) (leastSignificantBits & 0xFF) }, + actualArray); + } + + @Test + void writeBytes() { + byte[] bytes = new byte[128]; + random.nextBytes(bytes); + + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeBytes(bytes); + byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array(); + + assertArrayEquals(bytes, actualArray); + } + + @Test + void write_offset() { + byte[] bytes = new byte[128]; + random.nextBytes(bytes); + + ByteBuf byteBuf = Unpooled.buffer(); + NetByteBuf netByteBuf = new NetByteBuf(byteBuf); + + netByteBuf.writeBytes(bytes, 3, 11); + + byte[] actualBytes = new byte[11]; + System.arraycopy(byteBuf.array(), 0, actualBytes, 0, 11); + + byte[] expectedBytes = new byte[11]; + System.arraycopy(bytes, 3, expectedBytes, 0, 11); + + assertArrayEquals(expectedBytes, actualBytes); + } + + @SuppressWarnings("unused") + private static Stream paramsWriteBoolean() { + return Stream.of( + Arguments.of(false, (byte) 0x00), + Arguments.of(true, (byte) 0x01) + ); + } + + @SuppressWarnings("unused") + private static Stream paramsWriteByte() { + byte b = Integer.valueOf(random.nextInt()).byteValue(); + + return Stream.of( + Arguments.of(b, b), + Arguments.of((byte) 128, (byte) -128) + ); + } + + @SuppressWarnings("unused") + private static Stream paramsWriteString() { + return Stream.of( + Arguments.of("", 0), + Arguments.of("Latin", 5), + Arguments.of("Кириллица", 37), + // (9) -> "Кириллица"(18) => 18*2=36 (37?) + Arguments.of("العربية", 30), + // (7) -> "Ш§Щ„Ш№Ш±ШЁЩЉШ©"(14) => 14*2=28 (30?) + Arguments.of("ﬦﬣﬡ", 18), // Алфавитные формы представления + // (3) -> "ﬦﬣﬡ"(9) => 9*2=18 + Arguments.of("\uD800\uDD07", 4) // Эгейские цифры, [один] + // (1) -> "𐄇" => ...4! + ); + } + + @SuppressWarnings("unused") + private static Stream paramsWriteVarInt() { + return Stream.of( + Arguments.of(120, new byte[]{ 0x78 }), + Arguments.of(12000, new byte[]{ (byte) 0xE0, 0x5D }), + Arguments.of(120000, new byte[]{ (byte) 0xC0, (byte) 0xA9, 0x07 }), + Arguments.of(120000000, new byte[]{ (byte) 0x80, (byte) 0x9C, (byte) 0x9C, (byte) 0x39 }), + Arguments.of(1200000000, new byte[]{ (byte) 0x80, (byte) 0x98, (byte) 0x9A, (byte) 0xBC, 0x04 }) + ); + } + + @SuppressWarnings("unused") + private static Stream paramsWriteVarLong() { + return Stream.of( + Arguments.of( + 12_000_000_000L, + new byte[]{ (byte) 0x80, (byte) 0xF0, (byte) 0x85, (byte) 0xDA, 0x2C }), + Arguments.of( + 120_000_000_000L, + new byte[]{ (byte) 0x80, (byte) 0xE0, (byte) 0xBA, (byte) 0x84, (byte) 0xBF, 0x03 }), + Arguments.of( + 12_000_000_000_000L, + new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xF3, (byte) 0xBD, (byte) 0x9F, (byte) 0xDD, + 0x02 }), + Arguments.of( + 1_200_000_000_000_000L, + new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xEC, (byte) 0xAD, (byte) 0xCC, (byte) 0xEC, + (byte) 0x90, 0x02 }), + Arguments.of( + 120_000_000_000_000_000L, + new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xB0, (byte) 0xE8, (byte) 0xD3, (byte) 0xEB, + (byte) 0x94, (byte) 0xD5, 0x01 }), + Arguments.of( + Long.MIN_VALUE, + new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01 }) + ); + } +} diff --git a/protocol/src/test/java/mc/protocol/model/text/TextTest.java b/protocol/src/test/java/mc/protocol/model/text/TextTest.java new file mode 100644 index 0000000..601576c --- /dev/null +++ b/protocol/src/test/java/mc/protocol/model/text/TextTest.java @@ -0,0 +1,24 @@ +package mc.protocol.model.text; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TextTest { + + @Test + void contentTest() { + Text actual; + Text expected; + + actual = Text.builder().append("123").build(); + expected = new Text(null, null, "123", null); + assertEquals(expected, actual); + + actual = Text.builder().append("123").append(Text.of("456")).build(); + expected = new Text(null, null, "123", List.of(Text.of("456"))); + assertEquals(expected, actual); + } +} \ No newline at end of file diff --git a/protocol/src/test/java/mc/protocol/serializer/TextSerializerTest.java b/protocol/src/test/java/mc/protocol/serializer/TextSerializerTest.java new file mode 100644 index 0000000..61bd4fa --- /dev/null +++ b/protocol/src/test/java/mc/protocol/serializer/TextSerializerTest.java @@ -0,0 +1,40 @@ +package mc.protocol.serializer; + +import mc.protocol.model.text.Text; +import mc.protocol.model.text.TextColor; +import mc.protocol.model.text.TextStyle; +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.assertEquals; + +class TextSerializerTest { + + @ParameterizedTest + @MethodSource("paramsPlain") + void fromPlain(String sample, Text expected) { + Text actual = TextSerializer.fromPlain(sample); + assertEquals(expected, actual); + } + + @SuppressWarnings("unused") + static Stream paramsPlain() { + return Stream.of( + Arguments.of("text", Text.of("text")), + Arguments.of("&&text", Text.of("&text")), + Arguments.of("&ztext", Text.of("text")), + Arguments.of("&4red_text", Text.of(TextColor.DARK_RED, "red_text")), + Arguments.of("&l&4red_text", Text.of(TextColor.DARK_RED, TextStyle.BOLD, "red_text")), + Arguments.of("&4&lred_text", Text.of(TextColor.DARK_RED, TextStyle.BOLD, "red_text")), + + Arguments.of("&4red_text &eyellow_text", Text.builder() + .color(TextColor.DARK_RED) + .append("red_text ") + .append(Text.of(TextColor.YELLOW, "yellow_text")) + .build()) + ); + } +} \ No newline at end of file diff --git a/server/build.gradle b/server/build.gradle new file mode 100644 index 0000000..7b27ae3 --- /dev/null +++ b/server/build.gradle @@ -0,0 +1,39 @@ +/* +Запуск + gradle :server:run --args="--config=config.yml --logconfig==logback.xml" + +Сборка + gradle :server:shadowJar + */ + +//file:noinspection GrUnresolvedAccess +plugins { + id 'com.github.johnrengelman.shadow' version '7.0.0' +} + +apply from: rootDir.toPath().resolve('logic.gradle').toFile() +apply plugin: 'application' + +application { + mainClassName = 'mc.server.Main' + + if (project.hasProperty('jvmArgs')) { + applicationDefaultJvmArgs = List.of((project.jvmArgs as String).split('\\s+')) + } +} + +dependencies { + implementation project(':protocol') + + implementation libs.logger.logback + + implementation libs.yaml + implementation libs.ioutils + implementation libs.jopt +} + +shadowJar { + archiveBaseName.set(jar.archiveBaseName.get()) + archiveVersion.set(project.version as String) + archiveClassifier.set('') +} \ No newline at end of file diff --git a/server/gradle.properties b/server/gradle.properties new file mode 100644 index 0000000..e2373fb --- /dev/null +++ b/server/gradle.properties @@ -0,0 +1,3 @@ +# suppress inspection "UnusedProperty" for whole file +module.name=server +module.version=1.0-SNAPSHOT \ No newline at end of file diff --git a/server/src/main/java/mc/server/Main.java b/server/src/main/java/mc/server/Main.java new file mode 100644 index 0000000..c5f506e --- /dev/null +++ b/server/src/main/java/mc/server/Main.java @@ -0,0 +1,200 @@ +package mc.server; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.joran.spi.JoranException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.util.PathConverter; +import lombok.extern.slf4j.Slf4j; +import mc.protocol.NettyServer; +import mc.protocol.ProtocolConstant; +import mc.protocol.model.ServerInfo; +import mc.protocol.packets.PingPacket; +import mc.protocol.packets.client.HandshakePacket; +import mc.protocol.packets.client.LoginStartPacket; +import mc.protocol.packets.client.StatusServerRequestPacket; +import mc.protocol.packets.server.DisconnectPacket; +import mc.protocol.packets.server.StatusServerResponse; +import mc.protocol.serializer.TextSerializer; +import mc.server.config.Config; +import mc.server.di.ConfigModule; +import mc.server.di.DaggerServerComponent; +import mc.server.di.ServerComponent; +import org.apache.commons.io.IOUtils; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@Slf4j +@SuppressWarnings("java:S106") +public class Main { + private static final String CLI_CONFIG = "config"; + private static final String CLI_LOGCONFIG = "logconfig"; + + private void run(OptionSet optionSet) { + log.info("mc-project launch"); + + ConfigModule configModule = new ConfigModule((Path) optionSet.valueOf(CLI_CONFIG)); + + ServerComponent serverComponent = DaggerServerComponent.builder() + .configModule(configModule) + .build(); + + Config config = serverComponent.getConfig(); + + NettyServer server = NettyServer.createServer(); + + server.packetFlux(HandshakePacket.class) + .doOnNext(channel -> log.info("{}", channel.getPacket())) + .subscribe(channel -> channel.setState(channel.getPacket().getNextState())); + + server.packetFlux(PingPacket.class) + .doOnNext(channel -> log.info("{}", channel.getPacket())) + .subscribe(channel -> channel.getCtx().writeAndFlush(channel.getPacket()).channel().disconnect()); + + server.packetFlux(StatusServerRequestPacket.class) + .doOnNext(channel -> log.info("{}", channel.getPacket())) + .subscribe(channel -> { + ServerInfo serverInfo = new ServerInfo(); + serverInfo.version().name(ProtocolConstant.PROTOCOL_NAME); + serverInfo.version().protocol(ProtocolConstant.PROTOCOL_NUMBER); + serverInfo.players().max(config.players().maxOnlile()); + serverInfo.players().online(config.players().onlile()); + serverInfo.players().sample(Collections.emptyList()); + serverInfo.description(TextSerializer.fromPlain(config.motd())); + + if (config.iconPath() != null) { + serverInfo.favicon(faviconToBase64(config.iconPath())); + } + + StatusServerResponse response = new StatusServerResponse(); + response.setInfo(serverInfo); + + channel.getCtx().writeAndFlush(response); + }); + + server.packetFlux(LoginStartPacket.class) + .doOnNext(channel -> log.info("{}", channel.getPacket())) + .subscribe(channel -> { + DisconnectPacket disconnectPacket = new DisconnectPacket(); + disconnectPacket.setReason(TextSerializer.fromPlain(config.disconnectReason())); + + channel.getCtx().writeAndFlush(disconnectPacket).channel().disconnect(); + }); + + server.bind(config.server().host(), config.server().port()); + } + + public static void main(String[] args) throws IOException { + OptionParser optionParser = createOptionParser(); + OptionSet optionSet = optionParser.parse(args); + + if (optionSet.has("help")) { + try { + optionParser.printHelpOn(System.out); + } catch (IOException e) { + System.err.printf("Can't print help page: %s%n", e.getMessage()); + e.printStackTrace(System.err); + } + return; + } else if (optionSet.has("init")) { + Path configPath = (Path) optionSet.valueOf(CLI_CONFIG); + Path logbackPath = (Path) optionSet.valueOf(CLI_LOGCONFIG); + + if (!initializeCheckFiles(configPath, logbackPath)) { + return; + } + + InputStream configResource = Objects.requireNonNull(Main.class.getResourceAsStream("/config-sample.yml")); + InputStream logbackResource = Objects.requireNonNull(Main.class.getResourceAsStream("/logback-sample.xml")); + + try(OutputStream configOut = Files.newOutputStream(configPath); + OutputStream logbackOut = Files.newOutputStream(logbackPath)) { + IOUtils.copy(configResource, configOut); + IOUtils.copy(logbackResource, logbackOut); + } + + System.out.println("Initialization environment done."); + return; + } + + reconfigureLogback(optionSet); + + if (log.isDebugEnabled()) { + optionSet.asMap().forEach((optionSpec, objects) -> { + if (optionSpec.isForHelp()) return; + log.debug("OptionSet | {} = {}", optionSpec.options(), objects); + }); + } + + new Main().run(optionSet); + } + + private static OptionParser createOptionParser() { + OptionParser optionParser = new OptionParser(); + + optionParser.acceptsAll(List.of("h", "help"), "Help page").forHelp(); + optionParser.accepts("init", "Initialize environment"); + + optionParser.accepts(CLI_CONFIG, "Path to configuration file") + .withRequiredArg() + .withValuesConvertedBy(new PathConverter()) + .defaultsTo(Paths.get("config.yml")); + + optionParser.accepts(CLI_LOGCONFIG, "Path to logger configuratuin file") + .withRequiredArg() + .withValuesConvertedBy(new PathConverter()) + .defaultsTo(Paths.get("logback.xml")); + + return optionParser; + } + + private static String faviconToBase64(Path iconPath) { + try { + return "data:image/png;base64," + + Base64.getEncoder().encodeToString( + IOUtils.toByteArray(Files.newInputStream(iconPath))); + } catch (IOException e) { + log.error("Can't read icon '{}'", iconPath.toAbsolutePath(), e); + return ""; + } + } + + private static boolean initializeCheckFiles(Path... paths) { + for (Path path : paths) { + if (Files.exists(path)) { + System.err.printf("File '%s' already exist. Initialization environment canceled.%n", + path.toAbsolutePath()); + return false; + } + } + + return true; + } + + private static void reconfigureLogback(OptionSet optionSet) throws IOException { + LoggerContext logbackContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + logbackContext.reset(); + JoranConfigurator configurator = new JoranConfigurator(); + + Path logbackPath = (Path) optionSet.valueOf(CLI_LOGCONFIG); + try(InputStream in = Objects.requireNonNull( + Files.newInputStream(logbackPath), "File not found: " + logbackPath.toAbsolutePath())) { + + configurator.setContext(logbackContext); + configurator.doConfigure(in); + } catch (JoranException e) { + throw new IOException(e); + } + } +} diff --git a/server/src/main/java/mc/server/config/Config.java b/server/src/main/java/mc/server/config/Config.java new file mode 100644 index 0000000..07340fc --- /dev/null +++ b/server/src/main/java/mc/server/config/Config.java @@ -0,0 +1,38 @@ +package mc.server.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.nio.file.Path; + +@Accessors(fluent = true) +@Getter +@Setter +@ToString +public class Config { + + private final Server server = new Server(); + private final Players players = new Players(); + + private String motd; + private String disconnectReason; + private Path iconPath; + + @Getter + @Setter + @ToString + public static class Server { + private String host; + private int port; + } + + @Getter + @Setter + @ToString + public static class Players { + private int maxOnlile; + private int onlile; + } +} diff --git a/server/src/main/java/mc/server/di/ConfigModule.java b/server/src/main/java/mc/server/di/ConfigModule.java new file mode 100644 index 0000000..c4bd5b0 --- /dev/null +++ b/server/src/main/java/mc/server/di/ConfigModule.java @@ -0,0 +1,67 @@ +package mc.server.di; + +import dagger.Module; +import dagger.Provides; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.server.config.Config; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +@Slf4j +@Module +@RequiredArgsConstructor +public class ConfigModule { + + private final Path configPath; + + @Provides + Config provideConfig() { + Config config = new Config(); + Map map = new Yaml().load(readConfigAsString()); + + config.server().host(fromYamlPath("server/host", map, "127.0.0.1")); + config.server().port(fromYamlPath("server/port", map, 25565)); + config.motd(fromYamlPath("motd", map, "")); + config.disconnectReason(fromYamlPath("disconnect-reason", map, "")); + config.players().maxOnlile(fromYamlPath("players/max-online", map, 0)); + config.players().onlile(fromYamlPath("players/online", map, 0)); + + if (Boolean.TRUE.equals(fromYamlPath("icon/enable", map, false))) { + config.iconPath(Paths.get(fromYamlPath("icon/path", map, "favicon.png"))); + } + + map.clear(); + return config; + } + + private String readConfigAsString() { + try { + return Files.readString(configPath); + } catch (IOException e) { + log.error("Can't load config from '{}'", configPath.toAbsolutePath(), e); + return ""; + } + } + + @SuppressWarnings("unchecked") + private static T fromYamlPath(String mapPath, Map map, T defaultValue) { + String[] keys = mapPath.split("/", 2); + + if (map.containsKey(keys[0])) { + Object object = map.get(keys[0]); + if (keys.length > 1) { + return fromYamlPath(keys[1], (Map) object, defaultValue); + } else { + return (T) object; + } + } else { + return defaultValue; + } + } +} diff --git a/server/src/main/java/mc/server/di/ServerComponent.java b/server/src/main/java/mc/server/di/ServerComponent.java new file mode 100644 index 0000000..4147205 --- /dev/null +++ b/server/src/main/java/mc/server/di/ServerComponent.java @@ -0,0 +1,10 @@ +package mc.server.di; + +import dagger.Component; +import mc.server.config.Config; + +@Component(modules = ConfigModule.class) +public interface ServerComponent { + + Config getConfig(); +} diff --git a/server/src/main/resources/config-sample.yml b/server/src/main/resources/config-sample.yml new file mode 100644 index 0000000..c26428c --- /dev/null +++ b/server/src/main/resources/config-sample.yml @@ -0,0 +1,18 @@ +server: + host: 127.0.0.1 + port: 25565 + +motd: | + &bmc-project &8:: &4ZERO + &8develop by &7DmitriyMX + +disconnect-reason: '&4Server is not available.' + +players: + max-online: 0 + online: 0 + +# Размер значка: 64x64 px +icon: + enable: false + path: favicon.png \ No newline at end of file diff --git a/src/main/resources/logback.xml b/server/src/main/resources/logback-sample.xml similarity index 51% rename from src/main/resources/logback.xml rename to server/src/main/resources/logback-sample.xml index 9f1a1a5..f72ea29 100644 --- a/src/main/resources/logback.xml +++ b/server/src/main/resources/logback-sample.xml @@ -4,11 +4,17 @@ - %d{HH:mm:ss.SSS} %-5level [%t] [%logger{36}] -- %msg%n + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%35.35logger{34}] -- %msg%n + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 925cad0..03dd498 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,4 +10,5 @@ rootProject.projectDir.toPath().resolve('gradle.properties').readLines().forEach rootProject.name = map.get('project.name') +include('protocol') include('server') \ No newline at end of file diff --git a/src/main/java/mc/server/Main.java b/src/main/java/mc/server/Main.java deleted file mode 100644 index bde95b3..0000000 --- a/src/main/java/mc/server/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package mc.server; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class Main { - - public static void main(String[] args) { - log.info("hello"); - } -}