Commit 6e81dc39 authored by Magix's avatar Magix Committed by GitHub
Browse files

Update Grasscutter to v1.0.0

Merge development into stable (and all the support hell that comes with it).
parents 1456cd99 4e1ea6ab
......@@ -7,7 +7,7 @@ import emu.grasscutter.utils.Utils;
public class PlayerProfile {
@Transient private GenshinPlayer player;
private int id;
@AlsoLoad("id") private int uid;
private int nameCard;
private int avatarId;
private String name;
......@@ -22,12 +22,12 @@ public class PlayerProfile {
public PlayerProfile() { }
public PlayerProfile(GenshinPlayer player) {
this.id = player.getUid();
this.uid = player.getUid();
this.syncWithCharacter(player);
}
public int getId() {
return id;
public int getUid() {
return uid;
}
public GenshinPlayer getPlayer() {
......@@ -87,6 +87,7 @@ public class PlayerProfile {
return;
}
this.uid = player.getUid();
this.name = player.getNickname();
this.avatarId = player.getHeadImage();
this.signature = player.getSignature();
......
......@@ -92,7 +92,7 @@ public class GachaBanner {
}
public GachaInfo toProto() {
String record = "http://" + (Grasscutter.getConfig().DispatchServerPublicIp.isEmpty() ? Grasscutter.getConfig().DispatchServerIp : Grasscutter.getConfig().DispatchServerPublicIp) + "/gacha";
String record = "http://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + "/gacha";
GachaInfo.Builder info = GachaInfo.newBuilder()
.setGachaType(this.getGachaType())
......
package emu.grasscutter.game.gacha;
import java.io.File;
import java.io.FileReader;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
......@@ -8,6 +10,7 @@ import java.util.concurrent.ThreadLocalRandom;
import com.google.gson.reflect.TypeToken;
import com.sun.nio.file.SensitivityWatchEventModifier;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GenshinData;
import emu.grasscutter.data.def.ItemData;
......@@ -21,16 +24,19 @@ import emu.grasscutter.net.proto.GachaTransferItemOuterClass.GachaTransferItem;
import emu.grasscutter.net.proto.GetGachaInfoRspOuterClass.GetGachaInfoRsp;
import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.game.GameServerTickEvent;
import emu.grasscutter.server.packet.send.PacketDoGachaRsp;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import org.greenrobot.eventbus.Subscribe;
public class GachaManager {
private final GameServer server;
private final Int2ObjectMap<GachaBanner> gachaBanners;
private GetGachaInfoRsp cachedProto;
WatchService watchService;
private int[] yellowAvatars = new int[] {1003, 1016, 1042, 1035, 1041};
private int[] yellowWeapons = new int[] {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
......@@ -45,6 +51,7 @@ public class GachaManager {
this.server = server;
this.gachaBanners = new Int2ObjectOpenHashMap<>();
this.load();
this.startWatcher(server);
}
public GameServer getServer() {
......@@ -65,10 +72,17 @@ public class GachaManager {
public synchronized void load() {
try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Banners.json")) {
getGachaBanners().clear();
List<GachaBanner> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, GachaBanner.class).getType());
if(banners.size() > 0) {
for (GachaBanner banner : banners) {
getGachaBanners().put(banner.getGachaType(), banner);
}
Grasscutter.getLogger().info("Banners successfully loaded.");
this.cachedProto = createProto();
} else {
Grasscutter.getLogger().error("Unable to load banners. Banners size is 0.");
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
......@@ -204,7 +218,6 @@ public class GachaManager {
addStarglitter = 2;
// Add 1 const
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null));
gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(constItemId).setCount(1));
player.getInventory().addItem(constItemId, 1);
} else {
// Is max const
......@@ -267,6 +280,48 @@ public class GachaManager {
player.sendPacket(new PacketDoGachaRsp(banner, list));
}
private synchronized void startWatcher(GameServer server) {
if(this.watchService == null) {
try {
this.watchService = FileSystems.getDefault().newWatchService();
Path path = new File(Grasscutter.getConfig().DATA_FOLDER).toPath();
path.register(watchService, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY}, SensitivityWatchEventModifier.HIGH);
server.OnGameServerTick.register(this);
} catch (Exception e) {
Grasscutter.getLogger().error("Unable to load the Gacha Manager Watch Service. If ServerOptions.watchGacha is true it will not auto-reload");
e.printStackTrace();
}
} else {
Grasscutter.getLogger().error("Cannot reinitialise watcher ");
}
}
@Subscribe
public synchronized void watchBannerJson(GameServerTickEvent tickEvent) {
if(Grasscutter.getConfig().getGameServerOptions().WatchGacha) {
try {
WatchKey watchKey = watchService.take();
for (WatchEvent<?> event : watchKey.pollEvents()) {
final Path changed = (Path) event.context();
if (changed.endsWith("Banners.json")) {
Grasscutter.getLogger().info("Change detected with banners.json. Reloading gacha config");
this.load();
}
}
boolean valid = watchKey.reset();
if (!valid) {
Grasscutter.getLogger().error("Unable to reset Gacha Manager Watch Key. Auto-reload of banners.json will no longer work.");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private synchronized GetGachaInfoRsp createProto() {
GetGachaInfoRsp.Builder proto = GetGachaInfoRsp.newBuilder().setGachaRandom(12345);
......
......@@ -37,10 +37,10 @@ public class Inventory implements Iterable<GenshinItem> {
this.store = new Long2ObjectOpenHashMap<>();
this.inventoryTypes = new Int2ObjectOpenHashMap<>();
this.createInventoryTab(ItemType.ITEM_WEAPON, new EquipInventoryTab(Grasscutter.getConfig().getServerOptions().InventoryLimitWeapon));
this.createInventoryTab(ItemType.ITEM_RELIQUARY, new EquipInventoryTab(Grasscutter.getConfig().getServerOptions().InventoryLimitRelic));
this.createInventoryTab(ItemType.ITEM_MATERIAL, new MaterialInventoryTab(Grasscutter.getConfig().getServerOptions().InventoryLimitMaterial));
this.createInventoryTab(ItemType.ITEM_FURNITURE, new MaterialInventoryTab(Grasscutter.getConfig().getServerOptions().InventoryLimitFurniture));
this.createInventoryTab(ItemType.ITEM_WEAPON, new EquipInventoryTab(Grasscutter.getConfig().getGameServerOptions().InventoryLimitWeapon));
this.createInventoryTab(ItemType.ITEM_RELIQUARY, new EquipInventoryTab(Grasscutter.getConfig().getGameServerOptions().InventoryLimitRelic));
this.createInventoryTab(ItemType.ITEM_MATERIAL, new MaterialInventoryTab(Grasscutter.getConfig().getGameServerOptions().InventoryLimitMaterial));
this.createInventoryTab(ItemType.ITEM_FURNITURE, new MaterialInventoryTab(Grasscutter.getConfig().getGameServerOptions().InventoryLimitFurniture));
}
public GenshinPlayer getPlayer() {
......
package emu.grasscutter.game.managers;
import emu.grasscutter.commands.CommandMap;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.game.GenshinPlayer;
import emu.grasscutter.net.packet.GenshinPacket;
import emu.grasscutter.server.game.GameServer;
......
......@@ -589,7 +589,6 @@ public class InventoryManager {
// Update proud skills
AvatarSkillDepotData skillDepot = GenshinData.getAvatarSkillDepotDataMap().get(avatar.getSkillDepotId());
boolean hasAddedProudSkill = false;
if (skillDepot != null && skillDepot.getInherentProudSkillOpens() != null) {
for (InherentProudSkillOpens openData : skillDepot.getInherentProudSkillOpens()) {
......@@ -599,7 +598,6 @@ public class InventoryManager {
if (openData.getNeedAvatarPromoteLevel() == avatar.getPromoteLevel()) {
int proudSkillId = (openData.getProudSkillGroupId() * 100) + 1;
if (GenshinData.getProudSkillDataMap().containsKey(proudSkillId)) {
hasAddedProudSkill = true;
avatar.getProudSkillList().add(proudSkillId);
player.sendPacket(new PacketProudSkillChangeNotify(avatar));
}
......@@ -607,20 +605,13 @@ public class InventoryManager {
}
}
// Racalc stats and save avatar
avatar.recalcStats();
avatar.save();
// Resend ability embryos if proud skill has been added
if (hasAddedProudSkill && avatar.getAsEntity() != null) {
player.sendPacket(new PacketAbilityChangeNotify(avatar.getAsEntity()));
}
// TODO Send entity prop update packet to world
// Packets
player.sendPacket(new PacketAvatarPropNotify(avatar));
player.sendPacket(new PacketAvatarPromoteRsp(avatar));
// TODO Send entity prop update packet to world
avatar.recalcStats(true);
avatar.save();
}
public void upgradeAvatar(GenshinPlayer player, long guid, int itemId, int count) {
......@@ -827,25 +818,20 @@ public class InventoryManager {
// Apply + recalc
avatar.getTalentIdList().add(talentData.getId());
avatar.setCoreProudSkillLevel(currentTalentLevel + 1);
avatar.recalcStats();
// Packet
player.sendPacket(new PacketAvatarUnlockTalentNotify(avatar, nextTalentId));
player.sendPacket(new PacketUnlockAvatarTalentRsp(avatar, nextTalentId));
// Proud skill bonus map
// Proud skill bonus map (Extra skills)
OpenConfigEntry entry = GenshinData.getOpenConfigEntries().get(talentData.getOpenConfig());
if (entry != null && entry.getExtraTalentIndex() > 0) {
avatar.recalcProudSkillBonusMap();
player.sendPacket(new PacketProudSkillExtraLevelNotify(avatar, entry.getExtraTalentIndex()));
}
// Resend ability embryos
if (avatar.getAsEntity() != null) {
player.sendPacket(new PacketAbilityChangeNotify(avatar.getAsEntity()));
}
// Save avatar
// Recalc + save avatar
avatar.recalcStats(true);
avatar.save();
}
......
package emu.grasscutter.game.props;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public enum FetterState {
NONE(0),
NOT_OPEN(1),
OPEN(1),
FINISH(3);
private final int value;
private static final Int2ObjectMap<FetterState> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, FetterState> stringMap = new HashMap<>();
static {
Stream.of(values()).forEach(e -> {
map.put(e.getValue(), e);
stringMap.put(e.name(), e);
});
}
private FetterState(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static FetterState getTypeByValue(int value) {
return map.getOrDefault(value, NONE);
}
public static FetterState getTypeByName(String name) {
return stringMap.getOrDefault(name, NONE);
}
}
package emu.grasscutter.server.dispatch;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.security.KeyStore;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.protobuf.ByteString;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsServer;
import emu.grasscutter.Config;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
......@@ -32,30 +16,34 @@ import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegio
import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp;
import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson;
import emu.grasscutter.server.dispatch.json.ComboTokenResJson;
import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson;
import emu.grasscutter.server.dispatch.json.LoginResultJson;
import emu.grasscutter.server.dispatch.json.LoginTokenRequestJson;
import emu.grasscutter.server.dispatch.json.*;
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import com.sun.net.httpserver.HttpServer;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.security.KeyStore;
import java.util.*;
public final class DispatchServer {
public static String query_region_list = "";
public static String query_cur_region = "";
private final InetSocketAddress address;
private final Gson gson;
private QueryCurrRegionHttpRsp currRegion;
private final String defaultServerName = "os_usa";
public String regionListBase64;
public String regionCurrentBase64;
public static String query_region_list = "";
public static String query_cur_region = "";
public HashMap<String, RegionData> regions;
public DispatchServer() {
this.address = new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, Grasscutter.getConfig().DispatchServerPort);
this.regions = new HashMap<String, RegionData>();
this.address = new InetSocketAddress(Grasscutter.getConfig().getDispatchOptions().Ip, Grasscutter.getConfig().getDispatchOptions().Port);
this.gson = new GsonBuilder().create();
this.loadQueries();
......@@ -71,7 +59,13 @@ public final class DispatchServer {
}
public QueryCurrRegionHttpRsp getCurrRegion() {
return currRegion;
// Needs to be fixed by having the game servers connect to the dispatch server.
if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("HYBRID")) {
return regions.get(defaultServerName).parsedRegionQuery;
}
Grasscutter.getLogger().warn("[Dispatch] Unsupported run mode for getCurrRegion()");
return null;
}
public void loadQueries() {
......@@ -81,14 +75,14 @@ public final class DispatchServer {
if (file.exists()) {
query_region_list = new String(FileUtils.read(file));
} else {
Grasscutter.getLogger().warn("query_region_list not found! Using default region list.");
Grasscutter.getLogger().warn("[Dispatch] query_region_list not found! Using default region list.");
}
file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_cur_region.txt");
if (file.exists()) {
query_cur_region = new String(FileUtils.read(file));
} else {
Grasscutter.getLogger().warn("query_cur_region not found! Using default current region.");
Grasscutter.getLogger().warn("[Dispatch] query_cur_region not found! Using default current region.");
}
}
......@@ -100,52 +94,79 @@ public final class DispatchServer {
byte[] decoded2 = Base64.getDecoder().decode(query_cur_region);
QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2);
List<RegionSimpleInfo> servers = new ArrayList<RegionSimpleInfo>();
List<String> usedNames = new ArrayList<String>(); // List to check for potential naming conflicts
if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("HYBRID")) { // Automatically add the game server if in hybrid mode
RegionSimpleInfo server = RegionSimpleInfo.newBuilder()
.setName("os_usa")
.setTitle(Grasscutter.getConfig().GameServerName)
.setTitle(Grasscutter.getConfig().getGameServerOptions().Name)
.setType("DEV_PUBLIC")
.setDispatchUrl("https://" + (Grasscutter.getConfig().DispatchServerPublicIp.isEmpty() ? Grasscutter.getConfig().DispatchServerIp : Grasscutter.getConfig().DispatchServerPublicIp) + ":" + getAddress().getPort() + "/query_cur_region")
.setDispatchUrl("https://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + ":" + getAddress().getPort() + "/query_cur_region_" + defaultServerName)
.build();
usedNames.add(defaultServerName);
servers.add(server);
RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder()
.setIp((Grasscutter.getConfig().getGameServerOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getGameServerOptions().Ip : Grasscutter.getConfig().getGameServerOptions().PublicIp))
.setPort(Grasscutter.getConfig().getGameServerOptions().Port)
.setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin")))
.build();
RegionSimpleInfo serverTest2 = RegionSimpleInfo.newBuilder()
.setName("os_euro")
.setTitle("Grasscutter")
QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build();
regions.put(defaultServerName, new RegionData(parsedRegionQuery, Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray())));
} else {
if(Grasscutter.getConfig().getDispatchOptions().getGameServers().length == 0) {
Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state.");
System.exit(1);
}
}
for (Config.DispatchServerOptions.RegionInfo regionInfo : Grasscutter.getConfig().getDispatchOptions().getGameServers()) {
if(usedNames.contains(regionInfo.Name)) {
Grasscutter.getLogger().error("Region name already in use.");
continue;
}
RegionSimpleInfo server = RegionSimpleInfo.newBuilder()
.setName(regionInfo.Name)
.setTitle(regionInfo.Title)
.setType("DEV_PUBLIC")
.setDispatchUrl("https://" + (Grasscutter.getConfig().DispatchServerPublicIp.isEmpty() ? Grasscutter.getConfig().DispatchServerIp : Grasscutter.getConfig().DispatchServerPublicIp) + ":" + getAddress().getPort() + "/query_cur_region")
.setDispatchUrl("https://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + ":" + getAddress().getPort() + "/query_cur_region_" + regionInfo.Name)
.build();
usedNames.add(regionInfo.Name);
servers.add(server);
RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder()
.setIp(regionInfo.Ip)
.setPort(regionInfo.Port)
.setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin")))
.build();
QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build();
regions.put(regionInfo.Name, new RegionData(parsedRegionQuery, Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray())));
}
QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.newBuilder()
.addServers(server)
.addServers(serverTest2)
.addAllServers(servers)
.setClientSecretKey(rl.getClientSecretKey())
.setClientCustomConfigEncrypted(rl.getClientCustomConfigEncrypted())
.setEnableLoginPc(true)
.build();
RegionInfo currentRegion = regionQuery.getRegionInfo().toBuilder()
.setIp((Grasscutter.getConfig().GameServerPublicIp.isEmpty() ? Grasscutter.getConfig().GameServerIp : Grasscutter.getConfig().GameServerPublicIp))
.setPort(Grasscutter.getConfig().GameServerPort)
.setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin")))
.build();
QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(currentRegion).build();
this.regionListBase64 = Base64.getEncoder().encodeToString(regionList.toByteString().toByteArray());
this.regionCurrentBase64 = Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray());
this.currRegion = parsedRegionQuery;
} catch (Exception e) {
Grasscutter.getLogger().error("Error while initializing region info!", e);
Grasscutter.getLogger().error("[Dispatch] Error while initializing region info!", e);
}
}
public void start() throws Exception {
HttpServer server;
if(Grasscutter.getConfig().UseSSL) {
if (Grasscutter.getConfig().getDispatchOptions().UseSSL) {
HttpsServer httpsServer;
httpsServer = HttpsServer.create(getAddress(), 0);
SSLContext sslContext = SSLContext.getInstance("TLS");
try (FileInputStream fis = new FileInputStream(Grasscutter.getConfig().DispatchServerKeystorePath)) {
char[] keystorePassword = Grasscutter.getConfig().DispatchServerKeystorePassword.toCharArray();
try (FileInputStream fis = new FileInputStream(Grasscutter.getConfig().getDispatchOptions().KeystorePath)) {
char[] keystorePassword = Grasscutter.getConfig().getDispatchOptions().KeystorePassword.toCharArray();
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(fis, keystorePassword);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
......@@ -156,56 +177,39 @@ public final class DispatchServer {
httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
server = httpsServer;
} catch (Exception e) {
Grasscutter.getLogger().error("No SSL cert found!");
return;
Grasscutter.getLogger().warn("[Dispatch] No SSL cert found! Falling back to HTTP server.");
Grasscutter.getConfig().getDispatchOptions().UseSSL = false;
server = HttpServer.create(getAddress(), 0);
}
} else {
server = HttpServer.create(getAddress(), 0);
}
server.createContext("/", t -> {
//Create a response form the request query parameters
String response = "Hello";
//Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8"));
t.sendResponseHeaders(200, response.getBytes().length);
//Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
});
server.createContext("/", t -> responseHTML(t, "Hello"));
// Dispatch
server.createContext("/query_region_list", t -> {
// Log
Grasscutter.getLogger().info("Client request: query_region_list");
// Create a response form the request query parameters
String response = regionListBase64;
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", t.getRemoteAddress()));
responseHTML(t, regionListBase64);
});
server.createContext("/query_cur_region", t -> {
for (String regionName : regions.keySet()) {
server.createContext("/query_cur_region_" + regionName, t -> {
String regionCurrentBase64 = regions.get(regionName).Base64;
// Log
Grasscutter.getLogger().info("Client request: query_cur_region");
Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region_%s", t.getRemoteAddress(), regionName));
// Create a response form the request query parameters
URI uri = t.getRequestURI();
String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw==";
if (uri.getQuery() != null && uri.getQuery().length() > 0) {
response = regionCurrentBase64;
}
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
responseHTML(t, response);
});
}
// Login via account
server.createContext("/hk4e_global/mdk/shield/api/login", t -> {
// Get post data
......@@ -213,32 +217,44 @@ public final class DispatchServer {
try {
String body = Utils.toString(t.getRequestBody());
requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class);
} catch (Exception e) {
} catch (Exception ignored) { }
}
// Create response json
if (requestData == null) {
return;
}
LoginResultJson responseData = new LoginResultJson();
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s is trying to log in", t.getRemoteAddress()));
// Login
Account account = DatabaseHelper.getAccountByName(requestData.account);
// Check if account exists, else create a new one.
if (account == null) {
// Account doesnt exist, so we can either auto create it if the config value is set
if (Grasscutter.getConfig().ServerOptions.AutomaticallyCreateAccounts) {
if (Grasscutter.getConfig().getDispatchOptions().AutomaticallyCreateAccounts) {
// This account has been created AUTOMATICALLY. There will be no permissions added.
account = DatabaseHelper.createAccountWithId(requestData.account, 0);
if (account != null) {
responseData.message = "OK";
responseData.data.account.uid = account.getId();
responseData.data.account.token = account.generateSessionKey();
responseData.data.account.email = account.getEmail();
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to log in: Account %s created", t.getRemoteAddress(), responseData.data.account.uid));
} else {
responseData.retcode = -201;
responseData.message = "Username not found, create failed.";
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to log in: Account create failed", t.getRemoteAddress()));
}
} else {
responseData.retcode = -201;
responseData.message = "Username not found.";
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to log in: Account no found", t.getRemoteAddress()));
}
} else {
// Account was found, log the player in
......@@ -246,17 +262,11 @@ public final class DispatchServer {
responseData.data.account.uid = account.getId();
responseData.data.account.token = account.generateSessionKey();
responseData.data.account.email = account.getEmail();
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in as %s", t.getRemoteAddress(), responseData.data.account.uid));
}
// Create a response
String response = getGsonFactory().toJson(responseData);
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
responseJSON(t, responseData);
});
// Login via token
server.createContext("/hk4e_global/mdk/shield/api/verify", t -> {
......@@ -265,14 +275,14 @@ public final class DispatchServer {
try {
String body = Utils.toString(t.getRequestBody());
requestData = getGsonFactory().fromJson(body, LoginTokenRequestJson.class);
} catch (Exception e) {
} catch (Exception ignored) { }
}
// Create response json
if (requestData == null) {
return;
}
LoginResultJson responseData = new LoginResultJson();
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s is trying to log in via token", t.getRemoteAddress()));
// Login
Account account = DatabaseHelper.getAccountById(requestData.uid);
......@@ -281,22 +291,18 @@ public final class DispatchServer {
if (account == null || !account.getSessionKey().equals(requestData.token)) {
responseData.retcode = -111;
responseData.message = "Game account cache information error";
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to log in via token", t.getRemoteAddress()));
} else {
responseData.message = "OK";
responseData.data.account.uid = requestData.uid;
responseData.data.account.token = requestData.token;
responseData.data.account.email = account.getEmail();
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in via token as %s", t.getRemoteAddress(), responseData.data.account.uid));
}
// Create a response
String response = getGsonFactory().toJson(responseData);
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
responseJSON(t, responseData);
});
// Exchange for combo token
server.createContext("/hk4e_global/combo/granter/login/v2/login", t -> {
......@@ -305,9 +311,8 @@ public final class DispatchServer {
try {
String body = Utils.toString(t.getRequestBody());
requestData = getGsonFactory().fromJson(body, ComboTokenReqJson.class);
} catch (Exception e) {
} catch (Exception ignored) { }
}
// Create response json
if (requestData == null || requestData.data == null) {
return;
......@@ -322,22 +327,18 @@ public final class DispatchServer {
if (account == null || !account.getSessionKey().equals(loginData.token)) {
responseData.retcode = -201;
responseData.message = "Wrong session key.";
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s failed to exchange combo token", t.getRemoteAddress()));
} else {
responseData.message = "OK";
responseData.data.open_id = loginData.uid;
responseData.data.combo_id = "157795300";
responseData.data.combo_token = account.generateLoginToken();
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s succeed to exchange combo token", t.getRemoteAddress()));
}
// Create a response
String response = getGsonFactory().toJson(responseData);
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
responseJSON(t, responseData);
});
// Agreement and Protocol
server.createContext( // hk4e-sdk-os.hoyoverse.com
......@@ -405,57 +406,67 @@ public final class DispatchServer {
"/perf/config/verify",
new DispatchHttpJsonHandler("{\"code\":0}")
);
// Start server
server.start();
Grasscutter.getLogger().info("Dispatch server started on port " + getAddress().getPort());
// Logging servers
HttpServer overseaLogServer = HttpServer.create(new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, 8888), 0);
overseaLogServer.createContext( // overseauspider.yuanshen.com
server.createContext( // overseauspider.yuanshen.com
"/log",
new DispatchHttpJsonHandler("{\"code\":0}")
);
overseaLogServer.start();
Grasscutter.getLogger().info("Log server (overseauspider) started on port " + 8888);
HttpServer uploadLogServer = HttpServer.create(new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, Grasscutter.getConfig().UploadLogPort), 0);
uploadLogServer.createContext( // log-upload-os.mihoyo.com
server.createContext( // log-upload-os.mihoyo.com
"/crash/dataUpload",
new DispatchHttpJsonHandler("{\"code\":0}")
);
uploadLogServer.createContext("/gacha", t -> {
//Create a response form the request query parameters
String response = "<!doctype html><html lang=\"en\"><head><title>Gacha</title></head><body></body></html>";
//Set the response header status and length
server.createContext("/gacha", t -> responseHTML(t, "<!doctype html><html lang=\"en\"><head><title>Gacha</title></head><body></body></html>"));
// Start server
server.start();
Grasscutter.getLogger().info("[Dispatch] Dispatch server started on port " + getAddress().getPort());
}
private void responseJSON(HttpExchange t, Object data) throws IOException {
// Create a response
String response = getGsonFactory().toJson(data);
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
private void responseHTML(HttpExchange t, String response) throws IOException {
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8"));
t.sendResponseHeaders(200, response.getBytes().length);
//Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
});
uploadLogServer.start();
Grasscutter.getLogger().info("Log server (log-upload-os) started on port " + Grasscutter.getConfig().UploadLogPort);
}
private Map<String, String> parseQueryString(String qs) {
Map<String, String> result = new HashMap<>();
if (qs == null)
if (qs == null) {
return result;
}
int last = 0, next, l = qs.length();
while (last < l) {
next = qs.indexOf('&', last);
if (next == -1)
if (next == -1) {
next = l;
}
if (next > last) {
int eqPos = qs.indexOf('=', last);
try {
if (eqPos < 0 || eqPos > next)
if (eqPos < 0 || eqPos > next) {
result.put(URLDecoder.decode(qs.substring(last, next), "utf-8"), "");
else
} else {
result.put(URLDecoder.decode(qs.substring(last, eqPos), "utf-8"), URLDecoder.decode(qs.substring(eqPos + 1, next), "utf-8"));
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // will never happen, utf-8 support is mandatory for java
}
......@@ -464,4 +475,15 @@ public final class DispatchServer {
}
return result;
}
public static class RegionData {
QueryCurrRegionHttpRsp parsedRegionQuery;
String Base64;
public RegionData(QueryCurrRegionHttpRsp prq, String b64) {
this.parsedRegionQuery = prq;
this.Base64 = b64;
}
}
}
......@@ -7,7 +7,7 @@ public class ComboTokenReqJson {
public String device;
public String sign;
public class LoginTokenData {
public static class LoginTokenData {
public String uid;
public String token;
public boolean guest;
......
......@@ -5,7 +5,7 @@ public class ComboTokenResJson {
public int retcode;
public LoginData data = new LoginData();
public class LoginData {
public static class LoginData {
public int account_type = 1;
public boolean heartbeat;
public String combo_id;
......
......@@ -5,7 +5,7 @@ public class LoginResultJson {
public int retcode;
public VerifyData data = new VerifyData();
public class VerifyData {
public static class VerifyData {
public VerifyAccountData account = new VerifyAccountData();
public boolean device_grant_required = false;
public String realname_operation = "NONE";
......@@ -13,7 +13,7 @@ public class LoginResultJson {
public boolean safe_mobile_required = false;
}
public class VerifyAccountData {
public static class VerifyAccountData {
public String uid;
public String name = "";
public String email;
......
......@@ -6,7 +6,7 @@ import java.util.concurrent.ConcurrentHashMap;
import emu.grasscutter.GenshinConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.commands.CommandMap;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.GenshinPlayer;
......@@ -19,6 +19,7 @@ import emu.grasscutter.game.shop.ShopManager;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.netty.MihoyoKcpServer;
import org.greenrobot.eventbus.EventBus;
public final class GameServer extends MihoyoKcpServer {
private final InetSocketAddress address;
......@@ -34,9 +35,17 @@ public final class GameServer extends MihoyoKcpServer {
private final DungeonManager dungeonManager;
private final CommandMap commandMap;
public EventBus OnGameServerStartFinish;
public EventBus OnGameServerTick;
public EventBus OnGameServerStop;
public GameServer(InetSocketAddress address) {
super(address);
OnGameServerStartFinish = EventBus.builder().throwSubscriberException(true).logNoSubscriberMessages(false).build();
OnGameServerTick = EventBus.builder().throwSubscriberException(true).logNoSubscriberMessages(false).build();
OnGameServerStop = EventBus.builder().throwSubscriberException(true).logNoSubscriberMessages(false).build();
this.setServerInitializer(new GameServerInitializer(this));
this.address = address;
this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
......@@ -145,7 +154,7 @@ public final class GameServer extends MihoyoKcpServer {
public Account getAccountByName(String username) {
Optional<GenshinPlayer> playerOpt = getPlayers().values().stream().filter(player -> player.getAccount().getUsername().equals(username)).findFirst();
if (playerOpt.get() != null) {
if (playerOpt.isPresent()) {
return playerOpt.get().getAccount();
}
return DatabaseHelper.getAccountByName(username);
......@@ -155,14 +164,20 @@ public final class GameServer extends MihoyoKcpServer {
for (GenshinPlayer player : this.getPlayers().values()) {
player.onTick();
}
OnGameServerTick.post(new GameServerTickEvent());
}
@Override
public void onStartFinish() {
Grasscutter.getLogger().info("Game Server started on port " + address.getPort());
OnGameServerStartFinish.post(new GameServerStartFinishEvent());
}
public void onServerShutdown() {
OnGameServerStop.post(new GameServerStopEvent());
// Kick and save all players
List<GenshinPlayer> list = new ArrayList<>(this.getPlayers().size());
list.addAll(this.getPlayers().values());
......
......@@ -87,7 +87,7 @@ public class GameServerPacketHandler {
}
// Log unhandled packets
if (Grasscutter.getConfig().LOG_PACKETS) {
if (Grasscutter.getConfig().getGameServerOptions().LOG_PACKETS) {
//Grasscutter.getLogger().info("Unhandled packet (" + opcode + "): " + PacketOpcodesUtil.getOpcodeName(opcode));
}
}
......
package emu.grasscutter.server.game;
public class GameServerStartFinishEvent {
// Placeholder class for now, probably will get used later
}
package emu.grasscutter.server.game;
public class GameServerStopEvent {
// Placeholder class for now, probably will get used later
}
package emu.grasscutter.server.game;
public class GameServerTickEvent {
// Placeholder class for now, probably will get used later
}
......@@ -165,7 +165,7 @@ public class GameSession extends MihoyoKcpChannel {
byte[] data = genshinPacket.build();
// Log
if (Grasscutter.getConfig().LOG_PACKETS) {
if (Grasscutter.getConfig().getGameServerOptions().LOG_PACKETS) {
logPacket(genshinPacket);
}
......@@ -225,7 +225,7 @@ public class GameSession extends MihoyoKcpChannel {
}
// Log packet
if (Grasscutter.getConfig().LOG_PACKETS) {
if (Grasscutter.getConfig().getGameServerOptions().LOG_PACKETS) {
Grasscutter.getLogger().info("RECV: " + PacketOpcodesUtil.getOpcodeName(opcode) + " (" + opcode + ")");
System.out.println(Utils.bytesToHex(payload));
}
......
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
import emu.grasscutter.net.proto.ClientAbilityInitFinishNotifyOuterClass.ClientAbilityInitFinishNotify;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.game.GameSession;
@Opcodes(PacketOpcodes.ClientAbilityInitFinishNotify)
public class HandlerClientAbilityInitFinishNotify extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
ClientAbilityInitFinishNotify notif = ClientAbilityInitFinishNotify.parseFrom(payload);
for (AbilityInvokeEntry entry : notif.getInvokesList()) {
session.getPlayer().getClientAbilityInitFinishHandler().addEntry(entry.getForwardType(), entry);
}
if (notif.getInvokesList().size() > 0) {
session.getPlayer().getClientAbilityInitFinishHandler().update(session.getPlayer());
}
}
}
......@@ -31,8 +31,11 @@ public class HandlerEnterSceneDoneReq extends PacketHandler {
// Locations
session.send(new PacketWorldPlayerLocationNotify(session.getPlayer().getWorld()));
session.send(new PacketScenePlayerLocationNotify(session.getPlayer()));
session.send(new PacketScenePlayerLocationNotify(session.getPlayer().getScene()));
session.send(new PacketWorldPlayerRTTNotify(session.getPlayer().getWorld()));
// Reset timer for sending player locations
session.getPlayer().resetSendPlayerLocTime();
}
}
......@@ -35,15 +35,15 @@ public class HandlerGetPlayerTokenReq extends PacketHandler {
// Has character
boolean doesPlayerExist = false;
if (account.getPlayerId() > 0) {
if (account.getPlayerUid() > 0) {
// Set flag for player existing
doesPlayerExist = DatabaseHelper.checkPlayerExists(account.getPlayerId());
doesPlayerExist = DatabaseHelper.checkPlayerExists(account.getPlayerUid());
}
// Set reserve player id if account doesnt exist
if (!doesPlayerExist) {
int id = DatabaseHelper.getNextPlayerId(session.getAccount().getPlayerId());
if (id != session.getAccount().getPlayerId()) {
int id = DatabaseHelper.getNextPlayerId(session.getAccount().getPlayerUid());
if (id != session.getAccount().getPlayerUid()) {
session.getAccount().setPlayerId(id);
session.getAccount().save();
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment