Commit a8293102 authored by Melledy's avatar Melledy Committed by GitHub
Browse files

Merge branch 'development' into stable

parents 304b9cb8 ecf7a81a
package emu.grasscutter.tools;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
......@@ -14,7 +10,6 @@ import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import com.google.gson.reflect.TypeToken;
......@@ -24,12 +19,17 @@ import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.data.def.AvatarData;
import emu.grasscutter.data.def.ItemData;
import emu.grasscutter.data.def.MonsterData;
import emu.grasscutter.data.def.SceneData;
import emu.grasscutter.data.binout.MainQuestData;
import emu.grasscutter.data.excels.AvatarData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.QuestData;
import emu.grasscutter.data.excels.SceneData;
import emu.grasscutter.utils.Utils;
import static emu.grasscutter.utils.Language.translate;
import static emu.grasscutter.Configuration.*;
public final class Tools {
public static void createGmHandbook() throws Exception {
ToolsWithLanguageOption.createGmHandbook(getLanguageOption());
......@@ -39,50 +39,45 @@ public final class Tools {
ToolsWithLanguageOption.createGachaMapping(location, getLanguageOption());
}
public static List<String> getAvailableLanguage() throws Exception {
File textMapFolder = new File(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap");
List<String> availableLangList = new ArrayList<String>();
for (String textMapFileName : textMapFolder.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (name.startsWith("TextMap") && name.endsWith(".json")){
return true;
}
return false;
}
})) {
availableLangList.add(textMapFileName.replace("TextMap","").replace(".json","").toLowerCase());
}
return availableLangList;
public static List<String> getAvailableLanguage() {
File textMapFolder = new File(RESOURCE("TextMap"));
List<String> availableLangList = new ArrayList<>();
for (String textMapFileName : Objects.requireNonNull(textMapFolder.list((dir, name) -> name.startsWith("TextMap") && name.endsWith(".json")))) {
availableLangList.add(textMapFileName.replace("TextMap", "").replace(".json", "").toLowerCase());
} return availableLangList;
}
public static String getLanguageOption() throws Exception {
public static String getLanguageOption() {
List<String> availableLangList = getAvailableLanguage();
// Use system out for better format
if (availableLangList.size() == 1) {
return availableLangList.get(0).toUpperCase();
}
String stagedMessage = "";
stagedMessage += "The following languages mappings are available, please select one: [default: EN]\n";
String groupedLangList = ">\t";
StringBuilder stagedMessage = new StringBuilder();
stagedMessage.append("The following languages mappings are available, please select one: [default: EN] \n");
StringBuilder groupedLangList = new StringBuilder(">\t"); String input;
int groupedLangCount = 0;
String input = "";
for (String availableLanguage: availableLangList){
groupedLangCount++;
groupedLangList = groupedLangList + "" + availableLanguage + "\t";
groupedLangList.append(availableLanguage).append("\t");
if (groupedLangCount == 6) {
stagedMessage += groupedLangList + "\n";
stagedMessage.append(groupedLangList).append("\n");
groupedLangCount = 0;
groupedLangList = ">\t";
groupedLangList = new StringBuilder(">\t");
}
}
if (groupedLangCount > 0) {
stagedMessage += groupedLangList + "\n";
stagedMessage.append(groupedLangList).append("\n");
}
stagedMessage += "\nYour choice:[EN] ";
input = Grasscutter.getConsole().readLine(stagedMessage);
stagedMessage.append("\nYour choice:[EN] ");
input = Grasscutter.getConsole().readLine(stagedMessage.toString());
if (availableLangList.contains(input.toLowerCase())) {
return input.toUpperCase();
}
......@@ -95,10 +90,10 @@ public final class Tools {
final class ToolsWithLanguageOption {
@SuppressWarnings("deprecation")
public static void createGmHandbook(String language) throws Exception {
ResourceLoader.loadResources();
ResourceLoader.loadAll();
Map<Long, String> map;
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8)) {
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(RESOURCE("TextMap/TextMap"+language+".json"))), StandardCharsets.UTF_8)) {
map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<Long, String>>() {}.getType());
}
......@@ -116,13 +111,12 @@ final class ToolsWithLanguageOption {
writer.println("// Commands");
for (Command cmd : cmdList) {
String cmdName = cmd.label();
StringBuilder cmdName = new StringBuilder(cmd.label());
while (cmdName.length() <= 15) {
cmdName = " " + cmdName;
cmdName.insert(0, " ");
}
writer.println(cmdName + " : " + cmd.description());
writer.println(cmdName + " : " + translate(cmd.description()));
}
writer.println();
list = new ArrayList<>(GameData.getAvatarDataMap().keySet());
......@@ -158,6 +152,18 @@ final class ToolsWithLanguageOption {
writer.println();
writer.println("// Quests");
list = new ArrayList<>(GameData.getQuestDataMap().keySet());
Collections.sort(list);
for (Integer id : list) {
QuestData data = GameData.getQuestDataMap().get(id);
MainQuestData mainQuest = GameData.getMainQuestDataMap().get(data.getMainId());
writer.println(data.getId() + " : " + map.get(mainQuest.getTitleTextMapHash()) + " - " + map.get(data.getDescTextMapHash()));
}
writer.println();
writer.println("// Monsters");
list = new ArrayList<>(GameData.getMonsterDataMap().keySet());
Collections.sort(list);
......@@ -176,16 +182,13 @@ final class ToolsWithLanguageOption {
ResourceLoader.loadResources();
Map<Long, String> map;
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8)) {
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(RESOURCE("TextMap/TextMap" + language + ".json"))), StandardCharsets.UTF_8)) {
map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<Long, String>>() {}.getType());
}
List<Integer> list;
String fileName = location;
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), false)) {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(location), StandardCharsets.UTF_8), false)) {
list = new ArrayList<>(GameData.getAvatarDataMap().keySet());
Collections.sort(list);
......@@ -207,18 +210,11 @@ final class ToolsWithLanguageOption {
} else {
writer.print(",");
}
String color;
switch (data.getQualityType()){
case "QUALITY_PURPLE":
color = "purple";
break;
case "QUALITY_ORANGE":
color = "yellow";
break;
case "QUALITY_BLUE":
default:
color = "blue";
}
String color = switch (data.getQualityType()) {
case "QUALITY_PURPLE" -> "purple";
case "QUALITY_ORANGE" -> "yellow";
default -> "blue";
};
// Got the magic number 4233146695 from manually search in the json file
writer.println(
"\"" + (avatarID % 1000 + 1000) + "\" : [\""
......
package emu.grasscutter.utils;
import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.Grasscutter.ServerRunMode;
import java.io.FileReader;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Locale;
import static emu.grasscutter.Grasscutter.config;
/**
* *when your JVM fails*
*/
public class ConfigContainer {
private static int version() {
return 3;
}
/**
* Attempts to update the server's existing configuration to the latest
*/
public static void updateConfig() {
try { // Check if the server is using a legacy config.
JsonObject configObject = Grasscutter.getGsonFactory()
.fromJson(new FileReader(Grasscutter.configFile), JsonObject.class);
if(!configObject.has("version")) {
Grasscutter.getLogger().info("Updating legacy ..");
Grasscutter.saveConfig(null);
}
} catch (Exception ignored) { }
var existing = config.version;
var latest = version();
if(existing == latest)
return;
// Create a new configuration instance.
ConfigContainer updated = new ConfigContainer();
// Update all configuration fields.
Field[] fields = ConfigContainer.class.getDeclaredFields();
Arrays.stream(fields).forEach(field -> {
try {
field.set(updated, field.get(config));
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to update a configuration field.", exception);
}
}); updated.version = version();
try { // Save configuration & reload.
Grasscutter.saveConfig(updated);
Grasscutter.loadConfig();
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to inject the updated ", exception);
}
}
public Structure folderStructure = new Structure();
public Database databaseInfo = new Database();
public Language language = new Language();
public Account account = new Account();
public Server server = new Server();
// DO NOT. TOUCH. THE VERSION NUMBER.
public int version = version();
/* Option containers. */
public static class Database {
public DataStore server = new DataStore();
public DataStore game = new DataStore();
public static class DataStore {
public String connectionUri = "mongodb://localhost:27017";
public String collection = "grasscutter";
}
}
public static class Structure {
public String resources = "./resources/";
public String data = "./data/";
public String packets = "./packets/";
public String scripts = "./resources/Scripts/";
public String plugins = "./plugins/";
// UNUSED (potentially added later?)
// public String dumps = "./dumps/";
}
public static class Server {
public ServerDebugMode debugLevel = ServerDebugMode.NONE;
public ServerRunMode runMode = ServerRunMode.HYBRID;
public HTTP http = new HTTP();
public Game game = new Game();
public Dispatch dispatch = new Dispatch();
}
public static class Language {
public Locale language = Locale.getDefault();
public Locale fallback = Locale.US;
public String document = "EN";
}
public static class Account {
public boolean autoCreate = false;
public String[] defaultPermissions = {};
public int maxPlayer = -1;
}
/* Server options. */
public static class HTTP {
public String bindAddress = "0.0.0.0";
/* This is the address used in URLs. */
public String accessAddress = "127.0.0.1";
public int bindPort = 443;
/* This is the port used in URLs. */
public int accessPort = 0;
public Encryption encryption = new Encryption();
public Policies policies = new Policies();
public Files files = new Files();
}
public static class Game {
public String bindAddress = "0.0.0.0";
/* This is the address used in the default region. */
public String accessAddress = "127.0.0.1";
public int bindPort = 22102;
/* This is the port used in the default region. */
public int accessPort = 0;
public boolean enableConsole = true;
public GameOptions gameOptions = new GameOptions();
public JoinOptions joinOptions = new JoinOptions();
public ConsoleAccount serverAccount = new ConsoleAccount();
}
/* Data containers. */
public static class Dispatch {
public Region[] regions = {};
public String defaultName = "Grasscutter";
}
public static class Encryption {
public boolean useEncryption = true;
/* Should 'https' be appended to URLs? */
public boolean useInRouting = true;
public String keystore = "./keystore.p12";
public String keystorePassword = "123456";
}
public static class Policies {
public Policies.CORS cors = new Policies.CORS();
public static class CORS {
public boolean enabled = false;
public String[] allowedOrigins = new String[]{"*"};
}
}
public static class GameOptions {
public InventoryLimits inventoryLimits = new InventoryLimits();
public AvatarLimits avatarLimits = new AvatarLimits();
public int sceneEntityLimit = 1000; // Unenforced. TODO: Implement.
public boolean watchGachaConfig = false;
public boolean enableShopItems = true;
public boolean staminaUsage = true;
public boolean energyUsage = false;
public Rates rates = new Rates();
public static class InventoryLimits {
public int weapons = 2000;
public int relics = 2000;
public int materials = 2000;
public int furniture = 2000;
public int all = 30000;
}
public static class AvatarLimits {
public int singlePlayerTeam = 4;
public int multiplayerTeam = 4;
}
public static class Rates {
public float adventureExp = 1.0f;
public float mora = 1.0f;
public float leyLines = 1.0f;
}
}
public static class JoinOptions {
public int[] welcomeEmotes = {2007, 1002, 4010};
public String welcomeMessage = "Welcome to a Grasscutter server.";
public JoinOptions.Mail welcomeMail = new JoinOptions.Mail();
public static class Mail {
public String title = "Welcome to Grasscutter!";
public String content = """
Hi there!\r
First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r
\r
Check out our:\r
<type="browser" text="Discord" href="https://discord.gg/T5vZU6UyeG"/>
""";
public String sender = "Lawnmower";
public emu.grasscutter.game.mail.Mail.MailItem[] items = {
new emu.grasscutter.game.mail.Mail.MailItem(13509, 1, 1),
new emu.grasscutter.game.mail.Mail.MailItem(201, 99999, 1)
};
}
}
public static class ConsoleAccount {
public int avatarId = 10000007;
public int nameCardId = 210001;
public int adventureRank = 1;
public int worldLevel = 0;
public String nickName = "Server";
public String signature = "Welcome to Grasscutter!";
}
public static class Files {
public String indexFile = "./index.html";
public String errorFile = "./404.html";
}
/* Objects. */
public static class Region {
public Region() { }
public Region(
String name, String title,
String address, int port
) {
this.Name = name;
this.Title = title;
this.Ip = address;
this.Port = port;
}
public String Name = "os_usa";
public String Title = "Grasscutter";
public String Ip = "127.0.0.1";
public int Port = 22102;
}
}
......@@ -7,18 +7,24 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.proto.GetPlayerTokenRspOuterClass.GetPlayerTokenRsp;
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp;
import static emu.grasscutter.Configuration.*;
public final class Crypto {
private static final SecureRandom secureRandom = new SecureRandom();
public static final long ENCRYPT_SEED = Long.parseUnsignedLong("11468049314633205968");
public static byte[] ENCRYPT_SEED_BUFFER = new byte[0];
public static byte[] DISPATCH_KEY;
public static byte[] DISPATCH_SEED;
public static byte[] ENCRYPT_KEY;
public static long ENCRYPT_SEED = Long.parseUnsignedLong("11468049314633205968");
public static byte[] ENCRYPT_SEED_BUFFER = new byte[0];
public static void loadKeys() {
DISPATCH_KEY = FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchKey.bin");
ENCRYPT_KEY = FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "secretKey.bin");
ENCRYPT_SEED_BUFFER = FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "secretKeyBuffer.bin");
DISPATCH_KEY = FileUtils.readResource("/keys/dispatchKey.bin");
DISPATCH_SEED = FileUtils.readResource("/keys/dispatchSeed.bin");
ENCRYPT_KEY = FileUtils.readResource("/keys/secretKey.bin");
ENCRYPT_SEED_BUFFER = FileUtils.readResource("/keys/secretKeyBuffer.bin");
}
public static void xor(byte[] packet, byte[] key) {
......@@ -31,28 +37,9 @@ public final class Crypto {
}
}
public static void extractSecretKeyBuffer(byte[] data) {
try {
GetPlayerTokenRsp p = GetPlayerTokenRsp.parseFrom(data);
FileUtils.write(Grasscutter.getConfig().KEY_FOLDER + "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(Grasscutter.getConfig().KEY_FOLDER + "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);
return bytes;
return bytes;
}
}
package emu.grasscutter.utils;
import java.util.Date;
import java.util.Calendar;
import java.util.Date;
public final class DateHelper {
public static Date onlyYearMonthDay(Date now) {
......@@ -13,4 +13,8 @@ public final class DateHelper {
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
public static int getUnixTime(Date localDateTime){
return (int)(localDateTime.getTime() / 1000L);
}
}
......@@ -4,9 +4,15 @@ 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.Arrays;
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 +38,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 +74,43 @@ 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 {
List<Path> result = null;
// Get pathUri of the current running JAR
URI pathUri = Grasscutter.class.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI();
try {
// file walks JAR
URI uri = URI.create("jar:file:" + pathUri.getRawPath());
try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) {
result = Files.walk(fs.getPath(folder))
.filter(Files::isRegularFile)
.collect(Collectors.toList());
}
} catch (Exception e) {
// Eclipse puts resources in its bin folder
File f = new File(System.getProperty("user.dir") + folder);
if (!f.exists() || f.listFiles().length == 0) {
return null;
}
result = Arrays.stream(f.listFiles()).map(File::toPath).toList();
}
return result;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static String readToString(InputStream file) throws IOException {
byte[] content = file.readAllBytes();
return new String(content, StandardCharsets.UTF_8);
}
}
......@@ -3,15 +3,21 @@ package emu.grasscutter.utils;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.player.Player;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import static emu.grasscutter.Configuration.*;
public final class Language {
private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>();
private final JsonObject languageData;
private final Map<String, String> cachedTranslations = new HashMap<>();
private final String languageCode;
private final Map<String, String> cachedTranslations = new ConcurrentHashMap<>();
/**
* Creates a language instance from a code.
......@@ -19,7 +25,24 @@ public final class Language {
* @return A language instance.
*/
public static Language getLanguage(String langCode) {
return new Language(langCode + ".json", Grasscutter.getConfig().DefaultLanguage.toLanguageTag() + ".json");
if (cachedLanguages.containsKey(langCode)) {
return cachedLanguages.get(langCode);
}
var fallbackLanguageCode = Utils.getLanguageCode(FALLBACK_LANGUAGE);
var description = getLanguageFileDescription(langCode, fallbackLanguageCode);
var actualLanguageCode = description.getLanguageCode();
Language languageInst;
if (description.getLanguageFile() != null) {
languageInst = new Language(description);
cachedLanguages.put(actualLanguageCode, languageInst);
} else {
languageInst = cachedLanguages.get(actualLanguageCode);
cachedLanguages.put(langCode, languageInst);
}
return languageInst;
}
/**
......@@ -39,32 +62,88 @@ public final class Language {
}
}
/**
* Returns the translated value from the key while substituting arguments.
* @param player Target player
* @param key The key of the translated value to return.
* @param args The arguments to substitute.
* @return A translated value with arguments substituted.
*/
public static String translate(Player player, String key, Object... args) {
if (player == null) {
return translate(key, args);
}
var langCode = Utils.getLanguageCode(player.getAccount().getLocale());
String translated = Grasscutter.getLanguage(langCode).get(key);
try {
return translated.formatted(args);
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to format string: " + key, exception);
return translated;
}
}
/**
* get language code
*/
public String getLanguageCode() {
return languageCode;
}
/**
* Reads a file and creates a language instance.
* @param fileName The name of the language file.
*/
private Language(String fileName, String fallback) {
private Language(LanguageStreamDescription description) {
@Nullable JsonObject languageData = null;
languageCode = description.getLanguageCode();
try {
languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(description.getLanguageFile()), JsonObject.class);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to load language file: " + description.getLanguageCode(), exception);
}
this.languageData = languageData;
}
/**
* create a LanguageStreamDescription
* @param languageCode The name of the language code.
* @param fallbackLanguageCode The name of the fallback language code.
*/
private static LanguageStreamDescription getLanguageFileDescription(String languageCode, String fallbackLanguageCode) {
var fileName = languageCode + ".json";
var fallback = fallbackLanguageCode + ".json";
String actualLanguageCode = languageCode;
InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName);
if (file == null) { // Provided fallback language.
file = Grasscutter.class.getResourceAsStream("/languages/" + fallback);
Grasscutter.getLogger().warn("Failed to load language file: " + fileName + ", falling back to: " + fallback);
actualLanguageCode = fallbackLanguageCode;
if (cachedLanguages.containsKey(actualLanguageCode)) {
return new LanguageStreamDescription(actualLanguageCode, null);
}
file = Grasscutter.class.getResourceAsStream("/languages/" + fallback);
}
if(file == null) { // Fallback the fallback language.
file = Grasscutter.class.getResourceAsStream("/languages/en-US.json");
Grasscutter.getLogger().warn("Failed to load language file: " + fallback + ", falling back to: en-US.json");
actualLanguageCode = "en-US";
if (cachedLanguages.containsKey(actualLanguageCode)) {
return new LanguageStreamDescription(actualLanguageCode, null);
}
file = Grasscutter.class.getResourceAsStream("/languages/en-US.json");
}
if(file == null)
throw new RuntimeException("Unable to load the primary, fallback, and 'en-US' language files.");
try {
languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to load language file: " + fileName, exception);
}
this.languageData = languageData;
return new LanguageStreamDescription(actualLanguageCode, file);
}
/**
......@@ -81,7 +160,9 @@ public final class Language {
JsonObject object = this.languageData;
int index = 0;
String result = "This value does not exist. Please report this to the Discord: " + key;
String valueNotFoundPattern = "This value does not exist. Please report this to the Discord: ";
String result = valueNotFoundPattern + key;
boolean isValueFound = false;
while (true) {
if(index == keys.length) break;
......@@ -92,11 +173,37 @@ public final class Language {
if(element.isJsonObject())
object = element.getAsJsonObject();
else {
isValueFound = true;
result = element.getAsString(); break;
}
} else break;
}
if (!isValueFound && !languageCode.equals("en-US")) {
var englishValue = Grasscutter.getLanguage("en-US").get(key);
if (!englishValue.contains(valueNotFoundPattern)) {
result += "\nhere is english version:\n" + englishValue;
}
}
this.cachedTranslations.put(key, result); return result;
}
private static class LanguageStreamDescription {
private final String languageCode;
private final InputStream languageFile;
public LanguageStreamDescription(String languageCode, InputStream languageFile) {
this.languageCode = languageCode;
this.languageFile = languageFile;
}
public String getLanguageCode() {
return languageCode;
}
public InputStream getLanguageFile() {
return languageFile;
}
}
}
......@@ -2,6 +2,8 @@ package emu.grasscutter.utils;
import java.io.Serializable;
import com.google.gson.annotations.SerializedName;
import dev.morphia.annotations.Entity;
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
......@@ -9,8 +11,13 @@ import emu.grasscutter.net.proto.VectorOuterClass.Vector;
public class Position implements Serializable {
private static final long serialVersionUID = -2001232313615923575L;
@SerializedName(value="x", alternate={"_x", "X"})
private float x;
@SerializedName(value="y", alternate={"_y", "Y"})
private float y;
@SerializedName(value="z", alternate={"_z", "Z"})
private float z;
public Position() {
......
package emu.grasscutter.utils;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.spi.DeferredProcessingAware;
import ch.qos.logback.core.status.ErrorStatus;
import emu.grasscutter.server.event.internal.ServerLogEvent;
import emu.grasscutter.server.event.types.ServerEvent;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ServerLogEventAppender<E> extends AppenderBase<E> {
protected Encoder<E> encoder;
@Override
protected void append(E event) {
byte[] byteArray = this.encoder.encode(event);
ServerLogEvent sle = new ServerLogEvent(ServerEvent.Type.GAME, (ILoggingEvent) event, new String(byteArray, StandardCharsets.UTF_8));
sle.call();
}
public Encoder<E> getEncoder() {
return encoder;
}
public void setEncoder(Encoder<E> encoder) {
this.encoder = encoder;
}
}
......@@ -6,18 +6,20 @@ import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.time.*;
import java.time.temporal.TemporalAdjusters;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.*;
import emu.grasscutter.Config;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import org.slf4j.Logger;
import javax.annotation.Nullable;
import static emu.grasscutter.utils.Language.translate;
@SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"})
......@@ -172,12 +174,12 @@ public final class Utils {
* Checks for required files and folders before startup.
*/
public static void startupCheck() {
Config config = Grasscutter.getConfig();
ConfigContainer config = Grasscutter.getConfig();
Logger logger = Grasscutter.getLogger();
boolean exit = false;
String resourcesFolder = config.RESOURCE_FOLDER;
String dataFolder = config.DATA_FOLDER;
String resourcesFolder = config.folderStructure.resources;
String dataFolder = config.folderStructure.data;
// Check for resources folder.
if(!fileExists(resourcesFolder)) {
......@@ -197,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);
}
......@@ -253,7 +258,9 @@ public final class Utils {
* @param stream The input stream.
* @return The string.
*/
public static String readFromInputStream(InputStream stream) {
public static String readFromInputStream(@Nullable InputStream stream) {
if(stream == null) return "empty";
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
String line; while ((line = reader.readLine()) != null) {
......@@ -261,43 +268,113 @@ public final class Utils {
} stream.close();
} catch (IOException e) {
Grasscutter.getLogger().warn("Failed to read from input stream.");
} catch (NullPointerException ignored) {
return "empty";
} return stringBuilder.toString();
}
/**
* Switch properties from upper case to lower case?
* Performs a linear interpolation using a table of fixed points to create an effective piecewise f(x) = y function.
* @param x
* @param xyArray Array of points in [[x0,y0], ... [xN, yN]] format
* @return f(x) = y
*/
public static Map<String, Object> switchPropertiesUpperLowerCase(Map<String, Object> objMap, Class<?> cls) {
Map<String, Object> map = new HashMap<>(objMap.size());
for (String key : objMap.keySet()) {
try {
char c = key.charAt(0);
if (c >= 'a' && c <= 'z') {
try {
cls.getDeclaredField(key);
map.put(key, objMap.get(key));
} catch (NoSuchFieldException e) {
String s1 = String.valueOf(c).toUpperCase();
String after = key.length() > 1 ? s1 + key.substring(1) : s1;
cls.getDeclaredField(after);
map.put(after, objMap.get(key));
}
} else if (c >= 'A' && c <= 'Z') {
try {
cls.getDeclaredField(key);
map.put(key, objMap.get(key));
} catch (NoSuchFieldException e) {
String s1 = String.valueOf(c).toLowerCase();
String after = key.length() > 1 ? s1 + key.substring(1) : s1;
cls.getDeclaredField(after);
map.put(after, objMap.get(key));
}
public static int lerp(int x, int[][] xyArray) {
try {
if (x <= xyArray[0][0]){ // Clamp to first point
return xyArray[0][1];
} else if (x >= xyArray[xyArray.length-1][0]) { // Clamp to last point
return xyArray[xyArray.length-1][1];
}
// At this point we're guaranteed to have two lerp points, and pity be somewhere between them.
for (int i=0; i < xyArray.length-1; i++) {
if (x == xyArray[i+1][0]) {
return xyArray[i+1][1];
}
if (x < xyArray[i+1][0]) {
// We are between [i] and [i+1], interpolation time!
// Using floats would be slightly cleaner but we can just as easily use ints if we're careful with order of operations.
int position = x - xyArray[i][0];
int fullDist = xyArray[i+1][0] - xyArray[i][0];
int prevValue = xyArray[i][1];
int fullDelta = xyArray[i+1][1] - prevValue;
return prevValue + ( (position * fullDelta) / fullDist );
}
} catch (NoSuchFieldException e) {
map.put(key, objMap.get(key));
}
} catch (IndexOutOfBoundsException e) {
Grasscutter.getLogger().error("Malformed lerp point array. Must be of form [[x0, y0], ..., [xN, yN]].");
}
return 0;
}
return map;
/**
* Checks if an int is in an int[]
* @param key int to look for
* @param array int[] to look in
* @return key in array
*/
public static boolean intInArray(int key, int[] array) {
for (int i : array) {
if (i == key) {
return true;
}
}
return false;
}
/**
* Return a copy of minuend without any elements found in subtrahend.
* @param minuend The array we want elements from
* @param subtrahend The array whose elements we don't want
* @return The array with only the elements we want, in the order that minuend had them
*/
public static int[] setSubtract(int[] minuend, int[] subtrahend) {
IntList temp = new IntArrayList();
for (int i : minuend) {
if (!intInArray(i, subtrahend)) {
temp.add(i);
}
}
return temp.toIntArray();
}
/**
* Gets the language code from a given locale.
* @param locale A locale.
* @return A string in the format of 'XX-XX'.
*/
public static String getLanguageCode(Locale locale) {
return String.format("%s-%s", locale.getLanguage(), locale.getCountry());
}
/**
* Base64 encodes a given byte array.
* @param toEncode An array of bytes.
* @return A base64 encoded string.
*/
public static String base64Encode(byte[] toEncode) {
return Base64.getEncoder().encodeToString(toEncode);
}
/**
* Base64 decodes a given string.
* @param toDecode A base64 encoded string.
* @return An array of bytes.
*/
public static byte[] base64Decode(String toDecode) {
return Base64.getDecoder().decode(toDecode);
}
/**
* Safely JSON decodes a given string.
* @param jsonData The JSON-encoded data.
* @return JSON decoded data, or null if an exception occurred.
*/
public static <T> T jsonDecode(String jsonData, Class<T> classType) {
try {
return Grasscutter.getGsonFactory().fromJson(jsonData, classType);
} catch (Exception ignored) {
return null;
}
}
}
......@@ -6,44 +6,66 @@
"prefabPath": "GachaShowPanel_A022",
"previewPrefabPath": "UI_Tab_GachaShowPanel_A022",
"titlePath": "UI_GACHA_SHOW_PANEL_A022_TITLE",
"costItem": 224,
"costItemId": 224,
"costItemAmount": 1,
"costItemAmount10": 10,
"beginTime": 0,
"endTime": 1924992000,
"sortId": 1000,
"rateUpItems1": [],
"rateUpItems2": []
"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",
"costItem": 223,
"prefabPath": "GachaShowPanel_A081",
"previewPrefabPath": "UI_Tab_GachaShowPanel_A081",
"titlePath": "UI_GACHA_SHOW_PANEL_A081_TITLE",
"costItemId": 223,
"beginTime": 0,
"endTime": 1924992000,
"sortId": 9998,
"maxItemType": 1,
"rateUpItems1": [1002],
"rateUpItems2": [1053, 1020, 1045]
"rateUpItems4": [1034, 1014, 1048],
"rateUpItems5": [1060],
"fallbackItems5Pool2": [],
"weights5": [[1,80], [73,80], [90,10000]]
},
{
"gachaType": 400,
"scheduleId": 923,
"bannerType": "EVENT",
"prefabPath": "GachaShowPanel_A082",
"previewPrefabPath": "UI_Tab_GachaShowPanel_A082",
"titlePath": "UI_GACHA_SHOW_PANEL_A031_TITLE",
"costItemId": 223,
"beginTime": 0,
"endTime": 1924992000,
"sortId": 9998,
"rateUpItems4": [1034, 1014, 1048],
"rateUpItems5": [1026],
"fallbackItems5Pool2": [],
"weights5": [[1,80], [73,80], [90,10000]]
},
{
"gachaType": 302,
"scheduleId": 913,
"bannerType": "WEAPON",
"prefabPath": "GachaShowPanel_A080",
"previewPrefabPath": "UI_Tab_GachaShowPanel_A080",
"prefabPath": "GachaShowPanel_A083",
"previewPrefabPath": "UI_Tab_GachaShowPanel_A083",
"titlePath": "UI_GACHA_SHOW_PANEL_A021_TITLE",
"costItem": 223,
"costItemId": 223,
"beginTime": 0,
"endTime": 1924992000,
"sortId": 9997,
"minItemType": 2,
"eventChance": 75,
"softPity": 80,
"hardPity": 80,
"rateUpItems1": [11509, 12504],
"rateUpItems2": [11401, 12402, 13407, 14401, 15401]
"rateUpItems4": [11403, 12401, 13406, 14409, 15403],
"rateUpItems5": [15508, 13505],
"fallbackItems5Pool1": [],
"weights4": [[1,600], [7,600], [8, 6600], [10,12600]],
"weights5": [[1,100], [62,100], [73, 7800], [80,10000]]
}
]
[
{"monsterId":28040101,"dropDataList":[{"itemId":100084,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28040102,"dropDataList":[{"itemId":100084,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28040103,"dropDataList":[{"itemId":100084,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28040104,"dropDataList":[{"itemId":100084,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28040105,"dropDataList":[{"itemId":100084,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28040106,"dropDataList":[{"itemId":100084,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28040107,"dropDataList":[{"itemId":100084,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28040108,"dropDataList":[{"itemId":100084,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020301,"dropDataList":[{"itemId":100061,"minCount":2,"maxCount":2,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020302,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020101,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020102,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020103,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020104,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020105,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020106,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020701,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020702,"dropDataList":[{"itemId":100061,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020303,"dropDataList":[{"itemId":100094,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28020304,"dropDataList":[{"itemId":100094,"minCount":2,"maxCount":3,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030401,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030402,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030403,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030404,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030405,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030406,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030407,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030408,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030409,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030301,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030302,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030303,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030304,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030305,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030306,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030307,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030308,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030309,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030310,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{"monsterId":28030311,"dropDataList":[{"itemId":100064,"minCount":1,"maxCount":1,"minWeight":0,"maxWeight":10000}]},
{
"monsterId": 21010101,
"dropDataList": [
......@@ -132754,21 +132794,21 @@
"maxWeight": 1
},
{
"itemId": 112047,
"itemId": 112023,
"minCount": 1,
"maxCount": 3,
"minWeight": 2000,
"maxWeight": 6000
},
{
"itemId": 112048,
"itemId": 112024,
"minCount": 1,
"maxCount": 1,
"minWeight": 8001,
"maxWeight": 9500
},
{
"itemId": 112049,
"itemId": 112025,
"minCount": 1,
"maxCount": 1,
"minWeight": 9501,
......@@ -132815,21 +132855,21 @@
"maxWeight": 1
},
{
"itemId": 112047,
"itemId": 112023,
"minCount": 1,
"maxCount": 3,
"minWeight": 2000,
"maxWeight": 6000
},
{
"itemId": 112048,
"itemId": 112024,
"minCount": 1,
"maxCount": 1,
"minWeight": 8001,
"maxWeight": 9500
},
{
"itemId": 112049,
"itemId": 112025,
"minCount": 1,
"maxCount": 1,
"minWeight": 9501,
......@@ -132876,21 +132916,21 @@
"maxWeight": 1
},
{
"itemId": 112047,
"itemId": 112023,
"minCount": 1,
"maxCount": 3,
"minWeight": 2000,
"maxWeight": 6000
},
{
"itemId": 112048,
"itemId": 112024,
"minCount": 1,
"maxCount": 1,
"minWeight": 8001,
"maxWeight": 9500
},
{
"itemId": 112049,
"itemId": 112025,
"minCount": 1,
"maxCount": 1,
"minWeight": 9501,
......@@ -132937,21 +132977,21 @@
"maxWeight": 1
},
{
"itemId": 112047,
"itemId": 112023,
"minCount": 1,
"maxCount": 3,
"minWeight": 2000,
"maxWeight": 6000
},
{
"itemId": 112048,
"itemId": 112024,
"minCount": 1,
"maxCount": 1,
"minWeight": 8001,
"maxWeight": 9500
},
{
"itemId": 112049,
"itemId": 112025,
"minCount": 1,
"maxCount": 1,
"minWeight": 9501,
......@@ -132998,21 +133038,21 @@
"maxWeight": 1
},
{
"itemId": 112047,
"itemId": 112023,
"minCount": 1,
"maxCount": 3,
"minWeight": 2000,
"maxWeight": 6000
},
{
"itemId": 112048,
"itemId": 112024,
"minCount": 1,
"maxCount": 1,
"minWeight": 8001,
"maxWeight": 9500
},
{
"itemId": 112049,
"itemId": 112025,
"minCount": 1,
"maxCount": 1,
"minWeight": 9501,
......@@ -133059,21 +133099,21 @@
"maxWeight": 1
},
{
"itemId": 112047,
"itemId": 112023,
"minCount": 1,
"maxCount": 3,
"minWeight": 2000,
"maxWeight": 6000
},
{
"itemId": 112048,
"itemId": 112024,
"minCount": 1,
"maxCount": 1,
"minWeight": 8001,
"maxWeight": 9500
},
{
"itemId": 112049,
"itemId": 112025,
"minCount": 1,
"maxCount": 1,
"minWeight": 9501,
[
{
"dropId": 22010010,
"dropList": [
{ "ballId": 2024, "count": 1 }
]
},
{
"dropId": 22010030,
"dropList": [
{ "ballId": 2008, "count": 1 }
]
},
{
"dropId": 22010050,
"dropList": [
{ "ballId": 2024, "count": 3 },
{ "ballId": 2008, "count": 1 }
]
},
{
"dropId": 22010013,
"dropList": [
{ "ballId": 2019, "count": 1 }
]
},
{
"dropId": 22010033,
"dropList": [
{ "ballId": 2003, "count": 1 }
]
},
{
"dropId": 22010015,
"dropList": [
{ "ballId": 2021, "count": 1 }
]
},
{
"dropId": 22010035,
"dropList": [
{ "ballId": 2005, "count": 1 }
]
},
{
"dropId": 22010034,
"dropList": [
{ "ballId": 2004, "count": 1 }
]
},
{
"dropId": 22010037,
"dropList": [
{ "ballId": 2007, "count": 1 }
]
},
{
"dropId": 22010032,
"dropList": [
{ "ballId": 2002, "count": 1 }
]
},
{
"dropId": 22010022,
"dropList": [
{ "ballId": 2018, "count": 1 }
]
},
{
"dropId": 22010036,
"dropList": [
{ "ballId": 2006, "count": 1 }
]
},
{
"dropId": 22010026,
"dropList": [
{ "ballId": 2022, "count": 1 }
]
},
{
"dropId": 22010031,
"dropList": [
{ "ballId": 2001, "count": 1 }
]
},
{
"dropId": 22010014,
"dropList": [
{ "ballId": 2020, "count": 1 }
]
},
{
"dropId": 22010016,
"dropList": [
{ "ballId": 2022, "count": 1 }
]
},
{
"dropId": 22010012,
"dropList": [
{ "ballId": 2018, "count": 1 }
]
},
{
"dropId": 22010024,
"dropList": [
{ "ballId": 2004, "count": 1 }
]
},
{
"dropId": 22010011,
"dropList": [
{ "ballId": 2017, "count": 1 }
]
},
{
"dropId": 22010017,
"dropList": [
{ "ballId": 2023, "count": 1 }
]
},
{
"dropId": 22010021,
"dropList": [
{ "ballId": 2017, "count": 1 }
]
},
{
"dropId": 22010027,
"dropList": [
{ "ballId": 2007, "count": 1 }
]
},
{
"dropId": 22010040,
"dropList": [
{ "ballId": 2024, "count": 1 },
{ "ballId": 2008, "count": 1 }
]
},
{
"dropId": 22010025,
"dropList": [
{ "ballId": 2021, "count": 2 }
]
},
{
"dropId": 22010020,
"dropList": [
{ "ballId": 2024, "count": 1 }
]
},
{
"dropId": 22003100,
"dropList": [
]
},
{
"dropId": 22001000,
"dropList": [
]
},
{
"dropId": 22000100,
"dropList": [
]
},
{
"dropId": 22003000,
"dropList": [
]
},
{
"dropId": 22001100,
"dropList": [
]
},
{
"dropId": 22000000,
"dropList": [
]
}
]
\ No newline at end of file
{
"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
}
\ No newline at end of file
{
"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
}
\ No newline at end of file
......@@ -5,8 +5,8 @@
{
"goodsId": 1004202,
"goodsItem": {
"Id": 202,
"Count": 1000000
"id": 202,
"count": 1000000
},
"scoin": 1,
"buyLimit": 500,
......@@ -16,17 +16,18 @@
"maxLevel": 99,
"costItemList": [
{
"Id": 223,
"Count": 100
"id": 223,
"count": 100
}
]
},
{
"goodsId": 10048006,
"goodsItem": {
"Id": 108006,
"Count": 20
"id": 108006,
"count": 20
},
"costItemList": [],
"scoin": 100,
"hcoin": 100,
"mcoin": 100,
......@@ -39,9 +40,10 @@
{
"goodsId": 10048033,
"goodsItem": {
"Id": 108033,
"Count": 20
"id": 108033,
"count": 20
},
"costItemList": [],
"scoin": 1,
"buyLimit": 50000,
"beginTime": 1575129600,
......
[
{
"avatarId": 10000002,
"name": "Kamisato Ayaka",
"amountList": [
{
"value": 4,
"chance": 50
},
{
"value": 5,
"chance": 50
}
]
},
{
"avatarId": 10000003,
"name": "Jean",
"amountList": [
{
"value": 2,
"chance": 33
},
{
"value": 3,
"chance": 67
}
]
},
{
"avatarId": 10000005,
"name": "Traveler",
"amountList": [
{
"value": 3,
"chance": 67
},
{
"value": 4,
"chance": 33
}
]
},
{
"avatarId": 10000006,
"name": "Lisa",
"amountList": [
{
"value": 5,
"chance": 100
}
]
},
{
"avatarId": 10000007,
"name": "Traveler",
"amountList": [
{
"value": 3,
"chance": 67
},
{
"value": 4,
"chance": 33
}
]
},
{
"avatarId": 10000014,
"name": "Barbara",
"amountList": [
{
"value": 0,
"chance": 100
}
]
},
{
"avatarId": 10000015,
"name": "Kaeya",
"amountList": [
{
"value": 2,
"chance": 33
},
{
"value": 3,
"chance": 67
}
]
},
{
"avatarId": 10000016,
"name": "Diluc",
"amountList": [
{
"value": 1,
"chance": 33
},
{
"value": 2,
"chance": 67
}
]
},
{
"avatarId": 10000020,
"name": "Razor",
"amountList": [
{
"value": 3,
"chance": 100
}
]
},
{
"avatarId": 10000021,
"name": "Amber",
"amountList": [
{
"value": 4,
"chance": 100
}
]
},
{
"avatarId": 10000022,
"name": "Venti",
"amountList": [
{
"value": 3,
"chance": 100
}
]
},
{
"avatarId": 10000023,
"name": "Xiangling",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000024,
"name": "Beidou",
"amountList": [
{
"value": 2,
"chance": 100
}
]
},
{
"avatarId": 10000025,
"name": "Xingqiu",
"amountList": [
{
"value": 5,
"chance": 100
}
]
},
{
"avatarId": 10000026,
"name": "Xiao",
"amountList": [
{
"value": 3,
"chance": 100
}
]
},
{
"avatarId": 10000027,
"name": "Ningguang",
"amountList": [
{
"value": 3,
"chance": 33
},
{
"value": 4,
"chance": 67
}
]
},
{
"avatarId": 10000029,
"name": "Klee",
"amountList": [
{
"value": 4,
"chance": 100
}
]
},
{
"avatarId": 10000030,
"name": "Zhongli",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000031,
"name": "Fischl",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000032,
"name": "Bennett",
"amountList": [
{
"value": 2,
"chance": 75
},
{
"value": 3,
"chance": 25
}
]
},
{
"avatarId": 10000033,
"name": "Tartaglia",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000034,
"name": "Noelle",
"amountList": [
{
"value": 0,
"chance": 100
}
]
},
{
"avatarId": 10000035,
"name": "Qiqi",
"amountList": [
{
"value": 0,
"chance": 100
}
]
},
{
"avatarId": 10000036,
"name": "Chongyun",
"amountList": [
{
"value": 4,
"chance": 100
}
]
},
{
"avatarId": 10000037,
"name": "Ganyu",
"amountList": [
{
"value": 2,
"chance": 100
}
]
},
{
"avatarId": 10000038,
"name": "Albedo",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000039,
"name": "Diona",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000041,
"name": "Mona",
"amountList": [
{
"value": 3,
"chance": 67
},
{
"value": 4,
"chance": 33
}
]
},
{
"avatarId": 10000042,
"name": "Keqing",
"amountList": [
{
"value": 2,
"chance": 50
},
{
"value": 3,
"chance": 50
}
]
},
{
"avatarId": 10000043,
"name": "Sucrose",
"amountList": [
{
"value": 4,
"chance": 100
}
]
},
{
"avatarId": 10000044,
"name": "Xinyan",
"amountList": [
{
"value": 4,
"chance": 100
}
]
},
{
"avatarId": 10000045,
"name": "Rosaria",
"amountList": [
{
"value": 3,
"chance": 100
}
]
},
{
"avatarId": 10000046,
"name": "Hu Tao",
"amountList": [
{
"value": 2,
"chance": 50
},
{
"value": 3,
"chance": 50
}
]
},
{
"avatarId": 10000047,
"name": "Kaedehara Kazuha",
"amountList": [
{
"value": 3,
"chance": 50
},
{
"value": 4,
"chance": 50
}
]
},
{
"avatarId": 10000048,
"name": "Yanfei",
"amountList": [
{
"value": 3,
"chance": 100
}
]
},
{
"avatarId": 10000049,
"name": "Yoimiya",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000050,
"name": "Thoma",
"amountList": [
{
"value": 3,
"chance": 50
},
{
"value": 4,
"chance": 50
}
]
},
{
"avatarId": 10000051,
"name": "Eula",
"amountList": [
{
"value": 1,
"chance": 50
},
{
"value": 2,
"chance": 50
}
]
},
{
"avatarId": 10000052,
"name": "Raiden Shogun",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000053,
"name": "Sayu",
"amountList": [
{
"value": 2,
"chance": 100
}
]
},
{
"avatarId": 10000054,
"name": "Sangonomiya Kokomi",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000055,
"name": "Gorou",
"amountList": [
{
"value": 2,
"chance": 100
}
]
},
{
"avatarId": 10000056,
"name": "Kujou Sara",
"amountList": [
{
"value": 3,
"chance": 100
}
]
},
{
"avatarId": 10000057,
"name": "Arataki Itto",
"amountList": [
{
"value": 3,
"chance": 50
},
{
"value": 4,
"chance": 50
}
]
},
{
"avatarId": 10000058,
"name": "Yae Miko",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000060,
"name": "Yelan",
"amountList": [
{
"value": 4,
"chance": 100
}
]
},
{
"avatarId": 10000062,
"name": "Aloy",
"amountList": [
{
"value": 5,
"chance": 100
}
]
},
{
"avatarId": 10000063,
"name": "Shenhe",
"amountList": [
{
"value": 3,
"chance": 100
}
]
},
{
"avatarId": 10000064,
"name": "Yun Jin",
"amountList": [
{
"value": 2,
"chance": 100
}
]
},
{
"avatarId": 10000065,
"name": "Kuki Shinobu",
"amountList": [
{
"value": 1,
"chance": 100
}
]
},
{
"avatarId": 10000066,
"name": "Kamisato Ayato",
"amountList": [
{
"value": 1,
"chance": 50
},
{
"value": 2,
"chance": 50
}
]
}
]
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