Archived
0

52 Commits
1.0 ... master

Author SHA1 Message Date
325546a76d update README 2021-05-09 22:12:15 +03:00
4215b5615e update version 2021-05-09 22:07:25 +03:00
10af38e102 Merge branch 'develop' 2021-05-09 22:05:23 +03:00
b049352fe3 PlayerManager 2021-05-09 20:07:08 +03:00
c4767bd240 config: fake online 2021-05-09 18:54:23 +03:00
2d4895fef0 Merge branch 'dev/world' into develop 2021-05-09 18:48:54 +03:00
04316d9cbd грузим чанки при входе 2021-05-09 18:47:51 +03:00
20791ed881 VoidWorld, VoidChunk 2021-05-09 18:43:57 +03:00
2b0ad9895b add World, Chunk interfaces 2021-05-09 18:42:33 +03:00
ab17160f9d config: add view-distance 2021-05-09 18:41:31 +03:00
8a6f37924e PingPacket -> KeepAlivePacket 2021-05-09 17:00:25 +03:00
f10fb46d23 уменьшена скорость отдачи KeepAlive 2021-05-09 16:59:20 +03:00
c6669af651 EntityActionPacket 2021-05-08 20:14:14 +03:00
3984ab3fca порядок пакетов в State 2021-05-08 19:50:09 +03:00
bc2d5a7e75 правки режима и координат спавна 2021-05-08 18:13:24 +03:00
2521860bb4 fix DI 2021-05-06 14:55:14 +03:00
091b5adb91 Merge branch 'dev/network-api' into develop 2021-05-06 14:44:40 +03:00
4c20c7fd02 рефакторинг DI 2021-05-06 14:43:56 +03:00
0aaf17b17f добавлен пул для NettyConnectionContext 2021-05-06 14:21:24 +03:00
d02a80299f рефакторинг протокола 2021-05-06 13:58:05 +03:00
a3eb0eba86 рефакторинг EventBus 2021-05-06 13:50:40 +03:00
de43210747 рефакторинг протокола 2021-05-06 13:43:01 +03:00
39996f9847 Merge branch 'dev/event-bus' into dev/network-api
# Conflicts:
#	protocol/build.gradle
#	protocol/src/main/java/mc/protocol/NettyServer.java
#	protocol/src/main/java/mc/protocol/PacketInboundHandler.java
#	protocol/src/main/java/mc/protocol/State.java
#	protocol/src/main/java/mc/protocol/di/ProtocolModule.java
#	server/src/main/java/mc/server/Main.java
2021-05-06 13:42:01 +03:00
9b183b7d8d Merge branch 'dev/object-pool' into dev/network-api
# Conflicts:
#	protocol/src/main/java/mc/protocol/PacketInboundHandler.java
#	protocol/src/main/java/mc/protocol/di/ProtocolModule.java
#	protocol/src/main/java/mc/protocol/io/codec/ProtocolDecoder.java
2021-05-06 13:30:41 +03:00
5f431ff138 рефакторинг протокола 2021-05-06 13:14:42 +03:00
c4a6a01908 убираем reactor 2021-05-05 20:43:09 +03:00
205e813fc4 простая реализация EventBus 2021-05-05 20:09:49 +03:00
0f1c9bfb1b интерфейс EventBus 2021-05-05 20:09:32 +03:00
b77d6b16e8 повторное использование объектов Packet 2021-05-04 18:49:24 +03:00
87dc18f009 fix PLAY:KeepAlive 2021-05-03 17:10:26 +03:00
7d4c6e383e пересмотр событийной модели 2021-05-03 17:09:46 +03:00
de27654e67 check send packet 2021-05-03 17:07:51 +03:00
627cee9af3 fix 2021-05-03 17:07:22 +03:00
31059a4ad8 fix ChunkDataPacket 2021-05-03 16:06:53 +03:00
5833aab62a немного привёл код в порядок 2021-05-03 15:34:09 +03:00
052593bc14 PlayerLookPacket 2021-05-03 00:46:40 +03:00
531b0b97c1 PlayerPositionPacket 2021-05-03 00:46:40 +03:00
1a4600bdc9 fix NPE 2021-05-03 00:46:40 +03:00
b768ba5bd9 отправка пустого чанка 2021-05-03 00:46:40 +03:00
18a857193f debug: log send packet 2021-05-03 00:46:40 +03:00
a1a629279c debug: packet id as hex 2021-05-03 00:46:40 +03:00
824fdf9569 CPlayerPositionAndLookPacket 2021-05-03 00:46:39 +03:00
0835683294 TeleportConfirmPacket 2021-05-03 00:46:39 +03:00
2bd7fe9841 PlayerPositionAndLookPacket 2021-05-03 00:46:39 +03:00
23fd4e2c1a PlayerAbilitiesPacket 2021-05-03 00:46:39 +03:00
e5856b3d11 SpawnPositionPacket 2021-05-02 18:50:09 +03:00
17189effca PluginMessagePacket 2021-05-02 18:20:07 +03:00
bbeb41dd7e ClientSettingsPacket 2021-05-02 17:14:54 +03:00
3b3a80ca0a JoinGamePacket 2021-05-02 16:25:31 +03:00
a317e3e2c2 начало входа на сервер 2021-05-02 16:24:47 +03:00
19c1666c2e перенос обработчиков пакетов в отдельный класс 2021-05-02 15:05:27 +03:00
3282a3e9c0 next version 2021-05-01 16:57:27 +03:00
67 changed files with 1989 additions and 211 deletions

View File

