diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 93fa552dd..e1a0c3352 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -1,29 +1,29 @@ -package emu.grasscutter.command.commands; - -import static emu.grasscutter.utils.Language.translate; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.command.Command; -import emu.grasscutter.command.CommandHandler; -import emu.grasscutter.game.player.Player; -import java.util.List; - -@Command( - label = "reload", - permission = "server.reload", - targetRequirement = Command.TargetRequirement.NONE) -public final class ReloadCommand implements CommandHandler { - - @Override - public void execute(Player sender, Player targetPlayer, List args) { - CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_start")); - - Grasscutter.loadConfig(); - Grasscutter.loadLanguage(); - Grasscutter.getGameServer().getGachaSystem().load(); - Grasscutter.getGameServer().getDropSystem().load(); - Grasscutter.getGameServer().getShopSystem().load(); - - CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_done")); - } -} +package emu.grasscutter.command.commands; + +import static emu.grasscutter.utils.Language.translate; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; +import java.util.List; + +@Command( + label = "reload", + permission = "server.reload", + targetRequirement = Command.TargetRequirement.NONE) +public final class ReloadCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List args) { + CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_start")); + + Grasscutter.loadConfig(); + Grasscutter.loadLanguage(); + Grasscutter.getGameServer().getGachaSystem().load(); + Grasscutter.getGameServer().getDropSystem().load(); + Grasscutter.getGameServer().getShopSystem().load(); + + CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_done")); + } +} diff --git a/src/main/java/emu/grasscutter/game/drop/DropData.java b/src/main/java/emu/grasscutter/game/drop/DropData.java index f1b99a8aa..213007d94 100644 --- a/src/main/java/emu/grasscutter/game/drop/DropData.java +++ b/src/main/java/emu/grasscutter/game/drop/DropData.java @@ -1,51 +1,51 @@ -package emu.grasscutter.game.drop; - -public class DropData { - private int minWeight; - private int maxWeight; - private int itemId; - private int minCount; - private int maxCount; - private boolean share = false; - private boolean give = false; - - public boolean isGive() { - return give; - } - - public void setGive(boolean give) { - this.give = give; - } - - public int getItemId() { - return itemId; - } - - public void setItemId(int itemId) { - this.itemId = itemId; - } - - public int getMinCount() { - return minCount; - } - - public int getMaxCount() { - return maxCount; - } - - public int getMinWeight() { - return minWeight; - } - - public int getMaxWeight() { - return maxWeight; - } - - public boolean isShare() { - return share; - } - - public void setIsShare(boolean share) { - this.share = share; - } -} +package emu.grasscutter.game.drop; + +public class DropData { + private int minWeight; + private int maxWeight; + private int itemId; + private int minCount; + private int maxCount; + private boolean share = false; + private boolean give = false; + + public boolean isGive() { + return give; + } + + public void setGive(boolean give) { + this.give = give; + } + + public int getItemId() { + return itemId; + } + + public void setItemId(int itemId) { + this.itemId = itemId; + } + + public int getMinCount() { + return minCount; + } + + public int getMaxCount() { + return maxCount; + } + + public int getMinWeight() { + return minWeight; + } + + public int getMaxWeight() { + return maxWeight; + } + + public boolean isShare() { + return share; + } + + public void setIsShare(boolean share) { + this.share = share; + } +} diff --git a/src/main/java/emu/grasscutter/game/drop/DropSystem.java b/src/main/java/emu/grasscutter/game/drop/DropSystem.java index e04dd0cd2..34d92beb2 100644 --- a/src/main/java/emu/grasscutter/game/drop/DropSystem.java +++ b/src/main/java/emu/grasscutter/game/drop/DropSystem.java @@ -1,111 +1,111 @@ -package emu.grasscutter.game.drop; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.DataLoader; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.game.entity.EntityItem; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.inventory.ItemType; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.server.game.BaseGameSystem; -import emu.grasscutter.server.game.GameServer; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.List; - -public class DropSystem extends BaseGameSystem { - private final Int2ObjectMap> dropData; - - public DropSystem(GameServer server) { - super(server); - this.dropData = new Int2ObjectOpenHashMap<>(); - this.load(); - } - - public Int2ObjectMap> getDropData() { - return dropData; - } - - public synchronized void load() { - getDropData().clear(); - try { - List banners = DataLoader.loadList("Drop.json", DropInfo.class); - if (banners.size() > 0) { - for (DropInfo di : banners) { - getDropData().put(di.getMonsterId(), di.getDropDataList()); - } - Grasscutter.getLogger().debug("Drop data successfully loaded."); - } else { - Grasscutter.getLogger().error("Unable to load drop data. Drop data size is 0."); - } - } catch (Exception e) { - Grasscutter.getLogger().error("Unable to load drop data.", e); - } - } - - private void addDropEntity( - DropData dd, Scene dropScene, ItemData itemData, Position pos, int num, Player target) { - if (!dd.isGive() - && (itemData.getItemType() != ItemType.ITEM_VIRTUAL || itemData.getGadgetId() != 0)) { - EntityItem entity = new EntityItem(dropScene, target, itemData, pos, num, dd.isShare()); - if (!dd.isShare()) dropScene.addEntityToSingleClient(target, entity); - else dropScene.addEntity(entity); - } else { - if (target != null) { - target.getInventory().addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true); - } else { - // target is null if items will be added are shared. no one could pick it up because of the - // combination(give + shared) - // so it will be sent to all players' inventories directly. - dropScene - .getPlayers() - .forEach( - x -> - x.getInventory() - .addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true)); - } - } - } - - private void processDrop(DropData dd, EntityMonster em, Player gp) { - int target = Utils.randomRange(1, 10000); - if (target >= dd.getMinWeight() && target < dd.getMaxWeight()) { - ItemData itemData = GameData.getItemDataMap().get(dd.getItemId()); - int num = Utils.randomRange(dd.getMinCount(), dd.getMaxCount()); - - if (itemData == null) { - return; - } - if (itemData.isEquip()) { - for (int i = 0; i < num; i++) { - float range = (2.5f + (.05f * num)); - Position pos = em.getPosition().nearby2d(range).addY(3f); - addDropEntity(dd, em.getScene(), itemData, pos, num, gp); - } - } else { - Position pos = em.getPosition().clone().addY(3f); - addDropEntity(dd, em.getScene(), itemData, pos, num, gp); - } - } - } - - public void callDrop(EntityMonster em) { - int id = em.getMonsterData().getId(); - if (getDropData().containsKey(id)) { - for (DropData dd : getDropData().get(id)) { - if (dd.isShare()) processDrop(dd, em, null); - else { - for (Player gp : em.getScene().getPlayers()) { - processDrop(dd, em, gp); - } - } - } - } - } -} +package emu.grasscutter.game.drop; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.DataLoader; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.ItemData; +import emu.grasscutter.game.entity.EntityItem; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.inventory.ItemType; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.server.game.BaseGameSystem; +import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import java.util.List; + +public class DropSystem extends BaseGameSystem { + private final Int2ObjectMap> dropData; + + public DropSystem(GameServer server) { + super(server); + this.dropData = new Int2ObjectOpenHashMap<>(); + this.load(); + } + + public Int2ObjectMap> getDropData() { + return dropData; + } + + public synchronized void load() { + getDropData().clear(); + try { + List banners = DataLoader.loadList("Drop.json", DropInfo.class); + if (banners.size() > 0) { + for (DropInfo di : banners) { + getDropData().put(di.getMonsterId(), di.getDropDataList()); + } + Grasscutter.getLogger().debug("Drop data successfully loaded."); + } else { + Grasscutter.getLogger().error("Unable to load drop data. Drop data size is 0."); + } + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load drop data.", e); + } + } + + private void addDropEntity( + DropData dd, Scene dropScene, ItemData itemData, Position pos, int num, Player target) { + if (!dd.isGive() + && (itemData.getItemType() != ItemType.ITEM_VIRTUAL || itemData.getGadgetId() != 0)) { + EntityItem entity = new EntityItem(dropScene, target, itemData, pos, num, dd.isShare()); + if (!dd.isShare()) dropScene.addEntityToSingleClient(target, entity); + else dropScene.addEntity(entity); + } else { + if (target != null) { + target.getInventory().addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true); + } else { + // target is null if items will be added are shared. no one could pick it up because of the + // combination(give + shared) + // so it will be sent to all players' inventories directly. + dropScene + .getPlayers() + .forEach( + x -> + x.getInventory() + .addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true)); + } + } + } + + private void processDrop(DropData dd, EntityMonster em, Player gp) { + int target = Utils.randomRange(1, 10000); + if (target >= dd.getMinWeight() && target < dd.getMaxWeight()) { + ItemData itemData = GameData.getItemDataMap().get(dd.getItemId()); + int num = Utils.randomRange(dd.getMinCount(), dd.getMaxCount()); + + if (itemData == null) { + return; + } + if (itemData.isEquip()) { + for (int i = 0; i < num; i++) { + float range = (2.5f + (.05f * num)); + Position pos = em.getPosition().nearby2d(range).addY(3f); + addDropEntity(dd, em.getScene(), itemData, pos, num, gp); + } + } else { + Position pos = em.getPosition().clone().addY(3f); + addDropEntity(dd, em.getScene(), itemData, pos, num, gp); + } + } + } + + public void callDrop(EntityMonster em) { + int id = em.getMonsterData().getId(); + if (getDropData().containsKey(id)) { + for (DropData dd : getDropData().get(id)) { + if (dd.isShare()) processDrop(dd, em, null); + else { + for (Player gp : em.getScene().getPlayers()) { + processDrop(dd, em, gp); + } + } + } + } + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetChest.java b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetChest.java index 033ebb308..8b7041466 100644 --- a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetChest.java +++ b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetChest.java @@ -1,88 +1,88 @@ -package emu.grasscutter.game.entity.gadget; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.gadget.chest.BossChestInteractHandler; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.net.proto.BossChestInfoOuterClass.BossChestInfo; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.InterOpTypeOuterClass.InterOpType; -import emu.grasscutter.net.proto.InteractTypeOuterClass; -import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; -import emu.grasscutter.net.proto.ResinCostTypeOuterClass; -import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; -import emu.grasscutter.scripts.constants.ScriptGadgetState; -import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; - -public class GadgetChest extends GadgetContent { - - public GadgetChest(EntityGadget gadget) { - super(gadget); - } - - public boolean onInteract(Player player, GadgetInteractReq req) { - var chestInteractHandlerMap = - getGadget() - .getScene() - .getWorld() - .getServer() - .getWorldDataSystem() - .getChestInteractHandlerMap(); - var handler = chestInteractHandlerMap.get(getGadget().getGadgetData().getJsonName()); - if (handler == null) { - Grasscutter.getLogger() - .warn( - "Could not found the handler of this type of Chests {}", - getGadget().getGadgetData().getJsonName()); - return false; - } - - if (req.getOpType() == InterOpType.INTER_OP_TYPE_START && handler.isTwoStep()) { - player.sendPacket( - new PacketGadgetInteractRsp( - getGadget(), InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_START)); - return false; - } else { - boolean success; - if (handler instanceof BossChestInteractHandler bossChestInteractHandler) { - success = - bossChestInteractHandler.onInteract( - this, - player, - req.getResinCostType() - == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE); - } else { - success = handler.onInteract(this, player); - } - if (!success) { - return false; - } - - getGadget().updateState(ScriptGadgetState.ChestOpened); - player.sendPacket( - new PacketGadgetInteractRsp( - this.getGadget(), InteractTypeOuterClass.InteractType.INTERACT_TYPE_OPEN_CHEST)); - - return true; - } - } - - public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) { - if (getGadget().getMetaGadget() == null) { - return; - } - - var bossChest = getGadget().getMetaGadget().boss_chest; - if (bossChest != null) { - var players = getGadget().getScene().getPlayers().stream().map(Player::getUid).toList(); - - gadgetInfo.setBossChest( - BossChestInfo.newBuilder() - .setMonsterConfigId(bossChest.monster_config_id) - .setResin(bossChest.resin) - .addAllQualifyUidList(players) - .addAllRemainUidList(players) - .build()); - } - } -} +package emu.grasscutter.game.entity.gadget; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.gadget.chest.BossChestInteractHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.proto.BossChestInfoOuterClass.BossChestInfo; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.InterOpTypeOuterClass.InterOpType; +import emu.grasscutter.net.proto.InteractTypeOuterClass; +import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; +import emu.grasscutter.net.proto.ResinCostTypeOuterClass; +import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; +import emu.grasscutter.scripts.constants.ScriptGadgetState; +import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; + +public class GadgetChest extends GadgetContent { + + public GadgetChest(EntityGadget gadget) { + super(gadget); + } + + public boolean onInteract(Player player, GadgetInteractReq req) { + var chestInteractHandlerMap = + getGadget() + .getScene() + .getWorld() + .getServer() + .getWorldDataSystem() + .getChestInteractHandlerMap(); + var handler = chestInteractHandlerMap.get(getGadget().getGadgetData().getJsonName()); + if (handler == null) { + Grasscutter.getLogger() + .warn( + "Could not found the handler of this type of Chests {}", + getGadget().getGadgetData().getJsonName()); + return false; + } + + if (req.getOpType() == InterOpType.INTER_OP_TYPE_START && handler.isTwoStep()) { + player.sendPacket( + new PacketGadgetInteractRsp( + getGadget(), InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_START)); + return false; + } else { + boolean success; + if (handler instanceof BossChestInteractHandler bossChestInteractHandler) { + success = + bossChestInteractHandler.onInteract( + this, + player, + req.getResinCostType() + == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE); + } else { + success = handler.onInteract(this, player); + } + if (!success) { + return false; + } + + getGadget().updateState(ScriptGadgetState.ChestOpened); + player.sendPacket( + new PacketGadgetInteractRsp( + this.getGadget(), InteractTypeOuterClass.InteractType.INTERACT_TYPE_OPEN_CHEST)); + + return true; + } + } + + public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) { + if (getGadget().getMetaGadget() == null) { + return; + } + + var bossChest = getGadget().getMetaGadget().boss_chest; + if (bossChest != null) { + var players = getGadget().getScene().getPlayers().stream().map(Player::getUid).toList(); + + gadgetInfo.setBossChest( + BossChestInfo.newBuilder() + .setMonsterConfigId(bossChest.monster_config_id) + .setResin(bossChest.resin) + .addAllQualifyUidList(players) + .addAllRemainUidList(players) + .build()); + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/JlineLogbackAppender.java b/src/main/java/emu/grasscutter/utils/JlineLogbackAppender.java index d841cf6c2..39bc2da3b 100644 --- a/src/main/java/emu/grasscutter/utils/JlineLogbackAppender.java +++ b/src/main/java/emu/grasscutter/utils/JlineLogbackAppender.java @@ -1,17 +1,17 @@ -package emu.grasscutter.utils; - -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.ConsoleAppender; -import emu.grasscutter.Grasscutter; -import java.util.Arrays; - -public class JlineLogbackAppender extends ConsoleAppender { - @Override - protected void append(ILoggingEvent eventObject) { - if (!started) { - return; - } - Arrays.stream(new String(encoder.encode(eventObject)).split("\n\r")) - .forEach(Grasscutter.getConsole()::printAbove); - } -} +package emu.grasscutter.utils; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.ConsoleAppender; +import emu.grasscutter.Grasscutter; +import java.util.Arrays; + +public class JlineLogbackAppender extends ConsoleAppender { + @Override + protected void append(ILoggingEvent eventObject) { + if (!started) { + return; + } + Arrays.stream(new String(encoder.encode(eventObject)).split("\n\r")) + .forEach(Grasscutter.getConsole()::printAbove); + } +} diff --git a/src/main/java/emu/grasscutter/utils/JsonAdapters.java b/src/main/java/emu/grasscutter/utils/JsonAdapters.java index ed7f504c0..091f644b6 100644 --- a/src/main/java/emu/grasscutter/utils/JsonAdapters.java +++ b/src/main/java/emu/grasscutter/utils/JsonAdapters.java @@ -1,171 +1,171 @@ -package emu.grasscutter.utils; - -import com.google.gson.Gson; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import emu.grasscutter.data.common.DynamicFloat; -import it.unimi.dsi.fastutil.floats.FloatArrayList; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Objects; -import lombok.val; - -public class JsonAdapters { - static class DynamicFloatAdapter extends TypeAdapter { - @Override - public DynamicFloat read(JsonReader reader) throws IOException { - switch (reader.peek()) { - case STRING: - return new DynamicFloat(reader.nextString()); - case NUMBER: - return new DynamicFloat((float) reader.nextDouble()); - case BOOLEAN: - return new DynamicFloat(reader.nextBoolean()); - case BEGIN_ARRAY: - reader.beginArray(); - val opStack = new ArrayList(); - while (reader.hasNext()) { - opStack.add( - switch (reader.peek()) { - case STRING -> new DynamicFloat.StackOp(reader.nextString()); - case NUMBER -> new DynamicFloat.StackOp((float) reader.nextDouble()); - case BOOLEAN -> new DynamicFloat.StackOp(reader.nextBoolean()); - default -> throw new IOException( - "Invalid DynamicFloat definition - " + reader.peek().name()); - }); - } - reader.endArray(); - return new DynamicFloat(opStack); - default: - throw new IOException("Invalid DynamicFloat definition - " + reader.peek().name()); - } - } - - @Override - public void write(JsonWriter writer, DynamicFloat f) {} - } - - static class IntListAdapter extends TypeAdapter { - @Override - public IntList read(JsonReader reader) throws IOException { - if (Objects.requireNonNull(reader.peek()) == JsonToken.BEGIN_ARRAY) { - reader.beginArray(); - val i = new IntArrayList(); - while (reader.hasNext()) i.add(reader.nextInt()); - reader.endArray(); - i.trim(); // We might have a ton of these from resources and almost all of them - // immutable, don't overprovision! - return i; - } - throw new IOException("Invalid IntList definition - " + reader.peek().name()); - } - - @Override - public void write(JsonWriter writer, IntList l) throws IOException { - writer.beginArray(); - for (val i : l) // .forEach() doesn't appreciate exceptions - writer.value(i); - writer.endArray(); - } - } - - static class PositionAdapter extends TypeAdapter { - @Override - public Position read(JsonReader reader) throws IOException { - switch (reader.peek()) { - case BEGIN_ARRAY: // "pos": [x,y,z] - reader.beginArray(); - val array = new FloatArrayList(3); - while (reader.hasNext()) array.add(reader.nextInt()); - reader.endArray(); - return new Position(array); - case BEGIN_OBJECT: // "pos": {"x": x, "y": y, "z": z} - float x = 0f; - float y = 0f; - float z = 0f; - reader.beginObject(); - for (var next = reader.peek(); next != JsonToken.END_OBJECT; next = reader.peek()) { - val name = reader.nextName(); - switch (name) { - case "x", "X", "_x" -> x = (float) reader.nextDouble(); - case "y", "Y", "_y" -> y = (float) reader.nextDouble(); - case "z", "Z", "_z" -> z = (float) reader.nextDouble(); - default -> throw new IOException("Invalid field in Position definition - " + name); - } - } - reader.endObject(); - return new Position(x, y, z); - default: - throw new IOException("Invalid Position definition - " + reader.peek().name()); - } - } - - @Override - public void write(JsonWriter writer, Position i) throws IOException { - writer.beginArray(); - writer.value(i.getX()); - writer.value(i.getY()); - writer.value(i.getZ()); - writer.endArray(); - } - } - - static class EnumTypeAdapterFactory implements TypeAdapterFactory { - @SuppressWarnings("unchecked") - public TypeAdapter create(Gson gson, TypeToken type) { - Class enumClass = (Class) type.getRawType(); - if (!enumClass.isEnum()) return null; - - // Make mappings of (string) names to enum constants - val map = new HashMap(); - val enumConstants = enumClass.getEnumConstants(); - for (val constant : enumConstants) map.put(constant.toString(), constant); - - // If the enum also has a numeric value, map those to the constants too - // System.out.println("Looking for enum value field"); - for (Field f : enumClass.getDeclaredFields()) { - if (switch (f.getName()) { - case "value", "id" -> true; - default -> false; - }) { - // System.out.println("Enum value field found - " + f.getName()); - boolean acc = f.isAccessible(); - f.setAccessible(true); - try { - for (val constant : enumConstants) - map.put(String.valueOf(f.getInt(constant)), constant); - } catch (IllegalAccessException e) { - // System.out.println("Failed to access enum id field."); - } - f.setAccessible(acc); - break; - } - } - - return new TypeAdapter() { - public T read(JsonReader reader) throws IOException { - switch (reader.peek()) { - case STRING: - return map.get(reader.nextString()); - case NUMBER: - return map.get(String.valueOf(reader.nextInt())); - default: - throw new IOException("Invalid Enum definition - " + reader.peek().name()); - } - } - - public void write(JsonWriter writer, T value) throws IOException { - writer.value(value.toString()); - } - }; - } - } -} +package emu.grasscutter.utils; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import emu.grasscutter.data.common.DynamicFloat; +import it.unimi.dsi.fastutil.floats.FloatArrayList; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Objects; +import lombok.val; + +public class JsonAdapters { + static class DynamicFloatAdapter extends TypeAdapter { + @Override + public DynamicFloat read(JsonReader reader) throws IOException { + switch (reader.peek()) { + case STRING: + return new DynamicFloat(reader.nextString()); + case NUMBER: + return new DynamicFloat((float) reader.nextDouble()); + case BOOLEAN: + return new DynamicFloat(reader.nextBoolean()); + case BEGIN_ARRAY: + reader.beginArray(); + val opStack = new ArrayList(); + while (reader.hasNext()) { + opStack.add( + switch (reader.peek()) { + case STRING -> new DynamicFloat.StackOp(reader.nextString()); + case NUMBER -> new DynamicFloat.StackOp((float) reader.nextDouble()); + case BOOLEAN -> new DynamicFloat.StackOp(reader.nextBoolean()); + default -> throw new IOException( + "Invalid DynamicFloat definition - " + reader.peek().name()); + }); + } + reader.endArray(); + return new DynamicFloat(opStack); + default: + throw new IOException("Invalid DynamicFloat definition - " + reader.peek().name()); + } + } + + @Override + public void write(JsonWriter writer, DynamicFloat f) {} + } + + static class IntListAdapter extends TypeAdapter { + @Override + public IntList read(JsonReader reader) throws IOException { + if (Objects.requireNonNull(reader.peek()) == JsonToken.BEGIN_ARRAY) { + reader.beginArray(); + val i = new IntArrayList(); + while (reader.hasNext()) i.add(reader.nextInt()); + reader.endArray(); + i.trim(); // We might have a ton of these from resources and almost all of them + // immutable, don't overprovision! + return i; + } + throw new IOException("Invalid IntList definition - " + reader.peek().name()); + } + + @Override + public void write(JsonWriter writer, IntList l) throws IOException { + writer.beginArray(); + for (val i : l) // .forEach() doesn't appreciate exceptions + writer.value(i); + writer.endArray(); + } + } + + static class PositionAdapter extends TypeAdapter { + @Override + public Position read(JsonReader reader) throws IOException { + switch (reader.peek()) { + case BEGIN_ARRAY: // "pos": [x,y,z] + reader.beginArray(); + val array = new FloatArrayList(3); + while (reader.hasNext()) array.add(reader.nextInt()); + reader.endArray(); + return new Position(array); + case BEGIN_OBJECT: // "pos": {"x": x, "y": y, "z": z} + float x = 0f; + float y = 0f; + float z = 0f; + reader.beginObject(); + for (var next = reader.peek(); next != JsonToken.END_OBJECT; next = reader.peek()) { + val name = reader.nextName(); + switch (name) { + case "x", "X", "_x" -> x = (float) reader.nextDouble(); + case "y", "Y", "_y" -> y = (float) reader.nextDouble(); + case "z", "Z", "_z" -> z = (float) reader.nextDouble(); + default -> throw new IOException("Invalid field in Position definition - " + name); + } + } + reader.endObject(); + return new Position(x, y, z); + default: + throw new IOException("Invalid Position definition - " + reader.peek().name()); + } + } + + @Override + public void write(JsonWriter writer, Position i) throws IOException { + writer.beginArray(); + writer.value(i.getX()); + writer.value(i.getY()); + writer.value(i.getZ()); + writer.endArray(); + } + } + + static class EnumTypeAdapterFactory implements TypeAdapterFactory { + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, TypeToken type) { + Class enumClass = (Class) type.getRawType(); + if (!enumClass.isEnum()) return null; + + // Make mappings of (string) names to enum constants + val map = new HashMap(); + val enumConstants = enumClass.getEnumConstants(); + for (val constant : enumConstants) map.put(constant.toString(), constant); + + // If the enum also has a numeric value, map those to the constants too + // System.out.println("Looking for enum value field"); + for (Field f : enumClass.getDeclaredFields()) { + if (switch (f.getName()) { + case "value", "id" -> true; + default -> false; + }) { + // System.out.println("Enum value field found - " + f.getName()); + boolean acc = f.isAccessible(); + f.setAccessible(true); + try { + for (val constant : enumConstants) + map.put(String.valueOf(f.getInt(constant)), constant); + } catch (IllegalAccessException e) { + // System.out.println("Failed to access enum id field."); + } + f.setAccessible(acc); + break; + } + } + + return new TypeAdapter() { + public T read(JsonReader reader) throws IOException { + switch (reader.peek()) { + case STRING: + return map.get(reader.nextString()); + case NUMBER: + return map.get(String.valueOf(reader.nextInt())); + default: + throw new IOException("Invalid Enum definition - " + reader.peek().name()); + } + } + + public void write(JsonWriter writer, T value) throws IOException { + writer.value(value.toString()); + } + }; + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/JsonUtils.java b/src/main/java/emu/grasscutter/utils/JsonUtils.java index 64b43abca..457d89299 100644 --- a/src/main/java/emu/grasscutter/utils/JsonUtils.java +++ b/src/main/java/emu/grasscutter/utils/JsonUtils.java @@ -1,129 +1,129 @@ -package emu.grasscutter.utils; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonSyntaxException; -import com.google.gson.reflect.TypeToken; -import emu.grasscutter.data.common.DynamicFloat; -import emu.grasscutter.utils.JsonAdapters.DynamicFloatAdapter; -import emu.grasscutter.utils.JsonAdapters.EnumTypeAdapterFactory; -import emu.grasscutter.utils.JsonAdapters.IntListAdapter; -import emu.grasscutter.utils.JsonAdapters.PositionAdapter; -import it.unimi.dsi.fastutil.ints.IntList; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; - -public final class JsonUtils { - static final Gson gson = - new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(DynamicFloat.class, new DynamicFloatAdapter()) - .registerTypeAdapter(IntList.class, new IntListAdapter()) - .registerTypeAdapter(Position.class, new PositionAdapter()) - .registerTypeAdapterFactory(new EnumTypeAdapterFactory()) - .create(); - - /* - * Encode an object to a JSON string - */ - public static String encode(Object object) { - return gson.toJson(object); - } - - public static T decode(JsonElement jsonElement, Class classType) - throws JsonSyntaxException { - return gson.fromJson(jsonElement, classType); - } - - public static T loadToClass(Reader fileReader, Class classType) throws IOException { - return gson.fromJson(fileReader, classType); - } - - @Deprecated(forRemoval = true) - public static T loadToClass(String filename, Class classType) throws IOException { - try (InputStreamReader fileReader = - new InputStreamReader( - new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) { - return loadToClass(fileReader, classType); - } - } - - public static T loadToClass(Path filename, Class classType) throws IOException { - try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { - return loadToClass(fileReader, classType); - } - } - - public static List loadToList(Reader fileReader, Class classType) throws IOException { - return gson.fromJson(fileReader, TypeToken.getParameterized(List.class, classType).getType()); - } - - @Deprecated(forRemoval = true) - public static List loadToList(String filename, Class classType) throws IOException { - try (InputStreamReader fileReader = - new InputStreamReader( - new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) { - return loadToList(fileReader, classType); - } - } - - public static List loadToList(Path filename, Class classType) throws IOException { - try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { - return loadToList(fileReader, classType); - } - } - - public static Map loadToMap( - Reader fileReader, Class keyType, Class valueType) throws IOException { - return gson.fromJson( - fileReader, TypeToken.getParameterized(Map.class, keyType, valueType).getType()); - } - - @Deprecated(forRemoval = true) - public static Map loadToMap( - String filename, Class keyType, Class valueType) throws IOException { - try (InputStreamReader fileReader = - new InputStreamReader( - new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) { - return loadToMap(fileReader, keyType, valueType); - } - } - - public static Map loadToMap( - Path filename, Class keyType, Class valueType) throws IOException { - try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { - return loadToMap(fileReader, keyType, valueType); - } - } - - /** - * 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 decode(String jsonData, Class classType) { - try { - return gson.fromJson(jsonData, classType); - } catch (Exception ignored) { - return null; - } - } - - public static T decode(String jsonData, Type type) { - try { - return gson.fromJson(jsonData, type); - } catch (Exception ignored) { - return null; - } - } -} +package emu.grasscutter.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import emu.grasscutter.data.common.DynamicFloat; +import emu.grasscutter.utils.JsonAdapters.DynamicFloatAdapter; +import emu.grasscutter.utils.JsonAdapters.EnumTypeAdapterFactory; +import emu.grasscutter.utils.JsonAdapters.IntListAdapter; +import emu.grasscutter.utils.JsonAdapters.PositionAdapter; +import it.unimi.dsi.fastutil.ints.IntList; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +public final class JsonUtils { + static final Gson gson = + new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(DynamicFloat.class, new DynamicFloatAdapter()) + .registerTypeAdapter(IntList.class, new IntListAdapter()) + .registerTypeAdapter(Position.class, new PositionAdapter()) + .registerTypeAdapterFactory(new EnumTypeAdapterFactory()) + .create(); + + /* + * Encode an object to a JSON string + */ + public static String encode(Object object) { + return gson.toJson(object); + } + + public static T decode(JsonElement jsonElement, Class classType) + throws JsonSyntaxException { + return gson.fromJson(jsonElement, classType); + } + + public static T loadToClass(Reader fileReader, Class classType) throws IOException { + return gson.fromJson(fileReader, classType); + } + + @Deprecated(forRemoval = true) + public static T loadToClass(String filename, Class classType) throws IOException { + try (InputStreamReader fileReader = + new InputStreamReader( + new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) { + return loadToClass(fileReader, classType); + } + } + + public static T loadToClass(Path filename, Class classType) throws IOException { + try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { + return loadToClass(fileReader, classType); + } + } + + public static List loadToList(Reader fileReader, Class classType) throws IOException { + return gson.fromJson(fileReader, TypeToken.getParameterized(List.class, classType).getType()); + } + + @Deprecated(forRemoval = true) + public static List loadToList(String filename, Class classType) throws IOException { + try (InputStreamReader fileReader = + new InputStreamReader( + new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) { + return loadToList(fileReader, classType); + } + } + + public static List loadToList(Path filename, Class classType) throws IOException { + try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { + return loadToList(fileReader, classType); + } + } + + public static Map loadToMap( + Reader fileReader, Class keyType, Class valueType) throws IOException { + return gson.fromJson( + fileReader, TypeToken.getParameterized(Map.class, keyType, valueType).getType()); + } + + @Deprecated(forRemoval = true) + public static Map loadToMap( + String filename, Class keyType, Class valueType) throws IOException { + try (InputStreamReader fileReader = + new InputStreamReader( + new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) { + return loadToMap(fileReader, keyType, valueType); + } + } + + public static Map loadToMap( + Path filename, Class keyType, Class valueType) throws IOException { + try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { + return loadToMap(fileReader, keyType, valueType); + } + } + + /** + * 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 decode(String jsonData, Class classType) { + try { + return gson.fromJson(jsonData, classType); + } catch (Exception ignored) { + return null; + } + } + + public static T decode(String jsonData, Type type) { + try { + return gson.fromJson(jsonData, type); + } catch (Exception ignored) { + return null; + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/Location.java b/src/main/java/emu/grasscutter/utils/Location.java index 00718bf9c..829b7ed27 100644 --- a/src/main/java/emu/grasscutter/utils/Location.java +++ b/src/main/java/emu/grasscutter/utils/Location.java @@ -1,40 +1,40 @@ -package emu.grasscutter.utils; - -import dev.morphia.annotations.Entity; -import dev.morphia.annotations.Transient; -import emu.grasscutter.game.world.Scene; -import lombok.Getter; -import lombok.Setter; - -@Entity -public class Location extends Position { - @Transient @Getter @Setter private Scene scene; - - public Location(Scene scene, Position position) { - this.set(position); - - this.scene = scene; - } - - public Location(Scene scene, float x, float y) { - this.set(x, y); - - this.scene = scene; - } - - public Location(Scene scene, float x, float y, float z) { - this.set(x, y, z); - - this.scene = scene; - } - - @Override - public Location clone() { - return new Location(this.scene, super.clone()); - } - - @Override - public String toString() { - return String.format("%s:%s,%s,%s", this.scene.getId(), this.getX(), this.getY(), this.getZ()); - } -} +package emu.grasscutter.utils; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Transient; +import emu.grasscutter.game.world.Scene; +import lombok.Getter; +import lombok.Setter; + +@Entity +public class Location extends Position { + @Transient @Getter @Setter private Scene scene; + + public Location(Scene scene, Position position) { + this.set(position); + + this.scene = scene; + } + + public Location(Scene scene, float x, float y) { + this.set(x, y); + + this.scene = scene; + } + + public Location(Scene scene, float x, float y, float z) { + this.set(x, y, z); + + this.scene = scene; + } + + @Override + public Location clone() { + return new Location(this.scene, super.clone()); + } + + @Override + public String toString() { + return String.format("%s:%s,%s,%s", this.scene.getId(), this.getX(), this.getY(), this.getZ()); + } +} diff --git a/src/main/java/emu/grasscutter/utils/MessageHandler.java b/src/main/java/emu/grasscutter/utils/MessageHandler.java index db4e9f527..19c795731 100644 --- a/src/main/java/emu/grasscutter/utils/MessageHandler.java +++ b/src/main/java/emu/grasscutter/utils/MessageHandler.java @@ -1,21 +1,21 @@ -package emu.grasscutter.utils; - -public class MessageHandler { - private String message; - - public MessageHandler() { - this.message = ""; - } - - public void append(String message) { - this.message += message + "\r\n\r\n"; - } - - public String getMessage() { - return this.message; - } - - public void setMessage(String message) { - this.message = message; - } -} +package emu.grasscutter.utils; + +public class MessageHandler { + private String message; + + public MessageHandler() { + this.message = ""; + } + + public void append(String message) { + this.message += message + "\r\n\r\n"; + } + + public String getMessage() { + return this.message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/emu/grasscutter/utils/Position.java b/src/main/java/emu/grasscutter/utils/Position.java index fe2a6c42f..c143c0e39 100644 --- a/src/main/java/emu/grasscutter/utils/Position.java +++ b/src/main/java/emu/grasscutter/utils/Position.java @@ -1,196 +1,196 @@ -package emu.grasscutter.utils; - -import com.github.davidmoten.rtreemulti.geometry.Point; -import com.google.gson.annotations.SerializedName; -import dev.morphia.annotations.Entity; -import emu.grasscutter.net.proto.VectorOuterClass.Vector; -import java.io.Serializable; -import java.util.List; -import lombok.Getter; -import lombok.Setter; - -@Entity -public class Position implements Serializable { - private static final long serialVersionUID = -2001232313615923575L; - - @SerializedName( - value = "x", - alternate = {"_x", "X"}) - @Getter - @Setter - private float x; - - @SerializedName( - value = "y", - alternate = {"_y", "Y"}) - @Getter - @Setter - private float y; - - @SerializedName( - value = "z", - alternate = {"_z", "Z"}) - @Getter - @Setter - private float z; - - public Position() {} - - public Position(float x, float y) { - set(x, y); - } - - public Position(float x, float y, float z) { - set(x, y, z); - } - - public Position(List xyz) { - switch (xyz.size()) { - default: // Might want to error on excess elements, but maybe we want to extend to 3+3 - // representation later. - case 3: - this.z = xyz.get(2); // Fall-through - case 2: - this.y = xyz.get(1); // Fall-through - case 1: - this.y = xyz.get(0); // pointless fall-through - case 0: - break; - } - } - - public Position(String p) { - String[] split = p.split(","); - if (split.length >= 2) { - this.x = Float.parseFloat(split[0]); - this.y = Float.parseFloat(split[1]); - } - if (split.length >= 3) { - this.z = Float.parseFloat(split[2]); - } - } - - public Position(Vector vector) { - this.set(vector); - } - - public Position(Position pos) { - this.set(pos); - } - - public Position set(float x, float y) { - this.x = x; - this.y = y; - return this; - } - - // Deep copy - public Position set(Position pos) { - return this.set(pos.getX(), pos.getY(), pos.getZ()); - } - - public Position set(Vector pos) { - return this.set(pos.getX(), pos.getY(), pos.getZ()); - } - - public Position set(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - return this; - } - - public Position add(Position add) { - this.x += add.getX(); - this.y += add.getY(); - this.z += add.getZ(); - return this; - } - - public Position addX(float d) { - this.x += d; - return this; - } - - public Position addY(float d) { - this.y += d; - return this; - } - - public Position addZ(float d) { - this.z += d; - return this; - } - - public Position subtract(Position sub) { - this.x -= sub.getX(); - this.y -= sub.getY(); - this.z -= sub.getZ(); - return this; - } - - /** In radians */ - public Position translate(float dist, float angle) { - this.x += dist * Math.sin(angle); - this.y += dist * Math.cos(angle); - return this; - } - - public boolean equal2d(Position other) { - // Y is height - return getX() == other.getX() && getZ() == other.getZ(); - } - - public boolean equal3d(Position other) { - return getX() == other.getX() && getY() == other.getY() && getZ() == other.getZ(); - } - - public double computeDistance(Position b) { - double detX = getX() - b.getX(); - double detY = getY() - b.getY(); - double detZ = getZ() - b.getZ(); - return Math.sqrt(detX * detX + detY * detY + detZ * detZ); - } - - public Position nearby2d(float range) { - Position position = clone(); - position.z += Utils.randomFloatRange(-range, range); - position.x += Utils.randomFloatRange(-range, range); - return position; - } - - public Position translateWithDegrees(float dist, float angle) { - angle = (float) Math.toRadians(angle); - this.x += dist * Math.sin(angle); - this.y += -dist * Math.cos(angle); - return this; - } - - @Override - public Position clone() { - return new Position(x, y, z); - } - - @Override - public String toString() { - return "(" + this.getX() + ", " + this.getY() + ", " + this.getZ() + ")"; - } - - public Vector toProto() { - return Vector.newBuilder().setX(this.getX()).setY(this.getY()).setZ(this.getZ()).build(); - } - - public Point toPoint() { - return Point.create(x, y, z); - } - - /** To XYZ array for Spatial Index */ - public double[] toDoubleArray() { - return new double[] {x, y, z}; - } - - /** To XZ array for Spatial Index (Blocks) */ - public double[] toXZDoubleArray() { - return new double[] {x, z}; - } -} +package emu.grasscutter.utils; + +import com.github.davidmoten.rtreemulti.geometry.Point; +import com.google.gson.annotations.SerializedName; +import dev.morphia.annotations.Entity; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import java.io.Serializable; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Entity +public class Position implements Serializable { + private static final long serialVersionUID = -2001232313615923575L; + + @SerializedName( + value = "x", + alternate = {"_x", "X"}) + @Getter + @Setter + private float x; + + @SerializedName( + value = "y", + alternate = {"_y", "Y"}) + @Getter + @Setter + private float y; + + @SerializedName( + value = "z", + alternate = {"_z", "Z"}) + @Getter + @Setter + private float z; + + public Position() {} + + public Position(float x, float y) { + set(x, y); + } + + public Position(float x, float y, float z) { + set(x, y, z); + } + + public Position(List xyz) { + switch (xyz.size()) { + default: // Might want to error on excess elements, but maybe we want to extend to 3+3 + // representation later. + case 3: + this.z = xyz.get(2); // Fall-through + case 2: + this.y = xyz.get(1); // Fall-through + case 1: + this.y = xyz.get(0); // pointless fall-through + case 0: + break; + } + } + + public Position(String p) { + String[] split = p.split(","); + if (split.length >= 2) { + this.x = Float.parseFloat(split[0]); + this.y = Float.parseFloat(split[1]); + } + if (split.length >= 3) { + this.z = Float.parseFloat(split[2]); + } + } + + public Position(Vector vector) { + this.set(vector); + } + + public Position(Position pos) { + this.set(pos); + } + + public Position set(float x, float y) { + this.x = x; + this.y = y; + return this; + } + + // Deep copy + public Position set(Position pos) { + return this.set(pos.getX(), pos.getY(), pos.getZ()); + } + + public Position set(Vector pos) { + return this.set(pos.getX(), pos.getY(), pos.getZ()); + } + + public Position set(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public Position add(Position add) { + this.x += add.getX(); + this.y += add.getY(); + this.z += add.getZ(); + return this; + } + + public Position addX(float d) { + this.x += d; + return this; + } + + public Position addY(float d) { + this.y += d; + return this; + } + + public Position addZ(float d) { + this.z += d; + return this; + } + + public Position subtract(Position sub) { + this.x -= sub.getX(); + this.y -= sub.getY(); + this.z -= sub.getZ(); + return this; + } + + /** In radians */ + public Position translate(float dist, float angle) { + this.x += dist * Math.sin(angle); + this.y += dist * Math.cos(angle); + return this; + } + + public boolean equal2d(Position other) { + // Y is height + return getX() == other.getX() && getZ() == other.getZ(); + } + + public boolean equal3d(Position other) { + return getX() == other.getX() && getY() == other.getY() && getZ() == other.getZ(); + } + + public double computeDistance(Position b) { + double detX = getX() - b.getX(); + double detY = getY() - b.getY(); + double detZ = getZ() - b.getZ(); + return Math.sqrt(detX * detX + detY * detY + detZ * detZ); + } + + public Position nearby2d(float range) { + Position position = clone(); + position.z += Utils.randomFloatRange(-range, range); + position.x += Utils.randomFloatRange(-range, range); + return position; + } + + public Position translateWithDegrees(float dist, float angle) { + angle = (float) Math.toRadians(angle); + this.x += dist * Math.sin(angle); + this.y += -dist * Math.cos(angle); + return this; + } + + @Override + public Position clone() { + return new Position(x, y, z); + } + + @Override + public String toString() { + return "(" + this.getX() + ", " + this.getY() + ", " + this.getZ() + ")"; + } + + public Vector toProto() { + return Vector.newBuilder().setX(this.getX()).setY(this.getY()).setZ(this.getZ()).build(); + } + + public Point toPoint() { + return Point.create(x, y, z); + } + + /** To XYZ array for Spatial Index */ + public double[] toDoubleArray() { + return new double[] {x, y, z}; + } + + /** To XZ array for Spatial Index (Blocks) */ + public double[] toXZDoubleArray() { + return new double[] {x, z}; + } +} diff --git a/src/main/java/emu/grasscutter/utils/ProtoHelper.java b/src/main/java/emu/grasscutter/utils/ProtoHelper.java index 9ecd76ac6..9a79161fd 100644 --- a/src/main/java/emu/grasscutter/utils/ProtoHelper.java +++ b/src/main/java/emu/grasscutter/utils/ProtoHelper.java @@ -1,10 +1,10 @@ -package emu.grasscutter.utils; - -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.net.proto.PropValueOuterClass.PropValue; - -public final class ProtoHelper { - public static PropValue newPropValue(PlayerProperty key, int value) { - return PropValue.newBuilder().setType(key.getId()).setIval(value).setVal(value).build(); - } -} +package emu.grasscutter.utils; + +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.net.proto.PropValueOuterClass.PropValue; + +public final class ProtoHelper { + public static PropValue newPropValue(PlayerProperty key, int value) { + return PropValue.newBuilder().setType(key.getId()).setIval(value).setVal(value).build(); + } +} diff --git a/src/main/java/emu/grasscutter/utils/ServerLogEventAppender.java b/src/main/java/emu/grasscutter/utils/ServerLogEventAppender.java index 7c0287faa..5dc35df8d 100644 --- a/src/main/java/emu/grasscutter/utils/ServerLogEventAppender.java +++ b/src/main/java/emu/grasscutter/utils/ServerLogEventAppender.java @@ -1,27 +1,27 @@ -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 emu.grasscutter.server.event.internal.ServerLogEvent; -import java.nio.charset.StandardCharsets; - -public class ServerLogEventAppender extends AppenderBase { - protected Encoder encoder; - - @Override - protected void append(E event) { - byte[] byteArray = this.encoder.encode(event); - ServerLogEvent sle = - new ServerLogEvent((ILoggingEvent) event, new String(byteArray, StandardCharsets.UTF_8)); - sle.call(); - } - - public Encoder getEncoder() { - return this.encoder; - } - - public void setEncoder(Encoder encoder) { - this.encoder = encoder; - } -} +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 emu.grasscutter.server.event.internal.ServerLogEvent; +import java.nio.charset.StandardCharsets; + +public class ServerLogEventAppender extends AppenderBase { + protected Encoder encoder; + + @Override + protected void append(E event) { + byte[] byteArray = this.encoder.encode(event); + ServerLogEvent sle = + new ServerLogEvent((ILoggingEvent) event, new String(byteArray, StandardCharsets.UTF_8)); + sle.call(); + } + + public Encoder getEncoder() { + return this.encoder; + } + + public void setEncoder(Encoder encoder) { + this.encoder = encoder; + } +} diff --git a/src/main/java/emu/grasscutter/utils/SparseSet.java b/src/main/java/emu/grasscutter/utils/SparseSet.java index be536b121..4406845a4 100644 --- a/src/main/java/emu/grasscutter/utils/SparseSet.java +++ b/src/main/java/emu/grasscutter/utils/SparseSet.java @@ -1,67 +1,67 @@ -package emu.grasscutter.utils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -public final class SparseSet { - private final List rangeEntries; - private final Set denseEntries; - - public SparseSet(String csv) { - this.rangeEntries = new ArrayList<>(); - this.denseEntries = new TreeSet<>(); - - for (String token : csv.replace("\n", "").replace(" ", "").split(",")) { - String[] tokens = token.split("-"); - switch (tokens.length) { - case 1: - this.denseEntries.add(Integer.parseInt(tokens[0])); - break; - case 2: - this.rangeEntries.add( - new Range(Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1]))); - break; - default: - throw new IllegalArgumentException( - "Invalid token passed to SparseSet initializer - " - + token - + " (split length " - + tokens.length - + ")"); - } - } - } - - public boolean contains(int i) { - for (Range range : this.rangeEntries) { - if (range.check(i)) { - return true; - } - } - return this.denseEntries.contains(i); - } - - /* - * A convenience class for constructing integer sets out of large ranges - * Designed to be fed literal strings from this project only - - * can and will throw exceptions to tell you to fix your code if you feed it garbage. :) - */ - private static class Range { - private final int min, max; - - public Range(int min, int max) { - if (min > max) { - throw new IllegalArgumentException( - "Range passed minimum higher than maximum - " + min + " > " + max); - } - this.min = min; - this.max = max; - } - - public boolean check(int value) { - return value >= this.min && value <= this.max; - } - } -} +package emu.grasscutter.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +public final class SparseSet { + private final List rangeEntries; + private final Set denseEntries; + + public SparseSet(String csv) { + this.rangeEntries = new ArrayList<>(); + this.denseEntries = new TreeSet<>(); + + for (String token : csv.replace("\n", "").replace(" ", "").split(",")) { + String[] tokens = token.split("-"); + switch (tokens.length) { + case 1: + this.denseEntries.add(Integer.parseInt(tokens[0])); + break; + case 2: + this.rangeEntries.add( + new Range(Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1]))); + break; + default: + throw new IllegalArgumentException( + "Invalid token passed to SparseSet initializer - " + + token + + " (split length " + + tokens.length + + ")"); + } + } + } + + public boolean contains(int i) { + for (Range range : this.rangeEntries) { + if (range.check(i)) { + return true; + } + } + return this.denseEntries.contains(i); + } + + /* + * A convenience class for constructing integer sets out of large ranges + * Designed to be fed literal strings from this project only - + * can and will throw exceptions to tell you to fix your code if you feed it garbage. :) + */ + private static class Range { + private final int min, max; + + public Range(int min, int max) { + if (min > max) { + throw new IllegalArgumentException( + "Range passed minimum higher than maximum - " + min + " > " + max); + } + this.min = min; + this.max = max; + } + + public boolean check(int value) { + return value >= this.min && value <= this.max; + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/TsvUtils.java b/src/main/java/emu/grasscutter/utils/TsvUtils.java index 4c0c96fc5..4212ecb98 100644 --- a/src/main/java/emu/grasscutter/utils/TsvUtils.java +++ b/src/main/java/emu/grasscutter/utils/TsvUtils.java @@ -1,682 +1,682 @@ -package emu.grasscutter.utils; - -import static emu.grasscutter.utils.Utils.nonRegexSplit; - -import com.google.gson.*; -import com.google.gson.annotations.SerializedName; -import emu.grasscutter.Grasscutter; -import it.unimi.dsi.fastutil.Pair; -import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap; -import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; -import java.io.IOException; -import java.lang.reflect.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Function; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import lombok.val; - -// Throughout this file, commented System.out.println debug log calls are left in. -// This is because the default logger will deadlock when operating on parallel streams. -public class TsvUtils { - private static final Map defaultValues = - Map.ofEntries( - // Map.entry(String.class, null), // builder hates null values - Map.entry(Integer.class, 0), - Map.entry(int.class, 0), - Map.entry(Long.class, 0L), - Map.entry(long.class, 0L), - Map.entry(Float.class, 0f), - Map.entry(float.class, 0f), - Map.entry(Double.class, 0d), - Map.entry(double.class, 0d), - Map.entry(Boolean.class, false), - Map.entry(boolean.class, false)); - private static final Set primitiveTypes = - Set.of( - String.class, - Integer.class, - int.class, - Long.class, - long.class, - Float.class, - float.class, - Double.class, - double.class, - Boolean.class, - boolean.class); - - private static final Function parseString = value -> value; - private static final Function parseInt = - value -> (int) Double.parseDouble(value); // Integer::parseInt; - private static final Function parseLong = - value -> (long) Double.parseDouble(value); // Long::parseLong; - private static final Map, Function> enumTypeParsers = new HashMap<>(); - private static final Map> primitiveTypeParsers = - Map.ofEntries( - Map.entry(String.class, parseString), - Map.entry(Integer.class, parseInt), - Map.entry(int.class, parseInt), - Map.entry(Long.class, parseLong), - Map.entry(long.class, parseLong), - Map.entry(Float.class, Float::parseFloat), - Map.entry(float.class, Float::parseFloat), - Map.entry(Double.class, Double::parseDouble), - Map.entry(double.class, Double::parseDouble), - Map.entry(Boolean.class, Boolean::parseBoolean), - Map.entry(boolean.class, Boolean::parseBoolean)); - private static final Map> typeParsers = - new HashMap<>(primitiveTypeParsers); - private static final Map, Map> cachedClassFieldMaps = - new HashMap<>(); - - @SuppressWarnings("unchecked") - private static T parsePrimitive(Class type, String string) { - if (string == null || string.isEmpty()) return (T) defaultValues.get(type); - return (T) primitiveTypeParsers.get(type).apply(string); - } - - // This is more expensive than parsing as the correct types, but it is more tolerant of mismatched - // data like ints with .0 - private static double parseNumber(String string) { - if (string == null || string.isEmpty()) return 0d; - return Double.parseDouble(string); - } - - @SuppressWarnings("unchecked") - private static T parseEnum(Class enumType, String string) { - if (string == null || string.isEmpty()) return null; - return (T) getEnumTypeParser(enumType).apply(string); - } - - // This is idiotic. I hate it. I'll have to look into how Gson beats the JVM into submission over - // classes where reflection magically fails to find the NoArgsConstructor later. - public static T newObj(Class objClass) { - try { - return objClass.getDeclaredConstructor().newInstance(); - } catch (Exception ignored) { - return JsonUtils.decode("{}", objClass); - } - } - - @SuppressWarnings("deprecated") - // Field::isAccessible is deprecated because it doesn't do what people think it does. It does what - // we want it to, however. - private static Function makeEnumTypeParser(Class enumClass) { - if (!enumClass.isEnum()) { - // System.out.println("Called makeEnumTypeParser with non-enum enumClass "+enumClass); - return null; - } - - // Make mappings of (string) names to enum constants - val map = new HashMap(); - val enumConstants = enumClass.getEnumConstants(); - for (val constant : enumConstants) map.put(constant.toString(), constant); - - // If the enum also has a numeric value, map those to the constants too - // System.out.println("Looking for enum value field"); - for (Field f : enumClass.getDeclaredFields()) { - if (switch (f.getName()) { - case "value", "id" -> true; - default -> false; - }) { - // System.out.println("Enum value field found - " + f.getName()); - boolean acc = f.isAccessible(); - f.setAccessible(true); - try { - for (val constant : enumConstants) map.put(String.valueOf(f.getInt(constant)), constant); - } catch (IllegalAccessException e) { - // System.out.println("Failed to access enum id field."); - } - f.setAccessible(acc); - break; - } - } - return map::get; - } - - private static synchronized Function getEnumTypeParser(Class enumType) { - if (enumType == null) { - // System.out.println("Called getEnumTypeParser with null enumType"); - return null; - } - return enumTypeParsers.computeIfAbsent(enumType, TsvUtils::makeEnumTypeParser); - } - - private static synchronized Function getTypeParser(Type type) { - if (type == null) return parseString; - return typeParsers.computeIfAbsent(type, t -> value -> JsonUtils.decode(value, t)); - } - - private static Type class2Type(Class classType) { - return classType.getGenericSuperclass(); - } - - private static Class type2Class(Type type) { - if (type instanceof Class) { - return (Class) type; - } else if (type instanceof ParameterizedType) { - return (Class) ((ParameterizedType) type).getRawType(); - } else { - return type.getClass(); // Probably incorrect - } - } - - private static Map makeClassFieldMap(Class classType) { - val fieldMap = new HashMap(); - for (Field field : classType.getDeclaredFields()) { - field.setAccessible( - true); // Yes, we don't bother setting this back. No, it doesn't matter for this project. - val fieldParser = new FieldParser(field); - - val a = field.getDeclaredAnnotation(SerializedName.class); - if (a == null) { // No annotation, use raw field name - fieldMap.put(field.getName(), fieldParser); - } else { // Handle SerializedNames and alternatives - fieldMap.put(a.value(), fieldParser); - for (val alt : a.alternate()) { - fieldMap.put(alt, fieldParser); - } - } - } - return fieldMap; - } - - private static synchronized Map getClassFieldMap(Class classType) { - return cachedClassFieldMaps.computeIfAbsent(classType, TsvUtils::makeClassFieldMap); - } - - // Flat tab-separated value tables. - // Arrays are represented as arrayName.0, arrayName.1, etc. columns. - // Maps/POJOs are represented as objName.fieldOneName, objName.fieldTwoName, etc. columns. - // This is currently about 25x as slow as TSJ and Gson parsers, likely due to the tree spam. - public static List loadTsvToListSetField(Path filename, Class classType) { - try (val fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { - // val fieldMap = getClassFieldMap(classType); - // val constructor = classType.getDeclaredConstructor(); - - val headerNames = nonRegexSplit(fileReader.readLine(), '\t'); - val columns = headerNames.size(); - // If we just crawled through all fields to expand potential subobjects, we might hit - // recursive data structure explosions (e.g. if something has a Player object) - // So we'll only crawl through objects referenced by the header columns - val stringTree = new StringTree(); - headerNames.forEach(stringTree::addPath); - - return fileReader - .lines() - .parallel() - .map( - line -> { - // return fileReader.lines().map(line -> { - // System.out.println("Processing line of "+filename+" - "+line); - val tokens = nonRegexSplit(line, '\t'); - val m = Math.min(tokens.size(), columns); - int t = 0; - StringValueTree tree = new StringValueTree(stringTree); - try { - for (t = 0; t < m; t++) { - String token = tokens.get(t); - if (!token.isEmpty()) { - tree.setValue(headerNames.get(t), token); - } - } - // return JsonUtils.decode(tree.toJson(), classType); - return tree.toClass(classType, null); - } catch (Exception e) { - Grasscutter.getLogger() - .warn( - "Error deserializing an instance of class " - + classType.getCanonicalName()); - Grasscutter.getLogger().warn("At token #" + t + " of #" + m); - Grasscutter.getLogger().warn("Header names are: " + headerNames); - Grasscutter.getLogger().warn("Tokens are: " + tokens); - Grasscutter.getLogger().warn("Stacktrace is: ", e); - // System.out.println("Error deserializing an instance of class - // "+classType.getCanonicalName()); - // System.out.println("At token #"+t+" of #"+m); - // System.out.println("Header names are: "+headerNames.toString()); - // System.out.println("Tokens are: "+tokens.toString()); - // System.out.println("Json is: "+tree.toJson().toString()); - // System.out.println("Stacktrace is: "+ e); - return null; - } - }) - .toList(); - } catch (Exception e) { - Grasscutter.getLogger().error("Error loading file '" + filename + "' - Stacktrace is: ", e); - return null; - } - } - - // This uses a hybrid format where columns can hold JSON-encoded values. - // I'll term it TSJ (tab-separated JSON) for now, it has convenient properties. - public static List loadTsjToListSetField(Path filename, Class classType) { - try (val fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { - val fieldMap = getClassFieldMap(classType); - val constructor = classType.getDeclaredConstructor(); - - val headerNames = nonRegexSplit(fileReader.readLine(), '\t'); - val columns = headerNames.size(); - val fieldParsers = headerNames.stream().map(fieldMap::get).toList(); - - return fileReader - .lines() - .parallel() - .map( - line -> { - val tokens = nonRegexSplit(line, '\t'); - val m = Math.min(tokens.size(), columns); - int t = 0; - try { - T obj = constructor.newInstance(); - for (t = 0; t < m; t++) { - val fieldParser = fieldParsers.get(t); - if (fieldParser == null) continue; - - String token = tokens.get(t); - if (!token.isEmpty()) { - fieldParser.parse(obj, token); - } - } - return obj; - } catch (Exception e) { - Grasscutter.getLogger() - .warn( - "Error deserializing an instance of class " - + classType.getCanonicalName()); - Grasscutter.getLogger().warn("At token #" + t + " of #" + m); - Grasscutter.getLogger().warn("Header names are: " + headerNames); - Grasscutter.getLogger().warn("Tokens are: " + tokens); - Grasscutter.getLogger().warn("Stacktrace is: ", e); - return null; - } - }) - .toList(); - } catch (NoSuchFileException e) { - Grasscutter.getLogger() - .error( - "Error loading file '" - + filename - + "' - File does not exist. You are missing resources. Note that this file may exist in JSON, TSV, or TSJ format, any of which are suitable."); - return null; - } catch (IOException e) { - Grasscutter.getLogger().error("Error loading file '" + filename + "' - Stacktrace is: ", e); - return null; - } catch (NoSuchMethodException e) { - Grasscutter.getLogger() - .error("Error loading file '" + filename + "' - Class is missing NoArgsConstructor"); - return null; - } - } - - // ----------------------------------------------------------------- - // Everything below here is for the AllArgsConstructor TSJ parser - // ----------------------------------------------------------------- - // Sadly, this is a little bit slower than the SetField version. - // I've left it in as an example of an optimization attempt that didn't work out, since the naive - // reflection version will tempt people to try things like this. - @SuppressWarnings("unchecked") - private static Pair, String[]> getAllArgsConstructor(Class classType) { - for (var c : classType.getDeclaredConstructors()) { - val consParameters = - (java.beans.ConstructorProperties) - c.getAnnotation(java.beans.ConstructorProperties.class); - if (consParameters != null) { - return Pair.of((Constructor) c, consParameters.value()); - } - } - return null; - } - - public static List> loadTsjsToListsConstructor(Class classType, Path... filenames) - throws Exception { - val pair = getAllArgsConstructor(classType); - if (pair == null) { - Grasscutter.getLogger().error("No AllArgsContructor found for class: " + classType); - return null; - } - val constructor = pair.left(); - val conArgNames = pair.right(); - val numArgs = constructor.getParameterCount(); - - val argMap = new Object2IntArrayMap(); - for (int i = 0; i < conArgNames.length; i++) { - argMap.put(conArgNames[i], i); - } - - val argTypes = - new Type[numArgs]; // constructor.getParameterTypes() returns base types like java.util.List - // instead of java.util.List - for (Field field : classType.getDeclaredFields()) { - int index = argMap.getOrDefault(field.getName(), -1); - if (index < 0) continue; - - argTypes[index] = field.getGenericType(); // returns specialized type info e.g. - // java.util.List - - val a = field.getDeclaredAnnotation(SerializedName.class); - if (a != null) { // Handle SerializedNames and alternatives - argMap.put(a.value(), index); - for (val alt : a.alternate()) { - argMap.put(alt, index); - } - } - } - val argParsers = Stream.of(argTypes).map(TsvUtils::getTypeParser).toList(); - - val defaultArgs = new Object[numArgs]; - for (int i = 0; i < numArgs; i++) { - defaultArgs[i] = defaultValues.get(argTypes[i]); - } - - return Stream.of(filenames) - .parallel() - .map( - filename -> { - try (val fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { - val headerNames = nonRegexSplit(fileReader.readLine(), '\t'); - val columns = headerNames.size(); - val argPositions = - headerNames.stream().mapToInt(name -> argMap.getOrDefault(name, -1)).toArray(); - - return fileReader - .lines() - .parallel() - .map( - line -> { - val tokens = nonRegexSplit(line, '\t'); - val args = defaultArgs.clone(); - val m = Math.min(tokens.size(), columns); - int t = 0; - try { - for (t = 0; t < m; t++) { - val argIndex = argPositions[t]; - if (argIndex < 0) continue; - - String token = tokens.get(t); - if (!token.isEmpty()) { - args[argIndex] = argParsers.get(argIndex).apply(token); - } - } - return constructor.newInstance(args); - } catch (Exception e) { - Grasscutter.getLogger() - .warn( - "Error deserializing an instance of class " - + classType.getCanonicalName() - + " : " - + constructor.getName()); - Grasscutter.getLogger().warn("At token #" + t + " of #" + m); - Grasscutter.getLogger() - .warn("Arg names are: " + Arrays.toString(conArgNames)); - Grasscutter.getLogger() - .warn("Arg types are: " + Arrays.toString(argTypes)); - Grasscutter.getLogger() - .warn("Default Args are: " + Arrays.toString(defaultArgs)); - Grasscutter.getLogger().warn("Args are: " + Arrays.toString(args)); - Grasscutter.getLogger().warn("Header names are: " + headerNames); - Grasscutter.getLogger() - .warn( - "Header types are: " - + IntStream.of(argPositions) - .mapToObj(i -> (i >= 0) ? argTypes[i] : null) - .toList()); - Grasscutter.getLogger().warn("Tokens are: " + tokens); - Grasscutter.getLogger().warn("Stacktrace is: ", e); - return null; - } - }) - .toList(); - } catch (IOException e) { - Grasscutter.getLogger() - .error("Error loading file '" + filename + "' - Stacktrace is: ", e); - return null; - } - }) - .toList(); - } - - // A helper object that contains a Field and the function to parse a String to create the value - // for the Field. - private static class FieldParser { - public final Field field; - public final Type type; - public final Class classType; - public final Function parser; - - FieldParser(Field field) { - this.field = field; - this.type = field.getGenericType(); // returns specialized type info e.g. - // java.util.List - this.classType = field.getType(); - this.parser = getTypeParser(this.type); - } - - public Object parse(String token) { - return this.parser.apply(token); - } - - public void parse(Object obj, String token) throws IllegalAccessException { - this.field.set(obj, this.parser.apply(token)); - } - } - - private static class StringTree { - public final Map children = new TreeMap<>(); - - public void addPath(String path) { - if (path.isEmpty()) return; - - val firstDot = path.indexOf('.'); - val fieldPath = (firstDot < 0) ? path : path.substring(0, firstDot); - val remainder = (firstDot < 0) ? "" : path.substring(firstDot + 1); - this.children.computeIfAbsent(fieldPath, k -> new StringTree()).addPath(remainder); - } - } - - @SuppressWarnings("unchecked") - private static class StringValueTree { - public final SortedMap children = new TreeMap<>(); - public final Int2ObjectSortedMap arrayChildren = new Int2ObjectRBTreeMap<>(); - public String value; - - public StringValueTree(StringTree from) { - from.children.forEach( - (k, v) -> { - try { - this.arrayChildren.put(Integer.parseInt(k), new StringValueTree(v)); - } catch (NumberFormatException e) { - this.children.put(k, new StringValueTree(v)); - } - }); - } - - public void setValue(String path, String value) { - if (path.isEmpty()) { - this.value = value; - return; - } - - val firstDot = path.indexOf('.'); - val fieldPath = (firstDot < 0) ? path : path.substring(0, firstDot); - val remainder = (firstDot < 0) ? "" : path.substring(firstDot + 1); - try { - this.arrayChildren.get(Integer.parseInt(fieldPath)).setValue(remainder, value); - } catch (NumberFormatException e) { - this.children.get(fieldPath).setValue(remainder, value); - } - } - - public JsonElement toJson() { - // Determine if this is an object, an array, or a value - if (this.value != null) { // - return new JsonPrimitive(this.value); - } - if (!this.arrayChildren.isEmpty()) { - val arr = new JsonArray(this.arrayChildren.lastIntKey() + 1); - arrayChildren.forEach((k, v) -> arr.set(k, v.toJson())); - return arr; - } else if (this.children.isEmpty()) { - return JsonNull.INSTANCE; - } else { - val obj = new JsonObject(); - children.forEach( - (k, v) -> { - val j = v.toJson(); - if (j != JsonNull.INSTANCE) obj.add(k, v.toJson()); - }); - return obj; - } - } - - public T toClass(Class classType, Type type) { - // System.out.println("toClass called with Class: "+classType+" \tType: "+type); - if (type == null) type = class2Type(classType); - - if (primitiveTypeParsers.containsKey(classType)) { - return parsePrimitive(classType, this.value); - } else if (classType.isEnum()) { - return parseEnum(classType, this.value); - } else if (classType.isArray()) { - return this.toArray(classType); - } else if (List.class.isAssignableFrom(classType)) { - // if (type instanceof ParameterizedType) - val elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; - return (T) this.toList(type2Class(elementType), elementType); - } else if (Map.class.isAssignableFrom(classType)) { - // System.out.println("Class: "+classType+" \tClassTypeParams: - // "+Arrays.toString(classType.getTypeParameters())+" \tType: "+type+" \tTypeArguments: - // "+Arrays.toString(((ParameterizedType) type).getActualTypeArguments())); - // if (type instanceof ParameterizedType) - val keyType = ((ParameterizedType) type).getActualTypeArguments()[0]; - val valueType = ((ParameterizedType) type).getActualTypeArguments()[1]; - return (T) this.toMap(type2Class(keyType), type2Class(valueType), valueType); - } else { - return this.toObj(classType, type); - } - } - - private T toObj(Class objClass, Type objType) { - try { - // val obj = objClass.getDeclaredConstructor().newInstance(); - val obj = newObj(objClass); - val fieldMap = getClassFieldMap(objClass); - this.children.forEach( - (name, tree) -> { - val field = fieldMap.get(name); - if (field == null) return; - try { - if (primitiveTypes.contains(field.type)) { - if ((tree.value != null) && !tree.value.isEmpty()) field.parse(obj, tree.value); - } else { - val value = tree.toClass(field.classType, field.type); - // System.out.println("Setting field "+name+" to "+value); - field.field.set(obj, value); - // field.field.set(obj, tree.toClass(field.classType, field.type)); - } - } catch (Exception e) { - // System.out.println("Exception while setting field "+name+" for class "+objClass+" - // - "+e); - Grasscutter.getLogger() - .error( - "Exception while setting field " - + name - + " (" - + field.classType - + ")" - + " for class " - + objClass - + " - ", - e); - } - }); - return obj; - } catch (Exception e) { - // System.out.println("Exception while creating object of class "+objClass+" - "+e); - Grasscutter.getLogger() - .error("Exception while creating object of class " + objClass + " - ", e); - return null; - } - } - - public T toArray(Class classType) { - // Primitives don't play so nice with generics, so we handle all of them individually. - val containedClass = classType.getComponentType(); - // val arraySize = this.arrayChildren.size(); // Assume dense 0-indexed - val arraySize = this.arrayChildren.lastIntKey() + 1; // Could be sparse! - // System.out.println("toArray called with Class: "+classType+" \tContains: "+containedClass+" - // \tof size: "+arraySize); - if (containedClass == int.class) { - val output = new int[arraySize]; - this.arrayChildren.forEach((idx, tree) -> output[idx] = (int) parseNumber(tree.value)); - return (T) output; - } else if (containedClass == long.class) { - val output = new long[arraySize]; - this.arrayChildren.forEach((idx, tree) -> output[idx] = (long) parseNumber(tree.value)); - return (T) output; - } else if (containedClass == float.class) { - val output = new float[arraySize]; - this.arrayChildren.forEach((idx, tree) -> output[idx] = (float) parseNumber(tree.value)); - return (T) output; - } else if (containedClass == double.class) { - val output = new double[arraySize]; - this.arrayChildren.forEach((idx, tree) -> output[idx] = parseNumber(tree.value)); - return (T) output; - } else if (containedClass == byte.class) { - val output = new byte[arraySize]; - this.arrayChildren.forEach((idx, tree) -> output[idx] = (byte) parseNumber(tree.value)); - return (T) output; - } else if (containedClass == char.class) { - val output = new char[arraySize]; - this.arrayChildren.forEach((idx, tree) -> output[idx] = (char) parseNumber(tree.value)); - return (T) output; - } else if (containedClass == short.class) { - val output = new short[arraySize]; - this.arrayChildren.forEach((idx, tree) -> output[idx] = (short) parseNumber(tree.value)); - return (T) output; - } else if (containedClass == boolean.class) { - val output = new boolean[arraySize]; - this.arrayChildren.forEach( - (idx, tree) -> { - val value = - (tree.value != null) && !tree.value.isEmpty() && Boolean.parseBoolean(tree.value); - output[idx] = value; - }); - return (T) output; - } else { - val output = Array.newInstance(containedClass, arraySize); - this.arrayChildren.forEach( - (idx, tree) -> ((Object[]) output)[idx] = tree.toClass(containedClass, null)); - return (T) output; - } - } - - private List toList(Class valueClass, Type valueType) { - val arraySize = this.arrayChildren.lastIntKey() + 1; // Could be sparse! - // System.out.println("toList called with valueClass: "+valueClass+" \tvalueType: - // "+valueType+" \tof size: "+arraySize); - val list = new ArrayList(arraySize); - // Safe sparse version - for (int i = 0; i < arraySize; i++) list.add(null); - this.arrayChildren.forEach((idx, tree) -> list.set(idx, tree.toClass(valueClass, valueType))); - return list; - } - - private Map toMap(Class keyClass, Class valueClass, Type valueType) { - val map = new HashMap(); - val keyParser = getTypeParser(keyClass); - this.children.forEach( - (key, tree) -> { - if ((key != null) && !key.isEmpty()) - map.put((K) keyParser.apply(key), tree.toClass(valueClass, valueType)); - }); - return map; - } - } -} +package emu.grasscutter.utils; + +import static emu.grasscutter.utils.Utils.nonRegexSplit; + +import com.google.gson.*; +import com.google.gson.annotations.SerializedName; +import emu.grasscutter.Grasscutter; +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap; +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; +import java.io.IOException; +import java.lang.reflect.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import lombok.val; + +// Throughout this file, commented System.out.println debug log calls are left in. +// This is because the default logger will deadlock when operating on parallel streams. +public class TsvUtils { + private static final Map defaultValues = + Map.ofEntries( + // Map.entry(String.class, null), // builder hates null values + Map.entry(Integer.class, 0), + Map.entry(int.class, 0), + Map.entry(Long.class, 0L), + Map.entry(long.class, 0L), + Map.entry(Float.class, 0f), + Map.entry(float.class, 0f), + Map.entry(Double.class, 0d), + Map.entry(double.class, 0d), + Map.entry(Boolean.class, false), + Map.entry(boolean.class, false)); + private static final Set primitiveTypes = + Set.of( + String.class, + Integer.class, + int.class, + Long.class, + long.class, + Float.class, + float.class, + Double.class, + double.class, + Boolean.class, + boolean.class); + + private static final Function parseString = value -> value; + private static final Function parseInt = + value -> (int) Double.parseDouble(value); // Integer::parseInt; + private static final Function parseLong = + value -> (long) Double.parseDouble(value); // Long::parseLong; + private static final Map, Function> enumTypeParsers = new HashMap<>(); + private static final Map> primitiveTypeParsers = + Map.ofEntries( + Map.entry(String.class, parseString), + Map.entry(Integer.class, parseInt), + Map.entry(int.class, parseInt), + Map.entry(Long.class, parseLong), + Map.entry(long.class, parseLong), + Map.entry(Float.class, Float::parseFloat), + Map.entry(float.class, Float::parseFloat), + Map.entry(Double.class, Double::parseDouble), + Map.entry(double.class, Double::parseDouble), + Map.entry(Boolean.class, Boolean::parseBoolean), + Map.entry(boolean.class, Boolean::parseBoolean)); + private static final Map> typeParsers = + new HashMap<>(primitiveTypeParsers); + private static final Map, Map> cachedClassFieldMaps = + new HashMap<>(); + + @SuppressWarnings("unchecked") + private static T parsePrimitive(Class type, String string) { + if (string == null || string.isEmpty()) return (T) defaultValues.get(type); + return (T) primitiveTypeParsers.get(type).apply(string); + } + + // This is more expensive than parsing as the correct types, but it is more tolerant of mismatched + // data like ints with .0 + private static double parseNumber(String string) { + if (string == null || string.isEmpty()) return 0d; + return Double.parseDouble(string); + } + + @SuppressWarnings("unchecked") + private static T parseEnum(Class enumType, String string) { + if (string == null || string.isEmpty()) return null; + return (T) getEnumTypeParser(enumType).apply(string); + } + + // This is idiotic. I hate it. I'll have to look into how Gson beats the JVM into submission over + // classes where reflection magically fails to find the NoArgsConstructor later. + public static T newObj(Class objClass) { + try { + return objClass.getDeclaredConstructor().newInstance(); + } catch (Exception ignored) { + return JsonUtils.decode("{}", objClass); + } + } + + @SuppressWarnings("deprecated") + // Field::isAccessible is deprecated because it doesn't do what people think it does. It does what + // we want it to, however. + private static Function makeEnumTypeParser(Class enumClass) { + if (!enumClass.isEnum()) { + // System.out.println("Called makeEnumTypeParser with non-enum enumClass "+enumClass); + return null; + } + + // Make mappings of (string) names to enum constants + val map = new HashMap(); + val enumConstants = enumClass.getEnumConstants(); + for (val constant : enumConstants) map.put(constant.toString(), constant); + + // If the enum also has a numeric value, map those to the constants too + // System.out.println("Looking for enum value field"); + for (Field f : enumClass.getDeclaredFields()) { + if (switch (f.getName()) { + case "value", "id" -> true; + default -> false; + }) { + // System.out.println("Enum value field found - " + f.getName()); + boolean acc = f.isAccessible(); + f.setAccessible(true); + try { + for (val constant : enumConstants) map.put(String.valueOf(f.getInt(constant)), constant); + } catch (IllegalAccessException e) { + // System.out.println("Failed to access enum id field."); + } + f.setAccessible(acc); + break; + } + } + return map::get; + } + + private static synchronized Function getEnumTypeParser(Class enumType) { + if (enumType == null) { + // System.out.println("Called getEnumTypeParser with null enumType"); + return null; + } + return enumTypeParsers.computeIfAbsent(enumType, TsvUtils::makeEnumTypeParser); + } + + private static synchronized Function getTypeParser(Type type) { + if (type == null) return parseString; + return typeParsers.computeIfAbsent(type, t -> value -> JsonUtils.decode(value, t)); + } + + private static Type class2Type(Class classType) { + return classType.getGenericSuperclass(); + } + + private static Class type2Class(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } else { + return type.getClass(); // Probably incorrect + } + } + + private static Map makeClassFieldMap(Class classType) { + val fieldMap = new HashMap(); + for (Field field : classType.getDeclaredFields()) { + field.setAccessible( + true); // Yes, we don't bother setting this back. No, it doesn't matter for this project. + val fieldParser = new FieldParser(field); + + val a = field.getDeclaredAnnotation(SerializedName.class); + if (a == null) { // No annotation, use raw field name + fieldMap.put(field.getName(), fieldParser); + } else { // Handle SerializedNames and alternatives + fieldMap.put(a.value(), fieldParser); + for (val alt : a.alternate()) { + fieldMap.put(alt, fieldParser); + } + } + } + return fieldMap; + } + + private static synchronized Map getClassFieldMap(Class classType) { + return cachedClassFieldMaps.computeIfAbsent(classType, TsvUtils::makeClassFieldMap); + } + + // Flat tab-separated value tables. + // Arrays are represented as arrayName.0, arrayName.1, etc. columns. + // Maps/POJOs are represented as objName.fieldOneName, objName.fieldTwoName, etc. columns. + // This is currently about 25x as slow as TSJ and Gson parsers, likely due to the tree spam. + public static List loadTsvToListSetField(Path filename, Class classType) { + try (val fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { + // val fieldMap = getClassFieldMap(classType); + // val constructor = classType.getDeclaredConstructor(); + + val headerNames = nonRegexSplit(fileReader.readLine(), '\t'); + val columns = headerNames.size(); + // If we just crawled through all fields to expand potential subobjects, we might hit + // recursive data structure explosions (e.g. if something has a Player object) + // So we'll only crawl through objects referenced by the header columns + val stringTree = new StringTree(); + headerNames.forEach(stringTree::addPath); + + return fileReader + .lines() + .parallel() + .map( + line -> { + // return fileReader.lines().map(line -> { + // System.out.println("Processing line of "+filename+" - "+line); + val tokens = nonRegexSplit(line, '\t'); + val m = Math.min(tokens.size(), columns); + int t = 0; + StringValueTree tree = new StringValueTree(stringTree); + try { + for (t = 0; t < m; t++) { + String token = tokens.get(t); + if (!token.isEmpty()) { + tree.setValue(headerNames.get(t), token); + } + } + // return JsonUtils.decode(tree.toJson(), classType); + return tree.toClass(classType, null); + } catch (Exception e) { + Grasscutter.getLogger() + .warn( + "Error deserializing an instance of class " + + classType.getCanonicalName()); + Grasscutter.getLogger().warn("At token #" + t + " of #" + m); + Grasscutter.getLogger().warn("Header names are: " + headerNames); + Grasscutter.getLogger().warn("Tokens are: " + tokens); + Grasscutter.getLogger().warn("Stacktrace is: ", e); + // System.out.println("Error deserializing an instance of class + // "+classType.getCanonicalName()); + // System.out.println("At token #"+t+" of #"+m); + // System.out.println("Header names are: "+headerNames.toString()); + // System.out.println("Tokens are: "+tokens.toString()); + // System.out.println("Json is: "+tree.toJson().toString()); + // System.out.println("Stacktrace is: "+ e); + return null; + } + }) + .toList(); + } catch (Exception e) { + Grasscutter.getLogger().error("Error loading file '" + filename + "' - Stacktrace is: ", e); + return null; + } + } + + // This uses a hybrid format where columns can hold JSON-encoded values. + // I'll term it TSJ (tab-separated JSON) for now, it has convenient properties. + public static List loadTsjToListSetField(Path filename, Class classType) { + try (val fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { + val fieldMap = getClassFieldMap(classType); + val constructor = classType.getDeclaredConstructor(); + + val headerNames = nonRegexSplit(fileReader.readLine(), '\t'); + val columns = headerNames.size(); + val fieldParsers = headerNames.stream().map(fieldMap::get).toList(); + + return fileReader + .lines() + .parallel() + .map( + line -> { + val tokens = nonRegexSplit(line, '\t'); + val m = Math.min(tokens.size(), columns); + int t = 0; + try { + T obj = constructor.newInstance(); + for (t = 0; t < m; t++) { + val fieldParser = fieldParsers.get(t); + if (fieldParser == null) continue; + + String token = tokens.get(t); + if (!token.isEmpty()) { + fieldParser.parse(obj, token); + } + } + return obj; + } catch (Exception e) { + Grasscutter.getLogger() + .warn( + "Error deserializing an instance of class " + + classType.getCanonicalName()); + Grasscutter.getLogger().warn("At token #" + t + " of #" + m); + Grasscutter.getLogger().warn("Header names are: " + headerNames); + Grasscutter.getLogger().warn("Tokens are: " + tokens); + Grasscutter.getLogger().warn("Stacktrace is: ", e); + return null; + } + }) + .toList(); + } catch (NoSuchFileException e) { + Grasscutter.getLogger() + .error( + "Error loading file '" + + filename + + "' - File does not exist. You are missing resources. Note that this file may exist in JSON, TSV, or TSJ format, any of which are suitable."); + return null; + } catch (IOException e) { + Grasscutter.getLogger().error("Error loading file '" + filename + "' - Stacktrace is: ", e); + return null; + } catch (NoSuchMethodException e) { + Grasscutter.getLogger() + .error("Error loading file '" + filename + "' - Class is missing NoArgsConstructor"); + return null; + } + } + + // ----------------------------------------------------------------- + // Everything below here is for the AllArgsConstructor TSJ parser + // ----------------------------------------------------------------- + // Sadly, this is a little bit slower than the SetField version. + // I've left it in as an example of an optimization attempt that didn't work out, since the naive + // reflection version will tempt people to try things like this. + @SuppressWarnings("unchecked") + private static Pair, String[]> getAllArgsConstructor(Class classType) { + for (var c : classType.getDeclaredConstructors()) { + val consParameters = + (java.beans.ConstructorProperties) + c.getAnnotation(java.beans.ConstructorProperties.class); + if (consParameters != null) { + return Pair.of((Constructor) c, consParameters.value()); + } + } + return null; + } + + public static List> loadTsjsToListsConstructor(Class classType, Path... filenames) + throws Exception { + val pair = getAllArgsConstructor(classType); + if (pair == null) { + Grasscutter.getLogger().error("No AllArgsContructor found for class: " + classType); + return null; + } + val constructor = pair.left(); + val conArgNames = pair.right(); + val numArgs = constructor.getParameterCount(); + + val argMap = new Object2IntArrayMap(); + for (int i = 0; i < conArgNames.length; i++) { + argMap.put(conArgNames[i], i); + } + + val argTypes = + new Type[numArgs]; // constructor.getParameterTypes() returns base types like java.util.List + // instead of java.util.List + for (Field field : classType.getDeclaredFields()) { + int index = argMap.getOrDefault(field.getName(), -1); + if (index < 0) continue; + + argTypes[index] = field.getGenericType(); // returns specialized type info e.g. + // java.util.List + + val a = field.getDeclaredAnnotation(SerializedName.class); + if (a != null) { // Handle SerializedNames and alternatives + argMap.put(a.value(), index); + for (val alt : a.alternate()) { + argMap.put(alt, index); + } + } + } + val argParsers = Stream.of(argTypes).map(TsvUtils::getTypeParser).toList(); + + val defaultArgs = new Object[numArgs]; + for (int i = 0; i < numArgs; i++) { + defaultArgs[i] = defaultValues.get(argTypes[i]); + } + + return Stream.of(filenames) + .parallel() + .map( + filename -> { + try (val fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) { + val headerNames = nonRegexSplit(fileReader.readLine(), '\t'); + val columns = headerNames.size(); + val argPositions = + headerNames.stream().mapToInt(name -> argMap.getOrDefault(name, -1)).toArray(); + + return fileReader + .lines() + .parallel() + .map( + line -> { + val tokens = nonRegexSplit(line, '\t'); + val args = defaultArgs.clone(); + val m = Math.min(tokens.size(), columns); + int t = 0; + try { + for (t = 0; t < m; t++) { + val argIndex = argPositions[t]; + if (argIndex < 0) continue; + + String token = tokens.get(t); + if (!token.isEmpty()) { + args[argIndex] = argParsers.get(argIndex).apply(token); + } + } + return constructor.newInstance(args); + } catch (Exception e) { + Grasscutter.getLogger() + .warn( + "Error deserializing an instance of class " + + classType.getCanonicalName() + + " : " + + constructor.getName()); + Grasscutter.getLogger().warn("At token #" + t + " of #" + m); + Grasscutter.getLogger() + .warn("Arg names are: " + Arrays.toString(conArgNames)); + Grasscutter.getLogger() + .warn("Arg types are: " + Arrays.toString(argTypes)); + Grasscutter.getLogger() + .warn("Default Args are: " + Arrays.toString(defaultArgs)); + Grasscutter.getLogger().warn("Args are: " + Arrays.toString(args)); + Grasscutter.getLogger().warn("Header names are: " + headerNames); + Grasscutter.getLogger() + .warn( + "Header types are: " + + IntStream.of(argPositions) + .mapToObj(i -> (i >= 0) ? argTypes[i] : null) + .toList()); + Grasscutter.getLogger().warn("Tokens are: " + tokens); + Grasscutter.getLogger().warn("Stacktrace is: ", e); + return null; + } + }) + .toList(); + } catch (IOException e) { + Grasscutter.getLogger() + .error("Error loading file '" + filename + "' - Stacktrace is: ", e); + return null; + } + }) + .toList(); + } + + // A helper object that contains a Field and the function to parse a String to create the value + // for the Field. + private static class FieldParser { + public final Field field; + public final Type type; + public final Class classType; + public final Function parser; + + FieldParser(Field field) { + this.field = field; + this.type = field.getGenericType(); // returns specialized type info e.g. + // java.util.List + this.classType = field.getType(); + this.parser = getTypeParser(this.type); + } + + public Object parse(String token) { + return this.parser.apply(token); + } + + public void parse(Object obj, String token) throws IllegalAccessException { + this.field.set(obj, this.parser.apply(token)); + } + } + + private static class StringTree { + public final Map children = new TreeMap<>(); + + public void addPath(String path) { + if (path.isEmpty()) return; + + val firstDot = path.indexOf('.'); + val fieldPath = (firstDot < 0) ? path : path.substring(0, firstDot); + val remainder = (firstDot < 0) ? "" : path.substring(firstDot + 1); + this.children.computeIfAbsent(fieldPath, k -> new StringTree()).addPath(remainder); + } + } + + @SuppressWarnings("unchecked") + private static class StringValueTree { + public final SortedMap children = new TreeMap<>(); + public final Int2ObjectSortedMap arrayChildren = new Int2ObjectRBTreeMap<>(); + public String value; + + public StringValueTree(StringTree from) { + from.children.forEach( + (k, v) -> { + try { + this.arrayChildren.put(Integer.parseInt(k), new StringValueTree(v)); + } catch (NumberFormatException e) { + this.children.put(k, new StringValueTree(v)); + } + }); + } + + public void setValue(String path, String value) { + if (path.isEmpty()) { + this.value = value; + return; + } + + val firstDot = path.indexOf('.'); + val fieldPath = (firstDot < 0) ? path : path.substring(0, firstDot); + val remainder = (firstDot < 0) ? "" : path.substring(firstDot + 1); + try { + this.arrayChildren.get(Integer.parseInt(fieldPath)).setValue(remainder, value); + } catch (NumberFormatException e) { + this.children.get(fieldPath).setValue(remainder, value); + } + } + + public JsonElement toJson() { + // Determine if this is an object, an array, or a value + if (this.value != null) { // + return new JsonPrimitive(this.value); + } + if (!this.arrayChildren.isEmpty()) { + val arr = new JsonArray(this.arrayChildren.lastIntKey() + 1); + arrayChildren.forEach((k, v) -> arr.set(k, v.toJson())); + return arr; + } else if (this.children.isEmpty()) { + return JsonNull.INSTANCE; + } else { + val obj = new JsonObject(); + children.forEach( + (k, v) -> { + val j = v.toJson(); + if (j != JsonNull.INSTANCE) obj.add(k, v.toJson()); + }); + return obj; + } + } + + public T toClass(Class classType, Type type) { + // System.out.println("toClass called with Class: "+classType+" \tType: "+type); + if (type == null) type = class2Type(classType); + + if (primitiveTypeParsers.containsKey(classType)) { + return parsePrimitive(classType, this.value); + } else if (classType.isEnum()) { + return parseEnum(classType, this.value); + } else if (classType.isArray()) { + return this.toArray(classType); + } else if (List.class.isAssignableFrom(classType)) { + // if (type instanceof ParameterizedType) + val elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + return (T) this.toList(type2Class(elementType), elementType); + } else if (Map.class.isAssignableFrom(classType)) { + // System.out.println("Class: "+classType+" \tClassTypeParams: + // "+Arrays.toString(classType.getTypeParameters())+" \tType: "+type+" \tTypeArguments: + // "+Arrays.toString(((ParameterizedType) type).getActualTypeArguments())); + // if (type instanceof ParameterizedType) + val keyType = ((ParameterizedType) type).getActualTypeArguments()[0]; + val valueType = ((ParameterizedType) type).getActualTypeArguments()[1]; + return (T) this.toMap(type2Class(keyType), type2Class(valueType), valueType); + } else { + return this.toObj(classType, type); + } + } + + private T toObj(Class objClass, Type objType) { + try { + // val obj = objClass.getDeclaredConstructor().newInstance(); + val obj = newObj(objClass); + val fieldMap = getClassFieldMap(objClass); + this.children.forEach( + (name, tree) -> { + val field = fieldMap.get(name); + if (field == null) return; + try { + if (primitiveTypes.contains(field.type)) { + if ((tree.value != null) && !tree.value.isEmpty()) field.parse(obj, tree.value); + } else { + val value = tree.toClass(field.classType, field.type); + // System.out.println("Setting field "+name+" to "+value); + field.field.set(obj, value); + // field.field.set(obj, tree.toClass(field.classType, field.type)); + } + } catch (Exception e) { + // System.out.println("Exception while setting field "+name+" for class "+objClass+" + // - "+e); + Grasscutter.getLogger() + .error( + "Exception while setting field " + + name + + " (" + + field.classType + + ")" + + " for class " + + objClass + + " - ", + e); + } + }); + return obj; + } catch (Exception e) { + // System.out.println("Exception while creating object of class "+objClass+" - "+e); + Grasscutter.getLogger() + .error("Exception while creating object of class " + objClass + " - ", e); + return null; + } + } + + public T toArray(Class classType) { + // Primitives don't play so nice with generics, so we handle all of them individually. + val containedClass = classType.getComponentType(); + // val arraySize = this.arrayChildren.size(); // Assume dense 0-indexed + val arraySize = this.arrayChildren.lastIntKey() + 1; // Could be sparse! + // System.out.println("toArray called with Class: "+classType+" \tContains: "+containedClass+" + // \tof size: "+arraySize); + if (containedClass == int.class) { + val output = new int[arraySize]; + this.arrayChildren.forEach((idx, tree) -> output[idx] = (int) parseNumber(tree.value)); + return (T) output; + } else if (containedClass == long.class) { + val output = new long[arraySize]; + this.arrayChildren.forEach((idx, tree) -> output[idx] = (long) parseNumber(tree.value)); + return (T) output; + } else if (containedClass == float.class) { + val output = new float[arraySize]; + this.arrayChildren.forEach((idx, tree) -> output[idx] = (float) parseNumber(tree.value)); + return (T) output; + } else if (containedClass == double.class) { + val output = new double[arraySize]; + this.arrayChildren.forEach((idx, tree) -> output[idx] = parseNumber(tree.value)); + return (T) output; + } else if (containedClass == byte.class) { + val output = new byte[arraySize]; + this.arrayChildren.forEach((idx, tree) -> output[idx] = (byte) parseNumber(tree.value)); + return (T) output; + } else if (containedClass == char.class) { + val output = new char[arraySize]; + this.arrayChildren.forEach((idx, tree) -> output[idx] = (char) parseNumber(tree.value)); + return (T) output; + } else if (containedClass == short.class) { + val output = new short[arraySize]; + this.arrayChildren.forEach((idx, tree) -> output[idx] = (short) parseNumber(tree.value)); + return (T) output; + } else if (containedClass == boolean.class) { + val output = new boolean[arraySize]; + this.arrayChildren.forEach( + (idx, tree) -> { + val value = + (tree.value != null) && !tree.value.isEmpty() && Boolean.parseBoolean(tree.value); + output[idx] = value; + }); + return (T) output; + } else { + val output = Array.newInstance(containedClass, arraySize); + this.arrayChildren.forEach( + (idx, tree) -> ((Object[]) output)[idx] = tree.toClass(containedClass, null)); + return (T) output; + } + } + + private List toList(Class valueClass, Type valueType) { + val arraySize = this.arrayChildren.lastIntKey() + 1; // Could be sparse! + // System.out.println("toList called with valueClass: "+valueClass+" \tvalueType: + // "+valueType+" \tof size: "+arraySize); + val list = new ArrayList(arraySize); + // Safe sparse version + for (int i = 0; i < arraySize; i++) list.add(null); + this.arrayChildren.forEach((idx, tree) -> list.set(idx, tree.toClass(valueClass, valueType))); + return list; + } + + private Map toMap(Class keyClass, Class valueClass, Type valueType) { + val map = new HashMap(); + val keyParser = getTypeParser(keyClass); + this.children.forEach( + (key, tree) -> { + if ((key != null) && !key.isEmpty()) + map.put((K) keyParser.apply(key), tree.toClass(valueClass, valueType)); + }); + return map; + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index edd99d940..3d90f42ac 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -1,449 +1,449 @@ -package emu.grasscutter.utils; - -import static emu.grasscutter.utils.FileUtils.getResourcePath; -import static emu.grasscutter.utils.Language.translate; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.config.ConfigContainer; -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 java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.time.DayOfWeek; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.temporal.TemporalAdjusters; -import java.util.*; -import java.util.concurrent.ThreadLocalRandom; -import javax.annotation.Nullable; -import org.slf4j.Logger; - -@SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"}) -public final class Utils { - public static final Random random = new Random(); - private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); - - public static int randomRange(int min, int max) { - return random.nextInt(max - min + 1) + min; - } - - public static float randomFloatRange(float min, float max) { - return random.nextFloat() * (max - min) + min; - } - - public static double getDist(Position pos1, Position pos2) { - double xs = pos1.getX() - pos2.getX(); - xs = xs * xs; - - double ys = pos1.getY() - pos2.getY(); - ys = ys * ys; - - double zs = pos1.getZ() - pos2.getZ(); - zs = zs * zs; - - return Math.sqrt(xs + zs + ys); - } - - public static int getCurrentSeconds() { - return (int) (System.currentTimeMillis() / 1000.0); - } - - public static String lowerCaseFirstChar(String s) { - StringBuilder sb = new StringBuilder(s); - sb.setCharAt(0, Character.toLowerCase(sb.charAt(0))); - return sb.toString(); - } - - public static String toString(InputStream inputStream) throws IOException { - BufferedInputStream bis = new BufferedInputStream(inputStream); - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - for (int result = bis.read(); result != -1; result = bis.read()) { - buf.write((byte) result); - } - return buf.toString(); - } - - public static void logByteArray(byte[] array) { - ByteBuf b = Unpooled.wrappedBuffer(array); - Grasscutter.getLogger().info("\n" + ByteBufUtil.prettyHexDump(b)); - b.release(); - } - - public static String bytesToHex(byte[] bytes) { - if (bytes == null) return ""; - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - public static String bytesToHex(ByteBuf buf) { - return bytesToHex(byteBufToArray(buf)); - } - - public static byte[] byteBufToArray(ByteBuf buf) { - byte[] bytes = new byte[buf.capacity()]; - buf.getBytes(0, bytes); - return bytes; - } - - public static int abilityHash(String str) { - int v7 = 0; - int v8 = 0; - while (v8 < str.length()) { - v7 = str.charAt(v8++) + 131 * v7; - } - return v7; - } - - /** - * Creates a string with the path to a file. - * - * @param path The path to the file. - * @return A path using the operating system's file separator. - */ - public static String toFilePath(String path) { - return path.replace("/", File.separator); - } - - /** - * Checks if a file exists on the file system. - * - * @param path The path to the file. - * @return True if the file exists, false otherwise. - */ - public static boolean fileExists(String path) { - return new File(path).exists(); - } - - /** - * Creates a folder on the file system. - * - * @param path The path to the folder. - * @return True if the folder was created, false otherwise. - */ - public static boolean createFolder(String path) { - return new File(path).mkdirs(); - } - - /** - * Copies a file from the archive's resources to the file system. - * - * @param resource The path to the resource. - * @param destination The path to copy the resource to. - * @return True if the file was copied, false otherwise. - */ - public static boolean copyFromResources(String resource, String destination) { - try (InputStream stream = Grasscutter.class.getResourceAsStream(resource)) { - if (stream == null) { - Grasscutter.getLogger().warn("Could not find resource: " + resource); - return false; - } - - Files.copy(stream, new File(destination).toPath(), StandardCopyOption.REPLACE_EXISTING); - return true; - } catch (Exception exception) { - Grasscutter.getLogger() - .warn("Unable to copy resource " + resource + " to " + destination, exception); - return false; - } - } - - /** - * Logs an object to the console. - * - * @param object The object to log. - */ - public static void logObject(Object object) { - Grasscutter.getLogger().info(JsonUtils.encode(object)); - } - - /** Checks for required files and folders before startup. */ - public static void startupCheck() { - ConfigContainer config = Grasscutter.getConfig(); - Logger logger = Grasscutter.getLogger(); - boolean exit = false; - - String dataFolder = config.folderStructure.data; - - // Check for resources folder. - if (!Files.exists(getResourcePath(""))) { - logger.info(translate("messages.status.create_resources")); - logger.info(translate("messages.status.resources_error")); - createFolder(config.folderStructure.resources); - exit = true; - } - - // Check for BinOutput + ExcelBinOutput. - if (!Files.exists(getResourcePath("BinOutput")) - || !Files.exists(getResourcePath("ExcelBinOutput"))) { - logger.info(translate("messages.status.resources_error")); - exit = true; - } - - // Check for game data. - 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); - } - - /** - * Gets the timestamp of the next hour. - * - * @return The timestamp in UNIX seconds. - */ - public static int getNextTimestampOfThisHour(int hour, String timeZone, int param) { - ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); - for (int i = 0; i < param; i++) { - if (zonedDateTime.getHour() < hour) { - zonedDateTime = zonedDateTime.withHour(hour).withMinute(0).withSecond(0); - } else { - zonedDateTime = zonedDateTime.plusDays(1).withHour(hour).withMinute(0).withSecond(0); - } - } - return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); - } - - /** - * Gets the timestamp of the next hour in a week. - * - * @return The timestamp in UNIX seconds. - */ - public static int getNextTimestampOfThisHourInNextWeek(int hour, String timeZone, int param) { - ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); - for (int i = 0; i < param; i++) { - if (zonedDateTime.getDayOfWeek() == DayOfWeek.MONDAY && zonedDateTime.getHour() < hour) { - zonedDateTime = - ZonedDateTime.now(ZoneId.of(timeZone)).withHour(hour).withMinute(0).withSecond(0); - } else { - zonedDateTime = - zonedDateTime - .with(TemporalAdjusters.next(DayOfWeek.MONDAY)) - .withHour(hour) - .withMinute(0) - .withSecond(0); - } - } - return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); - } - - /** - * Gets the timestamp of the next hour in a month. - * - * @return The timestamp in UNIX seconds. - */ - public static int getNextTimestampOfThisHourInNextMonth(int hour, String timeZone, int param) { - ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); - for (int i = 0; i < param; i++) { - if (zonedDateTime.getDayOfMonth() == 1 && zonedDateTime.getHour() < hour) { - zonedDateTime = - ZonedDateTime.now(ZoneId.of(timeZone)).withHour(hour).withMinute(0).withSecond(0); - } else { - zonedDateTime = - zonedDateTime - .with(TemporalAdjusters.firstDayOfNextMonth()) - .withHour(hour) - .withMinute(0) - .withSecond(0); - } - } - return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); - } - - /** - * Retrieves a string from an input stream. - * - * @param stream The input stream. - * @return The string. - */ - 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) { - stringBuilder.append(line); - } - stream.close(); - } catch (IOException e) { - Grasscutter.getLogger().warn("Failed to read from input stream."); - } catch (NullPointerException ignored) { - return "empty"; - } - return stringBuilder.toString(); - } - - /** - * Performs a linear interpolation using a table of fixed points to create an effective piecewise - * f(x) = y function. - * - * @param x The x value. - * @param xyArray Array of points in [[x0,y0], ... [xN, yN]] format - * @return f(x) = y - */ - 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 (IndexOutOfBoundsException e) { - Grasscutter.getLogger() - .error("Malformed lerp point array. Must be of form [[x0, y0], ..., [xN, yN]]."); - } - return 0; - } - - /** - * 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); - } - - /*** - * Draws a random element from the given list, following the given probability distribution, if given. - * @param list The list from which to draw the element. - * @param probabilities The probability distribution. This is given as a list of probabilities of the same length it `list`. - * @return A randomly drawn element from the given list. - */ - public static T drawRandomListElement(List list, List probabilities) { - // If we don't have a probability distribution, or the size of the distribution does not match - // the size of the list, we assume uniform distribution. - if (probabilities == null || probabilities.size() <= 1 || probabilities.size() != list.size()) { - int index = ThreadLocalRandom.current().nextInt(0, list.size()); - return list.get(index); - } - - // Otherwise, we roll with the given distribution. - int totalProbabilityMass = probabilities.stream().reduce(Integer::sum).get(); - int roll = ThreadLocalRandom.current().nextInt(1, totalProbabilityMass + 1); - - int currentTotalChance = 0; - for (int i = 0; i < list.size(); i++) { - currentTotalChance += probabilities.get(i); - - if (roll <= currentTotalChance) { - return list.get(i); - } - } - - // Should never happen. - return list.get(0); - } - - /*** - * Draws a random element from the given list, following a uniform probability distribution. - * @param list The list from which to draw the element. - * @return A randomly drawn element from the given list. - */ - public static T drawRandomListElement(List list) { - return drawRandomListElement(list, null); - } - - /*** - * Splits a string by a character, into a list - * @param input The string to split - * @param separator The character to use as the split points - * @return A list of all the substrings - */ - public static List nonRegexSplit(String input, int separator) { - var output = new ArrayList(); - int start = 0; - for (int next = input.indexOf(separator); next > 0; next = input.indexOf(separator, start)) { - output.add(input.substring(start, next)); - start = next + 1; - } - if (start < input.length()) output.add(input.substring(start)); - return output; - } -} +package emu.grasscutter.utils; + +import static emu.grasscutter.utils.FileUtils.getResourcePath; +import static emu.grasscutter.utils.Language.translate; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.config.ConfigContainer; +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 java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.time.DayOfWeek; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import javax.annotation.Nullable; +import org.slf4j.Logger; + +@SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"}) +public final class Utils { + public static final Random random = new Random(); + private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); + + public static int randomRange(int min, int max) { + return random.nextInt(max - min + 1) + min; + } + + public static float randomFloatRange(float min, float max) { + return random.nextFloat() * (max - min) + min; + } + + public static double getDist(Position pos1, Position pos2) { + double xs = pos1.getX() - pos2.getX(); + xs = xs * xs; + + double ys = pos1.getY() - pos2.getY(); + ys = ys * ys; + + double zs = pos1.getZ() - pos2.getZ(); + zs = zs * zs; + + return Math.sqrt(xs + zs + ys); + } + + public static int getCurrentSeconds() { + return (int) (System.currentTimeMillis() / 1000.0); + } + + public static String lowerCaseFirstChar(String s) { + StringBuilder sb = new StringBuilder(s); + sb.setCharAt(0, Character.toLowerCase(sb.charAt(0))); + return sb.toString(); + } + + public static String toString(InputStream inputStream) throws IOException { + BufferedInputStream bis = new BufferedInputStream(inputStream); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + for (int result = bis.read(); result != -1; result = bis.read()) { + buf.write((byte) result); + } + return buf.toString(); + } + + public static void logByteArray(byte[] array) { + ByteBuf b = Unpooled.wrappedBuffer(array); + Grasscutter.getLogger().info("\n" + ByteBufUtil.prettyHexDump(b)); + b.release(); + } + + public static String bytesToHex(byte[] bytes) { + if (bytes == null) return ""; + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + public static String bytesToHex(ByteBuf buf) { + return bytesToHex(byteBufToArray(buf)); + } + + public static byte[] byteBufToArray(ByteBuf buf) { + byte[] bytes = new byte[buf.capacity()]; + buf.getBytes(0, bytes); + return bytes; + } + + public static int abilityHash(String str) { + int v7 = 0; + int v8 = 0; + while (v8 < str.length()) { + v7 = str.charAt(v8++) + 131 * v7; + } + return v7; + } + + /** + * Creates a string with the path to a file. + * + * @param path The path to the file. + * @return A path using the operating system's file separator. + */ + public static String toFilePath(String path) { + return path.replace("/", File.separator); + } + + /** + * Checks if a file exists on the file system. + * + * @param path The path to the file. + * @return True if the file exists, false otherwise. + */ + public static boolean fileExists(String path) { + return new File(path).exists(); + } + + /** + * Creates a folder on the file system. + * + * @param path The path to the folder. + * @return True if the folder was created, false otherwise. + */ + public static boolean createFolder(String path) { + return new File(path).mkdirs(); + } + + /** + * Copies a file from the archive's resources to the file system. + * + * @param resource The path to the resource. + * @param destination The path to copy the resource to. + * @return True if the file was copied, false otherwise. + */ + public static boolean copyFromResources(String resource, String destination) { + try (InputStream stream = Grasscutter.class.getResourceAsStream(resource)) { + if (stream == null) { + Grasscutter.getLogger().warn("Could not find resource: " + resource); + return false; + } + + Files.copy(stream, new File(destination).toPath(), StandardCopyOption.REPLACE_EXISTING); + return true; + } catch (Exception exception) { + Grasscutter.getLogger() + .warn("Unable to copy resource " + resource + " to " + destination, exception); + return false; + } + } + + /** + * Logs an object to the console. + * + * @param object The object to log. + */ + public static void logObject(Object object) { + Grasscutter.getLogger().info(JsonUtils.encode(object)); + } + + /** Checks for required files and folders before startup. */ + public static void startupCheck() { + ConfigContainer config = Grasscutter.getConfig(); + Logger logger = Grasscutter.getLogger(); + boolean exit = false; + + String dataFolder = config.folderStructure.data; + + // Check for resources folder. + if (!Files.exists(getResourcePath(""))) { + logger.info(translate("messages.status.create_resources")); + logger.info(translate("messages.status.resources_error")); + createFolder(config.folderStructure.resources); + exit = true; + } + + // Check for BinOutput + ExcelBinOutput. + if (!Files.exists(getResourcePath("BinOutput")) + || !Files.exists(getResourcePath("ExcelBinOutput"))) { + logger.info(translate("messages.status.resources_error")); + exit = true; + } + + // Check for game data. + 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); + } + + /** + * Gets the timestamp of the next hour. + * + * @return The timestamp in UNIX seconds. + */ + public static int getNextTimestampOfThisHour(int hour, String timeZone, int param) { + ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); + for (int i = 0; i < param; i++) { + if (zonedDateTime.getHour() < hour) { + zonedDateTime = zonedDateTime.withHour(hour).withMinute(0).withSecond(0); + } else { + zonedDateTime = zonedDateTime.plusDays(1).withHour(hour).withMinute(0).withSecond(0); + } + } + return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + } + + /** + * Gets the timestamp of the next hour in a week. + * + * @return The timestamp in UNIX seconds. + */ + public static int getNextTimestampOfThisHourInNextWeek(int hour, String timeZone, int param) { + ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); + for (int i = 0; i < param; i++) { + if (zonedDateTime.getDayOfWeek() == DayOfWeek.MONDAY && zonedDateTime.getHour() < hour) { + zonedDateTime = + ZonedDateTime.now(ZoneId.of(timeZone)).withHour(hour).withMinute(0).withSecond(0); + } else { + zonedDateTime = + zonedDateTime + .with(TemporalAdjusters.next(DayOfWeek.MONDAY)) + .withHour(hour) + .withMinute(0) + .withSecond(0); + } + } + return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + } + + /** + * Gets the timestamp of the next hour in a month. + * + * @return The timestamp in UNIX seconds. + */ + public static int getNextTimestampOfThisHourInNextMonth(int hour, String timeZone, int param) { + ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); + for (int i = 0; i < param; i++) { + if (zonedDateTime.getDayOfMonth() == 1 && zonedDateTime.getHour() < hour) { + zonedDateTime = + ZonedDateTime.now(ZoneId.of(timeZone)).withHour(hour).withMinute(0).withSecond(0); + } else { + zonedDateTime = + zonedDateTime + .with(TemporalAdjusters.firstDayOfNextMonth()) + .withHour(hour) + .withMinute(0) + .withSecond(0); + } + } + return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + } + + /** + * Retrieves a string from an input stream. + * + * @param stream The input stream. + * @return The string. + */ + 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) { + stringBuilder.append(line); + } + stream.close(); + } catch (IOException e) { + Grasscutter.getLogger().warn("Failed to read from input stream."); + } catch (NullPointerException ignored) { + return "empty"; + } + return stringBuilder.toString(); + } + + /** + * Performs a linear interpolation using a table of fixed points to create an effective piecewise + * f(x) = y function. + * + * @param x The x value. + * @param xyArray Array of points in [[x0,y0], ... [xN, yN]] format + * @return f(x) = y + */ + 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 (IndexOutOfBoundsException e) { + Grasscutter.getLogger() + .error("Malformed lerp point array. Must be of form [[x0, y0], ..., [xN, yN]]."); + } + return 0; + } + + /** + * 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); + } + + /*** + * Draws a random element from the given list, following the given probability distribution, if given. + * @param list The list from which to draw the element. + * @param probabilities The probability distribution. This is given as a list of probabilities of the same length it `list`. + * @return A randomly drawn element from the given list. + */ + public static T drawRandomListElement(List list, List probabilities) { + // If we don't have a probability distribution, or the size of the distribution does not match + // the size of the list, we assume uniform distribution. + if (probabilities == null || probabilities.size() <= 1 || probabilities.size() != list.size()) { + int index = ThreadLocalRandom.current().nextInt(0, list.size()); + return list.get(index); + } + + // Otherwise, we roll with the given distribution. + int totalProbabilityMass = probabilities.stream().reduce(Integer::sum).get(); + int roll = ThreadLocalRandom.current().nextInt(1, totalProbabilityMass + 1); + + int currentTotalChance = 0; + for (int i = 0; i < list.size(); i++) { + currentTotalChance += probabilities.get(i); + + if (roll <= currentTotalChance) { + return list.get(i); + } + } + + // Should never happen. + return list.get(0); + } + + /*** + * Draws a random element from the given list, following a uniform probability distribution. + * @param list The list from which to draw the element. + * @return A randomly drawn element from the given list. + */ + public static T drawRandomListElement(List list) { + return drawRandomListElement(list, null); + } + + /*** + * Splits a string by a character, into a list + * @param input The string to split + * @param separator The character to use as the split points + * @return A list of all the substrings + */ + public static List nonRegexSplit(String input, int separator) { + var output = new ArrayList(); + int start = 0; + for (int next = input.indexOf(separator); next > 0; next = input.indexOf(separator, start)) { + output.add(input.substring(start, next)); + start = next + 1; + } + if (start < input.length()) output.add(input.substring(start)); + return output; + } +} diff --git a/src/main/java/emu/grasscutter/utils/WeightedList.java b/src/main/java/emu/grasscutter/utils/WeightedList.java index cfdbaecea..10bb81ad1 100644 --- a/src/main/java/emu/grasscutter/utils/WeightedList.java +++ b/src/main/java/emu/grasscutter/utils/WeightedList.java @@ -1,28 +1,28 @@ -package emu.grasscutter.utils; - -import java.util.NavigableMap; -import java.util.TreeMap; -import java.util.concurrent.ThreadLocalRandom; - -public class WeightedList { - private final NavigableMap map = new TreeMap(); - private double total = 0; - - public WeightedList() {} - - public WeightedList add(double weight, E result) { - if (weight <= 0) return this; - total += weight; - map.put(total, result); - return this; - } - - public E next() { - double value = ThreadLocalRandom.current().nextDouble() * total; - return map.higherEntry(value).getValue(); - } - - public int size() { - return map.size(); - } -} +package emu.grasscutter.utils; + +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.concurrent.ThreadLocalRandom; + +public class WeightedList { + private final NavigableMap map = new TreeMap(); + private double total = 0; + + public WeightedList() {} + + public WeightedList add(double weight, E result) { + if (weight <= 0) return this; + total += weight; + map.put(total, result); + return this; + } + + public E next() { + double value = ThreadLocalRandom.current().nextDouble() * total; + return map.higherEntry(value).getValue(); + } + + public int size() { + return map.size(); + } +} diff --git a/src/test/java/io/grasscutter/GrasscutterTest.java b/src/test/java/io/grasscutter/GrasscutterTest.java index 6f9c581a3..07cdb2c62 100644 --- a/src/test/java/io/grasscutter/GrasscutterTest.java +++ b/src/test/java/io/grasscutter/GrasscutterTest.java @@ -1,60 +1,60 @@ -package io.grasscutter; - -import com.mchange.util.AssertException; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.config.Configuration; -import java.io.IOException; -import lombok.Getter; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -/** Testing entrypoint for {@link Grasscutter}. */ -public final class GrasscutterTest { - @Getter private static final OkHttpClient httpClient = new OkHttpClient(); - - @Getter private static int httpPort = -1; - @Getter private static int gamePort = -1; - - /** - * Creates an HTTP URL. - * - * @param route The route to use. - * @return The URL. - */ - public static String http(String route) { - return "http://127.0.0.1:" + GrasscutterTest.httpPort + "/" + route; - } - - @BeforeAll - public static void main() { - try { - // Start Grasscutter. - Grasscutter.main(new String[] {"-test"}); - } catch (Exception ignored) { - throw new AssertException("Grasscutter failed to start."); - } - - // Set the ports. - GrasscutterTest.httpPort = Configuration.SERVER.http.bindPort; - GrasscutterTest.gamePort = Configuration.SERVER.game.bindPort; - } - - @Test - @DisplayName("HTTP server check") - public void checkHttpServer() { - // Create a request. - var request = new Request.Builder().url(GrasscutterTest.http("")).build(); - - // Perform the request. - try (var response = GrasscutterTest.httpClient.newCall(request).execute()) { - // Check the response. - Assertions.assertTrue(response.isSuccessful()); - } catch (IOException exception) { - throw new AssertionError(exception); - } - } -} +package io.grasscutter; + +import com.mchange.util.AssertException; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.config.Configuration; +import java.io.IOException; +import lombok.Getter; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Testing entrypoint for {@link Grasscutter}. */ +public final class GrasscutterTest { + @Getter private static final OkHttpClient httpClient = new OkHttpClient(); + + @Getter private static int httpPort = -1; + @Getter private static int gamePort = -1; + + /** + * Creates an HTTP URL. + * + * @param route The route to use. + * @return The URL. + */ + public static String http(String route) { + return "http://127.0.0.1:" + GrasscutterTest.httpPort + "/" + route; + } + + @BeforeAll + public static void main() { + try { + // Start Grasscutter. + Grasscutter.main(new String[] {"-test"}); + } catch (Exception ignored) { + throw new AssertException("Grasscutter failed to start."); + } + + // Set the ports. + GrasscutterTest.httpPort = Configuration.SERVER.http.bindPort; + GrasscutterTest.gamePort = Configuration.SERVER.game.bindPort; + } + + @Test + @DisplayName("HTTP server check") + public void checkHttpServer() { + // Create a request. + var request = new Request.Builder().url(GrasscutterTest.http("")).build(); + + // Perform the request. + try (var response = GrasscutterTest.httpClient.newCall(request).execute()) { + // Check the response. + Assertions.assertTrue(response.isSuccessful()); + } catch (IOException exception) { + throw new AssertionError(exception); + } + } +}