mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-19 10:24:47 +01:00
Initial commit
This commit is contained in:
160
src/main/java/emu/grasscutter/server/game/GameServer.java
Normal file
160
src/main/java/emu/grasscutter/server/game/GameServer.java
Normal file
@@ -0,0 +1,160 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import emu.grasscutter.GenshinConstants;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.GenshinPlayer;
|
||||
import emu.grasscutter.game.dungeons.DungeonManager;
|
||||
import emu.grasscutter.game.gacha.GachaManager;
|
||||
import emu.grasscutter.game.managers.ChatManager;
|
||||
import emu.grasscutter.game.managers.InventoryManager;
|
||||
import emu.grasscutter.game.managers.MultiplayerManager;
|
||||
import emu.grasscutter.game.shop.ShopManager;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
|
||||
import emu.grasscutter.netty.MihoyoKcpServer;
|
||||
|
||||
public class GameServer extends MihoyoKcpServer {
|
||||
private final InetSocketAddress address;
|
||||
private final GameServerPacketHandler packetHandler;
|
||||
private final Timer gameLoop;
|
||||
|
||||
private final Map<Integer, GenshinPlayer> players;
|
||||
|
||||
private final ChatManager chatManager;
|
||||
private final InventoryManager inventoryManager;
|
||||
private final GachaManager gachaManager;
|
||||
private final ShopManager shopManager;
|
||||
private final MultiplayerManager multiplayerManager;
|
||||
private final DungeonManager dungeonManager;
|
||||
|
||||
public GameServer(InetSocketAddress address) {
|
||||
super(address);
|
||||
this.setServerInitializer(new GameServerInitializer(this));
|
||||
this.address = address;
|
||||
this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
|
||||
this.players = new ConcurrentHashMap<>();
|
||||
|
||||
this.chatManager = new ChatManager(this);
|
||||
this.inventoryManager = new InventoryManager(this);
|
||||
this.gachaManager = new GachaManager(this);
|
||||
this.shopManager = new ShopManager(this);
|
||||
this.multiplayerManager = new MultiplayerManager(this);
|
||||
this.dungeonManager = new DungeonManager(this);
|
||||
|
||||
// Ticker
|
||||
this.gameLoop = new Timer();
|
||||
this.gameLoop.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
onTick();
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}, new Date(), 1000L);
|
||||
|
||||
// Shutdown hook
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown));
|
||||
}
|
||||
|
||||
public GameServerPacketHandler getPacketHandler() {
|
||||
return packetHandler;
|
||||
}
|
||||
|
||||
public Map<Integer, GenshinPlayer> getPlayers() {
|
||||
return players;
|
||||
}
|
||||
|
||||
public ChatManager getChatManager() {
|
||||
return chatManager;
|
||||
}
|
||||
|
||||
public InventoryManager getInventoryManager() {
|
||||
return inventoryManager;
|
||||
}
|
||||
|
||||
public GachaManager getGachaManager() {
|
||||
return gachaManager;
|
||||
}
|
||||
|
||||
public ShopManager getShopManager() {
|
||||
return shopManager;
|
||||
}
|
||||
|
||||
public MultiplayerManager getMultiplayerManager() {
|
||||
return multiplayerManager;
|
||||
}
|
||||
|
||||
public DungeonManager getDungeonManager() {
|
||||
return dungeonManager;
|
||||
}
|
||||
|
||||
public void registerPlayer(GenshinPlayer player) {
|
||||
getPlayers().put(player.getId(), player);
|
||||
}
|
||||
|
||||
public GenshinPlayer getPlayerById(int id) {
|
||||
return this.getPlayers().get(id);
|
||||
}
|
||||
|
||||
public GenshinPlayer forceGetPlayerById(int id) {
|
||||
// Console check
|
||||
if (id == GenshinConstants.SERVER_CONSOLE_UID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get from online players
|
||||
GenshinPlayer player = this.getPlayerById(id);
|
||||
|
||||
// Check database if character isnt here
|
||||
if (player == null) {
|
||||
player = DatabaseHelper.getPlayerById(id);
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
public SocialDetail.Builder getSocialDetailById(int id) {
|
||||
// Get from online players
|
||||
GenshinPlayer player = this.forceGetPlayerById(id);
|
||||
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return player.getSocialDetail();
|
||||
}
|
||||
|
||||
public void onTick() throws Exception {
|
||||
for (GenshinPlayer player : this.getPlayers().values()) {
|
||||
player.onTick();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartFinish() {
|
||||
Grasscutter.getLogger().info("Game Server started on port " + address.getPort());
|
||||
}
|
||||
|
||||
public void onServerShutdown() {
|
||||
// Kick and save all players
|
||||
List<GenshinPlayer> list = new ArrayList<>(this.getPlayers().size());
|
||||
list.addAll(this.getPlayers().values());
|
||||
|
||||
for (GenshinPlayer player : list) {
|
||||
player.getSession().close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import emu.grasscutter.netty.MihoyoKcpServerInitializer;
|
||||
import io.jpower.kcp.netty.UkcpChannel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
|
||||
public class GameServerInitializer extends MihoyoKcpServerInitializer {
|
||||
private GameServer server;
|
||||
|
||||
public GameServerInitializer(GameServer server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initChannel(UkcpChannel ch) throws Exception {
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
GameSession session = new GameSession(server);
|
||||
pipeline.addLast(session);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.reflections.Reflections;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.server.game.GameSession.SessionState;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
public class GameServerPacketHandler {
|
||||
private final Int2ObjectMap<PacketHandler> handlers;
|
||||
|
||||
public GameServerPacketHandler(Class<? extends PacketHandler> handlerClass) {
|
||||
this.handlers = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
this.registerHandlers(handlerClass);
|
||||
}
|
||||
|
||||
public void registerHandlers(Class<? extends PacketHandler> handlerClass) {
|
||||
Reflections reflections = new Reflections("emu.grasscutter.server.packet");
|
||||
Set<?> handlerClasses = reflections.getSubTypesOf(handlerClass);
|
||||
|
||||
for (Object obj : handlerClasses) {
|
||||
Class<?> c = (Class<?>) obj;
|
||||
|
||||
try {
|
||||
Opcodes opcode = c.getAnnotation(Opcodes.class);
|
||||
|
||||
if (opcode == null || opcode.disabled() || opcode.value() <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PacketHandler packetHandler = (PacketHandler) c.newInstance();
|
||||
|
||||
this.handlers.put(opcode.value(), packetHandler);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Debug
|
||||
Grasscutter.getLogger().info("Registered " + this.handlers.size() + " " + handlerClass.getSimpleName() + "s");
|
||||
}
|
||||
|
||||
public void handle(GameSession session, int opcode, byte[] header, byte[] payload) {
|
||||
PacketHandler handler = null;
|
||||
|
||||
handler = this.handlers.get(opcode);
|
||||
|
||||
if (handler != null) {
|
||||
try {
|
||||
// Make sure session is ready for packets
|
||||
SessionState state = session.getState();
|
||||
|
||||
if (opcode == PacketOpcodes.PingReq) {
|
||||
// Always continue if packet is ping request
|
||||
} else if (opcode == PacketOpcodes.GetPlayerTokenReq) {
|
||||
if (state != SessionState.WAITING_FOR_TOKEN) {
|
||||
return;
|
||||
}
|
||||
} else if (opcode == PacketOpcodes.PlayerLoginReq) {
|
||||
if (state != SessionState.WAITING_FOR_LOGIN) {
|
||||
return;
|
||||
}
|
||||
} else if (opcode == PacketOpcodes.SetPlayerBornDataReq) {
|
||||
if (state != SessionState.PICKING_CHARACTER) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (state != SessionState.ACTIVE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle
|
||||
handler.handle(session, header, payload);
|
||||
} catch (Exception ex) {
|
||||
// TODO Remove this when no more needed
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return; // Packet successfully handled
|
||||
}
|
||||
|
||||
// Log unhandled packets
|
||||
if (Grasscutter.getConfig().LOG_PACKETS) {
|
||||
//Grasscutter.getLogger().info("Unhandled packet (" + opcode + "): " + PacketOpcodesUtil.getOpcodeName(opcode));
|
||||
}
|
||||
}
|
||||
}
|
||||
250
src/main/java/emu/grasscutter/server/game/GameSession.java
Normal file
250
src/main/java/emu/grasscutter/server/game/GameSession.java
Normal file
@@ -0,0 +1,250 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.game.GenshinPlayer;
|
||||
import emu.grasscutter.net.packet.GenshinPacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodesUtil;
|
||||
import emu.grasscutter.netty.MihoyoKcpChannel;
|
||||
import emu.grasscutter.utils.Crypto;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
|
||||
public class GameSession extends MihoyoKcpChannel {
|
||||
private GameServer server;
|
||||
|
||||
private Account account;
|
||||
private GenshinPlayer player;
|
||||
|
||||
private boolean useSecretKey;
|
||||
private SessionState state;
|
||||
|
||||
private int clientTime;
|
||||
private long lastPingTime;
|
||||
private int lastClientSeq = 10;
|
||||
|
||||
public GameSession(GameServer server) {
|
||||
this.server = server;
|
||||
this.state = SessionState.WAITING_FOR_TOKEN;
|
||||
this.lastPingTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public GameServer getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
public InetSocketAddress getAddress() {
|
||||
if (this.getChannel() == null) {
|
||||
return null;
|
||||
}
|
||||
return this.getChannel().remoteAddress();
|
||||
}
|
||||
|
||||
public boolean useSecretKey() {
|
||||
return useSecretKey;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public void setAccount(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public String getAccountId() {
|
||||
return this.getAccount().getId();
|
||||
}
|
||||
|
||||
public GenshinPlayer getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public synchronized void setPlayer(GenshinPlayer player) {
|
||||
this.player = player;
|
||||
this.player.setSession(this);
|
||||
this.player.setAccount(this.getAccount());
|
||||
}
|
||||
|
||||
public SessionState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(SessionState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public boolean isLoggedIn() {
|
||||
return this.getPlayer() != null;
|
||||
}
|
||||
|
||||
public void setUseSecretKey(boolean useSecretKey) {
|
||||
this.useSecretKey = useSecretKey;
|
||||
}
|
||||
|
||||
public int getClientTime() {
|
||||
return this.clientTime;
|
||||
}
|
||||
|
||||
public long getLastPingTime() {
|
||||
return lastPingTime;
|
||||
}
|
||||
|
||||
public void updateLastPingTime(int clientTime) {
|
||||
this.clientTime = clientTime;
|
||||
this.lastPingTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getNextClientSequence() {
|
||||
return ++lastClientSeq;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConnect() {
|
||||
Grasscutter.getLogger().info("Client connected from " + getAddress().getHostString().toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void onDisconnect() { // Synchronize so we dont add character at the same time
|
||||
Grasscutter.getLogger().info("Client disconnected from " + getAddress().getHostString().toLowerCase());
|
||||
|
||||
// Set state so no more packets can be handled
|
||||
this.setState(SessionState.INACTIVE);
|
||||
|
||||
// Save after disconnecting
|
||||
if (this.isLoggedIn()) {
|
||||
// Save
|
||||
getPlayer().onLogout();
|
||||
// Remove from gameserver
|
||||
getServer().getPlayers().remove(getPlayer().getId());
|
||||
}
|
||||
}
|
||||
|
||||
protected void logPacket(ByteBuffer buf) {
|
||||
ByteBuf b = Unpooled.wrappedBuffer(buf.array());
|
||||
logPacket(b);
|
||||
}
|
||||
|
||||
public void replayPacket(int opcode, String name) {
|
||||
String filePath = Grasscutter.getConfig().PACKETS_FOLDER + name;
|
||||
File p = new File(filePath);
|
||||
|
||||
if (!p.exists()) return;
|
||||
|
||||
byte[] packet = FileUtils.read(p);
|
||||
|
||||
GenshinPacket genshinPacket = new GenshinPacket(opcode);
|
||||
genshinPacket.setData(packet);
|
||||
|
||||
// Log
|
||||
logPacket(genshinPacket.getOpcode());
|
||||
|
||||
send(genshinPacket);
|
||||
}
|
||||
|
||||
public void send(GenshinPacket genshinPacket) {
|
||||
// Test
|
||||
if (genshinPacket.getOpcode() <= 0) {
|
||||
Grasscutter.getLogger().warn("Tried to send packet with missing cmd id!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Header
|
||||
if (genshinPacket.shouldBuildHeader()) {
|
||||
genshinPacket.buildHeader(this.getNextClientSequence());
|
||||
}
|
||||
|
||||
// Build packet
|
||||
byte[] data = genshinPacket.build();
|
||||
|
||||
// Log
|
||||
if (Grasscutter.getConfig().LOG_PACKETS) {
|
||||
logPacket(genshinPacket);
|
||||
}
|
||||
|
||||
// Send
|
||||
send(data);
|
||||
}
|
||||
|
||||
private void logPacket(int opcode) {
|
||||
//Grasscutter.getLogger().info("SEND: " + PacketOpcodesUtil.getOpcodeName(opcode));
|
||||
//System.out.println(Utils.bytesToHex(genshinPacket.getData()));
|
||||
}
|
||||
|
||||
private void logPacket(GenshinPacket genshinPacket) {
|
||||
Grasscutter.getLogger().info("SEND: " + PacketOpcodesUtil.getOpcodeName(genshinPacket.getOpcode()) + " (" + genshinPacket.getOpcode() + ")");
|
||||
System.out.println(Utils.bytesToHex(genshinPacket.getData()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ChannelHandlerContext ctx, ByteBuf data) {
|
||||
// Decrypt and turn back into a packet
|
||||
byte[] byteData = Utils.byteBufToArray(data);
|
||||
Crypto.xor(byteData, useSecretKey() ? Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY);
|
||||
ByteBuf packet = Unpooled.wrappedBuffer(byteData);
|
||||
|
||||
// Log
|
||||
//logPacket(packet);
|
||||
|
||||
// Handle
|
||||
try {
|
||||
while (packet.readableBytes() > 0) {
|
||||
// Length
|
||||
if (packet.readableBytes() < 12) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Packet sanity check
|
||||
int const1 = packet.readShort();
|
||||
if (const1 != 17767) {
|
||||
return; // Bad packet
|
||||
}
|
||||
|
||||
// Data
|
||||
int opcode = packet.readShort();
|
||||
int headerLength = packet.readShort();
|
||||
int payloadLength = packet.readInt();
|
||||
|
||||
byte[] header = new byte[headerLength];
|
||||
byte[] payload = new byte[payloadLength];
|
||||
|
||||
packet.readBytes(header);
|
||||
packet.readBytes(payload);
|
||||
|
||||
// Sanity check #2
|
||||
int const2 = packet.readShort();
|
||||
if (const2 != -30293) {
|
||||
return; // Bad packet
|
||||
}
|
||||
|
||||
// Log packet
|
||||
if (Grasscutter.getConfig().LOG_PACKETS) {
|
||||
Grasscutter.getLogger().info("RECV: " + PacketOpcodesUtil.getOpcodeName(opcode) + " (" + opcode + ")");
|
||||
System.out.println(Utils.bytesToHex(payload));
|
||||
}
|
||||
|
||||
// Handle
|
||||
getServer().getPacketHandler().handle(this, opcode, header, payload);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
packet.release();
|
||||
}
|
||||
}
|
||||
|
||||
public enum SessionState {
|
||||
INACTIVE,
|
||||
WAITING_FOR_TOKEN,
|
||||
WAITING_FOR_LOGIN,
|
||||
PICKING_CHARACTER,
|
||||
ACTIVE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user