@@ -1,12 +1,13 @@
# MC-SERVER
![version: 1.0-SNAPSHOT](https://img.shields.io/badge/version-1.0-05b.svg?style=flat)
![codename: ZERO](https://img.shields.io/badge/codename-ZERO-509.svg?style=flat)
![version: 1.1](https://img.shields.io/badge/version-1.1-05b.svg?style=flat)
![codename: VOID](https://img.shields.io/badge/codename-VOID-509.svg?style=flat)
![protocol: 1.12.2](https://img.shields.io/badge/protocol-1.12.2-075.svg?style=flat)
Написанный с нуля сервер **Minecraft 1.12.2**.
На данный момент может только показывать информацию о себе. Подключение к серверу не возможно.
На данный момент сервер может показывать о себе информацию в списке серверов (motd, онлайн, иконка) и позволять
игрокам подключиться к себе. Загружается пустой мир.
---

View File

@@ -1,3 +1,3 @@
project.group=mc-project
project.name=mc-server
project.version=1.0
project.version=1.1

View File

@@ -19,7 +19,8 @@ ext {
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'
jopt : 'net.sf.jopt-simple:jopt-simple:6.0-alpha-3',
objpool : 'org.apache.commons:commons-pool2:2.9.0'
]
libs.logger = [

View File

@@ -1,9 +1,9 @@
apply from: rootDir.toPath().resolve('logic.gradle').toFile()
dependencies {
api libs.netty
api libs.reactor
implementation libs.netty
implementation libs.json
implementation libs.objpool
testImplementation libs.lang3
}

View File

@@ -1,20 +0,0 @@
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);
}
}

View File

@@ -0,0 +1,85 @@
package mc.protocol;
import io.netty.channel.ChannelHandlerContext;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import mc.protocol.api.ConnectionContext;
import mc.protocol.packets.ServerSidePacket;
import mc.protocol.pool.Passivable;
import java.util.Map;
import java.util.Optional;
@EqualsAndHashCode
public class NettyConnectionContext implements ConnectionContext, Passivable {
@Accessors(chain = true)
@Setter
private ChannelHandlerContext ctx;
/**
* @deprecated костыль
*/
@Deprecated
@Getter
@Setter
private boolean usedContext;
@Override
public State getState() {
return ctx.channel().attr(NetworkAttributes.STATE).get();
}
@Override
public void setState(State state) {
ctx.channel().attr(NetworkAttributes.STATE).set(state);
}
/**
* @deprecated костыль
*/
@Deprecated
@Override
public <T> void setCustomProperty(String key, T value) {
Map<String, Object> map = ctx.channel().attr(NetworkAttributes.CUSTOM_PROPERTIES).get();
map.put(key, value);
}
/**
* @deprecated костыль
*/
@Deprecated
@SuppressWarnings("unchecked")
@Override
public <T> Optional<T> getCustomProperty(String key, Class<T> classResult) {
Map<String, Object> map = ctx.channel().attr(NetworkAttributes.CUSTOM_PROPERTIES).get();
return (Optional<T>) Optional.ofNullable(map.getOrDefault(key, null));
}
@Override
public void send(ServerSidePacket packet) {
ctx.write(packet);
}
@Override
public void sendNow(ServerSidePacket packet) {
ctx.writeAndFlush(packet);
}
@Override
public void flushSending() {
ctx.flush();
}
@Override
public void disconnect() {
ctx.disconnect();
}
@Override
public void passivate() {
this.ctx = null;
}
}

View File

@@ -1,29 +1,46 @@
package mc.protocol;
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 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 mc.protocol.api.ConnectionContext;
import mc.protocol.api.Server;
import mc.protocol.event.EventBus;
import mc.protocol.io.codec.ProtocolDecoder;
import mc.protocol.io.codec.ProtocolEncoder;
import mc.protocol.io.codec.ProtocolSplitter;
import mc.protocol.packets.ClientSidePacket;
import javax.annotation.Nonnull;
import javax.inject.Provider;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
@SuppressWarnings("rawtypes")
@Slf4j
@RequiredArgsConstructor
public class NettyServer {
public class NettyServer implements Server {
private final ServerBootstrap serverBootstrap;
private final Map<Class<? extends Packet>, Sinks.Many<ChannelContext>> observedMap;
private final Provider<ProtocolDecoder> protocolDecoderProvider;
private final Provider<PacketInboundHandler> packetInboundHandlerProvider;
private final EventBus eventBus;
private Consumer<ConnectionContext> consumerNewConnection;
private Consumer<ConnectionContext> consumerDisconnect;
@Override
public void bind(String host, int port) {
log.info("Network starting: {}:{}", host, port);
try {
serverBootstrap.bind(host, port).sync().channel().closeFuture().sync();
createServerBootstrap().bind(host, port).sync().channel().closeFuture().sync();
} catch (InterruptedException e) {
if (log.isTraceEnabled()) {
log.trace("{}: {}", e.getClass().getSimpleName(), e.getMessage(), e);
@@ -31,13 +48,56 @@ public class NettyServer {
}
}
@SuppressWarnings("unchecked")
public <P extends Packet> Flux<ChannelContext<P>> packetFlux(Class<P> packetClass) {
return observedMap.get(packetClass).asFlux().map(ChannelContext.class::cast);
@Override
public void onNewConnect(Consumer<ConnectionContext> consumer) {
this.consumerNewConnection = consumer;
}
public static NettyServer createServer() {
ProtocolComponent component = DaggerProtocolComponent.create();
return component.getNettyServer();
@Override
public void onDisonnect(Consumer<ConnectionContext> consumer) {
this.consumerDisconnect = consumer;
}
@Override
@SuppressWarnings("java:S2326") // Сонар, ты бредишь
public <P extends ClientSidePacket> void listenPacket(State state, Class<P> packetClass, EventBus.EventHandler<P> eventHandler) {
this.eventBus.subscribe(state, packetClass, eventHandler);
}
private ServerBootstrap createServerBootstrap() {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(createChannelChannelInitializer());
return bootstrap;
}
private ChannelInitializer<SocketChannel> createChannelChannelInitializer() {
return new ChannelInitializer<>() {
@Override
protected void initChannel(@Nonnull SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
createChannelHandlerMap().forEach(pipeline::addLast);
}
};
}
private Map<String, ChannelHandler> createChannelHandlerMap() {
Map<String, ChannelHandler> map = new LinkedHashMap<>();
map.put("packet_splitter", new ProtocolSplitter());
map.put("logger", new LoggingHandler(LogLevel.DEBUG));
ProtocolDecoder protocolDecoder = protocolDecoderProvider.get();
protocolDecoder.setConsumerNewConnection(consumerNewConnection);
protocolDecoder.setConsumerDisconnect(consumerDisconnect);
map.put("packet_decoder", protocolDecoder);
map.put("packet_encoder", new ProtocolEncoder());
map.put("packet_handler", packetInboundHandlerProvider.get());
return map;
}
}

View File

@@ -3,8 +3,16 @@ package mc.protocol;
import io.netty.util.AttributeKey;
import lombok.experimental.UtilityClass;
import java.util.Map;
@UtilityClass
public class NetworkAttributes {
public static final AttributeKey<State> STATE = AttributeKey.newInstance("STATE");
/**
* @deprecated костыль
*/
@Deprecated
public static final AttributeKey<Map<String, Object>> CUSTOM_PROPERTIES = AttributeKey.newInstance("CUSTOM_PROPERTIES");
}

View File

@@ -3,19 +3,46 @@ 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 lombok.extern.slf4j.Slf4j;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.event.EventBus;
import mc.protocol.pool.PacketPool;
import org.apache.commons.pool2.ObjectPool;
import java.util.Map;
import java.io.IOException;
@SuppressWarnings("rawtypes")
@Slf4j
@RequiredArgsConstructor
public class PacketInboundHandler extends SimpleChannelInboundHandler<Packet> {
public class PacketInboundHandler extends SimpleChannelInboundHandler<ClientSidePacket> {
private final Map<Class<? extends Packet>, Sinks.Many<ChannelContext>> observedMap;
private static final String CLIENT_FORCE_DISCONNECTED_IOEXCEPTION_MESSAGE_RU = "Программа на вашем хост-компьютере разорвала установленное подключение";
private final ObjectPool<NettyConnectionContext> poolNettyConnectionContext;
private final PacketPool poolPackets;
private final EventBus eventBus;
@Override
protected void channelRead0(ChannelHandlerContext ctx, Packet packet) {
observedMap.get(packet.getClass()).tryEmitNext(new ChannelContext<>(ctx, packet));
protected void channelRead0(ChannelHandlerContext ctx, ClientSidePacket packet) throws Exception {
State state = ctx.channel().attr(NetworkAttributes.STATE).get();
NettyConnectionContext context = poolNettyConnectionContext.borrowObject().setCtx(ctx);
eventBus.emit(state, context, packet);
if (!context.isUsedContext()) {
poolNettyConnectionContext.returnObject(context);
}
poolPackets.returnObject(packet);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof IOException && cause.getLocalizedMessage().equalsIgnoreCase(CLIENT_FORCE_DISCONNECTED_IOEXCEPTION_MESSAGE_RU)) {
log.warn("Client '{}' force disconnected", ctx.channel().remoteAddress());
if (log.isTraceEnabled()) {
log.trace("", cause);
}
} else {
log.error("{}", cause.getMessage(), cause);
}
}
}

View File

@@ -4,13 +4,10 @@ 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.KeepAlivePacket;
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 mc.protocol.packets.client.*;
import mc.protocol.packets.server.*;
import javax.annotation.Nullable;
import java.util.Collections;
@@ -27,19 +24,44 @@ public enum State {
// client side
Map.of(
0x00, StatusServerRequestPacket.class,
0x01, PingPacket.class
0x01, KeepAlivePacket.class
),
// server side
Map.of(
StatusServerResponse.class, 0x00,
PingPacket.class, 0x01
KeepAlivePacket.class, 0x01
)
),
LOGIN(2,
// server bound
Map.of(0x00, LoginStartPacket.class),
// client bound
Map.of(DisconnectPacket.class, 0x00)
Map.of(
DisconnectPacket.class, 0x00,
LoginSuccessPacket.class, 0x02
)
),
PLAY(3,
// server bound
Map.of(
0x00, TeleportConfirmPacket.class,
0x04, ClientSettingsPacket.class,
0x09, PluginMessagePacket.class,
0x0B, KeepAlivePacket.class,
0x0D, PlayerPositionPacket.class,
0x0E, CPlayerPositionAndLookPacket.class,
0x0F, PlayerLookPacket.class,
0x15, EntityActionPacket.class
),
// client bound
Map.of(
KeepAlivePacket.class, 0x1F,
ChunkDataPacket.class, 0x20,
JoinGamePacket.class, 0x23,
PlayerAbilitiesPacket.class,0x2C,
SPlayerPositionAndLookPacket.class, 0x2F,
SpawnPositionPacket.class, 0x46
)
);
@Nullable

View File

@@ -0,0 +1,42 @@
package mc.protocol.api;
import mc.protocol.State;
import mc.protocol.packets.ServerSidePacket;
import java.util.Optional;
public interface ConnectionContext {
/**
* @deprecated костыль
*/
@Deprecated
void setUsedContext(boolean value);
/**
* @deprecated костыль
*/
@Deprecated
boolean isUsedContext();
State getState();
void setState(State state);
/**
* @deprecated костыль
*/
@Deprecated
<T> void setCustomProperty(String key, T value);
/**
* @deprecated костыль
*/
@Deprecated
<T> Optional<T> getCustomProperty(String key, Class<T> classResult);
void send(ServerSidePacket packet);
void sendNow(ServerSidePacket packet);
void flushSending();
void disconnect();
}

View File

@@ -0,0 +1,18 @@
package mc.protocol.api;
import mc.protocol.State;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.event.EventBus;
import java.util.function.Consumer;
public interface Server {
void bind(String host, int port);
void onNewConnect(Consumer<ConnectionContext> consumer);
void onDisonnect(Consumer<ConnectionContext> consumer);
@SuppressWarnings("java:S2326") // Сонар, ты бредишь
<P extends ClientSidePacket> void listenPacket(State state, Class<P> packetClass, EventBus.EventHandler<P> eventHandler);
}

View File

@@ -0,0 +1,42 @@
package mc.protocol.di;
import dagger.Module;
import dagger.Provides;
import mc.protocol.NettyConnectionContext;
import mc.protocol.State;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.packets.UnknownPacket;
import mc.protocol.pool.NettyConnectionContextFactory;
import mc.protocol.pool.PacketFactory;
import mc.protocol.pool.PacketPool;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Module
public class PoolModule {
@Provides
@ServerScope
@SuppressWarnings({ "rawtypes", "unchecked" })
PacketPool providePacketPool() {
Map<Class<? extends ClientSidePacket>, ObjectPool> map = Stream.of(State.values())
.flatMap(state -> state.getClientSidePackets().values().stream())
.distinct()
.collect(Collectors.toMap(
packetClass -> packetClass,
packetClass -> new GenericObjectPool(new PacketFactory<>(packetClass))));
map.put(UnknownPacket.class, new GenericObjectPool(new PacketFactory<>(UnknownPacket.class)));
return new PacketPool(map);
}
@Provides
@ServerScope
ObjectPool<NettyConnectionContext> providePoolNettyConnectionContext() {
return new GenericObjectPool<>(new NettyConnectionContextFactory());
}
}

View File

@@ -1,11 +1,14 @@
package mc.protocol.di;
import dagger.Component;
import mc.protocol.NettyServer;
import mc.protocol.api.Server;
@Component(modules = ProtocolModule.class)
@Component(modules = {
ProtocolModule.class,
PoolModule.class
})
@ServerScope
public interface ProtocolComponent {
NettyServer getNettyServer();
Server getServer();
}

View File

@@ -2,88 +2,56 @@ 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 lombok.RequiredArgsConstructor;
import mc.protocol.NettyConnectionContext;
import mc.protocol.NettyServer;
import mc.protocol.PacketInboundHandler;
import mc.protocol.State;
import mc.protocol.api.Server;
import mc.protocol.event.EventBus;
import mc.protocol.event.SimpleEventBus;
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 mc.protocol.pool.PacketPool;
import org.apache.commons.pool2.ObjectPool;
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
@RequiredArgsConstructor
public class ProtocolModule {
@SuppressWarnings("rawtypes")
@Provides
NettyServer provideServer(ServerBootstrap serverBootstrap,
Map<Class<? extends Packet>, Sinks.Many<ChannelContext>> observedMap) {
return new NettyServer(serverBootstrap, observedMap);
}
private final boolean readUnknownPackets;
@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()));
Server provideServer(
Provider<ProtocolDecoder> protocolDecoderProvider,
Provider<PacketInboundHandler> packetInboundHandlerProvider,
EventBus eventBus
) {
return new NettyServer(protocolDecoderProvider, packetInboundHandlerProvider, eventBus);
}
@Provides
ProtocolDecoder provideProtocolDecoder(
ObjectPool<NettyConnectionContext> poolNettyConnectionContext,
PacketPool poolPackets
) {
return new ProtocolDecoder(readUnknownPackets, poolNettyConnectionContext, poolPackets);
}
@Provides
PacketInboundHandler providePacketInboundHandler(
ObjectPool<NettyConnectionContext> poolNettyConnectionContext,
PacketPool packetPool,
EventBus eventBus
) {
return new PacketInboundHandler(poolNettyConnectionContext, packetPool, eventBus);
}
@Provides
@ServerScope
EventBus provideEventBus() {
return new SimpleEventBus();
}
}

View File

@@ -0,0 +1,17 @@
package mc.protocol.event;
import mc.protocol.State;
import mc.protocol.api.ConnectionContext;
import mc.protocol.packets.ClientSidePacket;
public interface EventBus {
<P extends ClientSidePacket> void subscribe(State state, Class<P> packetClass, EventHandler<P> eventHandler);
<P extends ClientSidePacket> void emit(State state, ConnectionContext channelContext, P packet);
@FunctionalInterface
interface EventHandler<P extends ClientSidePacket> {
void handle(ConnectionContext channelContext, P packet);
}
}

View File

@@ -0,0 +1,26 @@
package mc.protocol.event;
import mc.protocol.State;
import mc.protocol.api.ConnectionContext;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.utils.Table;
@SuppressWarnings({ "rawtypes", "unchecked" })
public class SimpleEventBus implements EventBus {
private final Table<State, Class<? extends ClientSidePacket>, EventHandler> table = new Table<>();
@Override
public <P extends ClientSidePacket> void subscribe(State state, Class<P> packetClass, EventHandler<P> eventHandler) {
table.put(state, packetClass, eventHandler);
}
@Override
public <P extends ClientSidePacket> void emit(State state, ConnectionContext channelContext, P packet) {
EventHandler eventHandler = table.getColumnAndRow(state, packet.getClass());
if (eventHandler != null) {
eventHandler.handle(channelContext, packet);
}
}
}

View File

@@ -32,6 +32,8 @@ import java.util.UUID;
* | | | | этого числа). |
* | VarInt | >= 1 ; <= 5 | Число от -2147483648 и 2147483647 | 32-bit число с плавающей размерностью от 1 до 5 байт |
* | VarLong | >= 1 ; <= 10 | Число от -9223372036854775808 и 9223372036854775807 | 64-bit число с плавающей размерностью от 1 до 10 байт |
* | Position | 8 | 64-bit число разделённое на три части: x, y, z | Кодируется формулой: |
* | | | | ((x & 0x3FFFFFF) << 38) | ((y & 0xFFF) << 26) | (z & 0x3FFFFFF) |
*
* [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>
@@ -49,6 +51,10 @@ public class NetByteBuf extends ByteBuf {
@Delegate
private final ByteBuf byteBuf;
public void writeUnsignedByte(int value) {
byteBuf.writeByte((byte)(value & 0xFF));
}
//region String
public String readString() {
return readString(Short.MAX_VALUE);

View File

@@ -4,31 +4,57 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.NettyConnectionContext;
import mc.protocol.NetworkAttributes;
import mc.protocol.State;
import mc.protocol.api.ConnectionContext;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.packets.UnknownPacket;
import mc.protocol.pool.PacketPool;
import org.apache.commons.pool2.ObjectPool;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
@Slf4j
@RequiredArgsConstructor
@Slf4j
public class ProtocolDecoder extends ByteToMessageDecoder {
private final boolean readUnknownPackets;
private final ObjectPool<NettyConnectionContext> poolNettyConnectionContext;
private final PacketPool poolPackets;
@Setter
private Consumer<ConnectionContext> consumerNewConnection;
@Setter
private Consumer<ConnectionContext> consumerDisconnect;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().attr(NetworkAttributes.STATE).set(State.HANDSHAKING);
public void channelActive(@Nonnull ChannelHandlerContext ctx) throws Exception {
ctx.channel().attr(NetworkAttributes.CUSTOM_PROPERTIES).set(new HashMap<>());
NettyConnectionContext context = poolNettyConnectionContext.borrowObject().setCtx(ctx);
consumerNewConnection.accept(context);
poolNettyConnectionContext.returnObject(context);
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().attr(NetworkAttributes.STATE).set(null);
public void channelInactive(@Nonnull ChannelHandlerContext ctx) throws Exception {
NettyConnectionContext context = poolNettyConnectionContext.borrowObject().setCtx(ctx);
consumerDisconnect.accept(context);
ctx.channel().attr(NetworkAttributes.CUSTOM_PROPERTIES).get().clear();
ctx.channel().attr(NetworkAttributes.CUSTOM_PROPERTIES).set(null);
poolNettyConnectionContext.returnObject(context);
super.channelInactive(ctx);
}
@@ -40,19 +66,30 @@ public class ProtocolDecoder extends ByteToMessageDecoder {
int packetId = netByteBuf.readVarInt();
Class<? extends ClientSidePacket> packetClass = state.getClientSidePacketById(packetId);
if (packetClass == null) {
log.warn("Unkown packet: State {} ; Id {}", state, packetId);
log.warn("Unknown packet: State {} ; Id 0x{}", state, packetIdAsHexcode(packetId));
if (readUnknownPackets) {
UnknownPacket unknownPacket = new UnknownPacket(state, packetId, netByteBuf.readableBytes());
UnknownPacket unknownPacket = poolPackets.borrowObject(UnknownPacket.class);
unknownPacket.setState(state);
unknownPacket.setId(packetId);
unknownPacket.setDataSize(netByteBuf.readableBytes());
unknownPacket.readSelf(netByteBuf);
out.add(unknownPacket);
} else {
netByteBuf.skipBytes(netByteBuf.readableBytes());
}
} else {
ClientSidePacket packet = packetClass.getDeclaredConstructor().newInstance();
ClientSidePacket packet = poolPackets.borrowObject(packetClass);
packet.readSelf(netByteBuf);
log.debug("IN: {}:{}", state, packet);
out.add(packet);
}
}
private String packetIdAsHexcode(int packetId) {
String hexPacketId = Integer.toHexString(packetId).toUpperCase();
if (hexPacketId.length() == 1) hexPacketId = "0" + hexPacketId;
return hexPacketId;
}
}

View File

@@ -4,19 +4,25 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.NetworkAttributes;
import mc.protocol.State;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
import java.util.Objects;
@Slf4j
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()));
Integer packetId = state.getServerSidePacketId(packet.getClass());
if (packetId == null) {
log.error("Unknown send packet: State {} ; Class {}", state, packet.getClass());
return;
}
log.debug("OUT: {}:{}", state, packet);
NetByteBuf buffer = new NetByteBuf(Unpooled.buffer());
buffer.writeVarInt(packetId);

View File

@@ -0,0 +1,24 @@
package mc.protocol.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class Location {
private double x;
private double y;
private double z;
public int getIntX() {
return (int) x;
}
public int getIntZ() {
return (int) z;
}
public Location toChunkXZ() {
return new Location(this.getIntX() >> 4, 0d, this.getIntZ() >> 4);
}
}

View File

@@ -0,0 +1,11 @@
package mc.protocol.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class Look {
private float yaw;
private float pitch;
}

View File

@@ -1,11 +1,12 @@
package mc.protocol.packets;
import mc.protocol.io.NetByteBuf;
import mc.protocol.pool.Passivable;
/**
* Пакеты отправляемые клиентом.
*/
public interface ClientSidePacket extends Packet {
public interface ClientSidePacket extends Packet, Passivable {
void readSelf(NetByteBuf netByteBuf);
}

View File

@@ -13,4 +13,9 @@ public abstract class EmptyPacket implements ClientSidePacket, ServerSidePacket
public void writeSelf(NetByteBuf netByteBuf) {
// empty
}
@Override
public void passivate() {
// pass
}
}

View File

@@ -25,7 +25,7 @@ import mc.protocol.io.NetByteBuf;
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Keep_Alive">Keep Alive</a>
*/
@Data
public class PingPacket implements ClientSidePacket, ServerSidePacket {
public class KeepAlivePacket implements ClientSidePacket, ServerSidePacket {
private Long payload;
@@ -34,6 +34,11 @@ public class PingPacket implements ClientSidePacket, ServerSidePacket {
payload = netByteBuf.readLong();
}
@Override
public void passivate() {
this.payload = null;
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeLong(payload);

View File

@@ -1,17 +1,19 @@
package mc.protocol.packets;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.State;
import mc.protocol.io.NetByteBuf;
@NoArgsConstructor
@Data
@ToString(exclude = "rawData")
public class UnknownPacket implements ClientSidePacket {
private final State state;
private final int id;
private final int dataSize;
private State state;
private int id;
private int dataSize;
private byte[] rawData;
@Override
@@ -19,4 +21,12 @@ public class UnknownPacket implements ClientSidePacket {
rawData = new byte[dataSize];
netByteBuf.readBytes(rawData);
}
@Override
public void passivate() {
this.state = null;
this.id = 0;
this.dataSize = 0;
this.rawData = null;
}
}

View File

@@ -0,0 +1,64 @@
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.model.Location;
import mc.protocol.model.Look;
import mc.protocol.packets.ClientSidePacket;
/**
* Клиент сообщает о движении и повороте головы Игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-----------|---------|------------------------------------------------------------|
* | X | Double | Абсолютная позиция по X |
* | Y | Double | Абсолютная позиция по Y. |
* | | | Имеется ввиду позиция ног. Голова находиться выше на 1.62f |
* | Z | Double | Абсолютная позиция по Z |
* | Yaw | Float | Абсолютный поворот головы по OX, в градусах |
* | Pitch | Float | Абсолютный поворот головы по OY, в градусах |
* | On Ground | Boolean | true, если Игрок находится на земле |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Position_And_Look_.28serverbound.29">Player Position And Look (serverbound)</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class CPlayerPositionAndLookPacket implements ClientSidePacket {
private Location position;
private Look look;
private boolean onGround;
@Override
public void readSelf(NetByteBuf netByteBuf) {
double x = netByteBuf.readDouble();
double y = netByteBuf.readDouble();
double z = netByteBuf.readDouble();
this.position = new Location(x, y, z);
float yaw = netByteBuf.readFloat();
float pitch = netByteBuf.readFloat();
this.look = new Look(yaw, pitch);
this.onGround = netByteBuf.readBoolean();
}
@Override
public void passivate() {
this.position = null;
this.look = null;
this.onGround = false;
}
public double getYPositionHead() {
return this.position.getY() + 1.62f;
}
}

View File

@@ -0,0 +1,104 @@
package mc.protocol.packets.client;
import lombok.*;
import mc.protocol.utils.ChatMode;
import mc.protocol.utils.MainHand;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
/**
* Client settings packet.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------------- |---------------|---------------------------------------------------|
* | Locale | String (16) | например en_gb |
* | View Distance | Byte | Дистанция отрисовки со стороны Клиента, в чанках. |
* | Chat Mode | VarInt | 0: enabled |
* | | | 1: commands only |
* | | | 2: hidden |
* | | | [1] |
* | Chat Colors | Boolean | “Colors” multiplayer setting (???) |
* | Displayed Skin Parts | Unsigned Byte | битовая маска отображения скина. См. ниже |
* | Main Hand | VarInt | 0: Left |
* | | | 1: Right |
*
* [1] - <a href="https://wiki.vg/index.php?title=Chat&oldid=13165#Processing_chat">Processing chat</a>
* </pre>
*
* <p>Биты "Displayed Skin Parts"</p>
* <pre>
* Bit 0 (0x01): Плащ (Cape)
* Bit 1 (0x02): Рубашка (Jacket)
* Bit 2 (0x04): Левый рукав (Left Sleeve)
* Bit 3 (0x08): Правый рукав (Right Sleeve)
* Bit 4 (0x10): Левая штанина (Left Pants Leg)
* Bit 5 (0x20): Правая штанина (Right Pants Leg)
* Bit 6 (0x40): Шлем (Hat)
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Client_Settings">Client Settings</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class ClientSettingsPacket implements ClientSidePacket {
private String locale;
private int viewDistance;
private ChatMode chatMode;
private boolean chatColors;
@SuppressWarnings("java:S116")
private int $displayedSkinPartsBitMask;
private MainHand mainHand;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.locale = netByteBuf.readString(16);
this.viewDistance = netByteBuf.readByte();
this.chatMode = ChatMode.valueById(netByteBuf.readVarInt());
this.chatColors = netByteBuf.readBoolean();
this.$displayedSkinPartsBitMask = netByteBuf.readUnsignedByte();
this.mainHand = MainHand.valueById(netByteBuf.readVarInt());
}
@Override
public void passivate() {
this.locale = null;
this.viewDistance = 0;
this.chatMode = null;
this.chatColors = false;
this.$displayedSkinPartsBitMask = 0;
this.mainHand = null;
}
public boolean isCapeEnabled() {
return ($displayedSkinPartsBitMask & 0x01) > 0;
}
public boolean isJacketEnabled() {
return ($displayedSkinPartsBitMask & 0x02) > 0;
}
public boolean isLeftSleeveEnabled() {
return ($displayedSkinPartsBitMask & 0x04) > 0;
}
public boolean isRightSleeveEnabled() {
return ($displayedSkinPartsBitMask & 0x08) > 0;
}
public boolean isLeftPantsEnabled() {
return ($displayedSkinPartsBitMask & 0x10) > 0;
}
public boolean isRightPantsEnabled() {
return ($displayedSkinPartsBitMask & 0x20) > 0;
}
public boolean isHatEnabled() {
return ($displayedSkinPartsBitMask & 0x40) > 0;
}
}

View File

@@ -0,0 +1,52 @@
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;
import mc.protocol.utils.EntityActionAction;
/**
* Entity Action packet.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |------------|--------|-------------------------------------------|
* | Entity ID | VarInt | ID игрока |
* | Action ID | VarInt | ID действия |
* | Jump Boost | VarInt | Используется только при "Action ID" = 5. |
* | | | В этом случае значение будет от 0 до 100. |
* | | | В остальных случаях значение 0. |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Entity_Action" target="_top">Entity Action</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class EntityActionPacket implements ClientSidePacket {
private Integer entityId;
private EntityActionAction action;
private Integer jumpBoost;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.entityId = netByteBuf.readVarInt();
int actionId = netByteBuf.readVarInt();
this.jumpBoost = netByteBuf.readVarInt();
this.action = EntityActionAction.valueOfCode(actionId);
}
@Override
public void passivate() {
this.entityId = null;
this.action = null;
this.jumpBoost = null;
}
}

View File

@@ -47,4 +47,12 @@ public class HandshakePacket implements ClientSidePacket {
nextState = State.getById(netByteBuf.readVarInt());
}
@Override
public void passivate() {
this.protocolVersion = 0;
this.host = null;
this.port = 0;
this.nextState = null;
}
}

View File

@@ -34,4 +34,9 @@ public class LoginStartPacket implements ClientSidePacket {
this.name = netByteBuf.readString();
}
@Override
public void passivate() {
this.name = null;
}
}

View File

@@ -0,0 +1,48 @@
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.model.Look;
import mc.protocol.packets.ClientSidePacket;
/**
* Клиент сообщает о повороте головы Игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-----------|---------|---------------------------------------------|
* | Yaw | Float | Абсолютный поворот головы по OX, в градусах |
* | Pitch | Float | Абсолютный поворот головы по OY, в градусах |
* | On Ground | Boolean | true, если Игрок находится на земле |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Look">Player Look</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class PlayerLookPacket implements ClientSidePacket {
private Look look;
private boolean onGround;
@Override
public void readSelf(NetByteBuf netByteBuf) {
float yaw = netByteBuf.readFloat();
float pitch = netByteBuf.readFloat();
this.look = new Look(yaw, pitch);
this.onGround = netByteBuf.readBoolean();
}
@Override
public void passivate() {
this.look = null;
this.onGround = false;
}
}

View File

@@ -0,0 +1,55 @@
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.model.Location;
import mc.protocol.packets.ClientSidePacket;
/**
* Клиент сообщает о движении Игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-----------|---------|-------------------------------------|
* | X | Double | Абсолютная позиция по X |
* | Feet Y | Double | Абсолютная позиция ног по Y. |
* | | | Голова находиться выше на 1.62f |
* | Z | Double | Абсолютная позиция по Z |
* | On Ground | Boolean | true, если Игрок находится на земле |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Position">Player Position</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class PlayerPositionPacket implements ClientSidePacket {
private Location position;
private boolean onGround;
@Override
public void readSelf(NetByteBuf netByteBuf) {
double x = netByteBuf.readDouble();
double y = netByteBuf.readDouble();
double z = netByteBuf.readDouble();
this.position = new Location(x, y, z);
this.onGround = netByteBuf.readBoolean();
}
@Override
public void passivate() {
this.position = null;
this.onGround = false;
}
public double getYPositionHead() {
return this.position.getY() + 1.62f;
}
}

View File

@@ -0,0 +1,48 @@
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;
/**
* Plugin Message packet.
*
* <p>Канал связи для модов и плагинов.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |--------------|-------------|------------------|
* | Channel name | String (20) | Название канала |
* | Data | Byte array | Любые данные |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Plugin_Message_.28serverbound.29">Plugin Message (serverbound)</a>
* @see <a href="https://wiki.vg/index.php?title=Plugin_channels&oldid=14089">Plugin channels</a>
* @see <a href="https://dinnerbone.com/blog/2012/01/13/minecraft-plugin-channels-messaging/">Minecraft Plugin Channels + Messaging</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class PluginMessagePacket implements ClientSidePacket {
private String channelName;
private byte[] rawData;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.channelName = netByteBuf.readString(20);
this.rawData = new byte[netByteBuf.readableBytes()];
netByteBuf.readBytes(this.rawData);
}
@Override
public void passivate() {
this.channelName = null;
this.rawData = null;
}
}

View File

@@ -0,0 +1,41 @@
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;
import mc.protocol.packets.server.SPlayerPositionAndLookPacket;
/**
* Teleport сonfirm packet.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------|--------|-----------------------------------------------------------|
* | Teleport ID | VarInt | ID, который был выдан пакетом {@link SPlayerPositionAndLookPacket} |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Login_Start" target="_top">Login start</a>
* @see SPlayerPositionAndLookPacket
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class TeleportConfirmPacket implements ClientSidePacket {
private int teleportId;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.teleportId = netByteBuf.readVarInt();
}
@Override
public void passivate() {
this.teleportId = 0;
}
}

View File

@@ -0,0 +1,108 @@
package mc.protocol.packets.server;
import io.netty.buffer.Unpooled;
import lombok.Data;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
/**
* Данные чанка.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |--------------------------|------------- |------------------------------------------------------------------------------------|
* | Chunk X | Integer | Координаты чанка (координата блока, делённая на 16, округленная в меньшую сторону) |
* | Chunk Z | Integer | Координаты чанка (координата блока, делённая на 16, округленная в меньшую сторону) |
* | Is Full chunk | Boolean | См. Chunk Format |
* | Available Sections | VarInt | Битовая маска, где каждый бит - это часть чанка (0-15) |
* | Size of Data | VarInt | Размер поля "Data" |
* | Data | Byte array | Данные чанка. См. Chunk Format |
* | Number of block entities | VarInt | Количество элементов в поле "Block entities" |
* | Block entities | Array of NBT | Все сущности в чанке |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Chunk_Data">Chunk Data</a>
* @see <a href="https://wiki.vg/index.php?title=Chunk_Format&oldid=14135">Chunk Format</a>
*/
@Data
public class ChunkDataPacket implements ServerSidePacket {
private static NetByteBuf voidData;
private int x;
private int z;
@SuppressWarnings("java:S125")
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeInt(x);
netByteBuf.writeInt(z);
/* Временное отключение кода
netByteBuf.writeBoolean(true); // Is Full chunk
netByteBuf.writeVarInt(0b11111111); // Available Sections
NetByteBuf data = new NetByteBuf(Unpooled.buffer());
// <Data>
for (int i = 0; i < 16; i++) {
NetByteBuf dataBuff = new NetByteBuf(Unpooled.wrappedBuffer(new byte[4096]));
NetByteBuf blockLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048]));
NetByteBuf skyLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048]));
NetByteBuf biomes = new NetByteBuf(Unpooled.wrappedBuffer(new byte[256]));
// <Chunk Section>
data.writeUnsignedByte(13); // Bits Per Block
// <Palette>
data.writeUnsignedByte(0); // Palette Length (for direct)
// <Palette Data/>
// </Palette>
data.writeVarInt(dataBuff.readableBytes()); // Data Array Length
data.writeBytes(dataBuff); // Data Array
data.writeBytes(blockLight); // Block Light
data.writeBytes(skyLight); // Sky Light
// </Chunk Section>
data.writeBytes(biomes); // Biomes
}
// </Data>
netByteBuf.writeVarInt(data.readableBytes()); // Size of Data
netByteBuf.writeBytes(data); // Data
netByteBuf.writeVarInt(0); // Number of block entities
// write NBT's
*/
netByteBuf.writeBytes(voidData);
voidData.resetReaderIndex();
voidData.resetWriterIndex();
}
static {
voidData = new NetByteBuf(Unpooled.buffer());
voidData.writeBoolean(true); // Is Full chunk
voidData.writeVarInt(0b11111111); // Available Sections
NetByteBuf data = new NetByteBuf(Unpooled.buffer());
for (int i = 0; i < 16; i++) {
NetByteBuf dataBuff = new NetByteBuf(Unpooled.wrappedBuffer(new byte[4096]));
NetByteBuf blockLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048]));
NetByteBuf skyLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048]));
NetByteBuf biomes = new NetByteBuf(Unpooled.wrappedBuffer(new byte[256]));
data.writeUnsignedByte(13);
data.writeUnsignedByte(0);
data.writeVarInt(dataBuff.readableBytes());
data.writeBytes(dataBuff);
data.writeBytes(blockLight);
data.writeBytes(skyLight);
data.writeBytes(biomes);
}
voidData.writeVarInt(data.readableBytes());
voidData.writeBytes(data);
voidData.writeVarInt(0);
voidData.markReaderIndex();
voidData.markWriterIndex();
}
}

View File

@@ -0,0 +1,59 @@
package mc.protocol.packets.server;
import lombok.Data;
import mc.protocol.utils.Difficulty;
import mc.protocol.utils.GameMode;
import mc.protocol.utils.LevelType;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
/**
* Join game packet.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |--------------------|---------------|----------------------------------------------------------------------|
* | Entity ID | Integer | ID сущности (игрока) |
* | Gamemode | Unsigned Byte | 0: Survival |
* | | | 1: Creative |
* | | | 2: Adventure |
* | | | 3: Spectator |
* | | | Bit 3 (0x8) is the hardcore flag. |
* | Dimension | Integer | -1: Nether |
* | | | 0: Overworld |
* | | | 1: End |
* | Difficulty | Unsigned Byte | 0: peaceful |
* | | | 1: easy |
* | | | 2: normal |
* | | | 3: hard |
* | Max Players | Unsigned Byte | Когда-то использовался клиентом для |
* | | | отображения списка игроков. Теперь не используется |
* | Level Type | String (16) | Принимает одно из значений: |
* | | | default, flat, largeBiomes, amplified, default_1_1 |
* | Reduced Debug Info | Boolean | Если true, то Клиент отображает меньше отладочной информации (в F3?) |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Join_Game">Join Game</a>
*/
@Data
public class JoinGamePacket implements ServerSidePacket {
private int entityId;
private GameMode gameMode;
private int dimension;
private Difficulty difficulty;
private LevelType levelType;
private boolean reducedDebugInfo;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeInt(entityId);
netByteBuf.writeUnsignedByte(gameMode.getId());
netByteBuf.writeInt(dimension);
netByteBuf.writeUnsignedByte(difficulty.getId());
netByteBuf.writeUnsignedByte(0); // Max Players, unused
netByteBuf.writeString(levelType.getType());
netByteBuf.writeBoolean(reducedDebugInfo);
}
}

View File

@@ -0,0 +1,33 @@
package mc.protocol.packets.server;
import lombok.Data;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
import java.util.UUID;
/**
* Подтверждение успешного логина.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |----------|-------------|-------------------------------|
* | UUID | String (36) | Уникальный ID игрока |
* | Username | String (16) | Имя игрока, выданное сервером |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Login_Success">Login Success</a>
*/
@Data
public class LoginSuccessPacket implements ServerSidePacket {
private UUID uuid;
private String name;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeString(uuid.toString());
netByteBuf.writeString(name);
}
}

View File

@@ -0,0 +1,60 @@
package mc.protocol.packets.server;
import lombok.Data;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
/**
* Характеристики игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |------------------------------|----------|-----------------------------------------|
* | Flags | Byte | Битовая маска флагов. См. ниже значения |
* | Flying Speed | Float | Скорость полёта |
* | Field of View (FOV) Modifier | Float | Поле зрения |
* </pre>
*
* <p>Флаги "Flags"</p>
* <pre>
* Bit 0x01 - Неуязвимость (Invulnerable)
* Bit 0x02 - В полёте (Flying)
* Bit 0x04 - Может летать (Allow Flying)
* Bit 0x08 - Creative Mode (Instant Break)
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Abilities_.28clientbound.29">Player Abilities</a>
*/
@Data
public class PlayerAbilitiesPacket implements ServerSidePacket {
@SuppressWarnings("java:S116")
private byte $flags = 0;
private float flyingSpeed;
private float fieldOfView;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeByte(this.$flags);
netByteBuf.writeFloat(this.flyingSpeed);
netByteBuf.writeFloat(this.fieldOfView);
}
//FIXME использование value значений
public void setInvulnerable(boolean value) {
this.$flags = (byte) (this.$flags | 0x01);
}
public void setFlying(boolean value) {
this.$flags = (byte) (this.$flags | 0x02);
}
public void setCatFly(boolean value) {
this.$flags = (byte) (this.$flags | 0x04);
}
public void setCreativeMode(boolean value) {
this.$flags = (byte) (this.$flags | 0x08);
}
}

View File

@@ -0,0 +1,83 @@
package mc.protocol.packets.server;
import lombok.Data;
import mc.protocol.io.NetByteBuf;
import mc.protocol.model.Location;
import mc.protocol.model.Look;
import mc.protocol.packets.ServerSidePacket;
import mc.protocol.packets.client.TeleportConfirmPacket;
/**
* Установка позиции и угла осмотра Игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------|--------|-----------------------------------------------------------------------------------|
* | X | Double | Абсолютная или относительная позиция по X. Зависит от "Flags" |
* | Y | Double | Абсолютная или относительная позиция по Y. Зависит от "Flags" |
* | Z | Double | Абсолютная или относительная позиция по Z. Зависит от "Flags" |
* | Yaw | Float | Абсолютный или относительный поворот головы по OX, в градусах. Зависит от "Flags" |
* | Pitch | Float | Абсолютный или относительный поворот головы по OY, в градусах. Зависит от "Flags" |
* | Flags | Byte | Битовая маска значений флагов. См. значения ниже |
* | Teleport ID | VarInt | ID для подтверждения клиентом перемещения Игрока |
* </pre>
*
* <p>Значения "Flags"</p>
* <pre>
* | Field | Bit |
* |-------|------|
* | X | 0x01 |
* | Y | 0x02 |
* | Z | 0x04 |
* | X_ROT | 0x08 |
* | Y_ROT | 0x10 |
* </pre>
*
* <p>Примечание от Dinnerbone про "Flags":</p>
* <i>"It's a bitfield, X/Y/Z/Y_ROT/X_ROT. If X is set, the x value is relative and not absolute."</i>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Position_And_Look_.28clientbound.29">Player Position And Look (clientbound)</a>
* @see TeleportConfirmPacket
*/
@Data
public class SPlayerPositionAndLookPacket implements ServerSidePacket {
private Location position;
private Look look;
@SuppressWarnings("java:S116")
private byte $flags = 0;
private int teleportId;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeDouble(this.position.getX());
netByteBuf.writeDouble(this.position.getY());
netByteBuf.writeDouble(this.position.getZ());
netByteBuf.writeFloat(this.look.getYaw());
netByteBuf.writeFloat(this.look.getPitch());
netByteBuf.writeByte(this.$flags);
netByteBuf.writeVarInt(teleportId);
}
//FIXME использовать value значения
public void setFlagX(boolean value) {
this.$flags = (byte) (this.$flags | 0x01);
}
public void setFlagY(boolean value) {
this.$flags = (byte) (this.$flags | 0x02);
}
public void setFlagZ(boolean value) {
this.$flags = (byte) (this.$flags | 0x04);
}
public void setFlagXRot(boolean value) {
this.$flags = (byte) (this.$flags | 0x08);
}
public void setFlagYRot(boolean value) {
this.$flags = (byte) (this.$flags | 0x10);
}
}

