Initial commit

This commit is contained in:
Melledy
2022-04-17 05:43:07 -07:00
commit 7925d1cda3
354 changed files with 20869 additions and 0 deletions

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

View File

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

View File

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

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