Merge branch 'develop'
This commit is contained in:
98
README.MD
98
README.MD
@@ -4,6 +4,104 @@
|
||||

|
||||

|
||||
|
||||
Написанный с нуля сервер **Minecraft 1.12.2**.
|
||||
|
||||
На данный момент может только показывать информацию о себе. Подключение к серверу не возможно.
|
||||
|
||||
---
|
||||
|
||||
## Требования
|
||||
|
||||
* 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` | <span style="color:#000">Black</span> | `&l` | <span style="font-weight:bold">Bold</span> |
|
||||
| `&1` | <span style="color:#00A">Dark Blue</span> | `&o` | <span style="font-style:italic">Italic</span> |
|
||||
| `&2` | <span style="color:#0A0">Dark Green</span> | `&n` | <span style="text-decoration:underline">Underline</span> |
|
||||
| `&3` | <span style="color:#0AA">Dark Aqua</span> | `&m` | <span style="text-decoration:line-through">Strikethrough</span> |
|
||||
| `&4` | <span style="color:#A00">Dark Red</span> | `&k` | Obfuscated |
|
||||
| `&5` | <span style="color:#A0A">Dark Purple</span> |
|
||||
| `&6` | <span style="color:#FA0">Gold</span> |
|
||||
| `&7` | <span style="color:#AAA">Gray</span> |
|
||||
| `&8` | <span style="color:#555">Dark Gray</span> |
|
||||
| `&9` | <span style="color:#55F">Blue</span> |
|
||||
| `&a` | <span style="color:#5F5">Green</span> |
|
||||
| `&b` | <span style="color:#5FF">Aqua</span> |
|
||||
| `&c` | <span style="color:#F55">Red</span> |
|
||||
| `&d` | <span style="color:#F5F">Purple</span> |
|
||||
| `&e` | <span style="color:#FF5">Yellow</span> |
|
||||
| `&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**. Другие форматы или размеры _не поддерживаются_.
|
||||
43
build.gradle
43
build.gradle
@@ -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'
|
||||
@@ -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<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getExtensions().create("libs", LibsExtention.class);
|
||||
}
|
||||
}
|
||||
@@ -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<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getExtensions().create("logic", LogicExtention.class, project);
|
||||
}
|
||||
}
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
47
libs.gradle
Normal file
47
libs.gradle
Normal file
@@ -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}"
|
||||
]
|
||||
}
|
||||
43
logic.gradle
Normal file
43
logic.gradle
Normal file
@@ -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()
|
||||
}
|
||||
9
protocol/build.gradle
Normal file
9
protocol/build.gradle
Normal file
@@ -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
|
||||
}
|
||||
3
protocol/gradle.properties
Normal file
3
protocol/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
# suppress inspection "UnusedProperty" for whole file
|
||||
module.name=protocol
|
||||
module.version=1.0-SNAPSHOT
|
||||
20
protocol/src/main/java/mc/protocol/ChannelContext.java
Normal file
20
protocol/src/main/java/mc/protocol/ChannelContext.java
Normal file
@@ -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<P extends Packet> {
|
||||
|
||||
@Getter
|
||||
private final ChannelHandlerContext ctx;
|
||||
|
||||
@Getter
|
||||
private final P packet;
|
||||
|
||||
public void setState(State state) {
|
||||
ctx.channel().attr(NetworkAttributes.STATE).set(state);
|
||||
}
|
||||
}
|
||||
43
protocol/src/main/java/mc/protocol/NettyServer.java
Normal file
43
protocol/src/main/java/mc/protocol/NettyServer.java
Normal file
@@ -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<Class<? extends Packet>, Sinks.Many<ChannelContext>> 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 <P extends Packet> Flux<ChannelContext<P>> packetFlux(Class<P> packetClass) {
|
||||
return observedMap.get(packetClass).asFlux().map(ChannelContext.class::cast);
|
||||
}
|
||||
|
||||
public static NettyServer createServer() {
|
||||
ProtocolComponent component = DaggerProtocolComponent.create();
|
||||
return component.getNettyServer();
|
||||
}
|
||||
}
|
||||
10
protocol/src/main/java/mc/protocol/NetworkAttributes.java
Normal file
10
protocol/src/main/java/mc/protocol/NetworkAttributes.java
Normal file
@@ -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> STATE = AttributeKey.newInstance("STATE");
|
||||
}
|
||||
21
protocol/src/main/java/mc/protocol/PacketInboundHandler.java
Normal file
21
protocol/src/main/java/mc/protocol/PacketInboundHandler.java
Normal file
@@ -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<Packet> {
|
||||
|
||||
private final Map<Class<? extends Packet>, Sinks.Many<ChannelContext>> observedMap;
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, Packet packet) {
|
||||
observedMap.get(packet.getClass()).tryEmitNext(new ChannelContext<>(ctx, packet));
|
||||
}
|
||||
}
|
||||
10
protocol/src/main/java/mc/protocol/ProtocolConstant.java
Normal file
10
protocol/src/main/java/mc/protocol/ProtocolConstant.java
Normal file
@@ -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;
|
||||
}
|
||||
78
protocol/src/main/java/mc/protocol/State.java
Normal file
78
protocol/src/main/java/mc/protocol/State.java
Normal file
@@ -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<Integer, Class<? extends ClientSidePacket>> clientSidePackets;
|
||||
private final Map<Class<? extends ServerSidePacket>, Integer> serverSidePackets;
|
||||
|
||||
State(int id, Map<Integer, Class<? extends ClientSidePacket>> clientSidePackets) {
|
||||
this.id = id;
|
||||
this.clientSidePackets = clientSidePackets;
|
||||
this.serverSidePackets = Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Class<? extends ClientSidePacket> getClientSidePacketById(int id) {
|
||||
return clientSidePackets == null ? null : clientSidePackets.get(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getServerSidePacketId(Class<? extends Packet> clazz) {
|
||||
return serverSidePackets == null ? null : serverSidePackets.get(clazz);
|
||||
}
|
||||
}
|
||||
11
protocol/src/main/java/mc/protocol/di/ProtocolComponent.java
Normal file
11
protocol/src/main/java/mc/protocol/di/ProtocolComponent.java
Normal file
@@ -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();
|
||||
}
|
||||
89
protocol/src/main/java/mc/protocol/di/ProtocolModule.java
Normal file
89
protocol/src/main/java/mc/protocol/di/ProtocolModule.java
Normal file
@@ -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<Class<? extends Packet>, Sinks.Many<ChannelContext>> observedMap) {
|
||||
return new NettyServer(serverBootstrap, observedMap);
|
||||
}
|
||||
|
||||
@Provides
|
||||
ServerBootstrap provideServerBootstrap(ChannelInitializer<SocketChannel> channelChannelInitializer) {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
|
||||
bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.childHandler(channelChannelInitializer);
|
||||
|
||||
return bootstrap;
|
||||
}
|
||||
|
||||
@Provides
|
||||
ChannelInitializer<SocketChannel> provideChannelChannelInitializer(
|
||||
Provider<Map<String, ChannelHandler>> channelHandlerMapProvider) {
|
||||
|
||||
return new ChannelInitializer<>() {
|
||||
@Override
|
||||
protected void initChannel(@Nonnull SocketChannel socketChannel) {
|
||||
ChannelPipeline pipeline = socketChannel.pipeline();
|
||||
channelHandlerMapProvider.get().forEach(pipeline::addLast);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Provides
|
||||
Map<String, ChannelHandler> provideChannelHandlerMap(
|
||||
Map<Class<? extends Packet>, Sinks.Many<ChannelContext>> observedMap) {
|
||||
|
||||
Map<String, ChannelHandler> 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<Class<? extends Packet>, Sinks.Many<ChannelContext>> provideObservedMap() {
|
||||
return Stream.of(State.values())
|
||||
.flatMap(state -> state.getClientSidePackets().values().stream())
|
||||
.collect(Collectors.toMap(packetClass -> packetClass, v -> Sinks.many().multicast().directBestEffort()));
|
||||
}
|
||||
}
|
||||
10
protocol/src/main/java/mc/protocol/di/ServerScope.java
Normal file
10
protocol/src/main/java/mc/protocol/di/ServerScope.java
Normal file
@@ -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 {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package mc.protocol.io;
|
||||
|
||||
public class DecoderException extends RuntimeException {
|
||||
|
||||
public DecoderException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
180
protocol/src/main/java/mc/protocol/io/NetByteBuf.java
Normal file
180
protocol/src/main/java/mc/protocol/io/NetByteBuf.java
Normal file
@@ -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;
|
||||
|
||||
/**
|
||||
* Компонент чтения и записи данных протокола.
|
||||
*
|
||||
* <p>Data types</p>
|
||||
* <pre>
|
||||
* | 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] - <a href="https://en.wikipedia.org/wiki/Single-precision_floating-point_format">Single-precision floating-point format</a>
|
||||
* [2] - <a href="https://en.wikipedia.org/wiki/Double-precision_floating-point_format">Double-precision floating-point format</a>
|
||||
* [3] - <a href="http://unicode.org/glossary/#unicode_scalar_value">Unicode Scalar Value</a>
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Data_types">Data types</a>
|
||||
*/
|
||||
@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
|
||||
}
|
||||
@@ -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<Object> out) throws Exception {
|
||||
State state = Objects.requireNonNull(ctx.channel().attr(NetworkAttributes.STATE).get());
|
||||
NetByteBuf netByteBuf = new NetByteBuf(in);
|
||||
|
||||
int packetId = netByteBuf.readVarInt();
|
||||
Class<? extends ClientSidePacket> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ServerSidePacket> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<Object> 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);
|
||||
}
|
||||
}
|
||||
50
protocol/src/main/java/mc/protocol/model/ServerInfo.java
Normal file
50
protocol/src/main/java/mc/protocol/model/ServerInfo.java
Normal file
@@ -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<SamplePlayer> sample;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
public static class SamplePlayer {
|
||||
private final String id;
|
||||
private final String name;
|
||||
}
|
||||
}
|
||||
151
protocol/src/main/java/mc/protocol/model/text/Text.java
Normal file
151
protocol/src/main/java/mc/protocol/model/text/Text.java
Normal file
@@ -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<Text> 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<Text> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
protocol/src/main/java/mc/protocol/model/text/TextColor.java
Normal file
30
protocol/src/main/java/mc/protocol/model/text/TextColor.java
Normal file
@@ -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;
|
||||
}
|
||||
82
protocol/src/main/java/mc/protocol/model/text/TextStyle.java
Normal file
82
protocol/src/main/java/mc/protocol/model/text/TextStyle.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package mc.protocol.packets;
|
||||
|
||||
import mc.protocol.io.NetByteBuf;
|
||||
|
||||
/**
|
||||
* Пакеты отправляемые клиентом.
|
||||
*/
|
||||
public interface ClientSidePacket extends Packet {
|
||||
|
||||
void readSelf(NetByteBuf netByteBuf);
|
||||
}
|
||||
16
protocol/src/main/java/mc/protocol/packets/EmptyPacket.java
Normal file
16
protocol/src/main/java/mc/protocol/packets/EmptyPacket.java
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
19
protocol/src/main/java/mc/protocol/packets/Packet.java
Normal file
19
protocol/src/main/java/mc/protocol/packets/Packet.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package mc.protocol.packets;
|
||||
|
||||
/**
|
||||
* Пакет.
|
||||
*
|
||||
* <p>Структура пакета</p>
|
||||
* <pre>
|
||||
* | FIELD | TYPE | NOTES |
|
||||
* |-------------|--------|-------------------------------------------|
|
||||
* | SIZE | VarInt | = sizeOf(PACKET ID) + sizeOf(PACKET DATA) |
|
||||
* | PACKET ID | VarInt | |
|
||||
* | PACKET DATA | bytes | |
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Without_compression">Packet without compression</a>
|
||||
*/
|
||||
public interface Packet {
|
||||
|
||||
}
|
||||
41
protocol/src/main/java/mc/protocol/packets/PingPacket.java
Normal file
41
protocol/src/main/java/mc/protocol/packets/PingPacket.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package mc.protocol.packets;
|
||||
|
||||
import lombok.Data;
|
||||
import mc.protocol.io.NetByteBuf;
|
||||
|
||||
/**
|
||||
* Пинг-пакет.
|
||||
*
|
||||
* <p>Эхо-пакет, которым проверяется качество соединения между <b>Клиентом</b> и <b>Сервером</b>.</p>
|
||||
*
|
||||
* <p>По спецификации:</p>
|
||||
* <oi>
|
||||
* <li>если <b>Сервер</b> не ответил <b>Клиенту</b> в течении 20 секунд, <b>Клиент</b> отключается
|
||||
* и выдаёт ошибку <i>"Timed out"</i>.</li>
|
||||
* <li>если <b>Клиент</b> не отвечает <b>Серверу</b> в течении 30 секунд, <b>Сервер</b> отключает <b>Клиента</b>.</li>
|
||||
* </oi>
|
||||
*
|
||||
* <p>Структура пакета</p>
|
||||
* <pre>
|
||||
* | FIELD | TYPE | NOTES |
|
||||
* |---------|------|------------------------|
|
||||
* | Payload | Long | Любое уникальное число |
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Keep_Alive">Keep Alive</a>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package mc.protocol.packets;
|
||||
|
||||
import mc.protocol.io.NetByteBuf;
|
||||
|
||||
/**
|
||||
* Пакеты отправляемые сервером.
|
||||
*/
|
||||
public interface ServerSidePacket extends Packet {
|
||||
|
||||
void writeSelf(NetByteBuf netByteBuf);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Данный пакет заставляет сервер переключить текущий {@link State}</p>
|
||||
*
|
||||
* <p>Структура пакета</p>
|
||||
* <pre>
|
||||
* | FIELD | TYPE | NOTES |
|
||||
* |------------------|----------------|----------------------------------------------|
|
||||
* | Protocol version | VarInt | Версия протокола [1] |
|
||||
* | Server address | Stirng | Hostname или IP |
|
||||
* | Server port | Unsigned Short | Порт сервера |
|
||||
* | Next stage | VarInt | ID State на который необходимо переключиться |
|
||||
*
|
||||
* [1] - <a href="https://wiki.vg/Protocol_version_numbers" target="_top">Protocol version numbers</a>
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Handshake" target="_top">Handshake</a>
|
||||
* @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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Начало авторизации.</p>
|
||||
*
|
||||
* <p>Структура пакета</p>
|
||||
* <pre>
|
||||
* | FIELD | TYPE | NOTES |
|
||||
* |-------|--------|------------------|
|
||||
* | Name | String | Имя/Логин игрока |
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Login_Start" target="_top">Login start</a>
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
public class LoginStartPacket implements ClientSidePacket {
|
||||
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public void readSelf(NetByteBuf netByteBuf) {
|
||||
this.name = netByteBuf.readString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Клиент запрашивает получение информации о сервере</p>
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@ToString
|
||||
public class StatusServerRequestPacket extends EmptyPacket {
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Отключение клиента сервером с указанием причины.</p>
|
||||
*
|
||||
* <p>Структура пакета</p>
|
||||
* <pre>
|
||||
* | FIELD | TYPE | NOTES |
|
||||
* |-------------|--------|----------------------------------|
|
||||
* | JSON Reason | String | Причина отключения. Опционально. |
|
||||
* </pre>
|
||||
*
|
||||
* <p>Пример JSON Reason</p>
|
||||
* <pre>
|
||||
* {
|
||||
* "text": "foo"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Disconnect_2" target="_top">Disconnect</a>
|
||||
* @see State
|
||||
*/
|
||||
@Data
|
||||
public class DisconnectPacket implements ServerSidePacket {
|
||||
|
||||
/**
|
||||
* Причина отключения.
|
||||
*/
|
||||
private Text reason;
|
||||
|
||||
@Override
|
||||
public void writeSelf(NetByteBuf netByteBuf) {
|
||||
netByteBuf.writeString(TextSerializer.toJsonObject(reason).toString());
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Информация о сервере</p>
|
||||
*
|
||||
* <p>Структура пакета
|
||||
* <pre>
|
||||
* | FIELD | TYPE | NOTES |
|
||||
* |---------------|--------|-----------------------------------------|
|
||||
* | JSON Response | String | Информация о сервере в JSON формате [1] |
|
||||
*
|
||||
* [1] - <a href="https://wiki.vg/index.php?title=Server_List_Ping&oldid=7555#Response" target="_top">Server List Ping: Response</a>
|
||||
* </pre>
|
||||
*
|
||||
* <p>Пример JSON Response</p>
|
||||
* <pre>
|
||||
* {
|
||||
* "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>"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p><code>`$.favicon`</code> должен быть формата PNG и размеры 64x64 px</p>
|
||||
*/
|
||||
@Data
|
||||
public class StatusServerResponse implements ServerSidePacket {
|
||||
|
||||
/**
|
||||
* Информация о серере.
|
||||
*/
|
||||
private ServerInfo info;
|
||||
|
||||
@Override
|
||||
public void writeSelf(NetByteBuf netByteBuf) {
|
||||
netByteBuf.writeString(ServerInfoSerializer.toJsonObject(info).toString());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Character, TextStyle> legacyStyleCodes;
|
||||
private static final Map<Character, TextColor> 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
|
||||
);
|
||||
}
|
||||
}
|
||||
306
protocol/src/test/java/mc/protocol/io/NetByteBufReadTest.java
Normal file
306
protocol/src/test/java/mc/protocol/io/NetByteBufReadTest.java
Normal file
@@ -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<Arguments> paramsReadBoolean() {
|
||||
return Stream.of(
|
||||
Arguments.of((byte) 0x00, false),
|
||||
Arguments.of((byte) 0x01, true)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static Stream<Arguments> paramsReadUnsignedByte() {
|
||||
return Stream.of(
|
||||
Arguments.of((byte) 30, 30),
|
||||
Arguments.of((byte) (0xFF & 130), 130)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static Stream<Arguments> paramsReadString() {
|
||||
return Stream.of(
|
||||
Arguments.of(""),
|
||||
Arguments.of("Latin"),
|
||||
Arguments.of("Кириллица"),
|
||||
Arguments.of("العربية"),
|
||||
Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления
|
||||
Arguments.of("\uD800\uDD07") // Эгейские цифры, [один]
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static Stream<Arguments> 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<Arguments> 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
250
protocol/src/test/java/mc/protocol/io/NetByteBufWriteTest.java
Normal file
250
protocol/src/test/java/mc/protocol/io/NetByteBufWriteTest.java
Normal file
@@ -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<Arguments> paramsWriteBoolean() {
|
||||
return Stream.of(
|
||||
Arguments.of(false, (byte) 0x00),
|
||||
Arguments.of(true, (byte) 0x01)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static Stream<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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 })
|
||||
);
|
||||
}
|
||||
}
|
||||
24
protocol/src/test/java/mc/protocol/model/text/TextTest.java
Normal file
24
protocol/src/test/java/mc/protocol/model/text/TextTest.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Arguments> 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
39
server/build.gradle
Normal file
39
server/build.gradle
Normal file
@@ -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('')
|
||||
}
|
||||
3
server/gradle.properties
Normal file
3
server/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
# suppress inspection "UnusedProperty" for whole file
|
||||
module.name=server
|
||||
module.version=1.0-SNAPSHOT
|
||||
200
server/src/main/java/mc/server/Main.java
Normal file
200
server/src/main/java/mc/server/Main.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
server/src/main/java/mc/server/config/Config.java
Normal file
38
server/src/main/java/mc/server/config/Config.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
67
server/src/main/java/mc/server/di/ConfigModule.java
Normal file
67
server/src/main/java/mc/server/di/ConfigModule.java
Normal file
@@ -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<String, Object> 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> T fromYamlPath(String mapPath, Map<String, Object> 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<String, Object>) object, defaultValue);
|
||||
} else {
|
||||
return (T) object;
|
||||
}
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
server/src/main/java/mc/server/di/ServerComponent.java
Normal file
10
server/src/main/java/mc/server/di/ServerComponent.java
Normal file
@@ -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();
|
||||
}
|
||||
18
server/src/main/resources/config-sample.yml
Normal file
18
server/src/main/resources/config-sample.yml
Normal file
@@ -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
|
||||
@@ -4,11 +4,17 @@
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>%d{HH:mm:ss.SSS} %-5level [%t] [%logger{36}] -- %msg%n</Pattern>
|
||||
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%35.35logger{34}] -- %msg%n</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
<!-- раскоментировать для простотра дампа пакетов
|
||||
<logger name="io.netty.handler.logging.LoggingHandler" level="debug" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
-->
|
||||
</configuration>
|
||||
@@ -10,4 +10,5 @@ rootProject.projectDir.toPath().resolve('gradle.properties').readLines().forEach
|
||||
|
||||
rootProject.name = map.get('project.name')
|
||||
|
||||
include('protocol')
|
||||
include('server')
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user