View File

@@ -0,0 +1,41 @@
package mc.protocol.packets.server;
import lombok.Data;
import mc.protocol.io.NetByteBuf;
import mc.protocol.model.Location;
import mc.protocol.packets.ServerSidePacket;
/**
* Спавн позиция игрока.
*
* <p>Используется призаходе игрока на сервер.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |----------|----------|-----------------------|
* | Location | Position | Локация спавна игрока |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Spawn_Position">Spawn Position</a>
*/
@Data
public class SpawnPositionPacket implements ServerSidePacket {
private Location spawn;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
long spawnSerialized =
((long) (floorDouble(spawn.getX()) & 0x3FFFFFF) << 38)
| ((long) (floorDouble(spawn.getY()) & 0xFFF) << 26)
| (floorDouble(spawn.getZ()) & 0x3FFFFFF);
netByteBuf.writeLong(spawnSerialized);
}
private static int floorDouble(double value) {
int i = (int) value;
return value < (double) i ? i - 1 : i;
}
}

View File

@@ -0,0 +1,24 @@
package mc.protocol.pool;
import mc.protocol.NettyConnectionContext;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
public class NettyConnectionContextFactory extends BasePooledObjectFactory<NettyConnectionContext> {
@Override
public NettyConnectionContext create() throws Exception {
return new NettyConnectionContext();
}
@Override
public PooledObject<NettyConnectionContext> wrap(NettyConnectionContext context) {
return new DefaultPooledObject<>(context);
}
@Override
public void passivateObject(PooledObject<NettyConnectionContext> pooledObj) {
pooledObj.getObject().passivate();
}
}

