"Autogenerate" data files with data fallbacks and moved keys folder into jar resources (#927)

* Autogenerate keys and data files

* Update gacha html files

Accidentally pushed with old html files

* Keys no longer copied. No more manually retrieving listing files. Recursive directory creation

Removed unused code from old GC as well.

* Moved somethings and better errors

* Fixed resources from loading twice

* Data files fallback
This commit is contained in:
4Benj_
2022-05-17 18:00:52 +08:00
committed by GitHub
parent 003e28e3f8
commit ead0df336e
31 changed files with 266 additions and 127 deletions

View File

@@ -28,7 +28,6 @@ public final class Configuration extends ConfigContainer {
public static final Locale FALLBACK_LANGUAGE = config.language.fallback;
private static final String DATA_FOLDER = config.folderStructure.data;
private static final String RESOURCES_FOLDER = config.folderStructure.resources;
private static final String KEYS_FOLDER = config.folderStructure.keys;
private static final String PLUGINS_FOLDER = config.folderStructure.plugins;
private static final String SCRIPTS_FOLDER = config.folderStructure.scripts;
private static final String PACKETS_FOLDER = config.folderStructure.packets;
@@ -62,10 +61,6 @@ public final class Configuration extends ConfigContainer {
public static String RESOURCE(String path) {
return Paths.get(RESOURCES_FOLDER, path).toString();
}
public static String KEY(String path) {
return Paths.get(KEYS_FOLDER, path).toString();
}
public static String PLUGIN() {
return PLUGINS_FOLDER;

View File

@@ -0,0 +1,101 @@
package emu.grasscutter.data;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.handlers.GachaHandler;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import java.io.*;
import java.nio.file.Path;
import java.util.List;
import static emu.grasscutter.Configuration.DATA;
public class DataLoader {
/**
* Load a data file by its name. If the file isn't found within the /data directory then it will fallback to the default within the jar resources
* @see #load(String, boolean)
* @param resourcePath The path to the data file to be loaded.
* @return InputStream of the data file.
* @throws FileNotFoundException
*/
public static InputStream load(String resourcePath) throws FileNotFoundException {
return load(resourcePath, true);
}
/**
* Load a data file by its name.
* @param resourcePath The path to the data file to be loaded.
* @param useFallback If the file does not exist in the /data directory, should it use the default file in the jar?
* @return InputStream of the data file.
* @throws FileNotFoundException
*/
public static InputStream load(String resourcePath, boolean useFallback) throws FileNotFoundException {
if(Utils.fileExists(DATA(resourcePath))) {
// Data is in the resource directory
return new FileInputStream(DATA(resourcePath));
} else {
if(useFallback) {
return FileUtils.readResourceAsStream("/defaults/data/" + resourcePath);
}
}
return null;
}
public static void CheckAllFiles() {
try {
List<Path> filenames = FileUtils.getPathsFromResource("/defaults/data/");
for (Path file : filenames) {
String relativePath = String.valueOf(file).split("/defaults/data/")[1];
CheckAndCopyData(relativePath);
}
} catch (Exception e) {
Grasscutter.getLogger().error("An error occurred while trying to check the data folder. \n" + e);
}
GenerateGachaMappings();
}
private static void CheckAndCopyData(String name) {
String filePath = Utils.toFilePath(DATA(name));
if (!Utils.fileExists(filePath)) {
// Check if file is in subdirectory
if (name.indexOf("/") != -1) {
String[] path = name.split("/");
String folder = "";
for(int i = 0; i < (path.length - 1); i++) {
folder += path[i] + "/";
// Make sure the current folder exists
String folderToCreate = Utils.toFilePath(DATA(folder));
if(!Utils.fileExists(folderToCreate)) {
Grasscutter.getLogger().info("Creating data folder '" + folder + "'");
Utils.createFolder(folderToCreate);
}
}
}
Grasscutter.getLogger().info("Creating default '" + name + "' data");
FileUtils.copyResource("/defaults/data/" + name, filePath);
}
}
private static void GenerateGachaMappings() {
if (!Utils.fileExists(GachaHandler.gachaMappings)) {
try {
Grasscutter.getLogger().info("Creating default '" + GachaHandler.gachaMappings + "' data");
Tools.createGachaMapping(GachaHandler.gachaMappings);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to create gacha mappings. \n" + exception);
}
}
}
}

View File

@@ -1,7 +1,6 @@
package emu.grasscutter.data;
import java.io.File;
import java.io.FileReader;
import java.io.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
@@ -33,6 +32,8 @@ import static emu.grasscutter.Configuration.*;
public class ResourceLoader {
private static List<String> loadedResources = new ArrayList<String>();
public static List<Class<?>> getResourceDefClasses() {
Reflections reflections = new Reflections(ResourceLoader.class.getPackage().getName());
Set<?> classes = reflections.getSubTypesOf(GameResource.class);
@@ -98,6 +99,10 @@ public class ResourceLoader {
}
public static void loadResources() {
loadResources(false);
}
public static void loadResources(boolean doReload) {
for (Class<?> resourceDefinition : getResourceDefClasses()) {
ResourceType type = resourceDefinition.getAnnotation(ResourceType.class);
@@ -113,7 +118,7 @@ public class ResourceLoader {
}
try {
loadFromResource(resourceDefinition, type, map);
loadFromResource(resourceDefinition, type, map, doReload);
} catch (Exception e) {
Grasscutter.getLogger().error("Error loading resource file: " + Arrays.toString(type.name()), e);
}
@@ -121,13 +126,16 @@ public class ResourceLoader {
}
@SuppressWarnings("rawtypes")
protected static void loadFromResource(Class<?> c, ResourceType type, Int2ObjectMap map) throws Exception {
for (String name : type.name()) {
loadFromResource(c, name, map);
protected static void loadFromResource(Class<?> c, ResourceType type, Int2ObjectMap map, boolean doReload) throws Exception {
if(!loadedResources.contains(c.getSimpleName()) || doReload) {
for (String name : type.name()) {
loadFromResource(c, name, map);
}
Grasscutter.getLogger().info("Loaded " + map.size() + " " + c.getSimpleName() + "s.");
loadedResources.add(c.getSimpleName());
}
Grasscutter.getLogger().info("Loaded " + map.size() + " " + c.getSimpleName() + "s.");
}
@SuppressWarnings({"rawtypes", "unchecked"})
protected static void loadFromResource(Class<?> c, String fileName, Int2ObjectMap map) throws Exception {
FileReader fileReader = new FileReader(RESOURCE("ExcelBinOutput/" + fileName));
@@ -138,6 +146,9 @@ public class ResourceLoader {
Map<String, Object> tempMap = Utils.switchPropertiesUpperLowerCase((Map<String, Object>) o, c);
GameResource res = gson.fromJson(gson.toJson(tempMap), TypeToken.get(c).getType());
res.onLoad();
if(map.containsKey(res.getId())) {
map.remove(res.getId());
}
map.put(res.getId(), res);
}
}
@@ -191,18 +202,14 @@ public class ResourceLoader {
}
private static void loadAbilityEmbryos() {
// Read from cached file if exists
File embryoCache = new File(DATA("AbilityEmbryos.json"));
List<AbilityEmbryoEntry> embryoList = null;
if (embryoCache.exists()) {
// Load from cache
try (FileReader fileReader = new FileReader(embryoCache)) {
embryoList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, AbilityEmbryoEntry.class).getType());
} catch (Exception e) {
e.printStackTrace();
}
} else {
// Read from cached file if exists
try(InputStream embryoCache = DataLoader.load("AbilityEmbryos.json", false)) {
embryoList = Grasscutter.getGsonFactory().fromJson(new InputStreamReader(embryoCache), TypeToken.getParameterized(Collection.class, AbilityEmbryoEntry.class).getType());
} catch(Exception ignored) {}
if(embryoList == null) {
// Load from BinOutput
Pattern pattern = Pattern.compile("(?<=ConfigAvatar_)(.*?)(?=.json)");
@@ -316,18 +323,12 @@ public class ResourceLoader {
}
private static void loadSpawnData() {
// Read from cached file if exists
File spawnDataEntries = new File(DATA("Spawns.json"));
List<SpawnGroupEntry> spawnEntryList = null;
if (spawnDataEntries.exists()) {
// Load from cache
try (FileReader fileReader = new FileReader(spawnDataEntries)) {
spawnEntryList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, SpawnGroupEntry.class).getType());
} catch (Exception e) {
e.printStackTrace();
}
}
// Read from cached file if exists
try(InputStream spawnDataEntries = DataLoader.load("Spawns.json")) {
spawnEntryList = Grasscutter.getGsonFactory().fromJson(new InputStreamReader(spawnDataEntries), TypeToken.getParameterized(Collection.class, SpawnGroupEntry.class).getType());
} catch (Exception ignored) {}
if (spawnEntryList == null || spawnEntryList.isEmpty()) {
Grasscutter.getLogger().error("No spawn data loaded!");
@@ -342,16 +343,13 @@ public class ResourceLoader {
private static void loadOpenConfig() {
// Read from cached file if exists
File openConfigCache = new File(DATA("OpenConfig.json"));
List<OpenConfigEntry> list = null;
if (openConfigCache.exists()) {
try (FileReader fileReader = new FileReader(openConfigCache)) {
list = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, OpenConfigEntry.class).getType());
} catch (Exception e) {
e.printStackTrace();
}
} else {
try(InputStream openConfigCache = DataLoader.load("OpenConfig.json", false)) {
list = Grasscutter.getGsonFactory().fromJson(new InputStreamReader(openConfigCache), TypeToken.getParameterized(Collection.class, SpawnGroupEntry.class).getType());
} catch (Exception ignored) {}
if (list == null) {
Map<String, OpenConfigEntry> map = new TreeMap<>();
java.lang.reflect.Type type = new TypeToken<Map<String, OpenConfigData[]>>() {}.getType();
String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"};

View File

@@ -2,6 +2,7 @@ package emu.grasscutter.game.drop;
import com.google.gson.reflect.TypeToken;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.def.ItemData;
import emu.grasscutter.game.entity.EntityItem;
@@ -17,12 +18,11 @@ import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Collection;
import java.util.List;
import static emu.grasscutter.Configuration.*;
public class DropManager {
public GameServer getGameServer() {
return gameServer;
@@ -43,7 +43,7 @@ public class DropManager {
}
public synchronized void load() {
try (FileReader fileReader = new FileReader(DATA("Drop.json"))) {
try (Reader fileReader = new InputStreamReader(DataLoader.load("Drop.json"))) {
getDropData().clear();
List<DropInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, DropInfo.class).getType());
if(banners.size() > 0) {

View File

@@ -2,11 +2,14 @@ package emu.grasscutter.game.expedition;
import com.google.gson.reflect.TypeToken;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.server.game.GameServer;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Collection;
import java.util.List;
@@ -30,7 +33,7 @@ public class ExpeditionManager {
}
public synchronized void load() {
try (FileReader fileReader = new FileReader(DATA("ExpeditionReward.json"))) {
try (Reader fileReader = new InputStreamReader(DataLoader.load("ExpeditionReward.json"))) {
getExpeditionRewardDataList().clear();
List<ExpeditionRewardInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ExpeditionRewardInfo.class).getType());
if(banners.size() > 0) {

View File

@@ -2,6 +2,8 @@ package emu.grasscutter.game.gacha;
import java.io.File;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.Arrays;
@@ -13,6 +15,7 @@ import com.google.gson.reflect.TypeToken;
import com.sun.nio.file.SensitivityWatchEventModifier;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.def.ItemData;
@@ -74,7 +77,7 @@ public class GachaManager {
}
public synchronized void load() {
try (FileReader fileReader = new FileReader(DATA("Banners.json"))) {
try (Reader fileReader = new InputStreamReader(DataLoader.load("Banners.json"))) {
getGachaBanners().clear();
List<GachaBanner> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, GachaBanner.class).getType());
if(banners.size() > 0) {

View File

@@ -2,6 +2,7 @@ package emu.grasscutter.game.shop;
import com.google.gson.reflect.TypeToken;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.def.ShopGoodsData;
@@ -11,6 +12,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
@@ -58,7 +61,7 @@ public class ShopManager {
}
private void loadShop() {
try (FileReader fileReader = new FileReader(DATA("Shop.json"))) {
try (Reader fileReader = new InputStreamReader(DataLoader.load("Shop.json"))) {
getShopData().clear();
List<ShopTable> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopTable.class).getType());
if(banners.size() > 0) {
@@ -102,7 +105,7 @@ public class ShopManager {
}
private void loadShopChest() {
try (FileReader fileReader = new FileReader(DATA("ShopChest.json"))) {
try (Reader fileReader = new InputStreamReader(DataLoader.load("ShopChest.json"))) {
getShopChestData().clear();
List<ShopChestTable> shopChestTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestTable.class).getType());
if (shopChestTableList.size() > 0) {
@@ -117,7 +120,7 @@ public class ShopManager {
}
private void loadShopChestBatchUse() {
try (FileReader fileReader = new FileReader(DATA("ShopChestBatchUse.json"))) {
try (Reader fileReader = new InputStreamReader(DataLoader.load("ShopChestBatchUse.json"))) {
getShopChestBatchUseData().clear();
List<ShopChestBatchUseTable> shopChestBatchUseTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestBatchUseTable.class).getType());
if (shopChestBatchUseTableList.size() > 0) {

View File

@@ -1,11 +1,14 @@
package emu.grasscutter.game.tower;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.def.TowerScheduleData;
import emu.grasscutter.server.game.GameServer;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.List;
import static emu.grasscutter.Configuration.*;
@@ -25,7 +28,7 @@ public class TowerScheduleManager {
private TowerScheduleConfig towerScheduleConfig;
public synchronized void load(){
try (FileReader fileReader = new FileReader(DATA("TowerSchedule.json"))) {
try (Reader fileReader = new InputStreamReader(DataLoader.load("TowerSchedule.json"))) {
towerScheduleConfig = Grasscutter.getGsonFactory().fromJson(fileReader, TowerScheduleConfig.class);
} catch (Exception e) {
Grasscutter.getLogger().error("Unable to load tower schedule config.", e);

View File

@@ -1,6 +1,7 @@
package emu.grasscutter.server.http.handlers;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.FileUtils;
@@ -14,6 +15,7 @@ import io.javalin.Javalin;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
@@ -41,9 +43,21 @@ public final class AnnouncementsHandler implements Router {
private static void getAnnouncement(Request request, Response response) {
String data = "";
if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) {
data = readToString(new File(Utils.toFilePath(DATA("GameAnnouncement.json"))));
try {
data = FileUtils.readToString(DataLoader.load("GameAnnouncement.json"));
} catch (Exception e) {
if(e.getClass() == IOException.class) {
Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e);
}
}
} else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) {
data = readToString(new File(Utils.toFilePath(DATA("GameAnnouncementList.json"))));
try {
data = FileUtils.readToString(DataLoader.load("GameAnnouncementList.json"));
} catch (Exception e) {
if(e.getClass() == IOException.class) {
Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e);
}
}
} else {
response.send("{\"retcode\":404,\"message\":\"Unknown request path\"}");
}
@@ -64,29 +78,15 @@ public final class AnnouncementsHandler implements Router {
}
private static void getPageResources(Request request, Response response) {
String filename = Utils.toFilePath(DATA(request.path()));
File file = new File(filename);
if (file.exists() && file.isFile()) {
MediaType fromExtension = MediaType.getByExtension(filename.substring(filename.lastIndexOf(".") + 1));
try(InputStream filestream = DataLoader.load(request.path())) {
String possibleFilename = Utils.toFilePath(DATA(request.path()));
MediaType fromExtension = MediaType.getByExtension(possibleFilename.substring(possibleFilename.lastIndexOf(".") + 1));
response.type((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream");
response.send(FileUtils.read(file));
} else {
Grasscutter.getLogger().warn("File does not exist: " + file);
response.send(filestream.readAllBytes());
} catch (Exception e) {
Grasscutter.getLogger().warn("File does not exist: " + request.path());
response.status(404);
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private static String readToString(File file) {
byte[] content = new byte[(int) file.length()];
try {
FileInputStream in = new FileInputStream(file);
in.read(content); in.close();
} catch (IOException ignored) {
Grasscutter.getLogger().warn("File does not exist: " + file);
}
return new String(content, StandardCharsets.UTF_8);
}
}

View File

@@ -29,18 +29,7 @@ import static emu.grasscutter.utils.Language.translate;
* Handles all gacha-related HTTP requests.
*/
public final class GachaHandler implements Router {
private final String gachaMappings;
public GachaHandler() {
this.gachaMappings = Utils.toFilePath(DATA("gacha/mappings.js"));
if(!(new File(this.gachaMappings).exists())) {
try {
Tools.createGachaMapping(this.gachaMappings);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to create gacha mappings.", exception);
}
}
}
public static final String gachaMappings = DATA(Utils.toFilePath("gacha/mappings.js"));
@Override public void applyRoutes(Express express, Javalin handle) {
express.get("/gacha", GachaHandler::gachaRecords);

View File

@@ -84,7 +84,6 @@ public class ConfigContainer {
public String resources = "./resources/";
public String data = "./data/";
public String packets = "./packets/";
public String keys = "./keys/";
public String scripts = "./resources/scripts/";
public String plugins = "./plugins/";

View File

@@ -20,11 +20,11 @@ public final class Crypto {
public static byte[] ENCRYPT_SEED_BUFFER = new byte[0];
public static void loadKeys() {
DISPATCH_KEY = FileUtils.read(KEY("dispatchKey.bin"));
DISPATCH_SEED = FileUtils.read(KEY("dispatchSeed.bin"));
DISPATCH_KEY = FileUtils.readResource("/keys/dispatchKey.bin");
DISPATCH_SEED = FileUtils.readResource("/keys/dispatchSeed.bin");
ENCRYPT_KEY = FileUtils.read(KEY("secretKey.bin"));
ENCRYPT_SEED_BUFFER = FileUtils.read(KEY("secretKeyBuffer.bin"));
ENCRYPT_KEY = FileUtils.readResource("/keys/secretKey.bin");
ENCRYPT_SEED_BUFFER = FileUtils.readResource("/keys/secretKeyBuffer.bin");
}
public static void xor(byte[] packet, byte[] key) {
@@ -37,25 +37,6 @@ public final class Crypto {
}
}
public static void extractSecretKeyBuffer(byte[] data) {
try {
GetPlayerTokenRsp p = GetPlayerTokenRsp.parseFrom(data);
FileUtils.write(KEY("/secretKeyBuffer.bin"), p.getSecretKeyBytes().toByteArray());
Grasscutter.getLogger().info("Secret Key: " + p.getSecretKey());
} catch (Exception e) {
Grasscutter.getLogger().error("Crypto error.", e);
}
}
public static void extractDispatchSeed(String data) {
try {
QueryCurrRegionHttpRsp p = QueryCurrRegionHttpRsp.parseFrom(Base64.getDecoder().decode(data));
FileUtils.write(KEY("/dispatchSeed.bin"), p.getRegionInfo().getSecretKey().toByteArray());
} catch (Exception e) {
Grasscutter.getLogger().error("Crypto error.", e);
}
}
public static byte[] createSessionKey(int length) {
byte[] bytes = new byte[length];
secureRandom.nextBytes(bytes);

View File

@@ -4,9 +4,14 @@ import emu.grasscutter.Grasscutter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public final class FileUtils {
public static void write(String dest, byte[] bytes) {
@@ -32,10 +37,34 @@ public final class FileUtils {
return new byte[0];
}
public static InputStream readResourceAsStream(String resourcePath) {
return Grasscutter.class.getResourceAsStream(resourcePath);
}
public static byte[] readResource(String resourcePath) {
try (InputStream is = Grasscutter.class.getResourceAsStream(resourcePath)) {
return is.readAllBytes();
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to read resource: " + resourcePath);
exception.printStackTrace();
}
return new byte[0];
}
public static byte[] read(File file) {
return read(file.getPath());
}
public static void copyResource(String resourcePath, String destination) {
try {
byte[] resource = FileUtils.readResource(resourcePath);
FileUtils.write(destination, resource);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to copy resource: " + resourcePath + "\n" + exception);
}
}
public static String getFilenameWithoutPath(String fileName) {
if (fileName.indexOf(".") > 0) {
@@ -44,4 +73,33 @@ public final class FileUtils {
return fileName;
}
}
// From https://mkyong.com/java/java-read-a-file-from-resources-folder/
public static List<Path> getPathsFromResource(String folder) throws URISyntaxException, IOException {
List<Path> result;
// get path of the current running JAR
String jarPath = Grasscutter.class.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI()
.getPath();
// file walks JAR
URI uri = URI.create("jar:file:" + jarPath);
try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) {
result = Files.walk(fs.getPath(folder))
.filter(Files::isRegularFile)
.collect(Collectors.toList());
}
return result;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static String readToString(InputStream file) throws IOException {
byte[] content = file.readAllBytes();
return new String(content, StandardCharsets.UTF_8);
}
}

View File

@@ -9,6 +9,7 @@ import java.time.temporal.TemporalAdjusters;
import java.util.*;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
@@ -198,6 +199,9 @@ public final class Utils {
if(!fileExists(dataFolder))
createFolder(dataFolder);
// Make sure the data folder is populated, if there are any missing files copy them from resources
DataLoader.CheckAllFiles();
if(exit) System.exit(1);
}

View File

@@ -0,0 +1,55 @@
[
{
"gachaType": 200,
"scheduleId": 893,
"bannerType": "STANDARD",
"prefabPath": "GachaShowPanel_A022",
"previewPrefabPath": "UI_Tab_GachaShowPanel_A022",
"titlePath": "UI_GACHA_SHOW_PANEL_A022_TITLE",
"costItemId": 224,
"costItemAmount": 1,
"costItemAmount10": 10,
"beginTime": 0,
"endTime": 1924992000,
"sortId": 1000,
"fallbackItems4Pool1": [1006, 1014, 1015, 1020, 1021, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064],
"weights4": [[1,510], [8,510], [10,10000]],
"weights5": [[1,75], [73,150], [90,10000]]
},
{
"gachaType": 301,
"scheduleId": 903,
"bannerType": "EVENT",
"prefabPath": "GachaShowPanel_A079",
"previewPrefabPath": "UI_Tab_GachaShowPanel_A079",
"titlePath": "UI_GACHA_SHOW_PANEL_A048_TITLE",
"costItemId": 223,
"beginTime": 0,
"endTime": 1924992000,
"sortId": 9998,
"rateUpItems4": [1053, 1020, 1045],
"rateUpItems5": [1002],
"fallbackItems5Pool2": [],
"weights5": [[1,80], [73,80], [90,10000]]
},
{
"gachaType": 302,
"scheduleId": 913,
"bannerType": "WEAPON",
"prefabPath": "GachaShowPanel_A080",
"previewPrefabPath": "UI_Tab_GachaShowPanel_A080",
"titlePath": "UI_GACHA_SHOW_PANEL_A021_TITLE",
"costItemId": 223,
"beginTime": 0,
"endTime": 1924992000,
"sortId": 9997,
"eventChance": 75,
"softPity": 80,
"hardPity": 80,
"rateUpItems4": [11401, 12402, 13407, 14401, 15401],
"rateUpItems5": [11509, 12504],
"fallbackItems5Pool1": [],
"weights4": [[1,600], [7,600], [8, 6600], [10,12600]],
"weights5": [[1,100], [62,100], [73, 7800], [80,10000]]
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"t": "{{SYSTEM_TIME}}",
"list": [
{
"ann_id": 1,
"title": "<strong>Welcome to Grasscutter!</strong>",
"subtitle": "Welcome!",
"banner": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/banner/1.jpg",
"content": "<p>Hi there!</p><p>First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you!</p><br><p><strong>〓Discord〓</strong></p><a href=\"https://discord.gg/T5vZU6UyeG\">https://discord.gg/T5vZU6UyeG</a><br><br><p><strong>〓GitHub〓</strong><a href=\"https://github.com/Grasscutters/Grasscutter\">https://github.com/Grasscutters/Grasscutter</a>",
"lang": "en-US"
},
{
"ann_id": 2,
"title": "<strong>How to use announcements</strong>",
"subtitle": "How to use announcements",
"banner": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/banner/2.jpg",
"content": "<p>Announcement content uses HTML. The specific content of the announcement is stored in the program directory <code>GameAnnouncement.json</code>, while <code>GameAnnouncementList.json</code> stores the announcement list data.</p><h2><code>GameAnnouncement</code></h2><table><tr><th>Parameter</th><th>Description</th></tr><tr><td>ann_id</td><td>Unique ID</td></tr><tr><td>title</td><td>Title shown at the top of the content</td></tr><tr><td>subtitle</td><td>Short title shown on the left</td></tr><tr><td>banner</td><td>Image to display between content and title</td></tr><tr><td>content</td><td>Content body in HTML</td></tr><tr><td>lang</td><td>Language code for this entry</td></tr></table><h2><code>GameAnnouncementList</code></h2><p>If you want to add an announcement, please add the list data in the announcement type corresponding to <code>GameAnnouncementList</code>, and finally add the announcement content in <code>GameAnnouncement</code>.</p>",
"lang": "en-US"
}
],
"total": 2
}

View File

@@ -0,0 +1,62 @@
{
"t": "{{SYSTEM_TIME}}",
"list": [
{
"list": [
{
"ann_id": 1,
"title": "<strong>Welcome to Grasscutter!</strong>",
"subtitle": "Welcome!",
"banner": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/banner/1.jpg",
"tag_icon": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/tag_icon.png",
"type": 2,
"type_label": "System",
"lang": "en-US",
"start_time": "2020-09-25 04:05:30",
"end_time": "2030-10-30 11:00:00",
"content": "",
"has_content": true
},
{
"ann_id": 2,
"title": "<strong>How to use announcements</strong>",
"subtitle": "How to use announcements",
"banner": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/banner/2.jpg",
"tag_icon": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/tag_icon.png",
"type": 2,
"type_label": "System",
"lang": "en-US",
"start_time": "2020-09-25 04:05:30",
"end_time": "2030-10-30 11:00:00",
"content": "",
"has_content": true
}
],
"type_id": 2,
"type_label": "System"
},
{
"list": [
{}
],
"type_id": 3,
"type_label": "Events"
}
],
"total": 2,
"type_list": [
{
"id": 2,
"name": "游戏系统公告",
"mi18n_name": "System"
},
{
"id": 1,
"name": "活动公告",
"mi18n_name": "Activity"
}
],
"timezone": -5,
"alert": false,
"alert_id": 0
}

View File

@@ -0,0 +1,54 @@
[
{
"shopId": 1004,
"items": [
{
"goodsId": 1004202,
"goodsItem": {
"Id": 202,
"Count": 1000000
},
"scoin": 1,
"buyLimit": 500,
"beginTime": 1575129600,
"endTime": 2051193600,
"minLevel": 1,
"maxLevel": 99,
"costItemList": [
{
"Id": 223,
"Count": 100
}
]
},
{
"goodsId": 10048006,
"goodsItem": {
"Id": 108006,
"Count": 20
},
"scoin": 100,
"hcoin": 100,
"mcoin": 100,
"buyLimit": 50000,
"beginTime": 1575129600,
"endTime": 2051193600,
"minLevel": 1,
"maxLevel": 99
},
{
"goodsId": 10048033,
"goodsItem": {
"Id": 108033,
"Count": 20
},
"scoin": 1,
"buyLimit": 50000,
"beginTime": 1575129600,
"endTime": 2051193600,
"minLevel": 1,
"maxLevel": 99
}
]
}
]

View File

@@ -0,0 +1,153 @@
[
{
"itemId": 115019,
"containsItem": [
{
"Id": 104002,
"Count": 40
},
{
"Id": 202,
"Count": 30000
}
]
},
{
"itemId": 115020,
"containsItem": [
{
"Id": 104013,
"Count": 25
},
{
"Id": 202,
"Count": 30000
}
]
},
{
"itemId": 115021,
"containsItem": [
{
"Id": 115013,
"Count": 5
},
{
"Id": 104003,
"Count": 40
},
{
"Id": 202,
"Count": 120000
}
]
},
{
"itemId": 115022,
"containsItem": [
{
"Id": 115017,
"Count": 25
},
{
"Id": 202,
"Count": 150000
}
]
},
{
"itemId": 115023,
"containsItem": [
{
"Id": 115025,
"Count": 10
},
{
"Id": 202,
"Count": 60000
}
]
},
{
"itemId": 115029,
"containsItem": [
{
"Id": 104013,
"Count": 100
},
{
"Id": 202,
"Count": 100000
}
]
},
{
"itemId": 115030,
"containsItem": [
{
"Id": 104003,
"Count": 12
},
{
"Id": 202,
"Count": 10000
}
]
},
{
"itemId": 115034,
"containsItem": [
{
"Id": 115013,
"Count": 6
},
{
"Id": 202,
"Count": 60000
}
]
},
{
"itemId": 115032,
"containsItem": [
{
"Id": 115024,
"Count": 12
}
]
},
{
"itemId": 115010,
"containsItem": [
{
"Id": 104002,
"Count": 80
},
{
"Id": 104012,
"Count": 40
}
]
},
{
"itemId": 115011,
"containsItem": [
{
"Id": 104003,
"Count": 50
},
{
"Id": 104013,
"Count": 25
},
{
"Id": 107009,
"Count": 1
},
{
"Id": 202,
"Count": 50000
}
]
}
]

View File

@@ -0,0 +1,55 @@
[
{
"itemId": 115017,
"optionItem": [
104302,
104305,
104308,
104311,
104314,
104317,
104321,
104324,
104327
]
},
{
"itemId": 115024,
"optionItem": [
114001,
114005,
114009,
114013,
114017,
114021,
114025,
114029,
114033
]
},
{
"itemId": 115013,
"optionItem": [
104112,
104122,
104142,
104152,
104162,
104172
]
},
{
"itemId": 115025,
"optionItem": [
114002,
114006,
114010,
114014,
114018,
114022,
114026,
114030,
114034
]
}
]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
{
"scheduleId" : 45,
"scheduleStartTime" : "2022-05-01T00:00:00+08:00",
"nextScheduleChangeTime" : "2022-05-30T00:00:00+08:00"
}

View File

@@ -0,0 +1,121 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
<style>
body {
background-color: #f0f0f0;
}
p {
font-weight:300;
}
a,a:hover {
text-decoration:none !important;
color:#626976;
}
.content {
padding:3rem 0;
}
.container {
color:#626976;
position: relative;
}
h2 {
font-size:20px;
}
h3 {
font-size:16px;
}
</style>
<title>Banner Details</title>
<script type="text/javascript" src="/gacha/mappings"></script>
</head>
<body>
<div class="content">
<div class="container">
<h2 class="mb-5">{{TITLE}}</h2>
<h3 class="">{{AVAILABLE_FIVE_STARS}}</h3>
<hr />
<ul id="5-star-list">
</ul>
<h3 class="">{{AVAILABLE_FOUR_STARS}}</h3>
<hr />
<ul id="4-star-list">
</ul>
<h3 class="">{{AVAILABLE_THREE_STARS}}</h3>
<hr />
<ul id="3-star-list">
</ul>
</div>
</div>
<footer>
<div class="copyright">
<div class="container">
<div class="row">
<div class="col-md-6">
<span>
Template by BecodReyes. All rights reserved.
</span>
</div>
<div class="col-md-6">
<ul style="float:right">
<li class="list-inline-item">
<a href="https://github.com/Grasscutters/Grasscutter">Github</a>
</li>
<li class="list-inline-item">·</li>
<li class="list-inline-item">
<a href="https://github.com/Grasscutters/Grasscutter/blob/stable/LICENSE">License</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</footer>
<script>
var fiveStarItems = {{FIVE_STARS}};
var fourStarItems = {{FOUR_STARS}};
var threeStarItems = {{THREE_STARS}};
var lang = "{{LANGUAGE}}".toLowerCase();
function getNameForId(itemId) {
if (mappings[lang] != null && mappings[lang][itemId] != null) {
return mappings[lang][itemId][0];
}
else if (mappings["en-us"] != null && mappings["en-us"][itemId] != null) {
return mappings["en-us"][itemId][0];
}
return itemId.toString();
}
fiveStarList = document.getElementById("5-star-list");
fourStarList = document.getElementById("4-star-list");
threeStarList = document.getElementById("3-star-list");
fiveStarItems.forEach(element => {
var entry = document.createElement("li");
entry.innerHTML = getNameForId(element);
fiveStarList.appendChild(entry);
});
fourStarItems.forEach(element => {
var entry = document.createElement("li");
entry.innerHTML = getNameForId(element);
fourStarList.appendChild(entry);
});
threeStarItems.forEach(element => {
var entry = document.createElement("li");
entry.innerHTML = getNameForId(element);
threeStarList.appendChild(entry);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,175 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
<style>
body {
background-color: #f0f0f0;
}
p {
font-weight:300;
}
a,a:hover {
text-decoration:none !important;
color:#626976;
}
.content {
padding:3rem 0;
}
.container {
color:#626976;
position: relative;
}
h2 {
font-size:20px;
}
.custom-table {
min-width:900px;
}
.custom-table thead tr,.custom-table thead th {
padding-bottom:30px;
color:#000;
}
.custom-table tbody th,.custom-table tbody td {
color:#777;
font-weight:400;
padding-bottom:20px;
padding-top:20px;
font-weight:300;
border:none;
}
.yellow {
color: rgb(255, 162, 0);
}
.blue {
color: rgb(75, 107, 251);
}
.purple {
color: rgb(242, 40, 242);
}
</style>
<title>Gacha Records</title>
<!-- This file could be generated automatically using `java -jar grasscutter.jar -gachamap` -->
<!-- You can also modify the file manually to customize it -->
<!-- Otherwise you may onle see number IDs in the gacha record -->
<script type="text/javascript" src="/gacha/mappings"></script>
<script>
records = {{REPLACE_RECORDS}};
maxPage = {{REPLACE_MAXPAGE}};
mappings['default'] = mappings['en-us']; // make en-us as default/fallback option
</script>
</head>
<body>
<div class="content">
<div class="container">
<h2 class="mb-5">Gacha Records</h2>
<table id="container" class="table table-striped custom-table">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Item</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="navbar">
<a href="" id="prev">&lt;&lt;&lt;</a>
<span id="curpage">1</span>
<a href="" id="next">&gt;&gt;&gt;</a>
</div>
</div>
</div>
<footer>
<div class="copyright">
<div class="container">
<div class="row">
<div class="col-md-6">
<span>
Template by BecodReyes. All rights reserved.
</span>
</div>
<div class="col-md-6">
<ul style="float:right">
<li class="list-inline-item">
<a href="https://github.com/Grasscutters/Grasscutter">Github</a>
</li>
<li class="list-inline-item">·</li>
<li class="list-inline-item">
<a href="https://github.com/Grasscutters/Grasscutter/blob/stable/LICENSE">License</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</footer>
<script>
var lang = "{{LANGUAGE}}".toLowerCase();
function itemMapper(itemID) {
if (mappings[lang] != null && mappings[lang][itemID] != null) {
var entry = mappings[lang][itemID];
if (entry){
return "<span class='" + entry[1] + "'>" + entry[0] + "</span>";
}
} else {
if (mappings['default'][itemID] != null) {
var entry = mappings['default'][itemID];
if (entry){
return "<span class='" + entry[1] + "'>" + entry[0] + "</span>";
}
}
}
return "<span class='blue'>" + itemID + "</span>";
}
(function (){
var container = document.getElementById("container");
records.forEach(element => {
var e = document.createElement("tr");
e.innerHTML= "<td>" + (new Date(element.time).toLocaleString(lang)) + "</td><td>" + itemMapper(element.item) + "</td>";
container.appendChild(e);
});
// setup pagenation buttons
var page = parseInt(new window.URLSearchParams(window.location.search).get("p"));
if (!page) {
page = 0;
}
document.getElementById("curpage").innerText = page + 1;
var href = new URL(window.location);
href.searchParams.set("p", page - 1);
document.getElementById("prev").href = href.toString();
href.searchParams.set("p", page + 1);
document.getElementById("next").href = href.toString();
if (page <= 0) {
document.getElementById("prev").style.display = "none";
}
if (page >= maxPage - 1) {
document.getElementById("next").style.display = "none";
}
// setup gacha type info
var gachaType = new window.URLSearchParams(window.location.search).get("gachaType");
if (mappings[lang] != null && mappings[lang][gachaType] != null) {
var gachaString = mappings[lang][gachaType];
} else {
var gachaString = mappings['default'][gachaType];
if (gachaString == null) {
gachaString = gachaType;
}
}
document.getElementById("gacha-type").innerText = gachaString;
})();
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD>lt1L <09><>ܟ<EFBFBD>.<15>\<5C>pXP<58><50>"ƀ(<28>a<><61><EFBFBD>