View File

@@ -0,0 +1,30 @@
package mc.protocol.pool;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.packets.ClientSidePacket;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
@Slf4j
@RequiredArgsConstructor
public class PacketFactory<P extends ClientSidePacket> extends BasePooledObjectFactory<P> {
private final Class<P> clazz;
@Override
public P create() throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
@Override
public PooledObject<P> wrap(P packet) {
return new DefaultPooledObject<>(packet);
}
@Override
public void passivateObject(PooledObject<P> pooledPacket) {
pooledPacket.getObject().passivate();
}
}

View File

@@ -0,0 +1,26 @@
package mc.protocol.pool;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.packets.ClientSidePacket;
import org.apache.commons.pool2.ObjectPool;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
public class PacketPool {
@SuppressWarnings("rawtypes")
private final Map<Class<? extends ClientSidePacket>, ObjectPool> mapPoolPackets;
@SuppressWarnings("unchecked")
public <P extends ClientSidePacket> P borrowObject(Class<P> packetClass) throws Exception {
return (P) mapPoolPackets.get(packetClass).borrowObject();
}
@SuppressWarnings("unchecked")
public <P extends ClientSidePacket> void returnObject(P packet) throws Exception {
mapPoolPackets.get(packet.getClass()).returnObject(packet);
}
}

View File

@@ -0,0 +1,6 @@
package mc.protocol.pool;
public interface Passivable {
void passivate();
}

View File

@@ -0,0 +1,20 @@
package mc.protocol.utils;
import javax.annotation.Nullable;
public enum ChatMode {
FULL,
COMMANDS_ONLY,
HIDDEN;
@Nullable
public static ChatMode valueById(int id) {
// а зачем усложнять?
//@formatter:off
if (id == 1) return FULL;
else if (id == 2) return COMMANDS_ONLY;
else if (id == 3) return HIDDEN;
else return null;
//@formatter:on
}
}

View File

@@ -0,0 +1,15 @@
package mc.protocol.utils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum Difficulty {
PEACEFUL(0),
EASY(1),
NORMAL(2),
HARD(3);
private final int id;
}

View File

@@ -0,0 +1,33 @@
package mc.protocol.utils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import javax.annotation.Nullable;
@RequiredArgsConstructor
public enum EntityActionAction {
START_SNEAKING(0),
STOP_SNEAKING(1),
LEAVE_BED(2),
START_SPRINTING(3),
STOP_SPRINTING(4),
START_JUMP_WITH_HORSE(5),
STOP_JUMP_WITH_HORSE(6),
OPEN_HORSE_INVENTORY(7),
START_FLYING_WITH_ELYTRA(8);
@Nullable
public static EntityActionAction valueOfCode(int code) {
for (EntityActionAction action : EntityActionAction.values()) {
if (action.code == code) {
return action;
}
}
return null;
}
@Getter
private final int code;
}

View File

@@ -0,0 +1,15 @@
package mc.protocol.utils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum GameMode {
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2),
SPECTATOR(3);
private final int id;
}

View File

@@ -0,0 +1,16 @@
package mc.protocol.utils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum LevelType {
DEFAULT_TYPE("default"),
FLAT("flat"),
LARGE_BIOMES("largeBiomes"),
AMPLIFIED("amplified"),
DEFAULT_1_1("default_1_1");
private final String type;
}

View File

@@ -0,0 +1,18 @@
package mc.protocol.utils;
import javax.annotation.Nullable;
public enum MainHand {
LEFT,
RIGHT;
@Nullable
public static MainHand valueById(int id) {
// а зачем усложнять?
//@formatter:off
if (id == 0) return LEFT;
else if (id == 1) return RIGHT;
else return null;
//@formatter:on
}
}

View File

@@ -0,0 +1,23 @@
package mc.protocol.utils;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
public class Table<C, R, V> {
private final Map<C, Map<R, V>> map = new HashMap<>();
@Nullable
public V getColumnAndRow(C column, R row) {
if (!map.containsKey(column)) {
return null;
}
return map.get(column).get(row);
}
public void put(C column, R row, V value) {
map.computeIfAbsent(column, c -> new HashMap<>()).put(row, value);
}
}

View File

@@ -0,0 +1,7 @@
package mc.protocol.world;
public interface Chunk {
int getX();
int getZ();
}

View File

@@ -0,0 +1,13 @@
package mc.protocol.world;
import mc.protocol.model.Location;
import mc.protocol.utils.LevelType;
public interface World {
LevelType getLevelType();
Location getSpawn();
Chunk getChunk(int x, int z);
}

View File

@@ -7,20 +7,20 @@ 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.State;
import mc.protocol.api.Server;
import mc.protocol.di.DaggerProtocolComponent;
import mc.protocol.di.ProtocolComponent;
import mc.protocol.di.ProtocolModule;
import mc.protocol.packets.KeepAlivePacket;
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 mc.server.service.PlayerManager;
import org.apache.commons.io.IOUtils;
import org.slf4j.LoggerFactory;
@@ -30,8 +30,6 @@ 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;
@@ -51,46 +49,26 @@ public class Main {
.build();
Config config = serverComponent.getConfig();
PlayerManager playerManager = serverComponent.getPlayerManager();
NettyServer server = NettyServer.createServer();
ProtocolComponent protocolComponent = DaggerProtocolComponent.builder()
.protocolModule(new ProtocolModule(true))
.build();
server.packetFlux(HandshakePacket.class)
.doOnNext(channel -> log.info("{}", channel.getPacket()))
.subscribe(channel -> channel.setState(channel.getPacket().getNextState()));
Server server = protocolComponent.getServer();
PacketHandler packetHandler = serverComponent.getPacketHandler();
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.onNewConnect(connectionContext -> connectionContext.setState(State.HANDSHAKING));
server.onDisonnect(connectionContext -> {
connectionContext.setState(null);
connectionContext.getCustomProperty("player", Player.class).ifPresent(playerManager::remove);
});
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.listenPacket(State.HANDSHAKING, HandshakePacket.class, packetHandler::onHandshake);
server.listenPacket(State.STATUS, KeepAlivePacket.class, packetHandler::onKeepAlive);
server.listenPacket(State.STATUS, StatusServerRequestPacket.class, packetHandler::onServerStatus);
server.listenPacket(State.LOGIN, LoginStartPacket.class, packetHandler::onLoginStart);
server.listenPacket(State.PLAY, KeepAlivePacket.class, packetHandler::onKeepAlivePlay);
server.bind(config.server().host(), config.server().port());
}
@@ -159,17 +137,6 @@ public class Main {
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)) {

View File

@@ -0,0 +1,178 @@
package mc.server;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.*;
import mc.protocol.api.ConnectionContext;
import mc.protocol.model.Location;
import mc.protocol.model.Look;
import mc.protocol.model.ServerInfo;
import mc.protocol.packets.KeepAlivePacket;
import mc.protocol.packets.client.HandshakePacket;
import mc.protocol.packets.client.LoginStartPacket;
import mc.protocol.packets.client.StatusServerRequestPacket;
import mc.protocol.packets.server.*;
import mc.protocol.serializer.TextSerializer;
import mc.protocol.utils.Difficulty;
import mc.protocol.utils.GameMode;
import mc.protocol.world.Chunk;
import mc.protocol.world.World;
import mc.server.config.Config;
import mc.server.service.PlayerManager;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Collections;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Slf4j
@RequiredArgsConstructor
public class PacketHandler {
private final Random random = new Random(System.currentTimeMillis());
private final Config config;
private final World world;
private final PlayerManager playerManager;
public void onHandshake(ConnectionContext context, HandshakePacket packet) {
context.setState(packet.getNextState());
}
public void onKeepAlive(ConnectionContext context, KeepAlivePacket packet) {
context.sendNow(packet);
context.disconnect();
}
public void onKeepAlivePlay(ConnectionContext context, KeepAlivePacket packet) {
try {
TimeUnit.MILLISECONDS.sleep(50);
context.sendNow(packet);
} catch (InterruptedException e) {
if (log.isTraceEnabled()) {
log.trace("{}", e.getMessage(), e);
}
}
}
@SuppressWarnings("unused")
public void onServerStatus(ConnectionContext context, StatusServerRequestPacket packet) {
ServerInfo serverInfo = new ServerInfo();
serverInfo.version().name(ProtocolConstant.PROTOCOL_NAME);
serverInfo.version().protocol(ProtocolConstant.PROTOCOL_NUMBER);
serverInfo.players().max(config.players().maxOnlile());
if (config.players().fakeOnline().enable()) {
serverInfo.players().online(config.players().fakeOnline().value());
} else {
serverInfo.players().online(playerManager.online());
}
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);
context.sendNow(response);
}
@SuppressWarnings("java:S2589")
public void onLoginStart(ConnectionContext context, LoginStartPacket loginStartPacket) {
Player player = playerManager.addAndCreate(context, loginStartPacket.getName(), GameMode.SURVIVAL, world.getSpawn());
context.setCustomProperty("player", player);
var loginSuccessPacket = new LoginSuccessPacket();
loginSuccessPacket.setUuid(player.getUuid());
loginSuccessPacket.setName(player.getName());
context.sendNow(loginSuccessPacket);
context.setState(State.PLAY);
var joinGamePacket = new JoinGamePacket();
joinGamePacket.setEntityId(random.nextInt());
joinGamePacket.setGameMode(player.getGameMode());
joinGamePacket.setDimension(0/*Overworld*/);
joinGamePacket.setDifficulty(Difficulty.PEACEFUL);
joinGamePacket.setLevelType(world.getLevelType());
context.send(joinGamePacket);
var spawnPositionPacket = new SpawnPositionPacket();
spawnPositionPacket.setSpawn(player.getLocation());
context.send(spawnPositionPacket);
var playerAbilitiesPacket = new PlayerAbilitiesPacket();
playerAbilitiesPacket.setCatFly(true);
playerAbilitiesPacket.setFlying(true);
playerAbilitiesPacket.setCreativeMode(false);
playerAbilitiesPacket.setInvulnerable(true);
playerAbilitiesPacket.setFieldOfView(0.0f);
playerAbilitiesPacket.setFlyingSpeed(0.05f);
context.send(playerAbilitiesPacket);
context.flushSending();
Location chunkLocation = player.getLocation().toChunkXZ();
Chunk chunk = world.getChunk(chunkLocation.getIntX(), chunkLocation.getIntZ());
var chunkDataPacket = new ChunkDataPacket();
chunkDataPacket.setX(chunk.getX());
chunkDataPacket.setZ(chunk.getZ());
context.send(chunkDataPacket);
for (int i = 1; i <= config.world().viewDistance(); i++) {
int minX = chunkLocation.getIntX() - i;
int minZ = chunkLocation.getIntZ() - i;
int maxX = chunkLocation.getIntX() + i;
int maxZ = chunkLocation.getIntZ() + i;
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
if ((z == minZ || z == maxZ) || (x == minX || x == maxX)) {
chunkDataPacket = new ChunkDataPacket();
chunkDataPacket.setX(x);
chunkDataPacket.setZ(z);
context.send(chunkDataPacket);
}
}
}
}
context.flushSending();
var playerPositionAndLookPacket = new SPlayerPositionAndLookPacket();
playerPositionAndLookPacket.setPosition(player.getLocation());
playerPositionAndLookPacket.setLook(new Look(0f, 0f));
playerPositionAndLookPacket.setTeleportId(random.nextInt());
context.send(playerPositionAndLookPacket);
KeepAlivePacket keepAlivePacket = new KeepAlivePacket();
keepAlivePacket.setPayload(System.currentTimeMillis());
context.send(keepAlivePacket);
context.flushSending();
}
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 "";
}
}
}

View File

@@ -0,0 +1,20 @@
package mc.server;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import mc.protocol.api.ConnectionContext;
import mc.protocol.model.Location;
import mc.protocol.utils.GameMode;
import java.util.UUID;
@RequiredArgsConstructor
@Getter
public class Player {
private final ConnectionContext connectionContext;
private final UUID uuid;
private final String name;
private final GameMode gameMode;
private final Location location;
}

View File

@@ -15,6 +15,7 @@ public class Config {
private final Server server = new Server();
private final Players players = new Players();
private final World world = new World();
private String motd;
private String disconnectReason;
@@ -32,7 +33,23 @@ public class Config {
@Setter
@ToString
public static class Players {
private final FakeOnline fakeOnline = new FakeOnline();
private int maxOnlile;
private int onlile;
}
@Getter
@Setter
@ToString
public static class FakeOnline {
private boolean enable;
private int value;
}
@Getter
@Setter
@ToString
public static class World {
private int viewDistance;
}
}

View File

@@ -27,10 +27,15 @@ public class ConfigModule {
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));
config.players().fakeOnline().enable(fromYamlPath("players/fake-online/enable", map, false));
config.players().fakeOnline().value(fromYamlPath("players/fake-online/value", map, 0));
config.world().viewDistance(fromYamlPath("world/view-distance", map, 0));
if (Boolean.TRUE.equals(fromYamlPath("icon/enable", map, false))) {
config.iconPath(Paths.get(fromYamlPath("icon/path", map, "favicon.png")));

View File

@@ -0,0 +1,17 @@
package mc.server.di;
import dagger.Module;
import dagger.Provides;
import mc.protocol.world.World;
import mc.server.PacketHandler;
import mc.server.config.Config;
import mc.server.service.PlayerManager;
@Module
public class PacketHandlerModule {
@Provides
public PacketHandler providePacketHandler(Config config, World world, PlayerManager playerManager) {
return new PacketHandler(config, world, playerManager);
}
}

View File

@@ -0,0 +1,16 @@
package mc.server.di;
import dagger.Module;
import dagger.Provides;
import mc.protocol.di.ServerScope;
import mc.server.service.PlayerManager;
@Module
public class PlayersModule {
@Provides
@ServerScope
PlayerManager providePlayerManager() {
return new PlayerManager();
}
}

View File

@@ -1,10 +1,18 @@
package mc.server.di;
import dagger.Component;
import mc.protocol.di.ServerScope;
import mc.server.PacketHandler;
import mc.server.config.Config;
import mc.server.service.PlayerManager;
@Component(modules = ConfigModule.class)
@Component(modules = {
ConfigModule.class, PacketHandlerModule.class, WorldModule.class, PlayersModule.class
})
@ServerScope
public interface ServerComponent {
Config getConfig();
PacketHandler getPacketHandler();
PlayerManager getPlayerManager();
}

View File

@@ -0,0 +1,17 @@
package mc.server.di;
import dagger.Module;
import dagger.Provides;
import mc.protocol.di.ServerScope;
import mc.protocol.world.World;
import mc.server.world.VoidWorld;
@Module
public class WorldModule {
@Provides
@ServerScope
public World provideWorld() {
return new VoidWorld();
}
}

View File

@@ -0,0 +1,29 @@
package mc.server.service;
import mc.protocol.api.ConnectionContext;
import mc.protocol.model.Location;
import mc.protocol.utils.GameMode;
import mc.server.Player;
import java.util.LinkedList;
import java.util.UUID;
public class PlayerManager {
private final LinkedList<Player> players = new LinkedList<>();
public Player addAndCreate(ConnectionContext context, String name, GameMode gameMode, Location location) {
context.setUsedContext(true);
Player player = new Player(context, UUID.randomUUID(), name, gameMode, location);
players.add(player);
return player;
}
public void remove(Player player) {
players.remove(player);
}
public int online() {
return players.size();
}
}

View File

@@ -0,0 +1,13 @@
package mc.server.world;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import mc.protocol.world.Chunk;
@RequiredArgsConstructor
@Getter
public class VoidChunk implements Chunk {
private final int x;
private final int z;
}

View File

@@ -0,0 +1,26 @@
package mc.server.world;
import mc.protocol.model.Location;
import mc.protocol.utils.LevelType;
import mc.protocol.world.Chunk;
import mc.protocol.world.World;
public class VoidWorld implements World {
private static final Location spawn = new Location(7d, 130d, 7d);
@Override
public LevelType getLevelType() {
return LevelType.FLAT;
}
@Override
public Location getSpawn() {
return VoidWorld.spawn;
}
@Override
public Chunk getChunk(int x, int z) {
return new VoidChunk(x, z);
}
}

View File

@@ -10,9 +10,14 @@ disconnect-reason: '&4Server is not available.'
players:
max-online: 0
online: 0
fake-online:
enable: false
value: 0
# Размер значка: 64x64 px
icon:
enable: false
path: favicon.png
world:
view-distance: 1